Skip to content

Commit 4238f6f

Browse files
authored
Merge pull request #46 from dubinc/partner-discount-dx
Add `useAnalytics` hook and `trackClick` method
2 parents 2ac29d4 + e77d374 commit 4238f6f

File tree

15 files changed

+344
-13
lines changed

15 files changed

+344
-13
lines changed

.github/workflows/deploy-script.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
workingDirectory: packages/script
2929
packageManager: pnpm
3030

31+
# Dispatch deployment event to trigger Playwright tests
3132
- name: Dispatch deployment event
3233
uses: peter-evans/repository-dispatch@v3
3334
with:

apps/html/index.html

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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>Dub Analytics</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'].forEach(function (m) {
19+
w[da][m] = function () {
20+
w[da](m, ...arguments);
21+
};
22+
});
23+
})(window, 'dubAnalytics');
24+
25+
dubAnalytics('ready', function () {
26+
if (DubAnalytics.partner) {
27+
document.body.innerHTML += `<pre>${JSON.stringify(
28+
DubAnalytics,
29+
null,
30+
2,
31+
)}</pre>`;
32+
}
33+
});
34+
35+
dubAnalytics.trackClick({
36+
domain: 'getacme.link',
37+
key: 'derek',
38+
});
39+
</script>
40+
41+
<script>
42+
// Add a 3-second delay before loading the script to test queue functionality
43+
setTimeout(() => {
44+
const script = document.createElement('script');
45+
script.defer = true;
46+
script.src = './script.js';
47+
script.setAttribute('data-short-domain', 'getacme.link');
48+
document.head.appendChild(script);
49+
}, 3000);
50+
</script>
51+
52+
<header>
53+
<h1>Dub Analytics</h1>
54+
<button
55+
onclick="dubAnalytics.trackClick({domain: 'getacme.link', key: 'derek'})"
56+
>
57+
Track Click
58+
</button>
59+
</header>
60+
</body>
61+
</html>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { WithDomainKey } from './with-domain-key';
2+
3+
export default function Page() {
4+
return <WithDomainKey />;
5+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import { useAnalytics } from '@dub/analytics/react';
4+
import { useParams } from 'next/navigation';
5+
import { useEffect } from 'react';
6+
7+
export function WithDomainKey() {
8+
const { trackClick } = useAnalytics();
9+
const { username } = useParams<{ username: string }>();
10+
11+
useEffect(() => {
12+
if (!username) {
13+
return;
14+
}
15+
16+
trackClick({
17+
domain: 'getacme.link',
18+
key: username, // `getacme.link/derek`
19+
});
20+
}, [trackClick, username]);
21+
22+
return <div>{username}</div>;
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
3+
import { Discount, Partner, useAnalytics } from '@dub/analytics/react';
4+
import { useEffect } from 'react';
5+
6+
export function DiscountBanner() {
7+
const { partner, discount } = useAnalytics();
8+
9+
useEffect(() => {
10+
if (partner && discount) {
11+
alert(discountBannerText(partner, discount));
12+
}
13+
}, [partner, discount]);
14+
15+
return null;
16+
}
17+
18+
function discountBannerText(partner: Partner, discount: Discount) {
19+
return `${partner.name} has gifted you ${discount.amount} ${discount.type === 'percentage' ? '%' : '$'} off for ${discount.maxDuration} months!`;
20+
}

apps/nextjs/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from 'next';
22
import { Inter } from 'next/font/google';
33
import { Analytics as DubAnalytics } from '@dub/analytics/react';
44
import './globals.css';
5+
import { DiscountBanner } from './discount-banner';
56
import { DUB_ANALYTICS_SCRIPT_URL } from './constants';
67

78
const inter = Inter({ subsets: ['latin'] });
@@ -19,6 +20,7 @@ export default function RootLayout({
1920
return (
2021
<html lang="en">
2122
<body className={inter.className}>{children}</body>
23+
<DiscountBanner />
2224
<DubAnalytics
2325
domainsConfig={{
2426
refer: 'getacme.link',

apps/nextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626
"eslint-config-next": "14.2.4",
2727
"typescript": "^5"
2828
}
29-
}
29+
}

packages/script/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-script",
3-
"version": "0.0.27",
3+
"version": "0.0.28",
44
"main": "src/index.js",
55
"files": [
66
"dist/analytics/script.js",

packages/script/src/base.js

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,43 @@
6666
QUERY_PARAM,
6767
);
6868

69+
// Initialize global DubAnalytics object
70+
window.DubAnalytics = window.DubAnalytics || {
71+
partner: null,
72+
discount: null,
73+
};
74+
75+
// Initialize dubAnalytics
76+
if (window.dubAnalytics) {
77+
const original = window.dubAnalytics;
78+
const queue = original.q || [];
79+
80+
// Create a callable function
81+
function dubAnalytics(method, ...args) {
82+
if (method === 'ready') {
83+
dubAnalytics.ready(...args);
84+
} else if (method === 'trackClick') {
85+
dubAnalytics.trackClick(...args);
86+
} else {
87+
console.warn('[dubAnalytics] Unknown method:', method);
88+
}
89+
}
90+
91+
// Attach properties and methods
92+
dubAnalytics.q = queue;
93+
94+
dubAnalytics.ready = function (callback) {
95+
callback();
96+
};
97+
98+
dubAnalytics.trackClick = function (...args) {
99+
trackClick(...args);
100+
};
101+
102+
// Replace window.dubAnalytics with the callable + augmented function
103+
window.dubAnalytics = dubAnalytics;
104+
}
105+
69106
// Cookie management
70107
const cookieManager = {
71108
get(key) {
@@ -85,18 +122,58 @@
85122
},
86123
};
87124

125+
// Queue management
126+
const queueManager = {
127+
queue: window.dubAnalytics?.q || [],
128+
129+
// Process specific method types (e.g., only 'ready')
130+
flush(methodFilter) {
131+
const remainingQueue = [];
132+
133+
while (this.queue.length) {
134+
const [method, ...args] = this.queue.shift();
135+
136+
if (!methodFilter || methodFilter(method)) {
137+
this.process({ method, args });
138+
} else {
139+
remainingQueue.push([method, ...args]);
140+
}
141+
}
142+
143+
this.queue = remainingQueue;
144+
},
145+
146+
process({ method, args }) {
147+
if (method === 'ready') {
148+
const callback = args[0];
149+
callback();
150+
} else if (['trackClick'].includes(method)) {
151+
trackClick(...args);
152+
} else {
153+
console.warn('[dubAnalytics] Unknown method:', method);
154+
}
155+
},
156+
};
157+
88158
let clientClickTracked = false;
159+
89160
// Track click and set cookie
90-
function trackClick(identifier, serverClickId) {
91-
if (clientClickTracked) return;
161+
function trackClick({ domain, key }) {
162+
if (clientClickTracked) {
163+
return;
164+
}
165+
92166
clientClickTracked = true;
93167

168+
const params = new URLSearchParams(location.search);
169+
const serverClickId = params.get(DUB_ID_VAR);
170+
94171
fetch(`${API_HOST}/track/click`, {
95172
method: 'POST',
96173
headers: { 'Content-Type': 'application/json' },
97174
body: JSON.stringify({
98-
domain: SHORT_DOMAIN,
99-
key: identifier,
175+
domain,
176+
key,
100177
url: window.location.href,
101178
referrer: document.referrer,
102179
}),
@@ -106,7 +183,7 @@
106183
if (data) {
107184
if (serverClickId && serverClickId !== data.clickId) {
108185
console.warn(
109-
`Client-tracked click ID ${data.clickId} does not match server-tracked click ID ${serverClickId}, skipping...`,
186+
`[dubAnalytics] Client-tracked click ID ${data.clickId} does not match server-tracked click ID ${serverClickId}, skipping...`,
110187
);
111188
return;
112189
}
@@ -119,12 +196,21 @@
119196
partner: {
120197
...data.partner,
121198
name: encodeURIComponent(data.partner.name),
122-
image: encodeURIComponent(data.partner.image),
199+
image: data.partner.image
200+
? encodeURIComponent(data.partner.image)
201+
: null,
123202
},
124203
};
125204

126205
cookieManager.set(DUB_PARTNER_COOKIE, JSON.stringify(encodedData));
206+
207+
DubAnalytics.partner = data.partner;
208+
DubAnalytics.discount = data.discount;
209+
210+
queueManager.flush((method) => method === 'ready');
127211
}
212+
213+
return data;
128214
}
129215
});
130216
}
@@ -149,7 +235,27 @@
149235

150236
// Dub Partners tracking (via query param e.g. ?via=partner_id)
151237
if (QUERY_PARAM_VALUE && SHORT_DOMAIN && shouldSetCookie()) {
152-
trackClick(QUERY_PARAM_VALUE, clickId);
238+
trackClick({
239+
domain: SHORT_DOMAIN,
240+
key: QUERY_PARAM_VALUE,
241+
});
242+
}
243+
244+
// Process the queued methods
245+
queueManager.flush((method) => method === 'trackClick');
246+
247+
// Initialize DubAnalytics from cookie if it exists
248+
const partnerCookie = cookieManager.get(DUB_PARTNER_COOKIE);
249+
250+
if (partnerCookie) {
251+
try {
252+
const partnerData = JSON.parse(partnerCookie);
253+
254+
DubAnalytics.partner = partnerData.partner;
255+
DubAnalytics.discount = partnerData.discount;
256+
} catch (e) {
257+
console.error('[dubAnalytics] Failed to parse partner cookie:', e);
258+
}
153259
}
154260
}
155261

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.27",
3+
"version": "0.0.28",
44
"description": "",
55
"keywords": [
66
"analytics",

0 commit comments

Comments
 (0)