@reservamos/browser-analytics 1.1.8-alpha.1 → 1.1.8-alpha.3

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/README.md +32 -4
  3. package/dist/browser-analytics.cjs +2 -2
  4. package/dist/browser-analytics.cjs.map +1 -1
  5. package/dist/browser-analytics.d.ts +23 -10
  6. package/dist/browser-analytics.esm.js +466 -154
  7. package/dist/browser-analytics.esm.js.map +1 -1
  8. package/dist/browser-analytics.iife.js +2 -2
  9. package/dist/browser-analytics.iife.js.map +1 -1
  10. package/package.json +2 -2
  11. package/src/events/identify/identify.recovery.test.ts +367 -0
  12. package/src/events/identify/identify.robustness.test.ts +112 -13
  13. package/src/events/identify/identify.ts +72 -35
  14. package/src/events/passengersCreated/passengersCreatedSchema.test.ts +142 -0
  15. package/src/events/passengersCreated/passengersCreatedSchema.ts +7 -1
  16. package/src/events/purchaseConfirmation/purchaseConfirmation.integration.test.ts +49 -0
  17. package/src/events/purchaseConfirmation/purchaseConfirmation.test.ts +17 -0
  18. package/src/events/purchaseConfirmation/purchaseConfirmationSchema.ts +2 -0
  19. package/src/index.ts +1 -0
  20. package/src/init.ts +10 -1
  21. package/src/profiles/createAnonymousProfile/createAnonymousProfile.ts +18 -2
  22. package/src/services/botDetectionRelay.test.ts +3 -2
  23. package/src/services/fingerprint.test.ts +172 -0
  24. package/src/services/fingerprint.ts +106 -5
  25. package/src/services/fingerprint.wait.integration.test.ts +129 -0
  26. package/src/services/mixpanel.batch.test.ts +108 -0
  27. package/src/services/mixpanel.test.ts +77 -73
  28. package/src/services/mixpanel.ts +45 -18
  29. package/src/services/profileEnrichment.ts +258 -0
  30. package/src/track.storage.test.ts +27 -12
@@ -30,7 +30,16 @@ const PROXY_QUERY_PARAM =
30
30
  */
31
31
  let fingerprintAgentReady = false;
32
32
  let initialization = 0;
33
+ let startedInitialization = -1;
33
34
  const readinessSubscribers = new Set<() => void>();
35
+ const waitingConsumers = new Set<() => void>();
36
+ let pendingIdentification:
37
+ | {
38
+ promise: Promise<FingerprintIdentification>;
39
+ contexts: Set<IdentifyStageContext | undefined>;
40
+ isCurrent: () => boolean;
41
+ }
42
+ | undefined;
34
43
 
35
44
  /** Notifies deferred observations only when the actual provider agent has loaded. */
