# 📡 TelecomKz Analytics Mapper & Visual Simulator ### Архитектура, принципы работы и результаты аудита > Ревизия документа: 2026-08-24. Проверено на реальном устройстве > Samsung SM-S938B (Android 16, 1080×2340, WebView Chrome 151), приложение > `kz.telecom.app`, WebView `https://customer.telecom.kz/`. --- ## 📑 Содержание 1. [О проекте](#-1-о-проекте) 2. [Архитектура](#-2-архитектура) 3. [Система координат — главное, что нужно понять](#-3-система-координат--главное-что-нужно-понять) 4. [Достоверность данных](#-4-достоверность-данных) 5. [Функционал](#-5-функционал) 6. [Результаты аудита: что было сломано и как исправлено](#-6-результаты-аудита-что-было-сломано-и-как-исправлено) 7. [Структура проекта](#-7-структура-проекта) 8. [Запуск и типовые сценарии](#-8-запуск-и-типовые-сценарии) --- ## 🎯 1. О проекте Визуальный инструмент для сквозного маппинга UI-элементов мобильного приложения Казахтелеком на события Amplitude и таблицы ClickHouse: наведение на любую кнопку показывает ключ события и продуктовые метрики, а разметка снимается прямо с подключённого по USB смартфона. Приложение гибридное: нативная оболочка (тулбар сверху, таб-бар снизу) плюс Vue.js-WebView в середине. Из этого следует всё остальное — и способ съёмки, и способ получения координат, и способ перехвата событий. --- ## 🏗️ 2. Архитектура ```mermaid flowchart TD subgraph Device ["📱 Android (ADB)"] Native["Нативная оболочка: тулбар + таб-бар"] WV["WebView (Vue, customer.telecom.kz)"] end subgraph Core ["⚙️ tools/telecom_cdp.py — общее ядро"] ADB["ADB: screencap, uiautomator, input"] Sock["Поиск сокета webview_devtools_remote_<pid>"] CDP["CDP-клиент с сопоставлением id ответов"] Geom["probe_native_layout(): реальные границы WebView и таб-бара"] end subgraph Tools ["🐍 Инструменты"] Cap["capture_screen.py — сшивка по смещению прокрутки"] Live["live_auto_recorder.py — пересборка хотспотов"] Mon["monitor_live_events.py — перехват событий"] Crawl["crawl_and_map_all.py — автообход"] CH["fetch_clickhouse.py — метрики"] end subgraph Server ["🚀 Vite dev server"] A1["/api/capture-adb"] A2["/api/live-sync-webview"] A3["/api/save-app-map"] A4["/api/device-status"] end subgraph UI ["💻 React 19 + TypeScript"] Sim["PhoneSimulator"] Tip["HotspotTooltip"] Ed["GuiEditor"] end Native --> ADB WV --> CDP Sock --> CDP ADB --> Geom Core --> Tools Tools --> Server Server --> UI ``` **Стек.** React 19 + TypeScript + Vite; Python 3.10+ (Pillow, OpenCV, websockets); ADB + `uiautomator`; Chrome DevTools Protocol через `adb forward`. Ключевое архитектурное решение аудита: весь доступ к устройству идёт через единственный модуль `tools/telecom_cdp.py`. Раньше каждый скрипт заново реализовывал подключение — и каждый воспроизводил одни и те же три ошибки (см. §6). --- ## 📐 3. Система координат — главное, что нужно понять Все прямоугольники хотспотов живут в **пиксельной сетке сшитого скриншота**: ``` строки 0 .. webviewTop нативный тулбар webviewTop .. +contentHeight полный контент WebView последние navHeight строк нативный таб-бар ``` Элемент DOM со смещением в документе `(left, top + scrollY)` попадает в ``` x = round(left * scale) y = webviewTop + round((top + scrollY) * scale), где scale = ширина экрана / window.innerWidth ``` На проверенном устройстве: `webviewTop = 255`, `webviewBottom = 2139`, `scale = 1080 / 384 = 2.8125`. Но **эти числа измеряются на каждом запуске** через `uiautomator` (границы `android.webkit.WebView` и `kz.telecom.app:id/bottomNavigation`), а не задаются константами. ### Почему сшивка сделана именно так Скриншот снимается покадрово: страница прокручивается командой `window.scrollTo` через CDP, после каждого шага реальное значение `window.scrollY` **читается обратно**, и кадр вставляется на строку `round(scrollY * scale)`. Совпадение координат получается по построению, без поиска совпадений и без накопления ошибки. Два отвергнутых варианта: * **Template matching (старая реализация).** Высота результата непредсказуема и никак не связана с высотой документа, из которой считались координаты хотспотов. При неудачном совпадении код молча подставлял смещение 750 px. * **`Page.captureScreenshot` с `captureBeyondViewport`.** Проверено на живом приложении: карусель во второй строке не отрисовывается, вместо неё дублируется верх страницы. Изображение 1080×2531 выглядит правдоподобно, но нижняя треть не соответствует DOM. --- ## 🔍 4. Достоверность данных Инструмент показывает числа аналитику, поэтому источник каждого числа обозначен явно. ### 4.1 Источник метрик — `public/data/metrics.json`, поле `source` * `clickhouse` — выгружено `tools/fetch_clickhouse.py`; * `demo` — демонстрационные значения, **не аналитика**. Если ключа события нет в каталоге, у хотспота **нет блока `metrics`**, и интерфейс пишет «Нет данных». Ноль в этом месте означал бы «кнопку не нажимают» — совсем другое утверждение. ### 4.2 Достоверность ключа — поле `keyConfidence` `observed` › `dom-attribute` › `manual` › `rule` › `guessed`. Только `observed` означает, что ключ действительно снят с приложения. **Важно.** Ключи вида `PAYMENTS_CLICK`, `SERVICES_CLICK`, `MENUCLICKED` в поставляемой разметке имеют статус `rule` — это соглашение проекта. Реальные ключи, снятые с живого приложения 2026-08-24, выглядят иначе: ``` POST https://api.amplitude.com/ POST https://mc.yandex.ru/watch/96490559/1?page-url=goal://customer.telecom.kz/HOMEPAGEPAYMENTS POST https://mc.yandex.ru/watch/96490559/1?page-url=goal://customer.telecom.kz/OPENWINDOWPAYMENT ``` То есть `HOMEPAGEPAYMENTS`, а не `PAYMENTS_CLICK`. Перевести ключи в `observed`: ```bash py tools/monitor_live_events.py --seconds 120 --map main_dashboard ``` --- ## 🔑 4.3 Реальные ключи событий, снятые с приложения 36 ключей подтверждено на устройстве. Каждый снят с провода (`amplitude` — тело запроса к `api.amplitude.com`), ни один не выведен из подписи. | Экран | Элемент | Ключ | |---|---|---| | `main_dashboard` | AITU Music Слушайте без ограничений | `aitu_music_banner_clicked` | | `main_dashboard` | NEW TURBO Увеличьте скорость интернета | `turbo_widget_clicked` | | `main_dashboard` | TV+ Смотрите все новинки | `HOMEADDSERVICETVPLUS` | | `main_dashboard` | Telecom Shop Покупайте выгодно | `OPENINGWINDOWTELECOMSHOP` | | `main_dashboard` | Заявки | `HOMETAPORDERS` | | `main_dashboard` | Лицевой счет 11440366 г.Алматы, прс.До | `SELECTACCOUNT` | | `main_dashboard` | Меню (правый навбар) | `notautho_main` | | `main_dashboard` | Мои услуги | `HOMETAPMYSERVICES` | | `main_dashboard` | Платежи | `HOMEPAGEPAYMENTS` | | `main_dashboard` | Подключить интернет Скидки и бонусы | `fd_auth_tariffs_opened` | | `main_dashboard` | Свободные средства 16 484.62 ₸ | `detalization_widget_open` | | `main_dashboard` | Трафик | `HOMEDETALIZATION` | | `main_dashboard` | Удв. лич. | `ID_CARD_HOME_CLICK` | | `orders_screen` | Создать заявку | `ORDERSOPENSCREENCREATEORDER` | | `payments_screen` | История платежей | `PAYMENTTAPPAYMENTSTORIE` | | `payments_screen` | Мой автоплатеж | `PAYMENTSTAPMYAUTOPAYMENT` | | `services_screen` | Подробнее | `click_details_mp` | | `side_menu` | Заявки | `ORDERS` | | `side_menu` | Мои услуги | `MYSERVICES` | | `side_menu` | Оферта | `IMPORTANTDOCUMENTS` | | `side_menu` | Платежи | `PAYMENTS` | | `side_menu` | Помощь | `HELP` | | `side_menu` | Профиль телеком | `PROFILETELECOM` | | `side_menu` | Трафик | `DETALIZATION` | | `tariff_bereket` | Подробнее о скидке | `fd_auth_bereket_constructor_opened` | | `tariff_internet_200` | Подробнее о скидке | `fd_auth_internet200_constructor_opened` | | `tariff_internet_500` | Подробнее о скидке | `fd_auth_internet500_constructor_opened` | | `tariff_keremet_mobile` | Подробнее о скидке | `fd_auth_keremet_mobile_constructor_opened` | | `tariff_keremet_tv` | Подробнее о скидке | `fd_auth_keremet_tv_constructor_opened` | | `tariffs_full_digital` | Подробнее | `constructor_opened` | | `tariffs_full_digital` | Подробнее | `constructor_opened` | | `tariffs_full_digital` | Подробнее | `constructor_opened` | | `tariffs_full_digital` | Подробнее | `constructor_opened` | | `tariffs_full_digital` | Подробнее | `constructor_opened` | | `traffic_screen` | Домашний интернет | `button_click_home_internet` | | `traffic_screen` | Мобильная связь | `DETALIZATIONMOBILEINTERNET` | ### Что из этого важно * **Ни один ключ не совпал с тем, что предлагало правило по подписи.** «Трафик» — это `HOMEDETALIZATION`, «Заявки» — `HOMETAPORDERS`, «Свободные средства» — `detalization_widget_open`. * **Один и тот же раздел из разных точек входа даёт разные ключи.** Плитка на главном экране и строка бокового меню ведут на один маршрут, но событие разное: | Раздел | Плитка на главной | Строка бокового меню | |---|---|---| | Платежи | `HOMEPAGEPAYMENTS` | `PAYMENTS` | | Мои услуги | `HOMETAPMYSERVICES` | `MYSERVICES` | | Трафик | `HOMEDETALIZATION` | `DETALIZATION` | | Заявки | `HOMETAPORDERS` | `ORDERS` | Это ровно та разница, ради которой инструмент и делался: по одному ключу «переход в Платежи» нельзя понять, откуда пришёл пользователь. * **Именование в приложении несогласованное**: `HOMETAPORDERS` рядом с `turbo_widget_clicked`, `button_click_home_internet` и `click_details_mp`. * **Не инструментированы**: «Сервисы», «QR», «Мои бонусы» — переходят на свой маршрут, но не отправляют события. --- ## 🗺️ 4.4 Инвентарь экранов | Экран | Маршрут / поверхность | Зон | Подтверждено | |---|---|---|---| | `main_dashboard` | / | 24 | 13 | | `services_screen` | /services | 7 | 1 | | `traffic_screen` | /detalization | 10 | 2 | | `payments_screen` | /payments | 10 | 2 | | `orders_screen` | /appeals | 7 | 1 | | `music_screen` | native | 20 | 0 | | `side_menu` | / | 15 | 7 | | `payments_auto` | /payments/auto | 7 | 0 | | `payments_history` | /payments/history | 7 | 0 | | `detalization_internet` | /detalization/internet | 7 | 0 | | `detalization_mobile` | /detalization/mobile | 7 | 0 | | `appeals_create` | /appeals/create-appeal | 13 | 0 | | `service_details` | /ont/3608039 | 8 | 0 | | `profile_settings` | /settings | 10 | 0 | | `balance_detail` | /detalization/balance/current | 12 | 0 | | `bonuses_screen` | /bonuses | 7 | 0 | | `qr_scan` | /scan-qr | 6 | 0 | | `id_card` | /digital-document/id-card | 8 | 0 | | `services_catalog` | /extra-services | 12 | 0 | | `tariffs_full_digital` | /full-digital | 16 | 5 | | `tv_plus` | /tv-plus | 14 | 0 | | `turbo_landing` | /turbo | 7 | 0 | | `help_screen` | /help | 10 | 0 | | `important_docs` | /important-docs | 9 | 0 | | `aitu_music` | /aitu/music | 12 | 0 | | `tariff_bereket` | /full-digital/auth/tariff | 10 | 1 | | `connect_service` | /full-digital/auth/connect-service | 10 | 0 | | `tariff_keremet_mobile` | /full-digital/auth/tariff | 10 | 1 | | `tariff_keremet_tv` | /full-digital/auth/tariff | 10 | 1 | | `tariff_internet_500` | /full-digital/auth/tariff | 10 | 1 | | `tariff_internet_200` | /full-digital/auth/tariff | 10 | 1 | | `account_selector` | / | 11 | 0 | Названия маршрутов не совпадают с подписями: «Трафик» → `/detalization`, «Заявки» → `/appeals`, «Подключить интернет» → `/full-digital`, «Удв. лич.» → `/digital-document/id-card`. ### Три режима съёмки * **обычный** — сшивка по прокрутке для WebView-страниц; * **`--no-scroll`** — один экран без прокрутки; нужен для оверлеев: боковое меню закрывается при `window.scrollTo`; * **`--native`** — экран без WebView (Музыка): разметка из `uiautomator`. ### Заметки по подэкранам * **Один ключ на пять кнопок — это не ошибка.** Все пять «Подробнее» в каталоге тарифов отправляют `constructor_opened`; различить тариф можно только по следующему событию — `fd_auth_<тариф>_constructor_opened` у «Подробнее о скидке». * **Экраны с задержкой отрисовки.** «Удв. лич.» и каталог тарифов рисуют скелетон и наполняют его через 4-5 секунд. Съёмка ждёт, пока страница перестанет меняться (`--wait`, по умолчанию 20 с); в разметке остаётся флаг `renderSettled`. * **Выбор лицевого счёта** — это нижняя шторка на маршруте `/`, а не отдельный маршрут. Снимается с `--no-scroll`. * **Повторяющиеся подписи.** На экране может быть пять кнопок «Подробнее»; обход адресует их по порядковому номеру, иначе все пять кликают в первую. * **`/full-digital/auth/tariff`** — один маршрут для всех тарифов, поэтому каждый снят под своим id (`tariff_bereket`, `tariff_keremet_mobile`, ...). ### Ограничения перехвата * **Нативные вкладки** (Кабинет / TV+ / Музыка / Чаты / Бизнес), **аватар** и **уведомления** отправляют события из нативного SDK. Через WebView их не видно. * **Экран выбора лицевого счёта** отправляет `SELECTACCOUNT`, но затем требует пин-код — снять его скриншот автоматически нельзя. * **Цепочки событий.** Одно нажатие порождает несколько событий; при частых нажатиях позднее событие приписывается следующему элементу. Пауза 5+ секунд и повтор в двух прогонах. --- ## ⚡ 5. Функционал 1. **Симулятор** — мокап смартфона с прокруткой, переходами и историей «Назад». Переходы на несуществующие экраны не «проглатываются»: такая зона не кликабельна. 2. **Тултипы метрик** — ключ события, русское название, число событий, уникальные пользователи, события на пользователя, доля кликов, целевой экран; плюс пометки «демо-значения» и «ключ не проверен». 3. **Тепловая карта** — нормировка только по измеренным зонам; зоны без метрик серые. 4. **Редактор разметки** — координаты, ключ события с автодополнением из каталога, целевой экран. **Сохранение на диск** через `/api/save-app-map` (с резервной копией). 5. **Съёмка экрана с телефона** — полная прокрутка, нативная разметка из `uiautomator`, DOM-элементы из CDP, обрезка выходящих за экран каруселей, дедупликация. 6. **Перехват событий** — `fetch`, `XMLHttpRequest`, `sendBeacon` и (как запасной источник) analytics-connector; плюс цель Яндекс.Метрики как подтверждение. 7. **Автообход** — переход по разделам через DOM, съёмка каждого, запись пойманных ключей. 8. **Индикатор устройства** — различает «телефон не подключен», «нет WebView» и «сервер не отвечает». --- ## 🛠️ 6. Результаты аудита: что было сломано и как исправлено ### 6.1 Координаты хотспотов не совпадали с картинкой Разметка считалась в координатах документа WebView (`docHeight = scrollHeight * scale + 455`), а картинка бралась либо одиночным скриншотом 2340 px, либо непредсказуемой сшивкой. На момент аудита `main_dashboard` имел `totalHeight: 3886` при изображении высотой 2340: нижнее меню и часть кнопок оказывались за пределами картинки и были недоступны. *Исправлено:* детерминированная сшивка по прочитанному `scrollY` (§3). Проверено: изображение 1080×2989, все 24 зоны совпадают с элементами. ### 6.2 `live_auto_recorder.py` игнорировал аргумент Скрипт читал `sys.argv`, но `__main__` вызывал `sync_current_screen_from_webview("main_dashboard")` жёстко. Любой запрос `/api/live-sync-webview?screenId=…` перезаписывал главный экран. Воспроизведено: запрос для `services_screen` изменил `main_dashboard`. *Исправлено:* аргументы через `argparse`, валидация `screenId`. ### 6.3 Жёстко зашитый PID сокета WebView `webview_devtools_remote_18732` в двух скриптах. PID меняется при каждом перезапуске приложения. *Исправлено:* `find_webview_socket()` — `pidof` с проверкой по `/proc/net/unix`. ### 6.4 Выдуманные метрики выдавались за аналитику `totalEvents: 1200 + len(hotspots) * 410` в трёх скриптах; `metrics.json` целиком синтетический; `fetch_clickhouse.py` указывал на `http://100.x.x.x:8123`. *Исправлено:* генерация чисел удалена; `attach_metrics()` либо ставит реальную строку каталога, либо не ставит ничего; каталог помечается `source`; коннектор ClickHouse переписан на серверные параметры (`{name:Type}`) вместо подстановки в SQL. ### 6.5 Перехват событий не работал Хук ставился на `analyticsConnectorInstances['$default_instance'].eventBridge`. Проверено: хук цепляется (две инстанции, обе с `setEventReceiver`), но **не срабатывает ни разу** — приложение отправляет события напрямую HTTP-запросом на `api.amplitude.com`. *Исправлено:* `tools/amplitude_hook.py` оборачивает `fetch`, `XMLHttpRequest` и `sendBeacon`, разбирает `events[].event_type` из payload Amplitude HTTP V2 и дополнительно снимает цель Яндекс.Метрики. Именно так были получены реальные ключи из §4.2. ### 6.6 API возвращал успех после неудачи `/api/capture-adb` читал `${screenId}_result.json` от предыдущего запуска и возвращал его даже при падении Python-скрипта — интерфейс показывал «✅ обновлено» со старыми данными. *Исправлено:* решение принимается по JSON-вердикту скрипта; ошибка отдаётся с 502. ### 6.7 Подстановка `screenId` в командную строку `exec(\`py tools/capture_screen.py ${screenId} …\`)` — параметр запроса попадал в шелл. *Исправлено:* `execFile` с массивом аргументов плюс проверка `^[A-Za-z0-9_-]{1,64}$` на обеих сторонах. ### 6.8 Гонка записи Middleware запускал `capture_screen.py` и `live_auto_recorder.py` одновременно, оба писали `telecomkz_app_map.json`. *Исправлено:* очередь на сервере, атомарная запись через временный файл. ### 6.9 Правки редактора нельзя было сохранить Существовал только экспорт файла в «Загрузки»; всё, что редактировалось, терялось при перезагрузке, а Live-синхронизация раз в 2 секунды затирала форму. *Исправлено:* эндпойнт `/api/save-app-map` с резервной копией, кнопка «Сохранить разметку на диск», индикатор несохранённых изменений; Live-синхронизация приостанавливается в режиме редактора и при несохранённых правках. ### 6.10 Ошибки CDP-протокола во всех скриптах `await ws.recv()` после `Page.enable` / `Network.enable` считался ответом на команду, хотя CDP присылает события вперемешку с ответами; целевая страница выбиралась как `pages[0]` без проверки типа. *Исправлено:* `CdpSession.send()` сопоставляет ответы по `id` и буферизует события; `pick_page_target()` фильтрует по `type == "page"` и предпочитает `customer.telecom.kz`. ### 6.11 Найдено при доснятии остальных экранов * **Повторная съёмка стирала подтверждённые ключи.** `capture()` полностью заменяет хотспоты экрана, поэтому каждый пересъём терял все `observed`-ключи. Добавлен перенос доверенных ключей (`observed` / `manual` / `dom-attribute`) по подписи элемента: `telecom_cdp._merge_trusted_keys()`. * **Оверлей дублировался на скриншоте.** Боковое меню — `position: fixed` поверх документа, который всё ещё сообщает о прокрутке: `scrollY` менялся, пиксели — нет, и второй кадр вклеивался ниже, повторяя интерфейс. Теперь кадры сравниваются, и сшивка останавливается, как только картинка перестаёт меняться. * **Скрытые за оверлеем элементы попадали в разметку.** Добавлена проверка `document.elementFromPoint` по центру видимой части элемента. Для прокручиваемых экранов она применяется только к полностью видимым элементам — иначе карточки под «липким» баннером на нулевой прокрутке отбрасывались бы, хотя ниже по сшитому скриншоту они видны. * **Боковое меню отсутствовало в разметке.** Его строки — обычные `div.nav-link` без `role` и `href`, ни один селектор их не покрывал. * **Совпадающие ключи.** «Мой автоплатеж» и «История платежей» оба содержат «платеж» и получали один `PAYMENTS_CLICK`. Ключи на экране теперь уникальны. * **Блокировка экрана портила данные.** Заблокированный телефон продолжает отвечать на `screencap` и `uiautomator`, поэтому съёмка молча записывала экран блокировки поверх настоящего. Добавлена проверка `require_awake()`. * **Обрыв CDP терял собранное.** Переход на экран, заменяющий WebView, рвал сокет, и `ConnectionClosedError` уходил мимо обработчика — прогон падал вместе с уже пойманными событиями. Теперь это `DeviceError`, и монитор завершается штатно. --- ### 6.12 Прочее * Замороженный процесс приложения не отвечает по сокету — добавлена проверка переднего плана и вывод приложения на экран перед подключением. * `build_smart_hotspots()` игнорировала снятый дамп `uiautomator` и возвращала захардкоженную разметку главного экрана для **любого** `screenId`. Теперь нативные элементы берутся из дампа по `resource-id`, включая активную вкладку (`clickable="false"` у выбранного таба). * Кириллические ключи (`CLICK_ЧАТЫ`) заменены транслитерацией. * Элементы горизонтальных каруселей выходили за правый край (x + width = 1347 при ширине 1080) — теперь обрезаются по канве. * Порядок правил классификации: «Подключить интернет Скидки и бонусы» попадал в `BONUSES_CLICK`; правила переупорядочены от частных к общим. * Пути в скриптах резолвятся от корня проекта, а не от текущего каталога. * 11 дублирующих скриптов перенесены в `tools/legacy/`, на их местах — заглушки, перенаправляющие на рабочие реализации. --- ## 📂 7. Структура проекта ``` telecomkz_scraper/ ├── public/ │ ├── assets/screens/ скриншоты + *_result.json │ └── data/ │ ├── telecomkz_app_map.json экраны, зоны, связи │ └── metrics.json каталог метрик с полем source ├── src/ │ ├── components/ │ │ ├── PhoneSimulator.tsx мокап, оверлеи, тепловая карта │ │ ├── HotspotTooltip.tsx карточка метрик + пометки достоверности │ │ ├── GuiEditor.tsx редактор зон │ │ └── Sidebar.tsx экраны, режимы, съёмка, сохранение │ ├── types/simulator.ts типы + KeyConfidence / MetricsSource │ └── App.tsx контроллер ├── tools/ │ ├── telecom_cdp.py ядро: ADB, CDP, геометрия, app map │ ├── amplitude_hook.py перехват событий в странице │ ├── capture_screen.py съёмка + разметка │ ├── live_auto_recorder.py пересборка хотспотов экрана │ ├── monitor_live_events.py живой монитор событий │ ├── crawl_and_map_all.py автообход │ ├── fetch_clickhouse.py метрики │ ├── reclassify_hotspots.py пересчёт ключей без телефона │ ├── inspect_page.py диагностика WebView │ ├── device_status.py статус устройства │ └── legacy/ старые версии, не используются ├── vite.config.ts dev-сервер + API-мост └── .env.example настройки ClickHouse ``` --- ## 🚀 8. Запуск и типовые сценарии ```bash npm install py -m pip install opencv-python numpy pillow websockets requests npm run dev # http://localhost:5173 ``` **Проверить связь с телефоном** ```bash py tools/device_status.py ``` **Снять текущий экран** ```bash py tools/capture_screen.py main_dashboard --name "Главный экран (Кабинет)" ``` **Узнать настоящие ключи событий** (главный сценарий для аналитика) ```bash py tools/monitor_live_events.py --seconds 120 --map main_dashboard ``` **Подтянуть метрики** ```bash cp .env.example .env # указать CLICKHOUSE_HOST py tools/fetch_clickhouse.py --days 30 py tools/reclassify_hotspots.py ``` **Диагностика WebView** ```bash py tools/inspect_page.py amplitude bridge vue ``` ### Известные ограничения * Если приложение уходит в фон, Android замораживает процесс и отладочный сокет перестаёт отвечать. Инструменты сами выводят приложение на передний план. * Ключи событий в поставляемой разметке имеют статус `rule` и требуют подтверждения через `monitor_live_events.py` — до этого join с ClickHouse не даст результата. * `metrics.json` в репозитории помечен `source: "demo"`.