1/* PostHog bootstrap. 2 * 3 * Loaded with `defer` immediately after array.js, so window.posthog is already 4 * the real library when this runs -- array.js finishes with `window.posthog = 5 * <instance>` whether or not the queue stub is present, which is why 6 * templates/_posthog.html can skip that stub entirely. 7 * 8 * Config and identity arrive in the #posthog-config JSON tag rather than as 9 * inline JS, so this file stays static-cacheable and carries no per-request 10 * values. */ 11(function () { 12 if (window.__posthogInitialised) return; 13 14 var node = document.getElementById('posthog-config'); 15 if (!node || !window.posthog || typeof window.posthog.init !== 'function') return; 16 17 var cfg; 18 try { 19 cfg = JSON.parse(node.textContent || '{}'); 20 } catch (e) { 21 return; 22 } 23 if (!cfg.apiKey) return; 24 25 window.__posthogInitialised = true; 26 27 /* ---- session replay masking ------------------------------------------- 28 rrweb offers maskTextSelector but NOT unmaskTextSelector, so "mask 29 everything then unmask the safe parts" is not available. A selector list 30 on its own would be an allowlist, and an allowlist fails OPEN: the day 31 someone adds a page that prints an API key, it records in the clear and 32 nothing complains. That is the same failure mode that left datafa.st 33 blocked and Stripe attribution empty for months. 34 35 So the primary defence keys on what text LOOKS like, not where it sits. 36 A new page printing a token is masked on day one without anyone updating 37 a list. The selector list below is only a cheap second layer. 38 39 Deliberately still readable: button labels, headings, nav, and error 40 messages under 240 chars, because those are the whole point of watching a 41 replay to work out why somebody churned. */ 42 var SENSITIVE = new RegExp( 43 '[\\w.+-]+@[\\w-]+\\.[\\w.]{2,}' + '|' + // email addresses 44 '(?:sk|pk|phc|phx|rk|whsec)_[A-Za-z0-9_]{8,}' + '|' + // stripe / posthog style keys (sk_live_⦠keeps the underscore) 45 'Bearer\\s+[A-Za-z0-9._-]{12,}' + '|' + // auth headers 46 'eyJ[A-Za-z0-9._-]{20,}' + '|' + // JWTs 47 'gh[pousr]_[A-Za-z0-9]{20,}' + '|' + // github tokens 48 'xox[baprs]-[A-Za-z0-9-]{10,}' + '|' + // slack tokens 49 'AKIA[0-9A-Z]{12,}' + '|' + // aws access keys 50 '[A-Za-z0-9+/]{32,}={0,2}' // long opaque blobs 51 ); 52 var LONG_PROSE = 240; // article drafts and generated copy 53 54 function stars(text) { 55 // Keep whitespace so the replay still shows real layout, not one blob. 56 return text.replace(/\S/g, '*'); 57 } 58 59 function maskText(text) { 60 if (!text || text.length < 4) return text; 61 if (text.length > LONG_PROSE) return stars(text); 62 if (SENSITIVE.test(text)) return stars(text); 63 return text; 64 } 65 window.__phMaskText = maskText; // exported so it can be tested directly 66 67 window.posthog.init(cfg.apiKey, { 68 api_host: cfg.apiHost, 69 defaults: '2026-05-30', 70 /* Anonymous visitors still send events -- web analytics needs them -- they 71 just don't each get a person profile, which is what burns the quota. */ 72 person_profiles: 'identified_only', 73 session_recording: { 74 maskAllInputs: true, 75 maskTextFn: maskText, 76 maskTextSelector: [ 77 '[class*="api-key"]', '[class*="apikey"]', '[class*="apiKey"]', 78 '[id*="api-key"]', '[id*="apikey"]', '[id*="apiKey"]', 79 '[class*="token"]', '[id*="token"]', 80 '[class*="secret"]', '[id*="secret"]', 81 '[class*="password"]', '[id*="password"]', 82 '.ph-mask' 83 ].join(', ') 84 } 85 }); 86 87 if (cfg.person && cfg.person.distinctId) { 88 window.posthog.identify(cfg.person.distinctId, cfg.person.properties || {}); 89 } else if (typeof window.posthog._isIdentified === 'function' && window.posthog._isIdentified()) { 90 /* The session ended (logout or expiry). Stop attributing this browser to 91 the previous user -- but only when it was actually identified, since 92 resetting on every anonymous pageview would hand each one a fresh id and 93 break anonymous funnels. */ 94 window.posthog.reset(); 95 } 96})();
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.