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>
141 lines
5.5 KiB
Python
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())
|