PageSourceSearch

https://plixi.com/js/plixi-posthog.js

js plixi.com collected 2026-09-24 09:16:32 UTC 15,027 bytes, 489 lines download raw bytes

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.