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
6 changes: 2 additions & 4 deletions src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
PageEvent,
PAGE_VIEWS_MAX_COUNT,
buildPageEvents,
migrateLegacyPageViewStorage,
loadPageViews,
writePageViews,
clearPageViews,
Expand Down Expand Up @@ -881,7 +880,7 @@ class RoktKit implements KitInterface {
try {
pageUrl = sanitizeUrl(window.location.href);

const pageViews = loadPageViews(this.loggingService);
const pageViews = loadPageViews();
const pageView = buildPageEvent(event);
pageViews.push(pageView);

Expand Down Expand Up @@ -1263,7 +1262,6 @@ class RoktKit implements KitInterface {
}

if (event.EventDataType === MESSAGE_TYPE_SESSION_END) {
migrateLegacyPageViewStorage(this.loggingService);
clearPageViews();
}
}
Expand Down Expand Up @@ -1483,7 +1481,7 @@ class RoktKit implements KitInterface {
const filteredUserIdentities = this.returnUserIdentities(filteredUser);

const sessionAttributes = this.returnLocalSessionAttributes();
const pageEvents = buildPageEvents(loadPageViews(this.loggingService));
const pageEvents = buildPageEvents(loadPageViews());
const mpSessionId = this.readMpSessionId();
const mpDeviceId = this.readMpDeviceId();

Expand Down
33 changes: 2 additions & 31 deletions src/pageViewStorage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { LoggingService } from './Rokt-Kit';
import { readJSON, removeKey, readNamespacedField, writeNamespacedField, removeNamespacedField } from './storage';
import { readNamespacedField, writeNamespacedField, removeNamespacedField } from './storage';
import { sanitizeUrl } from './utils';

const LS_NAMESPACE_KEY = 'mp-rokt-kit';
const LS_PAGE_VIEWS_FIELD = 'pageViews';
const LEGACY_PAGE_VIEWS_KEY = 'mpPageViews';
export const PAGE_VIEWS_MAX_COUNT = 25;

export interface PageEvent {
Expand All @@ -21,34 +19,7 @@ function capPageViews(views: PageEvent[]): PageEvent[] {
return views.slice(-PAGE_VIEWS_MAX_COUNT);
}

export function migrateLegacyPageViewStorage(loggingService: LoggingService | null): void {
const legacyViews = readJSON(LEGACY_PAGE_VIEWS_KEY);
if (legacyViews === null) {
return;
}

const alreadyMigrated = readNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD) !== undefined;
const needsMigration = !alreadyMigrated && Array.isArray(legacyViews);

if (needsMigration) {
loggingService?.log({
message: 'Rokt Kit: Migrating legacy page-view storage',
code: 'PAGE_VIEW_LEGACY_MIGRATION',
});
const migrated = writeNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD, legacyViews);
if (!migrated) {
loggingService?.log({
message: 'Rokt Kit: Failed to migrate legacy page-view storage [reason: migration_retry]',
code: 'PAGE_VIEW_CAPTURE_FAILED',
});
}
}

removeKey(LEGACY_PAGE_VIEWS_KEY);
}

export function loadPageViews(loggingService: LoggingService | null): PageEvent[] {
migrateLegacyPageViewStorage(loggingService);
export function loadPageViews(): PageEvent[] {
const stored = readNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD);
return Array.isArray(stored) ? (stored as PageEvent[]) : [];
}
Expand Down
96 changes: 9 additions & 87 deletions test/src/pageViewStorage.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { readJSON, writeNamespacedField } from '../../src/storage';
import {
migrateLegacyPageViewStorage,
loadPageViews,
writePageViews,
clearPageViews,
buildPageEvents,
} from '../../src/pageViewStorage';
import type { LoggingService } from '../../src/Rokt-Kit';
import { loadPageViews, writePageViews, clearPageViews, buildPageEvents } from '../../src/pageViewStorage';

const NAMESPACE_KEY = 'mp-rokt-kit';
const PAGE_VIEWS_FIELD = 'pageViews';
const LEGACY_PAGE_VIEWS_KEY = 'mpPageViews';

const pageView = (id: string) => ({ pageUrl: 'https://example.com/' + id, sourceMessageId: id, timestamp: 1 });

Expand All @@ -25,102 +17,32 @@ describe('pageViewStorage', () => {
window.localStorage.clear();
});

describe('migrateLegacyPageViewStorage', () => {
it('is a no-op when the legacy key is absent', () => {
migrateLegacyPageViewStorage(null);
expect(readJSON(NAMESPACE_KEY)).toBeNull();
});

it('moves a legacy array into the namespaced field and removes the legacy key', () => {
window.localStorage.setItem(LEGACY_PAGE_VIEWS_KEY, JSON.stringify([pageView('home')]));
migrateLegacyPageViewStorage(null);

expect(readJSON(NAMESPACE_KEY)).toEqual({ [PAGE_VIEWS_FIELD]: [pageView('home')] });
expect(window.localStorage.getItem(LEGACY_PAGE_VIEWS_KEY)).toBeNull();
});

it('does not overwrite an already-migrated field, but still clears the legacy key', () => {
writeNamespacedField(NAMESPACE_KEY, PAGE_VIEWS_FIELD, [pageView('current')]);
window.localStorage.setItem(LEGACY_PAGE_VIEWS_KEY, JSON.stringify([pageView('stale')]));

migrateLegacyPageViewStorage(null);

expect(readJSON(NAMESPACE_KEY)).toEqual({ [PAGE_VIEWS_FIELD]: [pageView('current')] });
expect(window.localStorage.getItem(LEGACY_PAGE_VIEWS_KEY)).toBeNull();
});

it('logs PAGE_VIEW_LEGACY_MIGRATION when the migration path is taken', () => {
window.localStorage.setItem(LEGACY_PAGE_VIEWS_KEY, JSON.stringify([pageView('home')]));
const logger = { log: vi.fn() } as unknown as LoggingService;

migrateLegacyPageViewStorage(logger);

expect(logger.log).toHaveBeenCalledWith(expect.objectContaining({ code: 'PAGE_VIEW_LEGACY_MIGRATION' }));
});

it('removes the legacy key and logs when the migrating write fails', () => {
window.localStorage.setItem(LEGACY_PAGE_VIEWS_KEY, JSON.stringify([pageView('home')]));
const logger = { log: vi.fn() } as unknown as LoggingService;
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new DOMException('quota', 'QuotaExceededError');
});

migrateLegacyPageViewStorage(logger);

// Legacy key is removed even on failure — prevents the infinite retry loop.
expect(window.localStorage.getItem(LEGACY_PAGE_VIEWS_KEY)).toBeNull();
expect(logger.log).toHaveBeenCalledWith(expect.objectContaining({ code: 'PAGE_VIEW_CAPTURE_FAILED' }));
});

it('does not log PAGE_VIEW_CAPTURE_FAILED on a second call after a failed migration', () => {
window.localStorage.setItem(LEGACY_PAGE_VIEWS_KEY, JSON.stringify([pageView('home')]));
const logger = { log: vi.fn() } as unknown as LoggingService;
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new DOMException('quota', 'QuotaExceededError');
});

migrateLegacyPageViewStorage(logger); // first call: fails, legacy key removed
vi.restoreAllMocks();
logger.log.mockClear();

migrateLegacyPageViewStorage(logger); // second call: legacy key gone, no-op

expect(logger.log).not.toHaveBeenCalled();
});
});

