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-storage-error-fix.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 18 additions & 4 deletions packages/sdk/src/core/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,31 @@ 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. */
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. */
Expand Down
150 changes: 150 additions & 0 deletions packages/sdk/tests/sdk-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down