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>
143 lines
4.5 KiB
Python
143 lines
4.5 KiB
Python
"""
|
|
Diagnostic probes against the live WebView.
|
|
|
|
Consolidates what inspect_amplitude / inspect_bridge / inspect_window / inspect_vue_app /
|
|
read_amplitude_events / debug_cdp used to do separately. All of them shared the same
|
|
three defects: they assumed an adb forward already existed, they grabbed targets[0]
|
|
without checking its type, and they read one WebSocket frame after Page.enable and
|
|
treated whatever arrived as the command reply. The shared core fixes all three.
|
|
|
|
py tools/inspect_page.py # everything
|
|
py tools/inspect_page.py amplitude bridge
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
import telecom_cdp as T
|
|
|
|
PROBES = {
|
|
"page": """
|
|
(() => ({
|
|
url: location.href,
|
|
route: location.pathname,
|
|
title: document.title,
|
|
innerWidth: window.innerWidth,
|
|
innerHeight: window.innerHeight,
|
|
devicePixelRatio: window.devicePixelRatio,
|
|
scrollHeight: document.documentElement.scrollHeight,
|
|
scrollY: window.scrollY,
|
|
readyState: document.readyState
|
|
}))()
|
|
""",
|
|
"amplitude": """
|
|
(() => {
|
|
const connectors = window.analyticsConnectorInstances || {};
|
|
return {
|
|
hasAmplitudeGlobal: typeof window.amplitude !== 'undefined',
|
|
connectorInstances: Object.keys(connectors),
|
|
connectorShape: Object.keys(connectors).reduce((acc, k) => {
|
|
const inst = connectors[k] || {};
|
|
acc[k] = {
|
|
keys: Object.keys(inst).slice(0, 20),
|
|
hasEventBridge: !!inst.eventBridge,
|
|
hasSetEventReceiver: !!(inst.eventBridge && typeof inst.eventBridge.setEventReceiver === 'function')
|
|
};
|
|
return acc;
|
|
}, {}),
|
|
hasYandexMetrika: typeof window.ym === 'function',
|
|
hasGtag: typeof window.gtag === 'function'
|
|
};
|
|
})()
|
|
""",
|
|
"bridge": """
|
|
(() => {
|
|
const out = {};
|
|
['AndroidBridge', 'Android', 'Aitu', 'NativeBridge', 'webkit'].forEach(name => {
|
|
const obj = window[name];
|
|
if (!obj) return;
|
|
const methods = [];
|
|
for (const k in obj) {
|
|
try { if (typeof obj[k] === 'function') methods.push(k); } catch (e) { /* guarded getter */ }
|
|
}
|
|
out[name] = methods.slice(0, 60);
|
|
});
|
|
return out;
|
|
})()
|
|
""",
|
|
"window": """
|
|
(() => {
|
|
const builtin = new Set(Object.getOwnPropertyNames(Object.getPrototypeOf(window)));
|
|
const custom = Object.keys(window).filter(k => !builtin.has(k) && !k.startsWith('webkit'));
|
|
return {
|
|
customGlobals: custom.slice(0, 120),
|
|
total: custom.length
|
|
};
|
|
})()
|
|
""",
|
|
"vue": """
|
|
(() => {
|
|
const root = document.querySelector('#app') || document.body;
|
|
const vue = root && (root.__vue_app__ || root.__vue__);
|
|
return {
|
|
hasVueApp: !!(root && root.__vue_app__),
|
|
hasVueInstance: !!(root && root.__vue__),
|
|
version: (window.Vue && window.Vue.version) || (vue && vue.version) || null,
|
|
rootId: root ? root.id : null,
|
|
routerPath: (window.$nuxt && window.$nuxt.$route && window.$nuxt.$route.path) || location.pathname,
|
|
componentClasses: [...new Set([...document.querySelectorAll('[class]')]
|
|
.map(e => String(e.className).split(' ')[0]).filter(Boolean))].slice(0, 60)
|
|
};
|
|
})()
|
|
""",
|
|
"storage": """
|
|
(() => {
|
|
const pick = store => {
|
|
const out = {};
|
|
for (let i = 0; i < store.length; i++) {
|
|
const k = store.key(i);
|
|
if (!/amplitude|analytics|event|session|device/i.test(k)) continue;
|
|
const v = store.getItem(k) || '';
|
|
out[k] = v.length > 400 ? v.slice(0, 400) + '...(truncated)' : v;
|
|
}
|
|
return out;
|
|
};
|
|
return { localStorage: pick(window.localStorage), sessionStorage: pick(window.sessionStorage) };
|
|
})()
|
|
""",
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Probe the TelecomKz WebView.")
|
|
parser.add_argument("probes", nargs="*", choices=list(PROBES) + [], default=None)
|
|
args = parser.parse_args()
|
|
names = args.probes or list(PROBES)
|
|
|
|
try:
|
|
device, target = T.connect()
|
|
except T.DeviceError as exc:
|
|
print("Error: " + str(exc))
|
|
return 1
|
|
|
|
print("Device: " + device)
|
|
print("Target: " + str(target.get("title")) + " " + str(target.get("url")))
|
|
|
|
with T.CdpSession(target["webSocketDebuggerUrl"]) as session:
|
|
for name in names:
|
|
print("\n=== " + name + " ===")
|
|
try:
|
|
value = session.evaluate("JSON.stringify(" + PROBES[name] + ")")
|
|
print(json.dumps(json.loads(value), ensure_ascii=False, indent=2))
|
|
except Exception as exc:
|
|
print("probe failed: " + str(exc))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|