1/** 2 * plixi-posthog.js â the single client-side PostHog helper. 3 * 4 * Responsibilities (nothing about PostHog identity/experiments should live 5 * anywhere else on the client): 6 * - Load + init the PostHog SDK once, with one shared config. 7 * - Use email as the only identified distinct_id (anonymous id until then). 8 * - Keep experiment variants consistent across events, pages, and domains: 9 * first assignment wins, is persisted, carried forward through identify(), 10 * and re-attached to every event via register() so the value on events 11 * always matches the assigned flag value. 12 * - Optional cross-domain handoff via ?ph=<base64 json>. Entry can be either 13 * domain: if no handoff is present the current domain self-assigns. 14 * - Generic over N experiments â driven by window.PLIXI_EXPERIMENTS. 15 * 16 * Config is read from globals set just before this script (see 17 * views/components/posthog_head.marko): 18 * window.PLIXI_PH_KEY, window.PLIXI_EXPERIMENTS, window.PLIXI_PH_API_HOST, 19 * window.PLIXI_PH_DISTINCT_ID (bootstrap id), window.PLIXI_PH_BOOTSTRAP_VARIANTS. 20 */ 21(function (global) { 22 'use strict'; 23 24 var API_HOST = global.PLIXI_PH_API_HOST || 'https://us.i.posthog.com'; 25 var STORE_KEY = 'plixi_experiments'; 26 var LEGACY_STICKY_KEY = 'ph_sticky_feature_flags'; 27 var HANDOFF_PARAM = 'ph'; 28 29 function experimentKeys() { 30 var keys = global.PLIXI_EXPERIMENTS; 31 if (keys && keys.length) return keys; 32 return []; 33 } 34 35 function isAssigned(value) { 36 return value != null && value !== false && value !== ''; 37 } 38 39 function featureProp(flagKey) { 40 return '$feature/' + flagKey; 41 } 42 43 // ---- storage ------------------------------------------------------------ 44 45 function readStore() { 46 var map = {}; 47 try { 48 map = JSON.parse(global.localStorage.getItem(STORE_KEY) || '{}') || {}; 49 } catch (_err) { 50 map = {}; 51 } 52 // Merge legacy sticky store for continuity with older sessions. 53 try { 54 var legacy = JSON.parse(global.localStorage.getItem(LEGACY_STICKY_KEY) || '{}') || {}; 55 var keys = experimentKeys(); 56 for (var i = 0; i < keys.length; i++) { 57 var k = keys[i]; 58 if (!isAssigned(map[k]) && isAssigned(legacy[k])) map[k] = legacy[k]; 59 } 60 } catch (_err2) { 61 /* ignore */ 62 } 63 return sanitize(map); 64 } 65 66 function sanitize(map) { 67 var out = {}; 68 var keys = experimentKeys(); 69 if (!map || typeof map !== 'object') return out; 70 for (var i = 0; i < keys.length; i++) { 71 var k = keys[i]; 72 if (isAssigned(map[k])) out[k] = map[k]; 73 } 74 return out; 75 } 76 77 function writeStore(map) { 78 var clean = sanitize(map); 79 try { 80 global.localStorage.setItem(STORE_KEY, JSON.stringify(clean)); 81 } catch (_err) { 82 /* ignore quota / private mode */ 83 } 84 // Mirror into legacy keys so existing pricing-display readers keep working. 85 try { 86 global.localStorage.setItem(LEGACY_STICKY_KEY, JSON.stringify(clean)); 87 } catch (_err2) { 88 /* ignore */ 89 } 90 } 91 92 // Persist the generic variant map into a cookie so the server can read it. 93 function writeVariantsCookie(map) { 94 var clean = sanitize(map); 95 if (!Object.keys(clean).length) return; 96 try { 97 var maxAge = 30 * 24 * 60 * 60; 98 var secure = global.location.protocol === 'https:' ? '; Secure' : ''; 99 global.document.cookie = 100 'plixi_experiments=' + 101 encodeURIComponent(JSON.stringify(clean)) + 102 '; path=/; max-age=' + 103 maxAge + 104 '; SameSite=Lax' + 105 secure; 106 } catch (_err) { 107 /* ignore */ 108 } 109 } 110 111 // ---- handoff (cross-domain, optional) ----------------------------------- 112 113 function decodeHandoff() { 114 var out = {}; 115 var params; 116 try { 117 params = new global.URLSearchParams(global.location.search); 118 } catch (_err) { 119 return out; 120 } 121 var ph = params.get(HANDOFF_PARAM); 122 if (ph) { 123 try { 124 var json = global.atob(decodeURIComponent(ph)); 125 var parsed = JSON.parse(json); 126 out = sanitize(parsed); 127 } catch (_err2) { 128 /* ignore malformed handoff */ 129 } 130 } 131 return out; 132 } 133 134 function encodeVariants(map) { 135 var clean = sanitize(map); 136 if (!Object.keys(clean).length) return ''; 137 try { 138 return global.btoa(JSON.stringify(clean)); 139 } catch (_err) { 140 return ''; 141 } 142 } 143 144 // ---- PostHog SDK loader (standard snippet) ------------------------------ 145 146 function loadSdk() { 147 if (global.posthog && global.posthog.__loaded) return; 148 if (global.posthog && global.posthog._i && global.posthog._i.length) return; 149 !(function (t, e) { 150 var o, n, p, r; 151 e.__SV || 152 ((global.posthog = e), 153 (e._i = []), 154 (e.init = function (i, s, a) { 155 function g(t, e) { 156 var o = e.split('.'); 157 2 == o.length && ((t = t[o[0]]), (e = o[1])); 158 t[e] = function () { 159 t.push([e].concat(Array.prototype.slice.call(arguments, 0))); 160 }; 161 } 162 (p = t.createElement('script')).type = 'text/javascript'; 163 p.async = !0; 164 p.src = s.api_host.replace('.i.posthog.com', '-assets.i.posthog.com') + '/static/array.js'; 165 (r = t.getElementsByTagName('script')[0]).parentNode.insertBefore(p, r); 166 var u = e; 167 for ( 168 void 0 !== a ? (u = e[a] = []) : (a = 'posthog'), 169 u.people = u.people || [], 170 u.toString = function (t) { 171 var e = 'posthog'; 172 return 'posthog' !== a && (e += '.' + a), t || (e += ' (stub)'), e; 173 }, 174 u.people.toString = function () { 175 return u.toString(1) + '.people (stub)'; 176 }, 177 o = 178 'capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId'.split( 179 ' ' 180 ), 181 n = 0; 182 n < o.length; 183 n++ 184 ) 185 g(u, o[n]);
186 e._i.push([i, s, a]); 187 }), 188 (e.__SV = 1)); 189 })(global.document, global.posthog || []); 190 } 191 192 // ---- pinning ------------------------------------------------------------ 193 194 // overrideFeatureFlags() re-fires onFeatureFlags in PostHog.js â guard against 195 // pin -> onFeatureFlags -> sync -> pin infinite loops that freeze the page. 196 var isPinning = false; 197 var lastPinnedJson = ''; 198 var lastRegisteredJson = ''; 199 200 // Re-attach every stored experiment variant so all events carry the assigned 201 // value regardless of anonymous -> identified flag re-evaluation. 202 function pin() { 203 var ph = global.posthog; 204 if (!ph || isPinning) return; 205 var store = readStore(); 206 var keys = Object.keys(store); 207 if (!keys.length) return; 208 var json = JSON.stringify(store); 209 210 // register() is safe (does not re-fire onFeatureFlags). Always keep 211 // $feature/<key> on events, but skip if nothing changed. 212 if (json !== lastRegisteredJson) { 213 try { 214 if (typeof ph.register === 'function') { 215 var reg = {}; 216 for (var i = 0; i < keys.length; i++) { 217 reg[featureProp(keys[i])] = store[keys[i]]; 218 } 219 ph.register(reg); 220 lastRegisteredJson = json; 221 } 222 } catch (_err2) { 223 /* ignore */ 224 } 225 } 226 227 // Only override when the map actually changed. PostHog fires 228 // onFeatureFlags after every override call. 229 if (json === lastPinnedJson) return; 230 if (!ph.featureFlags || typeof ph.featureFlags.overrideFeatureFlags !== 'function') { 231 return; 232 } 233 isPinning = true; 234 try { 235 ph.featureFlags.overrideFeatureFlags({ flags: store }); 236 lastPinnedJson = json; 237 } catch (_err) { 238 /* ignore */ 239 } finally { 240 isPinning = false; 241 } 242 } 243 244 // First-assignment-wins: persist any newly resolved experiment flag, then pin. 245 function syncFromResolvedFlags() { 246 if (isPinning) return; 247 var ph = global.posthog; 248 if (!ph || typeof ph.getFeatureFlag !== 'function') return; 249 var store = readStore(); 250 var keys = experimentKeys(); 251 var changed = false; 252 for (var i = 0; i < keys.length; i++) { 253 var key = keys[i]; 254 if (isAssigned(store[key])) continue; // already assigned â never change it 255 var value = ph.getFeatureFlag(key); 256 if (isAssigned(value)) { 257 store[key] = value; 258 changed = true; 259 } 260 } 261 if (changed) { 262 writeStore(store); 263 writeVariantsCookie(store); 264 } 265 pin(); 266 } 267 268 function assignServerFlag(flagKey, variant) { 269 if (!flagKey || !isAssigned(variant)) return; 270 var store = readStore(); 271 store[flagKey] = variant; 272 writeStore(store); 273 writeVariantsCookie(store); 274 pin(); 275 } 276 277 var PRICING_EXPOSURE_PREFIX = 'ph_pricing_exposure_'; 278 279 function reportPricingExperimentExposure(flagKey, variant) { 280 if (!flagKey || !isAssigned(variant)) return; 281 assignServerFlag(flagKey, variant); 282 try { 283 if (global.sessionStorage.getItem(PRICING_EXPOSURE_PREFIX + flagKey)) return; 284 global.sessionStorage.setItem(PRICING_EXPOSURE_PREFIX + flagKey, '1'); 285 } catch (_err) { 286 /* ignore */ 287 } 288 var ph = global.posthog; 289 if (ph && typeof ph.capture === 'function') { 290 var props = {}; 291 props['$feature_flag'] = flagKey; 292 props['$feature_flag_response'] = variant; 293 props[featureProp(flagKey)] = variant; 294 props.origin = 'client'; 295 ph.capture('$feature_flag_called', props); 296 } 297 } 298 299 function syncPricingExperimentFromGlobal() { 300 var exp = global.plixiPricingExperiment; 301 if (!exp || !exp.active || !exp.posthogFlag) return; 302 var variant = exp.posthogRawVariant != null && exp.posthogRawVariant !== '' 303 ? exp.posthogRawVariant 304 : exp.variantKey; 305 reportPricingExperimentExposure(exp.posthogFlag, variant); 306 } 307 308 // ---- public API --------------------------------------------------------- 309 310 var initialized = false; 311 312 function init(options) { 313 options = options || {}; 314 var key = options.PH_KEY || global.PLIXI_PH_KEY; 315 if (!key || initialized) return; 316 initialized = true; 317 318 loadSdk(); 319 320 // Seed: handoff (?ph=) -> existing store -> server bootstrap globals. 321 var store = readStore(); 322 var handoff = decodeHandoff(); 323 var keys = experimentKeys(); 324 var k, i; 325 for (i = 0; i < keys.length; i++) { 326 k = keys[i]; 327 if (!isAssigned(store[k]) && isAssigned(handoff[k])) store[k] = handoff[k]; 328 } 329 var bootstrapVariants = options.bootstrapVariants || global.PLIXI_PH_BOOTSTRAP_VARIANTS || {}; 330 for (i = 0; i < keys.length; i++) { 331 k = keys[i]; 332 if (!isAssigned(store[k]) && isAssigned(bootstrapVariants[k])) store[k] = bootstrapVariants[k]; 333 } 334 if (Object.keys(store).length) { 335 writeStore(store); 336 writeVariantsCookie(store); 337 } 338 339 var config = { 340 api_host: API_HOST, 341 person_profiles: 'identified_only', 342 capture_pageview: true, 343 capture_pageleave: false, 344 // Wrap identify() so ANY caller (this helper, checkout templates, or 345 // future code calling posthog.identify directly) pins the experiment 346 // variants around the identity change. This is what guarantees a single 347 // variant from anonymous -> email: identify() can never silently 348 // re-bucket the flag to the email's raw hash and desync from the value 349 // the guest was already assigned. 350 loaded: function (ph) { 351 try { 352 if (ph.__plixiIdentifyWrapped) return; 353 ph.__plixiIdentifyWrapped = true; 354 var origIdentify = ph.identify ? ph.identify.bind(ph) : null; 355 if (!origIdentify) return; 356 ph.identify = function () { 357 // Snapshot the still-anonymous variant first (first-assignment 358 // -wins), then pin it so identify() inherits it instead of 359 // re-bucketing to the email's hash.
360 syncFromResolvedFlags(); 361 pin(); 362 var result; 363 try { 364 result = origIdentify.apply(null, arguments); 365 } finally { 366 pin(); 367 global.setTimeout(pin, 0); 368 global.setTimeout(syncFromResolvedFlags, 300); 369 } 370 return result; 371 }; 372 } catch (_err) { 373 /* ignore */ 374 } 375 }, 376 }; 377 378 var bootstrapDistinctId = options.distinctId || global.PLIXI_PH_DISTINCT_ID || ''; 379 var bootstrap = {}; 380 if (bootstrapDistinctId) { 381 bootstrap.distinctId = bootstrapDistinctId; 382 bootstrap.isIdentifiedID = String(bootstrapDistinctId).indexOf('@') !== -1; 383 } 384 if (Object.keys(store).length) { 385 bootstrap.featureFlags = store; 386 } 387 if (Object.keys(bootstrap).length) { 388 config.bootstrap = bootstrap; 389 } 390 391 global.posthog.init(key, config); 392 393 if (typeof global.posthog.onFeatureFlags === 'function') { 394 global.posthog.onFeatureFlags(function () { 395 syncFromResolvedFlags(); 396 }); 397 } 398 // Pin immediately (bootstrap value is available synchronously) and shortly after. 399 pin(); 400 global.setTimeout(syncFromResolvedFlags, 0); 401 global.setTimeout(syncFromResolvedFlags, 300); 402 } 403 404 function normalizeEmail(email) { 405 return String(email || '').trim().toLowerCase(); 406 } 407 408 // Email is the only identity. Re-pin variants around identify so the 409 // email re-hash can never change the value emitted on events. 410 function identify(email, properties) { 411 var normalized = normalizeEmail(email); 412 if (!normalized || normalized.indexOf('@') === -1) return false; 413 if (!global.posthog || typeof global.posthog.identify !== 'function') return false; 414 pin(); 415 global.posthog.identify(normalized, properties || {}); 416 pin(); 417 global.setTimeout(pin, 0); 418 global.setTimeout(syncFromResolvedFlags, 300); 419 return true; 420 } 421 422 function getVariant(flagKey) { 423 var store = readStore(); 424 if (isAssigned(store[flagKey])) return store[flagKey]; 425 var ph = global.posthog; 426 if (ph && typeof ph.getFeatureFlag === 'function') { 427 var live = ph.getFeatureFlag(flagKey); 428 if (isAssigned(live)) return live; 429 } 430 return null; 431 } 432 433 function featureProperties() { 434 var store = readStore(); 435 var props = {}; 436 for (var k in store) { 437 if (Object.prototype.hasOwnProperty.call(store, k)) { 438 props[featureProp(k)] = store[k]; 439 } 440 } 441 return props; 442 } 443 444 function capture(event, properties) { 445 if (!global.posthog || typeof global.posthog.capture !== 'function') return false; 446 var props = properties ? Object.assign({}, properties) : {}; 447 var flags = featureProperties(); 448 for (var k in flags) { 449 if (Object.prototype.hasOwnProperty.call(flags, k) && props[k] == null) { 450 props[k] = flags[k]; 451 } 452 } 453 global.posthog.capture(event, props); 454 return true; 455 } 456 457 // Append the current variant map to a URL for a cross-domain link. 458 function decorateUrl(url) { 459 var encoded = encodeVariants(readStore()); 460 if (!encoded) return url; 461 try { 462 var u = new global.URL(url, global.location.href); 463 u.searchParams.set(HANDOFF_PARAM, encoded); 464 return u.toString(); 465 } catch (_err) { 466 var sep = url.indexOf('?') === -1 ? '?' : '&'; 467 return url + sep + HANDOFF_PARAM + '=' + encodeURIComponent(encoded); 468 } 469 } 470 471 global.plixiPosthog = { 472 init: init, 473 identify: identify, 474 getVariant: getVariant, 475 capture: capture, 476 decorateUrl: decorateUrl, 477 featureProperties: featureProperties, 478 pin: pin, 479 assignServerFlag: assignServerFlag, 480 reportPricingExperimentExposure: reportPricingExperimentExposure, 481 syncPricingExperimentFromGlobal: syncPricingExperimentFromGlobal, 482 _readStore: readStore, 483 }; 484 485 // Auto-init when config globals are present (marko partial sets them first). 486 if (global.PLIXI_PH_KEY) { 487 init(); 488 } 489})(window);
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.