@reservamos/browser-analytics 1.1.8-alpha.1 → 1.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -2
- package/README.md +25 -3
- package/dist/browser-analytics.cjs +2 -2
- package/dist/browser-analytics.cjs.map +1 -1
- package/dist/browser-analytics.d.ts +15 -2
- package/dist/browser-analytics.esm.js +176 -54
- package/dist/browser-analytics.esm.js.map +1 -1
- package/dist/browser-analytics.iife.js +2 -2
- package/dist/browser-analytics.iife.js.map +1 -1
- package/package.json +2 -2
- package/src/events/identify/identify.robustness.test.ts +112 -13
- package/src/events/identify/identify.ts +84 -34
- package/src/events/passengersCreated/passengersCreatedSchema.test.ts +142 -0
- package/src/events/passengersCreated/passengersCreatedSchema.ts +7 -1
- package/src/events/purchaseConfirmation/purchaseConfirmation.integration.test.ts +49 -0
- package/src/events/purchaseConfirmation/purchaseConfirmation.test.ts +17 -0
- package/src/events/purchaseConfirmation/purchaseConfirmationSchema.ts +2 -0
- package/src/index.ts +1 -0
- package/src/services/botDetectionRelay.test.ts +3 -2
- package/src/services/fingerprint.test.ts +172 -0
- package/src/services/fingerprint.ts +106 -5
- package/src/services/fingerprint.wait.integration.test.ts +129 -0
- package/src/services/mixpanel.test.ts +77 -73
- package/src/services/mixpanel.ts +28 -18
- package/src/track.storage.test.ts +27 -12
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
48
|
-
expect(mixpanel.
|
|
49
|
-
|
|
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
|
-
|
|
89
|
+
expect(mixpanel.register).not.toHaveBeenCalledWith(
|
|
90
|
+
expect.objectContaining({ $lib_version: expect.anything() }),
|
|
91
|
+
);
|
|
71
92
|
mixpanelService.identify('sdk-version-user', {});
|
|
72
|
-
expect(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
source: 'checkout',
|
|
147
|
-
|
|
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('
|
|
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;
|
package/src/services/mixpanel.ts
CHANGED
|
@@ -105,21 +105,7 @@ function identify(
|
|
|
105
105
|
trackingLifecycle.identify(userId);
|
|
106
106
|
mixpanel.identify(userId);
|
|
107
107
|
if (!updateCachedDistinctId(userId)) report?.('cache', 'unavailable');
|
|
108
|
-
|
|
109
|
-
mixpanel.people.set(properties);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const observer = observeIdentifyResult('people', report);
|
|
113
|
-
try {
|
|
114
|
-
mixpanel.people.set(properties, (response) => {
|
|
115
|
-
const status = typeof response === 'number' ? response : response?.status;
|
|
116
|
-
observer.complete(
|
|
117
|
-
status === 1 ? undefined : status === 0 ? 'failed' : 'unconfirmed',
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
} catch {
|
|
121
|
-
observer.complete('failed');
|
|
122
|
-
}
|
|
108
|
+
writePeopleProperties(properties, report);
|
|
123
109
|
}
|
|
124
110
|
|
|
125
111
|
/**
|
|
@@ -142,12 +128,36 @@ function attachProperty(key: string, value: unknown): void {
|
|
|
142
128
|
* Sets people properties for the current Mixpanel user without changing their distinct ID.
|
|
143
129
|
* @param {Record<string, unknown>} properties - Profile properties to set on the current user.
|
|
144
130
|
*/
|
|
145
|
-
function setPeopleProperties(
|
|
131
|
+
function setPeopleProperties(
|
|
132
|
+
properties: Record<string, unknown>,
|
|
133
|
+
report?: IdentifyReporter,
|
|
134
|
+
): void {
|
|
146
135
|
if (!isReady()) {
|
|
147
136
|
return;
|
|
148
137
|
}
|
|
149
138
|
|
|
150
|
-
|
|
139
|
+
writePeopleProperties(properties, report);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function writePeopleProperties(
|
|
143
|
+
properties: Record<string, unknown>,
|
|
144
|
+
report?: IdentifyReporter,
|
|
145
|
+
): void {
|
|
146
|
+
if (!report) {
|
|
147
|
+
mixpanel.people.set(properties);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const observer = observeIdentifyResult('people', report);
|
|
151
|
+
try {
|
|
152
|
+
mixpanel.people.set(properties, (response) => {
|
|
153
|
+
const status = typeof response === 'number' ? response : response?.status;
|
|
154
|
+
observer.complete(
|
|
155
|
+
status === 1 ? undefined : status === 0 ? 'failed' : 'unconfirmed',
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
observer.complete('failed');
|
|
160
|
+
}
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
/**
|
|
@@ -178,7 +188,7 @@ function track(eventName: string, properties: Record<string, unknown>): void {
|
|
|
178
188
|
* @returns {string | null} The live or cached distinct ID, or null when neither is available.
|
|
179
189
|
*/
|
|
180
190
|
export const getMixpanelDistinctId = (): string | null => {
|
|
181
|
-
const distinctId = mixpanel
|
|
191
|
+
const distinctId = isReady() ? mixpanel.get_distinct_id() : null;
|
|
182
192
|
if (distinctId) {
|
|
183
193
|
updateCachedDistinctId(distinctId);
|
|
184
194
|
return distinctId;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import coreApi from '@reservamos/js-api-client/core';
|
|
2
|
+
import mixpanel from 'mixpanel-browser';
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import fingerprintService from '@/services/fingerprint';
|
|
4
5
|
import mixpanelService from '@/services/mixpanel';
|
|
@@ -10,7 +11,22 @@ const { getVisitorData, init, client } = vi.hoisted(() => ({
|
|
|
10
11
|
client: vi.fn(),
|
|
11
12
|
}));
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
const { provider } = vi.hoisted(() => ({
|
|
15
|
+
provider: {
|
|
16
|
+
init: vi.fn((_token: string, config: { loaded: () => void }) =>
|
|
17
|
+
config.loaded(),
|
|
18
|
+
),
|
|
19
|
+
register: vi.fn(),
|
|
20
|
+
set_config: vi.fn(),
|
|
21
|
+
identify: vi.fn(),
|
|
22
|
+
track: vi.fn(),
|
|
23
|
+
get_distinct_id: vi.fn(() => 'live-id'),
|
|
24
|
+
people: { set: vi.fn(), union: vi.fn() },
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
vi.mock('mixpanel-browser', () => ({ default: provider }));
|
|
28
|
+
|
|
29
|
+
// Keep the SDK Fingerprint cache, Mixpanel service, tracking, validation, and relay real.
|
|
14
30
|
vi.mock('@fingerprintjs/fingerprintjs-pro-spa', () => ({
|
|
15
31
|
FingerprintJSPro: {
|
|
16
32
|
defaultEndpoint: 'https://fpjscdn.net',
|
|
@@ -57,6 +73,7 @@ const checkout = {
|
|
|
57
73
|
describe('tracking with blocked SDK Fingerprint cache writes', () => {
|
|
58
74
|
beforeEach(async () => {
|
|
59
75
|
localStorage.clear();
|
|
76
|
+
await mixpanelService.init('test-storage-token');
|
|
60
77
|
client.mockImplementation(() => ({ init, getVisitorData }));
|
|
61
78
|
init.mockResolvedValue(undefined);
|
|
62
79
|
getVisitorData.mockResolvedValue({
|
|
@@ -67,14 +84,8 @@ describe('tracking with blocked SDK Fingerprint cache writes', () => {
|
|
|
67
84
|
cacheHit: false,
|
|
68
85
|
});
|
|
69
86
|
await fingerprintService.initFingerprint('test-api-key');
|
|
70
|
-
vi.spyOn(
|
|
71
|
-
vi.spyOn(
|
|
72
|
-
'live-id',
|
|
73
|
-
);
|
|
74
|
-
vi.spyOn(mixpanelService, 'track').mockImplementation(() => undefined);
|
|
75
|
-
vi.spyOn(mixpanelService, 'setPeopleProperties').mockImplementation(
|
|
76
|
-
() => undefined,
|
|
77
|
-
);
|
|
87
|
+
vi.spyOn(mixpanel, 'get_distinct_id').mockReturnValue('live-id');
|
|
88
|
+
vi.spyOn(mixpanel.people, 'set').mockImplementation(() => undefined);
|
|
78
89
|
vi.spyOn(coreApi.fingerprint, 'sendObservation').mockResolvedValue(true);
|
|
79
90
|
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
80
91
|
throw new DOMException('Blocked', 'SecurityError');
|
|
@@ -96,16 +107,20 @@ describe('tracking with blocked SDK Fingerprint cache writes', () => {
|
|
|
96
107
|
async (eventName, properties, eligible) => {
|
|
97
108
|
await trackEvent(eventName, properties);
|
|
98
109
|
|
|
99
|
-
expect(
|
|
100
|
-
expect(
|
|
110
|
+
expect(mixpanel.track).toHaveBeenCalledOnce();
|
|
111
|
+
expect(mixpanel.track).toHaveBeenCalledWith(
|
|
101
112
|
eventName,
|
|
102
113
|
expect.objectContaining({
|
|
103
114
|
'User Fingerprint': expect.any(String),
|
|
104
115
|
'product': 'web',
|
|
105
116
|
}),
|
|
106
117
|
);
|
|
118
|
+
/**
|
|
119
|
+
* Checks effects that may finish after event submission during background refresh.
|
|
120
|
+
* @returns {void} Throws until the expected profile and relay effects are observed.
|
|
121
|
+
*/
|
|
107
122
|
const verifyIdentificationEffects = () => {
|
|
108
|
-
expect(
|
|
123
|
+
expect(mixpanel.people.set).toHaveBeenCalledWith({
|
|
109
124
|
'Visitor Found': true,
|
|
110
125
|
});
|
|
111
126
|
expect(coreApi.fingerprint.sendObservation).toHaveBeenCalledTimes(
|