36
45
  function onReady(notify: () => void): () => void {
@@ -44,6 +53,8 @@ function onReady(notify: () => void): () => void {
44
53
  function invalidate(): void {
45
54
  initialization++;
46
55
  fingerprintAgentReady = false;
56
+ pendingIdentification = undefined;
57
+ waitingConsumers.forEach((cancel) => cancel());
47
58
  }
48
59
 
49
60
  /**
@@ -133,7 +144,8 @@ async function initFingerprint(
133
144
  customScriptEndpointArray.unshift(buildProxyUrl(proxyUrl));
134
145
  }
135
146
 
136
- invalidate();
147
+ if (startedInitialization === initialization) invalidate();
148
+ startedInitialization = initialization;
137
149
  const attempt = initialization;
138
150
  window.fingerprintConfig = {};
139
151
  const client = new FpjsClient({
@@ -154,6 +166,7 @@ async function initFingerprint(
154
166
  } catch (error) {
155
167
  if (window.fpClient === client && attempt === initialization) {
156
168
  fingerprintAgentReady = false;
169
+ waitingConsumers.forEach((cancel) => cancel());
157
170
  }
158
171
  console.error('Error initializing identification service:', error);
159
172
  throw error;
@@ -214,6 +227,50 @@ async function identifyVisitor(
214
227
  async function requestIdentification(
215
228
  ignoreCache: boolean,
216
229
  context?: IdentifyStageContext,
230
+ ): Promise<FingerprintIdentification> {
231
+ if (!ignoreCache && pendingIdentification?.isCurrent()) {
232
+ for (const participant of pendingIdentification.contexts) {
233
+ if (participant && !participant.isActive())
234
+ pendingIdentification.contexts.delete(participant);
235
+ }
236
+ pendingIdentification.contexts.add(context);
237
+ return pendingIdentification.promise;
238
+ }
239
+ const contexts = new Set([context]);
240
+ const attempt = initialization;
241
+ const currentLifecycle = trackingLifecycle.capture();
242
+ const isCurrent = () => attempt === initialization && currentLifecycle();
243
+ const sharedContext: IdentifyStageContext = {
244
+ isActive: () =>
245
+ isCurrent() &&
246
+ [...contexts].some(
247
+ (participant) => !participant || participant.isActive(),
248
+ ),
249
+ report: (...args) => {
250
+ for (const participant of contexts) {
251
+ if (participant?.isActive()) participant.report(...args);
252
+ }
253
+ },
254
+ };
255
+ const request = {
256
+ contexts,
257
+ isCurrent,
258
+ promise: performIdentification(ignoreCache, sharedContext, () =>
259
+ contexts.has(undefined),
260
+ ),
261
+ };
262
+ pendingIdentification = request;
263
+ const clear = () => {
264
+ if (pendingIdentification === request) pendingIdentification = undefined;
265
+ };
266
+ void request.promise.then(clear, clear);
267
+ return request.promise;
268
+ }
269
+
270
+ async function performIdentification(
271
+ ignoreCache: boolean,
272
+ context: IdentifyStageContext,
273
+ hasUnscopedConsumer: () => boolean,
217
274
  ): Promise<FingerprintIdentification> {
218
275
  if (!isFingerprintReady()) {
219
276
  throw new Error('Identification service is not initialized.');
@@ -221,7 +278,7 @@ async function requestIdentification(
221
278
 
222
279
  try {
223
280
  const result = await window.fpClient.getVisitorData({}, ignoreCache);
224
- if (!context || context.isActive()) {
281
+ if (context.isActive()) {
225
282
  try {
226
283
  setCachedFingerprint(result.visitorId);
227
284
  } catch {
@@ -233,8 +290,9 @@ async function requestIdentification(
233
290
  'Visitor Found': result.visitorFound,
234
291
  });
235
292
  } catch (error) {
236
- if (context) context.report('fingerprint', 'failed');
237
- else console.error('Error updating Mixpanel visitor profile:', error);
293
+ context.report('fingerprint', 'failed');
294
+ if (hasUnscopedConsumer())
295
+ console.error('Error updating Mixpanel visitor profile:', error);
238
296
  }
239
297
  }
240
298
  }
@@ -249,11 +307,53 @@ async function requestIdentification(
249
307
  cacheHit: result.cacheHit,
250
308
  };
251
309
  } catch (error) {
252
- if (!context) console.error('Error retrieving fingerprint:', error);
310
+ if (hasUnscopedConsumer())
311
+ console.error('Error retrieving fingerprint:', error);
253
312
  throw error;
254
313
  }
255
314
  }
256
315
 
316
+ /**
317
+ * Waits for cached or in-flight identity without forcing a fresh provider request.
318
+ * @param {number} timeoutMs - Maximum wait in milliseconds; defaults to 2000.
319
+ * @returns {Promise<string | null>} Visitor ID, or null when unavailable within the budget.
320
+ */
321
+ function waitForFingerprintId(timeoutMs = 2000): Promise<string | null> {
322
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 2147483647)
323
+ return Promise.resolve(null);
324
+ const cached = getCachedFingerprint();
325
+ if (cached) return Promise.resolve(cached);
326
+ return new Promise((resolve) => {
327
+ let active = true;
328
+ let started = false;
329
+ const finish = (visitorId: string | null) => {
330
+ if (!active) return;
331
+ active = false;
332
+ clearTimeout(timer);
333
+ stopReady();
334
+ stopLifecycle();
335
+ waitingConsumers.delete(cancel);
336
+ resolve(visitorId);
337
+ };
338
+ const cancel = () => finish(null);
339
+ const start = () => {
340
+ if (!active || started || !isFingerprintReady()) return;
341
+ started = true;
342
+ const identity = getFingerprint(false, {
343
+ // Caller timeout ends waiting; the shared request still checks its lifecycle.
344
+ isActive: () => true,
345
+ report: () => undefined,
346
+ });
347
+ void identity.then((visitorId) => finish(visitorId || null), cancel);
348
+ };
349
+ const timer = setTimeout(cancel, timeoutMs);
350
+ const stopReady = onReady(start);
351
+ const stopLifecycle = trackingLifecycle.onChange(cancel);
352
+ waitingConsumers.add(cancel);
353
+ start();
354
+ });
355
+ }
356
+
257
357
  /**
258
358
  * Checks if the Fingerprint agent finished loading and can identify visitors.
259
359
  * @returns {boolean} True only after `initFingerprint()` has successfully loaded the agent.
@@ -270,6 +370,7 @@ const fingerprintService = {
270
370
  onReady,
271
371
  invalidate,
272
372
  getCachedFingerprint,
373
+ waitForFingerprintId,
273
374
  };
274
375
 
275
376
  // One module-lifetime listener invalidates the old agent before Mixpanel signals readiness.
@@ -0,0 +1,129 @@
1
+ import { afterEach, beforeEach, expect, it, vi } from 'vitest';
2
+ import identify from '@/events/identify';
3
+ import init from '@/init';
4
+ import fingerprintService from '@/services/fingerprint';
5
+ import mixpanelService from '@/services/mixpanel';
6
+ import trackingLifecycle from '@/services/trackingLifecycle';
7
+ import { trackEvent } from '@/track';
8
+
9
+ const mocks = vi.hoisted(() => ({ load: vi.fn(), get: vi.fn() }));
10
+ const visitor = {
11
+ visitorId: 'shared-visitor',
12
+ requestId: 'request',
13
+ visitorFound: true,
14
+ confidence: { score: 1 },
15
+ cacheHit: false,
16
+ };
17
+ vi.mock('@fingerprintjs/fingerprintjs-pro-spa', () => ({
18
+ FingerprintJSPro: {
19
+ defaultEndpoint: 'https://fp.test',
20
+ defaultScriptUrlPattern: 'https://fp.test/agent',
21
+ },
22
+ FpjsClient: vi.fn(() => ({ init: mocks.load, getVisitorData: mocks.get })),
23
+ }));
24
+ vi.mock('@reservamos/js-api-client/core', () => ({
25
+ default: {
26
+ setConfig: vi.fn(),
27
+ profiles: { createAnonymousProfile: vi.fn().mockResolvedValue(undefined) },
28
+ },
29
+ }));
30
+ vi.mock('@/services/config', () => ({
31
+ default: {
32
+ setEnvironment: vi.fn(),
33
+ setAppName: vi.fn(),
34
+ getCoreAPIConfig: vi
35
+ .fn()
36
+ .mockReturnValue({ baseUrl: 'https://core.test', defaultHeaders: {} }),
37
+ },
38
+ }));
39
+ vi.mock('@/services/geolocation', () => ({
40
+ default: {
41
+ init: vi.fn().mockResolvedValue(undefined),
42
+ getCoordinates: vi.fn().mockReturnValue(null),
43
+ getGeolocationSource: vi.fn().mockReturnValue(null),
44
+ },
45
+ }));
46
+ vi.mock('@/services/mixpanel', () => ({
47
+ default: {
48
+ validateConfiguration: vi.fn(),
49
+ init: vi.fn(async () => {
50
+ trackingLifecycle.beginInitialization();
51
+ }),
52
+ isReady: vi.fn().mockReturnValue(true),
53
+ identify: vi.fn((user: string) => trackingLifecycle.identify(user)),
54
+ setPeopleProperties: vi.fn(),
55
+ attachProperty: vi.fn(),
56
+ getMixpanelDistinctId: vi.fn().mockReturnValue('user'),
57
+ track: vi.fn(),
58
+ },
59
+ }));
60
+
61
+ beforeEach(() => {
62
+ vi.useFakeTimers();
63
+ vi.clearAllMocks();
64
+ localStorage.clear();
65
+ mocks.load.mockResolvedValue(undefined);
66
+ mocks.get.mockResolvedValue(visitor);
67
+ });
68
+ afterEach(() => {
69
+ vi.clearAllTimers();
70
+ vi.useRealTimers();
71
+ localStorage.clear();
72
+ });
73
+
74
+ it('preserves a waiter registered by Tracker Ready during that same initialization', async () => {
75
+ let loaded!: () => void;
76
+ mocks.load.mockReturnValue(
77
+ new Promise<void>((resolve) => {
78
+ loaded = resolve;
79
+ }),
80
+ );
81
+ let waiting: Promise<string | null> | undefined;
82
+ let settled = false;
83
+ window.addEventListener(
84
+ 'Tracker Ready',
85
+ () => {
86
+ waiting = fingerprintService.waitForFingerprintId(1000).then((value) => {
87
+ settled = true;
88
+ return value;
89
+ });
90
+ },
91
+ { once: true },
92
+ );
93
+ const initialization = init({
94
+ mixpanelToken: 'test',
95
+ identificationKey: 'test',
96
+ });
97
+ await vi.advanceTimersByTimeAsync(0);
98
+ expect(waiting).toBeDefined();
99
+ expect(settled).toBe(false);
100
+ loaded();
101
+ await initialization;
102
+ await expect(waiting).resolves.toBe('shared-visitor');
103
+ expect(mocks.get).toHaveBeenCalledTimes(1);
104
+ });
105
+
106
+ it('keeps cache and profile effects for a current Identify joining a superseded request', async () => {
107
+ await init({ mixpanelToken: 'test', identificationKey: 'test' });
108
+ let finish!: (value: typeof visitor) => void;
109
+ mocks.get.mockReturnValue(
110
+ new Promise((resolve) => {
111
+ finish = resolve;
112
+ }),
113
+ );
114
+ await identify('user', { email: 'test@example.com' });
115
+ await identify('user', { email: 'test@example.com' });
116
+ finish(visitor);
117
+ await vi.advanceTimersByTimeAsync(0);
118
+ expect(mocks.get).toHaveBeenCalledTimes(1);
119
+ expect(fingerprintService.getCachedFingerprint()).toBe('shared-visitor');
120
+ expect(mixpanelService.setPeopleProperties).toHaveBeenCalledWith({
121
+ 'Visitor Found': true,
122
+ });
123
+ expect(mixpanelService.attachProperty).toHaveBeenCalledTimes(1);
124
+ await trackEvent('Cache Check', { step: 'after identify' });
125
+ expect(mixpanelService.track).toHaveBeenCalledWith(
126
+ 'Cache Check',
127
+ expect.objectContaining({ 'User Fingerprint': 'shared-visitor' }),
128
+ );
129
+ });
@@ -0,0 +1,108 @@
1
+ import mixpanel from 'mixpanel-browser';
2
+ import { afterAll, beforeAll, expect, it, vi } from 'vitest';
3
+ import mixpanelService from './mixpanel';
4
+
5
+ interface BatchPayload {
6
+ event?: string;
7
+ properties?: Record<string, unknown>;
8
+ $distinct_id?: string;
9
+ $set?: Record<string, unknown>;
10
+ }
11
+
12
+ // Keep the real identity handling, batching, storage, and timers; replace HTTP only.
13
+ const transport = mixpanel as unknown as {
14
+ _send_request(
15
+ url: string,
16
+ data: { data: string },
17
+ options: object,
18
+ callback?: (response: number) => void,
19
+ ): boolean;
20
+ stop_batch_senders(): void;
21
+ _flags: { identify_called: boolean };
22
+ };
23
+ const batches: { url: string; payload: BatchPayload[] }[] = [];
24
+
25
+ beforeAll(async () => {
26
+ vi.useFakeTimers();
27
+ localStorage.clear();
28
+ await mixpanelService.init('one-second-batching-token');
29
+ vi.spyOn(transport, '_send_request').mockImplementation(
30
+ (url, data, _options, callback) => {
31
+ if (url.endsWith('/track/') || url.endsWith('/engage/')) {
32
+ batches.push({
33
+ url,
34
+ payload: JSON.parse(data.data) as BatchPayload[],
35
+ });
36
+ }
37
+ callback?.(1);
38
+ return true;
39
+ },
40
+ );
41
+ });
42
+
43
+ afterAll(() => {
44
+ transport.stop_batch_senders();
45
+ vi.restoreAllMocks();
46
+ vi.clearAllTimers();
47
+ vi.useRealTimers();
48
+ localStorage.clear();
49
+ });
50
+
51
+ it('batches native identity and people updates within one second', async () => {
52
+ mixpanelService.identify('redirect-user', { $email: 'redirect@example.com' });
53
+
54
+ await vi.advanceTimersByTimeAsync(999);
55
+ expect(batches).toEqual([]);
56
+
57
+ await vi.advanceTimersByTimeAsync(1);
58
+ expect(batches).toHaveLength(2);
59
+ expect(batches.find(({ url }) => url.endsWith('/track/'))?.payload).toEqual([
60
+ expect.objectContaining({
61
+ event: '$identify',
62
+ properties: expect.objectContaining({ distinct_id: 'redirect-user' }),
63
+ }),
64
+ ]);
65
+ expect(
66
+ batches.find(({ url }) => url.endsWith('/engage/'))?.payload,
67
+ ).toContainEqual(
68
+ expect.objectContaining({
69
+ $distinct_id: 'redirect-user',
70
+ $set: expect.objectContaining({ $email: 'redirect@example.com' }),
71
+ }),
72
+ );
73
+ });
74
+
75
+ it('restores people readiness for the unchanged user without another native identity event', async () => {
76
+ mixpanelService.identify('restored-user', {});
77
+ await vi.advanceTimersByTimeAsync(1000);
78
+ batches.length = 0;
79
+ transport._flags.identify_called = false;
80
+ const accepted = vi.fn();
81
+ const report = vi.fn();
82
+
83
+ mixpanelService.restorePeopleProperties(
84
+ 'other-user',
85
+ { reservamos_one_id: 'wrong' },
86
+ report,
87
+ accepted,
88
+ );
89
+ expect(accepted).not.toHaveBeenCalled();
90
+
91
+ mixpanelService.restorePeopleProperties(
92
+ 'restored-user',
93
+ { reservamos_one_id: 42 },
94
+ report,
95
+ accepted,
96
+ );
97
+ expect(accepted).toHaveBeenCalledTimes(1);
98
+ await vi.advanceTimersByTimeAsync(1000);
99
+ expect(batches).toHaveLength(1);
100
+ expect(batches[0].url).toContain('/engage/');
101
+ expect(batches[0].payload).toContainEqual(
102
+ expect.objectContaining({
103
+ $distinct_id: 'restored-user',
104
+ $set: expect.objectContaining({ reservamos_one_id: 42 }),
105
+ }),
106
+ );
107
+ expect(report).not.toHaveBeenCalled();
108
+ });
@@ -1,5 +1,4 @@
1
1
  import mixpanel from 'mixpanel-browser';
2
- import { version as mixpanelVersion } from 'mixpanel-browser/package.json';
3
2
  import {
4
3
  afterAll,
5
4
  afterEach,
@@ -13,23 +12,61 @@ import { trackEvent } from '@/track';
13
12
  import { version } from '../../package.json';
14
13
  import mixpanelService from './mixpanel';
15
14
 
16
- interface TransportData {
17
- event?: string;
18
- properties?: Record<string, unknown>;
19
- }
15
+ const { provider } = vi.hoisted(() => ({
16
+ provider: {
17
+ init: vi.fn((_token: string, config: { loaded: () => void }) =>
18
+ config.loaded(),
19
+ ),
20
+ register: vi.fn(),
21
+ set_config: vi.fn(),
22
+ identify: vi.fn(),
23
+ track: vi.fn(),
24
+ get_distinct_id: vi.fn(() => 'live-id'),
25
+ people: { set: vi.fn(), union: vi.fn() },
26
+ },
27
+ }));
20
28
 
21
- // Intercept transport only: identify, track, register, and property merging stay real.
22
- const transport = mixpanel as unknown as {
23
- _track_or_batch(options: { data: TransportData }): TransportData;
24
- stop_batch_senders(): void;
25
- };
29
+ // Exercise SDK service methods while replacing the vendor before initialization.
30
+ vi.mock('mixpanel-browser', () => ({ default: provider }));
31
+
32
+ describe('distinct ID lookup before initialization', () => {
33
+ afterEach(() => {
34
+ vi.restoreAllMocks();
35
+ localStorage.clear();
36
+ });
37
+
38
+ it.each(['cached', 'empty', 'blocked'])(
39
+ 'uses the %s cache without calling the uninitialized provider',
40
+ (mode) => {
41
+ const previous = window.mixpanel;
42
+ Reflect.deleteProperty(window, 'mixpanel');
43
+ const lookup = vi
44
+ .spyOn(mixpanel, 'get_distinct_id')
45
+ .mockImplementation(() => {
46
+ throw new Error('Provider persistence is not initialized');
47
+ });
48
+ if (mode === 'cached')
49
+ localStorage.setItem('mp_distinct_id', 'cached-id');
50
+ if (mode === 'blocked')
51
+ vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
52
+ throw new DOMException('Blocked', 'SecurityError');
53
+ });
54
+ try {
55
+ expect(mixpanelService.getMixpanelDistinctId()).toBe(
56
+ mode === 'cached' ? 'cached-id' : null,
57
+ );
58
+ expect(lookup).not.toHaveBeenCalled();
59
+ } finally {
60
+ if (previous) window.mixpanel = previous;
61
+ }
62
+ },
63
+ );
64
+ });
26
65
 
27
66
  describe('Mixpanel SDK version metadata', () => {
28
67
  beforeAll(async () => {
29
68
  localStorage.clear();
30
69
  await mixpanelService.init('test-sdk-version-token');
31
- transport.stop_batch_senders();
32
- mixpanel.set_config({ disable_persistence: true });
33
70
  });
34
71
 
35
72
  afterEach(() => {
@@ -37,54 +74,30 @@ describe('Mixpanel SDK version metadata', () => {
37
74
  vi.unstubAllGlobals();
38
75
  vi.clearAllTimers();
39
76
  vi.useRealTimers();
77
+ vi.clearAllMocks();
40
78
  });
41
79
 
42
80
  afterAll(() => {
43
- transport.stop_batch_senders();
44
81
  localStorage.clear();
45
82
  });
46
83
 
47
- it('registers SDK metadata and propagates it to the native $identify event', () => {
48
- expect(mixpanel.get_property('Event Source')).toBe('sdk');
49
- expect(mixpanel.get_property('Reservamos Version')).toBe(version);
50
-
51
- const send = vi
52
- .spyOn(transport, '_track_or_batch')
53
- .mockImplementation(({ data }) => data);
54
-
55
- mixpanelService.identify('sdk-version-user', {});
56
- const events = send.mock.calls
57
- .map(([options]) => options.data)
58
- .filter((data) => data.event);
59
-
60
- expect(events).toHaveLength(1);
61
- expect(events[0]).toEqual({
62
- event: '$identify',
63
- properties: expect.objectContaining({
64
- 'Event Source': 'sdk',
65
- 'Reservamos Version': version,
66
- '$lib_version': mixpanelVersion,
67
- }),
84
+ it('registers SDK metadata before identifying at the provider boundary', () => {
85
+ expect(mixpanel.register).toHaveBeenCalledWith({
86
+ 'Event Source': 'sdk',
87
+ 'Reservamos Version': version,
68
88
  });
69
-
70
- send.mockClear();
89
+ expect(mixpanel.register).not.toHaveBeenCalledWith(
90
+ expect.objectContaining({ $lib_version: expect.anything() }),
91
+ );
71
92
  mixpanelService.identify('sdk-version-user', {});
72
- expect(send.mock.calls.some(([options]) => options.data.event)).toBe(false);
93
+ expect(mixpanel.identify).toHaveBeenCalledWith('sdk-version-user');
94
+ expect(mixpanel.people.set).toHaveBeenCalledWith({});
73
95
  });
74
96
 
75
97
  it('keeps the SDK version authoritative for SDK-tracked events', () => {
76
- const send = vi
77
- .spyOn(transport, '_track_or_batch')
78
- .mockImplementation(({ data }) => data);
79
-
80
98
  mixpanelService.track('Test', { 'Reservamos Version': 'caller-version' });
81
-
82
- expect(send.mock.calls[0][0].data).toEqual({
83
- event: 'Test',
84
- properties: expect.objectContaining({
85
- 'Event Source': 'sdk',
86
- 'Reservamos Version': version,
87
- }),
99
+ expect(mixpanel.track).toHaveBeenCalledWith('Test', {
100
+ 'Reservamos Version': version,
88
101
  });
89
102
  });
90
103
 
@@ -104,17 +117,12 @@ describe('Mixpanel SDK version metadata', () => {
104
117
  });
105
118
 
106
119
  it('still sets people properties when the SDK distinct ID cache is blocked', () => {
107
- vi.spyOn(transport, '_track_or_batch').mockImplementation(
108
- ({ data }) => data,
109
- );
110
120
  vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
111
121
  throw new DOMException('Blocked', 'SecurityError');
112
122
  });
113
- const people = vi
114
- .spyOn(mixpanel.people, 'set')
115
- .mockImplementation(() => undefined);
116
123
  mixpanelService.identify('cache-blocked-user', {});
117
- expect(people).toHaveBeenCalledWith({});
124
+ expect(mixpanel.identify).toHaveBeenCalledWith('cache-blocked-user');
125
+ expect(mixpanel.people.set).toHaveBeenCalledWith({});
118
126
  });
119
127
 
120
128
  it.each(['getItem', 'setItem'] as const)(
@@ -131,31 +139,33 @@ describe('Mixpanel SDK version metadata', () => {
131
139
  it.each(['getItem', 'setItem'] as const)(
132
140
  'submits the original event when SDK storage %s is blocked',
133
141
  async (method) => {
134
- const send = vi
135
- .spyOn(transport, '_track_or_batch')
136
- .mockImplementation(({ data }) => data);
137
- mixpanel.identify('storage-test-id');
138
- send.mockClear();
142
+ vi.spyOn(mixpanel, 'get_distinct_id').mockReturnValue('storage-test-id');
139
143
  vi.spyOn(Storage.prototype, method).mockImplementation(() => {
140
144
  throw new DOMException('Blocked', 'SecurityError');
141
145
  });
142
146
  await trackEvent('Storage Failure Test', { source: 'checkout' });
143
- expect(send.mock.calls.map(([options]) => options.data)).toContainEqual({
144
- event: 'Storage Failure Test',
145
- properties: expect.objectContaining({
146
- source: 'checkout',
147
- distinct_id: 'storage-test-id',
147
+ expect(mixpanel.track).toHaveBeenCalledWith(
148
+ 'Storage Failure Test',
149
+ expect.objectContaining({
150
+ 'source': 'checkout',
151
+ 'Reservamos Version': version,
148
152
  }),
149
- });
153
+ );
154
+ expect(mixpanelService.getMixpanelDistinctId()).toBe('storage-test-id');
150
155
  },
151
156
  );
152
157
 
153
- it('returns null when no live distinct ID is available and the cache is blocked', () => {
158
+ it('submits the original event when no live distinct ID is available and the cache is blocked', async () => {
154
159
  vi.spyOn(mixpanel, 'get_distinct_id').mockReturnValue('');
155
160
  vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
156
161
  throw new DOMException('Blocked', 'SecurityError');
157
162
  });
158
163
  expect(mixpanelService.getMixpanelDistinctId()).toBeNull();
164
+ await trackEvent('Storage Failure Test', { source: 'checkout' });
165
+ expect(mixpanel.track).toHaveBeenCalledWith(
166
+ 'Storage Failure Test',
167
+ expect.objectContaining({ source: 'checkout' }),
168
+ );
159
169
  });
160
170
 
161
171
  it('uses the live distinct ID first and null fallback when localStorage is null', () => {
@@ -214,9 +224,6 @@ describe('Mixpanel SDK version metadata', () => {
214
224
  (response) => {
215
225
  vi.useFakeTimers();
216
226
  vi.spyOn(mixpanel, 'identify').mockImplementation(() => undefined);
217
- vi.spyOn(transport, '_track_or_batch').mockImplementation(
218
- ({ data }) => data,
219
- );
220
227
  let callback: ((response: unknown) => void) | undefined;
221
228
  vi.spyOn(mixpanel.people, 'set').mockImplementation((...args) => {
222
229
  callback = args[1] as (response: unknown) => void;
@@ -245,9 +252,6 @@ describe('Mixpanel SDK version metadata', () => {
245
252
  it('reports missing people callback as unobserved and ignores a late result', () => {
246
253
  vi.useFakeTimers();
247
254
  vi.spyOn(mixpanel, 'identify').mockImplementation(() => undefined);
248
- vi.spyOn(transport, '_track_or_batch').mockImplementation(
249
- ({ data }) => data,
250
- );
251
255
  let callback: ((response: unknown) => void) | undefined;
252
256
  vi.spyOn(mixpanel.people, 'set').mockImplementation((...args) => {
253
257
  callback = args[1] as (response: unknown) => void;