From 3ca27d77fedf923f2bb72830682d1263917c3be1 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 15:32:57 -0400 Subject: [PATCH 1/6] 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 ac4e465667e82091894f5362d35c241ad9ba55e1 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 15:33:01 -0400 Subject: [PATCH 2/6] feat: log PAGE_VIEW_QUOTA_EVICTION when page-view storage is reduced under quota pressure Emits a Datadog-visible log when writePageViews succeeds after evicting records, making quota-pressure eviction visible without requiring a full storage failure. Allows measuring the effectiveness of the eviction retry. --- src/Rokt-Kit.ts | 2 +- src/pageViewStorage.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 1ff847f..47ab6bd 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, this.loggingService)) { 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 bb10003..2dcd178 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -53,10 +53,17 @@ 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[], loggingService: LoggingService | null): boolean { let toWrite = capPageViews(pageViews); + const requested = toWrite.length; while (toWrite.length > 0) { if (writeNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD, toWrite)) { + if (toWrite.length < requested) { + loggingService?.log({ + message: `Rokt Kit: Page view storage reduced from ${requested} to ${toWrite.length} record(s) under quota pressure [reason: quota_eviction]`, + code: 'PAGE_VIEW_QUOTA_EVICTION', + }); + } return true; } toWrite = toWrite.slice(1); From 3d1149f832a90d066805e474bd2dcf59241f3ff6 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 15:34:18 -0400 Subject: [PATCH 3/6] test: add PAGE_VIEW_QUOTA_EVICTION logging tests --- test/src/pageViewStorage.spec.ts | 40 ++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/test/src/pageViewStorage.spec.ts b/test/src/pageViewStorage.spec.ts index 18d375b..6b81541 100644 --- a/test/src/pageViewStorage.spec.ts +++ b/test/src/pageViewStorage.spec.ts @@ -113,13 +113,13 @@ describe('pageViewStorage', () => { describe('writePageViews', () => { it('persists the page views and returns true', () => { - expect(writePageViews([pageView('home')])).toBe(true); + expect(writePageViews([pageView('home')], null)).toBe(true); 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, null)).toBe(true); 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, null)).toBe(true); 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, null)).toBe(true); 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, null); 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, null)).toBe(true); const stored = loadPageViews(null); expect(stored).toHaveLength(5); expect(stored[0].sourceMessageId).toBe('existing-1'); @@ -181,7 +181,33 @@ describe('pageViewStorage', () => { vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new DOMException('quota', 'QuotaExceededError'); }); - expect(writePageViews([pageView('home')])).toBe(false); + expect(writePageViews([pageView('home')], null)).toBe(false); + }); + + it('does not log when the write succeeds on the first attempt', () => { + const logger = { log: vi.fn() } as unknown as LoggingService; + expect(writePageViews([pageView('home')], logger)).toBe(true); + expect(logger.log).not.toHaveBeenCalled(); + }); + + it('logs PAGE_VIEW_QUOTA_EVICTION with before/after counts when eviction occurs', () => { + const views = Array.from({ length: 5 }, (_, i) => pageView('page-' + i)); + const logger = { log: vi.fn() } as unknown as LoggingService; + 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, logger)).toBe(true); + expect(logger.log).toHaveBeenCalledOnce(); + expect(logger.log).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'PAGE_VIEW_QUOTA_EVICTION', + message: expect.stringContaining('reduced from 5 to 4'), + }), + ); }); }); From d08641d311c8e4d948f1f52096d75c9b4a8f649e Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 15:48:32 -0400 Subject: [PATCH 4/6] refactor: use for-loop with index for writePageViews eviction retry --- src/pageViewStorage.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pageViewStorage.ts b/src/pageViewStorage.ts index 2dcd178..0b99334 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -54,19 +54,18 @@ export function loadPageViews(loggingService: LoggingService | null): PageEvent[ } export function writePageViews(pageViews: PageEvent[], loggingService: LoggingService | null): boolean { - let toWrite = capPageViews(pageViews); - const requested = toWrite.length; - 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)) { - if (toWrite.length < requested) { + if (i > 0) { loggingService?.log({ - message: `Rokt Kit: Page view storage reduced from ${requested} to ${toWrite.length} record(s) under quota pressure [reason: quota_eviction]`, + message: `Rokt Kit: Page view storage reduced from ${views.length} to ${toWrite.length} record(s) under quota pressure [reason: quota_eviction]`, code: 'PAGE_VIEW_QUOTA_EVICTION', }); } return true; } - toWrite = toWrite.slice(1); } return false; } From eb4b5c66ed17928567ea322272c9bf1290a5e572 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 15:59:15 -0400 Subject: [PATCH 5/6] refactor: writePageViews returns stored count; log eviction at call site --- src/Rokt-Kit.ts | 9 ++++++- src/pageViewStorage.ts | 12 +++------ test/src/pageViewStorage.spec.ts | 44 +++++++------------------------- 3 files changed, 20 insertions(+), 45 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 47ab6bd..cd8cb9a 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -884,12 +884,19 @@ class RoktKit implements KitInterface { const pageView = buildPageEvent(event); pageViews.push(pageView); - if (!writePageViews(pageViews, this.loggingService)) { + const requested = pageViews.length; + const stored = writePageViews(pageViews); + if (stored === 0) { const reason = isLocalStorageAvailable() ? 'quota' : 'ls_unavailable'; this.loggingService?.log({ message: `Rokt Kit: Failed to persist page view for ${pageUrl} [reason: ${reason}]`, code: 'PAGE_VIEW_CAPTURE_FAILED', }); + } else if (stored < requested) { + this.loggingService?.log({ + message: `Rokt Kit: Page view storage reduced from ${requested} to ${stored} record(s) under quota pressure [reason: quota_eviction]`, + code: 'PAGE_VIEW_QUOTA_EVICTION', + }); } } catch (err) { const reason = isLocalStorageAvailable() ? 'exception' : 'ls_unavailable'; diff --git a/src/pageViewStorage.ts b/src/pageViewStorage.ts index 0b99334..d8fffc4 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -53,21 +53,15 @@ export function loadPageViews(loggingService: LoggingService | null): PageEvent[ return Array.isArray(stored) ? (stored as PageEvent[]) : []; } -export function writePageViews(pageViews: PageEvent[], loggingService: LoggingService | null): 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)) { - if (i > 0) { - loggingService?.log({ - message: `Rokt Kit: Page view storage reduced from ${views.length} to ${toWrite.length} record(s) under quota pressure [reason: quota_eviction]`, - code: 'PAGE_VIEW_QUOTA_EVICTION', - }); - } - 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 6b81541..049c76a 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')], null)).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, null)).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, null)).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, null)).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, null); + writePageViews(existing); // seed storage const updated = [...existing, pageView('new')]; const original = Storage.prototype.setItem; @@ -170,44 +170,18 @@ describe('pageViewStorage', () => { original.call(this, key, value); }); - expect(writePageViews(updated, null)).toBe(true); + expect(writePageViews(updated)).toBe(5); 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', () => { + it('returns 0 when every write attempt fails', () => { vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new DOMException('quota', 'QuotaExceededError'); }); - expect(writePageViews([pageView('home')], null)).toBe(false); - }); - - it('does not log when the write succeeds on the first attempt', () => { - const logger = { log: vi.fn() } as unknown as LoggingService; - expect(writePageViews([pageView('home')], logger)).toBe(true); - expect(logger.log).not.toHaveBeenCalled(); - }); - - it('logs PAGE_VIEW_QUOTA_EVICTION with before/after counts when eviction occurs', () => { - const views = Array.from({ length: 5 }, (_, i) => pageView('page-' + i)); - const logger = { log: vi.fn() } as unknown as LoggingService; - 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, logger)).toBe(true); - expect(logger.log).toHaveBeenCalledOnce(); - expect(logger.log).toHaveBeenCalledWith( - expect.objectContaining({ - code: 'PAGE_VIEW_QUOTA_EVICTION', - message: expect.stringContaining('reduced from 5 to 4'), - }), - ); + expect(writePageViews([pageView('home')])).toBe(0); }); }); From 4680a77e9fd95344f8c37aed1d82b88e40d6f1e1 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 19 Aug 2026 16:09:13 -0400 Subject: [PATCH 6/6] fix: cap requested count at PAGE_VIEWS_MAX_COUNT to avoid false-positive quota_eviction logs --- src/Rokt-Kit.ts | 3 ++- src/pageViewStorage.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index cd8cb9a..52beb8f 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -26,6 +26,7 @@ import { import { PageEvent, + PAGE_VIEWS_MAX_COUNT, buildPageEvents, migrateLegacyPageViewStorage, loadPageViews, @@ -884,7 +885,7 @@ class RoktKit implements KitInterface { const pageView = buildPageEvent(event); pageViews.push(pageView); - const requested = pageViews.length; + const requested = Math.min(pageViews.length, PAGE_VIEWS_MAX_COUNT); const stored = writePageViews(pageViews); if (stored === 0) { const reason = isLocalStorageAvailable() ? 'quota' : 'ls_unavailable'; diff --git a/src/pageViewStorage.ts b/src/pageViewStorage.ts index d8fffc4..81396f6 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -5,7 +5,7 @@ import { sanitizeUrl } from './utils'; const LS_NAMESPACE_KEY = 'mp-rokt-kit'; const LS_PAGE_VIEWS_FIELD = 'pageViews'; const LEGACY_PAGE_VIEWS_KEY = 'mpPageViews'; -const PAGE_VIEWS_MAX_COUNT = 25; +export const PAGE_VIEWS_MAX_COUNT = 25; export interface PageEvent { pageUrl: string;