Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 60 additions & 14 deletions packages/audience/core/src/attribution.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { collectAttribution, clearAttribution } from './attribution';
import { collectSessionAttribution, collectPageAttribution, clearAttribution } from './attribution';

const STORAGE_KEY = '__imtbl_attribution';

Expand All @@ -15,13 +15,13 @@ function setLocation(url: string) {
});
}

describe('collectAttribution', () => {
describe('collectSessionAttribution', () => {
it('parses UTM parameters from the URL', () => {
setLocation(
'https://example.com/?utm_source=google&utm_medium=cpc&utm_campaign=spring&utm_content=banner&utm_term=nft',
);

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.utm_source).toBe('google');
expect(result.utm_medium).toBe('cpc');
expect(result.utm_campaign).toBe('spring');
Expand All @@ -34,7 +34,7 @@ describe('collectAttribution', () => {
'https://example.com/?gclid=abc&dclid=dc1&fbclid=fb2&ttclid=tt3&msclkid=ms4&li_fat_id=li5',
);

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.gclid).toBe('abc');
expect(result.dclid).toBe('dc1');
expect(result.fbclid).toBe('fb2');
Expand All @@ -50,57 +50,57 @@ describe('collectAttribution', () => {
configurable: true,
});

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.referrer).toBe('https://google.com/search?q=nft');
expect(result.landing_page).toBe('https://game.example.com/landing');
});

it('caches in sessionStorage and returns cached on second call', () => {
setLocation('https://example.com/?utm_source=google');

const first = collectAttribution();
const first = collectSessionAttribution();
expect(first.utm_source).toBe('google');

// Change URL — should still return cached value
setLocation('https://example.com/?utm_source=facebook');
const second = collectAttribution();
const second = collectSessionAttribution();
expect(second.utm_source).toBe('google');
});

it('parses referral_code from the URL', () => {
setLocation('https://example.com/?referral_code=PARTNER42');

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.referral_code).toBe('PARTNER42');
});

it('sets touchpoint_type to click when UTMs are present', () => {
setLocation('https://example.com/?utm_source=google');

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.touchpoint_type).toBe('click');
});

it('sets touchpoint_type to click when a click ID is present', () => {
setLocation('https://example.com/?gclid=abc123');

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.touchpoint_type).toBe('click');
});

it('does not set touchpoint_type when no UTMs or click IDs are present', () => {
setLocation('https://example.com/');
Object.defineProperty(document, 'referrer', { value: 'https://other.com', configurable: true });

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.touchpoint_type).toBeUndefined();
});

it('returns empty attribution when no params are present', () => {
setLocation('https://example.com/');
Object.defineProperty(document, 'referrer', { value: '', configurable: true });

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.utm_source).toBeUndefined();
expect(result.gclid).toBeUndefined();
expect(result.referrer).toBeUndefined();
Expand All @@ -115,15 +115,61 @@ describe('collectAttribution', () => {
throw new Error('storage disabled');
});

const result = collectAttribution();
const result = collectSessionAttribution();
expect(result.utm_source).toBe('twitter');
});
});

describe('collectPageAttribution', () => {
it('always parses from the current URL, ignoring sessionStorage', () => {
setLocation('https://example.com/?utm_source=google');
collectSessionAttribution(); // seeds sessionStorage

// Change URL — collectSessionAttribution would return cached 'google',
// but collectPageAttribution reads the new URL.
setLocation('https://example.com/?utm_source=facebook');
const result = collectPageAttribution();
expect(result.utm_source).toBe('facebook');
});

it('does not write to sessionStorage', () => {
setLocation('https://example.com/?utm_source=twitter');

collectPageAttribution();
expect(sessionStorage.getItem(STORAGE_KEY)).toBeNull();
});

it('does not include landing_page', () => {
setLocation('https://example.com/?utm_source=google');

const result = collectPageAttribution();
expect(result.utm_source).toBe('google');
expect(result.landing_page).toBeUndefined();
});

it('sets touchpoint_type to click when UTMs are present', () => {
setLocation('https://example.com/?utm_source=google');

const result = collectPageAttribution();
expect(result.touchpoint_type).toBe('click');
});

it('captures referrer', () => {
setLocation('https://example.com/');
Object.defineProperty(document, 'referrer', {
value: 'https://google.com/',
configurable: true,
});

const result = collectPageAttribution();
expect(result.referrer).toBe('https://google.com/');
});
});

