telecomkz_scraper/PROJECT_DOCUMENTATION.md
Iliyas Kyrykbayev 5cb347a44b TelecomKz Analytics Mapper: audit fixes, real event keys, 32 mapped screens
Rebuilt the capture and mapping pipeline after an audit found the simulator's
data could not be trusted:

* Hotspot coordinates never matched the screenshots. Capture now scrolls the
  page over CDP and pastes each frame at the measured scrollY, so image pixels
  and DOM coordinates share one grid by construction.
* Metrics were synthesised (1200 + n*410) and presented as analytics. Numbers
  are now attached only when the catalog has a matching row; metrics.json
  carries a `source` label and the UI says "no data" instead of showing zeros.
* Event interception hooked a connector bridge that never fires. The app posts
  to api.amplitude.com using the legacy form-urlencoded v1 API; the hook now
  reads event_type off the wire. 36 keys are verified as `observed`.
* All device access moved into tools/telecom_cdp.py: dynamic WebView socket
  discovery (the PID was hardcoded), id-matched CDP, measured native geometry.
* Editor edits can now be saved to disk; API failures no longer report success
  from a stale result file; screenId is no longer interpolated into a shell.

Screens went from 7 (with fabricated markup) to 32, all verified: image height
equals map height, no out-of-bounds hotspots, no dead links.

The id_card screenshot has been manually redacted - it showed a national ID.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 18:16:46 +05:00

33 KiB
Raw Permalink Blame History

📡 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. О проекте
  2. Архитектура
  3. Система координат — главное, что нужно понять
  4. Достоверность данных
  5. Функционал
  6. Результаты аудита: что было сломано и как исправлено
  7. Структура проекта
  8. Запуск и типовые сценарии

🎯 1. О проекте

Визуальный инструмент для сквозного маппинга UI-элементов мобильного приложения Казахтелеком на события Amplitude и таблицы ClickHouse: наведение на любую кнопку показывает ключ события и продуктовые метрики, а разметка снимается прямо с подключённого по USB смартфона.

Приложение гибридное: нативная оболочка (тулбар сверху, таб-бар снизу) плюс Vue.js-WebView в середине. Из этого следует всё остальное — и способ съёмки, и способ получения координат, и способ перехвата событий.


🏗️ 2. Архитектура

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_&lt;pid&gt;"]
        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:

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. Запуск и типовые сценарии

npm install
py -m pip install opencv-python numpy pillow websockets requests
npm run dev          # http://localhost:5173

Проверить связь с телефоном

py tools/device_status.py

Снять текущий экран

py tools/capture_screen.py main_dashboard --name "Главный экран (Кабинет)"

Узнать настоящие ключи событий (главный сценарий для аналитика)

py tools/monitor_live_events.py --seconds 120 --map main_dashboard

Подтянуть метрики

cp .env.example .env    # указать CLICKHOUSE_HOST
py tools/fetch_clickhouse.py --days 30
py tools/reclassify_hotspots.py

Диагностика WebView

py tools/inspect_page.py amplitude bridge vue

Известные ограничения

  • Если приложение уходит в фон, Android замораживает процесс и отладочный сокет перестаёт отвечать. Инструменты сами выводят приложение на передний план.
  • Ключи событий в поставляемой разметке имеют статус rule и требуют подтверждения через monitor_live_events.py — до этого join с ClickHouse не даст результата.
  • metrics.json в репозитории помечен source: "demo".