@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.
- package/CHANGELOG.md +19 -2
- package/README.md +32 -4
- package/dist/browser-analytics.cjs +2 -2
- package/dist/browser-analytics.cjs.map +1 -1
- package/dist/browser-analytics.d.ts +23 -10
- package/dist/browser-analytics.esm.js +466 -154
- 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.recovery.test.ts +367 -0
- package/src/events/identify/identify.robustness.test.ts +112 -13
- package/src/events/identify/identify.ts +72 -35
- 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/init.ts +10 -1
- package/src/profiles/createAnonymousProfile/createAnonymousProfile.ts +18 -2
- 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.batch.test.ts +108 -0
- package/src/services/mixpanel.test.ts +77 -73
- package/src/services/mixpanel.ts +45 -18
- package/src/services/profileEnrichment.ts +258 -0
- package/src/track.storage.test.ts +27 -12
package/src/services/mixpanel.ts
CHANGED
|
@@ -59,6 +59,7 @@ function init(
|
|
|
59
59
|
return new Promise<void>((resolve) => {
|
|
60
60
|
mixpanel.init(mixpanelToken, {
|
|
61
61
|
debug,
|
|
62
|
+
batch_flush_interval_ms: 1000,
|
|
62
63
|
loaded: () => {
|
|
63
64
|
if (attempt !== initialization) {
|
|
64
65
|
resolve();
|
|
@@ -105,21 +106,7 @@ function identify(
|
|
|
105
106
|
trackingLifecycle.identify(userId);
|
|
106
107
|
mixpanel.identify(userId);
|
|
107
108
|
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
|
-
}
|
|
109
|
+
writePeopleProperties(properties, report);
|
|
123
110
|
}
|
|
124
111
|
|
|
125
112
|
/**
|
|
@@ -142,12 +129,51 @@ function attachProperty(key: string, value: unknown): void {
|
|
|
142
129
|
* Sets people properties for the current Mixpanel user without changing their distinct ID.
|
|
143
130
|
* @param {Record<string, unknown>} properties - Profile properties to set on the current user.
|
|
144
131
|
*/
|
|
145
|
-
function setPeopleProperties(
|
|
132
|
+
function setPeopleProperties(
|
|
133
|
+
properties: Record<string, unknown>,
|
|
134
|
+
report?: IdentifyReporter,
|
|
135
|
+
onAccepted?: () => void,
|
|
136
|
+
): void {
|
|
146
137
|
if (!isReady()) {
|
|
147
138
|
return;
|
|
148
139
|
}
|
|
149
140
|
|
|
150
|
-
|
|
141
|
+
writePeopleProperties(properties, report, onAccepted);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Restores people readiness only when the native persisted identity still matches. */
|
|
145
|
+
function restorePeopleProperties(
|
|
146
|
+
userId: string,
|
|
147
|
+
properties: Record<string, unknown>,
|
|
148
|
+
report: IdentifyReporter,
|
|
149
|
+
onAccepted: () => void,
|
|
150
|
+
): void {
|
|
151
|
+
if (!isReady() || mixpanel.get_distinct_id() !== userId) return;
|
|
152
|
+
mixpanel.identify(userId);
|
|
153
|
+
writePeopleProperties(properties, report, onAccepted);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function writePeopleProperties(
|
|
157
|
+
properties: Record<string, unknown>,
|
|
158
|
+
report?: IdentifyReporter,
|
|
159
|
+
onAccepted?: () => void,
|
|
160
|
+
): void {
|
|
161
|
+
if (!report && !onAccepted) {
|
|
162
|
+
mixpanel.people.set(properties);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const observer = report ? observeIdentifyResult('people', report) : undefined;
|
|
166
|
+
try {
|
|
167
|
+
mixpanel.people.set(properties, (response) => {
|
|
168
|
+
const status = typeof response === 'number' ? response : response?.status;
|
|
169
|
+
if (status === 1) onAccepted?.();
|
|
170
|
+
observer?.complete(
|
|
171
|
+
status === 1 ? undefined : status === 0 ? 'failed' : 'unconfirmed',
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
observer?.complete('failed');
|
|
176
|
+
}
|
|
151
177
|
}
|
|
152
178
|
|
|
153
179
|
/**
|
|
@@ -178,7 +204,7 @@ function track(eventName: string, properties: Record<string, unknown>): void {
|
|
|
178
204
|
* @returns {string | null} The live or cached distinct ID, or null when neither is available.
|
|
179
205
|
*/
|
|
180
206
|
export const getMixpanelDistinctId = (): string | null => {
|
|
181
|
-
const distinctId = mixpanel
|
|
207
|
+
const distinctId = isReady() ? mixpanel.get_distinct_id() : null;
|
|
182
208
|
if (distinctId) {
|
|
183
209
|
updateCachedDistinctId(distinctId);
|
|
184
210
|
return distinctId;
|
|
@@ -214,6 +240,7 @@ const mixpanelService = {
|
|
|
214
240
|
identify,
|
|
215
241
|
attachProperty,
|
|
216
242
|
setPeopleProperties,
|
|
243
|
+
restorePeopleProperties,
|
|
217
244
|
getMixpanelDistinctId,
|
|
218
245
|
};
|
|
219
246
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { IdentifyReporter } from '@/util/identifyAttempt';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
createAnonymousProfileForIdentify,
|
|
5
|
+
prepareAnonymousProfileForIdentify,
|
|
6
|
+
requestPreparedAnonymousProfile,
|
|
7
|
+
} from '@/profiles/createAnonymousProfile/createAnonymousProfile';
|
|
8
|
+
import mixpanelService from '@/services/mixpanel';
|
|
9
|
+
import trackingLifecycle from '@/services/trackingLifecycle';
|
|
10
|
+
import {
|
|
11
|
+
createIdentifyReporter,
|
|
12
|
+
waitForIdentifyStage,
|
|
13
|
+
} from '@/util/identifyAttempt';
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = 'reservamos_pending_profile_v1';
|
|
16
|
+
const LIFETIME_MS = 10 * 60 * 1000;
|
|
17
|
+
const MAX_ATTEMPTS = 3;
|
|
18
|
+
const MAX_BYTES = 4096;
|
|
19
|
+
const profileIdSchema = z.union([
|
|
20
|
+
z.string().min(1),
|
|
21
|
+
z.number().int().positive(),
|
|
22
|
+
]);
|
|
23
|
+
const requestSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
identifier_key: z.enum(['email', 'phone']),
|
|
26
|
+
identifier_value: z.string().min(1),
|
|
27
|
+
identifiers: z.array(
|
|
28
|
+
z.object({ key: z.string().min(1), value: z.string().min(1) }).strict(),
|
|
29
|
+
),
|
|
30
|
+
details: z.record(z.string()),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
const pendingSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
version: z.literal(1),
|
|
36
|
+
id: z.string().min(1),
|
|
37
|
+
userId: z.string().min(1),
|
|
38
|
+
scope: z.string().min(1),
|
|
39
|
+
expiresAt: z.number().int(),
|
|
40
|
+
attempts: z.number().int().min(1).max(MAX_ATTEMPTS),
|
|
41
|
+
request: requestSchema.optional(),
|
|
42
|
+
profileId: profileIdSchema.optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict()
|
|
45
|
+
.refine((entry) => entry.request || entry.profileId);
|
|
46
|
+
type PendingProfile = z.infer<typeof pendingSchema>;
|
|
47
|
+
interface ProfileLink {
|
|
48
|
+
id: z.infer<typeof profileIdSchema>;
|
|
49
|
+
}
|
|
50
|
+
interface RecoveryScope {
|
|
51
|
+
project: string;
|
|
52
|
+
proxy?: string;
|
|
53
|
+
apiBaseUrl: string;
|
|
54
|
+
appName?: string;
|
|
55
|
+
}
|
|
56
|
+
let scope: string | undefined;
|
|
57
|
+
let generation = 0;
|
|
58
|
+
|
|
59
|
+
function remove(report: IdentifyReporter, id?: string): void {
|
|
60
|
+
try {
|
|
61
|
+
if (id) {
|
|
62
|
+
const stored = JSON.parse(
|
|
63
|
+
sessionStorage.getItem(STORAGE_KEY) || 'null',
|
|
64
|
+
) as PendingProfile | null;
|
|
65
|
+
if (stored?.id !== id) return;
|
|
66
|
+
}
|
|
67
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
68
|
+
} catch {
|
|
69
|
+
report('cache', 'unavailable');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function save(entry: PendingProfile, report: IdentifyReporter): boolean {
|
|
74
|
+
try {
|
|
75
|
+
const serialized = JSON.stringify(entry);
|
|
76
|
+
if (new Blob([serialized]).size > MAX_BYTES) {
|
|
77
|
+
remove(report);
|
|
78
|
+
report('cache', 'unavailable');
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
sessionStorage.setItem(STORAGE_KEY, serialized);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
remove(report);
|
|
85
|
+
report('cache', 'unavailable');
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function read(report: IdentifyReporter): PendingProfile | undefined {
|
|
91
|
+
try {
|
|
92
|
+
const serialized = sessionStorage.getItem(STORAGE_KEY);
|
|
93
|
+
if (!serialized) return undefined;
|
|
94
|
+
if (new Blob([serialized]).size > MAX_BYTES) throw new Error();
|
|
95
|
+
const entry = pendingSchema.parse(JSON.parse(serialized));
|
|
96
|
+
if (
|
|
97
|
+
entry.expiresAt <= Date.now() ||
|
|
98
|
+
entry.expiresAt > Date.now() + LIFETIME_MS
|
|
99
|
+
) {
|
|
100
|
+
remove(report);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return entry;
|
|
104
|
+
} catch {
|
|
105
|
+
remove(report);
|
|
106
|
+
report('cache', 'unavailable');
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function currentUserIs(userId: string): boolean {
|
|
112
|
+
return (
|
|
113
|
+
mixpanelService.isReady() &&
|
|
114
|
+
mixpanelService.getMixpanelDistinctId() === userId
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function resolveProfile(
|
|
119
|
+
userId: string,
|
|
120
|
+
properties: Record<string, string | number | boolean | undefined>,
|
|
121
|
+
isCurrent: () => boolean,
|
|
122
|
+
report: IdentifyReporter,
|
|
123
|
+
entry?: PendingProfile,
|
|
124
|
+
recovering = false,
|
|
125
|
+
): Promise<ProfileLink | undefined> {
|
|
126
|
+
if (!isCurrent()) return undefined;
|
|
127
|
+
const profile =
|
|
128
|
+
entry?.profileId !== undefined
|
|
129
|
+
? { id: entry.profileId }
|
|
130
|
+
: await waitForIdentifyStage(
|
|
131
|
+
'profile',
|
|
132
|
+
3000,
|
|
133
|
+
(context) =>
|
|
134
|
+
isCurrent()
|
|
135
|
+
? entry?.request
|
|
136
|
+
? requestPreparedAnonymousProfile(entry.request)
|
|
137
|
+
: createAnonymousProfileForIdentify(
|
|
138
|
+
{ ...properties },
|
|
139
|
+
context.report,
|
|
140
|
+
)
|
|
141
|
+
: Promise.resolve(undefined),
|
|
142
|
+
report,
|
|
143
|
+
);
|
|
144
|
+
if (!isCurrent()) return undefined;
|
|
145
|
+
if (!profile) {
|
|
146
|
+
if (entry && entry.attempts >= MAX_ATTEMPTS) remove(report, entry.id);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const parsedId = profileIdSchema.safeParse(profile.id);
|
|
150
|
+
if (!parsedId.success) {
|
|
151
|
+
report('profile', 'failed');
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const profileId = parsedId.data;
|
|
155
|
+
if (entry) save({ ...entry, request: undefined, profileId }, report);
|
|
156
|
+
const accepted = () => {
|
|
157
|
+
if (entry && isCurrent()) remove(report, entry.id);
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
const properties = { reservamos_one_id: profileId };
|
|
161
|
+
if (recovering) {
|
|
162
|
+
mixpanelService.restorePeopleProperties(
|
|
163
|
+
userId,
|
|
164
|
+
properties,
|
|
165
|
+
report,
|
|
166
|
+
accepted,
|
|
167
|
+
);
|
|
168
|
+
} else if (entry) {
|
|
169
|
+
mixpanelService.setPeopleProperties(properties, report, accepted);
|
|
170
|
+
} else {
|
|
171
|
+
mixpanelService.setPeopleProperties(properties, report);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
report('profile', 'failed');
|
|
175
|
+
}
|
|
176
|
+
return { id: profileId };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Records the original normalized request while preserving the ordinary in-memory enrichment. */
|
|
180
|
+
function enrich(
|
|
181
|
+
userId: string,
|
|
182
|
+
properties: Record<string, string | number | boolean | undefined>,
|
|
183
|
+
isActive: () => boolean,
|
|
184
|
+
report: IdentifyReporter,
|
|
185
|
+
): Promise<ProfileLink | undefined> {
|
|
186
|
+
if (!isActive()) return Promise.resolve(undefined);
|
|
187
|
+
const attempt = ++generation;
|
|
188
|
+
let request: z.infer<typeof requestSchema> | undefined;
|
|
189
|
+
if (scope) {
|
|
190
|
+
try {
|
|
191
|
+
const prepared = requestSchema.safeParse(
|
|
192
|
+
prepareAnonymousProfileForIdentify({ ...properties }, report),
|
|
193
|
+
);
|
|
194
|
+
if (prepared.success) request = prepared.data;
|
|
195
|
+
} catch {
|
|
196
|
+
// The ordinary bounded request below retains its existing validation handling.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const entry: PendingProfile | undefined =
|
|
200
|
+
scope && request
|
|
201
|
+
? {
|
|
202
|
+
version: 1,
|
|
203
|
+
id: `${Date.now()}-${Math.random()}`,
|
|
204
|
+
userId,
|
|
205
|
+
scope,
|
|
206
|
+
expiresAt: Date.now() + LIFETIME_MS,
|
|
207
|
+
attempts: 1,
|
|
208
|
+
request,
|
|
209
|
+
}
|
|
210
|
+
: undefined;
|
|
211
|
+
if (entry) save(entry, report);
|
|
212
|
+
else if (scope) remove(report);
|
|
213
|
+
const isCurrent = () =>
|
|
214
|
+
attempt === generation &&
|
|
215
|
+
isActive() &&
|
|
216
|
+
(!entry || Date.now() < entry.expiresAt) &&
|
|
217
|
+
(!scope || currentUserIs(userId));
|
|
218
|
+
return resolveProfile(userId, properties, isCurrent, report, entry);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Resumes only matching tab-local work after Mixpanel and Core configuration are ready. */
|
|
222
|
+
function initialize(configuration: RecoveryScope): void {
|
|
223
|
+
scope = JSON.stringify([
|
|
224
|
+
configuration.project,
|
|
225
|
+
configuration.proxy ?? '',
|
|
226
|
+
configuration.apiBaseUrl,
|
|
227
|
+
configuration.appName ?? '',
|
|
228
|
+
]);
|
|
229
|
+
const attempt = ++generation;
|
|
230
|
+
const report = createIdentifyReporter();
|
|
231
|
+
const entry = read(report);
|
|
232
|
+
if (!entry) return;
|
|
233
|
+
if (
|
|
234
|
+
entry.scope !== scope ||
|
|
235
|
+
!currentUserIs(entry.userId) ||
|
|
236
|
+
(entry.profileId === undefined && entry.attempts >= MAX_ATTEMPTS)
|
|
237
|
+
) {
|
|
238
|
+
remove(report, entry.id);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const lifecycle = trackingLifecycle.capture();
|
|
242
|
+
const isCurrent = () =>
|
|
243
|
+
attempt === generation &&
|
|
244
|
+
lifecycle() &&
|
|
245
|
+
Date.now() < entry.expiresAt &&
|
|
246
|
+
currentUserIs(entry.userId);
|
|
247
|
+
const resumed = {
|
|
248
|
+
...entry,
|
|
249
|
+
attempts: entry.attempts + (entry.profileId === undefined ? 1 : 0),
|
|
250
|
+
};
|
|
251
|
+
if (entry.profileId === undefined && !save(resumed, report)) return;
|
|
252
|
+
const scopedReport: IdentifyReporter = (...args) => {
|
|
253
|
+
if (isCurrent()) report(...args);
|
|
254
|
+
};
|
|
255
|
+
void resolveProfile(entry.userId, {}, isCurrent, scopedReport, resumed, true);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default { enrich, initialize };
|
|
@@ -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(
|