Skip to content
Merged
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
10 changes: 9 additions & 1 deletion src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {

import {
PageEvent,
PAGE_VIEWS_MAX_COUNT,
buildPageEvents,
migrateLegacyPageViewStorage,
loadPageViews,
Expand Down Expand Up @@ -884,12 +885,19 @@ class RoktKit implements KitInterface {
const pageView = buildPageEvent(event);
pageViews.push(pageView);

if (!writePageViews(pageViews)) {
const requested = Math.min(pageViews.length, PAGE_VIEWS_MAX_COUNT);
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';
Expand Down
13 changes: 10 additions & 3 deletions src/pageViewStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,8 +53,15 @@ export function loadPageViews(loggingService: LoggingService | null): PageEvent[
return Array.isArray(stored) ? (stored as PageEvent[]) : [];
}

export function writePageViews(pageViews: PageEvent[]): boolean {
return writeNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD, capPageViews(pageViews));
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 toWrite.length;
}
}
return 0;
}

export function clearPageViews(): void {
Expand Down
64 changes: 61 additions & 3 deletions test/src/pageViewStorage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,77 @@ 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');
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(4);
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(2);
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); // seed storage

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(5);
const stored = loadPageViews(null);
expect(stored).toHaveLength(5);
expect(stored[0].sourceMessageId).toBe('existing-1');
expect(stored[4].sourceMessageId).toBe('new');
});

it('returns 0 when every write attempt fails', () => {
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new DOMException('quota', 'QuotaExceededError');
});
expect(writePageViews([pageView('home')])).toBe(0);
});
});

describe('buildPageEvents', () => {
Expand Down