@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.
@@ -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(true);
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(false);
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 cleanly, reports a controlled diagnostic, and ignores late readiness', async () => {
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(5000);
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
- * Waits up to five seconds for Mixpanel readiness before event submission.
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
- return tryTrackEvent(
147
- () => submitEvent(eventName, eventProperties, meta),
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(cacheOnly);
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
- mixpanelService.track(eventName, properties);
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
- void identifyAndRelayFingerprint(eventName, distinctId);
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
- /** Identifies and relays only after the original event has been submitted. */
219
- async function identifyAndRelayFingerprint(
305
+ /** Refreshes optional identity after submission without delaying the event. */
306
+ async function refreshFingerprint(
220
307
  eventName: string,
221
- distinctId: string | null,
308
+ isCurrent: () => boolean,
222
309
  ): Promise<void> {
310
+ const deadline = Date.now() + 10000;
223
311
  try {
224
- if (!fingerprintService.isFingerprintReady()) return;
225
-
226
- const identification = await fingerprintService.identifyVisitor(true);
227
- if (!identification.requestId) return;
228
-
229
- await botDetectionRelayService.relay({
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