From 1f76423bfa7e442fe786e369b6d90a0b1c58454c Mon Sep 17 00:00:00 2001 From: Ben Booth Date: Tue, 7 Apr 2026 13:13:35 +1000 Subject: [PATCH 1/2] feat(audience): scaffold @imtbl/pixel package with attribution, loader, and snippet Add the pixel package scaffold and three self-contained modules that have no dependency on PR #2824. The package builds to a single IIFE bundle (dist/imtbl.js) targeting <10KB gzipped (currently 823 bytes). Modules: - attribution: UTM params, ad click IDs, referrer, landing page (session-cached) - loader: command-queue pattern (window.__imtbl) with pre-load replay - snippet: embeddable '); + expect(html).toContain('https://cdn.immutable.com/pixel/v1/imtbl.js'); + expect(html).toContain('"key":"pk_test_123"'); + }); + + it('uses a custom CDN URL when provided', () => { + const html = generateSnippet({ + key: 'pk_test_123', + cdnUrl: 'https://cdn.dev.immutable.com/pixel/v1/imtbl.js', + }); + + expect(html).toContain('https://cdn.dev.immutable.com/pixel/v1/imtbl.js'); + expect(html).not.toContain('https://cdn.immutable.com/pixel/v1/imtbl.js'); + }); + + it('includes consent level when provided', () => { + const html = generateSnippet({ key: 'pk_test_123', consent: 'anonymous' }); + + expect(html).toContain('"consent":"anonymous"'); + }); + + it('omits consent from init args when not provided', () => { + const html = generateSnippet({ key: 'pk_test_123' }); + + expect(html).not.toContain('consent'); + }); + + it('creates the __imtbl stub array and pushes init command', () => { + const html = generateSnippet({ key: 'pk_test_123' }); + + expect(html).toContain('w[i]=w[i]||[]'); + expect(html).toContain('w[i].push(["init"'); + }); + + it('loads the script asynchronously', () => { + const html = generateSnippet({ key: 'pk_test_123' }); + + expect(html).toContain('s.async=1'); + expect(html).toContain('document.head.appendChild(s)'); + }); +}); diff --git a/packages/audience/pixel/src/snippet.ts b/packages/audience/pixel/src/snippet.ts new file mode 100644 index 0000000000..ef5c7d59bb --- /dev/null +++ b/packages/audience/pixel/src/snippet.ts @@ -0,0 +1,29 @@ +const DEFAULT_CDN_URL = 'https://cdn.immutable.com/pixel/v1/imtbl.js'; + +export interface SnippetOptions { + key: string; + cdnUrl?: string; + consent?: 'none' | 'anonymous' | 'full'; +} + +export function generateSnippet(options: SnippetOptions): string { + const { key, cdnUrl = DEFAULT_CDN_URL, consent } = options; + + const initArgs: Record = { key }; + if (consent) { + initArgs.consent = consent; + } + + const argsJson = JSON.stringify(initArgs); + + return [ + '', + ].join(''); +} diff --git a/packages/audience/pixel/tsconfig.eslint.json b/packages/audience/pixel/tsconfig.eslint.json new file mode 100644 index 0000000000..7a70f2c77d --- /dev/null +++ b/packages/audience/pixel/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": [] +} diff --git a/packages/audience/pixel/tsconfig.json b/packages/audience/pixel/tsconfig.json new file mode 100644 index 0000000000..4840acb09d --- /dev/null +++ b/packages/audience/pixel/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDirs": ["src"], + "customConditions": ["development"] + }, + "include": ["src"], + "exclude": ["dist", "node_modules", "src/**/*.test.ts"] +} diff --git a/packages/audience/pixel/tsup.config.ts b/packages/audience/pixel/tsup.config.ts new file mode 100644 index 0000000000..9c12136721 --- /dev/null +++ b/packages/audience/pixel/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: 'dist', + format: ['iife'], + globalName: '__imtblPixelInternal', + platform: 'browser', + target: 'es2020', + minify: true, + treeshake: true, + splitting: false, + sourcemap: false, + clean: true, + outExtension: () => ({ js: '.js' }), + esbuildOptions(options) { + options.outbase = 'src'; + options.entryNames = 'imtbl'; + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64014f7b3e..b643082044 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,7 +138,7 @@ importers: version: 0.25.21(@emotion/react@11.11.3(@types/react@18.3.12)(react@18.3.1))(@rive-app/react-canvas-lite@4.9.0(react@18.3.1))(embla-carousel-react@8.1.5(react@18.3.1))(framer-motion@11.18.2(@emotion/is-prop-valid@0.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@imtbl/sdk': specifier: latest - version: 2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10) + version: 2.12.6(typescript@5.6.2) next: specifier: 14.2.25 version: 14.2.25(@babel/core@7.26.10)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1031,6 +1031,33 @@ importers: specifier: ^5.6.2 version: 5.6.2 + packages/audience/pixel: + devDependencies: + '@swc/core': + specifier: ^1.4.2 + version: 1.15.3(@swc/helpers@0.5.15) + '@swc/jest': + specifier: ^0.2.37 + version: 0.2.37(@swc/core@1.15.3(@swc/helpers@0.5.15)) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.14 + eslint: + specifier: ^8.56.0 + version: 8.57.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + jest-environment-jsdom: + specifier: ^29.4.3 + version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + tsup: + specifier: ^8.3.0 + version: 8.3.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(jiti@1.21.0)(postcss@8.4.49)(typescript@5.6.2)(yaml@2.5.0) + typescript: + specifier: ^5.6.2 + version: 5.6.2 + packages/auth: dependencies: '@imtbl/generated-clients': @@ -19682,7 +19709,7 @@ snapshots: '@confio/ics23@0.6.8': dependencies: - '@noble/hashes': 1.5.0 + '@noble/hashes': 1.8.0 protobufjs: 6.11.4 '@cosmjs/amino@0.31.3': @@ -19721,7 +19748,7 @@ snapshots: '@cosmjs/encoding': 0.31.3 '@cosmjs/math': 0.31.3 '@cosmjs/utils': 0.31.3 - '@noble/hashes': 1.5.0 + '@noble/hashes': 1.8.0 bn.js: 5.2.1 elliptic: 6.6.1 libsodium-wrappers-sumo: 0.7.15 @@ -19731,7 +19758,7 @@ snapshots: '@cosmjs/encoding': 0.32.4 '@cosmjs/math': 0.32.4 '@cosmjs/utils': 0.32.4 - '@noble/hashes': 1.5.0 + '@noble/hashes': 1.8.0 bn.js: 5.2.1 elliptic: 6.6.1 libsodium-wrappers-sumo: 0.7.15 @@ -20000,11 +20027,11 @@ snapshots: '@emnapi/core@1.2.0': dependencies: '@emnapi/wasi-threads': 1.0.1 - tslib: 2.7.0 + tslib: 2.8.1 '@emnapi/runtime@1.2.0': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 '@emnapi/runtime@1.8.1': dependencies: @@ -20013,7 +20040,7 @@ snapshots: '@emnapi/wasi-threads@1.0.1': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 '@emotion/babel-plugin@11.11.0': dependencies: @@ -20764,7 +20791,7 @@ snapshots: transitivePeerDependencies: - debug - '@imtbl/bridge-sdk@2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@imtbl/bridge-sdk@2.12.6': dependencies: '@imtbl/config': 2.12.6 '@jest/globals': 29.7.0 @@ -20776,16 +20803,16 @@ snapshots: - supports-color - utf-8-validate - '@imtbl/checkout-sdk@2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)': + '@imtbl/checkout-sdk@2.12.6(typescript@5.6.2)': dependencies: '@imtbl/blockchain-data': 2.12.6 - '@imtbl/bridge-sdk': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@imtbl/bridge-sdk': 2.12.6 '@imtbl/config': 2.12.6 - '@imtbl/dex-sdk': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@imtbl/dex-sdk': 2.12.6 '@imtbl/generated-clients': 2.12.6 '@imtbl/metrics': 2.12.6 - '@imtbl/orderbook': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@imtbl/passport': 2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10) + '@imtbl/orderbook': 2.12.6 + '@imtbl/passport': 2.12.6(typescript@5.6.2) '@metamask/detect-provider': 2.0.0 axios: 1.7.7 ethers: 6.13.5(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -20847,7 +20874,7 @@ snapshots: - typescript - utf-8-validate - '@imtbl/dex-sdk@2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@imtbl/dex-sdk@2.12.6': dependencies: '@imtbl/config': 2.12.6 '@uniswap/sdk-core': 3.2.3 @@ -20889,7 +20916,7 @@ snapshots: - debug - pg-native - '@imtbl/orderbook@2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@imtbl/orderbook@2.12.6': dependencies: '@imtbl/config': 2.12.6 '@imtbl/metrics': 2.12.6 @@ -20903,16 +20930,16 @@ snapshots: - debug - utf-8-validate - '@imtbl/passport@2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)': + '@imtbl/passport@2.12.6(typescript@5.6.2)': dependencies: '@imtbl/auth': 2.12.6 '@imtbl/config': 2.12.6 '@imtbl/generated-clients': 2.12.6 '@imtbl/metrics': 2.12.6 - '@imtbl/toolkit': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@imtbl/wallet': 2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10) - '@imtbl/x-client': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@imtbl/x-provider': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@imtbl/toolkit': 2.12.6 + '@imtbl/wallet': 2.12.6(typescript@5.6.2) + '@imtbl/x-client': 2.12.6 + '@imtbl/x-provider': 2.12.6 ethers: 6.13.5(bufferutil@4.0.8)(utf-8-validate@5.0.10) localforage: 1.10.0 oidc-client-ts: 3.4.1 @@ -20931,19 +20958,19 @@ snapshots: - encoding - supports-color - '@imtbl/sdk@2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)': + '@imtbl/sdk@2.12.6(typescript@5.6.2)': dependencies: '@imtbl/auth': 2.12.6 '@imtbl/blockchain-data': 2.12.6 - '@imtbl/checkout-sdk': 2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10) + '@imtbl/checkout-sdk': 2.12.6(typescript@5.6.2) '@imtbl/config': 2.12.6 '@imtbl/minting-backend': 2.12.6 - '@imtbl/orderbook': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@imtbl/passport': 2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10) - '@imtbl/wallet': 2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10) + '@imtbl/orderbook': 2.12.6 + '@imtbl/passport': 2.12.6(typescript@5.6.2) + '@imtbl/wallet': 2.12.6(typescript@5.6.2) '@imtbl/webhook': 2.12.6 - '@imtbl/x-client': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@imtbl/x-provider': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@imtbl/x-client': 2.12.6 + '@imtbl/x-provider': 2.12.6 transitivePeerDependencies: - bufferutil - debug @@ -20954,9 +20981,9 @@ snapshots: - utf-8-validate - zod - '@imtbl/toolkit@2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@imtbl/toolkit@2.12.6': dependencies: - '@imtbl/x-client': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@imtbl/x-client': 2.12.6 '@metamask/detect-provider': 2.0.0 axios: 1.7.7 bn.js: 5.2.1 @@ -20968,7 +20995,7 @@ snapshots: - debug - utf-8-validate - '@imtbl/wallet@2.12.6(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)': + '@imtbl/wallet@2.12.6(typescript@5.6.2)': dependencies: '@imtbl/auth': 2.12.6 '@imtbl/generated-clients': 2.12.6 @@ -20989,7 +21016,7 @@ snapshots: transitivePeerDependencies: - debug - '@imtbl/x-client@2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@imtbl/x-client@2.12.6': dependencies: '@ethereumjs/wallet': 2.0.4 '@imtbl/config': 2.12.6 @@ -21005,12 +21032,12 @@ snapshots: - debug - utf-8-validate - '@imtbl/x-provider@2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@imtbl/x-provider@2.12.6': dependencies: '@imtbl/config': 2.12.6 '@imtbl/generated-clients': 2.12.6 - '@imtbl/toolkit': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@imtbl/x-client': 2.12.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@imtbl/toolkit': 2.12.6 + '@imtbl/x-client': 2.12.6 '@metamask/detect-provider': 2.0.0 axios: 1.7.7 enc-utils: 3.0.0 @@ -21825,7 +21852,7 @@ snapshots: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.1.0 '@noble/hashes': 1.8.0 - '@scure/base': 1.1.7 + '@scure/base': 1.2.6 '@types/debug': 4.1.8 debug: 4.3.7(supports-color@8.1.1) pony-cause: 2.1.11 @@ -21839,7 +21866,7 @@ snapshots: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.1.0 '@noble/hashes': 1.8.0 - '@scure/base': 1.1.7 + '@scure/base': 1.2.6 '@types/debug': 4.1.8 debug: 4.3.7(supports-color@8.1.1) pony-cause: 2.1.11 @@ -21870,7 +21897,7 @@ snapshots: '@motionone/easing': 10.17.0 '@motionone/types': 10.17.0 '@motionone/utils': 10.17.0 - tslib: 2.7.0 + tslib: 2.8.1 '@motionone/dom@10.17.0': dependencies: @@ -21879,23 +21906,23 @@ snapshots: '@motionone/types': 10.17.0 '@motionone/utils': 10.17.0 hey-listen: 1.0.8 - tslib: 2.7.0 + tslib: 2.8.1 '@motionone/easing@10.17.0': dependencies: '@motionone/utils': 10.17.0 - tslib: 2.7.0 + tslib: 2.8.1 '@motionone/generators@10.17.0': dependencies: '@motionone/types': 10.17.0 '@motionone/utils': 10.17.0 - tslib: 2.7.0 + tslib: 2.8.1 '@motionone/svelte@10.16.4': dependencies: '@motionone/dom': 10.17.0 - tslib: 2.7.0 + tslib: 2.8.1 '@motionone/types@10.17.0': {} @@ -21903,12 +21930,12 @@ snapshots: dependencies: '@motionone/types': 10.17.0 hey-listen: 1.0.8 - tslib: 2.7.0 + tslib: 2.8.1 '@motionone/vue@10.16.4': dependencies: '@motionone/dom': 10.17.0 - tslib: 2.7.0 + tslib: 2.8.1 '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2': optional: true @@ -23157,7 +23184,7 @@ snapshots: is-glob: 4.0.3 open: 9.1.0 picocolors: 1.1.1 - tslib: 2.7.0 + tslib: 2.8.1 '@playwright/test@1.45.3': dependencies: @@ -24894,7 +24921,7 @@ snapshots: '@swc/helpers@0.5.13': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 '@swc/helpers@0.5.15': dependencies: @@ -25010,7 +25037,7 @@ snapshots: '@tybys/wasm-util@0.9.0': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 '@typechain/ethers-v6@0.5.1(ethers@6.13.5(bufferutil@4.0.8)(utf-8-validate@5.0.10))(typechain@8.3.0(typescript@5.6.2))(typescript@5.6.2)': dependencies: @@ -27243,7 +27270,7 @@ snapshots: camel-case@4.1.2: dependencies: pascal-case: 3.1.2 - tslib: 2.7.0 + tslib: 2.8.1 camelcase-css@2.0.1: {} @@ -28367,7 +28394,7 @@ snapshots: dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 dotenv-expand@11.0.6: dependencies: @@ -33912,7 +33939,7 @@ snapshots: no-case@3.0.4: dependencies: lower-case: 2.0.2 - tslib: 2.7.0 + tslib: 2.8.1 nocache@3.0.4: {} @@ -34451,7 +34478,7 @@ snapshots: param-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 parcel@2.16.3(@swc/helpers@0.5.15): dependencies: @@ -34511,7 +34538,7 @@ snapshots: pascal-case@3.1.2: dependencies: no-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 pascalcase@0.1.1: {} @@ -36749,7 +36776,7 @@ snapshots: snake-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 snapdragon-node@2.1.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8043dcd6aa..3366bd5f10 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,6 +25,7 @@ packages: - "packages/checkout/widgets-lib" - "packages/blockchain-data/sdk" - "packages/audience/core" + - "packages/audience/pixel" - "packages/game-bridge" - "packages/webhook/sdk" - "packages/minting-backend/sdk" From 09736d77d971e96f432cc072d119274bbddb0cad Mon Sep 17 00:00:00 2001 From: Ben Booth Date: Tue, 7 Apr 2026 14:38:06 +1000 Subject: [PATCH 2/2] fix(audience): add missing click IDs and attribution fields per event reference Add dclid (Google DV360) and li_fat_id (LinkedIn) to match the Tracking Pixel Event Reference doc. Also add referral_code parsing and touchpoint_type derivation (set to 'click' when UTMs or click IDs are present). Co-Authored-By: Claude Opus 4.6 --- .../audience/pixel/src/attribution.test.ts | 41 ++++++++++++++++--- packages/audience/pixel/src/attribution.ts | 16 ++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/audience/pixel/src/attribution.test.ts b/packages/audience/pixel/src/attribution.test.ts index 6bd82647a1..b742260a35 100644 --- a/packages/audience/pixel/src/attribution.test.ts +++ b/packages/audience/pixel/src/attribution.test.ts @@ -31,14 +31,16 @@ describe('collectAttribution', () => { it('parses ad network click IDs', () => { setLocation( - 'https://example.com/?gclid=abc123&fbclid=fb456&ttclid=tt789&msclkid=ms000', + 'https://example.com/?gclid=abc&dclid=dc1&fbclid=fb2&ttclid=tt3&msclkid=ms4&li_fat_id=li5', ); const result = collectAttribution(); - expect(result.gclid).toBe('abc123'); - expect(result.fbclid).toBe('fb456'); - expect(result.ttclid).toBe('tt789'); - expect(result.msclkid).toBe('ms000'); + expect(result.gclid).toBe('abc'); + expect(result.dclid).toBe('dc1'); + expect(result.fbclid).toBe('fb2'); + expect(result.ttclid).toBe('tt3'); + expect(result.msclkid).toBe('ms4'); + expect(result.li_fat_id).toBe('li5'); }); it('captures referrer and landing page', () => { @@ -65,6 +67,35 @@ describe('collectAttribution', () => { expect(second.utm_source).toBe('google'); }); + it('parses referral_code from the URL', () => { + setLocation('https://example.com/?referral_code=PARTNER42'); + + const result = collectAttribution(); + 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(); + 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(); + 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(); + 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 }); diff --git a/packages/audience/pixel/src/attribution.ts b/packages/audience/pixel/src/attribution.ts index 61ffe26b31..0046ba054d 100644 --- a/packages/audience/pixel/src/attribution.ts +++ b/packages/audience/pixel/src/attribution.ts @@ -8,9 +8,11 @@ const UTM_PARAMS = [ const CLICK_ID_PARAMS = [ 'gclid', + 'dclid', 'fbclid', 'ttclid', 'msclkid', + 'li_fat_id', ] as const; const STORAGE_KEY = '__imtbl_attribution'; @@ -22,11 +24,15 @@ export interface Attribution { utm_content?: string; utm_term?: string; gclid?: string; + dclid?: string; fbclid?: string; ttclid?: string; msclkid?: string; + li_fat_id?: string; + referral_code?: string; referrer?: string; landing_page?: string; + touchpoint_type?: string; } type AttributionKey = keyof Attribution; @@ -46,6 +52,12 @@ function parseParams(url: string): Attribution { result[key as AttributionKey] = value; } } + + const referralCode = params.get('referral_code'); + if (referralCode) { + result.referral_code = referralCode; + } + return result; } @@ -79,10 +91,14 @@ export function collectAttribution(): Attribution { ? 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 = { ...urlParams, referrer, landing_page: landingPage, + touchpoint_type: hasClickId || hasUtm ? 'click' : undefined, }; saveToStorage(attribution);