Skip to content

Commit 336f06f

Browse files
authored
Merge pull request #62 from dubinc/track-lead-sale
add `trackLead` and `trackSale` method
2 parents ff7776e + 5dac70b commit 336f06f

12 files changed

Lines changed: 365 additions & 15 deletions

File tree

apps/html/conversion-tracking.html

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="description" content="Your website description" />
7+
<title>Client Conversion Tracking</title>
8+
</head>
9+
<body>
10+
<script>
11+
!(function (w, da) {
12+
w[da] =
13+
w[da] ||
14+
function () {
15+
(w[da].q = w[da].q || []).push(arguments);
16+
};
17+
18+
['trackClick', 'trackLead', 'trackSale'].forEach(function (m) {
19+
w[da][m] = function () {
20+
w[da](m, ...arguments);
21+
};
22+
});
23+
})(window, 'dubAnalytics');
24+
</script>
25+
26+
<script>
27+
// Add a 3-second delay before loading the script to test queue functionality
28+
setTimeout(() => {
29+
const script = document.createElement('script');
30+
script.defer = true;
31+
script.src = './analytics/script.conversion-tracking.js';
32+
script.setAttribute('data-api-host', 'http://api.localhost:8888');
33+
script.setAttribute(
34+
'data-publishable-key',
35+
'dub_pk_BgyBCEJCPCGN3RN7oieLVHRs',
36+
);
37+
document.head.appendChild(script);
38+
}, 3000);
39+
</script>
40+
41+
<header>
42+
<h1>Client Conversion Tracking</h1>
43+
<button
44+
onclick="dubAnalytics.trackLead({
45+
eventName: 'Account created',
46+
customerExternalId: '1234567890',
47+
customerName: 'John Doe',
48+
customerEmail: 'john.doe@example.com',
49+
})"
50+
>
51+
Track Lead
52+
</button>
53+
54+
<button
55+
onclick="dubAnalytics.trackSale({
56+
eventName: 'Purchase completed',
57+
customerExternalId: 'CXvG5QOLi8QKBA2jYmDh',
58+
invoiceId: Math.random().toString(36).substring(2, 15),
59+
amount: 5000, // defaults to usd cents, use `currency` prop to specify a different currency
60+
})"
61+
>
62+
Track Sale
63+
</button>
64+
</header>
65+
</body>
66+
</html>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client';
2+
3+
import { useAnalytics } from '@dub/analytics/react';
4+
5+
export function ConversionTrackingPageClient() {
6+
const { trackLead, trackSale } = useAnalytics();
7+
8+
const handleTrackLead = () => {
9+
trackLead({
10+
eventName: 'Account created',
11+
customerExternalId: '1234567890',
12+
});
13+
};
14+
15+
const handleTrackSale = () => {
16+
trackSale({
17+
eventName: 'Purchase completed',
18+
customerExternalId: 'CXvG5QOLi8QKBA2jYmDh',
19+
amount: 5000, // defaults to usd cents, use `currency` prop to specify a different currency
20+
});
21+
};
22+
23+
return (
24+
<div className="flex gap-4 p-4">
25+
<button
26+
onClick={handleTrackLead}
27+
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
28+
>
29+
Track Lead
30+
</button>
31+
<button
32+
onClick={handleTrackSale}
33+
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
34+
>
35+
Track Sale
36+
</button>
37+
</div>
38+
);
39+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ConversionTrackingPageClient } from './page-client';
2+
3+
export default function ConversionTrackingPage() {
4+
return <ConversionTrackingPageClient />;
5+
}

packages/script/build.js

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ fs.copyFileSync(
2424
path.join(__dirname, 'dist/_redirects'),
2525
);
2626

