@reservamos/browser-analytics 1.1.6 → 1.1.7-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.
@@ -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 {
@@ -19,6 +22,13 @@ const DEFAULT_CACHE_TIME_IN_DAYS = 7;
19
22
  const PROXY_QUERY_PARAM =
20
23
  'apiKey=<apiKey>&version=<version>&loaderVersion=<loaderVersion>';
21
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
+ */
30
+ let fingerprintAgentReady = false;
31
+
22
32
  /**
23
33
  * Calculates the expiration date based on the cache time.
24
34
  * @param {number} cacheTimeInDays - The cache time in days.
@@ -37,6 +47,10 @@ const getExpirationDate = (cacheTimeInDays: number): number => {
37
47
  * @returns {string | null} - The cached fingerprint or null if it is expired or not found.
38
48
  */
39
49
  const getCachedFingerprint = (): string | null => {
50
+ return readCachedFingerprint();
51
+ };
52
+
53
+ function readCachedFingerprint(context?: IdentifyStageContext): string | null {
40
54
  const { cacheName = DEFAULT_CACHE_NAME } = window.fingerprintConfig;
41
55
 
42
56
  try {
@@ -47,9 +61,10 @@ const getCachedFingerprint = (): string | null => {
47
61
  localStorage.removeItem(cacheName);
48
62
  return null;
49
63
  } catch {
64
+ context?.report('cache', 'unavailable');
50
65
  return null;
51
66
  }
52
- };
67
+ }
53
68
 
54
69
  /**
55
70
  * Sets the cached fingerprint in local storage.
@@ -102,18 +117,26 @@ async function initFingerprint(
102
117
  customScriptEndpointArray.unshift(buildProxyUrl(proxyUrl));
103
118
  }
104
119
 
120
+ fingerprintAgentReady = false;
105
121
  window.fingerprintConfig = {};
106
- window.fpClient = new FpjsClient({
122
+ const client = new FpjsClient({
107
123
  loadOptions: {
108
124
  apiKey,
109
125
  endpoint: customEndpointArray,
110
126
  scriptUrlPattern: customScriptEndpointArray,
111
127
  },
112
128
  });
129
+ window.fpClient = client;
113
130
 
114
131
  try {
115
- await window.fpClient.init();
132
+ await client.init();
133
+ if (window.fpClient === client) {
134
+ fingerprintAgentReady = true;
135
+ }
116
136
  } catch (error) {
137
+ if (window.fpClient === client) {
138
+ fingerprintAgentReady = false;
139
+ }
117
140
  console.error('Error initializing identification service:', error);
118
141
  throw error;
119
142
  }
@@ -121,11 +144,26 @@ async function initFingerprint(
121
144
 
122
145
  /**
123
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.
124
148
  * @param {boolean} cacheOnly - Optional flag to retrieve the fingerprint only from the cache.
125
- * @returns {Promise<string>} A promise that resolves with the fingerprint.
126
- * @throws {Error} Throws an error if retrieval fails.
149
+ * @returns {Promise<string>} A promise that resolves with the fingerprint, or an empty string when unavailable.
150
+ * @throws {Error} Throws an error if retrieval fails after the agent has loaded.
127
151
  */
128
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> {
129
167
  if (!window.fingerprintConfig) {
130
168
  if (cacheOnly) {
131
169
  return '';
@@ -134,44 +172,87 @@ async function getFingerprint(cacheOnly = false): Promise<string> {
134
172
  throw new Error('Fingerprint configuration is not initialized.');
135
173
  }
136
174
 
137
- const cachedFingerprint = getCachedFingerprint();
175
+ const cachedFingerprint = readCachedFingerprint(context);
138
176
  if (cachedFingerprint) {
139
177
  return cachedFingerprint;
140
178
  }
141
179
 
142
- if (cacheOnly) {
180
+ if (cacheOnly || !isFingerprintReady()) {
143
181
  return '';
144
182
  }
145
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> {
146
204
  if (!isFingerprintReady()) {
147
205
  throw new Error('Identification service is not initialized.');
148
206
  }
149
207
 
150
208
  try {
151
- const fpClient = window.fpClient;
152
- const { visitorId: fingerprint } = await fpClient.getVisitorData();
153
- setCachedFingerprint(fingerprint);
154
- 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
+ };
155
238
  } catch (error) {
156
- console.error('Error retrieving fingerprint:', error);
239
+ if (!context) console.error('Error retrieving fingerprint:', error);
157
240
  throw error;
158
241
  }
159
242
  }
160
243
 
161
244
  /**
162
- * Checks if the identification service is ready.
163
- *
164
- * This function verifies whether the identification service has been initialized
165
- * and is ready to use by checking if the `fpClient` instance exists on the window object.
166
- * @returns {boolean} Returns true if the identification service is ready, otherwise false.
245
+ * Checks if the Fingerprint agent finished loading and can identify visitors.
246
+ * @returns {boolean} True only after `initFingerprint()` has successfully loaded the agent.
167
247
  */
168
248
  function isFingerprintReady(): boolean {
169
- return window.fpClient !== undefined;
249
+ return window.fpClient !== undefined && fingerprintAgentReady;
170
250
  }
171
251
 
172
252
  const fingerprintService = {
173
253
  initFingerprint,
174
254
  getFingerprint,
255
+ identifyVisitor,
175
256
  isFingerprintReady,
176
257
  getCachedFingerprint,
177
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 };