telecomkz_scraper/tools/monitor_live_events.py
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

141 lines
5.5 KiB
Python

"""
Watch TelecomKz analytics events fire in real time while you tap the phone.
This is the fastest way to learn a button's true event key: the Amplitude export and
the ClickHouse stream lag by an hour or more, but the app sends the event the instant
you tap, and this reads it off the wire.
py tools/monitor_live_events.py --seconds 120
py tools/monitor_live_events.py --seconds 120 --map main_dashboard
With --map, every observed key is written onto the hotspot whose caption matched the
tap, with keyConfidence "observed" - the only confidence level in this project that
means the key is real rather than derived from a button label.
"""
import argparse
import json
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import telecom_cdp as T
from amplitude_hook import DRAIN_JS, INSTALL_HOOK_JS
def stamp_observed(screen_id, pairs):
"""Write observed event keys onto matching hotspots of one screen."""
if not pairs:
return 0
app_map = T.load_app_map()
catalog = T.load_metrics_catalog()
source = T.metrics_source()
updated = 0
for screen in app_map.get("screens", []):
if screen["id"] != screen_id:
continue
for hs in screen.get("hotspots", []):
label = (hs.get("label") or "").strip().lower()
if not label:
continue
for text, key, _via in pairs:
if text and (text in label or label in text):
hs["eventKey"] = key
hs["keyConfidence"] = "observed"
T.attach_metrics(hs, catalog, source)
updated += 1
break
if updated:
T.save_app_map(app_map)
return updated
def monitor(seconds, map_screen_id=None, poll=0.6):
device, target = T.connect()
print("Connected to " + str(target.get("url")))
with T.CdpSession(target["webSocketDebuggerUrl"]) as session:
hook = json.loads(session.evaluate("JSON.stringify(" + INSTALL_HOOK_JS + ")"))
print("Hooked transports: " + ", ".join(hook.get("transports") or ["(none)"]))
if hook.get("already"):
print("(hook was already installed from an earlier run)")
session.evaluate(DRAIN_JS)
print("\nTap buttons on the phone now. Listening for " + str(seconds) + "s...\n")
deadline = time.time() + seconds
pending_click = None
pairs = []
seen_keys = {}
while time.time() < deadline:
time.sleep(poll)
try:
batch = session.evaluate(DRAIN_JS) or []
except T.DeviceError as exc:
# Keep going to the summary: events already observed are the whole
# point of the run and must not be thrown away with the connection.
print("\nLost the page: " + str(exc))
print("Reporting what was captured before the disconnect.")
break
for item in batch:
kind = item.get("kind")
if kind == "click":
pending_click = (item.get("text") or "").strip()
print(" [tap] " + (pending_click or "(no caption)") + " route=" + str(item.get("route")))
elif kind in ("amplitude", "connector", "metrika-goal"):
key = item.get("eventType")
if not key:
continue
tag = {"amplitude": "AMPLITUDE", "connector": "CONNECTOR", "metrika-goal": "metrika "}[kind]
print(" [" + tag + "] " + key + (" <- " + pending_click if pending_click else ""))
seen_keys[key] = seen_keys.get(key, 0) + 1
# Amplitude's own payload is authoritative. The Metrika goal name
# is identical to event_type (verified on matching taps), so it is
# accepted as a fallback when the Amplitude body cannot be read.
if kind in ("amplitude", "connector", "metrika-goal") and pending_click:
pairs.append((pending_click.lower(), key, kind))
pending_click = None
print("\n--- summary ---")
if not seen_keys:
print("No analytics events observed. Did you tap anything on the phone?")
for key, count in sorted(seen_keys.items(), key=lambda kv: -kv[1]):
print(" " + str(count) + "x " + key)
print("\nTap -> event pairs: " + str(len(pairs)))
for text, key, via in pairs:
print(" " + key + " <- " + text + " [" + via + "]")
if map_screen_id and pairs:
updated = stamp_observed(map_screen_id, pairs)
print("\nWrote " + str(updated) + " observed keys into screen " + map_screen_id + ".")
elif map_screen_id:
print("\nNothing to write into " + map_screen_id + ".")
def main():
parser = argparse.ArgumentParser(description="Live analytics event monitor for TelecomKz.")
parser.add_argument("--seconds", type=int, default=60)
parser.add_argument(
"--map",
dest="map_screen_id",
default=None,
help="Write observed keys onto this screen's hotspots in the app map",
)
args = parser.parse_args()
try:
monitor(args.seconds, args.map_screen_id)
except T.DeviceError as exc:
print("Error: " + str(exc))
return 1
except KeyboardInterrupt:
print("\nStopped.")
return 0
if __name__ == "__main__":
sys.exit(main())