pub const OBSERVER_SAMPLE_CAP: usize = 2000;
pub fn observer_js() -> String {
format!(
r#"(() => {{
try {{
if (window.__crawlex_observer_installed__) return;
Object.defineProperty(window, '__crawlex_observer_installed__', {{
value: true, writable: false, configurable: false, enumerable: false
}});
const CAP = {cap};
window.__crawlex_runtime_routes__ = [];
window.__crawlex_network_endpoints__ = [];
window.__crawlex_idb_audit__ = [];
const routes = window.__crawlex_runtime_routes__;
const endpoints = window.__crawlex_network_endpoints__;
const idbAudit = window.__crawlex_idb_audit__;
const pushIdb = (rec) => {{
try {{ if (idbAudit.length < CAP) idbAudit.push(rec); }} catch (_) {{}}
}};
const pushRoute = (type, url) => {{
try {{
if (routes.length >= CAP) return;
const abs = (() => {{
try {{ return new URL(url, document.baseURI).href; }}
catch (_) {{ return String(url); }}
}})();
routes.push({{ type: String(type), url: abs, at: Date.now() }});
}} catch (_) {{}}
}};
const pushEndpoint = (rec) => {{
try {{
if (endpoints.length >= CAP) return;
endpoints.push(rec);
}} catch (_) {{}}
}};
// --- History API -------------------------------------------------
try {{
const origPush = history.pushState;
const origReplace = history.replaceState;
history.pushState = function(state, title, url) {{
const ret = origPush.apply(this, arguments);
if (url !== undefined && url !== null) pushRoute('pushState', url);
return ret;
}};
history.replaceState = function(state, title, url) {{
const ret = origReplace.apply(this, arguments);
if (url !== undefined && url !== null) pushRoute('replaceState', url);
return ret;
}};
}} catch (_) {{}}
// --- popstate / hashchange --------------------------------------
try {{
window.addEventListener('popstate', () => {{
pushRoute('popstate', location.href);
}}, true);
window.addEventListener('hashchange', () => {{
pushRoute('hashchange', location.href);
}}, true);
}} catch (_) {{}}
// --- fetch wrapper ----------------------------------------------
try {{
const origFetch = window.fetch;
if (typeof origFetch === 'function') {{
window.fetch = function(input, init) {{
let url = '';
let method = 'GET';
try {{
if (typeof input === 'string') url = input;
else if (input && typeof input.url === 'string') {{ url = input.url; method = input.method || method; }}
else url = String(input);
if (init && typeof init.method === 'string') method = init.method;
}} catch (_) {{}}
const started = Date.now();
const rec = {{ kind: 'fetch', method: String(method).toUpperCase(), url, started_at: started }};
pushEndpoint(rec);
let p;
try {{ p = origFetch.apply(this, arguments); }}
catch (e) {{ rec.error = String(e && e.message || e); throw e; }}
return p.then((resp) => {{
try {{
rec.status = resp && resp.status;
rec.ok = resp && resp.ok;
rec.duration_ms = Date.now() - started;
}} catch (_) {{}}
return resp;
}}, (err) => {{
try {{
rec.error = String(err && err.message || err);
rec.duration_ms = Date.now() - started;
}} catch (_) {{}}
throw err;
}});
}};
}}
}} catch (_) {{}}
// --- XHR wrapper -------------------------------------------------
try {{
const XHRProto = XMLHttpRequest && XMLHttpRequest.prototype;
if (XHRProto) {{
const origOpen = XHRProto.open;
const origSend = XHRProto.send;
XHRProto.open = function(method, url) {{
try {{
this.__crawlex_xhr__ = {{
method: String(method || 'GET').toUpperCase(),
url: String(url || ''),
}};
}} catch (_) {{}}
return origOpen.apply(this, arguments);
}};
XHRProto.send = function() {{
try {{
const info = this.__crawlex_xhr__ || {{ method: 'GET', url: '' }};
const started = Date.now();
const rec = {{
kind: 'xhr', method: info.method, url: info.url, started_at: started,
}};
pushEndpoint(rec);
const onDone = () => {{
try {{
rec.status = this.status;
rec.ok = this.status >= 200 && this.status < 400;
rec.duration_ms = Date.now() - started;
}} catch (_) {{}}
}};
this.addEventListener('loadend', onDone, {{ once: true }});
}} catch (_) {{}}
return origSend.apply(this, arguments);
}};
}}
}} catch (_) {{}}
// --- IndexedDB transaction-order audit --------------------------
// Wraps IDBObjectStore.put / add / delete so we record the order
// writes were issued in. A follow-up collector can re-read the
// store and compare order; divergence emits a
// `VendorTelemetryObserved` host-event, log-only. Keeps semantics:
// we return the original IDBRequest so callers see no change.
try {{
const OSProto = (typeof IDBObjectStore !== 'undefined') ? IDBObjectStore.prototype : null;
if (OSProto) {{
const origPut = OSProto.put;
const origAdd = OSProto.add;
const origDel = OSProto.delete;
const wrap = (op, orig) => function(value, key) {{
try {{
const storeName = (this && this.name) || '<anon>';
const keyRepr = (() => {{
try {{
if (arguments.length >= 2) return String(key);
if (value && typeof value === 'object' && 'id' in value) return String(value.id);
}} catch (_) {{}}
return null;
}})();
pushIdb({{ op: op, store: String(storeName), key: keyRepr, at: Date.now() }});
}} catch (_) {{}}
return orig.apply(this, arguments);
}};
if (typeof origPut === 'function') OSProto.put = wrap('put', origPut);
if (typeof origAdd === 'function') OSProto.add = wrap('add', origAdd);
if (typeof origDel === 'function') OSProto.delete = wrap('delete', origDel);
}}
}} catch (_) {{}}
}} catch (_) {{}}
}})();"#,
cap = OBSERVER_SAMPLE_CAP
)
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct RouteObservation {
#[serde(rename = "type")]
pub kind: String,
pub url: String,
#[serde(default)]
pub at: Option<i64>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct NetworkEndpointObservation {
pub kind: String,
pub method: String,
pub url: String,
#[serde(default)]
pub started_at: Option<i64>,
#[serde(default)]
pub status: Option<i64>,
#[serde(default)]
pub ok: Option<bool>,
#[serde(default)]
pub duration_ms: Option<i64>,
#[serde(default)]
pub error: Option<String>,
}
pub fn collect_expression() -> &'static str {
r#"JSON.parse(JSON.stringify({
routes: (window.__crawlex_runtime_routes__ || []),
endpoints: (window.__crawlex_network_endpoints__ || []),
idb_audit: (window.__crawlex_idb_audit__ || []),
}))"#
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct IdbAuditEntry {
pub op: String,
pub store: String,
#[serde(default)]
pub key: Option<String>,
#[serde(default)]
pub at: Option<i64>,
}
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct CollectedObservations {
#[serde(default)]
pub routes: Vec<RouteObservation>,
#[serde(default)]
pub endpoints: Vec<NetworkEndpointObservation>,
#[serde(default)]
pub idb_audit: Vec<IdbAuditEntry>,
}