From 3ca27d77fedf923f2bb72830682d1263917c3be1 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 15:32:57 -0400 Subject: [PATCH 1/3] fix: retry writePageViews with fewer records on quota failure When localStorage is full, evict the oldest page-view records one by one and retry until the write succeeds or no records remain. Preserves the most-recent data under quota pressure instead of discarding the entire write. --- src/pageViewStorage.ts | 9 ++++- test/src/pageViewStorage.spec.ts | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/pageViewStorage.ts b/src/pageViewStorage.ts index 3a041be..bb10003 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -54,7 +54,14 @@ export function loadPageViews(loggingService: LoggingService | null): PageEvent[ } export function writePageViews(pageViews: PageEvent[]): boolean { - return writeNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD, capPageViews(pageViews)); + let toWrite = capPageViews(pageViews); + while (toWrite.length > 0) { + if (writeNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD, toWrite)) { + return true; + } + toWrite = toWrite.slice(1); + } + return false; } export function clearPageViews(): void { diff --git a/test/src/pageViewStorage.spec.ts b/test/src/pageViewStorage.spec.ts index 84af00c..18d375b 100644 --- a/test/src/pageViewStorage.spec.ts +++ b/test/src/pageViewStorage.spec.ts @@ -125,6 +125,64 @@ describe('pageViewStorage', () => { expect(stored[0].sourceMessageId).toBe('page-15'); expect(stored[24].sourceMessageId).toBe('page-39'); }); + + it('evicts oldest records and retries when the initial write fails due to quota', () => { + const views = Array.from({ length: 5 }, (_, i) => pageView('page-' + i)); + const original = Storage.prototype.setItem; + let calls = 0; + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(function (this: Storage, key: string, value: string) { + if (++calls === 1) throw new DOMException('quota', 'QuotaExceededError'); + original.call(this, key, value); + }); + + expect(writePageViews(views)).toBe(true); + const stored = loadPageViews(null); + expect(stored).toHaveLength(4); + expect(stored[0].sourceMessageId).toBe('page-1'); + expect(stored[3].sourceMessageId).toBe('page-4'); + }); + + it('evicts oldest records across multiple failed writes until one succeeds', () => { + const views = Array.from({ length: 5 }, (_, i) => pageView('page-' + i)); + const original = Storage.prototype.setItem; + let calls = 0; + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(function (this: Storage, key: string, value: string) { + if (++calls <= 3) throw new DOMException('quota', 'QuotaExceededError'); + original.call(this, key, value); + }); + + expect(writePageViews(views)).toBe(true); + const stored = loadPageViews(null); + expect(stored).toHaveLength(2); + expect(stored[0].sourceMessageId).toBe('page-3'); + expect(stored[1].sourceMessageId).toBe('page-4'); + }); + + it('evicts the oldest record from pre-existing storage when quota is tight on the next write', () => { + const existing = Array.from({ length: 5 }, (_, i) => pageView('existing-' + i)); + writePageViews(existing); + + const updated = [...existing, pageView('new')]; + const original = Storage.prototype.setItem; + let calls = 0; + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(function (this: Storage, key: string, value: string) { + if (++calls === 1) throw new DOMException('quota', 'QuotaExceededError'); + original.call(this, key, value); + }); + + expect(writePageViews(updated)).toBe(true); + const stored = loadPageViews(null); + expect(stored).toHaveLength(5); + expect(stored[0].sourceMessageId).toBe('existing-1'); + expect(stored[4].sourceMessageId).toBe('new'); + }); + + it('returns false when every write attempt fails', () => { + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new DOMException('quota', 'QuotaExceededError'); + }); + expect(writePageViews([pageView('home')])).toBe(false); + }); }); describe('buildPageEvents', () => { From 980b58e1b20168044a0dc69da999b730bbe7d591 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 15:48:31 -0400 Subject: [PATCH 2/3] refactor: use for-loop with index for writePageViews eviction retry --- src/pageViewStorage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pageViewStorage.ts b/src/pageViewStorage.ts index bb10003..c9b9fd8 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -54,12 +54,12 @@ export function loadPageViews(loggingService: LoggingService | null): PageEvent[ } export function writePageViews(pageViews: PageEvent[]): boolean { - let toWrite = capPageViews(pageViews); - while (toWrite.length > 0) { + const views = capPageViews(pageViews); + for (let i = 0; i < views.length; i++) { + const toWrite = views.slice(i); if (writeNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD, toWrite)) { return true; } - toWrite = toWrite.slice(1); } return false; } From 999cb42b5b2397ceae5f45ba0c8ad8cd13f3e4fa Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 15:59:15 -0400 Subject: [PATCH 3/3] refactor: writePageViews returns stored count instead of boolean --- src/Rokt-Kit.ts | 2 +- src/pageViewStorage.ts | 6 +++--- test/src/pageViewStorage.spec.ts | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 1ff847f..cb93564 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -884,7 +884,7 @@ class RoktKit implements KitInterface { const pageView = buildPageEvent(event); pageViews.push(pageView); - if (!writePageViews(pageViews)) { + if (writePageViews(pageViews) === 0) { const reason = isLocalStorageAvailable() ? 'quota' : 'ls_unavailable'; this.loggingService?.log({ message: `Rokt Kit: Failed to persist page view for ${pageUrl} [reason: ${reason}]`, diff --git a/src/pageViewStorage.ts b/src/pageViewStorage.ts index c9b9fd8..d8fffc4 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -53,15 +53,15 @@ export function loadPageViews(loggingService: LoggingService | null): PageEvent[ return Array.isArray(stored) ? (stored as PageEvent[]) : []; } -export function writePageViews(pageViews: PageEvent[]): boolean { +export function writePageViews(pageViews: PageEvent[]): number { const views = capPageViews(pageViews); for (let i = 0; i < views.length; i++) { const toWrite = views.slice(i); if (writeNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD, toWrite)) { - return true; + return toWrite.length; } } - return false; + return 0; } export function clearPageViews(): void { diff --git a/test/src/pageViewStorage.spec.ts b/test/src/pageViewStorage.spec.ts index 18d375b..0de3b30 100644 --- a/test/src/pageViewStorage.spec.ts +++ b/test/src/pageViewStorage.spec.ts @@ -112,14 +112,14 @@ describe('pageViewStorage', () => { }); describe('writePageViews', () => { - it('persists the page views and returns true', () => { - expect(writePageViews([pageView('home')])).toBe(true); + it('persists the page views and returns the stored count', () => { + expect(writePageViews([pageView('home')])).toBe(1); expect(loadPageViews(null)).toEqual([pageView('home')]); }); it('keeps only the 25 most-recent views when given more than 25', () => { const views = Array.from({ length: 40 }, (_, i) => pageView('page-' + i)); - expect(writePageViews(views)).toBe(true); + expect(writePageViews(views)).toBe(25); const stored = loadPageViews(null); expect(stored).toHaveLength(25); expect(stored[0].sourceMessageId).toBe('page-15'); @@ -135,7 +135,7 @@ describe('pageViewStorage', () => { original.call(this, key, value); }); - expect(writePageViews(views)).toBe(true); + expect(writePageViews(views)).toBe(4); const stored = loadPageViews(null); expect(stored).toHaveLength(4); expect(stored[0].sourceMessageId).toBe('page-1'); @@ -151,7 +151,7 @@ describe('pageViewStorage', () => { original.call(this, key, value); }); - expect(writePageViews(views)).toBe(true); + expect(writePageViews(views)).toBe(2); const stored = loadPageViews(null); expect(stored).toHaveLength(2); expect(stored[0].sourceMessageId).toBe('page-3'); @@ -160,7 +160,7 @@ describe('pageViewStorage', () => { it('evicts the oldest record from pre-existing storage when quota is tight on the next write', () => { const existing = Array.from({ length: 5 }, (_, i) => pageView('existing-' + i)); - writePageViews(existing); + writePageViews(existing); // seed storage const updated = [...existing, pageView('new')]; const original = Storage.prototype.setItem; @@ -170,7 +170,7 @@ describe('pageViewStorage', () => { original.call(this, key, value); }); - expect(writePageViews(updated)).toBe(true); + expect(writePageViews(updated)).toBe(5); const stored = loadPageViews(null); expect(stored).toHaveLength(5); expect(stored[0].sourceMessageId).toBe('existing-1'); @@ -181,7 +181,7 @@ describe('pageViewStorage', () => { vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new DOMException('quota', 'QuotaExceededError'); }); - expect(writePageViews([pageView('home')])).toBe(false); + expect(writePageViews([pageView('home')])).toBe(0); }); });