Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions apps/marketing/src/providers/statsig/statsig-stable-id.ts
Original file line number Diff line number Diff line change
@@ -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);
}
})();
97 changes: 97 additions & 0 deletions apps/marketing/tests/e2e/statsig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => {
Expand Down
Loading