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
75 changes: 21 additions & 54 deletions src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ import {

import {
PageEvent,
buildPageEvents,
migrateLegacyPageViewStorage,
loadPageViews,
writePageViews,
clearPageViews,
readCanonicalUrl,
} from './pageViewStorage';

import { isObject, isString, isEmpty } from './utils';
import { isObject, isString, isEmpty, sanitizeUrl } from './utils';

interface RoktKitSettings {
accountId: string;
Expand Down Expand Up @@ -445,19 +447,6 @@ function hashEventMessage(messageType: number, eventType: number, eventName: str
return mp().generateHash([messageType, eventType, eventName].join(''));
}

// 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 @@ -709,6 +698,22 @@ class LoggingService {
}
}

function buildPageEvent(event: SDKEvent): PageEvent {
const pageUrl = sanitizeUrl(window.location.href);
const pageTitle = event.EventAttributes?.title || document.title;
const canonicalUrl = readCanonicalUrl();
const activeTimeOnSite = event.ActiveTimeOnSite;

return {
pageUrl,
sourceMessageId: event.SourceMessageId,
timestamp: event.Timestamp,
...(pageTitle ? { pageTitle } : {}),
...(canonicalUrl !== undefined ? { canonicalUrl } : {}),
...(Number.isFinite(activeTimeOnSite) ? { activeTimeOnSite } : {}),
};
}

// ============================================================
// RoktKit class
// ============================================================
Expand Down Expand Up @@ -860,17 +865,7 @@ class RoktKit implements KitInterface {
pageUrl = sanitizeUrl(window.location.href);

const pageViews = loadPageViews(this.loggingService);

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

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

const pageView = buildPageEvent(event);
pageViews.push(pageView);

if (!writePageViews(pageViews)) {
Expand Down Expand Up @@ -913,34 +908,6 @@ class RoktKit implements KitInterface {
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.activeTimeOnPage = diff;
}
}
return pageEvent;
});
}

private replaceOtherIdentityWithEmailsha256(userIdentities: IUserIdentities): Record<string, string> {
const newUserIdentities: Record<string, string> = { ...(userIdentities || {}) };
const key = this._mappedEmailSha256Key;
Expand Down Expand Up @@ -1476,7 +1443,7 @@ class RoktKit implements KitInterface {
const filteredUserIdentities = this.returnUserIdentities(filteredUser);

const sessionAttributes = this.returnLocalSessionAttributes();
const pageEvents = this.buildPageEvents(loadPageViews(this.loggingService));
const pageEvents = buildPageEvents(loadPageViews(this.loggingService));

const selectPlacementsAttributes: Record<string, unknown> = {
...(filteredUserIdentities as Record<string, unknown>),
Expand Down
35 changes: 35 additions & 0 deletions src/pageViewStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
removeNamespacedField,
writeNamespacedFieldWithinBudget,
} from './storage';
import { sanitizeUrl } from './utils';

const LS_NAMESPACE_KEY = 'mp-rokt-kit';
const LS_PAGE_VIEWS_FIELD = 'pageViews';
Expand All @@ -17,6 +18,8 @@ export interface PageEvent {
pageUrl: string;
sourceMessageId: string;
timestamp: number;
pageTitle?: string;
canonicalUrl?: string;
activeTimeOnSite?: number;
activeTimeOnPage?: number;
}
Expand Down Expand Up @@ -57,3 +60,35 @@ export function writePageViews(pageViews: PageEvent[]): boolean {
export function clearPageViews(): void {
removeNamespacedField(LS_NAMESPACE_KEY, LS_PAGE_VIEWS_FIELD);
}

export function buildPageEvents(pageViews: PageEvent[]): PageEvent[] {
return pageViews.map((pageView, index) => {
const activeTimeOnSite = pageView.activeTimeOnSite;
const hasActiveTime = activeTimeOnSite !== undefined && Number.isFinite(activeTimeOnSite);

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

const diff = hasActiveTime && hasNextActiveTimeOnSite ? nextActiveTimeOnSite - activeTimeOnSite : undefined;

return {
pageUrl: pageView.pageUrl,
sourceMessageId: pageView.sourceMessageId,
timestamp: pageView.timestamp,
...(pageView.pageTitle !== undefined ? { pageTitle: pageView.pageTitle } : {}),
...(pageView.canonicalUrl !== undefined ? { canonicalUrl: pageView.canonicalUrl } : {}),
...(hasActiveTime ? { activeTimeOnSite } : {}),
...(diff !== undefined && diff >= 0 ? { activeTimeOnPage: diff } : {}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this activeTimeOnPage - am I right that this is technically activeTimeOnThePreviousPage?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically...

};
});
}

export function readCanonicalUrl(): string | undefined {
const link = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
const href = link?.href;
if (!href) {
return undefined;
}
return sanitizeUrl(href);
}
13 changes: 13 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ export function isEmpty(value: unknown): boolean {
}
return false;
}

// Strips the query string from a 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.
export function sanitizeUrl(href: string): string {
try {
const url = new URL(href);
url.search = '';
return url.toString();
} catch {
return href;
}
}
151 changes: 151 additions & 0 deletions test/src/tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5686,11 +5686,125 @@ describe('Rokt Forwarder', () => {
pageUrl: window.location.href,
sourceMessageId: 'source-message-id-1',
timestamp: 1712345678000,
pageTitle: 'Home',
activeTimeOnSite: 4200,
},
]);
});

