Skip to content

Commit

Permalink
Add an option to generate the page view ID according to changes in th…
Browse files Browse the repository at this point in the history
…e page URL to account for events tracked before page views in SPAs (close #1307 and #1125)

PR #1308 
* Add an option to generate the page view ID according to changes in the page URL to account for events tracked before page views in SPAs (close #1307)

* Generate a new page view ID on the second page view tracked on the same page regardless of whether preservePageViewIdForUrl is enabled

* Handle multiple trackers with shared state such that they share the page view IDs for events tracked after each other
  • Loading branch information
matus-tomlein authored and greg-el committed Jun 17, 2024
1 parent 4160f4f commit 6ed21d3
Show file tree
Hide file tree
Showing 10 changed files with 464 additions and 11 deletions.
4 changes: 2 additions & 2 deletions .bundlemonrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
{
"path": "./trackers/browser-tracker/dist/index.umd.min.js",
"maxSize": "15.5kb",
"maxSize": "16kb",
"maxPercentIncrease": 10
},
{
Expand All @@ -22,7 +22,7 @@
},
{
"path": "./libraries/browser-tracker-core/dist/index.module.js",
"maxSize": "27kb",
"maxSize": "28kb",
"maxPercentIncrease": 10
},
{
Expand Down
5 changes: 5 additions & 0 deletions api-docs/docs/browser-tracker/browser-tracker.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface BrowserTracker {
namespace: string;
newSession: () => void;
preservePageViewId: () => void;
preservePageViewIdForUrl: (preserve: PreservePageViewIdForUrl) => void;
setBufferSize: (newBufferSize: number) => void;
setCollectorUrl: (collectorUrl: string) => void;
setCookiePath: (path: string) => void;
Expand Down Expand Up @@ -291,6 +292,9 @@ export type PostBatch = Record<string, unknown>[];
// @public
export function preservePageViewId(trackers?: Array<string>): void;

// @public (undocumented)
export type PreservePageViewIdForUrl = boolean | "full" | "pathname" | "pathnameAndSearch";

// @public
export function removeGlobalContexts(contexts: Array<ConditionalContextProvider | ContextPrimitive>, trackers?: Array<string>): void;

Expand Down Expand Up @@ -417,6 +421,7 @@ export type TrackerConfiguration = {
retryFailedRequests?: boolean;
onRequestSuccess?: (data: EventBatch) => void;
onRequestFailure?: (data: RequestFailure) => void;
preservePageViewIdForUrl?: PreservePageViewIdForUrl;
};

// @public
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/browser-tracker-core",
"comment": "Add an option to generate the page view ID according to changes in the page URL to account for events tracked before page views in SPAs (#1307 and #1125)",
"type": "none"
}
],
"packageName": "@snowplow/browser-tracker-core"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/browser-tracker",
"comment": "Add an option to generate the page view ID according to changes in the page URL to account for events tracked before page views in SPAs (#1307 and #1125)",
"type": "none"
}
],
"packageName": "@snowplow/browser-tracker"
}
2 changes: 2 additions & 0 deletions libraries/browser-tracker-core/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export class SharedState {
/* pageViewId, which can changed by other trackers on page;
* initialized by tracker sent first event */
pageViewId?: string;
/* URL of the page view which the `pageViewId` was generated for */
pageViewUrl?: string;
}

