Skip to content

Commit

Permalink
feat: analytics referrer attribution spa on main
Browse files Browse the repository at this point in the history
- Set up analytics behavior for referrer attribution in SPA;
- Fix additional issues related to analytics context in tracking events;
- Fix inconsistencies in analytics tests.
  • Loading branch information
danielbento92 committed Jan 22, 2024
1 parent 98818d6 commit 1729a08
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 134 deletions.
35 changes: 13 additions & 22 deletions packages/analytics/src/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,10 @@ class Analytics {
*
* @returns Promise that will resolve when the method finishes.
*/
protected async onLoadedIntegrations(
protected onLoadedIntegrations(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadedIntegrations: IntegrationRuntimeData[],
): Promise<void> {
): void | Promise<void> {
// Do nothing
}

Expand Down Expand Up @@ -442,31 +442,29 @@ class Analytics {
eventContext?: EventContextData,
): Promise<this> {
return await this.trackInternal(
TrackType.Track,
event,
properties,
eventContext,
await this.getTrackEventData(
TrackType.Track,
event,
properties,
eventContext,
),
);
}

/**
* Internal track method used by the public track method. Builds the track object
* with page default properties on the context.
*
* @param type - Type of event to be tracked.
* @param event - Name of the event.
* @param properties - Properties of the event.
* @param eventContext - Context data that is specific for this event.
* @param trackData - Data for the event.
*
* @returns Promise that will resolve with the instance that was used when calling this method to allow
* chaining.
*/
protected async trackInternal(
type: TrackTypesValues = TrackType.Track,
event: string,
properties?: EventProperties,
eventContext?: EventContextData,
trackData: EventData<TrackTypesValues>,
): Promise<this> {
const { event } = trackData;

if (!this.isReady) {
logger.error(
`Analytics tried to track the event ${event} but failed. Did you forget to call "analytics.ready()?"`,
Expand All @@ -486,15 +484,8 @@ class Analytics {
await this.setUserPromise;

try {
const data = await this.getTrackEventData(
type,
event,
properties,
eventContext,
);

this.forEachIntegrationSafe(this.activeIntegrations, integration =>
integration.track(data),
integration.track(trackData),
);
} catch (error) {
logger.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Object {
"LAST_FROM_PARAMETER_KEY": "__lastFromParameter",
"LOAD_INTEGRATION_TRACK_TYPE": "loadIntegration",
"ON_SET_USER_TRACK_TYPE": "onSetUser",
"PAGE_LOCATION_REFERRER_KEY": "pageLocationReferrer",
"StorageWrapper": [Function],
"getCheckoutOrderIdentificationProperties": [Function],
"getCheckoutProperties": [Function],
Expand Down
10 changes: 8 additions & 2 deletions packages/analytics/src/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,10 @@ describe('analytics', () => {
const spyTrack = jest.spyOn(integrationInstance, 'track');

// @ts-expect-error Forcing call to trackInternal
analytics.trackInternal(TrackType.Page, PageType.Homepage);
analytics.trackInternal({
type: TrackType.Page,
event: PageType.Homepage,
});

analytics.track('myEvent');

Expand Down Expand Up @@ -951,7 +954,10 @@ describe('analytics', () => {
);

// @ts-expect-error
await analytics.trackInternal(TrackType.Page, PageType.Homepage);
await analytics.trackInternal({
type: TrackType.Page,
event: PageType.Homepage,
});

expect(spyTrack).toHaveBeenCalled();

Expand Down
1 change: 1 addition & 0 deletions packages/analytics/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export const ANALYTICS_UNIQUE_EVENT_ID = '__blackoutAnalyticsEventId';
export const ANALYTICS_UNIQUE_VIEW_ID = '__uniqueViewId';
export const ANALYTICS_PREVIOUS_UNIQUE_VIEW_ID = '__previousUniqueViewId';
export const LAST_FROM_PARAMETER_KEY = '__lastFromParameter';
export const PAGE_LOCATION_REFERRER_KEY = 'pageLocationReferrer';
1 change: 1 addition & 0 deletions packages/analytics/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
ANALYTICS_UNIQUE_VIEW_ID,
ANALYTICS_PREVIOUS_UNIQUE_VIEW_ID,
LAST_FROM_PARAMETER_KEY,
PAGE_LOCATION_REFERRER_KEY,
} from './constants.js';
export * from './defaults.js';
export * from './getters.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ Object {
"integrations": Map {},
"isReady": false,
"lastFromParameter": null,
"lastPageLocation": "",
"platform": "web",
"previousUniqueViewId": null,
"setStoragePromise": Promise {},
Expand Down
141 changes: 133 additions & 8 deletions packages/react/src/analytics/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AnalyticsCore, {
FromParameterType,
type IntegrationOptions,
integrations,
PageType,
} from '@farfetch/blackout-analytics';
import TestStorage from 'test-storage';
import type { WebContext } from '../context.js';
Expand Down Expand Up @@ -123,10 +124,14 @@ describe('analytics web', () => {
await analytics.page(event, properties, eventContext);

expect(coreTrackSpy).toHaveBeenCalledWith(
analyticsTrackTypes.Page,
event,
properties,
eventContext,
expect.objectContaining({
type: analyticsTrackTypes.Page,
event,
properties,
context: expect.objectContaining({
event: expect.objectContaining(eventContext),
}),
}),
);

// Allow the integration to run - this will trigger the flow to track the previously stored page() event
Expand Down Expand Up @@ -276,10 +281,130 @@ describe('analytics web', () => {
await analytics.page(mockEvent, mockEventProperties, mockEventContext);

expect(coreTrackSpy).toHaveBeenCalledWith(
analyticsTrackTypes.Page,
mockEvent,
mockEventProperties,
mockEventContext,
expect.objectContaining({
type: analyticsTrackTypes.Page,
event: mockEvent,
properties: mockEventProperties,
context: expect.objectContaining({
event: expect.objectContaining(mockEventContext),
}),
}),
);
});

describe('Page Location Referrer Property', () => {
// The 'page location referrer' property is significant within the analytics context, specifically concerning web pages.
// It is essential to ensure, through various tests, that different scenarios verify the proper operation of references,
// especially in the case of a Single Page Application (SPA).

const newAnalyticsIntance = () =>
new (analytics.constructor as {
new (): typeof analytics;
})();
// @ts-expect-error
const coreTrackSpy = jest.spyOn(AnalyticsCore.prototype, 'trackInternal');

beforeEach(() => {
coreTrackSpy.mockClear();
});

it('Should retrieve pageLocationReferrer value from origin on first page view', async () => {
const origin = 'www.example.com';

jest.spyOn(document, 'referrer', 'get').mockReturnValueOnce(origin);

const analyticsClean = newAnalyticsIntance();

await analyticsClean.page(PageType.Homepage);

expect(coreTrackSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: analyticsTrackTypes.Page,
event: PageType.Homepage,
context: expect.objectContaining({
web: expect.objectContaining({
pageLocationReferrer: origin,
}),
}),
}),
);
});

it('Should set `pageLocationReferrer` value to the previous page instead of document.referrer after the first navigation event', async () => {
const origin = 'www.example.com';

jest.spyOn(document, 'referrer', 'get').mockReturnValueOnce(origin);
window.location.href = `${origin}/${PageType.Homepage}`;

const analyticsClean = newAnalyticsIntance();

await analyticsClean.page(PageType.Homepage);

expect(coreTrackSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: analyticsTrackTypes.Page,
event: PageType.Homepage,
context: expect.objectContaining({
web: expect.objectContaining({
pageLocationReferrer: origin,
}),
}),
}),
);

// set another page location
window.location.href = `${origin}/${PageType.About}`;

await analyticsClean.page(PageType.About);

expect(coreTrackSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: analyticsTrackTypes.Page,
event: PageType.About,
context: expect.objectContaining({
web: expect.objectContaining({
pageLocationReferrer: `${origin}/${PageType.Homepage}`,
}),
}),
}),
);
});

it('should set `pageLocationReferrer` value to the previous page instead of document.referrer on track actions', async () => {
const origin = 'www.example.com';

jest.spyOn(document, 'referrer', 'get').mockReturnValueOnce(origin);
window.location.href = `${origin}/${PageType.Homepage}`;

const analyticsClean = newAnalyticsIntance();

await analyticsClean.track(mockEvent, mockEventProperties);

expect(coreTrackSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: analyticsTrackTypes.Track,
event: mockEvent,
context: expect.objectContaining({
web: expect.objectContaining({
pageLocationReferrer: origin,
}),
}),
}),
);

await analyticsClean.page(PageType.Homepage);

expect(coreTrackSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: analyticsTrackTypes.Page,
event: PageType.Homepage,
context: expect.objectContaining({
web: expect.objectContaining({
pageLocationReferrer: `${origin}/${PageType.Homepage}`,
}),
}),
}),
);
});
});
});
1 change: 0 additions & 1 deletion packages/react/src/analytics/__tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ describe('context', () => {
title: document.title,
referrer: document.referrer,
},
pageLocationReferrer: window.location.href,
},
};

Expand Down
Loading

0 comments on commit 1729a08

Please sign in to comment.