From 77bbbb110560d77a83985027e74bcaa5dfd20ea2 Mon Sep 17 00:00:00 2001 From: Nick Lathe Date: Fri, 10 Apr 2026 15:51:51 -0700 Subject: [PATCH] Support statsig_stable_id passed as URL param Signed-off-by: Nick Lathe --- .../providers/statsig/statsig-stable-id.ts | 93 ++++++++++++++++++ apps/marketing/tests/e2e/statsig.spec.ts | 97 +++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 apps/marketing/src/providers/statsig/statsig-stable-id.ts diff --git a/apps/marketing/src/providers/statsig/statsig-stable-id.ts b/apps/marketing/src/providers/statsig/statsig-stable-id.ts new file mode 100644 index 00000000..78e4d8de --- /dev/null +++ b/apps/marketing/src/providers/statsig/statsig-stable-id.ts @@ -0,0 +1,93 @@ +/** + * This script enables subdomain tracking for Statsig by managing a stable ID. + * The original script can be found at https://docs.statsig.com/guides/sidecar-experiments/advanced-configurations#persisting-stableid-across-subdomains + * The modifications to this file include: + * 1. Fixes a bug related to port numbers in the domain when setting cookies. + * 2. Uses 'statsig_stable_id' as the cookie name instead of the default 'statsiguuid'. + */ + +const LOCAL_STORAGE_KEY = 'STATSIG_LOCAL_STORAGE_STABLE_ID'; +const COOKIE_NAME = 'statsig_stable_id'; +const URL_PARAM_NAME = 'statsig_stable_id'; +const UUID_V4_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +function generateUUID(): string { + if (crypto && crypto.randomUUID) { + return crypto.randomUUID(); + } + + const hex = () => + Math.floor(65536 * Math.random()) + .toString(16) + .padStart(4, '0'); + + return `${hex()}${hex()}-${hex()}-4${hex().substring(1)}-${hex()}-${hex()}${hex()}${hex()}`; +} + +function setCookie(value: string) { + const expires = new Date(); + expires.setMonth(expires.getMonth() + 12); + + const hostParts = window.location.host.split('.'); + if (hostParts.length > 2) { + hostParts.shift(); + } + // Strip port number from the last part of the domain + hostParts[hostParts.length - 1] = hostParts[hostParts.length - 1].split(':')[0]; + + const domain = `.${hostParts.join('.')}`; + document.cookie = `${COOKIE_NAME}=${value || generateUUID()};Expires=${expires};Domain=${domain};Path=/`; +} + +function isValidUUID(value: string): boolean { + return UUID_V4_REGEX.test(value); +} + +function getStableIdFromUrlParam(): string | null { + const params = new URLSearchParams(window.location.search); + return params.get(URL_PARAM_NAME); +} + +function removeStableIdFromUrl() { + const url = new URL(window.location.href); + url.searchParams.delete(URL_PARAM_NAME); + window.history.replaceState(window.history.state, '', url.toString()); +} + +function getStableIdFromCookie(): string | null { + const match = document.cookie.match(/statsig_stable_id=([\w-]+);?/); + return match ? match[1] : null; +} + +(function () { + // URL parameter takes highest precedence — override everything if valid + const urlParamId = getStableIdFromUrlParam(); + if (urlParamId && isValidUUID(urlParamId)) { + localStorage.setItem(LOCAL_STORAGE_KEY, urlParamId); + setCookie(urlParamId); + removeStableIdFromUrl(); + return; + } + + const localStorageId = localStorage.getItem(LOCAL_STORAGE_KEY) || null; + const cookieId = getStableIdFromCookie(); + + if (cookieId && localStorageId && cookieId === localStorageId) { + // Already in sync, nothing to do + return; + } + + if (cookieId && localStorageId && cookieId !== localStorageId) { + // Cookie takes precedence over localStorage + localStorage.setItem(LOCAL_STORAGE_KEY, cookieId); + } else if (cookieId && !localStorageId) { + // Cookie exists but localStorage doesn't — sync localStorage + localStorage.setItem(LOCAL_STORAGE_KEY, cookieId); + } else { + // No cookie — generate a new ID and set both + const newId = generateUUID(); + localStorage.setItem(LOCAL_STORAGE_KEY, newId); + setCookie(newId); + } +})(); diff --git a/apps/marketing/tests/e2e/statsig.spec.ts b/apps/marketing/tests/e2e/statsig.spec.ts index 9da7ce86..80796785 100644 --- a/apps/marketing/tests/e2e/statsig.spec.ts +++ b/apps/marketing/tests/e2e/statsig.spec.ts @@ -36,6 +36,103 @@ test.describe('Statsig Tests', () => { expect(stableIdLocalStorage).toBe(stableIdCookie); }); + test('should set stable id from URL parameter, overriding existing values', async ({ + page, + }) => { + test.skip(getSiteType() !== 'corporate', 'Only runs on corporate site'); + const allTheThingsPage = new AllTheThingsPage(page, {locale: 'en-US'}); + + // First visit to establish an existing stable id + await allTheThingsPage.goto(); + + const originalStableId = await page.evaluate(() => { + return window.localStorage.getItem('STATSIG_LOCAL_STORAGE_STABLE_ID'); + }); + expect(originalStableId).toBeTruthy(); + + // Visit again with a URL parameter — should override the existing id + const overrideId = '550e8400-e29b-41d4-a716-446655440000'; + await allTheThingsPage.goto(undefined, { + statsig_stable_id: overrideId, + }); + + const stableIdLocalStorage = await page.evaluate(() => { + return window.localStorage.getItem('STATSIG_LOCAL_STORAGE_STABLE_ID'); + }); + const cookies = await page.context().cookies(); + const stableIdCookie = cookies.find( + cookie => cookie.name === 'statsig_stable_id', + )?.value; + + expect(stableIdLocalStorage).toBe(overrideId); + expect(stableIdCookie).toBe(overrideId); + }); + + test('should set stable id from URL parameter when no existing values', async ({ + page, + }) => { + test.skip(getSiteType() !== 'corporate', 'Only runs on corporate site'); + const allTheThingsPage = new AllTheThingsPage(page, {locale: 'en-US'}); + + const overrideId = '550e8400-e29b-41d4-a716-446655440000'; + await allTheThingsPage.goto(undefined, { + statsig_stable_id: overrideId, + }); + + const stableIdLocalStorage = await page.evaluate(() => { + return window.localStorage.getItem('STATSIG_LOCAL_STORAGE_STABLE_ID'); + }); + const cookies = await page.context().cookies(); + const stableIdCookie = cookies.find( + cookie => cookie.name === 'statsig_stable_id', + )?.value; + + expect(stableIdLocalStorage).toBe(overrideId); + expect(stableIdCookie).toBe(overrideId); + }); + + test('should ignore invalid statsig_stable_id URL parameter', async ({ + page, + }) => { + test.skip(getSiteType() !== 'corporate', 'Only runs on corporate site'); + const allTheThingsPage = new AllTheThingsPage(page, {locale: 'en-US'}); + + await allTheThingsPage.goto(undefined, { + statsig_stable_id: 'not-a-valid-uuid', + }); + + const stableIdLocalStorage = await page.evaluate(() => { + return window.localStorage.getItem('STATSIG_LOCAL_STORAGE_STABLE_ID'); + }); + + // Should have generated its own id, not used the invalid one + expect(stableIdLocalStorage).toBeTruthy(); + expect(stableIdLocalStorage).not.toBe('not-a-valid-uuid'); + }); + + test('should remove statsig_stable_id URL parameter after processing', async ({ + page, + }) => { + test.skip(getSiteType() !== 'corporate', 'Only runs on corporate site'); + const allTheThingsPage = new AllTheThingsPage(page, {locale: 'en-US'}); + + const overrideId = '550e8400-e29b-41d4-a716-446655440000'; + await allTheThingsPage.goto(undefined, { + statsig_stable_id: overrideId, + }); + + // Wait for the script to process and remove the param + await page.waitForFunction( + () => !new URLSearchParams(window.location.search).has('statsig_stable_id'), + ); + + const url = new URL(page.url()); + expect(url.searchParams.has('statsig_stable_id')).toBe(false); + + // Other query params (utm_source etc. added by the POM) should still be present + expect(url.searchParams.has('utm_source')).toBe(true); + }); + test('should still initialize even if stable id script is blocked', async ({ page, }) => {