@reservamos/browser-analytics 1.1.7-alpha.0 → 1.1.7

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.
@@ -1,7 +1,10 @@
1
+ import type { FingerprintIdentification } from '@/types/botDetection';
2
+ import type { IdentifyStageContext } from '@/util/identifyAttempt';
1
3
  import {
2
4
  FingerprintJSPro,
3
5
  FpjsClient,
4
6
  } from '@fingerprintjs/fingerprintjs-pro-spa';
7
+ import mixpanelService from '@/services/mixpanel';
5
8
 
6
9
  declare global {
7
10
  interface Window {
@@ -18,6 +21,12 @@ const DEFAULT_CACHE_NAME = 'default_fingerprint_cache';
18
21
  const DEFAULT_CACHE_TIME_IN_DAYS = 7;
19
22
  const PROXY_QUERY_PARAM =
20
23
  'apiKey=<apiKey>&version=<version>&loaderVersion=<loaderVersion>';
24
+
25
+ /**
26
+ * True only after `fpClient.init()` succeeds. `window.fpClient` exists as soon as
27
+ * the client is constructed, but that instance still uses a stub agent that throws
28
+ * "FPJSAgent hasn't loaded yet" until init completes.
29
+ */
21
30
  let fingerprintAgentReady = false;
22
31
 
23
32
  /**
@@ -38,6 +47,10 @@ const getExpirationDate = (cacheTimeInDays: number): number => {
38
47
  * @returns {string | null} - The cached fingerprint or null if it is expired or not found.
39
48
  */
40
49
  const getCachedFingerprint = (): string | null => {
50
+ return readCachedFingerprint();
51
+ };
52
+
53
+ function readCachedFingerprint(context?: IdentifyStageContext): string | null {
41
54
  const { cacheName = DEFAULT_CACHE_NAME } = window.fingerprintConfig;
42
55
 
43
56
  try {
@@ -48,9 +61,10 @@ const getCachedFingerprint = (): string | null => {
48
61
  localStorage.removeItem(cacheName);
49
62
  return null;
50
63
  } catch {
64
+ context?.report('cache', 'unavailable');
51
65
  return null;
52
66
  }
53
- };
67
+ }
54
68
 
55
69
  /**
56
70
  * Sets the cached fingerprint in local storage.
@@ -130,11 +144,26 @@ async function initFingerprint(
130
144
 
131
145
  /**
132
146
  * Retrieves the fingerprint, first checking the cache and then querying the identification service if not found.
147
+ * Returns an empty string when the Fingerprint agent has not loaded, so callers like identify() can continue.
133
148
  * @param {boolean} cacheOnly - Optional flag to retrieve the fingerprint only from the cache.
134
- * @returns {Promise<string>} A promise that resolves with the fingerprint, or an empty string when the agent is unavailable.
149
+ * @returns {Promise<string>} A promise that resolves with the fingerprint, or an empty string when unavailable.
135
150
  * @throws {Error} Throws an error if retrieval fails after the agent has loaded.
136
151
  */
137
152
  async function getFingerprint(cacheOnly = false): Promise<string> {
153
+ return readFingerprint(cacheOnly);
154
+ }
155
+
156
+ /** Internal identify path with late-effect suppression and sanitized diagnostics. */
157
+ export function getFingerprintForIdentify(
158
+ context: IdentifyStageContext,
159
+ ): Promise<string> {
160
+ return readFingerprint(false, context);
161
+ }
162
+
163
+ async function readFingerprint(
164
+ cacheOnly: boolean,
165
+ context?: IdentifyStageContext,
166
+ ): Promise<string> {
138
167
  if (!window.fingerprintConfig) {
139
168
  if (cacheOnly) {
140
169
  return '';
@@ -143,7 +172,7 @@ async function getFingerprint(cacheOnly = false): Promise<string> {
143
172
  throw new Error('Fingerprint configuration is not initialized.');
144
173
  }
145
174
 
146
- const cachedFingerprint = getCachedFingerprint();
175
+ const cachedFingerprint = readCachedFingerprint(context);
147
176
  if (cachedFingerprint) {
148
177
  return cachedFingerprint;
149
178
  }
@@ -152,22 +181,69 @@ async function getFingerprint(cacheOnly = false): Promise<string> {
152
181
  return '';
153
182
  }
154
183
 
184
+ const { visitorId } = await requestIdentification(false, context);
185
+ return visitorId;
186
+ }
187
+
188
+ /**
189
+ * Requests detailed visitor identification data from Fingerprint.
190
+ * @param {boolean} ignoreCache - Whether the Fingerprint SPA cache must be bypassed.
191
+ * @returns {Promise<FingerprintIdentification>} Request-scoped identification data.
192
+ * @throws {Error} If the identification service is not ready or retrieval fails.
193
+ */
194
+ async function identifyVisitor(
195
+ ignoreCache = false,
196
+ ): Promise<FingerprintIdentification> {
197
+ return requestIdentification(ignoreCache);
198
+ }
199
+
200
+ async function requestIdentification(
201
+ ignoreCache: boolean,
202
+ context?: IdentifyStageContext,
203
+ ): Promise<FingerprintIdentification> {
204
+ if (!isFingerprintReady()) {
205
+ throw new Error('Identification service is not initialized.');
206
+ }
207
+
155
208
  try {
156
- const fpClient = window.fpClient;
157
- const { visitorId: fingerprint } = await fpClient.getVisitorData();
158
- setCachedFingerprint(fingerprint);
159
- return fingerprint;
209
+ const result = await window.fpClient.getVisitorData({}, ignoreCache);
210
+ if (!context || context.isActive()) {
211
+ try {
212
+ setCachedFingerprint(result.visitorId);
213
+ } catch (error) {
214
+ if (!context) throw error;
215
+ context.report('cache', 'unavailable');
216
+ }
217
+ if (typeof result.visitorFound === 'boolean') {
218
+ try {
219
+ mixpanelService.setPeopleProperties({
220
+ 'Visitor Found': result.visitorFound,
221
+ });
222
+ } catch (error) {
223
+ if (context) context.report('fingerprint', 'failed');
224
+ else console.error('Error updating Mixpanel visitor profile:', error);
225
+ }
226
+ }
227
+ }
228
+
229
+ return {
230
+ visitorId: result.visitorId,
231
+ requestId: result.requestId,
232
+ ...(typeof result.visitorFound === 'boolean'
233
+ ? { visitorFound: result.visitorFound }
234
+ : {}),
235
+ confidenceScore: result.confidence.score,
236
+ cacheHit: result.cacheHit,
237
+ };
160
238
  } catch (error) {
161
- console.error('Error retrieving fingerprint:', error);
239
+ if (!context) console.error('Error retrieving fingerprint:', error);
162
240
  throw error;
163
241
  }
164
242
  }
165
243
 
166
244
  /**
167
- * Checks if the identification service is ready.
168
- *
169
- * This function verifies that the Fingerprint agent loaded successfully.
170
- * @returns {boolean} Returns true only after the agent has loaded.
245
+ * Checks if the Fingerprint agent finished loading and can identify visitors.
246
+ * @returns {boolean} True only after `initFingerprint()` has successfully loaded the agent.
171
247
  */
172
248
  function isFingerprintReady(): boolean {
173
249
  return window.fpClient !== undefined && fingerprintAgentReady;
@@ -176,6 +252,7 @@ function isFingerprintReady(): boolean {
176
252
  const fingerprintService = {
177
253
  initFingerprint,
178
254
  getFingerprint,
255
+ identifyVisitor,
179
256
  isFingerprintReady,
180
257
  getCachedFingerprint,
181
258
  };
@@ -0,0 +1,160 @@
1
+ import mixpanel from 'mixpanel-browser';
2
+ import { version as mixpanelVersion } from 'mixpanel-browser/package.json';
3
+ import {
4
+ afterAll,
5
+ afterEach,
6
+ beforeAll,
7
+ describe,
8
+ expect,
9
+ it,
10
+ vi,
11
+ } from 'vitest';
12
+ import { version } from '../../package.json';
13
+ import mixpanelService from './mixpanel';
14
+
15
+ interface TransportData {
16
+ event?: string;
17
+ properties?: Record<string, unknown>;
18
+ }
19
+
20
+ // Intercept transport only: identify, track, register, and property merging stay real.
21
+ const transport = mixpanel as unknown as {
22
+ _track_or_batch(options: { data: TransportData }): TransportData;
23
+ stop_batch_senders(): void;
24
+ };
25
+
26
+ describe('Mixpanel SDK version metadata', () => {
27
+ beforeAll(async () => {
28
+ localStorage.clear();
29
+ await mixpanelService.init('test-sdk-version-token');
30
+ transport.stop_batch_senders();
31
+ mixpanel.set_config({ disable_persistence: true });
32
+ });
33
+
34
+ afterEach(() => {
35
+ vi.restoreAllMocks();
36
+ vi.clearAllTimers();
37
+ vi.useRealTimers();
38
+ });
39
+
40
+ afterAll(() => {
41
+ transport.stop_batch_senders();
42
+ localStorage.clear();
43
+ });
44
+
45
+ it('registers SDK metadata and propagates it to the native $identify event', () => {
46
+ expect(mixpanel.get_property('Event Source')).toBe('sdk');
47
+ expect(mixpanel.get_property('Reservamos Version')).toBe(version);
48
+
49
+ const send = vi
50
+ .spyOn(transport, '_track_or_batch')
51
+ .mockImplementation(({ data }) => data);
52
+
53
+ mixpanelService.identify('sdk-version-user', {});
54
+ const events = send.mock.calls
55
+ .map(([options]) => options.data)
56
+ .filter((data) => data.event);
57
+
58
+ expect(events).toHaveLength(1);
59
+ expect(events[0]).toEqual({
60
+ event: '$identify',
61
+ properties: expect.objectContaining({
62
+ 'Event Source': 'sdk',
63
+ 'Reservamos Version': version,
64
+ '$lib_version': mixpanelVersion,
65
+ }),
66
+ });
67
+
68
+ send.mockClear();
69
+ mixpanelService.identify('sdk-version-user', {});
70
+ expect(send.mock.calls.some(([options]) => options.data.event)).toBe(false);
71
+ });
72
+
73
+ it('keeps the SDK version authoritative for SDK-tracked events', () => {
74
+ const send = vi
75
+ .spyOn(transport, '_track_or_batch')
76
+ .mockImplementation(({ data }) => data);
77
+
78
+ mixpanelService.track('Test', { 'Reservamos Version': 'caller-version' });
79
+
80
+ expect(send.mock.calls[0][0].data).toEqual({
81
+ event: 'Test',
82
+ properties: expect.objectContaining({
83
+ 'Event Source': 'sdk',
84
+ 'Reservamos Version': version,
85
+ }),
86
+ });
87
+ });
88
+
89
+ it('still sets people properties when the SDK distinct ID cache is blocked', () => {
90
+ vi.spyOn(transport, '_track_or_batch').mockImplementation(
91
+ ({ data }) => data,
92
+ );
93
+ vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
94
+ throw new DOMException('Blocked', 'SecurityError');
95
+ });
96
+ const people = vi
97
+ .spyOn(mixpanel.people, 'set')
98
+ .mockImplementation(() => undefined);
99
+ mixpanelService.identify('cache-blocked-user', {});
100
+ expect(people).toHaveBeenCalledWith({});
101
+ });
102
+
103
+ it.each([
104
+ 1,
105
+ 0,
106
+ { status: 1, error: null },
107
+ { status: 0, error: 'private-id' },
108
+ -1,
109
+ ])(
110
+ 'observes the people callback %j with controlled diagnostics',
111
+ (response) => {
112
+ vi.useFakeTimers();
113
+ vi.spyOn(mixpanel, 'identify').mockImplementation(() => undefined);
114
+ vi.spyOn(transport, '_track_or_batch').mockImplementation(
115
+ ({ data }) => data,
116
+ );
117
+ let callback: ((response: unknown) => void) | undefined;
118
+ vi.spyOn(mixpanel.people, 'set').mockImplementation((...args) => {
119
+ callback = args[1] as (response: unknown) => void;
120
+ });
121
+ const report = vi.fn();
122
+ const schedule = vi.spyOn(globalThis, 'setTimeout');
123
+ const cancel = vi.spyOn(globalThis, 'clearTimeout');
124
+ mixpanelService.identify('callback-user', {}, report);
125
+ expect(schedule).toHaveBeenCalledWith(expect.any(Function), 5000);
126
+ const timeout = schedule.mock.results.at(-1)?.value;
127
+ callback?.(response);
128
+ expect(cancel).toHaveBeenCalledWith(timeout);
129
+ const status = typeof response === 'number' ? response : response.status;
130
+ if (status === 1) expect(report).not.toHaveBeenCalled();
131
+ else
132
+ expect(report).toHaveBeenCalledWith(
133
+ 'people',
134
+ status === 0 ? 'failed' : 'unconfirmed',
135
+ undefined,
136
+ );
137
+ vi.advanceTimersByTime(5000);
138
+ expect(report).toHaveBeenCalledTimes(status === 1 ? 0 : 1);
139
+ },
140
+ );
141
+
142
+ it('reports missing people callback as unobserved and ignores a late result', () => {
143
+ vi.useFakeTimers();
144
+ vi.spyOn(mixpanel, 'identify').mockImplementation(() => undefined);
145
+ vi.spyOn(transport, '_track_or_batch').mockImplementation(
146
+ ({ data }) => data,
147
+ );
148
+ let callback: ((response: unknown) => void) | undefined;
149
+ vi.spyOn(mixpanel.people, 'set').mockImplementation((...args) => {
150
+ callback = args[1] as (response: unknown) => void;
151
+ });
152
+ const report = vi.fn();
153
+ mixpanelService.identify('callback-user', {}, report);
154
+ vi.advanceTimersByTime(5000);
155
+ callback?.(0);
156
+ expect(report).toHaveBeenCalledOnce();
157
+ expect(report).toHaveBeenCalledWith('people', 'unobserved', undefined);
158
+ expect(vi.getTimerCount()).toBe(0);
159
+ });
160
+ });
@@ -1,4 +1,6 @@
1
+ import type { IdentifyReporter } from '@/util/identifyAttempt';
1
2
  import mixpanel from 'mixpanel-browser';
3
+ import { observeIdentifyResult } from '@/util/identifyAttempt';
2
4
  import { version } from '../../package.json';
3
5
 
4
6
  declare global {
@@ -10,10 +12,11 @@ declare global {
10
12
  const MIXPANEL_DISTINCT_ID_CACHE_KEY = 'mp_distinct_id';
11
13
 
12
14
  /**
13
- * Super property sent on every Mixpanel event. Fixed SDK marker; not configurable via the public API.
15
+ * SDK metadata sent on every Mixpanel event, including native identification events.
14
16
  */
15
- const EVENT_SOURCE_SUPER_PROPERTY = {
17
+ const SDK_SUPER_PROPERTIES = {
16
18
  'Event Source': 'sdk',
19
+ 'Reservamos Version': version,
17
20
  } as const;
18
21
 
19
22
  /**
@@ -33,7 +36,7 @@ function init(
33
36
  debug,
34
37
  loaded: () => {
35
38
  window.mixpanel = mixpanel;
36
- mixpanel.register({ ...EVENT_SOURCE_SUPER_PROPERTY });
39
+ mixpanel.register({ ...SDK_SUPER_PROPERTIES });
37
40
  resolve();
38
41
  },
39
42
  ...(proxyUrl && { api_host: proxyUrl }),
@@ -48,10 +51,32 @@ function init(
48
51
  * @returns {void}
49
52
  * @throws {Error} If Mixpanel is not initialized (handled internally).
50
53
  */
51
- function identify(userId: string, properties: Record<string, unknown>): void {
54
+ function identify(
55
+ userId: string,
56
+ properties: Record<string, unknown>,
57
+ report?: IdentifyReporter,
58
+ ): void {
52
59
  mixpanel.identify(userId);
53
- updateCachedDistinctId(userId);
54
- mixpanel.people.set(properties);
60
+ try {
61
+ updateCachedDistinctId(userId);
62
+ } catch {
63
+ report?.('cache', 'unavailable');
64
+ }
65
+ if (!report) {
66
+ mixpanel.people.set(properties);
67
+ return;
68
+ }
69
+ const observer = observeIdentifyResult('people', report);
70
+ try {
71
+ mixpanel.people.set(properties, (response) => {
72
+ const status = typeof response === 'number' ? response : response?.status;
73
+ observer.complete(
74
+ status === 1 ? undefined : status === 0 ? 'failed' : 'unconfirmed',
75
+ );
76
+ });
77
+ } catch {
78
+ observer.complete('failed');
79
+ }
55
80
  }
56
81
 
57
82
  /**
@@ -100,6 +125,7 @@ function track(eventName: string, properties: Record<string, unknown>): void {
100
125
  if (!isReady()) {
101
126
  return;
102
127
  }
128
+ // Event properties override superproperties; keep the SDK version authoritative.
103
129
  mixpanel.track(eventName, { ...properties, 'Reservamos Version': version });
104
130
  }
105
131
 
@@ -0,0 +1,156 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import botDetectionRelayService from '@/services/botDetectionRelay';
3
+ import fingerprintService from '@/services/fingerprint';
4
+ import mixpanelService from '@/services/mixpanel';
5
+ import { trackEvent } from './track';
6
+
7
+ vi.mock('@/services/botDetectionRelay', () => ({
8
+ default: {
9
+ isEnabledFor: vi.fn(),
10
+ relay: vi.fn().mockResolvedValue(undefined),
11
+ },
12
+ }));
13
+
14
+ vi.mock('@/services/fingerprint', () => ({
15
+ default: {
16
+ getFingerprint: vi.fn(),
17
+ identifyVisitor: vi.fn(),
18
+ isFingerprintReady: vi.fn(),
19
+ },
20
+ }));
21
+
22
+ vi.mock('@/services/geolocation', () => ({
23
+ default: {
24
+ getCoordinates: vi.fn().mockReturnValue(null),
25
+ getGeolocationSource: vi.fn().mockReturnValue(null),
26
+ },
27
+ }));
28
+
29
+ vi.mock('@/services/mixpanel', () => ({
30
+ default: {
31
+ isReady: vi.fn().mockReturnValue(true),
32
+ getMixpanelDistinctId: vi.fn().mockReturnValue('distinct-123'),
33
+ track: vi.fn(),
34
+ },
35
+ }));
36
+
37
+ vi.mock('@/services/validator', () => ({
38
+ default: {
39
+ parseEventProps: vi.fn(),
40
+ },
41
+ }));
42
+
43
+ describe('trackEvent Bot Detection relay', () => {
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ vi.mocked(mixpanelService.isReady).mockReturnValue(true);
47
+ vi.mocked(mixpanelService.getMixpanelDistinctId).mockReturnValue(
48
+ 'distinct-123',
49
+ );
50
+ vi.mocked(fingerprintService.isFingerprintReady).mockReturnValue(true);
51
+ });
52
+
53
+ it('uses a fresh identification and relays a configured event', async () => {
54
+ vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(true);
55
+ vi.mocked(fingerprintService.identifyVisitor).mockResolvedValue({
56
+ visitorId: 'visitor-123',
57
+ requestId: 'request-123',
58
+ visitorFound: true,
59
+ confidenceScore: 0.99,
60
+ cacheHit: false,
61
+ });
62
+
63
+ await trackEvent('Payment Attempt', { 'Payment Type': 'credit_card' });
64
+
65
+ expect(fingerprintService.identifyVisitor).toHaveBeenCalledWith(true);
66
+ expect(fingerprintService.getFingerprint).not.toHaveBeenCalled();
67
+ expect(mixpanelService.track).toHaveBeenCalledWith('Payment Attempt', {
68
+ 'User Fingerprint': 'visitor-123',
69
+ 'Payment Type': 'credit_card',
70
+ });
71
+ expect(botDetectionRelayService.relay).toHaveBeenCalledWith({
72
+ requestId: 'request-123',
73
+ visitorId: 'visitor-123',
74
+ distinctId: 'distinct-123',
75
+ eventName: 'Payment Attempt',
76
+ });
77
+ });
78
+
79
+ it('reports a failed fresh identification without tracking or relaying the configured event', async () => {
80
+ vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(true);
81
+ vi.mocked(fingerprintService.identifyVisitor).mockRejectedValue(
82
+ new Error('Fingerprint identification failed'),
83
+ );
84
+ const eventProperties = { 'Payment Type': 'credit_card' };
85
+
86
+ await trackEvent('Payment Attempt', eventProperties);
87
+
88
+ expect(fingerprintService.identifyVisitor).toHaveBeenCalledWith(true);
89
+ expect(fingerprintService.getFingerprint).not.toHaveBeenCalled();
90
+ expect(botDetectionRelayService.relay).not.toHaveBeenCalled();
91
+ expect(mixpanelService.track).toHaveBeenCalledTimes(1);
92
+ expect(mixpanelService.track).toHaveBeenCalledWith('Track Event Error', {
93
+ 'Failed Event Name': 'Payment Attempt',
94
+ 'Error Message': 'Fingerprint identification failed',
95
+ 'Validation Errors': [],
96
+ 'Event Payload': eventProperties,
97
+ });
98
+ });
99
+
100
+ it('preserves existing fingerprint behavior for an unconfigured event', async () => {
101
+ vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(false);
102
+ vi.mocked(fingerprintService.getFingerprint).mockResolvedValue(
103
+ 'cached-visitor',
104
+ );
105
+
106
+ await trackEvent('Payment Attempt', { 'Payment Type': 'credit_card' });
107
+
108
+ expect(fingerprintService.getFingerprint).toHaveBeenCalledWith(true);
109
+ expect(fingerprintService.identifyVisitor).not.toHaveBeenCalled();
110
+ expect(mixpanelService.track).toHaveBeenCalledWith('Payment Attempt', {
111
+ 'User Fingerprint': 'cached-visitor',
112
+ 'Payment Type': 'credit_card',
113
+ });
114
+ expect(botDetectionRelayService.relay).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('uses a fresh identification and relays Search', async () => {
118
+ vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(true);
119
+ vi.mocked(fingerprintService.identifyVisitor).mockResolvedValue({
120
+ visitorId: 'visitor-123',
121
+ requestId: 'search-request-123',
122
+ visitorFound: true,
123
+ confidenceScore: 0.99,
124
+ cacheHit: false,
125
+ });
126
+
127
+ await trackEvent('Search', {});
128
+
129
+ expect(fingerprintService.identifyVisitor).toHaveBeenCalledWith(true);
130
+ expect(fingerprintService.getFingerprint).not.toHaveBeenCalled();
131
+ expect(mixpanelService.track).toHaveBeenCalledWith('Search', {
132
+ 'User Fingerprint': 'visitor-123',
133
+ });
134
+ expect(botDetectionRelayService.relay).toHaveBeenCalledWith({
135
+ requestId: 'search-request-123',
136
+ visitorId: 'visitor-123',
137
+ distinctId: 'distinct-123',
138
+ eventName: 'Search',
139
+ });
140
+ });
141
+
142
+ it('tracks Search without a relay when Fingerprint is not ready', async () => {
143
+ vi.mocked(botDetectionRelayService.isEnabledFor).mockReturnValue(true);
144
+ vi.mocked(fingerprintService.isFingerprintReady).mockReturnValue(false);
145
+ vi.mocked(fingerprintService.getFingerprint).mockResolvedValue('');
146
+
147
+ await trackEvent('Search', {});
148
+
149
+ expect(fingerprintService.identifyVisitor).not.toHaveBeenCalled();
150
+ expect(fingerprintService.getFingerprint).toHaveBeenCalledWith(false);
151
+ expect(mixpanelService.track).toHaveBeenCalledWith('Search', {
152
+ 'User Fingerprint': '',
153
+ });
154
+ expect(botDetectionRelayService.relay).not.toHaveBeenCalled();
155
+ });
156
+ });
package/src/track.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  EventData,
4
4
  EventMetadata,
5
5
  } from './types/eventData';
6
+ import botDetectionRelayService from '@/services/botDetectionRelay';
6
7
  import fingerprintService from '@/services/fingerprint';
7
8
  import geolocationService from '@/services/geolocation';
8
9
  import mixpanelService from '@/services/mixpanel';
@@ -117,12 +118,21 @@ export async function trackEvent(
117
118
 
118
119
  try {
119
120
  validator.parseEventProps(eventName, eventProperties);
120
- const cacheOnly = !FP_TRIGGER_EVENTS.includes(eventName);
121
-
122
121
  // Call the Mixpanel service to store the distinct ID in the cache
123
- mixpanelService.getMixpanelDistinctId();
124
-
125
- const fingerprint = await fingerprintService.getFingerprint(cacheOnly);
122
+ const distinctId = mixpanelService.getMixpanelDistinctId();
123
+ const botDetectionEnabled =
124
+ botDetectionRelayService.isEnabledFor(eventName);
125
+ let fingerprint: string;
126
+ let fingerprintRequestId: string | undefined;
127
+
128
+ if (botDetectionEnabled && fingerprintService.isFingerprintReady()) {
129
+ const identification = await fingerprintService.identifyVisitor(true);
130
+ fingerprint = identification.visitorId;
131
+ fingerprintRequestId = identification.requestId;
132
+ } else {
133
+ const cacheOnly = !FP_TRIGGER_EVENTS.includes(eventName);
134
+ fingerprint = await fingerprintService.getFingerprint(cacheOnly);
135
+ }
126
136
  const defaultCoordinates: Record<string, string | number> = {};
127
137
  const coordinates = geolocationService.getCoordinates();
128
138
  const geolocationSource = geolocationService.getGeolocationSource();
@@ -148,6 +158,15 @@ export async function trackEvent(
148
158
  };
149
159
 
150
160
  mixpanelService.track(eventName, properties);
161
+
162
+ if (fingerprintRequestId) {
163
+ void botDetectionRelayService.relay({
164
+ requestId: fingerprintRequestId,
165
+ visitorId: fingerprint,
166
+ ...(distinctId && { distinctId }),
167
+ eventName,
168
+ });
169
+ }
151
170
  } catch (error) {
152
171
  console.error(`Error tracking event '${eventName}':`, error);
153
172
  trackEventError(eventName, error, eventProperties);
@@ -0,0 +1,16 @@
1
+ interface FingerprintIdentification {
2
+ visitorId: string;
3
+ requestId: string;
4
+ visitorFound?: boolean;
5
+ confidenceScore: number;
6
+ cacheHit: boolean;
7
+ }
8
+
9
+ interface BotDetectionRelayPayload {
10
+ requestId: string;
11
+ visitorId: string;
12
+ distinctId?: string;
13
+ eventName: string;
14
+ }
15
+
16
+ export type { BotDetectionRelayPayload, FingerprintIdentification };