From 5e753a2a9e5a93b76d4e063289f885567aa7b7bc Mon Sep 17 00:00:00 2001 From: Rishabh Gusain Date: Mon, 31 Aug 2026 00:24:26 +0530 Subject: [PATCH] fix(sdk): respect the 30-minute session expiry in getSessionId --- .changeset/sdk-session-id-expiry.md | 5 +++ packages/sdk/src/core/tracker.ts | 20 ++++++++- packages/sdk/tests/sdk-functions.spec.ts | 54 +++++++++++++++++++++++- 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 .changeset/sdk-session-id-expiry.md diff --git a/.changeset/sdk-session-id-expiry.md b/.changeset/sdk-session-id-expiry.md new file mode 100644 index 0000000000..169f595ce3 --- /dev/null +++ b/.changeset/sdk-session-id-expiry.md @@ -0,0 +1,5 @@ +--- +"@databuddy/sdk": patch +--- + +`getSessionId` now respects the tracker's 30-minute inactivity window: it returns `null` when the stored session has no timestamp or the timestamp is older than 30 minutes, instead of handing back a session id the tracker has already rotated. `getTrackingIds` and `getTrackingParams` inherit the same behavior. URL params still take priority. diff --git a/packages/sdk/src/core/tracker.ts b/packages/sdk/src/core/tracker.ts index 62576370e3..ec2e784898 100644 --- a/packages/sdk/src/core/tracker.ts +++ b/packages/sdk/src/core/tracker.ts @@ -18,6 +18,10 @@ const PENDING_LIMIT = 100; const PENDING_POLL_MS = 150; const PENDING_TIMEOUT_MS = 15_000; +// Must match the tracker's inactivity window: it rotates `did_session` once +// the timestamp is older than this, so a stale id is no longer current. +const SESSION_TIMEOUT_MS = 30 * 60 * 1000; + const pendingCalls: Array<() => void> = []; let pendingPoll: ReturnType | undefined; let pendingSince = 0; @@ -181,7 +185,21 @@ export function getSessionId(urlParams?: URLSearchParams): string | null { return fromParams; } try { - return sessionStorage.getItem("did_session") || null; + const storedId = sessionStorage.getItem("did_session"); + if (!storedId) { + return null; + } + const storedAt = Number.parseInt( + sessionStorage.getItem("did_session_timestamp") ?? "", + 10 + ); + if ( + !Number.isFinite(storedAt) || + Date.now() - storedAt >= SESSION_TIMEOUT_MS + ) { + return null; + } + return storedId; } catch { return null; } diff --git a/packages/sdk/tests/sdk-functions.spec.ts b/packages/sdk/tests/sdk-functions.spec.ts index d2976a74d5..0f79493979 100644 --- a/packages/sdk/tests/sdk-functions.spec.ts +++ b/packages/sdk/tests/sdk-functions.spec.ts @@ -219,14 +219,54 @@ test.describe("SDK Functions", () => { expect(result).toBeNull(); }); - test("returns did_session from sessionStorage", async ({ page }) => { + test("returns did_session when the session is fresh", async ({ page }) => { const result = await page.evaluate(() => { sessionStorage.setItem("did_session", "sess-456"); + sessionStorage.setItem( + "did_session_timestamp", + Date.now().toString() + ); return window.__SDK__.getSessionId(); }); expect(result).toBe("sess-456"); }); + test("returns null when did_session has no timestamp", async ({ page }) => { + const result = await page.evaluate(() => { + sessionStorage.setItem("did_session", "sess-no-timestamp"); + return window.__SDK__.getSessionId(); + }); + expect(result).toBeNull(); + }); + + test("returns null when the session is older than 30 minutes", async ({ + page, + }) => { + const result = await page.evaluate(() => { + sessionStorage.setItem("did_session", "sess-expired"); + sessionStorage.setItem( + "did_session_timestamp", + (Date.now() - 31 * 60 * 1000).toString() + ); + return window.__SDK__.getSessionId(); + }); + expect(result).toBeNull(); + }); + + test("returns the session at exactly the 30 minute boundary as expired", async ({ + page, + }) => { + const result = await page.evaluate(() => { + sessionStorage.setItem("did_session", "sess-boundary"); + sessionStorage.setItem( + "did_session_timestamp", + (Date.now() - 30 * 60 * 1000).toString() + ); + return window.__SDK__.getSessionId(); + }); + expect(result).toBeNull(); + }); + test("prioritizes URL param over sessionStorage", async ({ page }) => { const result = await page.evaluate(() => { sessionStorage.setItem("did_session", "sess-local"); @@ -242,6 +282,10 @@ test.describe("SDK Functions", () => { const result = await page.evaluate(() => { localStorage.setItem("did", "anon-x"); sessionStorage.setItem("did_session", "sess-y"); + sessionStorage.setItem( + "did_session_timestamp", + Date.now().toString() + ); return window.__SDK__.getTrackingIds(); }); expect(result.anonId).toBe("anon-x"); @@ -260,6 +304,10 @@ test.describe("SDK Functions", () => { const result = await page.evaluate(() => { localStorage.setItem("did", "anon-a"); sessionStorage.setItem("did_session", "sess-b"); + sessionStorage.setItem( + "did_session_timestamp", + Date.now().toString() + ); return window.__SDK__.getTrackingParams(); }); @@ -342,6 +390,10 @@ test.describe("SDK Functions", () => { }) => { const result = await page.evaluate(() => { sessionStorage.setItem("did_session", "sess-ok"); + sessionStorage.setItem( + "did_session_timestamp", + Date.now().toString() + ); const { getItem } = Storage.prototype; localStorage.getItem = () => { throw new DOMException("Access denied", "SecurityError");