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
66 changes: 66 additions & 0 deletions apps/html/conversion-tracking.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Your website description" />
<title>Client Conversion Tracking</title>
</head>
<body>
<script>
!(function (w, da) {
w[da] =
w[da] ||
function () {
(w[da].q = w[da].q || []).push(arguments);
};

['trackClick', 'trackLead', 'trackSale'].forEach(function (m) {
w[da][m] = function () {
w[da](m, ...arguments);
};
});
})(window, 'dubAnalytics');
</script>

<script>
// Add a 3-second delay before loading the script to test queue functionality
setTimeout(() => {
const script = document.createElement('script');
script.defer = true;
script.src = './analytics/script.conversion-tracking.js';
script.setAttribute('data-api-host', 'http://api.localhost:8888');
script.setAttribute(
'data-publishable-key',
'dub_pk_BgyBCEJCPCGN3RN7oieLVHRs',
);
document.head.appendChild(script);
}, 3000);
</script>

<header>
<h1>Client Conversion Tracking</h1>
<button
onclick="dubAnalytics.trackLead({
eventName: 'Account created',
customerExternalId: '1234567890',
customerName: 'John Doe',
customerEmail: 'john.doe@example.com',
})"
>
Track Lead
</button>

<button
onclick="dubAnalytics.trackSale({
eventName: 'Purchase completed',
customerExternalId: 'CXvG5QOLi8QKBA2jYmDh',
invoiceId: Math.random().toString(36).substring(2, 15),
amount: 5000, // defaults to usd cents, use `currency` prop to specify a different currency
})"
>
Track Sale
</button>
</header>
</body>
</html>
39 changes: 39 additions & 0 deletions apps/nextjs/app/conversion-tracking/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import { useAnalytics } from '@dub/analytics/react';

export function ConversionTrackingPageClient() {
const { trackLead, trackSale } = useAnalytics();

const handleTrackLead = () => {
trackLead({
eventName: 'Account created',
customerExternalId: '1234567890',
});
};

const handleTrackSale = () => {
trackSale({
eventName: 'Purchase completed',
customerExternalId: 'CXvG5QOLi8QKBA2jYmDh',
amount: 5000, // defaults to usd cents, use `currency` prop to specify a different currency
});
};

return (
<div className="flex gap-4 p-4">
<button
onClick={handleTrackLead}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Track Lead
</button>
<button
onClick={handleTrackSale}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
>
Track Sale
</button>
Comment thread
steven-tey marked this conversation as resolved.
</div>
);
}
5 changes: 5 additions & 0 deletions apps/nextjs/app/conversion-tracking/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ConversionTrackingPageClient } from './page-client';

export default function ConversionTrackingPage() {
return <ConversionTrackingPageClient />;
}
71 changes: 66 additions & 5 deletions packages/script/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ fs.copyFileSync(
path.join(__dirname, 'dist/_redirects'),
);

