From c3470075969546bc972a7c6f9600a503c5df1555 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Thu, 20 Aug 2026 16:35:49 -0400 Subject: [PATCH 1/3] feat: capture UTM params from page views and forward to selectPlacements --- src/Rokt-Kit.ts | 8 ++++ src/pageViewStorage.ts | 51 +++++++++++++++++++- test/src/pageViewStorage.spec.ts | 80 ++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index d3b5b9b..0aadb69 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -32,6 +32,9 @@ import { writePageViews, clearPageViews, readCanonicalUrl, + captureUtmParams, + loadUtmParams, + clearUtmParams, } from './pageViewStorage'; import { isLocalStorageAvailable } from './storage'; @@ -877,6 +880,7 @@ class RoktKit implements KitInterface { try { pageUrl = sanitizeUrl(window.location.href); + captureUtmParams(this.loggingService); const pageViews = loadPageViews(this.loggingService); const pageView = buildPageEvent(event); @@ -1158,6 +1162,7 @@ class RoktKit implements KitInterface { if (this.isTargetingDisabled()) { try { clearPageViews(); + clearUtmParams(); } catch (err) { this.errorReportingService?.report({ message: 'Rokt Kit: Failed to clear page views when targeting is disabled', @@ -1247,6 +1252,7 @@ class RoktKit implements KitInterface { if (event.EventDataType === MESSAGE_TYPE_SESSION_END) { migrateLegacyPageViewStorage(this.loggingService); clearPageViews(); + clearUtmParams(); } } @@ -1466,6 +1472,7 @@ class RoktKit implements KitInterface { const sessionAttributes = this.returnLocalSessionAttributes(); const pageEvents = buildPageEvents(loadPageViews(this.loggingService)); + const utmParams = loadUtmParams(); const mpSessionId = this.readMpSessionId(); const selectPlacementsAttributes: Record = { @@ -1474,6 +1481,7 @@ class RoktKit implements KitInterface { ...optimizelyAttributes, ...sessionAttributes, ...(pageEvents.length ? { [PAGE_EVENTS_KEY]: JSON.stringify(pageEvents) } : {}), + ...(utmParams ? { page_view_attributes: utmParams } : {}), ...(this.userIdentifiedInWorkspace ? { [USER_IDENTIFIED_IN_WORKSPACE_KEY]: true } : {}), ...(mpSessionId ? { [MPARTICLE_SESSION_ID_KEY]: mpSessionId } : {}), mpid, diff --git a/src/pageViewStorage.ts b/src/pageViewStorage.ts index c06a8a3..7a9ca3c 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -1,12 +1,24 @@ import type { LoggingService } from './Rokt-Kit'; -import { readJSON, removeKey, readNamespacedField, writeNamespacedField, removeNamespacedField } from './storage'; +import { + readJSON, + removeKey, + readNamespacedField, + writeNamespacedField, + removeNamespacedField, + isLocalStorageAvailable, +} from './storage'; import { sanitizeUrl } from './utils'; const LS_NAMESPACE_KEY = 'mp-rokt-kit'; const LS_PAGE_VIEWS_FIELD = 'pageViews'; +const LS_UTM_PARAMS_FIELD = 'utmParams'; const LEGACY_PAGE_VIEWS_KEY = 'mpPageViews'; const PAGE_VIEWS_MAX_COUNT = 25; +const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'] as const; +type UtmKey = (typeof UTM_KEYS)[number]; +export type UtmParams = Partial>; + export interface PageEvent { pageUrl: string; sourceMessageId: string; @@ -79,6 +91,43 @@ export function buildPageEvents(pageViews: PageEvent[]): PageEvent[] { }); } +export function captureUtmParams(loggingService: LoggingService | null): void { + if (readNamespacedField(LS_NAMESPACE_KEY, LS_UTM_PARAMS_FIELD) !== undefined) { + return; + } + const search = new URLSearchParams(window.location.search); + const params: UtmParams = {}; + for (const key of UTM_KEYS) { + const value = search.get(key); + if (value) params[key] = value; + } + if (Object.keys(params).length === 0) { + return; + } + const captured = Object.keys(params).join(', '); + if (!writeNamespacedField(LS_NAMESPACE_KEY, LS_UTM_PARAMS_FIELD, params)) { + const reason = isLocalStorageAvailable() ? 'quota' : 'ls_unavailable'; + loggingService?.log({ + message: `Rokt Kit: Failed to persist UTM params [reason: ${reason}]`, + code: 'UTM_CAPTURE_FAILED', + }); + return; + } + loggingService?.log({ + message: `Rokt Kit: Captured UTM params [${captured}]`, + code: 'UTM_CAPTURE_SUCCESS', + }); +} + +export function loadUtmParams(): UtmParams | null { + const stored = readNamespacedField(LS_NAMESPACE_KEY, LS_UTM_PARAMS_FIELD); + return stored !== undefined && stored !== null && typeof stored === 'object' ? (stored as UtmParams) : null; +} + +export function clearUtmParams(): void { + removeNamespacedField(LS_NAMESPACE_KEY, LS_UTM_PARAMS_FIELD); +} + export function readCanonicalUrl(): string | undefined { const link = document.querySelector('link[rel="canonical"]'); const href = link?.href; diff --git a/test/src/pageViewStorage.spec.ts b/test/src/pageViewStorage.spec.ts index 325eb48..61edaa6 100644 --- a/test/src/pageViewStorage.spec.ts +++ b/test/src/pageViewStorage.spec.ts @@ -6,13 +6,21 @@ import { writePageViews, clearPageViews, buildPageEvents, + captureUtmParams, + loadUtmParams, + clearUtmParams, } from '../../src/pageViewStorage'; import type { LoggingService } from '../../src/Rokt-Kit'; const NAMESPACE_KEY = 'mp-rokt-kit'; const PAGE_VIEWS_FIELD = 'pageViews'; +const UTM_PARAMS_FIELD = 'utmParams'; const LEGACY_PAGE_VIEWS_KEY = 'mpPageViews'; +function stubSearch(search: string): void { + vi.stubGlobal('location', { ...window.location, search }); +} + const pageView = (id: string) => ({ pageUrl: 'https://example.com/' + id, sourceMessageId: id, timestamp: 1 }); describe('pageViewStorage', () => { @@ -146,4 +154,76 @@ describe('pageViewStorage', () => { expect(blob).toHaveProperty('unrelatedField', 'keep-me'); }); }); + + describe('captureUtmParams', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('stores present UTM params on first call', () => { + stubSearch('?utm_source=google&utm_medium=cpc&utm_campaign=spring'); + captureUtmParams(null); + expect(readJSON(NAMESPACE_KEY)).toHaveProperty(UTM_PARAMS_FIELD, { + utm_source: 'google', + utm_medium: 'cpc', + utm_campaign: 'spring', + }); + }); + + it('only stores the keys that are present', () => { + stubSearch('?utm_source=email'); + captureUtmParams(null); + const stored = readJSON(NAMESPACE_KEY) as Record; + expect(stored[UTM_PARAMS_FIELD]).toEqual({ utm_source: 'email' }); + }); + + it('does nothing when no UTM params are in the URL', () => { + stubSearch('?unrelated=value'); + captureUtmParams(null); + expect(readJSON(NAMESPACE_KEY)).toBeNull(); + }); + + it('does nothing when the URL has no query string', () => { + stubSearch(''); + captureUtmParams(null); + expect(readJSON(NAMESPACE_KEY)).toBeNull(); + }); + + it('first touch wins — does not overwrite when UTMs are already stored', () => { + stubSearch('?utm_source=google'); + captureUtmParams(null); + stubSearch('?utm_source=facebook&utm_medium=paid'); + captureUtmParams(null); + const stored = readJSON(NAMESPACE_KEY) as Record; + expect(stored[UTM_PARAMS_FIELD]).toEqual({ utm_source: 'google' }); + }); + }); + + describe('loadUtmParams', () => { + it('returns null when nothing is stored', () => { + expect(loadUtmParams()).toBeNull(); + }); + + it('returns the stored UTM params', () => { + writeNamespacedField(NAMESPACE_KEY, UTM_PARAMS_FIELD, { utm_source: 'google', utm_medium: 'cpc' }); + expect(loadUtmParams()).toEqual({ utm_source: 'google', utm_medium: 'cpc' }); + }); + + it('returns null when the stored value is not an object', () => { + writeNamespacedField(NAMESPACE_KEY, UTM_PARAMS_FIELD, 'invalid'); + expect(loadUtmParams()).toBeNull(); + }); + }); + + describe('clearUtmParams', () => { + it('removes the utmParams field without affecting other fields', () => { + writeNamespacedField(NAMESPACE_KEY, UTM_PARAMS_FIELD, { utm_source: 'google' }); + writeNamespacedField(NAMESPACE_KEY, PAGE_VIEWS_FIELD, [pageView('home')]); + clearUtmParams(); + + const blob = readJSON(NAMESPACE_KEY); + expect(blob).not.toHaveProperty(UTM_PARAMS_FIELD); + expect(blob).toHaveProperty(PAGE_VIEWS_FIELD); + }); + }); }); From 2174fab5ac83a657da15963c306fc4117beb41db Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Thu, 20 Aug 2026 16:43:31 -0400 Subject: [PATCH 2/3] refactor: use isObject util in loadUtmParams --- src/pageViewStorage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pageViewStorage.ts b/src/pageViewStorage.ts index 7a9ca3c..8324ef1 100644 --- a/src/pageViewStorage.ts +++ b/src/pageViewStorage.ts @@ -7,7 +7,7 @@ import { removeNamespacedField, isLocalStorageAvailable, } from './storage'; -import { sanitizeUrl } from './utils'; +import { sanitizeUrl, isObject } from './utils'; const LS_NAMESPACE_KEY = 'mp-rokt-kit'; const LS_PAGE_VIEWS_FIELD = 'pageViews'; @@ -121,7 +121,7 @@ export function captureUtmParams(loggingService: LoggingService | null): void { export function loadUtmParams(): UtmParams | null { const stored = readNamespacedField(LS_NAMESPACE_KEY, LS_UTM_PARAMS_FIELD); - return stored !== undefined && stored !== null && typeof stored === 'object' ? (stored as UtmParams) : null; + return isObject(stored) ? (stored as UtmParams) : null; } export function clearUtmParams(): void { From fd998319a04769991c781e49d16b7dcd62481e48 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 21 Aug 2026 10:23:26 -0400 Subject: [PATCH 3/3] refactor(utm): extract page_view_attributes key const, lift captureUtmParams to process() #agentic --- src/Rokt-Kit.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 0aadb69..0c7c409 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -263,6 +263,7 @@ const USER_IDENTIFIED_IN_WORKSPACE_KEY = 'userIdentifiedInWorkspace'; const MESSAGE_TYPE_PAGE_VIEW = 3; // mParticle MessageType.PageView const MESSAGE_TYPE_SESSION_END = 2; // mParticle MessageType.SessionEnd const PAGE_EVENTS_KEY = 'page_events'; +const PAGE_VIEW_ATTRIBUTES_KEY = 'page_view_attributes'; const MPARTICLE_SESSION_ID_KEY = 'mparticle_session_id'; // Bound on how long selectPlacements will wait for an in-flight Workspace @@ -880,7 +881,6 @@ class RoktKit implements KitInterface { try { pageUrl = sanitizeUrl(window.location.href); - captureUtmParams(this.loggingService); const pageViews = loadPageViews(this.loggingService); const pageView = buildPageEvent(event); @@ -1246,6 +1246,7 @@ class RoktKit implements KitInterface { public process(event: SDKEvent): string { if (!this.isTargetingDisabled()) { if (event.EventDataType === MESSAGE_TYPE_PAGE_VIEW) { + captureUtmParams(this.loggingService); this.capturePageView(event); } @@ -1481,7 +1482,7 @@ class RoktKit implements KitInterface { ...optimizelyAttributes, ...sessionAttributes, ...(pageEvents.length ? { [PAGE_EVENTS_KEY]: JSON.stringify(pageEvents) } : {}), - ...(utmParams ? { page_view_attributes: utmParams } : {}), + ...(utmParams ? { [PAGE_VIEW_ATTRIBUTES_KEY]: utmParams } : {}), ...(this.userIdentifiedInWorkspace ? { [USER_IDENTIFIED_IN_WORKSPACE_KEY]: true } : {}), ...(mpSessionId ? { [MPARTICLE_SESSION_ID_KEY]: mpSessionId } : {}), mpid,