Skip to content

Commit 001f816

Browse files
committed
fix some tracker issues, more tests
1 parent c9eecb8 commit 001f816

19 files changed

+4363
-104
lines changed

packages/tracker/src/core/tracker.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { HttpClient } from "./client";
2-
import type { EventContext, TrackerOptions } from "./types";
2+
import type { BaseEvent, EventContext, TrackerOptions } from "./types";
33
import { generateUUIDv4, logger } from "./utils";
44

55
const HEADLESS_CHROME_REGEX = /\bHeadlessChrome\b/i;
@@ -29,8 +29,9 @@ export class BaseTracker {
2929
hasInteracted = false;
3030

3131
// Queues
32-
batchQueue: any[] = [];
32+
batchQueue: BaseEvent[] = [];
3333
batchTimer: Timer | null = null;
34+
private isFlushing = false;
3435

3536
constructor(options: TrackerOptions) {
3637
this.options = {
@@ -320,7 +321,7 @@ export class BaseTracker {
320321
};
321322
}
322323

323-
send(event: any): Promise<any> {
324+
send(event: BaseEvent & { isForceSend?: boolean }): Promise<unknown> {
324325
if (this.shouldSkipTracking()) {
325326
return Promise.resolve();
326327
}
@@ -329,6 +330,12 @@ export class BaseTracker {
329330
return Promise.resolve();
330331
}
331332

333+
const samplingRate = this.options.samplingRate ?? 1.0;
334+
if (samplingRate < 1.0 && Math.random() > samplingRate) {
335+
logger.log("Event sampled out", event);
336+
return Promise.resolve();
337+
}
338+
332339
if (this.options.enableBatching && !event.isForceSend) {
333340
logger.log("Queueing event for batch", event);
334341
return this.addToBatch(event);
@@ -338,7 +345,7 @@ export class BaseTracker {
338345
return this.api.fetch("/", event, { keepalive: true });
339346
}
340347

341-
addToBatch(event: any): Promise<void> {
348+
addToBatch(event: BaseEvent): Promise<void> {
342349
this.batchQueue.push(event);
343350
if (this.batchTimer === null) {
344351
this.batchTimer = setTimeout(
@@ -357,10 +364,11 @@ export class BaseTracker {
357364
clearTimeout(this.batchTimer);
358365
this.batchTimer = null;
359366
}
360-
if (this.batchQueue.length === 0) {
367+
if (this.batchQueue.length === 0 || this.isFlushing) {
361368
return;
362369
}
363370

371+
this.isFlushing = true;
364372
const batchEvents = [...this.batchQueue];
365373
this.batchQueue = [];
366374

@@ -378,6 +386,30 @@ export class BaseTracker {
378386
this.send({ ...evt, isForceSend: true });
379387
}
380388
return null;
389+
} finally {
390+
this.isFlushing = false;
391+
}
392+
}
393+
394+
sendBeacon(data: unknown, endpoint = "/vitals"): boolean {
395+
if (this.isServer()) {
396+
return false;
381397
}
398+
if (typeof navigator === "undefined" || !navigator.sendBeacon) {
399+
return false;
400+
}
401+
try {
402+
const blob = new Blob([JSON.stringify(data)], {
403+
type: "application/json",
404+
});
405+
const baseUrl = this.options.apiUrl || "https://basket.databuddy.cc";
406+
return navigator.sendBeacon(`${baseUrl}${endpoint}`, blob);
407+
} catch {
408+
return false;
409+
}
410+
}
411+
412+
sendBatchBeacon(events: unknown[]): boolean {
413+
return this.sendBeacon(events, "/batch");
382414
}
383415
}

packages/tracker/src/core/types.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,12 @@ export type TrackEvent = BaseEvent & {
6767
};
6868

6969
export type DatabuddyGlobal = {
70-
track: (name: string, props?: any) => void;
71-
screenView: (props?: any) => void;
72-
identify: () => void;
70+
track: (name: string, props?: Record<string, unknown>) => void;
71+
screenView: (props?: Record<string, unknown>) => void;
7372
clear: () => void;
7473
flush: () => void;
75-
setGlobalProperties: () => void;
76-
trackCustomEvent: () => void;
74+
setGlobalProperties: (props: Record<string, unknown>) => void;
75+
trackCustomEvent: (name: string, props?: Record<string, unknown>) => void;
7776
options: TrackerOptions;
7877
};
7978

packages/tracker/src/index.ts

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { initScrollDepthTracking } from "./plugins/scroll-depth";
88
import { initWebVitalsTracking } from "./plugins/vitals";
99

1010
export class Databuddy extends BaseTracker {
11+
// Store references for cleanup
12+
private cleanupFns: Array<() => void> = [];
13+
private originalPushState: typeof history.pushState | null = null;
14+
private originalReplaceState: typeof history.replaceState | null = null;
15+
private globalProperties: Record<string, unknown> = {};
16+
1117
constructor(options: TrackerOptions) {
1218
super(options);
1319

@@ -43,15 +49,12 @@ export class Databuddy extends BaseTracker {
4349

4450
if (typeof window !== "undefined") {
4551
window.databuddy = {
46-
track: (name: string, props?: any) => this.track(name, props),
47-
screenView: (props?: any) => this.screenView(props),
48-
identify: () => { },
49-
clear: () => { }, // Placeholder
50-
flush: () => {
51-
this.flushBatch();
52-
},
53-
setGlobalProperties: () => { }, // Placeholder
54-
trackCustomEvent: () => { }, // Placeholder
52+
track: (name: string, props?: Record<string, unknown>) => this.track(name, props),
53+
screenView: (props?: Record<string, unknown>) => this.screenView(props),
54+
flush: () => this.flushBatch(),
55+
clear: () => this.clear(),
56+
setGlobalProperties: (props: Record<string, unknown>) => this.setGlobalProperties(props),
57+
trackCustomEvent: (name: string, props?: Record<string, unknown>) => this.trackCustomEvent(name, props),
5558
options: this.options,
5659
};
5760
window.db = window.databuddy;
@@ -63,25 +66,31 @@ export class Databuddy extends BaseTracker {
6366
return;
6467
}
6568

66-
const pushState = history.pushState;
69+
// Store original methods for cleanup
70+
this.originalPushState = history.pushState;
71+
this.originalReplaceState = history.replaceState;
72+
73+
const originalPushState = this.originalPushState;
6774
history.pushState = (...args) => {
68-
const ret = pushState.apply(history, args);
75+
const ret = originalPushState.apply(history, args);
6976
window.dispatchEvent(new Event("pushstate"));
7077
window.dispatchEvent(new Event("locationchange"));
7178
return ret;
7279
};
7380

74-
const replaceState = history.replaceState;
81+
const originalReplaceState = this.originalReplaceState;
7582
history.replaceState = (...args) => {
76-
const ret = replaceState.apply(history, args);
83+
const ret = originalReplaceState.apply(history, args);
7784
window.dispatchEvent(new Event("replacestate"));
7885
window.dispatchEvent(new Event("locationchange"));
7986
return ret;
8087
};
8188

82-
window.addEventListener("popstate", () => {
89+
const popstateHandler = () => {
8390
window.dispatchEvent(new Event("locationchange"));
84-
});
91+
};
92+
window.addEventListener("popstate", popstateHandler);
93+
this.cleanupFns.push(() => window.removeEventListener("popstate", popstateHandler));
8594

8695
let debounceTimer: ReturnType<typeof setTimeout>;
8796
const debouncedScreenView = () => {
@@ -92,17 +101,32 @@ export class Databuddy extends BaseTracker {
92101
};
93102

94103
window.addEventListener("locationchange", debouncedScreenView);
104+
this.cleanupFns.push(() => window.removeEventListener("locationchange", debouncedScreenView));
105+
95106
if (this.options.trackHashChanges) {
96107
window.addEventListener("hashchange", debouncedScreenView);
108+
this.cleanupFns.push(() => window.removeEventListener("hashchange", debouncedScreenView));
97109
}
98110
}
99111

100-
screenView(props?: any) {
112+
screenView(props?: Record<string, unknown>) {
101113
if (this.isServer()) {
102114
return;
103115
}
104116
const url = window.location.href;
105117
if (this.lastPath !== url) {
118+
if (!this.options.trackHashChanges && this.lastPath) {
119+
const lastUrl = new URL(this.lastPath);
120+
const currentUrl = new URL(url);
121+
const isHashOnlyChange =
122+
lastUrl.origin === currentUrl.origin &&
123+
lastUrl.pathname === currentUrl.pathname &&
124+
lastUrl.search === currentUrl.search &&
125+
lastUrl.hash !== currentUrl.hash;
126+
if (isHashOnlyChange) {
127+
return;
128+
}
129+
}
106130
this.lastPath = url;
107131
this.pageCount += 1;
108132
this.track("screen_view", {
@@ -113,7 +137,7 @@ export class Databuddy extends BaseTracker {
113137
}
114138

115139
trackOutgoingLinks() {
116-
document.addEventListener("click", (e) => {
140+
const handler = (e: MouseEvent) => {
117141
const target = e.target as HTMLElement;
118142
const link = target.closest("a");
119143
if (link && link.hostname !== window.location.hostname) {
@@ -132,17 +156,19 @@ export class Databuddy extends BaseTracker {
132156
{ keepalive: true }
133157
);
134158
}
135-
});
159+
};
160+
document.addEventListener("click", handler);
161+
this.cleanupFns.push(() => document.removeEventListener("click", handler));
136162
}
137163

138164
trackAttributes() {
139-
document.addEventListener("click", (e) => {
165+
const handler = (e: MouseEvent) => {
140166
const target = e.target as HTMLElement;
141167
const trackable = target.closest("[data-track]");
142168
if (trackable) {
143169
const eventName = trackable.getAttribute("data-track");
144170
if (eventName) {
145-
const properties: Record<string, any> = {};
171+
const properties: Record<string, string> = {};
146172
for (const attr of trackable.attributes) {
147173
if (attr.name.startsWith("data-") && attr.name !== "data-track") {
148174
const key = attr.name
@@ -154,21 +180,88 @@ export class Databuddy extends BaseTracker {
154180
this.track(eventName, properties);
155181
}
156182
}
157-
});
183+
};
184+
document.addEventListener("click", handler);
185+
this.cleanupFns.push(() => document.removeEventListener("click", handler));
158186
}
159187

160-
track(name: string, props: any) {
188+
track(name: string, props?: Record<string, unknown>) {
161189
const payload = {
162190
eventId: generateUUIDv4(),
163191
name,
164192
anonymousId: this.anonymousId,
165193
sessionId: this.sessionId,
166194
timestamp: Date.now(),
167195
...this.getBaseContext(),
196+
...this.globalProperties,
168197
...props,
169198
};
170199
this.send(payload);
171200
}
201+
202+
trackCustomEvent(name: string, props?: Record<string, unknown>) {
203+
this.track(name, {
204+
event_type: "custom",
205+
...props,
206+
});
207+
}
208+
209+
setGlobalProperties(props: Record<string, unknown>) {
210+
this.globalProperties = { ...this.globalProperties, ...props };
211+
}
212+
213+
clear() {
214+
this.globalProperties = {};
215+
216+
if (!this.isServer()) {
217+
try {
218+
localStorage.removeItem("did");
219+
sessionStorage.removeItem("did_session");
220+
sessionStorage.removeItem("did_session_timestamp");
221+
sessionStorage.removeItem("did_session_start");
222+
} catch {
223+
}
224+
}
225+
226+
this.anonymousId = this.generateAnonymousId();
227+
this.sessionId = this.generateSessionId();
228+
this.sessionStartTime = Date.now();
229+
this.pageCount = 0;
230+
this.lastPath = "";
231+
this.interactionCount = 0;
232+
this.maxScrollDepth = 0;
233+
}
234+
235+
destroy() {
236+
// Run all cleanup functions
237+
for (const cleanup of this.cleanupFns) {
238+
cleanup();
239+
}
240+
this.cleanupFns = [];
241+
242+
// Restore original history methods
243+
if (this.originalPushState) {
244+
history.pushState = this.originalPushState;
245+
this.originalPushState = null;
246+
}
247+
if (this.originalReplaceState) {
248+
history.replaceState = this.originalReplaceState;
249+
this.originalReplaceState = null;
250+
}
251+
252+
// Clear batch queue and timer
253+
if (this.batchTimer) {
254+
clearTimeout(this.batchTimer);
255+
this.batchTimer = null;
256+
}
257+
this.batchQueue = [];
258+
259+
// Remove global references
260+
if (typeof window !== "undefined") {
261+
window.databuddy = undefined;
262+
window.db = undefined;
263+
}
264+
}
172265
}
173266

174267
function initializeDatabuddy() {
@@ -183,7 +276,6 @@ function initializeDatabuddy() {
183276
window.databuddy = {
184277
track: () => { },
185278
screenView: () => { },
186-
identify: () => { },
187279
clear: () => { },
188280
flush: () => { },
189281
setGlobalProperties: () => { },

0 commit comments

Comments
 (0)