Skip to content

Commit 313de18

Browse files
feat: capture page title and canonical URL in page view events
Add optional pageTitle and canonicalUrl fields to captured page-view records so they surface in the page_events attribute sent to selectPlacements. - pageTitle is read from document.title - canonicalUrl is read from <link rel="canonical"> and sanitized through the same helper as pageUrl (query string stripped, hash fragment retained) Both fields are omitted when unavailable, matching the existing activeTimeOnSite handling.
1 parent dec20cd commit 313de18

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

src/Rokt-Kit.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ interface PageEvent {
6565
pageUrl: string;
6666
sourceMessageId: string;
6767
timestamp: number;
68+
pageTitle?: string;
69+
canonicalUrl?: string;
6870
activeTimeOnSite?: number;
6971
// Derived at transmission not at capture based
7072
// on the next page view's activeTimeOnSite,
@@ -508,6 +510,19 @@ function sanitizeUrl(href: string): string {
508510
}
509511
}
510512

513+
// Reads the page's canonical URL from <link rel="canonical">, resolving it
514+
// against the document base and sanitizing it through the same helper as
515+
// pageUrl (query string stripped, hash fragment retained).
516+
// Returns undefined when no canonical link is present or its href is empty.
517+
function readCanonicalUrl(): string | undefined {
518+
const link = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
519+
const href = link?.href;
520+
if (!href) {
521+
return undefined;
522+
}
523+
return sanitizeUrl(href);
524+
}
525+
511526
function generateIntegrationName(customIntegrationName?: string): string {
512527
const coreSdkVersion = mp().getVersion();
513528
const kitVersion = process.env.PACKAGE_VERSION;
@@ -917,6 +932,16 @@ class RoktKit implements KitInterface {
917932
timestamp: event.Timestamp,
918933
};
919934

935+
const pageTitle = document.title;
936+
if (pageTitle) {
937+
pageView.pageTitle = pageTitle;
938+
}
939+
940+
const canonicalUrl = readCanonicalUrl();
941+
if (canonicalUrl) {
942+
pageView.canonicalUrl = canonicalUrl;
943+
}
944+
920945
if (Number.isFinite(event.ActiveTimeOnSite)) {
921946
pageView.activeTimeOnSite = event.ActiveTimeOnSite;
922947
}
@@ -970,6 +995,14 @@ class RoktKit implements KitInterface {
970995
timestamp: pageView.timestamp,
971996
};
972997

998+
if (pageView.pageTitle !== undefined) {
999+
pageEvent.pageTitle = pageView.pageTitle;
1000+
}
1001+
1002+
if (pageView.canonicalUrl !== undefined) {
1003+
pageEvent.canonicalUrl = pageView.canonicalUrl;
1004+
}
1005+
9731006
const activeTimeOnSite = pageView.activeTimeOnSite;
9741007
const hasActiveTime = activeTimeOnSite !== undefined && Number.isFinite(activeTimeOnSite);
9751008
if (hasActiveTime) {

test/src/tests.spec.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5687,6 +5687,81 @@ describe('Rokt Forwarder', () => {
56875687
]);
56885688
});
56895689

5690+
it('captures the page title and canonical URL when present', async () => {
5691+
document.title = 'Home Page Title';
5692+
const canonical = document.createElement('link');
5693+
canonical.setAttribute('rel', 'canonical');
5694+
canonical.setAttribute('href', 'https://example.com/canonical?tracking=abc#section');
5695+
document.head.appendChild(canonical);
5696+
5697+
try {
5698+
await (window as any).mParticle.forwarder.init(
5699+
{
5700+
accountId: '123456',
5701+
},
5702+
reportService.cb,
5703+
true,
5704+
null,
5705+
{},
5706+
);
5707+
5708+
await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled);
5709+
5710+
(window as any).mParticle.forwarder.process({
5711+
EventName: 'Home Page',
5712+
EventCategory: EventType.Unknown,
5713+
EventDataType: MessageType.PageView,
5714+
SourceMessageId: 'source-message-id-title',
5715+
Timestamp: 1712345678000,
5716+
});
5717+
5718+
expect(readStoredPageViews()).toEqual([
5719+
{
5720+
pageUrl: window.location.href,
5721+
sourceMessageId: 'source-message-id-title',
5722+
timestamp: 1712345678000,
5723+
pageTitle: 'Home Page Title',
5724+
// Sanitized via the same helper as pageUrl: query string is stripped,
5725+
// the hash fragment is retained.
5726+
canonicalUrl: 'https://example.com/canonical#section',
5727+
},
5728+
]);
5729+
} finally {
5730+
document.title = '';
5731+
document.head.removeChild(canonical);
5732+
}
5733+
});
5734+
5735+
it('omits pageTitle and canonicalUrl when neither is available', async () => {
5736+
await (window as any).mParticle.forwarder.init(
5737+
{
5738+
accountId: '123456',
5739+
},
5740+
reportService.cb,
5741+
true,
5742+
null,
5743+
{},
5744+
);
5745+
5746+
await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled);
5747+
5748+
(window as any).mParticle.forwarder.process({
5749+
EventName: 'Home Page',
5750+
EventCategory: EventType.Unknown,
5751+
EventDataType: MessageType.PageView,
5752+
SourceMessageId: 'source-message-id-bare',
5753+
Timestamp: 1712345678000,
5754+
});
5755+
5756+
expect(readStoredPageViews()).toEqual([
5757+
{
5758+
pageUrl: window.location.href,
5759+
sourceMessageId: 'source-message-id-bare',
5760+
timestamp: 1712345678000,
5761+
},
5762+
]);
5763+
});
5764+
56905765
it('omits activeTimeOnSite when the source value is non-finite', async () => {
56915766
await (window as any).mParticle.forwarder.init(
56925767
{
@@ -5989,6 +6064,46 @@ describe('Rokt Forwarder', () => {
59896064
]);
59906065
});
59916066

6067+
it('carries pageTitle and canonicalUrl through selectPlacements when stored', async () => {
6068+
window.localStorage.setItem(
6069+
'mpPageViews',
6070+
JSON.stringify([
6071+
{
6072+
pageUrl: 'https://example.com/a',
6073+
sourceMessageId: 'source-message-id-a',
6074+
timestamp: 1712345678000,
6075+
pageTitle: 'Page A',
6076+
canonicalUrl: 'https://example.com/canonical-a',
6077+
},
6078+
]),
6079+
);
6080+
6081+
await (window as any).mParticle.forwarder.init(
6082+
{
6083+
accountId: '123456',
6084+
},
6085+
reportService.cb,
6086+
true,
6087+
null,
6088+
{},
6089+
);
6090+
6091+
await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled);
6092+
6093+
await (window as any).mParticle.forwarder.selectPlacements({ attributes: {} });
6094+
6095+
const forwardedAttributes = (window as any).mParticle.Rokt.selectPlacementsOptions.attributes;
6096+
expect(JSON.parse(forwardedAttributes.page_events)).toEqual([
6097+
{
6098+
pageUrl: 'https://example.com/a',
6099+
sourceMessageId: 'source-message-id-a',
6100+
timestamp: 1712345678000,
6101+
pageTitle: 'Page A',
6102+
canonicalUrl: 'https://example.com/canonical-a',
6103+
},
6104+
]);
6105+
});
6106+
59926107
it('does not add a page_events attribute when no page views are stored', async () => {
59936108
await (window as any).mParticle.forwarder.init(
59946109
{

0 commit comments

Comments
 (0)