27-
// Build all variants
27+
// Build all variants (8 total combinations of 3 extensions)
2828
Promise.all([
29-
// Base script
29+
// 1. Base script (no extensions)
3030
esbuild.build({
3131
...baseConfig,
3232
stdin: {
@@ -37,7 +37,7 @@ Promise.all([
3737
outfile: 'dist/analytics/script.js',
3838
}),
3939

40-
// Site visit tracking
40+
// 2. Site visit only
4141
esbuild.build({
4242
...baseConfig,
4343
stdin: {
@@ -48,7 +48,7 @@ Promise.all([
4848
outfile: 'dist/analytics/script.site-visit.js',
4949
}),
5050

51-
// Outbound domains tracking
51+
// 3. Outbound domains only
5252
esbuild.build({
5353
...baseConfig,
5454
stdin: {
@@ -62,7 +62,21 @@ Promise.all([
6262
outfile: 'dist/analytics/script.outbound-domains.js',
6363
}),
6464

65-
// Complete script with concatenated feature names
65+
// 4. Conversion tracking only
66+
esbuild.build({
67+
...baseConfig,
68+
stdin: {
69+
contents: combineFiles([
70+
'src/base.js',
71+
'src/extensions/conversion-tracking.js',
72+
]),
73+
resolveDir: __dirname,
74+
sourcefile: 'combined.js',
75+
},
76+
outfile: 'dist/analytics/script.conversion-tracking.js',
77+
}),
78+
79+
// 5. Site visit + Outbound domains
6680
esbuild.build({
6781
...baseConfig,
6882
stdin: {
@@ -76,4 +90,51 @@ Promise.all([
7690
},
7791
outfile: 'dist/analytics/script.site-visit.outbound-domains.js',
7892
}),
93+
94+
// 6. Site visit + Conversion tracking
95+
esbuild.build({
96+
...baseConfig,
97+
stdin: {
98+
contents: combineFiles([
99+
'src/base.js',
100+
'src/extensions/site-visit.js',
101+
'src/extensions/conversion-tracking.js',
102+
]),
103+
resolveDir: __dirname,
104+
sourcefile: 'combined.js',
105+
},
106+
outfile: 'dist/analytics/script.site-visit.conversion-tracking.js',
107+
}),
108+
109+
// 7. Outbound domains + Conversion tracking
110+
esbuild.build({
111+
...baseConfig,
112+
stdin: {
113+
contents: combineFiles([
114+
'src/base.js',
115+
'src/extensions/outbound-domains.js',
116+
'src/extensions/conversion-tracking.js',
117+
]),
118+
resolveDir: __dirname,
119+
sourcefile: 'combined.js',
120+
},
121+
outfile: 'dist/analytics/script.outbound-domains.conversion-tracking.js',
122+
}),
123+
124+
// 8. All extensions combined
125+
esbuild.build({
126+
...baseConfig,
127+
stdin: {
128+
contents: combineFiles([
129+
'src/base.js',
130+
'src/extensions/site-visit.js',
131+
'src/extensions/outbound-domains.js',
132+
'src/extensions/conversion-tracking.js',
133+
]),
134+
resolveDir: __dirname,
135+
sourcefile: 'combined.js',
136+
},
137+
outfile:
138+
'dist/analytics/script.site-visit.outbound-domains.conversion-tracking.js',
139+
}),
79140
]).catch(() => process.exit(1));

packages/script/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
{
22
"name": "@dub/analytics-script",
3-
"version": "0.0.29",
3+
"version": "0.0.30",
44
"main": "src/index.js",
55
"files": [
66
"dist/analytics/script.js",
77
"dist/analytics/script.site-visit.js",
88
"dist/analytics/script.outbound-domains.js",
9-
"dist/analytics/script.site-visit.outbound-domains.js"
9+
"dist/analytics/script.conversion-tracking.js",
10+
"dist/analytics/script.site-visit.outbound-domains.js",
11+
"dist/analytics/script.site-visit.conversion-tracking.js",
12+
"dist/analytics/script.outbound-domains.conversion-tracking.js",
13+
"dist/analytics/script.site-visit.outbound-domains.conversion-tracking.js"
1014
],
1115
"scripts": {
1216
"prebuild": "mkdir -p dist/analytics",

packages/script/src/base.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
// Common script attributes
1111
const API_HOST = script.getAttribute('data-api-host') || 'https://api.dub.co';
12+
const PUBLISHABLE_KEY = script.getAttribute('data-publishable-key');
1213
const COOKIE_OPTIONS = (() => {
1314
const defaultOptions = {
1415
domain:
@@ -271,6 +272,8 @@
271272
p: QUERY_PARAM, // was QUERY_PARAM
272273
v: QUERY_PARAM_VALUE, // was QUERY_PARAM_VALUE
273274
n: DOMAINS_CONFIG, // was DOMAINS_CONFIG
275+
k: PUBLISHABLE_KEY,
276+
qm: queueManager,
274277
};
275278

276279
// Initialize
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
const initConversionTracking = () => {
2+
const {
3+
a: API_HOST,
4+
k: PUBLISHABLE_KEY,
5+
c: cookieManager,
6+
i: DUB_ID_VAR,
7+
} = window._dubAnalytics || {};
8+
9+
if (!API_HOST) {
10+
console.warn('[dubAnalytics] Missing API_HOST');
11+
return;
12+
}
13+
14+
if (!PUBLISHABLE_KEY) {
15+
console.warn('[dubAnalytics] Missing PUBLISHABLE_KEY');
16+
return;
17+
}
18+
19+
// Track lead conversion
20+
const trackLead = async (input) => {
21+
const clickId = cookieManager?.get(DUB_ID_VAR);
22+
23+
const requestBody = {
24+
...(clickId && { clickId }),
25+
...input,
26+
};
27+
28+
const response = await fetch(`${API_HOST}/track/lead/client`, {
29+
method: 'POST',
30+
headers: {
31+
'Content-Type': 'application/json',
32+
Authorization: `Bearer ${PUBLISHABLE_KEY}`,
33+
},
34+
body: JSON.stringify(requestBody),
35+
});
36+
37+
const result = await response.json();
38+
39+
if (!response.ok) {
40+
console.error('[dubAnalytics] trackLead failed', result.error);
41+
}
42+
43+
return result;
44+
};
45+
46+
// Track sale conversion
47+
const trackSale = async (input) => {
48+
const response = await fetch(`${API_HOST}/track/sale/client`, {
49+
method: 'POST',
50+
headers: {
51+
'Content-Type': 'application/json',
52+
Authorization: `Bearer ${PUBLISHABLE_KEY}`,
53+
},
54+
body: JSON.stringify(input),
55+
});
56+
57+
const result = await response.json();
58+
59+
if (!response.ok) {
60+
console.error('[dubAnalytics] trackSale failed', result.error);
61+
}
62+
63+
return result;
64+
};
65+
66+
// Add methods to the global dubAnalytics object for direct calls
67+
if (window.dubAnalytics) {
68+
window.dubAnalytics.trackLead = function (...args) {
69+
trackLead(...args);
70+
};
71+
72+
window.dubAnalytics.trackSale = function (...args) {
73+
trackSale(...args);
74+
};
75+
}
76+
77+
// Process any existing queued conversion events
78+
if (window._dubAnalytics && window._dubAnalytics.qm) {
79+
const queueManager = window._dubAnalytics.qm;
80+
const existingQueue = queueManager.queue || [];
81+
82+
const remainingQueue = existingQueue.filter(([method, ...args]) => {
83+
if (method === 'trackLead') {
84+
trackLead(...args);
85+
return false;
86+
} else if (method === 'trackSale') {
87+
trackSale(...args);
88+
return false;
89+
}
90+
91+
return true;
92+
});
93+
94+
// Update the queue with remaining items
95+
queueManager.queue = remainingQueue;
96+
}
97+
};
98+
99+
// Run when base script is ready
100+
if (window._dubAnalytics) {
101+
initConversionTracking();
102+
} else {
103+
window.addEventListener('load', initConversionTracking);
104+
}

packages/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dub/analytics",
3-
"version": "0.0.29",
3+
"version": "0.0.30",
44
"description": "",
55
"keywords": [
66
"analytics",

packages/web/src/generic.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function inject(props: AnalyticsProps): void {
1919
(w[da].q = w[da].q || []).push(arguments);
2020
};
2121

22-
['trackClick'].forEach(function (m) {
22+
['trackClick', 'trackLead', 'trackSale'].forEach(function (m) {
2323
w[da][m] = function () {
2424
w[da](m, ...Array.from(arguments));
2525
};
@@ -32,6 +32,7 @@ function inject(props: AnalyticsProps): void {
3232

3333
if (props.domainsConfig?.site) features.push('site-visit');
3434
if (props.domainsConfig?.outbound) features.push('outbound-domains');
35+
if (props.publishableKey) features.push('conversion-tracking');
3536

3637
const src =
3738
props.scriptProps?.src ||
@@ -51,6 +52,10 @@ function inject(props: AnalyticsProps): void {
5152
script.setAttribute('data-api-host', props.apiHost);
5253
}
5354

55+
if (props.publishableKey) {
56+
script.setAttribute('data-publishable-key', props.publishableKey);
57+
}
58+
5459
if (props.domainsConfig) {
5560
script.setAttribute('data-domains', JSON.stringify(props.domainsConfig));
5661
}

0 commit comments

Comments
 (0)