describe('clearAttribution', () => {
it('removes cached attribution from sessionStorage', () => {
setLocation('https://example.com/?utm_source=google');
collectAttribution();
collectSessionAttribution();
expect(sessionStorage.getItem(STORAGE_KEY)).not.toBeNull();

clearAttribution();
Expand Down
34 changes: 25 additions & 9 deletions packages/audience/core/src/attribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,33 +78,49 @@ function saveToStorage(attribution: Attribution): void {
}
}

export function collectAttribution(): Attribution {
const cached = loadFromStorage();
if (cached) return cached;

function buildAttribution(): Attribution {
const urlParams = typeof window !== 'undefined' && window.location
? parseParams(window.location.href)
: {};

const referrer = typeof document !== 'undefined' ? document.referrer || undefined : undefined;
const landingPage = typeof window !== 'undefined' && window.location
? window.location.href
: undefined;

const hasClickId = CLICK_ID_PARAMS.some((key) => key in urlParams);
const hasUtm = UTM_PARAMS.some((key) => key in urlParams);

const attribution: Attribution = {
return {
...urlParams,
referrer,
landing_page: landingPage,
touchpoint_type: hasClickId || hasUtm ? 'click' : undefined,
};
}

export function collectSessionAttribution(): Attribution {
const cached = loadFromStorage();
if (cached) return cached;

const landingPage = typeof window !== 'undefined' && window.location
? window.location.href
: undefined;

const attribution: Attribution = {
...buildAttribution(),
landing_page: landingPage,
};

saveToStorage(attribution);
return attribution;
}

/**
* Parse attribution from the current URL without reading or writing
* sessionStorage. Returns the UTM / click-ID params on the URL right
* now, not the ones cached at session start.
*/
export function collectPageAttribution(): Attribution {
return buildAttribution();
}

export function clearAttribution(): void {
try {
sessionStorage.removeItem(STORAGE_KEY);
Expand Down
2 changes: 1 addition & 1 deletion packages/audience/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export { isTimestampValid, isAliasValid, truncate } from './validation';
export { getOrCreateSession } from './session';
export type { SessionResult } from './session';

export { collectAttribution } from './attribution';
export { collectSessionAttribution, collectPageAttribution } from './attribution';
export type { Attribution } from './attribution';

export {
Expand Down
2 changes: 1 addition & 1 deletion packages/audience/pixel/src/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jest.mock('@imtbl/audience-core', () => ({
isBrowser: jest.fn().mockReturnValue(true),
getCookie: jest.fn(),
setCookie: jest.fn(),
collectAttribution: jest.fn().mockReturnValue({}),
collectSessionAttribution: jest.fn().mockReturnValue({}),
getOrCreateSession: jest.fn().mockReturnValue({ sessionId: 's', isNew: false }),
createConsentManager: jest.fn().mockReturnValue({ level: 'none', setLevel: jest.fn() }),
}));
Expand Down
2 changes: 1 addition & 1 deletion packages/audience/pixel/src/pixel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jest.mock('@imtbl/audience-core', () => ({
isBrowser: jest.fn().mockReturnValue(true),
getCookie: jest.fn(),
setCookie: jest.fn(),
collectAttribution: jest.fn().mockReturnValue({
collectSessionAttribution: jest.fn().mockReturnValue({
utm_source: 'google',
landing_page: 'https://example.com',
}),
Expand Down
4 changes: 2 additions & 2 deletions packages/audience/pixel/src/pixel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getTimestamp,
isBrowser,
getCookie,
collectAttribution,
collectSessionAttribution,
getOrCreateSession,
createConsentManager,
canTrack,
Expand Down Expand Up @@ -132,7 +132,7 @@ export class Pixel {

const { sessionId, isNew } = getOrCreateSession(this.domain);
this.refreshSession(sessionId, isNew);
const attribution = collectAttribution();
const attribution = collectSessionAttribution();
const thirdPartyIds = this.collectThirdPartyIds();

const message: PageMessage = {
Expand Down
4 changes: 4 additions & 0 deletions packages/audience/sdk/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const config: Config = {
},
moduleNameMapper: {
'^@imtbl/audience-core$': '<rootDir>/../core/src/index.ts',
// The core source code imports other @imtbl packages (e.g. metrics).
// This tells jest where to find them so we can run tests without
// building the whole monorepo first.
'^@imtbl/(.*)$': '<rootDir>/../../../node_modules/@imtbl/$1/src',
Comment thread
nattb8 marked this conversation as resolved.
},
};

Expand Down
Loading
Loading