PageSourceSearch

https://socialgravity.ai/pages-analytics.js

js socialgravity.ai collected 2026-10-05 13:55:55 UTC 11,894 bytes, 244 lines download raw bytes

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.