it('captures the page title and canonical URL when present', async () => {
document.title = 'Home Page Title';
const canonical = document.createElement('link');
canonical.setAttribute('rel', 'canonical');
canonical.setAttribute('href', 'https://example.com/canonical?tracking=abc#section');
document.head.appendChild(canonical);

try {
await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
},
reportService.cb,
true,
null,
{},
);

await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled);

(window as any).mParticle.forwarder.process({
EventName: 'Home Page',
EventCategory: EventType.Unknown,
EventDataType: MessageType.PageView,
SourceMessageId: 'source-message-id-title',
Timestamp: 1712345678000,
});

expect(readStoredPageViews()).toEqual([
{
pageUrl: window.location.href,
sourceMessageId: 'source-message-id-title',
timestamp: 1712345678000,
pageTitle: 'Home Page Title',
canonicalUrl: 'https://example.com/canonical#section',
},
]);
} finally {
document.title = '';
document.head.removeChild(canonical);
}
});

it('prefers the event title over document.title when both are present', async () => {
document.title = 'Document Title';

try {
await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
},
reportService.cb,
true,
null,
{},
);

await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled);

(window as any).mParticle.forwarder.process({
EventName: 'Home Page',
EventCategory: EventType.Unknown,
EventDataType: MessageType.PageView,
SourceMessageId: 'source-message-id-event-title',
Timestamp: 1712345678000,
EventAttributes: {
title: 'Event Title',
},
});

expect(readStoredPageViews()).toEqual([
{
pageUrl: window.location.href,
sourceMessageId: 'source-message-id-event-title',
timestamp: 1712345678000,
pageTitle: 'Event Title',
},
]);
} finally {
document.title = '';
}
});

it('omits pageTitle and canonicalUrl when neither is available', async () => {
await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
},
reportService.cb,
true,
null,
{},
);

await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled);

(window as any).mParticle.forwarder.process({
EventName: 'Home Page',
EventCategory: EventType.Unknown,
EventDataType: MessageType.PageView,
SourceMessageId: 'source-message-id-bare',
Timestamp: 1712345678000,
});

expect(readStoredPageViews()).toEqual([
{
pageUrl: window.location.href,
sourceMessageId: 'source-message-id-bare',
timestamp: 1712345678000,
},
]);
});

it('omits activeTimeOnSite when the source value is non-finite', async () => {
await (window as any).mParticle.forwarder.init(
{
Expand Down Expand Up @@ -6238,6 +6352,43 @@ describe('Rokt Forwarder', () => {
]);
});

it('carries pageTitle and canonicalUrl through selectPlacements when stored', async () => {
seedStoredPageViews([
{
pageUrl: 'https://example.com/a',
sourceMessageId: 'source-message-id-a',
timestamp: 1712345678000,
pageTitle: 'Page A',
canonicalUrl: 'https://example.com/canonical-a',
},
]);

await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
},
reportService.cb,
true,
null,
{},
);

await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled);

await (window as any).mParticle.forwarder.selectPlacements({ attributes: {} });

const forwardedAttributes = (window as any).mParticle.Rokt.selectPlacementsOptions.attributes;
expect(JSON.parse(forwardedAttributes.page_events)).toEqual([
{
pageUrl: 'https://example.com/a',
sourceMessageId: 'source-message-id-a',
timestamp: 1712345678000,
pageTitle: 'Page A',
canonicalUrl: 'https://example.com/canonical-a',
},
]);
});

it('does not add a page_events attribute when no page views are stored', async () => {
await (window as any).mParticle.forwarder.init(
{
Expand Down
Loading