fix(superset): заменена нечитаемая гео-карта

- Зачем:
  - гео-блок дашборда должен показывать понятную метрику, единицы и сравнение стран.
- Что:
  - legacy world_map заменён на столбцы Top Countries by Events с tooltip и легендой.
  - синхронизирован экспорт дашборда и добавлены контрактные тесты.
  - обновлены документы и урок Superset по новому гео-блоку.
- Проверка:
  - uv run --with pytest pytest tests/test_superset_dashboard_config.py.
  - uv run python -m py_compile superset/create_dashboard.py tests/test_superset_dashboard_config.py.
  - jq empty superset/dashboards/ecommerce_analytics.zip.json.
This commit is contained in:
2026-07-04 22:05:56 +03:00
parent 0f435d70fd
commit 0c80e2438e
5 changed files with 389 additions and 78 deletions
+12 -1
View File
@@ -67,7 +67,18 @@ KPI разложены в одну строку по 12-колоночной с
- **📱 Traffic by Device** — pie chart распределения по устройствам - **📱 Traffic by Device** — pie chart распределения по устройствам
#### География #### География
- **🌍 Geography Map** — world map с распределением по странам - **🌍 Top Countries by Events** — top-15 стран по количеству событий
(`COUNT(*)`, единицы — события, штуки). Столбцы заменили legacy world map:
на текущем разреженном распределении так видны страна, значение, порядок и
tooltip. Перекос стран приходит из гео-фактуры статического сида
`geo_by_click_id`; своя генерация гео описана как отдельный будущий шаг в
ADR-0006 и не лечится настройкой чарта.
> **Что проверили по Superset.** Через MCP Context7 проверили `/apache/superset`:
> legacy world map описан как отдельный legacy-плагин, а ECharts bar chart имеет
> штатные параметры `show_legend`, `rich_tooltip`, подписи осей и формат чисел.
> Поэтому для разреженной географии выбран top-N bar chart
> (`viz_type: echarts_timeseries_bar`), а не донастройка `world_map`.
#### Маркетинг #### Маркетинг
- **🔗 UTM Effectiveness Table** — таблица эффективности UTM-меток - **🔗 UTM Effectiveness Table** — таблица эффективности UTM-меток
+1 -1
View File
@@ -119,7 +119,7 @@ http://localhost:8088/superset/dashboard/ecommerce-analytics/
- KPI сверху: `Total Events`, `Unique Users`, `Avg Events/Visit`, - KPI сверху: `Total Events`, `Unique Users`, `Avg Events/Visit`,
`Conversion to /confirmation`; `Conversion to /confirmation`;
- динамика: `Events over Time`, `Traffic by Device`; - динамика: `Events over Time`, `Traffic by Device`;
- география: `Geography Map`; - география: `Top Countries by Events`;
- маркетинг: `UTM Effectiveness Table`, `Page Funnel`; - маркетинг: `UTM Effectiveness Table`, `Page Funnel`;
- прохождение строк по слоям: `Rows by Layer (event)`. - прохождение строк по слоям: `Rows by Layer (event)`.
+58 -11
View File
@@ -147,18 +147,40 @@ CHARTS_CONFIG = [
}, },
# География # География
{ {
"slice_name": "🌍 Geography Map", "slice_name": "🌍 Top Countries by Events",
"viz_type": "world_map", "previous_slice_names": ["🌍 Geography Map"],
# Legacy world_map показывает разреженную географию плохо: нет явной
# легенды, подписи единиц и стабильного tooltip. Для текущего сида
# читаемее top-N стран столбцами: сразу видны страна, значение и порядок.
# Перекос стран — свойство geo-фактуры из сида, а не настройка чарта.
"viz_type": "echarts_timeseries_bar",
"dataset_name": "v_events_enriched", "dataset_name": "v_events_enriched",
"params": { "params": {
"entity": "geo_country", "x_axis": "geo_country",
"metric": { "metrics": [
{
"expressionType": "SQL", "expressionType": "SQL",
"sqlExpression": "COUNT(*)", "sqlExpression": "COUNT(*)",
"label": "Events" "label": "Events, pcs",
}, }
"row_limit": 500, ],
"linear_color_scheme": "blue_white_yellow", "row_limit": 15,
"order_desc": True,
"sort_series_type": "sum",
"orientation": "vertical",
"color_scheme": "supersetColors",
"show_legend": True,
"legendOrientation": "top",
"legendType": "scroll",
"rich_tooltip": True,
"tooltipTimeFormat": "smart_date",
"x_axis_title": "Country",
"x_axis_title_margin": 15,
"truncateXAxis": True,
"y_axis_title": "Events, pcs",
"y_axis_title_margin": 15,
"y_axis_title_position": "Left",
"y_axis_format": ",d",
"time_range": "No filter" "time_range": "No filter"
} }
}, },
@@ -290,7 +312,7 @@ DASHBOARD_ROWS = [
# Динамика во времени + разрез по устройствам # Динамика во времени + разрез по устройствам
[("📅 Events over Time", 8), ("📱 Traffic by Device", 4)], [("📅 Events over Time", 8), ("📱 Traffic by Device", 4)],
# География + эффективность маркетинговых каналов # География + эффективность маркетинговых каналов
[("🌍 Geography Map", 6), ("🔗 UTM Effectiveness Table", 6)], [("🌍 Top Countries by Events", 6), ("🔗 UTM Effectiveness Table", 6)],
# Популярные страницы + прохождение строк по слоям # Популярные страницы + прохождение строк по слоям
[("🪜 Page Funnel", 6), ("🧱 Rows by Layer (event)", 6)], [("🪜 Page Funnel", 6), ("🧱 Rows by Layer (event)", 6)],
] ]
@@ -360,6 +382,19 @@ def sync_query_context(chart, params: dict, dataset_id: int) -> None:
chart.query_context = json.dumps(query_context) chart.query_context = json.dumps(query_context)
def choose_chart_to_sync(existing_charts: list, current_name: str):
"""Выбирает один chart для синхронизации и отдаёт лишние дубли на удаление."""
if not existing_charts:
return None, []
selected = next(
(chart for chart in existing_charts if chart.slice_name == current_name),
existing_charts[0],
)
duplicates = [chart for chart in existing_charts if chart is not selected]
return selected, duplicates
def build_dashboard_metadata(filter_dataset_id: int | None) -> str: def build_dashboard_metadata(filter_dataset_id: int | None) -> str:
"""Формирует json_metadata с валидными datasetId для native filters.""" """Формирует json_metadata с валидными datasetId для native filters."""
native_filters = [] native_filters = []
@@ -467,11 +502,23 @@ def main() -> bool:
# идемпотентного rename без дублей в списке Charts. # идемпотентного rename без дублей в списке Charts.
chart_names = [chart_config["slice_name"]] chart_names = [chart_config["slice_name"]]
chart_names.extend(chart_config.get("previous_slice_names", [])) chart_names.extend(chart_config.get("previous_slice_names", []))
existing = db.session.query(Slice).filter( existing_charts = db.session.query(Slice).filter(
Slice.slice_name.in_(chart_names) Slice.slice_name.in_(chart_names)
).order_by(Slice.id.asc()).first() ).order_by(Slice.id.asc()).all()
existing, duplicate_charts = choose_chart_to_sync(
existing_charts,
chart_config["slice_name"],
)
if existing: if existing:
for duplicate in duplicate_charts:
db.session.delete(duplicate)
logger.info(
"Deleted duplicate chart after rename: %s (ID: %s)",
duplicate.slice_name,
duplicate.id,
)
# Синхронизируем параметры существующего чарта с конфигом. # Синхронизируем параметры существующего чарта с конфигом.
existing.slice_name = chart_config["slice_name"] existing.slice_name = chart_config["slice_name"]
existing.viz_type = chart_config["viz_type"] existing.viz_type = chart_config["viz_type"]
+186 -63
View File
@@ -3,12 +3,23 @@
{ {
"__Dashboard__": { "__Dashboard__": {
"dashboard_title": "🛒 E-commerce Analytics Dashboard", "dashboard_title": "🛒 E-commerce Analytics Dashboard",
"description": "Аналитический дашборд для e-commerce кликстрима: трафик, конверсии, география и качество данных.", "description": "Аналитический дашборд для e-commerce кликстрима: трафик, конверсии, география и прохождение строк по слоям.",
"slug": "ecommerce-analytics", "slug": "ecommerce-analytics",
"published": true, "published": true,
"json_metadata": "{\"native_filter_configuration\": [{\"id\": \"date_filter\", \"name\": \"📅 Date Range\", \"filterType\": \"filter_time\", \"targets\": [{\"datasetId\": null, \"column\": {\"name\": \"event_date\"}}], \"defaultValue\": \"Last week\", \"scope\": {\"root\": [\"ROOT_ID\"], \"excluded\": []}, \"cascadeParentIds\": [], \"isInstant\": true}, {\"id\": \"country_filter\", \"name\": \"🌍 Country\", \"filterType\": \"filter_select\", \"targets\": [{\"datasetId\": null, \"column\": {\"name\": \"geo_country\"}}], \"scope\": {\"root\": [\"ROOT_ID\"], \"excluded\": []}, \"isInstant\": true, \"allowsMultipleValues\": true, \"isRequired\": false}, {\"id\": \"device_filter\", \"name\": \"📱 Device Type\", \"filterType\": \"filter_select\", \"targets\": [{\"datasetId\": null, \"column\": {\"name\": \"device_type\"}}], \"scope\": {\"root\": [\"ROOT_ID\"], \"excluded\": []}, \"isInstant\": true, \"allowsMultipleValues\": true, \"isRequired\": false}, {\"id\": \"browser_filter\", \"name\": \"🌐 Browser\", \"filterType\": \"filter_select\", \"targets\": [{\"datasetId\": null, \"column\": {\"name\": \"browser_name\"}}], \"scope\": {\"root\": [\"ROOT_ID\"], \"excluded\": []}, \"isInstant\": true, \"allowsMultipleValues\": true, \"isRequired\": false}], \"color_scheme\": \"supersetColors\", \"label_colors\": {}}", "json_metadata": "{\"native_filter_configuration\": [{\"id\": \"date_filter\", \"name\": \"📅 Date Range\", \"filterType\": \"filter_time\", \"targets\": [{\"datasetId\": null, \"column\": {\"name\": \"event_date\"}}], \"defaultValue\": \"Last week\", \"scope\": {\"root\": [\"ROOT_ID\"], \"excluded\": []}, \"cascadeParentIds\": [], \"isInstant\": true}, {\"id\": \"country_filter\", \"name\": \"🌍 Country\", \"filterType\": \"filter_select\", \"targets\": [{\"datasetId\": null, \"column\": {\"name\": \"geo_country\"}}], \"scope\": {\"root\": [\"ROOT_ID\"], \"excluded\": []}, \"isInstant\": true, \"allowsMultipleValues\": true, \"isRequired\": false}, {\"id\": \"device_filter\", \"name\": \"📱 Device Type\", \"filterType\": \"filter_select\", \"targets\": [{\"datasetId\": null, \"column\": {\"name\": \"device_type\"}}], \"scope\": {\"root\": [\"ROOT_ID\"], \"excluded\": []}, \"isInstant\": true, \"allowsMultipleValues\": true, \"isRequired\": false}, {\"id\": \"browser_filter\", \"name\": \"🌐 Browser\", \"filterType\": \"filter_select\", \"targets\": [{\"datasetId\": null, \"column\": {\"name\": \"browser_name\"}}], \"scope\": {\"root\": [\"ROOT_ID\"], \"excluded\": []}, \"isInstant\": true, \"allowsMultipleValues\": true, \"isRequired\": false}], \"color_scheme\": \"supersetColors\", \"label_colors\": {}}",
"position_json": "{\"DASHBOARD_VERSION_KEY\": \"v2\", \"CHART-1\": {\"id\": \"CHART-1\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 1, \"sliceName\": \"📊 Total Events\", \"height\": 50, \"width\": 4, \"x\": 0, \"y\": 0}}, \"CHART-2\": {\"id\": \"CHART-2\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 2, \"sliceName\": \"👤 Unique Users\", \"height\": 50, \"width\": 4, \"x\": 4, \"y\": 0}}, \"CHART-3\": {\"id\": \"CHART-3\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 3, \"sliceName\": \"🎯 Unique Sessions\", \"height\": 50, \"width\": 4, \"x\": 8, \"y\": 0}}, \"CHART-4\": {\"id\": \"CHART-4\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 4, \"sliceName\": \"📈 Avg Events/Session\", \"height\": 50, \"width\": 4, \"x\": 0, \"y\": 50}}, \"CHART-5\": {\"id\": \"CHART-5\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 5, \"sliceName\": \"📅 Events by Hour\", \"height\": 50, \"width\": 8, \"x\": 0, \"y\": 100}}, \"CHART-6\": {\"id\": \"CHART-6\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 6, \"sliceName\": \"📱 Traffic by Device\", \"height\": 50, \"width\": 4, \"x\": 8, \"y\": 100}}, \"CHART-7\": {\"id\": \"CHART-7\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 7, \"sliceName\": \"🌍 Geography Map\", \"height\": 50, \"width\": 6, \"x\": 0, \"y\": 150}}, \"CHART-8\": {\"id\": \"CHART-8\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 8, \"sliceName\": \"🔗 UTM Effectiveness Table\", \"height\": 50, \"width\": 6, \"x\": 6, \"y\": 150}}, \"CHART-9\": {\"id\": \"CHART-9\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 9, \"sliceName\": \"📄 Top Pages\", \"height\": 50, \"width\": 6, \"x\": 0, \"y\": 200}}, \"CHART-10\": {\"id\": \"CHART-10\", \"type\": \"CHART\", \"parents\": [\"ROOT_ID\"], \"meta\": {\"chartId\": 10, \"sliceName\": \"🔍 Data Quality Summary\", \"height\": 50, \"width\": 6, \"x\": 6, \"y\": 200}}}", "position_json": "{\"DASHBOARD_VERSION_KEY\": \"v2\", \"ROOT_ID\": {\"id\": \"ROOT_ID\", \"type\": \"ROOT\", \"children\": [\"GRID_ID\"]}, \"GRID_ID\": {\"id\": \"GRID_ID\", \"type\": \"GRID\", \"children\": [\"ROW-0\", \"ROW-1\", \"ROW-2\", \"ROW-3\"], \"parents\": [\"ROOT_ID\"], \"meta\": {\"background\": \"BACKGROUND_TRANSPARENT\"}}, \"CHART-1\": {\"id\": \"CHART-1\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-0\"], \"meta\": {\"chartId\": 1, \"sliceName\": \"📊 Total Events\", \"width\": 3, \"height\": 30}}, \"CHART-2\": {\"id\": \"CHART-2\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-0\"], \"meta\": {\"chartId\": 2, \"sliceName\": \"👤 Unique Users\", \"width\": 3, \"height\": 30}}, \"CHART-3\": {\"id\": \"CHART-3\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-0\"], \"meta\": {\"chartId\": 3, \"sliceName\": \"📈 Avg Events/Visit\", \"width\": 3, \"height\": 30}}, \"CHART-4\": {\"id\": \"CHART-4\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-0\"], \"meta\": {\"chartId\": 4, \"sliceName\": \"🎯 Conversion to /confirmation\", \"width\": 3, \"height\": 30}}, \"ROW-0\": {\"id\": \"ROW-0\", \"type\": \"ROW\", \"children\": [\"CHART-1\", \"CHART-2\", \"CHART-3\", \"CHART-4\"], \"parents\": [\"ROOT_ID\", \"GRID_ID\"], \"meta\": {\"background\": \"BACKGROUND_TRANSPARENT\"}}, \"CHART-5\": {\"id\": \"CHART-5\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-1\"], \"meta\": {\"chartId\": 5, \"sliceName\": \"📅 Events over Time\", \"width\": 8, \"height\": 60}}, \"CHART-6\": {\"id\": \"CHART-6\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-1\"], \"meta\": {\"chartId\": 6, \"sliceName\": \"📱 Traffic by Device\", \"width\": 4, \"height\": 60}}, \"ROW-1\": {\"id\": \"ROW-1\", \"type\": \"ROW\", \"children\": [\"CHART-5\", \"CHART-6\"], \"parents\": [\"ROOT_ID\", \"GRID_ID\"], \"meta\": {\"background\": \"BACKGROUND_TRANSPARENT\"}}, \"CHART-7\": {\"id\": \"CHART-7\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-2\"], \"meta\": {\"chartId\": 7, \"sliceName\": \"🌍 Top Countries by Events\", \"width\": 6, \"height\": 60}}, \"CHART-8\": {\"id\": \"CHART-8\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-2\"], \"meta\": {\"chartId\": 8, \"sliceName\": \"🔗 UTM Effectiveness Table\", \"width\": 6, \"height\": 60}}, \"ROW-2\": {\"id\": \"ROW-2\", \"type\": \"ROW\", \"children\": [\"CHART-7\", \"CHART-8\"], \"parents\": [\"ROOT_ID\", \"GRID_ID\"], \"meta\": {\"background\": \"BACKGROUND_TRANSPARENT\"}}, \"CHART-9\": {\"id\": \"CHART-9\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-3\"], \"meta\": {\"chartId\": 9, \"sliceName\": \"🪜 Page Funnel\", \"width\": 6, \"height\": 60}}, \"CHART-10\": {\"id\": \"CHART-10\", \"type\": \"CHART\", \"children\": [], \"parents\": [\"ROOT_ID\", \"GRID_ID\", \"ROW-3\"], \"meta\": {\"chartId\": 10, \"sliceName\": \"🧱 Rows by Layer (event)\", \"width\": 6, \"height\": 60}}, \"ROW-3\": {\"id\": \"ROW-3\", \"type\": \"ROW\", \"children\": [\"CHART-9\", \"CHART-10\"], \"parents\": [\"ROOT_ID\", \"GRID_ID\"], \"meta\": {\"background\": \"BACKGROUND_TRANSPARENT\"}}}",
"slices": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] "slices": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
]
} }
} }
], ],
@@ -16,51 +27,51 @@
{ {
"__Slice__": { "__Slice__": {
"slice_name": "📊 Total Events", "slice_name": "📊 Total Events",
"viz_type": "big_number", "viz_type": "big_number_total",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_events_enriched", "datasource_name": "dm.v_events_enriched",
"params": "{\"datasource\": \"1__table\", \"viz_type\": \"big_number\", \"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*)\", \"column\": null, \"aggregate\": null, \"label\": \"Total Events\", \"optionName\": \"metric_1\"}, \"y_axis_format\": \",d\", \"show_trend_line\": false, \"time_range\": \"No filter\"}", "params": "{\"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*)\", \"column\": null, \"aggregate\": null, \"label\": \"Total Events\", \"optionName\": \"metric_1\"}, \"y_axis_format\": \",d\", \"time_range\": \"No filter\", \"datasource\": \"1__table\", \"viz_type\": \"big_number_total\"}",
"description": "Общее количество событий" "description": "Chart created automatically for v_events_enriched"
} }
}, },
{ {
"__Slice__": { "__Slice__": {
"slice_name": "👤 Unique Users", "slice_name": "👤 Unique Users",
"viz_type": "big_number", "viz_type": "big_number_total",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_events_enriched", "datasource_name": "dm.v_events_enriched",
"params": "{\"datasource\": \"1__table\", \"viz_type\": \"big_number\", \"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(DISTINCT user_domain_id)\", \"label\": \"Unique Users\", \"optionName\": \"metric_2\"}, \"y_axis_format\": \",d\", \"show_trend_line\": false, \"time_range\": \"No filter\"}", "params": "{\"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(DISTINCT user_domain_id)\", \"label\": \"Unique Users\", \"optionName\": \"metric_2\"}, \"y_axis_format\": \",d\", \"time_range\": \"No filter\", \"datasource\": \"1__table\", \"viz_type\": \"big_number_total\"}",
"description": "Уникальные пользователи" "description": "Chart created automatically for v_events_enriched"
} }
}, },
{ {
"__Slice__": { "__Slice__": {
"slice_name": "🎯 Unique Sessions", "slice_name": "📈 Avg Events/Visit",
"viz_type": "big_number", "viz_type": "big_number_total",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_events_enriched", "datasource_name": "dm.v_events_enriched",
"params": "{\"datasource\": \"1__table\", \"viz_type\": \"big_number\", \"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(DISTINCT click_id)\", \"label\": \"Unique Sessions\", \"optionName\": \"metric_3\"}, \"y_axis_format\": \",d\", \"show_trend_line\": false, \"time_range\": \"No filter\"}", "params": "{\"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*) / COUNT(DISTINCT click_id)\", \"label\": \"Avg Events/Visit\", \"optionName\": \"metric_4\"}, \"y_axis_format\": \".1f\", \"time_range\": \"No filter\", \"datasource\": \"1__table\", \"viz_type\": \"big_number_total\"}",
"description": "Уникальные сессии" "description": "Chart created automatically for v_events_enriched"
} }
}, },
{ {
"__Slice__": { "__Slice__": {
"slice_name": "📈 Avg Events/Session", "slice_name": "🎯 Conversion to /confirmation",
"viz_type": "big_number", "viz_type": "big_number_total",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_events_enriched", "datasource_name": "dm.v_events_enriched",
"params": "{\"datasource\": \"1__table\", \"viz_type\": \"big_number\", \"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*) / COUNT(DISTINCT click_id)\", \"label\": \"Avg Events/Session\", \"optionName\": \"metric_4\"}, \"y_axis_format\": \".2f\", \"show_trend_line\": false, \"time_range\": \"No filter\"}", "params": "{\"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"if(countIf(page_url_path = '/home') = 0, 0, countIf(page_url_path = '/confirmation') / countIf(page_url_path = '/home'))\", \"label\": \"Conversion to /confirmation\", \"optionName\": \"metric_5\"}, \"y_axis_format\": \".1%\", \"time_range\": \"No filter\", \"datasource\": \"1__table\", \"viz_type\": \"big_number_total\"}",
"description": "Среднее количество событий на сессию" "description": "Chart created automatically for v_events_enriched"
} }
}, },
{ {
"__Slice__": { "__Slice__": {
"slice_name": "📅 Events by Hour", "slice_name": "📅 Events over Time",
"viz_type": "echarts_timeseries_line", "viz_type": "echarts_timeseries_line",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_events_enriched", "datasource_name": "dm.v_events_enriched",
"params": "{\"datasource\": \"1__table\", \"viz_type\": \"echarts_timeseries_line\", \"granularity_sqla\": \"event_ts\", \"time_grain_sqla\": \"PT1H\", \"metrics\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*)\", \"label\": \"Events\"}], \"groupby\": [], \"time_range\": \"Last week\", \"adhoc_filters\": [], \"row_limit\": 10000}", "params": "{\"granularity_sqla\": \"event_ts\", \"time_grain_sqla\": \"PT5M\", \"metrics\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*)\", \"label\": \"Events\"}], \"groupby\": [], \"time_range\": \"No filter\", \"adhoc_filters\": [], \"row_limit\": 10000, \"datasource\": \"1__table\", \"viz_type\": \"echarts_timeseries_line\"}",
"description": "События по часам" "description": "Chart created automatically for v_events_enriched"
} }
}, },
{ {
@@ -69,18 +80,18 @@
"viz_type": "pie", "viz_type": "pie",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_events_enriched", "datasource_name": "dm.v_events_enriched",
"params": "{\"datasource\": \"1__table\", \"viz_type\": \"pie\", \"groupby\": [\"device_type\"], \"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*)\", \"label\": \"Count\"}, \"row_limit\": 100, \"donut\": true, \"show_legend\": true, \"labels_outside\": true, \"time_range\": \"No filter\"}", "params": "{\"groupby\": [\"device_type\"], \"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*)\", \"label\": \"Count\"}, \"row_limit\": 100, \"donut\": true, \"show_legend\": true, \"labels_outside\": true, \"time_range\": \"No filter\", \"datasource\": \"1__table\", \"viz_type\": \"pie\"}",
"description": "Распределение трафика по устройствам" "description": "Chart created automatically for v_events_enriched"
} }
}, },
{ {
"__Slice__": { "__Slice__": {
"slice_name": "🌍 Geography Map", "slice_name": "🌍 Top Countries by Events",
"viz_type": "world_map", "viz_type": "echarts_timeseries_bar",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_events_enriched", "datasource_name": "dm.v_events_enriched",
"params": "{\"datasource\": \"1__table\", \"viz_type\": \"world_map\", \"entity\": \"geo_country\", \"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*)\", \"label\": \"Events\"}, \"row_limit\": 500, \"linear_color_scheme\": \"blue_white_yellow\", \"time_range\": \"No filter\"}", "params": "{\"x_axis\": \"geo_country\", \"metrics\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"COUNT(*)\", \"label\": \"Events, pcs\"}], \"row_limit\": 15, \"order_desc\": true, \"sort_series_type\": \"sum\", \"orientation\": \"vertical\", \"color_scheme\": \"supersetColors\", \"show_legend\": true, \"legendOrientation\": \"top\", \"legendType\": \"scroll\", \"rich_tooltip\": true, \"tooltipTimeFormat\": \"smart_date\", \"x_axis_title\": \"Country\", \"x_axis_title_margin\": 15, \"truncateXAxis\": true, \"y_axis_title\": \"Events, pcs\", \"y_axis_title_margin\": 15, \"y_axis_title_position\": \"Left\", \"y_axis_format\": \",d\", \"time_range\": \"No filter\", \"datasource\": \"1__table\", \"viz_type\": \"echarts_timeseries_bar\"}",
"description": "География посетителей" "description": "Chart created automatically for v_events_enriched"
} }
}, },
{ {
@@ -89,28 +100,28 @@
"viz_type": "table", "viz_type": "table",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_utm_effectiveness", "datasource_name": "dm.v_utm_effectiveness",
"params": "{\"datasource\": \"2__table\", \"viz_type\": \"table\", \"groupby\": [\"utm_source\", \"utm_medium\", \"utm_campaign\"], \"metrics\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(clicks)\", \"label\": \"Clicks\"}, {\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(uniq_users)\", \"label\": \"Users\"}, {\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(uniq_sessions)\", \"label\": \"Sessions\"}], \"row_limit\": 100, \"time_range\": \"No filter\", \"adhoc_filters\": [{\"clause\": \"WHERE\", \"expressionType\": \"SQL\", \"sqlExpression\": \"utm_source IS NOT NULL\", \"subject\": null, \"operator\": null, \"comparator\": null}]}\n", "params": "{\"groupby\": [\"utm_source\", \"utm_medium\", \"utm_campaign\"], \"metrics\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(clicks)\", \"label\": \"Clicks\"}, {\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(uniq_users)\", \"label\": \"Users\"}, {\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(uniq_sessions)\", \"label\": \"Sessions\"}], \"row_limit\": 100, \"time_range\": \"No filter\", \"adhoc_filters\": [{\"clause\": \"WHERE\", \"expressionType\": \"SQL\", \"sqlExpression\": \"utm_source IS NOT NULL\", \"subject\": null, \"operator\": null, \"comparator\": null}], \"datasource\": \"2__table\", \"viz_type\": \"table\"}",
"description": "Эффективность UTM-кампаний" "description": "Chart created automatically for v_utm_effectiveness"
} }
}, },
{ {
"__Slice__": { "__Slice__": {
"slice_name": "📄 Top Pages", "slice_name": "🪜 Page Funnel",
"viz_type": "dist_bar", "viz_type": "funnel",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.v_top_pages_daily", "datasource_name": "dm.v_top_pages_daily",
"params": "{\"datasource\": \"3__table\", \"viz_type\": \"dist_bar\", \"groupby\": [\"page_url_path\"], \"metrics\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(pageviews)\", \"label\": \"Pageviews\"}], \"row_limit\": 20, \"order_by_cols\": [[\"SUM(pageviews)\", false]], \"time_range\": \"No filter\", \"orientation\": \"vertical\", \"show_legend\": false}", "params": "{\"groupby\": [\"page_url_path\"], \"metric\": {\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(pageviews)\", \"label\": \"Pageviews\"}, \"row_limit\": 20, \"time_range\": \"No filter\", \"sort_by_metric\": true, \"percent_calculation_type\": \"first_step\", \"color_scheme\": \"supersetColors\", \"show_legend\": true, \"legendOrientation\": \"top\", \"legendMargin\": 50, \"tooltip_label_type\": 5, \"number_format\": \"SMART_NUMBER\", \"show_labels\": true, \"show_tooltip_labels\": true, \"datasource\": \"3__table\", \"viz_type\": \"funnel\"}",
"description": "Топ страниц по просмотрам" "description": "Chart created automatically for v_top_pages_daily"
} }
}, },
{ {
"__Slice__": { "__Slice__": {
"slice_name": "🔍 Data Quality Summary", "slice_name": "🧱 Rows by Layer (event)",
"viz_type": "dist_bar", "viz_type": "dist_bar",
"datasource_type": "table", "datasource_type": "table",
"datasource_name": "dm.dq_summary", "datasource_name": "dm.dq_summary",
"params": "{\"datasource\": \"4__table\", \"viz_type\": \"dist_bar\", \"groupby\": [\"layer\"], \"metrics\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(check_value)\", \"label\": \"Row Count\"}], \"adhoc_filters\": [{\"clause\": \"WHERE\", \"expressionType\": \"SQL\", \"sqlExpression\": \"check_name = 'total_rows'\", \"subject\": null, \"operator\": null, \"comparator\": null}], \"row_limit\": 100, \"time_range\": \"No filter\", \"show_legend\": false}", "params": "{\"groupby\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"multiIf(layer = 'stg', '1 · stg', layer = 'ods', '2 · ods', layer = 'dds', '3 · dds', '4 · dm')\", \"label\": \"Layer\"}], \"metrics\": [{\"expressionType\": \"SQL\", \"sqlExpression\": \"SUM(check_value)\", \"label\": \"Rows\"}], \"adhoc_filters\": [{\"clause\": \"WHERE\", \"expressionType\": \"SQL\", \"sqlExpression\": \"check_name = 'total_rows' AND table_name IN ('browser_raw', 'browser_event', 'event', 'v_events_enriched')\", \"subject\": null, \"operator\": null, \"comparator\": null}], \"order_bars\": true, \"row_limit\": 100, \"time_range\": \"No filter\", \"y_axis_format\": \",d\", \"show_legend\": false, \"datasource\": \"4__table\", \"viz_type\": \"dist_bar\"}",
"description": "Сводка по качеству данных" "description": "Chart created automatically for dq_summary"
} }
} }
], ],
@@ -122,18 +133,66 @@
"database": "clickhouse_dwh", "database": "clickhouse_dwh",
"description": "Полная обогащённая витрина событий (event + click)", "description": "Полная обогащённая витрина событий (event + click)",
"columns": [ "columns": [
{"column_name": "event_id", "type": "UUID", "description": "UUID события"}, {
{"column_name": "event_ts", "type": "DateTime64(6)", "description": "Время события"}, "column_name": "event_id",
{"column_name": "event_date", "type": "Date", "description": "Дата события"}, "type": "UUID",
{"column_name": "event_type", "type": "String", "description": "Тип события"}, "description": "UUID события"
{"column_name": "click_id", "type": "UUID", "description": "ID сессии"}, },
{"column_name": "user_domain_id", "type": "UUID", "description": "ID пользователя"}, {
{"column_name": "device_type", "type": "String", "description": "Тип устройства"}, "column_name": "event_ts",
{"column_name": "geo_country", "type": "String", "description": "Страна"}, "type": "DateTime64(6)",
{"column_name": "browser_name", "type": "String", "description": "Браузер"}, "description": "Время события"
{"column_name": "utm_source", "type": "String", "description": "UTM Source"}, },
{"column_name": "utm_medium", "type": "String", "description": "UTM Medium"}, {
{"column_name": "page_url_path", "type": "String", "description": "Путь URL"} "column_name": "event_date",
"type": "Date",
"description": "Дата события"
},
{
"column_name": "event_type",
"type": "String",
"description": "Тип события"
},
{
"column_name": "click_id",
"type": "UUID",
"description": "ID сессии"
},
{
"column_name": "user_domain_id",
"type": "UUID",
"description": "ID пользователя"
},
{
"column_name": "device_type",
"type": "String",
"description": "Тип устройства"
},
{
"column_name": "geo_country",
"type": "String",
"description": "Страна"
},
{
"column_name": "browser_name",
"type": "String",
"description": "Браузер"
},
{
"column_name": "utm_source",
"type": "String",
"description": "UTM Source"
},
{
"column_name": "utm_medium",
"type": "String",
"description": "UTM Medium"
},
{
"column_name": "page_url_path",
"type": "String",
"description": "Путь URL"
}
] ]
} }
}, },
@@ -144,13 +203,41 @@
"database": "clickhouse_dwh", "database": "clickhouse_dwh",
"description": "Эффективность UTM-кампаний", "description": "Эффективность UTM-кампаний",
"columns": [ "columns": [
{"column_name": "event_date", "type": "Date", "description": "Дата"}, {
{"column_name": "utm_source", "type": "String", "description": "UTM Source"}, "column_name": "event_date",
{"column_name": "utm_medium", "type": "String", "description": "UTM Medium"}, "type": "Date",
{"column_name": "utm_campaign", "type": "String", "description": "UTM Campaign"}, "description": "Дата"
{"column_name": "clicks", "type": "UInt64", "description": "Клики"}, },
{"column_name": "uniq_users", "type": "UInt64", "description": "Уникальные пользователи"}, {
{"column_name": "uniq_sessions", "type": "UInt64", "description": "Уникальные сессии"} "column_name": "utm_source",
"type": "String",
"description": "UTM Source"
},
{
"column_name": "utm_medium",
"type": "String",
"description": "UTM Medium"
},
{
"column_name": "utm_campaign",
"type": "String",
"description": "UTM Campaign"
},
{
"column_name": "clicks",
"type": "UInt64",
"description": "Клики"
},
{
"column_name": "uniq_users",
"type": "UInt64",
"description": "Уникальные пользователи"
},
{
"column_name": "uniq_sessions",
"type": "UInt64",
"description": "Уникальные сессии"
}
] ]
} }
}, },
@@ -161,10 +248,26 @@
"database": "clickhouse_dwh", "database": "clickhouse_dwh",
"description": "Популярность страниц по дням", "description": "Популярность страниц по дням",
"columns": [ "columns": [
{"column_name": "event_date", "type": "Date", "description": "Дата"}, {
{"column_name": "page_url_path", "type": "String", "description": "Путь URL"}, "column_name": "event_date",
{"column_name": "pageviews", "type": "UInt64", "description": "Просмотры"}, "type": "Date",
{"column_name": "uniq_clicks", "type": "UInt64", "description": "Уникальные клики"} "description": "Дата"
},
{
"column_name": "page_url_path",
"type": "String",
"description": "Путь URL"
},
{
"column_name": "pageviews",
"type": "UInt64",
"description": "Просмотры"
},
{
"column_name": "uniq_clicks",
"type": "UInt64",
"description": "Уникальные клики"
}
] ]
} }
}, },
@@ -175,11 +278,31 @@
"database": "clickhouse_dwh", "database": "clickhouse_dwh",
"description": "Сводка по качеству данных", "description": "Сводка по качеству данных",
"columns": [ "columns": [
{"column_name": "check_date", "type": "Date", "description": "Дата проверки"}, {
{"column_name": "layer", "type": "String", "description": "Слой (stg/ods/dds)"}, "column_name": "check_date",
{"column_name": "table_name", "type": "String", "description": "Имя таблицы"}, "type": "Date",
{"column_name": "check_name", "type": "String", "description": "Тип проверки"}, "description": "Дата проверки"
{"column_name": "check_value", "type": "UInt64", "description": "Значение"} },
{
"column_name": "layer",
"type": "String",
"description": "Слой (stg/ods/dds)"
},
{
"column_name": "table_name",
"type": "String",
"description": "Имя таблицы"
},
{
"column_name": "check_name",
"type": "String",
"description": "Тип проверки"
},
{
"column_name": "check_value",
"type": "UInt64",
"description": "Значение"
}
] ]
} }
} }
+130
View File
@@ -0,0 +1,130 @@
import importlib.util
import json
from pathlib import Path
def load_dashboard_module():
module_path = Path(__file__).resolve().parents[1] / "superset" / "create_dashboard.py"
spec = importlib.util.spec_from_file_location("create_dashboard", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def chart_config(module, slice_name):
return next(chart for chart in module.CHARTS_CONFIG if chart["slice_name"] == slice_name)
def exported_chart(exported_dashboard, slice_name):
return next(
chart["__Slice__"]
for chart in exported_dashboard["charts"]
if chart["__Slice__"]["slice_name"] == slice_name
)
def load_exported_dashboard():
export_path = Path(__file__).resolve().parents[1] / "superset" / "dashboards" / "ecommerce_analytics.zip.json"
return json.loads(export_path.read_text())
class FakeChart:
def __init__(self, chart_id, slice_name):
self.id = chart_id
self.slice_name = slice_name
def test_geo_chart_uses_readable_top_countries_bar_config():
module = load_dashboard_module()
geo_chart = chart_config(module, "🌍 Top Countries by Events")
assert geo_chart["viz_type"] == "echarts_timeseries_bar"
assert geo_chart["previous_slice_names"] == ["🌍 Geography Map"]
params = geo_chart["params"]
assert params["x_axis"] == "geo_country"
assert params["metrics"] == [
{
"expressionType": "SQL",
"sqlExpression": "COUNT(*)",
"label": "Events, pcs",
}
]
assert params["row_limit"] == 15
assert params["order_desc"] is True
assert params["sort_series_type"] == "sum"
assert params["show_legend"] is True
assert params["rich_tooltip"] is True
assert params["y_axis_title"] == "Events, pcs"
assert params["x_axis_title"] == "Country"
assert params["y_axis_format"] == ",d"
def test_exported_dashboard_uses_same_readable_geo_chart():
exported_dashboard = load_exported_dashboard()
geo_chart = exported_chart(exported_dashboard, "🌍 Top Countries by Events")
assert geo_chart["viz_type"] == "echarts_timeseries_bar"
params = json.loads(geo_chart["params"])
assert params["viz_type"] == "echarts_timeseries_bar"
assert params["x_axis"] == "geo_country"
assert params["metrics"][0]["label"] == "Events, pcs"
assert params["show_legend"] is True
assert params["rich_tooltip"] is True
assert params["y_axis_title"] == "Events, pcs"
position_json = json.loads(exported_dashboard["dashboards"][0]["__Dashboard__"]["position_json"])
assert position_json["CHART-7"]["meta"]["sliceName"] == "🌍 Top Countries by Events"
def test_exported_dashboard_chart_names_match_current_config():
module = load_dashboard_module()
exported_dashboard = load_exported_dashboard()
expected_names = [chart["slice_name"] for chart in module.CHARTS_CONFIG]
actual_names = [chart["__Slice__"]["slice_name"] for chart in exported_dashboard["charts"]]
assert actual_names == expected_names
def test_exported_dashboard_layout_matches_current_rows():
module = load_dashboard_module()
exported_dashboard = load_exported_dashboard()
position_json = json.loads(exported_dashboard["dashboards"][0]["__Dashboard__"]["position_json"])
chart_titles_by_id = {
component_id: component["meta"]["sliceName"]
for component_id, component in position_json.items()
if isinstance(component, dict) and component.get("type") == "CHART"
}
actual_rows = []
for row_id in position_json["GRID_ID"]["children"]:
row = []
for chart_component_id in position_json[row_id]["children"]:
chart_component = position_json[chart_component_id]
row.append(
(
chart_titles_by_id[chart_component_id],
chart_component["meta"]["width"],
)
)
actual_rows.append(row)
assert actual_rows == module.DASHBOARD_ROWS
def test_choose_chart_to_sync_prefers_current_name_and_marks_old_name_duplicate():
module = load_dashboard_module()
old_chart = FakeChart(7, "🌍 Geography Map")
current_chart = FakeChart(11, "🌍 Top Countries by Events")
selected, duplicates = module.choose_chart_to_sync(
[old_chart, current_chart],
"🌍 Top Countries by Events",
)
assert selected is current_chart
assert duplicates == [old_chart]