// Build all variants
// Build all variants (8 total combinations of 3 extensions)
Promise.all([
// Base script
// 1. Base script (no extensions)
esbuild.build({
...baseConfig,
stdin: {
Expand All @@ -37,7 +37,7 @@ Promise.all([
outfile: 'dist/analytics/script.js',
}),

// Site visit tracking
// 2. Site visit only
esbuild.build({
...baseConfig,
stdin: {
Expand All @@ -48,7 +48,7 @@ Promise.all([
outfile: 'dist/analytics/script.site-visit.js',
}),

// Outbound domains tracking
// 3. Outbound domains only
esbuild.build({
...baseConfig,
stdin: {
Expand All @@ -62,7 +62,21 @@ Promise.all([
outfile: 'dist/analytics/script.outbound-domains.js',
}),

// Complete script with concatenated feature names
// 4. Conversion tracking only
esbuild.build({
...baseConfig,
stdin: {
contents: combineFiles([
'src/base.js',
'src/extensions/conversion-tracking.js',
]),
resolveDir: __dirname,
sourcefile: 'combined.js',
},
outfile: 'dist/analytics/script.conversion-tracking.js',
}),

// 5. Site visit + Outbound domains
esbuild.build({
...baseConfig,
stdin: {
Expand All @@ -76,4 +90,51 @@ Promise.all([
},
outfile: 'dist/analytics/script.site-visit.outbound-domains.js',
}),

// 6. Site visit + Conversion tracking
esbuild.build({
...baseConfig,
stdin: {
contents: combineFiles([
'src/base.js',
'src/extensions/site-visit.js',
'src/extensions/conversion-tracking.js',
]),
resolveDir: __dirname,
sourcefile: 'combined.js',
},
outfile: 'dist/analytics/script.site-visit.conversion-tracking.js',
}),

// 7. Outbound domains + Conversion tracking
esbuild.build({
...baseConfig,
stdin: {
contents: combineFiles([
'src/base.js',
'src/extensions/outbound-domains.js',
'src/extensions/conversion-tracking.js',
]),
resolveDir: __dirname,
sourcefile: 'combined.js',
},
outfile: 'dist/analytics/script.outbound-domains.conversion-tracking.js',
}),

// 8. All extensions combined
esbuild.build({
...baseConfig,
stdin: {
contents: combineFiles([
'src/base.js',
'src/extensions/site-visit.js',
'src/extensions/outbound-domains.js',
'src/extensions/conversion-tracking.js',
]),
resolveDir: __dirname,
sourcefile: 'combined.js',
},
outfile:
'dist/analytics/script.site-visit.outbound-domains.conversion-tracking.js',
}),
]).catch(() => process.exit(1));
8 changes: 6 additions & 2 deletions packages/script/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
{
"name": "@dub/analytics-script",
"version": "0.0.29",
"version": "0.0.30",
"main": "src/index.js",
"files": [
"dist/analytics/script.js",
"dist/analytics/script.site-visit.js",
"dist/analytics/script.outbound-domains.js",
"dist/analytics/script.site-visit.outbound-domains.js"
"dist/analytics/script.conversion-tracking.js",
"dist/analytics/script.site-visit.outbound-domains.js",
"dist/analytics/script.site-visit.conversion-tracking.js",
"dist/analytics/script.outbound-domains.conversion-tracking.js",
"dist/analytics/script.site-visit.outbound-domains.conversion-tracking.js"
],
"scripts": {
"prebuild": "mkdir -p dist/analytics",
Expand Down
3 changes: 3 additions & 0 deletions packages/script/src/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

// Common script attributes
const API_HOST = script.getAttribute('data-api-host') || 'https://api.dub.co';
const PUBLISHABLE_KEY = script.getAttribute('data-publishable-key');
Comment thread
steven-tey marked this conversation as resolved.
const COOKIE_OPTIONS = (() => {
const defaultOptions = {
domain:
Expand Down Expand Up @@ -271,6 +272,8 @@
p: QUERY_PARAM, // was QUERY_PARAM
v: QUERY_PARAM_VALUE, // was QUERY_PARAM_VALUE
n: DOMAINS_CONFIG, // was DOMAINS_CONFIG
k: PUBLISHABLE_KEY,
qm: queueManager,
};

// Initialize
Expand Down
104 changes: 104 additions & 0 deletions packages/script/src/extensions/conversion-tracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const initConversionTracking = () => {
const {
a: API_HOST,
k: PUBLISHABLE_KEY,
c: cookieManager,
i: DUB_ID_VAR,
} = window._dubAnalytics || {};

if (!API_HOST) {
console.warn('[dubAnalytics] Missing API_HOST');
return;
}

if (!PUBLISHABLE_KEY) {
console.warn('[dubAnalytics] Missing PUBLISHABLE_KEY');
return;
}

// Track lead conversion
const trackLead = async (input) => {
const clickId = cookieManager?.get(DUB_ID_VAR);

const requestBody = {
...(clickId && { clickId }),
...input,
};

const response = await fetch(`${API_HOST}/track/lead/client`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${PUBLISHABLE_KEY}`,
},
body: JSON.stringify(requestBody),
});

const result = await response.json();

if (!response.ok) {
console.error('[dubAnalytics] trackLead failed', result.error);
}

return result;
};

// Track sale conversion
const trackSale = async (input) => {
const response = await fetch(`${API_HOST}/track/sale/client`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${PUBLISHABLE_KEY}`,
},
body: JSON.stringify(input),
});

const result = await response.json();

if (!response.ok) {
console.error('[dubAnalytics] trackSale failed', result.error);
}

return result;
};

// Add methods to the global dubAnalytics object for direct calls
if (window.dubAnalytics) {
window.dubAnalytics.trackLead = function (...args) {
trackLead(...args);
};

window.dubAnalytics.trackSale = function (...args) {
trackSale(...args);
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Process any existing queued conversion events
if (window._dubAnalytics && window._dubAnalytics.qm) {
const queueManager = window._dubAnalytics.qm;
const existingQueue = queueManager.queue || [];

const remainingQueue = existingQueue.filter(([method, ...args]) => {
if (method === 'trackLead') {
trackLead(...args);
return false;
} else if (method === 'trackSale') {
trackSale(...args);
return false;
}

return true;
});

// Update the queue with remaining items
queueManager.queue = remainingQueue;
}
};

// Run when base script is ready
if (window._dubAnalytics) {
initConversionTracking();
} else {
window.addEventListener('load', initConversionTracking);
}
2 changes: 1 addition & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dub/analytics",
"version": "0.0.29",
"version": "0.0.30",
"description": "",
"keywords": [
"analytics",
Expand Down
7 changes: 6 additions & 1 deletion packages/web/src/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function inject(props: AnalyticsProps): void {
(w[da].q = w[da].q || []).push(arguments);
};

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

if (props.domainsConfig?.site) features.push('site-visit');
if (props.domainsConfig?.outbound) features.push('outbound-domains');
if (props.publishableKey) features.push('conversion-tracking');

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

if (props.publishableKey) {
script.setAttribute('data-publishable-key', props.publishableKey);
}
Comment thread
devkiran marked this conversation as resolved.

if (props.domainsConfig) {
script.setAttribute('data-domains', JSON.stringify(props.domainsConfig));
}
Expand Down
Loading