diff --git a/apps/html/conversion-tracking.html b/apps/html/conversion-tracking.html
new file mode 100644
index 0000000..2b71d0b
--- /dev/null
+++ b/apps/html/conversion-tracking.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ Client Conversion Tracking
+
+
+
+
+
+
+
+ Client Conversion Tracking
+
+
+
+
+
+
diff --git a/apps/nextjs/app/conversion-tracking/page-client.tsx b/apps/nextjs/app/conversion-tracking/page-client.tsx
new file mode 100644
index 0000000..f9019bf
--- /dev/null
+++ b/apps/nextjs/app/conversion-tracking/page-client.tsx
@@ -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 (
+
+
+
+
+ );
+}
diff --git a/apps/nextjs/app/conversion-tracking/page.tsx b/apps/nextjs/app/conversion-tracking/page.tsx
new file mode 100644
index 0000000..03e0679
--- /dev/null
+++ b/apps/nextjs/app/conversion-tracking/page.tsx
@@ -0,0 +1,5 @@
+import { ConversionTrackingPageClient } from './page-client';
+
+export default function ConversionTrackingPage() {
+ return ;
+}
diff --git a/packages/script/build.js b/packages/script/build.js
index 43785b3..9232a35 100644
--- a/packages/script/build.js
+++ b/packages/script/build.js
@@ -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: {
@@ -37,7 +37,7 @@ Promise.all([
outfile: 'dist/analytics/script.js',
}),
- // Site visit tracking
+ // 2. Site visit only
esbuild.build({
...baseConfig,
stdin: {
@@ -48,7 +48,7 @@ Promise.all([
outfile: 'dist/analytics/script.site-visit.js',
}),
- // Outbound domains tracking
+ // 3. Outbound domains only
esbuild.build({
...baseConfig,
stdin: {
@@ -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: {
@@ -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));
diff --git a/packages/script/package.json b/packages/script/package.json
index db30093..fbcee0a 100644
--- a/packages/script/package.json
+++ b/packages/script/package.json
@@ -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",
diff --git a/packages/script/src/base.js b/packages/script/src/base.js
index cec19f2..6cfc9c1 100644
--- a/packages/script/src/base.js
+++ b/packages/script/src/base.js
@@ -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');
const COOKIE_OPTIONS = (() => {
const defaultOptions = {
domain:
@@ -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
diff --git a/packages/script/src/extensions/conversion-tracking.js b/packages/script/src/extensions/conversion-tracking.js
new file mode 100644
index 0000000..b8efba6
--- /dev/null
+++ b/packages/script/src/extensions/conversion-tracking.js
@@ -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);
+ };
+ }
+
+ // 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);
+}
diff --git a/packages/web/package.json b/packages/web/package.json
index 3506ee1..34ad9f9 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@dub/analytics",
- "version": "0.0.29",
+ "version": "0.0.30",
"description": "",
"keywords": [
"analytics",
diff --git a/packages/web/src/generic.ts b/packages/web/src/generic.ts
index ac5c2be..a7aa44b 100644
--- a/packages/web/src/generic.ts
+++ b/packages/web/src/generic.ts
@@ -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));
};
@@ -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 ||
@@ -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);
+ }
+
if (props.domainsConfig) {
script.setAttribute('data-domains', JSON.stringify(props.domainsConfig));
}
diff --git a/packages/web/src/react.tsx b/packages/web/src/react.tsx
index ac989e6..b4d8e92 100644
--- a/packages/web/src/react.tsx
+++ b/packages/web/src/react.tsx
@@ -1,6 +1,12 @@
import { useEffect } from 'react';
import { inject } from './generic';
-import type { AnalyticsProps, Discount, Partner } from './types';
+import type {
+ AnalyticsProps,
+ Discount,
+ Partner,
+ TrackLeadInput,
+ TrackSaleInput,
+} from './types';
import { useAnalytics } from './use-analytics';
/**
@@ -28,4 +34,10 @@ function Analytics(props: AnalyticsProps): null {
}
export { Analytics, useAnalytics };
-export type { AnalyticsProps, Partner, Discount };
+export type {
+ AnalyticsProps,
+ Partner,
+ Discount,
+ TrackLeadInput,
+ TrackSaleInput,
+};
diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts
index e4de099..8076f02 100644
--- a/packages/web/src/types.ts
+++ b/packages/web/src/types.ts
@@ -1,5 +1,3 @@
-export type AllowedPropertyValues = string | number | boolean | null;
-
export interface AnalyticsProps {
/**
* The API endpoint to send analytics data to.
@@ -7,6 +5,12 @@ export interface AnalyticsProps {
*/
apiHost?: string;
+ /**
+ * The publishable key for client conversion tracking
+ * @example 'dub_pk_BgyBCEJCPCGN3RN7oieLVHRs'
+ */
+ publishableKey?: string;
+
/**
* This is a JSON object that configures the domains that Dub will track.
*
@@ -161,6 +165,27 @@ export interface TrackClickInput {
key: string;
}
+export interface TrackLeadInput {
+ clickId?: string; // falls back to dub_id cookie
+ eventName: string;
+ customerExternalId: string;
+ customerName?: string;
+ customerEmail?: string;
+ customerAvatar?: string;
+ mode?: string;
+ metadata?: Record;
+}
+
+export interface TrackSaleInput {
+ eventName: string;
+ customerExternalId: string;
+ paymentProcessor?: string;
+ amount: number;
+ invoiceId?: string;
+ currency?: string;
+ metadata?: Record;
+}
+
export interface Partner {
id: string;
name: string;
diff --git a/packages/web/src/use-analytics.ts b/packages/web/src/use-analytics.ts
index 61fbcbc..cb979a3 100644
--- a/packages/web/src/use-analytics.ts
+++ b/packages/web/src/use-analytics.ts
@@ -1,5 +1,11 @@
import { useCallback, useEffect, useState } from 'react';
-import type { Discount, Partner, TrackClickInput } from './types';
+import type {
+ Discount,
+ Partner,
+ TrackClickInput,
+ TrackLeadInput,
+ TrackSaleInput,
+} from './types';
import { isDubAnalyticsReady } from './utils';
interface PartnerData {
@@ -12,13 +18,15 @@ declare global {
DubAnalytics: PartnerData;
dubAnalytics: ((event: 'ready', callback: () => void) => void) & {
trackClick: (event: TrackClickInput) => void;
+ trackLead: (event: TrackLeadInput) => void;
+ trackSale: (event: TrackSaleInput) => void;
};
}
}
/**
* Hook to access Dub Web Analytics data including partner and discount information.
- * @returns Object containing partner data, and discount information.
+ * @returns Object containing partner data, discount information, and tracking methods.
* ```js
* import { useAnalytics } from '@dub/analytics/react';
*
@@ -59,6 +67,22 @@ export function useAnalytics() {
window.dubAnalytics.trackClick(event);
}, []);
+ const trackLead = useCallback((event: TrackLeadInput) => {
+ if (!isDubAnalyticsReady()) {
+ return;
+ }
+
+ window.dubAnalytics.trackLead(event);
+ }, []);
+
+ const trackSale = useCallback((event: TrackSaleInput) => {
+ if (!isDubAnalyticsReady()) {
+ return;
+ }
+
+ window.dubAnalytics.trackSale(event);
+ }, []);
+
useEffect(() => {
initialize();
}, [initialize]);
@@ -66,5 +90,7 @@ export function useAnalytics() {
return {
...data,
trackClick,
+ trackLead,
+ trackSale,
};
}