Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/sdk-session-id-expiry.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 19 additions & 1 deletion packages/sdk/src/core/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof setInterval> | undefined;
let pendingSince = 0;
Expand Down Expand Up @@ -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;
}
Expand Down
54 changes: 53 additions & 1 deletion packages/sdk/tests/sdk-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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();
});

Expand Down Expand Up @@ -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");
Expand Down
Loading