Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3509d9e
fix: fix web attribution identify and session start order
yuhao900914 Mar 29, 2024
cb448e4
build: update other sdk dependency
yuhao900914 Mar 29, 2024
ff7f12a
fix: make campaign tracking order before page view tracking
yuhao900914 Apr 8, 2024
0151368
fix: fix the event id not right for web attribution identify
yuhao900914 Apr 8, 2024
45aa354
test: test
yuhao900914 Apr 9, 2024
0222d83
fix: clean up
yuhao900914 Apr 9, 2024
2ec62f5
fix: clean up
yuhao900914 Apr 9, 2024
cb3db32
fix: remove default web attribution plugin installation
yuhao900914 Apr 9, 2024
5ba44b1
build: revert the dependency in node and react-native
yuhao900914 Apr 9, 2024
d18e26a
test: clean the example
yuhao900914 Apr 9, 2024
7d7645f
fix: remove no-non-null-assertion
yuhao900914 Apr 9, 2024
9f6cfc8
fix: fix session event not fired
yuhao900914 Apr 9, 2024
1260a18
fix: merge with main
yuhao900914 Apr 10, 2024
a241181
fix: refine structure and fix test coverage
yuhao900914 Apr 10, 2024
f5a470c
fix: fixes based on commend
yuhao900914 Apr 10, 2024
ca2214d
test: fix test
yuhao900914 Apr 10, 2024
92e5ea5
fix: refine the logic
yuhao900914 Apr 10, 2024
847d367
test: fix the test
yuhao900914 Apr 11, 2024
e411381
fix: refactor move web attribution logic in analytics-client-common
yuhao900914 Apr 11, 2024
baff0c6
fix: nits
yuhao900914 Apr 13, 2024
7530205
fix: remove await on page view tracking
yuhao900914 Apr 16, 2024
b6b2ca3
feat: make setSessionId return promise
yuhao900914 Apr 17, 2024
5294eae
test: fix test
yuhao900914 Apr 17, 2024
feffbd3
fix: fix the track campaign event
yuhao900914 Apr 17, 2024
9c442c8
fix: fix event promise
yuhao900914 Apr 17, 2024
fb8630a
fix: add type
yuhao900914 Apr 18, 2024
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
1 change: 0 additions & 1 deletion packages/analytics-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"@amplitude/analytics-core": "^2.2.4-beta.0",
"@amplitude/analytics-types": "^2.5.0",
"@amplitude/plugin-page-view-tracking-browser": "^2.2.6-beta.0",
"@amplitude/plugin-web-attribution-browser": "^2.1.7-beta.0",
"tslib": "^2.4.1"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/analytics-browser/playground/amplitude.js

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions packages/analytics-browser/playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@
<title>Amplitude SDK Playground</title>
</head>
<script src="./amplitude.js"></script>
<script>
amplitude.init('API_KEY', '[email protected]');

<script>
amplitude.init('82b148f7211db7f9ccaff8048d0f7192', '[email protected]',
{defaultTracking: {
attribution: true,
pageViews: true,
sessions: true,
sessionTimeOut: 1000
}, flushQueueSize: 10});
//{resetSessionOnNewCampaign:true},
//, sessionTimeOut: 1000
amplitude.track('test_event')

