1(function () { 2 // Kept equal to packages/agent-id/package.json by 3 // tests/test_sdk_v02_package.py. This sensor is served as a plain 4 // browser script and shipped in the package byte-for-byte, so it can 5 // neither read the package at runtime nor go through a build step: 6 // the version is written once here and CI refuses a mismatch. 7 var SDK_VERSION = "0.3.49"; 8 var w = window; 9 var d = document; 10 var s = d.currentScript || {}; 11 var cfg = w.twin3AEO || {}; 12 var endpoint = cfg.endpoint || s.getAttribute && s.getAttribute("data-endpoint") || "/api/agent/sdk_event"; 13 var agentId = cfg.agentId || s.getAttribute && s.getAttribute("data-agent-id") || ""; 14 var siteToken = cfg.siteToken || s.getAttribute && s.getAttribute("data-site-token") || ""; 15 var eventName = cfg.event || "page_view"; 16 var aiRefHosts = /(chatgpt|openai|perplexity|gemini|bard|claude|anthropic|copilot|bing|you\.com|poe\.com)/i; 17 var clsScore = 0; 18 var clsEntries = 0; 19 20 function clip(value, n) { 21 value = value == null ? "" : String(value); 22 return value.length > n ? value.slice(0, n) : value; 23 } 24 25 function attr(el, name) { 26 return el && el.getAttribute ? (el.getAttribute(name) || "") : ""; 27 } 28 29 function text(el) { 30 return clip((el && el.textContent || "").replace(/\s+/g, " ").trim(), 160); 31 } 32 33 function meta(name) { 34 var el = d.querySelector('meta[name="' + name + '"],meta[property="' + name + '"]'); 35 return attr(el, "content"); 36 } 37 38 function hasName(el) { 39 if (!el) return false; 40 return !!(attr(el, "aria-label") || attr(el, "title") || attr(el, "alt") || attr(el, "placeholder") || attr(el, "name") || text(el)); 41 } 42 43 function cleanData(obj, depth) { 44 if (!obj || typeof obj !== "object" || depth > 2) return obj; 45 var out = Array.isArray(obj) ? [] : {}; 46 Object.keys(obj).slice(0, 40).forEach(function (key) { 47 var low = key.toLowerCase(); 48 if (/email|phone|tel|name|note|message|token|key|secret|password/.test(low)) { 49 out[key] = "[redacted]"; 50 return; 51 } 52 var v = obj[key]; 53 if (typeof v === "string") out[key] = clip(v, 160); 54 else if (typeof v === "number" || typeof v === "boolean") out[key] = v; 55 else if (v && typeof v === "object") out[key] = cleanData(v, depth + 1); 56 }); 57 return out; 58 } 59 60 function collectSignals() { 61 var forms = Array.prototype.slice.call(d.querySelectorAll("form")); 62 var webmcpForms = forms.filter(function (f) { return attr(f, "toolname") && attr(f, "tooldescription"); }); 63 var badToolInputs = 0; 64 webmcpForms.forEach(function (f) { 65 Array.prototype.slice.call(f.querySelectorAll("input,select,textarea")).forEach(function (el) { 66 if ((attr(el, "type") || "").toLowerCase() === "hidden") return; 67 if (!attr(el, "name")) badToolInputs += 1; 68 }); 69 }); 70 var interactive = Array.prototype.slice.call(d.querySelectorAll("a,button,input,select,textarea")); 71 var namedInteractive = interactive.filter(function (el) { 72 if (el.tagName === "INPUT" && (attr(el, "type") || "").toLowerCase() === "hidden") return true; 73 return hasName(el); 74 }); 75 var imgs = Array.prototype.slice.call(d.querySelectorAll("img")); 76 var missingImgSize = imgs.filter(function (img) { return !(attr(img, "width") && attr(img, "height")); }); 77 var ref = d.referrer || ""; 78 var refHost = ""; 79 try { refHost = ref ? (new URL(ref)).hostname : ""; } catch (_) { refHost = ""; } 80 return { 81 title_len: (d.title || "").length, 82 description_len: meta("description").length, 83 canonical: !!d.querySelector('link[rel~="canonical"]'), 84 viewport: !!meta("viewport"), 85 lang: clip(d.documentElement && d.documentElement.lang || "", 24), 86 h1_count: d.querySelectorAll("h1").length, 87 jsonld_count: d.querySelectorAll('script[type="application/ld+json"]').length, 88 links: d.querySelectorAll("a[href]").length, 89 forms: forms.length, 90 webmcp_forms: webmcpForms.length, 91 webmcp_bad_inputs: badToolInputs, 92 interactive: interactive.length, 93 named_interactive: namedInteractive.length, 94 images: imgs.length, 95 images_missing_size: missingImgSize.length, 96 cls_score: Math.round(clsScore * 10000) / 10000, 97 cls_entries: clsEntries, 98 referrer_host: clip(refHost, 80), 99 ai_referrer: aiRefHosts.test(ref) 100 }; 101 } 102 103 function observeLayoutShift() { 104 if (!("PerformanceObserver" in w)) return; 105 try { 106 var observer = new PerformanceObserver(function (list) {
107 list.getEntries().forEach(function (entry) { 108 if (entry.hadRecentInput) return; 109 clsScore += entry.value || 0; 110 clsEntries += 1; 111 }); 112 }); 113 observer.observe({ type: "layout-shift", buffered: true }); 114 } catch (_) {} 115 } 116 117 function payload(event, data) { 118 var out = { 119 sdk: "aeo-site-sdk", 120 sdk_version: SDK_VERSION, 121 event: event || eventName, 122 timestamp: new Date().toISOString(), 123 agent_id: agentId, 124 url: location.href, 125 path: location.pathname 126 }; 127 if (siteToken) out.site_token = siteToken; 128 if (data) out.data = cleanData(data, 0); 129 return JSON.stringify(out); 130 } 131 132 function post(body) { 133 if (navigator.sendBeacon) { 134 var ok = navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" })); 135 if (ok) return; 136 } 137 fetch(endpoint, { 138 method: "POST", 139 headers: { "content-type": "application/json" }, 140 body: body, 141 keepalive: true, 142 credentials: "omit" 143 }).catch(function () {}); 144 } 145 146 function send(event, data) { 147 post(payload(event, data)); 148 } 149 150 function signal(extra) { 151 send("page_signal", { signals: collectSignals(), extra: extra || {} }); 152 } 153 154 function conversion(name, data) { 155 send("conversion", { conversion: clip(name || "conversion", 80), referrer: d.referrer || "", value: cleanData(data || {}, 0) }); 156 } 157 158 w.twin3AEO = { 159 agentId: agentId, 160 siteToken: siteToken, 161 endpoint: endpoint, 162 version: SDK_VERSION, 163 track: send, 164 signal: signal, 165 conversion: conversion, 166 collectSignals: collectSignals 167 }; 168 169 function boot() { 170 observeLayoutShift(); 171 send("page_view"); 172 setTimeout(function () { signal(); }, 700); 173 } 174 175 if (d.readyState === "loading") { 176 d.addEventListener("DOMContentLoaded", boot, { once: true }); 177 } else { 178 boot(); 179 } 180})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.