From b28376b37cbac4a4a0c518b72bbae7286450387e Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 21 Aug 2025 12:16:50 +0530 Subject: [PATCH 01/14] add trackLead and trackSale method --- apps/html/conversion-tracking.html | 68 ++++++++++++++ packages/script/build.js | 14 +++ packages/script/package.json | 1 + packages/script/src/base.js | 2 + .../extensions/client-conversion-tracking.js | 94 +++++++++++++++++++ packages/web/src/generic.ts | 4 + packages/web/src/types.ts | 6 ++ 7 files changed, 189 insertions(+) create mode 100644 apps/html/conversion-tracking.html create mode 100644 packages/script/src/extensions/client-conversion-tracking.js diff --git a/apps/html/conversion-tracking.html b/apps/html/conversion-tracking.html new file mode 100644 index 0000000..6e9c2c6 --- /dev/null +++ b/apps/html/conversion-tracking.html @@ -0,0 +1,68 @@ + + + + + + + Client Conversion Tracking + + + + + + +
+

Client Conversion Tracking

+ + + +
+ + diff --git a/packages/script/build.js b/packages/script/build.js index 43785b3..398a9ab 100644 --- a/packages/script/build.js +++ b/packages/script/build.js @@ -62,6 +62,20 @@ Promise.all([ outfile: 'dist/analytics/script.outbound-domains.js', }), + // Client conversion tracking + esbuild.build({ + ...baseConfig, + stdin: { + contents: combineFiles([ + 'src/base.js', + 'src/extensions/client-conversion-tracking.js', + ]), + resolveDir: __dirname, + sourcefile: 'combined.js', + }, + outfile: 'dist/analytics/script.client-conversion-tracking.js', + }), + // Complete script with concatenated feature names esbuild.build({ ...baseConfig, diff --git a/packages/script/package.json b/packages/script/package.json index db30093..fc0da76 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -6,6 +6,7 @@ "dist/analytics/script.js", "dist/analytics/script.site-visit.js", "dist/analytics/script.outbound-domains.js", + "dist/analytics/script.client-conversion-tracking.js", "dist/analytics/script.site-visit.outbound-domains.js" ], "scripts": { diff --git a/packages/script/src/base.js b/packages/script/src/base.js index cec19f2..cdfecd1 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,7 @@ p: QUERY_PARAM, // was QUERY_PARAM v: QUERY_PARAM_VALUE, // was QUERY_PARAM_VALUE n: DOMAINS_CONFIG, // was DOMAINS_CONFIG + k: PUBLISHABLE_KEY, }; // Initialize diff --git a/packages/script/src/extensions/client-conversion-tracking.js b/packages/script/src/extensions/client-conversion-tracking.js new file mode 100644 index 0000000..ea713c1 --- /dev/null +++ b/packages/script/src/extensions/client-conversion-tracking.js @@ -0,0 +1,94 @@ +const initClientConversionTracking = () => { + console.debug('Running initClientConversionTracking'); + + const { a: API_HOST, k: PUBLISHABLE_KEY } = window._dubAnalytics; + + console.log({ + API_HOST, + PUBLISHABLE_KEY, + }); + + // Track lead conversion + const trackLead = async (input) => { + console.debug('Calling trackLead', input); + + const response = await fetch(`${API_HOST}/track/lead/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] trackLead failed', result.error); + } + + return result; + }; + + // Track sale conversion + const trackSale = async (input) => { + console.debug('Calling trackSale', 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; + }; + + // Process the queued events + if (window.dubAnalytics) { + const original = window.dubAnalytics; + const queue = original.q || []; + + // Create a callable function + // Eg: dubAnalytics('trackLead', {}); + function dubAnalytics(method, ...args) { + if (method === 'trackLead') { + trackLead(...args); + } else if (method === 'trackSale') { + trackSale(...args); + } else { + console.warn('[dubAnalytics] Unknown method:', method); + } + } + + dubAnalytics.q = queue; + + dubAnalytics.trackLead = function (...args) { + trackLead(...args); + }; + + dubAnalytics.trackSale = function (...args) { + trackSale(...args); + }; + + window.dubAnalytics = { + ...original, + ...dubAnalytics, + }; + } +}; + +// Run when base script is ready +if (window._dubAnalytics) { + initClientConversionTracking(); +} else { + window.addEventListener('load', initClientConversionTracking); +} diff --git a/packages/web/src/generic.ts b/packages/web/src/generic.ts index ac5c2be..a0aa10e 100644 --- a/packages/web/src/generic.ts +++ b/packages/web/src/generic.ts @@ -51,6 +51,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/types.ts b/packages/web/src/types.ts index e4de099..451fcd1 100644 --- a/packages/web/src/types.ts +++ b/packages/web/src/types.ts @@ -7,6 +7,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. * From 8a9adcec55896dffd7ff191fdc14fe2b020be020 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 21 Aug 2025 12:24:20 +0530 Subject: [PATCH 02/14] Update client-conversion-tracking.js --- .../extensions/client-conversion-tracking.js | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/packages/script/src/extensions/client-conversion-tracking.js b/packages/script/src/extensions/client-conversion-tracking.js index ea713c1..4ec0795 100644 --- a/packages/script/src/extensions/client-conversion-tracking.js +++ b/packages/script/src/extensions/client-conversion-tracking.js @@ -53,24 +53,58 @@ const initClientConversionTracking = () => { }; // Process the queued events + const processQueuedEvents = (queue) => { + if (!queue || !Array.isArray(queue)) { + return; + } + + console.debug( + '[dubAnalytics] Processing queued conversion events:', + queue.length, + ); + + queue.forEach(([method, ...args]) => { + if (method === 'trackLead') { + console.debug('[dubAnalytics] Processing queued trackLead:', args); + trackLead(...args); + } else if (method === 'trackSale') { + console.debug('[dubAnalytics] Processing queued trackSale:', args); + trackSale(...args); + } + }); + }; + if (window.dubAnalytics) { const original = window.dubAnalytics; - const queue = original.q || []; + const existingQueue = original.q || []; - // Create a callable function - // Eg: dubAnalytics('trackLead', {}); function dubAnalytics(method, ...args) { + console.debug('[dubAnalytics] Called with method:', method, args); + if (method === 'trackLead') { trackLead(...args); } else if (method === 'trackSale') { trackSale(...args); + } else if (method === 'ready') { + // Handle ready callback + const callback = args[0]; + if (typeof callback === 'function') { + callback(); + } } else { - console.warn('[dubAnalytics] Unknown method:', method); + // Delegate to original dubAnalytics for other methods + if (original && typeof original === 'function') { + original(method, ...args); + } else { + console.warn('[dubAnalytics] Unknown method:', method); + } } } - dubAnalytics.q = queue; + // Preserve the existing queue + dubAnalytics.q = existingQueue; + // Add conversion tracking methods dubAnalytics.trackLead = function (...args) { trackLead(...args); }; @@ -79,10 +113,55 @@ const initClientConversionTracking = () => { trackSale(...args); }; + // Process existing queued events that are conversion-related + processQueuedEvents(existingQueue); + + // Replace window.dubAnalytics with the enhanced version window.dubAnalytics = { ...original, ...dubAnalytics, }; + } else { + // If dubAnalytics doesn't exist yet, create it + window.dubAnalytics = function (method, ...args) { + if (method === 'trackLead') { + trackLead(...args); + } else if (method === 'trackSale') { + trackSale(...args); + } else if (method === 'ready') { + const callback = args[0]; + if (typeof callback === 'function') { + callback(); + } + } else { + console.warn('[dubAnalytics] Unknown method:', method); + } + }; + + window.dubAnalytics.q = []; + window.dubAnalytics.trackLead = function (...args) { + trackLead(...args); + }; + window.dubAnalytics.trackSale = function (...args) { + trackSale(...args); + }; + } + + // Alternative approach: Hook into the base script's queue manager if available + // This provides better integration with the existing queue system + if (window._dubAnalytics && window._dubAnalytics.queueManager) { + const originalProcess = window._dubAnalytics.queueManager.process; + + window._dubAnalytics.queueManager.process = function ({ method, args }) { + if (method === 'trackLead') { + trackLead(...args); + } else if (method === 'trackSale') { + trackSale(...args); + } else { + // Call original process for other methods + originalProcess.call(this, { method, args }); + } + }; } }; From 61a0a825aa8be76529995d57e793df47bd0d2d3f Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 21 Aug 2025 12:41:02 +0530 Subject: [PATCH 03/14] process the queued events --- packages/script/src/base.js | 1 + .../extensions/client-conversion-tracking.js | 120 +++--------------- 2 files changed, 17 insertions(+), 104 deletions(-) diff --git a/packages/script/src/base.js b/packages/script/src/base.js index cdfecd1..6cfc9c1 100644 --- a/packages/script/src/base.js +++ b/packages/script/src/base.js @@ -273,6 +273,7 @@ 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/client-conversion-tracking.js b/packages/script/src/extensions/client-conversion-tracking.js index 4ec0795..42731af 100644 --- a/packages/script/src/extensions/client-conversion-tracking.js +++ b/packages/script/src/extensions/client-conversion-tracking.js @@ -3,15 +3,8 @@ const initClientConversionTracking = () => { const { a: API_HOST, k: PUBLISHABLE_KEY } = window._dubAnalytics; - console.log({ - API_HOST, - PUBLISHABLE_KEY, - }); - // Track lead conversion const trackLead = async (input) => { - console.debug('Calling trackLead', input); - const response = await fetch(`${API_HOST}/track/lead/client`, { method: 'POST', headers: { @@ -32,8 +25,6 @@ const initClientConversionTracking = () => { // Track sale conversion const trackSale = async (input) => { - console.debug('Calling trackSale', input); - const response = await fetch(`${API_HOST}/track/sale/client`, { method: 'POST', headers: { @@ -52,116 +43,37 @@ const initClientConversionTracking = () => { return result; }; - // Process the queued events - const processQueuedEvents = (queue) => { - if (!queue || !Array.isArray(queue)) { - return; - } - - console.debug( - '[dubAnalytics] Processing queued conversion events:', - queue.length, - ); - - queue.forEach(([method, ...args]) => { - if (method === 'trackLead') { - console.debug('[dubAnalytics] Processing queued trackLead:', args); - trackLead(...args); - } else if (method === 'trackSale') { - console.debug('[dubAnalytics] Processing queued trackSale:', args); - trackSale(...args); - } - }); - }; - + // Add methods to the global dubAnalytics object for direct calls if (window.dubAnalytics) { - const original = window.dubAnalytics; - const existingQueue = original.q || []; - - function dubAnalytics(method, ...args) { - console.debug('[dubAnalytics] Called with method:', method, args); - - if (method === 'trackLead') { - trackLead(...args); - } else if (method === 'trackSale') { - trackSale(...args); - } else if (method === 'ready') { - // Handle ready callback - const callback = args[0]; - if (typeof callback === 'function') { - callback(); - } - } else { - // Delegate to original dubAnalytics for other methods - if (original && typeof original === 'function') { - original(method, ...args); - } else { - console.warn('[dubAnalytics] Unknown method:', method); - } - } - } - - // Preserve the existing queue - dubAnalytics.q = existingQueue; - - // Add conversion tracking methods - dubAnalytics.trackLead = function (...args) { - trackLead(...args); - }; - - dubAnalytics.trackSale = function (...args) { - trackSale(...args); - }; - - // Process existing queued events that are conversion-related - processQueuedEvents(existingQueue); - - // Replace window.dubAnalytics with the enhanced version - window.dubAnalytics = { - ...original, - ...dubAnalytics, - }; - } else { - // If dubAnalytics doesn't exist yet, create it - window.dubAnalytics = function (method, ...args) { - if (method === 'trackLead') { - trackLead(...args); - } else if (method === 'trackSale') { - trackSale(...args); - } else if (method === 'ready') { - const callback = args[0]; - if (typeof callback === 'function') { - callback(); - } - } else { - console.warn('[dubAnalytics] Unknown method:', method); - } - }; - - window.dubAnalytics.q = []; window.dubAnalytics.trackLead = function (...args) { trackLead(...args); }; + window.dubAnalytics.trackSale = function (...args) { trackSale(...args); }; } - // Alternative approach: Hook into the base script's queue manager if available - // This provides better integration with the existing queue system - if (window._dubAnalytics && window._dubAnalytics.queueManager) { - const originalProcess = window._dubAnalytics.queueManager.process; + // Process any existing queued conversion events + if (window._dubAnalytics && window._dubAnalytics.qm) { + const queueManager = window._dubAnalytics.qm; + const existingQueue = queueManager.queue || []; + + console.debug( + '[dubAnalytics] Processing existing queue:', + existingQueue.length, + 'events', + ); - window._dubAnalytics.queueManager.process = function ({ method, args }) { + existingQueue.forEach(([method, ...args]) => { if (method === 'trackLead') { + console.debug('[dubAnalytics] Processing queued trackLead:', args); trackLead(...args); } else if (method === 'trackSale') { + console.debug('[dubAnalytics] Processing queued trackSale:', args); trackSale(...args); - } else { - // Call original process for other methods - originalProcess.call(this, { method, args }); } - }; + }); } }; From 7928ab47d53002f945e193a1455fcc1dc2aca1c8 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 21 Aug 2025 13:06:14 +0530 Subject: [PATCH 04/14] add to react package --- .../client-conversion-tracking.tsx | 41 +++++++++++++++++++ apps/nextjs/app/client-tracking/page.tsx | 5 +++ packages/web/src/generic.ts | 3 +- packages/web/src/react.tsx | 16 +++++++- packages/web/src/types.ts | 23 ++++++++++- packages/web/src/use-analytics.ts | 30 +++++++++++++- 6 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 apps/nextjs/app/client-tracking/client-conversion-tracking.tsx create mode 100644 apps/nextjs/app/client-tracking/page.tsx diff --git a/apps/nextjs/app/client-tracking/client-conversion-tracking.tsx b/apps/nextjs/app/client-tracking/client-conversion-tracking.tsx new file mode 100644 index 0000000..d666518 --- /dev/null +++ b/apps/nextjs/app/client-tracking/client-conversion-tracking.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useAnalytics } from '@dub/analytics/react'; + +export function ClientConversionTracking() { + const { trackLead, trackSale } = useAnalytics(); + + const handleTrackLead = () => { + trackLead({ + clickId: 'W13FJbgeLIGdlx7s', + eventName: 'Account created', + customerExternalId: '1234567890', + }); + }; + + const handleTrackSale = () => { + trackSale({ + eventName: 'Purchase completed', + customerExternalId: 'CXvG5QOLi8QKBA2jYmDh', + paymentProcessor: 'stripe', + amount: 5000, + }); + }; + + return ( +
+ + +
+ ); +} diff --git a/apps/nextjs/app/client-tracking/page.tsx b/apps/nextjs/app/client-tracking/page.tsx new file mode 100644 index 0000000..c8e74bc --- /dev/null +++ b/apps/nextjs/app/client-tracking/page.tsx @@ -0,0 +1,5 @@ +import { ClientConversionTracking } from './client-conversion-tracking'; + +export default function ClientTrackingPage() { + return ; +} diff --git a/packages/web/src/generic.ts b/packages/web/src/generic.ts index a0aa10e..3ef1e93 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('client-conversion-tracking'); const src = props.scriptProps?.src || 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 451fcd1..b508855 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. @@ -167,6 +165,27 @@ export interface TrackClickInput { key: string; } +export interface TrackLeadInput { + clickId: string; + eventName: string; + customerExternalId: string; + customerName?: string | null; + customerEmail?: string | null; + customerAvatar?: string | null; + mode?: string; + metadata?: Record; +} + +export interface TrackSaleInput { + eventName: string; + customerExternalId: string; + paymentProcessor: string; + amount: number; + invoiceId?: string | null; + 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, }; } From 151723d2a29fe7a8bad9d60762dc46bdb6f44d2e Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 21 Aug 2025 16:23:50 +0530 Subject: [PATCH 05/14] Update client-conversion-tracking.js --- .../src/extensions/client-conversion-tracking.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/script/src/extensions/client-conversion-tracking.js b/packages/script/src/extensions/client-conversion-tracking.js index 42731af..4b9c495 100644 --- a/packages/script/src/extensions/client-conversion-tracking.js +++ b/packages/script/src/extensions/client-conversion-tracking.js @@ -1,6 +1,4 @@ const initClientConversionTracking = () => { - console.debug('Running initClientConversionTracking'); - const { a: API_HOST, k: PUBLISHABLE_KEY } = window._dubAnalytics; // Track lead conversion @@ -59,18 +57,10 @@ const initClientConversionTracking = () => { const queueManager = window._dubAnalytics.qm; const existingQueue = queueManager.queue || []; - console.debug( - '[dubAnalytics] Processing existing queue:', - existingQueue.length, - 'events', - ); - existingQueue.forEach(([method, ...args]) => { if (method === 'trackLead') { - console.debug('[dubAnalytics] Processing queued trackLead:', args); trackLead(...args); } else if (method === 'trackSale') { - console.debug('[dubAnalytics] Processing queued trackSale:', args); trackSale(...args); } }); From 905b11a3db0b6c814b087600d018dcb7140a452e Mon Sep 17 00:00:00 2001 From: Kiran K Date: Thu, 21 Aug 2025 16:38:59 +0530 Subject: [PATCH 06/14] Update conversion-tracking.html --- apps/html/conversion-tracking.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/html/conversion-tracking.html b/apps/html/conversion-tracking.html index 6e9c2c6..cfb38bf 100644 --- a/apps/html/conversion-tracking.html +++ b/apps/html/conversion-tracking.html @@ -54,6 +54,7 @@

Client Conversion Tracking