1// Lightweight first-touch analytics for public reading pages. 2// 3// These pages are static on purpose, but a visitor who arrives from ChatGPT or another answer 4// engine must keep that discovery source when they continue into the application. PostHog uses 5// sessionStorage here and in the app, so the attribution survives a same-origin navigation and 6// expires with the tab. Session replay stays off on reading pages. Query strings are reduced to 7// the campaign allow-list before any property leaves the browser. 8(function () { 9 var firstTouchKey = 'sg_discovery_first_touch_v1'; 10 11 function campaign() { 12 var properties = {}; 13 var url; 14 try { url = new URL(location.href); } catch (error) { return properties; } 15 [['utm_source', 80], ['utm_medium', 80], ['utm_campaign', 120]].forEach(function (entry) { 16 var value = (url.searchParams.get(entry[0]) || '').trim().toLowerCase(); 17 if (value) properties[entry[0]] = value.slice(0, entry[1]); 18 }); 19 return properties; 20 } 21 22 function normalizeAttribution(value) { 23 var channels = ['ai_assistant', 'organic_search', 'campaign', 'direct', 'internal', 'referral']; 24 if (!value || typeof value !== 'object' || channels.indexOf(value.discovery_channel) === -1 || 25 typeof value.discovery_source !== 'string' || !value.discovery_source.trim()) return null; 26 var clean = { 27 discovery_channel: value.discovery_channel, 28 discovery_source: value.discovery_source.trim().toLowerCase().slice(0, 120) 29 }; 30 [['utm_source', 80], ['utm_medium', 80], ['utm_campaign', 120]].forEach(function (entry) { 31 if (typeof value[entry[0]] === 'string' && value[entry[0]].trim()) { 32 clean[entry[0]] = value[entry[0]].trim().toLowerCase().slice(0, entry[1]); 33 } 34 }); 35 return clean; 36 } 37 38 function firstTouch() { 39 var current = discovery(); 40 var campaignValues = campaign(); 41 Object.keys(campaignValues).forEach(function (name) { current[name] = campaignValues[name]; }); 42 current = normalizeAttribution(current); 43 try { 44 var stored = normalizeAttribution(JSON.parse(sessionStorage.getItem(firstTouchKey) || 'null')); 45 if (stored) return stored; 46 sessionStorage.setItem(firstTouchKey, JSON.stringify(current)); 47 } catch (error) { /* Storage-disabled browsers keep the current touch in memory. */ } 48 return current; 49 } 50 51 // Capture the handoff even when analytics is disabled or blocked. This record stays in the 52 // browser until the buyer submits the form and the edge function accepts the bounded fields. 53 var durableFirstTouch = firstTouch(); 54 var key = 'phc_C5Dt5i3XruCxBkfPf5kX5trFtWj5r6zJcazDpFLUSNiv'; 55 if (!key || key.indexOf('__POSTHOG') === 0) return; 56 57 // Only the synthetic walkthrough can use this seam. Reconstruct each property from a fixed 58 // vocabulary, never forward event detail or verifier payloads. Keep a bounded in-memory queue 59 // for real interactions made while the analytics SDK loads. No key means no listener or queue. 60 var proofPending = []; 61 var toolPending = []; 62 var proofReady = false; 63 function captureProof(event) { 64 try { window.posthog.capture(event.name, event.properties); } catch (error) { /* Analytics cannot break verification. */ } 65 } 66 if (location.pathname.replace(/\/+$/, '') === '/resources/verify-a-receipt') { 67 document.addEventListener('sg:receipt-proof', function (event) { 68 var detail = event.detail || {}; 69 var properties = {}; 70 var name; 71 if (detail.type === 'cta' && ['request', 'verifier', 'checklist'].indexOf(detail.destination) !== -1) { 72 name = 'receipt_proof_cta'; 73 properties.proof_destination = detail.destination; 74 } else if (['selected', 'checked'].indexOf(detail.type) !== -1 && ['original', 'permission', 'file', 'key'].indexOf(detail.scenario) !== -1) { 75 properties.proof_scenario = detail.scenario; 76 name = 'receipt_proof_selected'; 77 if (detail.type === 'checked') { 78 if (['pass', 'fail', 'incomplete', 'unavailable'].indexOf(detail.verdict) === -1) return; 79 name = 'receipt_proof_checked'; 80 properties.proof_verdict = detail.verdict; 81 } 82 } else return; 83 var clean = { name: name, properties: properties }; 84 if (proofReady) captureProof(clean); 85 else if (proofPending.length < 16) proofPending.push(clean); 86 }); 87 } 88 89 function captureTool(event) { 90 try { window.posthog.capture(event.name, event.properties); } catch (error) { /* Analytics cannot break a public tool. */ } 91 } 92 93 function queueTool(event) { 94 if (proofReady) captureTool(event); 95 else if (toolPending.length < 24) toolPending.push(event); 96 } 97 98 // One named step between an answer-page visit and the agency form. The destination and intent 99 // both come from fixed public vocabularies; link text, page text and query strings never leave. 100 document.addEventListener('click', function (event) { 101 var element = event.target; 102 var anchor = element && element.closest ? element.closest('a[href]') : null; 103 if (!anchor) return; 104 var destination; 105 try { destination = new URL(anchor.href, location.href); } catch (error) { return; } 106 if (destination.origin !== location.origin || destination.pathname.replace(/\/+$/, '') !== '/for-agencies') return; 107 var intent = destination.searchParams.get('intent'); 108 if (['voice', 'digital-replica', 'provenance'].indexOf(intent) === -1) return; 109 queueTool({ name: 'authority_conversion_click', properties: { entry_intent: intent } }); 110 }); 111 112 if (location.pathname.replace(/\/+$/, '') === '/resources/ai-likeness-licence-scope-builder') { 113 document.addEventListener('sg:scope-builder', function (event) { 114 var detail = event.detail || {}; 115 var actions = ['copy', 'download', 'print', 'request', 'handoff', 'evidence']; 116 if (actions.indexOf(detail.action) === -1) return; 117 var values = [detail.present, detail.unresolved, detail.not_reviewed]; 118 if (values.some(function (value) { return !Number.isInteger(value) || value < 0 || value > 7; })) return; 119 if (values[0] + values[1] + values[2] !== 7) return; 120 queueTool({ name: 'licence_scope_builder_action', properties: { 121 scope_action: detail.action, 122 controls_present: detail.present, 123 controls_unresolved: detail.unresolved, 124 controls_not_reviewed: detail.not_reviewed 125 } }); 126 }); 127 } 128 129 if (location.pathname.replace(/\/+$/, '') === '/resources/ai-identity-source-registry') { 130 document.addEventListener('sg:source-registry', function (event) { 131 var detail = event.detail || {}; 132 var types = ['all', 'enacted_law', 'proposed_legislation', 'government_report_or_guidance', 'collective_agreement', 'technical_standard']; 133 var controls = ['all', 'permission', 'specific_scope', 'compensation', 'asset_control', 'disclosure', 'provenance', 'lifecycle', 'remedy']; 134 if (types.indexOf(detail.authority_type) === -1 || controls.indexOf(detail.control)
134=== -1 || typeof detail.has_search !== 'boolean') return; 135 queueTool({ name: 'source_registry_filter', properties: { 136 registry_authority_type: detail.authority_type, 137 registry_control: detail.control, 138 registry_has_search: detail.has_search 139 } }); 140 }); 141 } 142 143 var keep = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'ref', 'gclid', 'fbclid']; 144 145 function scrubUrl(value) { 146 if (typeof value !== 'string' || !value) return value; 147 var relative = !/^[a-z][a-z0-9+.-]*:\/\//i.test(value); 148 if (relative && value.charAt(0) !== '/') return value; 149 var url; 150 try { url = new URL(value, 'http://scrub.invalid'); } catch (error) { return value; } 151 var kept = []; 152 keep.forEach(function (name) { 153 var selected = url.searchParams.get(name); 154 if (selected !== null) kept.push(encodeURIComponent(name) + '=' + encodeURIComponent(selected)); 155 }); 156 return (relative ? '' : url.origin) + url.pathname + (kept.length ? '?' + kept.join('&') : ''); 157 } 158 159 function scrub(properties, depth) { 160 if (properties == null || (depth || 0) > 6) return properties; 161 if (typeof properties === 'string') return scrubUrl(properties); 162 if (Object.prototype.toString.call(properties) === '[object Array]') { 163 return properties.map(function (value) { return scrub(value, (depth || 0) + 1); }); 164 } 165 if (typeof properties !== 'object') return properties; 166 var clean = {}; 167 for (var name in properties) { 168 if (Object.prototype.hasOwnProperty.call(properties, name)) { 169 clean[name] = scrub(properties[name], (depth || 0) + 1); 170 } 171 } 172 return clean; 173 } 174 175 function hostOf(value) { 176 try { return new URL(value).hostname.toLowerCase().replace(/^www\./, ''); } catch (error) { return ''; } 177 } 178 179 function providerFor(value, providers) { 180 var normalized = value.toLowerCase(); 181 for (var index = 0; index < providers.length; index += 1) { 182 if (normalized.indexOf(providers[index][0]) !== -1) return providers[index][1]; 183 } 184 return ''; 185 } 186 187 function discovery() { 188 var campaignSource = ''; 189 try { campaignSource = (new URL(location.href).searchParams.get('utm_source') || '').toLowerCase(); } catch (error) {} 190 var referrerHost = hostOf(document.referrer); 191 var ai = providerFor(campaignSource || referrerHost, [ 192 ['chatgpt.com', 'chatgpt'], ['openai.com', 'chatgpt'], ['perplexity.ai', 'perplexity'], 193 ['claude.ai', 'claude'], ['copilot.microsoft.com', 'microsoft_copilot'], 194 ['gemini.google.com', 'google_gemini'], ['meta.ai', 'meta_ai'], ['you.com', 'you'] 195 ]); 196 if (ai) return { discovery_channel: 'ai_assistant', discovery_source: ai }; 197 198 var search = providerFor(referrerHost, [ 199 ['google.', 'google'], ['bing.com', 'bing'], ['duckduckgo.com', 'duckduckgo'], 200 ['search.brave.com', 'brave'], ['search.yahoo.com', 'yahoo'] 201 ]); 202 if (search) return { discovery_channel: 'organic_search', discovery_source: search }; 203 var namedReferral = providerFor(referrerHost, [ 204 ['linkedin.com', 'linkedin'], ['lnkd.in', 'linkedin'] 205 ]); 206 if (namedReferral) return { discovery_channel: 'referral', discovery_source: namedReferral }; 207 if (campaignSource) return { discovery_channel: 'campaign', discovery_source: campaignSource.slice(0, 80) }; 208 if (!referrerHost) return { discovery_channel: 'direct', discovery_source: 'direct' }; 209 if (referrerHost === location.hostname.replace(/^www\./, '')) { 210 return { discovery_channel: 'internal', discovery_source: 'socialgravity' }; 211 } 212 return { discovery_channel: 'referral', discovery_source: referrerHost.slice(0, 120) }; 213 } 214 215 var script = document.createElement('script'); 216 script.async = true; 217 script.src = '/ph/static/array.js'; 218 script.onload = function () { 219 if (!window.posthog || !window.posthog.init) return; 220 window.posthog.init(key, { 221 api_host: '/ph', 222 ui_host: 'https://eu.posthog.com', 223 autocapture: true, 224 capture_pageview: false, 225 capture_pageleave: true, 226 capture_heatmaps: false, 227 capture_dead_clicks: false, 228 persistence: 'sessionStorage', 229 person_profiles: 'identified_only', 230 disable_session_recording: true, 231 disable_surveys: true, 232 sanitize_properties: function (properties) { return scrub(properties, 0); } 233 }); 234 if (!window.posthog.get_property('discovery_channel')) window.posthog.register(durableFirstTouch);
235 var group = location.pathname.split('/').filter(Boolean)[0] || 'home'; 236 var routeProperties = { route: location.pathname, route_name: 'public_reading', content_group: group }; 237 window.posthog.register(routeProperties); 238 window.posthog.capture('$pageview', routeProperties); 239 proofReady = true; 240 proofPending.splice(0).forEach(captureProof); 241 toolPending.splice(0).forEach(captureTool); 242 }; 243 document.head.appendChild(script); 244})();
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.