Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
64 changes: 32 additions & 32 deletions packages/analytics-browser-test/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,22 +907,6 @@ describe('integration', () => {
{
device_id: uuid,
event_id: 101,
event_type: 'session_start',
insert_id: uuid,
ip: '$remote',
language: 'en-US',
library,
partner_id: undefined,
plan: undefined,
platform: 'Web',
session_id: number,
time: number,
user_agent: userAgent,
user_id: '[email protected]',
},
{
device_id: uuid,
event_id: 102,
event_type: '$identify',
insert_id: uuid,
ip: '$remote',
Expand Down Expand Up @@ -980,6 +964,22 @@ describe('integration', () => {
},
},
},
{
device_id: uuid,
event_id: 102,
event_type: 'session_start',
insert_id: uuid,
ip: '$remote',
language: 'en-US',
library,
partner_id: undefined,
plan: undefined,
platform: 'Web',
session_id: number,
time: number,
user_agent: userAgent,
user_id: '[email protected]',
},
{
device_id: uuid,
event_id: 103,
Expand Down Expand Up @@ -1115,22 +1115,6 @@ describe('integration', () => {
{
device_id: uuid,
event_id: 0,
event_type: 'session_start',
insert_id: uuid,
ip: '$remote',
language: 'en-US',
library,
partner_id: undefined,
plan: undefined,
platform: 'Web',
session_id: number,
time: number,
user_agent: userAgent,
user_id: '[email protected]',
},
{
device_id: uuid,
event_id: 1,
event_type: '$identify',
insert_id: uuid,
ip: '$remote',
Expand Down Expand Up @@ -1188,6 +1172,22 @@ describe('integration', () => {
},
},
},
{
device_id: uuid,
event_id: 1,
event_type: 'session_start',
insert_id: uuid,
ip: '$remote',
language: 'en-US',
library,
partner_id: undefined,
plan: undefined,
platform: 'Web',
session_id: number,
time: number,
user_agent: userAgent,
user_id: '[email protected]',
},
{
device_id: uuid,
event_id: 2,
Expand Down
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",
"@amplitude/analytics-types": "^2.5.0",
"@amplitude/plugin-page-view-tracking-browser": "^2.2.6",
"@amplitude/plugin-web-attribution-browser": "^2.1.7",
"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.

3 changes: 2 additions & 1 deletion packages/analytics-browser/playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
<title>Amplitude SDK Playground</title>
</head>
<script src="./amplitude.js"></script>
<script>

<script>
amplitude.init('API_KEY', '[email protected]');
</script>
<body>
Expand Down
54 changes: 39 additions & 15 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,6 +72,14 @@ 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.config);
// Fetch the current campaign, check if need to track web attribution later
await this.webAttribution.init();
}

// Step 3: Set session ID
// Priority 1: `options.sessionId`
// Priority 2: last known sessionId from user identity storage
Expand Down Expand Up @@ -109,13 +118,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 +171,11 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {
return this.config?.sessionId;
}

setSessionId(sessionId: number) {
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 @@ -198,8 +199,20 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {
user_id: this.previousSessionUserId,
});
}

this.config.lastEventTime = this.config.sessionId;
}

// 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?.shouldTrackNewCampaign || shouldTrackNewCampaign) {
if (this.webAttribution) {
const campaignEvent = this.webAttribution.generateCampaignEvent(++lastEventId);
this.track(campaignEvent);
}
}

if (isSessionTrackingEnabled(this.config.defaultTracking)) {
this.track(DEFAULT_SESSION_START_EVENT, undefined, {
event_id: ++lastEventId,
session_id: this.config.sessionId,
Expand Down Expand Up @@ -261,16 +274,27 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient {
}

async process(event: Event) {
let shouldTrackNewCampaign = false;
let shouldSetSessionId = false;
if (this.webAttribution) {
shouldTrackNewCampaign = this.webAttribution.shouldTrackNewCampaign;
shouldSetSessionId = shouldTrackNewCampaign && !!this.webAttribution.options.resetSessionOnNewCampaign;
}

const currentTime = Date.now();
const isEventInNewSession = isNewSession(this.config.sessionTimeout, this.config.lastEventTime);

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) {
this.setSessionId(currentTime, shouldTrackNewCampaign);
} else if (this.webAttribution && !isEventInNewSession && shouldTrackNewCampaign) {
// web attribution should be track during the middle of the session if there has any new campaign
const campaignEvent = this.webAttribution.generateCampaignEvent();
this.track(campaignEvent);
}
}

return super.process(event);
Expand Down
57 changes: 57 additions & 0 deletions packages/analytics-browser/src/utils/web-attribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { BrowserConfig } from '@amplitude/analytics-types';
import { Campaign, Storage } from '@amplitude/analytics-types';
import {
Options,
getDefaultExcludedReferrers,
getStorageKey,
createCampaignEvent,
isNewCampaign,
CampaignParser,
} from '@amplitude/analytics-client-common';

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

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

async init() {
[this.currentCampaign, this.previousCampaign] = await this.fetchCampaign();
if (isNewCampaign(this.currentCampaign, this.previousCampaign, this.options)) {
this.shouldTrackNewCampaign = true;
await this.storage.set(this.storageKey, this.currentCampaign);
}
}

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
*/
generateCampaignEvent(event_id?: number) {
// Mark this campaign has been tracked
this.shouldTrackNewCampaign = false;
const campaignEvent = createCampaignEvent(this.currentCampaign, this.options);
if (event_id) {
campaignEvent.event_id = event_id;
}
return campaignEvent;
}
}
68 changes: 40 additions & 28 deletions packages/analytics-browser/test/browser-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import * as SnippetHelper from '../src/utils/snippet-helper';
import * as AnalyticsClientCommon from '@amplitude/analytics-client-common';
import * as fileDownloadTracking from '../src/plugins/file-download-tracking';
import * as formInteractionTracking from '../src/plugins/form-interaction-tracking';
import * as webAttributionPlugin from '@amplitude/plugin-web-attribution-browser';
import * as networkConnectivityChecker from '../src/plugins/network-connectivity-checker';
import * as pageViewTracking from '@amplitude/plugin-page-view-tracking-browser';

Expand Down Expand Up @@ -259,32 +258,6 @@ describe('browser-client', () => {
expect(formInteractionTrackingPlugin).toHaveBeenCalledTimes(0);
});

test('should add web attribution tracking plugin', async () => {
jest.spyOn(CookieMigration, 'parseLegacyCookies').mockResolvedValueOnce({
optOut: false,
lastEventTime: Date.now(),
});
const webAttributionPluginPlugin = jest.spyOn(webAttributionPlugin, 'webAttributionPlugin');
jest.spyOn(client, 'dispatch').mockReturnValueOnce(
Promise.resolve({
code: 200,
message: '',
event: {
event_type: 'event_type',
},
}),
);
await client.init(apiKey, userId, {
optOut: false,
defaultTracking: {
...defaultTracking,
attribution: {},
},
sessionId: Date.now(),
}).promise;
expect(webAttributionPluginPlugin).toHaveBeenCalledTimes(1);
});

test('should add network connectivity checker plugin by default', async () => {
const networkConnectivityCheckerPlugin = jest.spyOn(
networkConnectivityChecker,
Expand Down Expand Up @@ -640,6 +613,44 @@ describe('browser-client', () => {
expect(track).toHaveBeenCalledTimes(2);
});

test('should set session id with start and end session event and web attribution event', async () => {
jest.spyOn(CookieMigration, 'parseLegacyCookies').mockResolvedValueOnce({
optOut: false,
sessionId: 1,
lastEventId: 100,
lastEventTime: Date.now() - 1000,
});
const result = {
promise: Promise.resolve({
code: 200,
event: {
event_type: 'a',
},
message: 'success',
}),
};
const track = jest.spyOn(client, 'track').mockReturnValue(result);
await client.init(apiKey, {
sessionTimeout: 5000,
defaultTracking: {
...defaultTracking,
attribution: true,
pageViews: false,
sessions: true,
},
}).promise;

client.setSessionId(2);

expect(client.getSessionId()).toBe(2);
return new Promise<void>((resolve) => {
setTimeout(() => {
expect(track).toHaveBeenCalledTimes(3);
resolve();
}, 4000);
});
});

test('should defer set session id', () => {
return new Promise<void>((resolve) => {
void client.init(apiKey, { defaultTracking }).promise.then(() => {
Expand Down Expand Up @@ -669,6 +680,7 @@ describe('browser-client', () => {
// send an event
await client.track('test 1').promise;
const eventTime1 = client.config.lastEventTime ?? -1;
console.log(eventTime1);
expect(eventTime1 > 0).toBeTruthy();

// wait for session to almost expire, then extend it
Expand All @@ -692,7 +704,7 @@ describe('browser-client', () => {
setTimeout(async () => {
await client.track('test 2').promise;
resolve();
}, 15),
}, 10),
);

// assert session id is unchanged
Expand Down
Loading