describe('loadPageViews', () => {
it('returns an empty array when nothing is stored', () => {
expect(loadPageViews(null)).toEqual([]);
expect(loadPageViews()).toEqual([]);
});

it('returns the stored page views', () => {
writeNamespacedField(NAMESPACE_KEY, PAGE_VIEWS_FIELD, [pageView('home'), pageView('about')]);
expect(loadPageViews(null)).toEqual([pageView('home'), pageView('about')]);
expect(loadPageViews()).toEqual([pageView('home'), pageView('about')]);
});

it('returns an empty array when the stored value is not an array', () => {
writeNamespacedField(NAMESPACE_KEY, PAGE_VIEWS_FIELD, { not: 'an array' });
expect(loadPageViews(null)).toEqual([]);
});

it('migrates the legacy key before reading', () => {
window.localStorage.setItem(LEGACY_PAGE_VIEWS_KEY, JSON.stringify([pageView('legacy')]));
expect(loadPageViews(null)).toEqual([pageView('legacy')]);
expect(window.localStorage.getItem(LEGACY_PAGE_VIEWS_KEY)).toBeNull();
expect(loadPageViews()).toEqual([]);
});
});

describe('writePageViews', () => {
it('persists the page views and returns the stored count', () => {
expect(writePageViews([pageView('home')])).toBe(1);
expect(loadPageViews(null)).toEqual([pageView('home')]);
expect(loadPageViews()).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(25);
const stored = loadPageViews(null);
const stored = loadPageViews();
expect(stored).toHaveLength(25);
expect(stored[0].sourceMessageId).toBe('page-15');
expect(stored[24].sourceMessageId).toBe('page-39');
Expand All @@ -136,7 +58,7 @@ describe('pageViewStorage', () => {
});

expect(writePageViews(views)).toBe(4);
const stored = loadPageViews(null);
const stored = loadPageViews();
expect(stored).toHaveLength(4);
expect(stored[0].sourceMessageId).toBe('page-1');
expect(stored[3].sourceMessageId).toBe('page-4');
Expand All @@ -152,7 +74,7 @@ describe('pageViewStorage', () => {
});

expect(writePageViews(views)).toBe(2);
const stored = loadPageViews(null);
const stored = loadPageViews();
expect(stored).toHaveLength(2);
expect(stored[0].sourceMessageId).toBe('page-3');
expect(stored[1].sourceMessageId).toBe('page-4');
Expand All @@ -171,7 +93,7 @@ describe('pageViewStorage', () => {
});

expect(writePageViews(updated)).toBe(5);
const stored = loadPageViews(null);
const stored = loadPageViews();
expect(stored).toHaveLength(5);
expect(stored[0].sourceMessageId).toBe('existing-1');
expect(stored[4].sourceMessageId).toBe('new');
Expand Down
Loading