Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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 and #1125) #1308

Merged
merged 9 commits into from
Jun 5, 2024
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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/javascript-tracker",
"comment": "Fix running integration tests due to a problem with Micro",
"type": "none"
}
],
"packageName": "@snowplow/javascript-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
Loading