</script>
<body>
<h1>Amplitude SDK Playground</h1>
Expand Down
56 changes: 36 additions & 20 deletions packages/analytics-browser/src/browser-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { convertProxyObjectToRealObject, isInstanceProxy } from './utils/snippet-helper';
import { Context } from './plugins/context';
import { useBrowserConfig, createTransport } from './config';
import { webAttributionPlugin } from '@amplitude/plugin-web-attribution-browser';
import { WebAttribution } from './utils/web-attribution';
import { pageViewTrackingPlugin } from '@amplitude/plugin-page-view-tracking-browser';
import { formInteractionTracking } from './plugins/form-interaction-tracking';
import { fileDownloadTracking } from './plugins/file-download-tracking';
Expand All @@ -41,6 +41,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {
config: BrowserConfig;
previousSessionDeviceId: string | undefined;
previousSessionUserId: string | undefined;
webAttribution: WebAttribution | undefined;

init(apiKey = '', userIdOrOptions?: string | BrowserOptions, maybeOptions?: BrowserOptions) {
let userId: string | undefined;
Expand Down Expand Up @@ -71,12 +72,18 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {
const browserOptions = await useBrowserConfig(options.apiKey, options, this);
this.config = browserOptions;

// Add web attribution plugin
if (isAttributionTrackingEnabled(this.config.defaultTracking)) {
const attributionTrackingOptions = getAttributionTrackingConfig(this.config);
this.webAttribution = new WebAttribution(attributionTrackingOptions, this, this.config);
}

// Step 3: Set session ID
// Priority 1: `options.sessionId`
// Priority 2: last known sessionId from user identity storage
// Default: `Date.now()`
// Session ID is handled differently than device ID and user ID due to session events
this.setSessionId(options.sessionId ?? this.config.sessionId ?? Date.now());
await this.setSessionId(options.sessionId ?? this.config.sessionId ?? Date.now());

await super._init(this.config);

Expand Down Expand Up @@ -109,13 +116,6 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {
await this.add(formInteractionTracking()).promise;
}

// Add web attribution plugin
if (isAttributionTrackingEnabled(this.config.defaultTracking)) {
const attributionTrackingOptions = getAttributionTrackingConfig(this.config);
const webAttribution = webAttributionPlugin(attributionTrackingOptions);
await this.add(webAttribution).promise;
}

// Add page view plugin
if (isPageViewTrackingEnabled(this.config.defaultTracking)) {
await this.add(pageViewTrackingPlugin(getPageViewTrackingConfig(this.config))).promise;
Expand Down Expand Up @@ -169,12 +169,11 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {
return this.config?.sessionId;
}

setSessionId(sessionId: number) {
async setSessionId(sessionId: number, shouldTrackNewCampaign?: boolean) {
if (!this.config) {
this.q.push(this.setSessionId.bind(this, sessionId));
return;
}

// Prevents starting a new session with the same session ID
if (sessionId === this.config.sessionId) {
return;
Expand All @@ -190,21 +189,31 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {

if (isSessionTrackingEnabled(this.config.defaultTracking)) {
if (previousSessionId && lastEventTime) {
this.track(DEFAULT_SESSION_END_EVENT, undefined, {
await this.track(DEFAULT_SESSION_END_EVENT, undefined, {
device_id: this.previousSessionDeviceId,
event_id: ++lastEventId,
session_id: previousSessionId,
time: lastEventTime + 1,
user_id: this.previousSessionUserId,
});
}).promise;
}

this.config.lastEventTime = this.config.sessionId;
this.track(DEFAULT_SESSION_START_EVENT, undefined, {
}

// Fire web attribution events when enable webAttribution tracking and either
// 1. has new campaign (manually call setSessionId or call setSessionId from init function)
// 2. or shouldTrackNewCampaign (call setSessionId from async process(event) when there has new campaign and resetSessionOnNewCampaign = true )
if ((this.webAttribution && (await this.webAttribution.shouldTrackNewCampaign())) || shouldTrackNewCampaign) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.webAttribution!.track(++lastEventId);
}

if (isSessionTrackingEnabled(this.config.defaultTracking)) {
await this.track(DEFAULT_SESSION_START_EVENT, undefined, {
event_id: ++lastEventId,
session_id: this.config.sessionId,
time: this.config.lastEventTime,
});
}).promise;
}

this.previousSessionDeviceId = this.config.deviceId;
Expand Down Expand Up @@ -263,14 +272,21 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {
async process(event: Event) {
const currentTime = Date.now();
const isEventInNewSession = isNewSession(this.config.sessionTimeout, this.config.lastEventTime);

const shouldTrackNewCampaign = this.webAttribution && (await this.webAttribution.shouldTrackNewCampaign());
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const shouldSetSessionId = shouldTrackNewCampaign && this.webAttribution!.options.resetSessionOnNewCampaign;
if (
event.event_type !== DEFAULT_SESSION_START_EVENT &&
event.event_type !== DEFAULT_SESSION_END_EVENT &&
(!event.session_id || event.session_id === this.getSessionId()) &&
isEventInNewSession
(!event.session_id || event.session_id === this.getSessionId())
) {
this.setSessionId(currentTime);
if (isEventInNewSession || shouldSetSessionId) {
await this.setSessionId(currentTime, shouldTrackNewCampaign);
} else if (!isEventInNewSession && shouldTrackNewCampaign) {
// web attribution should be track during the middle of the session if there has any new campaign
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await this.webAttribution!.track().promise;
}
}

return super.process(event);
Expand Down
88 changes: 88 additions & 0 deletions packages/analytics-browser/src/utils/web-attribution-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { BASE_CAMPAIGN } from '@amplitude/analytics-client-common';
import { AMPLITUDE_PREFIX, createIdentifyEvent, Identify } from '@amplitude/analytics-core';
import { Campaign } from '@amplitude/analytics-types';

export interface Options {
excludeReferrers?: (string | RegExp)[];
initialEmptyValue?: string;
resetSessionOnNewCampaign?: boolean;
}

export const getStorageKey = (apiKey: string, postKey = '', limit = 10) => {
return [AMPLITUDE_PREFIX, postKey, apiKey.substring(0, limit)].filter(Boolean).join('_');
};

const domainWithoutSubdomain = (domain: string) => {
const parts = domain.split('.');

if (parts.length <= 2) {
return domain;
}

return parts.slice(parts.length - 2, parts.length).join('.');
};

//Direct traffic mean no external referral, no UTMs, no click-ids, and no other customer identified marketing campaign url params.
const isDirectTraffic = (current: Campaign) => {
return Object.values(current).every((value) => !value);
};

export const isNewCampaign = (
current: Campaign,
previous: Campaign | undefined,
options: Options,
isNewSession = true,
) => {
const { referrer, referring_domain, ...currentCampaign } = current;
const { referrer: _previous_referrer, referring_domain: prevReferringDomain, ...previousCampaign } = previous || {};

if (isExcludedReferrer(options.excludeReferrers, current.referring_domain)) {
return false;
}

//In the same session, direct traffic should not override or unset any persisting query params
if (!isNewSession && isDirectTraffic(current) && previous) {
return false;
}

const hasNewCampaign = JSON.stringify(currentCampaign) !== JSON.stringify(previousCampaign);
const hasNewDomain =
domainWithoutSubdomain(referring_domain || '') !== domainWithoutSubdomain(prevReferringDomain || '');

return !previous || hasNewCampaign || hasNewDomain;
};

export const isExcludedReferrer = (excludeReferrers: (string | RegExp)[] = [], referringDomain = '') => {
return excludeReferrers.some((value) =>
value instanceof RegExp ? value.test(referringDomain) : value === referringDomain,
);
};

export const createCampaignEvent = (campaign: Campaign, options: Options) => {
const campaignParameters: Campaign = {
// This object definition allows undefined keys to be iterated on
// in .reduce() to build indentify object
...BASE_CAMPAIGN,
...campaign,
};
const identifyEvent = Object.entries(campaignParameters).reduce((identify, [key, value]) => {
identify.setOnce(`initial_${key}`, value ?? options.initialEmptyValue ?? 'EMPTY');
if (value) {
return identify.set(key, value);
}
return identify.unset(key);
}, new Identify());

return createIdentifyEvent(identifyEvent);
};

export const getDefaultExcludedReferrers = (cookieDomain: string | undefined) => {
let domain = cookieDomain;
if (domain) {
if (domain.startsWith('.')) {
domain = domain.substring(1);
}
return [new RegExp(`${domain.replace('.', '\\.')}$`)];
}
return [];
};
58 changes: 58 additions & 0 deletions packages/analytics-browser/src/utils/web-attribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { BrowserClient, BrowserConfig } from '@amplitude/analytics-types';
import { Campaign, Storage } from '@amplitude/analytics-types';
import {
Options,
getDefaultExcludedReferrers,
getStorageKey,
createCampaignEvent,
isNewCampaign,
} from './web-attribution-helper';
import { CampaignParser } from '@amplitude/analytics-client-common';

export class WebAttribution {
options: Options;
storage: Storage<Campaign>;
storageKey: string;
amplitude: BrowserClient;
previousCampaign: Campaign | undefined;
currentCampaign!: Campaign;

constructor(options: Options, amplitude: BrowserClient, config: BrowserConfig) {
this.options = {
initialEmptyValue: 'EMPTY',
resetSessionOnNewCampaign: false,
excludeReferrers: getDefaultExcludedReferrers(config.cookieOptions?.domain),
...options,
};
this.amplitude = amplitude;
this.storage = config.cookieStorage as unknown as Storage<Campaign>;
this.storageKey = getStorageKey(config.apiKey, 'MKTG');
}

async shouldTrackNewCampaign() {
[this.currentCampaign, this.previousCampaign] = await this.fetchCampaign();

await this.storage.set(this.storageKey, this.currentCampaign);
if (isNewCampaign(this.currentCampaign, this.previousCampaign, this.options)) {
return true;
}
return false;
}

async fetchCampaign() {
return await Promise.all([new CampaignParser().parse(), this.storage.get(this.storageKey)]);
}

/**
* This can be called when enable web attribution and either
* 1. set a new session
* 2. has new campaign and enable resetSessionOnNewCampaign
*/
track(event_id?: number) {
const campaignEvent = createCampaignEvent(this.currentCampaign, this.options);
if (event_id) {
campaignEvent.event_id = event_id;
}
return this.amplitude.track(campaignEvent);
}
}
Loading