Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b6d0d75
docs: design spec for page-view capture into local session attributes
alexs-mparticle Jul 31, 2026
c97568c
docs: capture ActiveTimeOnSite, pageUrl, SourceMessageId, timestamp, …
alexs-mparticle Jul 31, 2026
d16c1ab
feat: surface captured page views as flat page_events in selectPlacem…
alexs-mparticle Jul 31, 2026
ba89396
feat: derive timeOnPage per entry in page_events
alexs-mparticle Jul 31, 2026
a62a003
Merge remote-tracking branch 'origin/development' into capture-page-v…
alexs-mparticle Jul 31, 2026
59297d6
chore: revert dist to match development (CI-generated, not hand-autho…
alexs-mparticle Jul 31, 2026
a7fc33c
chore: drop design spec doc from PR
alexs-mparticle Jul 31, 2026
f0b4e89
refactor: trim redundant comments in page-view code
alexs-mparticle Jul 31, 2026
b8973ca
refactor: store page view event name as event_name at capture
alexs-mparticle Jul 31, 2026
89353f8
Apply suggestion from @alexs-mparticle
alexs-mparticle Jul 31, 2026
2ab8c00
refactor: type page_events explicitly and keep legacy tests on PageView
alexs-mparticle Jul 31, 2026
f24d719
refactor: address PR review — no store mutation, explicit names, hone…
alexs-mparticle Jul 31, 2026
d9b1654
refactor: drop unnecessary comments in page-view code
alexs-mparticle Jul 31, 2026
2ff8907
Apply suggestion from @alexs-mparticle
alexs-mparticle Jul 31, 2026
efc2ce2
refactor: address PR review — persist page views as JSON, rename LS k…
alexs-mparticle Aug 3, 2026
db6c1bc
fix: strip query params from page URLs and address review nits
alexs-mparticle Aug 3, 2026
7b5c33a
refactor: drop event_name and event attributes from page events
alexs-mparticle Aug 3, 2026
9c7b27f
fix: stringify page_events before sending to selectPlacements
alexs-mparticle Aug 3, 2026
f12e8a8
refactor: capture page URL early and collapse page-view schema
alexs-mparticle Aug 3, 2026
329efb3
fix: Remove isKitReady guard from process events function
alexs-mparticle Aug 3, 2026
4c278f5
feat: cap page-view history by byte budget keyed to storage backend
alexs-mparticle Aug 3, 2026
8cf350b
refactor: move page-view capture to kit-owned localStorage
alexs-mparticle Aug 3, 2026
9fc8b0e
feat: clear kit-owned page views on session end
alexs-mparticle Aug 3, 2026
9c340f1
fix: make PageEvent.activeTimeOnSite optional for storage round-trip
alexs-mparticle Aug 3, 2026
799cbc5
fix: restore not-ready signal and gate page-view capture on targeting
alexs-mparticle Aug 3, 2026
ccdc2fa
fix: drop activeTimeOnSite guard and restore non-optional type
alexs-mparticle Aug 3, 2026
2ca0366
fix: coerce non-finite ActiveTimeOnSite to 0 at write boundary
alexs-mparticle Aug 3, 2026
7b1cf3b
fix: omit non-finite activeTimeOnSite and clear page views when targe…
alexs-mparticle Aug 4, 2026
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
171 changes: 166 additions & 5 deletions src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ interface RoktExtensionEntry {
value: string;
}

interface PageEvent {
pageUrl: string;
sourceMessageId: string;
timestamp: number;
activeTimeOnSite?: number;
// Derived at transmission not at capture based
// on the next page view's activeTimeOnSite,
// so it is absent on stored records.
timeOnPage?: number;
}

interface RoktSelection {
context?: {
sessionId?: Promise<string>;
Expand Down Expand Up @@ -244,6 +255,19 @@ const ROKT_INTEGRATION_SCRIPT_ID = 'rokt-launcher';
const ROKT_THANK_YOU_ELEMENT_SCRIPT_ID = 'rokt-thank-you-element';
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
// localStorage key under which captured page views are persisted (as a JSON
// string). The kit owns this storage directly — separate from mParticle's
// cookie/localStorage — so page-view capture does not affect mParticle
// persistence or cookie sync. Distinct from PAGE_EVENTS_KEY, which is the
// flattened wire shape sent to Rokt on selectPlacements.
const LS_PAGE_VIEWS_KEY = 'mpPageViews';
// Fixed cap on the number of persisted page views (oldest evicted first). Code
// constant, not a kit setting — change it here.
const PAGE_VIEWS_MAX_COUNT = 25;
const PAGE_EVENTS_KEY = 'page_events';

// Bound on how long selectPlacements will wait for an in-flight Workspace
// IDSync search before proceeding without the userIdentifiedInWorkspace flag.
// Long enough to cover the typical /v1/search round-trip (~50ms); short enough that a
Expand Down Expand Up @@ -287,6 +311,27 @@ function mp(): MParticleExtended {
// Module-level utility functions
// ============================================================

function readPageViewsStorage(): PageEvent[] {
try {
const stored = window.localStorage.getItem(LS_PAGE_VIEWS_KEY);
if (stored === null) {
return [];
}
const parsed = JSON.parse(stored);
return Array.isArray(parsed) ? (parsed as PageEvent[]) : [];
} catch {
return [];
}
}

function writePageViewsStorage(pageViews: PageEvent[]): void {
window.localStorage.setItem(LS_PAGE_VIEWS_KEY, JSON.stringify(pageViews));
}

function clearPageViewsStorage(): void {
window.localStorage.removeItem(LS_PAGE_VIEWS_KEY);
}

function generateLauncherScript(domain: string | undefined, extensions: string[]): string {
const launcherPath = '/wsdk/integrations/launcher.js';
const baseUrl = [generateBaseUrl(domain), launcherPath].join('');
Expand Down Expand Up @@ -450,6 +495,19 @@ function isString(value: unknown): value is string {
return typeof value === 'string';
}

// Strips the query string from a page-view URL before it is persisted and sent
// to Rokt, since query params commonly carry PII (emails, tokens, order refs).
// Returns the input unchanged if it can't be parsed as a URL.
function sanitizeUrl(href: string): string {
try {
const url = new URL(href);
url.search = '';
return url.toString();
} catch {
return href;
}
}

function generateIntegrationName(customIntegrationName?: string): string {
const coreSdkVersion = mp().getVersion();
const kitVersion = process.env.PACKAGE_VERSION;
Expand Down Expand Up @@ -845,6 +903,41 @@ class RoktKit implements KitInterface {
}
}

private capturePageView(event: SDKEvent): void {
let pageUrl: string | undefined;

try {
pageUrl = sanitizeUrl(window.location.href);

const pageViews = readPageViewsStorage();

const pageView: PageEvent = {
pageUrl,
sourceMessageId: event.SourceMessageId,
timestamp: event.Timestamp,
};

if (Number.isFinite(event.ActiveTimeOnSite)) {
pageView.activeTimeOnSite = event.ActiveTimeOnSite;
}

pageViews.push(pageView);

while (pageViews.length > PAGE_VIEWS_MAX_COUNT) {
pageViews.shift();
}

writePageViewsStorage(pageViews);
} catch (err) {
this.errorReportingService?.report({
Comment thread
alexs-mparticle marked this conversation as resolved.
message: `Rokt Kit: Failed to capture page view for ${pageUrl}`,
code: 'PAGE_VIEW_CAPTURE_FAILED',
severity: WSDKErrorSeverity.WARNING,
stackTrace: err instanceof Error ? err.stack : undefined,
});
}
}

private isLauncherReadyToAttach(): boolean {
return !!window.Rokt && typeof window.Rokt.createLauncher === 'function';
}
Expand All @@ -866,12 +959,37 @@ class RoktKit implements KitInterface {
if (!mp().Rokt || typeof mp().Rokt.getLocalSessionAttributes !== 'function') {
return {};
}
if (isEmpty(this.placementEventMappingLookup) && isEmpty(this.placementEventAttributeMappingLookup)) {
Comment thread
alexs-mparticle marked this conversation as resolved.
return {};
}
return mp().Rokt.getLocalSessionAttributes!();
}

private buildPageEvents(pageViews: PageEvent[]): PageEvent[] {
return pageViews.map((pageView, index) => {
const pageEvent: PageEvent = {
pageUrl: pageView.pageUrl,
sourceMessageId: pageView.sourceMessageId,
timestamp: pageView.timestamp,
};

const activeTimeOnSite = pageView.activeTimeOnSite;
const hasActiveTime = activeTimeOnSite !== undefined && Number.isFinite(activeTimeOnSite);
if (hasActiveTime) {
pageEvent.activeTimeOnSite = activeTimeOnSite;
}

const next = pageViews[index + 1];
const nextActiveTimeOnSite = next?.activeTimeOnSite;
const hasNextActiveTimeOnSite = nextActiveTimeOnSite !== undefined && Number.isFinite(nextActiveTimeOnSite);

if (hasActiveTime && hasNextActiveTimeOnSite) {
const diff = nextActiveTimeOnSite - activeTimeOnSite;
if (diff >= 0) {
pageEvent.timeOnPage = diff;
}
}
return pageEvent;
});
}

private replaceOtherIdentityWithEmailsha256(userIdentities: IUserIdentities): Record<string, string> {
const newUserIdentities: Record<string, string> = { ...(userIdentities || {}) };
const key = this._mappedEmailSha256Key;
Expand Down Expand Up @@ -1007,6 +1125,12 @@ class RoktKit implements KitInterface {
return !!(this.isInitialized && this.launcher);
}

// When the partner has opted out of targeting (noTargeting launcher option),
// the kit must not collect behavioral targeting signals such as page views.
private isTargetingDisabled(): boolean {
return (mp().Rokt?.launcherOptions as Record<string, unknown> | undefined)?.noTargeting === true;
}

private isPartnerInLocalLauncherTestGroup(): boolean {
return !!(mp().config && mp().config!.isLocalLauncherEnabled && this.isAssignedToSampleGroup());
}
Expand Down Expand Up @@ -1091,6 +1215,19 @@ class RoktKit implements KitInterface {
this.errorReportingService = errorReportingService;
this.loggingService = loggingService;

if (this.isTargetingDisabled()) {
try {
clearPageViewsStorage();
} catch (err) {
this.errorReportingService?.report({
message: 'Rokt Kit: Failed to clear page views when targeting is disabled',
code: 'PAGE_VIEW_CAPTURE_FAILED',
severity: WSDKErrorSeverity.WARNING,
stackTrace: err instanceof Error ? err.stack : undefined,
});
}
}

if (mp()._registerErrorReportingService) {
mp()._registerErrorReportingService!(errorReportingService);
}
Expand Down Expand Up @@ -1162,9 +1299,31 @@ class RoktKit implements KitInterface {
}

public process(event: SDKEvent): string {
Comment thread
jamesnrokt marked this conversation as resolved.
if (!this.isTargetingDisabled()) {
Comment thread
jamesnrokt marked this conversation as resolved.
if (event.EventDataType === MESSAGE_TYPE_PAGE_VIEW) {
this.capturePageView(event);
}

if (event.EventDataType === MESSAGE_TYPE_SESSION_END) {
try {
clearPageViewsStorage();
} catch (err) {
this.errorReportingService?.report({
message: 'Rokt Kit: Failed to clear page views on session end',
code: 'PAGE_VIEW_CAPTURE_FAILED',
severity: WSDKErrorSeverity.WARNING,
stackTrace: err instanceof Error ? err.stack : undefined,
});
}
}
}

// The forwarding work below (LSA mapping) depends on the launcher, so guard
// it here and surface the not-ready signal to the core SDK.
if (!this.isKitReady()) {
return 'Kit not ready for forwarder: ' + name;
}

if (typeof mp().Rokt?.setLocalSessionAttribute === 'function') {
if (!isEmpty(this.placementEventAttributeMappingLookup)) {
this.applyPlacementEventAttributeMapping(event);
Expand Down Expand Up @@ -1373,13 +1532,15 @@ class RoktKit implements KitInterface {

const filteredUserIdentities = this.returnUserIdentities(filteredUser);

const localSessionAttributes = this.returnLocalSessionAttributes();
const sessionAttributes = this.returnLocalSessionAttributes();
const pageEvents = this.buildPageEvents(readPageViewsStorage());

const selectPlacementsAttributes: Record<string, unknown> = {
...(filteredUserIdentities as Record<string, unknown>),
...filteredAttributes,
...optimizelyAttributes,
...localSessionAttributes,
...sessionAttributes,
...(pageEvents.length ? { [PAGE_EVENTS_KEY]: JSON.stringify(pageEvents) } : {}),
...(this.userIdentifiedInWorkspace ? { [USER_IDENTIFIED_IN_WORKSPACE_KEY]: true } : {}),
mpid,
};
Expand Down
Loading
Loading