diff --git a/.changeset/sdk-storage-error-fix.md b/.changeset/sdk-storage-error-fix.md new file mode 100644 index 000000000..54d4fb714 --- /dev/null +++ b/.changeset/sdk-storage-error-fix.md @@ -0,0 +1,5 @@ +--- +"@databuddy/sdk": patch +--- + +`getAnonymousId` and `getSessionId` now return `null` instead of throwing when `localStorage` or `sessionStorage` access raises a `DOMException`. Follows the same try/catch pattern already used by `getProfileId`. URL params continue to take priority without touching storage. diff --git a/packages/sdk/src/core/tracker.ts b/packages/sdk/src/core/tracker.ts index cfb18ec64..62576370e 100644 --- a/packages/sdk/src/core/tracker.ts +++ b/packages/sdk/src/core/tracker.ts @@ -160,7 +160,15 @@ export function getAnonymousId(urlParams?: URLSearchParams): string | null { if (typeof window === "undefined") { return null; } - return urlParams?.get("anonId") || localStorage.getItem("did") || null; + const fromParams = urlParams?.get("anonId"); + if (fromParams) { + return fromParams; + } + try { + return localStorage.getItem("did") || null; + } catch { + return null; + } } /** Get current session ID. Priority: URL params → sessionStorage. Resets after 30 min inactivity. */ @@ -168,9 +176,15 @@ export function getSessionId(urlParams?: URLSearchParams): string | null { if (typeof window === "undefined") { return null; } - return ( - urlParams?.get("sessionId") || sessionStorage.getItem("did_session") || null - ); + const fromParams = urlParams?.get("sessionId"); + if (fromParams) { + return fromParams; + } + try { + return sessionStorage.getItem("did_session") || null; + } catch { + return null; + } } /** Get both anonymous ID and session ID in one call. */ diff --git a/packages/sdk/tests/sdk-functions.spec.ts b/packages/sdk/tests/sdk-functions.spec.ts index 904b7683e..d2976a74d 100644 --- a/packages/sdk/tests/sdk-functions.spec.ts +++ b/packages/sdk/tests/sdk-functions.spec.ts @@ -284,6 +284,156 @@ test.describe("SDK Functions", () => { }); }); + test.describe("tracking helpers — storage throws", () => { + test("getAnonymousId() returns null when localStorage throws", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const { getItem } = Storage.prototype; + localStorage.getItem = () => { + throw new DOMException("Access denied", "SecurityError"); + }; + try { + return window.__SDK__.getAnonymousId(); + } finally { + localStorage.getItem = getItem; + } + }); + expect(result).toBeNull(); + }); + + test("getSessionId() returns null when sessionStorage throws", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const { getItem } = Storage.prototype; + sessionStorage.getItem = () => { + throw new DOMException("Access denied", "SecurityError"); + }; + try { + return window.__SDK__.getSessionId(); + } finally { + sessionStorage.getItem = getItem; + } + }); + expect(result).toBeNull(); + }); + + test("getTrackingIds() returns both null when both storages throw", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const original = Storage.prototype.getItem; + Storage.prototype.getItem = () => { + throw new DOMException("Access denied", "SecurityError"); + }; + try { + return window.__SDK__.getTrackingIds(); + } finally { + Storage.prototype.getItem = original; + } + }); + expect(result.anonId).toBeNull(); + expect(result.sessionId).toBeNull(); + }); + + test("getTrackingIds() returns sessionId when only localStorage throws", async ({ + page, + }) => { + const result = await page.evaluate(() => { + sessionStorage.setItem("did_session", "sess-ok"); + const { getItem } = Storage.prototype; + localStorage.getItem = () => { + throw new DOMException("Access denied", "SecurityError"); + }; + try { + return window.__SDK__.getTrackingIds(); + } finally { + localStorage.getItem = getItem; + } + }); + expect(result.anonId).toBeNull(); + expect(result.sessionId).toBe("sess-ok"); + }); + + test("getTrackingIds() returns anonId when only sessionStorage throws", async ({ + page, + }) => { + const result = await page.evaluate(() => { + localStorage.setItem("did", "anon-ok"); + const { getItem } = Storage.prototype; + sessionStorage.getItem = () => { + throw new DOMException("Access denied", "SecurityError"); + }; + try { + return window.__SDK__.getTrackingIds(); + } finally { + sessionStorage.getItem = getItem; + } + }); + expect(result.anonId).toBe("anon-ok"); + expect(result.sessionId).toBeNull(); + }); + + test("getTrackingParams() returns empty string when both storages throw", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const original = Storage.prototype.getItem; + Storage.prototype.getItem = () => { + throw new DOMException("Access denied", "SecurityError"); + }; + try { + return window.__SDK__.getTrackingParams(); + } finally { + Storage.prototype.getItem = original; + } + }); + expect(result).toBe(""); + }); + + test("getTrackingParams() returns partial string when only sessionStorage throws", async ({ + page, + }) => { + const result = await page.evaluate(() => { + localStorage.setItem("did", "anon-partial"); + const { getItem } = Storage.prototype; + sessionStorage.getItem = () => { + throw new DOMException("Access denied", "SecurityError"); + }; + try { + return window.__SDK__.getTrackingParams(); + } finally { + sessionStorage.getItem = getItem; + } + }); + expect(result).toContain("anonId=anon-partial"); + expect(result).not.toContain("sessionId"); + }); + + test("URL param takes priority without touching storage when localStorage throws", async ({ + page, + }) => { + const result = await page.evaluate(() => { + const original = Storage.prototype.getItem; + let storageWasAccessed = false; + Storage.prototype.getItem = () => { + storageWasAccessed = true; + throw new DOMException("Access denied", "SecurityError"); + }; + const params = new URLSearchParams("anonId=anon-from-url"); + try { + const id = window.__SDK__.getAnonymousId(params); + return { id, storageWasAccessed }; + } finally { + Storage.prototype.getItem = original; + } + }); + expect(result.id).toBe("anon-from-url"); + expect(result.storageWasAccessed).toBe(false); + }); + }); + test.describe("getTracker", () => { test("returns null when tracker is not loaded", async ({ page }) => { const result = await page.evaluate(() => window.__SDK__.getTracker());