export function createSharedState(): SharedState {
Expand Down
54 changes: 47 additions & 7 deletions libraries/browser-tracker-core/src/tracker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
ClientSession,
ExtendedCrossDomainLinkerOptions,
ParsedIdCookie,
PreservePageViewIdForUrl,
} from './types';
import {
parseIdCookie,
Expand Down Expand Up @@ -292,8 +293,10 @@ export function Tracker(
),
// Whether pageViewId should be regenerated after each trackPageView. Affect web_page context
preservePageViewId = false,
// Whether first trackPageView was fired and pageViewId should not be changed anymore until reload
pageViewSent = false,
// Whether pageViewId should be kept the same until the page URL changes. Affects web_page context
preservePageViewIdForUrl = trackerConfiguration.preservePageViewIdForUrl ?? false,
// The pageViewId of the last page view event or undefined if no page view tracked yet. Used to determine if pageViewId should be regenerated for a new page view.
lastSentPageViewId: string | undefined = undefined,
// Activity tracking config for callback and page ping variants
activityTrackingConfig: ActivityTrackingConfig = {
enabled: false,
Expand Down Expand Up @@ -719,6 +722,7 @@ export function Tracker(
function resetPageView() {
if (!preservePageViewId || state.pageViewId == null) {
state.pageViewId = uuid();
state.pageViewUrl = configCustomUrl || locationHrefAlias;
}
}

Expand All @@ -727,10 +731,42 @@ export function Tracker(
* Generates it if it wasn't initialized by other tracker
*/
function getPageViewId() {
if (state.pageViewId == null) {
if (shouldGenerateNewPageViewId()) {
state.pageViewId = uuid();
state.pageViewUrl = configCustomUrl || locationHrefAlias;
}
return state.pageViewId!;
}

function shouldGenerateNewPageViewId() {
// If pageViewId is not initialized, generate it
if (state.pageViewId == null) {
return true;
}
// If pageViewId should be preserved regardless of the URL, don't generate a new one
if (preservePageViewId || !preservePageViewIdForUrl) {
return false;
}
return state.pageViewId;
// If doesn't have previous URL in state, generate a new pageViewId
if (state.pageViewUrl === undefined) {
return true;
}
const current = configCustomUrl || locationHrefAlias;
// If full preserve is enabled, compare the full URL
if (preservePageViewIdForUrl === true || preservePageViewIdForUrl == 'full' || !('URL' in window)) {
return state.pageViewUrl != current;
}
const currentUrl = new URL(current);
const previousUrl = new URL(state.pageViewUrl);
// If pathname preserve is enabled, compare the pathname
if (preservePageViewIdForUrl == 'pathname') {
return currentUrl.pathname != previousUrl.pathname;
}
// If pathname and search preserve is enabled, compare the pathname and search
if (preservePageViewIdForUrl == 'pathnameAndSearch') {
return currentUrl.pathname != previousUrl.pathname || currentUrl.search != previousUrl.search;
}
return false;
}

/**
Expand Down Expand Up @@ -943,11 +979,11 @@ export function Tracker(

function logPageView({ title, context, timestamp, contextCallback }: PageViewEvent & CommonEventProperties) {
refreshUrl();
if (pageViewSent) {
// Do not reset pageViewId if previous events were not page_view
if (lastSentPageViewId && lastSentPageViewId == getPageViewId()) {
// Do not reset pageViewId if a page view was not tracked yet or a different page view ID was used (in order to support multiple trackers with shared state)
resetPageView();
}
pageViewSent = true;
lastSentPageViewId = getPageViewId();

// So we know what document.title was at the time of trackPageView
lastDocumentTitle = document.title;
Expand Down Expand Up @@ -1291,6 +1327,10 @@ export function Tracker(
preservePageViewId = true;
},

preservePageViewIdForUrl: function (preserve: PreservePageViewIdForUrl) {
preservePageViewIdForUrl = preserve;
},

disableAnonymousTracking: function (configuration?: DisableAnonymousTrackingConfiguration) {
trackerConfiguration.anonymousTracking = false;

Expand Down
25 changes: 25 additions & 0 deletions libraries/browser-tracker-core/src/tracker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export type ExtendedCrossDomainLinkerAttributes = {

export type ExtendedCrossDomainLinkerOptions = boolean | ExtendedCrossDomainLinkerAttributes;

/* Setting for the `preservePageViewIdForUrl` configuration that decides how to preserve the pageViewId on URL changes. */
export type PreservePageViewIdForUrl = boolean | 'full' | 'pathname' | 'pathnameAndSearch';

/**
* The configuration object for initialising the tracker
* @example
Expand Down Expand Up @@ -266,6 +269,17 @@ export type TrackerConfiguration = {
* @param data - The data associated with the event(s) that failed to send
*/
onRequestFailure?: (data: RequestFailure) => void;

/**
* Decide how the `pageViewId` should be preserved based on the URL.
* If set to `false`, the `pageViewId` will be regenerated on the second and each following page view event (first page view doesn't change the page view ID since tracker initialization).
* If set to `true` or `'full'`, the `pageViewId` will be kept the same for all page views with that exact URL (even for events tracked before the page view event).
* If set to `'pathname'`, the `pageViewId` will be kept the same for all page views with the same pathname (search params or fragment may change).
* If set to `'pathnameAndSearch'`, the `pageViewId` will be kept the same for all page views with the same pathname and search params (fragment may change).
* If `preservePageViewId` is enabled, the `preservePageViewIdForUrl` setting is ignored.
* Defaults to `false`.
*/
preservePageViewIdForUrl?: PreservePageViewIdForUrl;
};

/**
Expand Down Expand Up @@ -603,6 +617,17 @@ export interface BrowserTracker {
*/
preservePageViewId: () => void;

/**
* Decide how the `pageViewId` should be preserved based on the URL.
* If set to `false`, the `pageViewId` will be regenerated on the second and each following page view event (first page view doesn't change the page view ID since tracker initialization).
* If set to `true` or `'full'`, the `pageViewId` will be kept the same for all page views with that exact URL (even for events tracked before the page view event).
* If set to `'pathname'`, the `pageViewId` will be kept the same for all page views with the same pathname (search params or fragment may change).
* If set to `'pathnameAndSearch'`, the `pageViewId` will be kept the same for all page views with the same pathname and search params (fragment may change).
* If `preservePageViewId` is enabled, the `preservePageViewIdForUrl` setting is ignored.
* Defaults to `false`.
*/
preservePageViewIdForUrl: (preserve: PreservePageViewIdForUrl) => void;

/**
* Log visit to this page
*
Expand Down
4 changes: 2 additions & 2 deletions libraries/browser-tracker-core/test/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function createTestSessionIdCookie(params?: CreateTestSessionIdCookie) {
return `_sp_ses.${domainHash}=*; Expires=; Path=/; SameSite=None; Secure;`;
}

export function createTracker(configuration?: TrackerConfiguration) {
export function createTracker(configuration?: TrackerConfiguration, sharedState?: SharedState) {
let id = 'sp-' + Math.random();
return addTracker(id, id, '', '', new SharedState(), configuration);
return addTracker(id, id, '', '', sharedState ?? new SharedState(), configuration);
}
Loading

0 comments on commit 6ed21d3

Please sign in to comment.