@reservamos/browser-analytics 1.1.8-alpha.0 → 1.1.8-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -1
- package/README.md +8 -4
- package/dist/browser-analytics.cjs +2 -2
- package/dist/browser-analytics.cjs.map +1 -1
- package/dist/browser-analytics.esm.js +417 -58
- package/dist/browser-analytics.esm.js.map +1 -1
- package/dist/browser-analytics.iife.js +2 -2
- package/dist/browser-analytics.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/events/sharedSchemas/tripSchema.ts +3 -8
- package/src/init.botDetection.test.ts +187 -0
- package/src/init.test.ts +1 -0
- package/src/init.ts +15 -2
- package/src/services/deferredBotDetection.ts +60 -0
- package/src/services/fingerprint.test.ts +18 -0
- package/src/services/fingerprint.ts +36 -9
- package/src/services/mixpanel.test.ts +42 -0
- package/src/services/mixpanel.ts +54 -5
- package/src/services/trackingLifecycle.ts +54 -0
- package/src/services/validator/validator.test.ts +79 -0
- package/src/track.readiness.test.ts +131 -0
- package/src/track.recovery.test.ts +339 -0
- package/src/track.storage.test.ts +127 -0
- package/src/track.test.ts +130 -5
- package/src/track.ts +110 -25
- package/src/util/pendingWork.test.ts +164 -0
- package/src/util/pendingWork.ts +148 -0
- package/src/util/snapshotEventData.ts +15 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import coreApi from '@reservamos/js-api-client/core';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import fingerprintService from '@/services/fingerprint';
|
|
4
|
+
import mixpanelService from '@/services/mixpanel';
|
|
5
|
+
import { trackEvent } from './track';
|
|
6
|
+
|
|
7
|
+
const { getVisitorData, init, client } = vi.hoisted(() => ({
|
|
8
|
+
getVisitorData: vi.fn(),
|
|
9
|
+
init: vi.fn(),
|
|
10
|
+
client: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Keep the SDK Fingerprint cache, tracking, validation, and relay real.
|
|
14
|
+
vi.mock('@fingerprintjs/fingerprintjs-pro-spa', () => ({
|
|
15
|
+
FingerprintJSPro: {
|
|
16
|
+
defaultEndpoint: 'https://fpjscdn.net',
|
|
17
|
+
defaultScriptUrlPattern: 'https://fpjscdn.net/agent',
|
|
18
|
+
},
|
|
19
|
+
FpjsClient: client,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const search = {
|
|
23
|
+
'Departure': '2026-11-01',
|
|
24
|
+
'Departure Delta': 1,
|
|
25
|
+
'Destination': 'Puebla',
|
|
26
|
+
'Destination Terminal': 'CAPU',
|
|
27
|
+
'Origin': 'Mexico City',
|
|
28
|
+
'Origin Terminal': 'TAPO',
|
|
29
|
+
'Passengers': 1,
|
|
30
|
+
'Route': 'Mexico City-Puebla',
|
|
31
|
+
'product': 'web',
|
|
32
|
+
};
|
|
33
|
+
const checkout = {
|
|
34
|
+
'Trips': [
|
|
35
|
+
{
|
|
36
|
+
'Departure Arrival': '2026-11-01T12:00:00',
|
|
37
|
+
'Departure Destination': 'Puebla',
|
|
38
|
+
'Departure Destination Terminal': 'CAPU',
|
|
39
|
+
'Departure Line': 'Bus line',
|
|
40
|
+
'Departure Origin': 'Mexico City',
|
|
41
|
+
'Departure Origin Terminal': 'TAPO',
|
|
42
|
+
'Departure Price': 100,
|
|
43
|
+
'Departure Route': 'Mexico City-Puebla',
|
|
44
|
+
'Departure Stops': 1,
|
|
45
|
+
'Departure Time': '2026-11-01T10:00:00',
|
|
46
|
+
'Departure Transport Type': 'bus',
|
|
47
|
+
'Departure Transporter': 'Carrier',
|
|
48
|
+
'Recommended Trip': false,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
'Passenger Count': 1,
|
|
52
|
+
'Trip Count': 1,
|
|
53
|
+
'Total': 100,
|
|
54
|
+
'product': 'web',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe('tracking with blocked SDK Fingerprint cache writes', () => {
|
|
58
|
+
beforeEach(async () => {
|
|
59
|
+
localStorage.clear();
|
|
60
|
+
client.mockImplementation(() => ({ init, getVisitorData }));
|
|
61
|
+
init.mockResolvedValue(undefined);
|
|
62
|
+
getVisitorData.mockResolvedValue({
|
|
63
|
+
visitorId: 'visitor-123',
|
|
64
|
+
requestId: 'request-123',
|
|
65
|
+
visitorFound: true,
|
|
66
|
+
confidence: { score: 0.99 },
|
|
67
|
+
cacheHit: false,
|
|
68
|
+
});
|
|
69
|
+
await fingerprintService.initFingerprint('test-api-key');
|
|
70
|
+
vi.spyOn(mixpanelService, 'isReady').mockReturnValue(true);
|
|
71
|
+
vi.spyOn(mixpanelService, 'getMixpanelDistinctId').mockReturnValue(
|
|
72
|
+
'live-id',
|
|
73
|
+
);
|
|
74
|
+
vi.spyOn(mixpanelService, 'track').mockImplementation(() => undefined);
|
|
75
|
+
vi.spyOn(mixpanelService, 'setPeopleProperties').mockImplementation(
|
|
76
|
+
() => undefined,
|
|
77
|
+
);
|
|
78
|
+
vi.spyOn(coreApi.fingerprint, 'sendObservation').mockResolvedValue(true);
|
|
79
|
+
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
80
|
+
throw new DOMException('Blocked', 'SecurityError');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
vi.restoreAllMocks();
|
|
86
|
+
vi.clearAllMocks();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it.each([
|
|
90
|
+
['Search', search, true],
|
|
91
|
+
['View Results', { ...search, 'Bus count': 10 }, false],
|
|
92
|
+
['Purchase Attempt', checkout, true],
|
|
93
|
+
['Payment Attempt', { ...checkout, 'Payment Type': 'credit_card' }, true],
|
|
94
|
+
] as const)(
|
|
95
|
+
'preserves valid %s and its eligible observation',
|
|
96
|
+
async (eventName, properties, eligible) => {
|
|
97
|
+
await trackEvent(eventName, properties);
|
|
98
|
+
|
|
99
|
+
expect(mixpanelService.track).toHaveBeenCalledOnce();
|
|
100
|
+
expect(mixpanelService.track).toHaveBeenCalledWith(
|
|
101
|
+
eventName,
|
|
102
|
+
expect.objectContaining({
|
|
103
|
+
'User Fingerprint': expect.any(String),
|
|
104
|
+
'product': 'web',
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
const verifyIdentificationEffects = () => {
|
|
108
|
+
expect(mixpanelService.setPeopleProperties).toHaveBeenCalledWith({
|
|
109
|
+
'Visitor Found': true,
|
|
110
|
+
});
|
|
111
|
+
expect(coreApi.fingerprint.sendObservation).toHaveBeenCalledTimes(
|
|
112
|
+
eligible ? 1 : 0,
|
|
113
|
+
);
|
|
114
|
+
if (eligible) {
|
|
115
|
+
expect(coreApi.fingerprint.sendObservation).toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
requestId: 'request-123',
|
|
118
|
+
visitorId: 'visitor-123',
|
|
119
|
+
distinctId: 'live-id',
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
await vi.waitFor(verifyIdentificationEffects);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
});
|
package/src/track.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import botDetectionRelayService from '@/services/botDetectionRelay';
|
|
3
3
|
import fingerprintService from '@/services/fingerprint';
|
|
4
4
|
import mixpanelService from '@/services/mixpanel';
|
|
5
|
+
import trackingLifecycle from '@/services/trackingLifecycle';
|
|
5
6
|
import validator from '@/services/validator';
|
|
6
7
|
import { trackEvent, trackEventError, tryTrackEvent } from './track';
|
|
7
8
|
|
|
@@ -18,6 +19,7 @@ vi.mock('@/services/fingerprint', () => ({
|
|
|
18
19
|
getCachedFingerprint: vi.fn(),
|
|
19
20
|
identifyVisitor: vi.fn(),
|
|
20
21
|
isFingerprintReady: vi.fn(),
|
|
22
|
+
onReady: vi.fn().mockReturnValue(() => undefined),
|
|
21
23
|
},
|
|
22
24
|
}));
|
|
23
25
|
|
|
@@ -30,6 +32,7 @@ vi.mock('@/services/geolocation', () => ({
|
|
|
30
32
|
|
|
31
33
|
vi.mock('@/services/mixpanel', () => ({
|
|
32
34
|
default: {
|
|
35
|
+
onReady: vi.fn().mockReturnValue(() => undefined),
|
|
33
36
|
isReady: vi.fn().mockReturnValue(true),
|
|
34
37
|
getMixpanelDistinctId: vi.fn().mockReturnValue('distinct-123'),
|
|
35
38
|
track: vi.fn(),
|
|
@@ -42,9 +45,12 @@ vi.mock('@/services/validator', () => ({
|
|
|
42
45
|
},
|
|
43
46
|
}));
|
|
44
47
|
|
|
48
|
+
let testIdentity = 0;
|
|
45
49
|
describe('trackEvent Bot Detection relay', () => {
|
|
46
50
|
beforeEach(() => {
|
|
51
|
+
trackingLifecycle.identify(`test-${testIdentity++}`);
|
|
47
52
|
vi.resetAllMocks();
|
|
53
|
+
vi.mocked(fingerprintService.onReady).mockReturnValue(() => undefined);
|
|
48
54
|
vi.mocked(mixpanelService.isReady).mockReturnValue(true);
|
|
49
55
|
vi.mocked(mixpanelService.getMixpanelDistinctId).mockReturnValue(
|
|
50
56
|
'distinct-123',
|
|
@@ -97,7 +103,10 @@ describe('trackEvent Bot Detection relay', () => {
|
|
|
97
103
|
expect(mixpanelService.track).toHaveBeenCalledTimes(1);
|
|
98
104
|
await completion;
|
|
99
105
|
expect(fingerprintService.getFingerprint).not.toHaveBeenCalled();
|
|
100
|
-
expect(fingerprintService.identifyVisitor).toHaveBeenCalledWith(
|
|
106
|
+
expect(fingerprintService.identifyVisitor).toHaveBeenCalledWith(
|
|
107
|
+
true,
|
|
108
|
+
expect.objectContaining({ isActive: expect.any(Function) }),
|
|
109
|
+
);
|
|
101
110
|
await vi.waitFor(() =>
|
|
102
111
|
expect(botDetectionRelayService.relay).toHaveBeenCalledOnce(),
|
|
103
112
|
);
|
|
@@ -248,13 +257,64 @@ describe('trackEvent Bot Detection relay', () => {
|
|
|
248
257
|
expect(botDetectionRelayService.relay).not.toHaveBeenCalled();
|
|
249
258
|
});
|
|
250
259
|
|
|
260
|
+
it('submits View Results while identification remains pending beyond 5.2 seconds', async () => {
|
|
261
|
+
vi.useFakeTimers();
|
|
262
|
+
try {
|
|
263
|
+
vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(false);
|
|
264
|
+
vi.mocked(fingerprintService.getCachedFingerprint).mockReturnValue(null);
|
|
265
|
+
vi.mocked(fingerprintService.getFingerprint).mockReturnValue(
|
|
266
|
+
new Promise(() => undefined),
|
|
267
|
+
);
|
|
268
|
+
const onComplete = vi.fn();
|
|
269
|
+
void trackEvent('View Results', { 'Results Count': 2 }).then(onComplete);
|
|
270
|
+
await vi.advanceTimersByTimeAsync(5200);
|
|
271
|
+
expect(mixpanelService.track).toHaveBeenCalledWith('View Results', {
|
|
272
|
+
'User Fingerprint': '',
|
|
273
|
+
'Results Count': 2,
|
|
274
|
+
});
|
|
275
|
+
expect(onComplete).toHaveBeenCalledOnce();
|
|
276
|
+
expect(botDetectionRelayService.relay).not.toHaveBeenCalled();
|
|
277
|
+
} finally {
|
|
278
|
+
vi.useRealTimers();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('keeps cached View Results identity and does not replay after late identification', async () => {
|
|
283
|
+
vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(false);
|
|
284
|
+
let finish!: (visitor: string) => void;
|
|
285
|
+
vi.mocked(fingerprintService.getFingerprint).mockImplementation(() => {
|
|
286
|
+
expect(mixpanelService.track).toHaveBeenCalledOnce();
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
finish = resolve;
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
await trackEvent('View Results', { 'Results Count': 2 }, { source: 'web' });
|
|
292
|
+
expect(mixpanelService.track).toHaveBeenCalledWith('View Results', {
|
|
293
|
+
'User Fingerprint': 'cached-visitor',
|
|
294
|
+
'Results Count': 2,
|
|
295
|
+
'source': 'web',
|
|
296
|
+
});
|
|
297
|
+
expect(fingerprintService.getFingerprint).toHaveBeenCalledWith(
|
|
298
|
+
false,
|
|
299
|
+
expect.objectContaining({ isActive: expect.any(Function) }),
|
|
300
|
+
);
|
|
301
|
+
finish('late-visitor');
|
|
302
|
+
await Promise.resolve();
|
|
303
|
+
expect(mixpanelService.track).toHaveBeenCalledOnce();
|
|
304
|
+
expect(botDetectionRelayService.relay).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
|
|
251
307
|
it('submits View Results with an empty fingerprint if identification rejects', async () => {
|
|
252
308
|
vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(false);
|
|
309
|
+
vi.mocked(fingerprintService.getCachedFingerprint).mockReturnValue(null);
|
|
253
310
|
vi.mocked(fingerprintService.getFingerprint).mockRejectedValue(
|
|
254
311
|
new Error('Fingerprint failed'),
|
|
255
312
|
);
|
|
256
313
|
await trackEvent('View Results', { 'Results Count': 2 }, { source: 'web' });
|
|
257
|
-
expect(fingerprintService.getFingerprint).toHaveBeenCalledWith(
|
|
314
|
+
expect(fingerprintService.getFingerprint).toHaveBeenCalledWith(
|
|
315
|
+
false,
|
|
316
|
+
expect.objectContaining({ isActive: expect.any(Function) }),
|
|
317
|
+
);
|
|
258
318
|
expect(mixpanelService.track).toHaveBeenCalledTimes(1);
|
|
259
319
|
expect(mixpanelService.track).toHaveBeenCalledWith('View Results', {
|
|
260
320
|
'User Fingerprint': '',
|
|
@@ -277,6 +337,70 @@ describe('trackEvent Bot Detection relay', () => {
|
|
|
277
337
|
});
|
|
278
338
|
expect(botDetectionRelayService.relay).not.toHaveBeenCalled();
|
|
279
339
|
});
|
|
340
|
+
|
|
341
|
+
it('keeps the original event exactly once when identification omits requestId', async () => {
|
|
342
|
+
const identification = {
|
|
343
|
+
visitorId: 'fresh-visitor',
|
|
344
|
+
confidenceScore: 0.99,
|
|
345
|
+
cacheHit: false,
|
|
346
|
+
} as Awaited<ReturnType<typeof fingerprintService.identifyVisitor>>;
|
|
347
|
+
vi.mocked(fingerprintService.identifyVisitor).mockResolvedValue(
|
|
348
|
+
identification,
|
|
349
|
+
);
|
|
350
|
+
await trackEvent('Search', {});
|
|
351
|
+
await Promise.resolve();
|
|
352
|
+
expect(mixpanelService.track).toHaveBeenCalledTimes(1);
|
|
353
|
+
expect(mixpanelService.track).toHaveBeenCalledWith('Search', {
|
|
354
|
+
'User Fingerprint': 'cached-visitor',
|
|
355
|
+
});
|
|
356
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
357
|
+
expect(botDetectionRelayService.relay).not.toHaveBeenCalled();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('omits an unavailable distinctId from the background relay', async () => {
|
|
361
|
+
vi.mocked(mixpanelService.getMixpanelDistinctId).mockReturnValue(null);
|
|
362
|
+
await trackEvent('Search', {});
|
|
363
|
+
await Promise.resolve();
|
|
364
|
+
await vi.waitFor(() => {
|
|
365
|
+
expect(botDetectionRelayService.relay).toHaveBeenCalledTimes(1);
|
|
366
|
+
expect(botDetectionRelayService.relay).toHaveBeenCalledWith({
|
|
367
|
+
requestId: 'fresh-request',
|
|
368
|
+
visitorId: 'fresh-visitor',
|
|
369
|
+
eventName: 'Search',
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
expect(mixpanelService.track).toHaveBeenCalledTimes(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it.each(['fingerprint', 'relay'] as const)(
|
|
376
|
+
'reports a controlled local diagnostic for a background %s failure',
|
|
377
|
+
async (stage) => {
|
|
378
|
+
const error = new Error('private visitor identifier');
|
|
379
|
+
if (stage === 'fingerprint') {
|
|
380
|
+
vi.mocked(fingerprintService.identifyVisitor).mockRejectedValue(error);
|
|
381
|
+
} else {
|
|
382
|
+
vi.mocked(botDetectionRelayService.relay).mockRejectedValue(error);
|
|
383
|
+
}
|
|
384
|
+
const diagnostic = vi
|
|
385
|
+
.spyOn(console, 'error')
|
|
386
|
+
.mockImplementation(() => undefined);
|
|
387
|
+
try {
|
|
388
|
+
await trackEvent('Search', {});
|
|
389
|
+
await Promise.resolve();
|
|
390
|
+
await vi.waitFor(() => {
|
|
391
|
+
expect(diagnostic).toHaveBeenCalledWith('Bot Detection diagnostic', {
|
|
392
|
+
eventName: 'Search',
|
|
393
|
+
stage,
|
|
394
|
+
reason: 'failed',
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
expect(diagnostic.mock.calls.flat()).not.toContain(error);
|
|
398
|
+
expect(mixpanelService.track).toHaveBeenCalledTimes(1);
|
|
399
|
+
} finally {
|
|
400
|
+
diagnostic.mockRestore();
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
);
|
|
280
404
|
});
|
|
281
405
|
|
|
282
406
|
describe('bounded tracking readiness', () => {
|
|
@@ -284,6 +408,7 @@ describe('bounded tracking readiness', () => {
|
|
|
284
408
|
vi.useFakeTimers();
|
|
285
409
|
vi.resetAllMocks();
|
|
286
410
|
vi.mocked(mixpanelService.isReady).mockReturnValue(false);
|
|
411
|
+
vi.mocked(mixpanelService.onReady).mockReturnValue(() => undefined);
|
|
287
412
|
vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(false);
|
|
288
413
|
vi.mocked(fingerprintService.getFingerprint).mockResolvedValue('');
|
|
289
414
|
vi.mocked(fingerprintService.isFingerprintReady).mockReturnValue(false);
|
|
@@ -336,15 +461,15 @@ describe('bounded tracking readiness', () => {
|
|
|
336
461
|
expect(vi.getTimerCount()).toBe(0);
|
|
337
462
|
});
|
|
338
463
|
|
|
339
|
-
it('expires
|
|
464
|
+
it('expires after the retention deadline and ignores later readiness', async () => {
|
|
340
465
|
const remove = vi.spyOn(window, 'removeEventListener');
|
|
341
466
|
const pending = trackEvent('Payment Attempt', {
|
|
342
467
|
secret: 'private-payload',
|
|
343
468
|
});
|
|
344
469
|
void pending.catch(() => undefined);
|
|
345
|
-
await vi.advanceTimersByTimeAsync(
|
|
470
|
+
await vi.advanceTimersByTimeAsync(60000);
|
|
346
471
|
await expect(pending).resolves.toBeUndefined();
|
|
347
|
-
expect(console.error).toHaveBeenCalledWith('Tracking diagnostic', {
|
|
472
|
+
expect(console.error).toHaveBeenCalledWith('Tracking recovery diagnostic', {
|
|
348
473
|
eventName: 'Payment Attempt',
|
|
349
474
|
stage: 'readiness',
|
|
350
475
|
reason: 'timeout',
|
package/src/track.ts
CHANGED
|
@@ -4,10 +4,14 @@ import type {
|
|
|
4
4
|
EventMetadata,
|
|
5
5
|
} from './types/eventData';
|
|
6
6
|
import botDetectionRelayService from '@/services/botDetectionRelay';
|
|
7
|
+
import { deferBotDetection } from '@/services/deferredBotDetection';
|
|
7
8
|
import fingerprintService from '@/services/fingerprint';
|
|
8
9
|
import geolocationService from '@/services/geolocation';
|
|
9
10
|
import mixpanelService from '@/services/mixpanel';
|
|
11
|
+
import trackingLifecycle from '@/services/trackingLifecycle';
|
|
10
12
|
import validator from '@/services/validator';
|
|
13
|
+
import { createPendingWorkQueue } from '@/util/pendingWork';
|
|
14
|
+
import { snapshotEventData } from '@/util/snapshotEventData';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* List of events that trigger the fingerprint to be sent with the event. other events will only fetch the cached fingerprint.
|
|
@@ -99,12 +103,14 @@ export async function tryTrackEvent(
|
|
|
99
103
|
const finish = (isReady: boolean) => {
|
|
100
104
|
clearTimeout(timer);
|
|
101
105
|
window.removeEventListener('Tracker Ready', onReady);
|
|
106
|
+
unsubscribe();
|
|
102
107
|
resolve(isReady);
|
|
103
108
|
};
|
|
104
109
|
const onReady = () => {
|
|
105
110
|
if (mixpanelService.isReady()) finish(true);
|
|
106
111
|
};
|
|
107
112
|
const timer = setTimeout(() => finish(mixpanelService.isReady()), 5000);
|
|
113
|
+
const unsubscribe = mixpanelService.onReady(onReady);
|
|
108
114
|
window.addEventListener('Tracker Ready', onReady);
|
|
109
115
|
onReady();
|
|
110
116
|
});
|
|
@@ -135,25 +141,96 @@ export async function tryTrackEvent(
|
|
|
135
141
|
* @param {string} eventName - The name of the event to track.
|
|
136
142
|
* @param {object} eventProperties - The properties of the event to track.
|
|
137
143
|
* @param {EventMetadata} meta - Additional metadata to include in the event.
|
|
138
|
-
*
|
|
139
|
-
* Optional fingerprint failure does not discard an otherwise valid event.
|
|
144
|
+
* Retains unsent events for up to sixty seconds without retrying a started submission.
|
|
140
145
|
*/
|
|
146
|
+
const pendingEvents = createPendingWorkQueue({
|
|
147
|
+
isReady: () => mixpanelService.isReady(),
|
|
148
|
+
subscribe(notify) {
|
|
149
|
+
const stopReady = mixpanelService.onReady(notify);
|
|
150
|
+
const stopIdentity = trackingLifecycle.onChange(notify);
|
|
151
|
+
window.addEventListener('Tracker Ready', notify);
|
|
152
|
+
return () => {
|
|
153
|
+
stopReady();
|
|
154
|
+
stopIdentity();
|
|
155
|
+
window.removeEventListener('Tracker Ready', notify);
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
maxEntries: 100,
|
|
159
|
+
maxBytes: 1024 * 1024,
|
|
160
|
+
maxActive: 100,
|
|
161
|
+
lifetime: 60000,
|
|
162
|
+
wait: 5000,
|
|
163
|
+
stage: 'readiness',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/** Resolves within the initial five-second wait; unsent events can remain pending for 60 seconds. */
|
|
141
167
|
export function trackEvent(
|
|
142
168
|
eventName: string,
|
|
143
169
|
eventProperties: object,
|
|
144
170
|
meta: EventMetadata = {},
|
|
145
171
|
): Promise<void> {
|
|
146
|
-
|
|
147
|
-
|
|
172
|
+
const isCurrent = trackingLifecycle.capture();
|
|
173
|
+
const trackingParams = extractTrackingParams();
|
|
174
|
+
if (mixpanelService.isReady()) {
|
|
175
|
+
return submitEvent(
|
|
176
|
+
eventName,
|
|
177
|
+
eventProperties,
|
|
178
|
+
meta,
|
|
179
|
+
isCurrent,
|
|
180
|
+
trackingParams,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
validator.parseEventProps(eventName, eventProperties);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
trackEventError(eventName, error, eventProperties);
|
|
187
|
+
return Promise.resolve();
|
|
188
|
+
}
|
|
189
|
+
let snapshot: { eventProperties: object; meta: EventMetadata };
|
|
190
|
+
let bytes: number;
|
|
191
|
+
try {
|
|
192
|
+
const serialized = JSON.stringify(
|
|
193
|
+
{ eventName, eventProperties, meta, trackingParams },
|
|
194
|
+
(_key, value: unknown) => {
|
|
195
|
+
if (typeof value === 'function' || typeof value === 'symbol')
|
|
196
|
+
throw new Error();
|
|
197
|
+
return value === undefined ? null : value;
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
bytes = new Blob([serialized]).size;
|
|
201
|
+
if (bytes > 1024 * 1024) throw new Error();
|
|
202
|
+
snapshot = snapshotEventData({ eventProperties, meta });
|
|
203
|
+
} catch {
|
|
204
|
+
console.error('Tracking recovery diagnostic', {
|
|
205
|
+
eventName,
|
|
206
|
+
stage: 'readiness',
|
|
207
|
+
reason: 'snapshot_unavailable',
|
|
208
|
+
});
|
|
209
|
+
return Promise.resolve();
|
|
210
|
+
}
|
|
211
|
+
return pendingEvents.enqueue({
|
|
148
212
|
eventName,
|
|
149
|
-
|
|
213
|
+
bytes,
|
|
214
|
+
isCurrent,
|
|
215
|
+
run: (isActive) =>
|
|
216
|
+
submitEvent(
|
|
217
|
+
eventName,
|
|
218
|
+
snapshot.eventProperties,
|
|
219
|
+
snapshot.meta,
|
|
220
|
+
isActive,
|
|
221
|
+
trackingParams,
|
|
222
|
+
),
|
|
223
|
+
});
|
|
150
224
|
}
|
|
151
225
|
|
|
152
226
|
async function submitEvent(
|
|
153
227
|
eventName: string,
|
|
154
228
|
eventProperties: object,
|
|
155
229
|
meta: EventMetadata,
|
|
230
|
+
isCurrent: () => boolean,
|
|
231
|
+
trackingParams: Record<string, string>,
|
|
156
232
|
): Promise<void> {
|
|
233
|
+
const observationContext = trackingLifecycle.capture();
|
|
157
234
|
try {
|
|
158
235
|
validator.parseEventProps(eventName, eventProperties);
|
|
159
236
|
// Call the Mixpanel service to store the distinct ID in the cache
|
|
@@ -162,16 +239,15 @@ async function submitEvent(
|
|
|
162
239
|
botDetectionRelayService.isEnabledFor(eventName);
|
|
163
240
|
let fingerprint = '';
|
|
164
241
|
|
|
165
|
-
if (botDetectionEnabled) {
|
|
242
|
+
if (botDetectionEnabled || FP_TRIGGER_EVENTS.includes(eventName)) {
|
|
166
243
|
try {
|
|
167
244
|
fingerprint = fingerprintService.getCachedFingerprint() ?? '';
|
|
168
245
|
} catch (error) {
|
|
169
246
|
console.error('Error reading cached fingerprint:', error);
|
|
170
247
|
}
|
|
171
248
|
} else {
|
|
172
|
-
const cacheOnly = !FP_TRIGGER_EVENTS.includes(eventName);
|
|
173
249
|
try {
|
|
174
|
-
fingerprint = await fingerprintService.getFingerprint(
|
|
250
|
+
fingerprint = await fingerprintService.getFingerprint(true);
|
|
175
251
|
} catch {
|
|
176
252
|
console.error('Tracking diagnostic', {
|
|
177
253
|
eventName,
|
|
@@ -193,7 +269,6 @@ async function submitEvent(
|
|
|
193
269
|
const defaultProperties = {
|
|
194
270
|
'User Fingerprint': fingerprint,
|
|
195
271
|
};
|
|
196
|
-
const trackingParams = extractTrackingParams();
|
|
197
272
|
|
|
198
273
|
const simplifiedData = flattenEventData(eventProperties);
|
|
199
274
|
const properties = {
|
|
@@ -204,10 +279,22 @@ async function submitEvent(
|
|
|
204
279
|
...meta,
|
|
205
280
|
};
|
|
206
281
|
|
|
207
|
-
|
|
282
|
+
if (!isCurrent()) return;
|
|
283
|
+
try {
|
|
284
|
+
mixpanelService.track(eventName, properties);
|
|
285
|
+
} catch {
|
|
286
|
+
console.error('Tracking diagnostic', {
|
|
287
|
+
eventName,
|
|
288
|
+
stage: 'submission',
|
|
289
|
+
reason: 'uncertain',
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
208
293
|
|
|
209
294
|
if (botDetectionEnabled) {
|
|
210
|
-
|
|
295
|
+
deferBotDetection(eventName, distinctId, observationContext);
|
|
296
|
+
} else if (FP_TRIGGER_EVENTS.includes(eventName)) {
|
|
297
|
+
void refreshFingerprint(eventName, observationContext);
|
|
211
298
|
}
|
|
212
299
|
} catch (error) {
|
|
213
300
|
console.error(`Error tracking event '${eventName}':`, error);
|
|
@@ -215,25 +302,23 @@ async function submitEvent(
|
|
|
215
302
|
}
|
|
216
303
|
}
|
|
217
304
|
|
|
218
|
-
/**
|
|
219
|
-
async function
|
|
305
|
+
/** Refreshes optional identity after submission without delaying the event. */
|
|
306
|
+
async function refreshFingerprint(
|
|
220
307
|
eventName: string,
|
|
221
|
-
|
|
308
|
+
isCurrent: () => boolean,
|
|
222
309
|
): Promise<void> {
|
|
310
|
+
const deadline = Date.now() + 10000;
|
|
223
311
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
requestId: identification.requestId,
|
|
231
|
-
visitorId: identification.visitorId,
|
|
232
|
-
...(distinctId && { distinctId }),
|
|
312
|
+
await fingerprintService.getFingerprint(false, {
|
|
313
|
+
isActive: () => isCurrent() && Date.now() < deadline,
|
|
314
|
+
report: () => undefined,
|
|
315
|
+
});
|
|
316
|
+
} catch {
|
|
317
|
+
console.error('Tracking diagnostic', {
|
|
233
318
|
eventName,
|
|
319
|
+
stage: 'fingerprint',
|
|
320
|
+
reason: 'failed',
|
|
234
321
|
});
|
|
235
|
-
} catch (error) {
|
|
236
|
-
console.error(`Error running Bot Detection for '${eventName}':`, error);
|
|
237
322
|
}
|
|
238
323
|
}
|
|
239
324
|
|