@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.
- package/CHANGELOG.md +7 -1
- package/README.md +35 -0
- package/dist/browser-analytics.cjs +3 -3
- package/dist/browser-analytics.cjs.map +1 -1
- package/dist/browser-analytics.d.ts +55 -55
- package/dist/browser-analytics.esm.js +1138 -844
- package/dist/browser-analytics.esm.js.map +1 -1
- package/dist/browser-analytics.iife.js +3 -3
- package/dist/browser-analytics.iife.js.map +1 -1
- package/package.json +3 -3
- package/src/events/identify/identify.robustness.test.ts +220 -0
- package/src/events/identify/identify.test.ts +6 -4
- package/src/events/identify/identify.ts +84 -42
- package/src/index.ts +2 -0
- package/src/profiles/createAnonymousProfile/createAnonymousProfile.identify.test.ts +56 -0
- package/src/profiles/createAnonymousProfile/createAnonymousProfile.ts +39 -12
- package/src/services/botDetectionRelay.test.ts +154 -0
- package/src/services/botDetectionRelay.ts +54 -0
- package/src/services/fingerprint.test.ts +250 -35
- package/src/services/fingerprint.ts +89 -12
- package/src/services/mixpanel.test.ts +160 -0
- package/src/services/mixpanel.ts +32 -6
- package/src/track.test.ts +156 -0
- package/src/track.ts +24 -5
- package/src/types/botDetection.ts +16 -0
- package/src/util/identifyAttempt.ts +139 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import coreApi from '@reservamos/js-api-client/core';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import botDetectionRelayService from './botDetectionRelay';
|
|
4
|
+
|
|
5
|
+
const payload = {
|
|
6
|
+
requestId: 'request-123',
|
|
7
|
+
visitorId: 'visitor-123',
|
|
8
|
+
distinctId: 'distinct-123',
|
|
9
|
+
eventName: 'Payment Attempt',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const coreApiConfig = {
|
|
13
|
+
baseUrl: 'https://data-lake.reservamossaas.com/api',
|
|
14
|
+
defaultHeaders: {
|
|
15
|
+
'Authorization': 'Bearer test-token',
|
|
16
|
+
'Origin': 'https://example.com',
|
|
17
|
+
'x-app-name': 'reservamos-web',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('botDetectionRelayService', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.unstubAllGlobals();
|
|
24
|
+
coreApi.setConfig(coreApiConfig);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.useRealTimers();
|
|
29
|
+
vi.unstubAllGlobals();
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('enables only the SDK-owned Bot Detection events', () => {
|
|
34
|
+
expect(botDetectionRelayService.isEnabledFor('Search')).toBe(true);
|
|
35
|
+
expect(botDetectionRelayService.isEnabledFor('Payment Attempt')).toBe(true);
|
|
36
|
+
expect(botDetectionRelayService.isEnabledFor('Purchase Attempt')).toBe(
|
|
37
|
+
true,
|
|
38
|
+
);
|
|
39
|
+
expect(botDetectionRelayService.isEnabledFor('View Results')).toBe(false);
|
|
40
|
+
expect(
|
|
41
|
+
botDetectionRelayService.isEnabledFor('Purchase Confirmation Viewed'),
|
|
42
|
+
).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('sends an observation through the configured client', async () => {
|
|
46
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 202 });
|
|
47
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
48
|
+
|
|
49
|
+
await botDetectionRelayService.relay(payload);
|
|
50
|
+
|
|
51
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
52
|
+
'https://data-lake.reservamossaas.com/api/v1/fingerprint/bot-detection',
|
|
53
|
+
{
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'x-app-name': 'reservamos-web',
|
|
58
|
+
'Authorization': 'Bearer test-token',
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
requestId: payload.requestId,
|
|
62
|
+
visitorId: payload.visitorId,
|
|
63
|
+
distinctId: payload.distinctId,
|
|
64
|
+
eventName: 'Fingerprint Requested',
|
|
65
|
+
}),
|
|
66
|
+
signal: expect.any(AbortSignal),
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('omits distinctId when unavailable', async () => {
|
|
72
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 202 });
|
|
73
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
74
|
+
|
|
75
|
+
await botDetectionRelayService.relay({
|
|
76
|
+
requestId: payload.requestId,
|
|
77
|
+
visitorId: payload.visitorId,
|
|
78
|
+
eventName: 'Search',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
82
|
+
expect.any(String),
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
requestId: payload.requestId,
|
|
86
|
+
visitorId: payload.visitorId,
|
|
87
|
+
eventName: 'Fingerprint Requested',
|
|
88
|
+
}),
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('contains rejected requests and reports the failure', async () => {
|
|
94
|
+
const error = new Error('Network unavailable');
|
|
95
|
+
const consoleError = vi
|
|
96
|
+
.spyOn(console, 'error')
|
|
97
|
+
.mockImplementation(() => undefined);
|
|
98
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(error));
|
|
99
|
+
|
|
100
|
+
await expect(
|
|
101
|
+
botDetectionRelayService.relay(payload),
|
|
102
|
+
).resolves.toBeUndefined();
|
|
103
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
104
|
+
'Error relaying Fingerprint identification:',
|
|
105
|
+
{ httpStatus: null, reason: 'network_or_cors' },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
consoleError.mockRestore();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('contains non-success responses and reports their status', async () => {
|
|
112
|
+
const consoleError = vi
|
|
113
|
+
.spyOn(console, 'error')
|
|
114
|
+
.mockImplementation(() => undefined);
|
|
115
|
+
vi.stubGlobal(
|
|
116
|
+
'fetch',
|
|
117
|
+
vi.fn().mockResolvedValue({ ok: false, status: 503 }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
botDetectionRelayService.relay(payload),
|
|
122
|
+
).resolves.toBeUndefined();
|
|
123
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
124
|
+
'Error relaying Fingerprint identification:',
|
|
125
|
+
{ httpStatus: 503, reason: 'service_unavailable' },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
consoleError.mockRestore();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('contains client timeouts and reports a sanitized diagnostic', async () => {
|
|
132
|
+
vi.useFakeTimers();
|
|
133
|
+
const consoleError = vi
|
|
134
|
+
.spyOn(console, 'error')
|
|
135
|
+
.mockImplementation(() => undefined);
|
|
136
|
+
const fetchMock = vi.fn(
|
|
137
|
+
(_url: string, { signal }: { signal: AbortSignal }) =>
|
|
138
|
+
new Promise<Response>((_resolve, reject) => {
|
|
139
|
+
signal.addEventListener('abort', () => reject(new Error('Aborted')));
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
143
|
+
|
|
144
|
+
const attempt = botDetectionRelayService.relay(payload);
|
|
145
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
146
|
+
|
|
147
|
+
await expect(attempt).resolves.toBeUndefined();
|
|
148
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
150
|
+
'Error relaying Fingerprint identification:',
|
|
151
|
+
{ httpStatus: null, reason: 'timeout' },
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BotDetectionRelayPayload } from '@/types/botDetection';
|
|
2
|
+
import coreApi from '@reservamos/js-api-client/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Events that require a fresh Fingerprint identification and Core API relay.
|
|
6
|
+
* Owned by the SDK so consumers do not choose which events participate.
|
|
7
|
+
*/
|
|
8
|
+
const BOT_DETECTION_EVENTS = [
|
|
9
|
+
'Search',
|
|
10
|
+
'Payment Attempt',
|
|
11
|
+
'Purchase Attempt',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks whether an event is eligible for Bot Detection.
|
|
16
|
+
* @param {string} eventName - Mixpanel event name.
|
|
17
|
+
* @returns {boolean} Whether the event should obtain and relay a fresh identification.
|
|
18
|
+
*/
|
|
19
|
+
function isEnabledFor(eventName: string): boolean {
|
|
20
|
+
return (BOT_DETECTION_EVENTS as readonly string[]).includes(eventName);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Relays Fingerprint identification context to Core API.
|
|
25
|
+
* Failures are contained so they cannot affect an event already submitted to Mixpanel.
|
|
26
|
+
* @param {BotDetectionRelayPayload} payload - Request correlation data.
|
|
27
|
+
* @returns {Promise<void>} Resolves after the relay attempt completes.
|
|
28
|
+
*/
|
|
29
|
+
async function relay(payload: BotDetectionRelayPayload): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
await coreApi.fingerprint.sendObservation({
|
|
32
|
+
requestId: payload.requestId,
|
|
33
|
+
visitorId: payload.visitorId,
|
|
34
|
+
distinctId: payload.distinctId,
|
|
35
|
+
onDiagnostic(diagnostic) {
|
|
36
|
+
if (diagnostic.reason !== 'http_accepted') {
|
|
37
|
+
console.error(
|
|
38
|
+
'Error relaying Fingerprint identification:',
|
|
39
|
+
diagnostic,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Error relaying Fingerprint identification:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const botDetectionRelayService = {
|
|
50
|
+
isEnabledFor,
|
|
51
|
+
relay,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default botDetectionRelayService;
|
|
@@ -1,66 +1,279 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import fingerprintService from './fingerprint';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import fingerprintService, { getFingerprintForIdentify } from './fingerprint';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const visitorData = {
|
|
5
|
+
visitorId: 'visitor-123',
|
|
6
|
+
requestId: 'request-123',
|
|
7
|
+
visitorFound: true,
|
|
8
|
+
confidence: { score: 0.99 },
|
|
9
|
+
cacheHit: false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const { getVisitorDataMock, initMock, setPeoplePropertiesMock, clientMock } =
|
|
13
|
+
vi.hoisted(() => ({
|
|
14
|
+
clientMock: vi.fn(),
|
|
15
|
+
getVisitorDataMock: vi.fn(),
|
|
16
|
+
initMock: vi.fn(),
|
|
17
|
+
setPeoplePropertiesMock: vi.fn(),
|
|
18
|
+
}));
|
|
8
19
|
|
|
9
20
|
vi.mock('@fingerprintjs/fingerprintjs-pro-spa', () => ({
|
|
10
21
|
FingerprintJSPro: {
|
|
11
22
|
defaultEndpoint: 'https://fpjscdn.net',
|
|
12
23
|
defaultScriptUrlPattern: 'https://fpjscdn.net/agent',
|
|
13
24
|
},
|
|
14
|
-
FpjsClient:
|
|
15
|
-
init: initMock,
|
|
16
|
-
getVisitorData: getVisitorDataMock,
|
|
17
|
-
})),
|
|
25
|
+
FpjsClient: clientMock,
|
|
18
26
|
}));
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
vi.mock('@/services/mixpanel', () => ({
|
|
29
|
+
default: { setPeopleProperties: setPeoplePropertiesMock },
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe('fingerprintService', () => {
|
|
33
|
+
let storage: Record<string, string>;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
clientMock.mockImplementation(() => ({
|
|
37
|
+
init: initMock,
|
|
38
|
+
getVisitorData: getVisitorDataMock,
|
|
39
|
+
}));
|
|
40
|
+
storage = {};
|
|
41
|
+
vi.stubGlobal('localStorage', {
|
|
42
|
+
getItem: (key: string) => storage[key] ?? null,
|
|
43
|
+
setItem: (key: string, value: string) => {
|
|
44
|
+
storage[key] = value;
|
|
45
|
+
},
|
|
46
|
+
removeItem: (key: string) => {
|
|
47
|
+
storage = Object.fromEntries(
|
|
48
|
+
Object.entries(storage).filter(([entryKey]) => entryKey !== key),
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
clear: () => {
|
|
52
|
+
storage = {};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
24
55
|
getVisitorDataMock.mockReset();
|
|
56
|
+
initMock.mockReset();
|
|
57
|
+
setPeoplePropertiesMock.mockReset();
|
|
58
|
+
getVisitorDataMock.mockResolvedValue(visitorData);
|
|
59
|
+
initMock.mockResolvedValue(undefined);
|
|
60
|
+
await fingerprintService.initFingerprint('test-api-key');
|
|
25
61
|
});
|
|
26
62
|
|
|
27
|
-
|
|
28
|
-
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
vi.restoreAllMocks();
|
|
65
|
+
vi.unstubAllGlobals();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('omits unavailable Visitor Found metadata without blocking identification', async () => {
|
|
69
|
+
getVisitorDataMock.mockResolvedValue({
|
|
70
|
+
...visitorData,
|
|
71
|
+
visitorFound: undefined,
|
|
72
|
+
});
|
|
73
|
+
const result = await fingerprintService.identifyVisitor();
|
|
74
|
+
expect(result.visitorId).toBe('visitor-123');
|
|
75
|
+
expect(result.requestId).toBe('request-123');
|
|
76
|
+
expect(result).not.toHaveProperty('visitorFound');
|
|
77
|
+
expect(setPeoplePropertiesMock).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('suppresses identify-specific cache and profile effects after its scope closes', async () => {
|
|
81
|
+
let finish: (value: typeof visitorData) => void = () => undefined;
|
|
82
|
+
getVisitorDataMock.mockReturnValue(
|
|
83
|
+
new Promise((resolve) => {
|
|
84
|
+
finish = resolve;
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
let active = true;
|
|
88
|
+
const pending = getFingerprintForIdentify({
|
|
89
|
+
isActive: () => active,
|
|
90
|
+
report: vi.fn(),
|
|
91
|
+
});
|
|
92
|
+
active = false;
|
|
93
|
+
finish(visitorData);
|
|
94
|
+
await pending;
|
|
95
|
+
expect(fingerprintService.getCachedFingerprint()).toBeNull();
|
|
96
|
+
expect(setPeoplePropertiesMock).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('continues the identify path when fingerprint cache writes fail', async () => {
|
|
100
|
+
vi.spyOn(localStorage, 'setItem').mockImplementation(() => {
|
|
101
|
+
throw new Error('private-id');
|
|
102
|
+
});
|
|
103
|
+
const report = vi.fn();
|
|
104
|
+
await expect(
|
|
105
|
+
getFingerprintForIdentify({ isActive: () => true, report }),
|
|
106
|
+
).resolves.toBe('visitor-123');
|
|
107
|
+
expect(report).toHaveBeenCalledWith('cache', 'unavailable');
|
|
108
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
109
|
+
'Visitor Found': true,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('sanitizes errors only on the identify-specific fingerprint path', async () => {
|
|
114
|
+
const error = new Error('private-id');
|
|
115
|
+
getVisitorDataMock.mockRejectedValue(error);
|
|
29
116
|
const consoleError = vi
|
|
30
117
|
.spyOn(console, 'error')
|
|
31
118
|
.mockImplementation(() => undefined);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
119
|
+
await expect(
|
|
120
|
+
getFingerprintForIdentify({ isActive: () => true, report: vi.fn() }),
|
|
121
|
+
).rejects.toBe(error);
|
|
122
|
+
expect(consoleError).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns request-scoped identification data and bypasses the SPA cache', async () => {
|
|
126
|
+
await expect(fingerprintService.identifyVisitor(true)).resolves.toEqual({
|
|
127
|
+
visitorId: 'visitor-123',
|
|
128
|
+
requestId: 'request-123',
|
|
129
|
+
visitorFound: true,
|
|
130
|
+
confidenceScore: 0.99,
|
|
131
|
+
cacheHit: false,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(getVisitorDataMock).toHaveBeenCalledWith({}, true);
|
|
135
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
136
|
+
'Visitor Found': true,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('writes a false visitor match to the Mixpanel profile', async () => {
|
|
141
|
+
getVisitorDataMock.mockResolvedValue({
|
|
142
|
+
...visitorData,
|
|
143
|
+
visitorFound: false,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await expect(
|
|
147
|
+
fingerprintService.identifyVisitor(true),
|
|
148
|
+
).resolves.toMatchObject({ visitorFound: false });
|
|
149
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
150
|
+
'Visitor Found': false,
|
|
151
|
+
});
|
|
152
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(getVisitorDataMock).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('keeps identification data when the Mixpanel profile update fails', async () => {
|
|
157
|
+
const error = new Error('Mixpanel unavailable');
|
|
158
|
+
const consoleError = vi
|
|
159
|
+
.spyOn(console, 'error')
|
|
160
|
+
.mockImplementation(() => undefined);
|
|
161
|
+
setPeoplePropertiesMock.mockImplementation(() => {
|
|
162
|
+
throw error;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await expect(fingerprintService.identifyVisitor(true)).resolves.toEqual({
|
|
166
|
+
visitorId: 'visitor-123',
|
|
167
|
+
requestId: 'request-123',
|
|
168
|
+
visitorFound: true,
|
|
169
|
+
confidenceScore: 0.99,
|
|
170
|
+
cacheHit: false,
|
|
171
|
+
});
|
|
172
|
+
expect(fingerprintService.getCachedFingerprint()).toBe('visitor-123');
|
|
173
|
+
expect(getVisitorDataMock).toHaveBeenCalledTimes(1);
|
|
174
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
175
|
+
'Error updating Mixpanel visitor profile:',
|
|
176
|
+
error,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
consoleError.mockRestore();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('preserves the visitor-only SDK cache contract', async () => {
|
|
183
|
+
await fingerprintService.identifyVisitor(true);
|
|
184
|
+
|
|
185
|
+
expect(fingerprintService.getCachedFingerprint()).toBe('visitor-123');
|
|
186
|
+
await expect(fingerprintService.getFingerprint(true)).resolves.toBe(
|
|
187
|
+
'visitor-123',
|
|
37
188
|
);
|
|
189
|
+
expect(getVisitorDataMock).toHaveBeenCalledTimes(1);
|
|
190
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledTimes(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('does not reuse a legacy cached visitor ID as a request-scoped result', async () => {
|
|
194
|
+
localStorage.setItem(
|
|
195
|
+
'default_fingerprint_cache',
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
fingerprint: 'legacy-visitor',
|
|
198
|
+
expiresAt: Date.now() + 60_000,
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
await expect(
|
|
203
|
+
fingerprintService.identifyVisitor(true),
|
|
204
|
+
).resolves.toMatchObject({
|
|
205
|
+
visitorId: 'visitor-123',
|
|
206
|
+
requestId: 'request-123',
|
|
207
|
+
});
|
|
208
|
+
expect(getVisitorDataMock).toHaveBeenCalledWith({}, true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('reports and rethrows identification errors', async () => {
|
|
212
|
+
const error = new Error('Fingerprint unavailable');
|
|
213
|
+
const consoleError = vi
|
|
214
|
+
.spyOn(console, 'error')
|
|
215
|
+
.mockImplementation(() => undefined);
|
|
216
|
+
getVisitorDataMock.mockRejectedValue(error);
|
|
217
|
+
|
|
218
|
+
await expect(fingerprintService.identifyVisitor(true)).rejects.toThrow(
|
|
219
|
+
'Fingerprint unavailable',
|
|
220
|
+
);
|
|
221
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
222
|
+
'Error retrieving fingerprint:',
|
|
223
|
+
error,
|
|
224
|
+
);
|
|
225
|
+
expect(setPeoplePropertiesMock).not.toHaveBeenCalled();
|
|
226
|
+
|
|
227
|
+
consoleError.mockRestore();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('does not treat a constructed client as ready until the agent loads', async () => {
|
|
231
|
+
const consoleError = vi
|
|
232
|
+
.spyOn(console, 'error')
|
|
233
|
+
.mockImplementation(() => undefined);
|
|
234
|
+
const initError = new Error('Agent script blocked');
|
|
235
|
+
initMock.mockRejectedValue(initError);
|
|
38
236
|
|
|
39
237
|
await expect(
|
|
40
|
-
fingerprintService.initFingerprint('test-key'),
|
|
238
|
+
fingerprintService.initFingerprint('test-api-key'),
|
|
41
239
|
).rejects.toThrow('Agent script blocked');
|
|
42
240
|
expect(fingerprintService.isFingerprintReady()).toBe(false);
|
|
43
|
-
await expect(fingerprintService.getFingerprint()).resolves.toBe('');
|
|
44
|
-
expect(getVisitorDataMock).not.toHaveBeenCalled();
|
|
45
241
|
expect(consoleError).toHaveBeenCalledWith(
|
|
46
242
|
'Error initializing identification service:',
|
|
47
243
|
initError,
|
|
48
244
|
);
|
|
49
245
|
|
|
246
|
+
await expect(fingerprintService.getFingerprint()).resolves.toBe('');
|
|
247
|
+
await expect(fingerprintService.identifyVisitor(true)).rejects.toThrow(
|
|
248
|
+
'Identification service is not initialized.',
|
|
249
|
+
);
|
|
250
|
+
expect(getVisitorDataMock).not.toHaveBeenCalled();
|
|
251
|
+
|
|
50
252
|
consoleError.mockRestore();
|
|
51
253
|
});
|
|
52
254
|
|
|
53
|
-
it('
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
255
|
+
it('returns a cached visitor ID when the agent failed to load', async () => {
|
|
256
|
+
localStorage.setItem(
|
|
257
|
+
'default_fingerprint_cache',
|
|
258
|
+
JSON.stringify({
|
|
259
|
+
fingerprint: 'cached-visitor',
|
|
260
|
+
expiresAt: Date.now() + 60_000,
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
const consoleError = vi
|
|
264
|
+
.spyOn(console, 'error')
|
|
265
|
+
.mockImplementation(() => undefined);
|
|
266
|
+
initMock.mockRejectedValue(new Error('Agent script blocked'));
|
|
58
267
|
|
|
59
|
-
expect(
|
|
268
|
+
await expect(
|
|
269
|
+
fingerprintService.initFingerprint('test-api-key'),
|
|
270
|
+
).rejects.toThrow('Agent script blocked');
|
|
60
271
|
await expect(fingerprintService.getFingerprint()).resolves.toBe(
|
|
61
|
-
'visitor
|
|
272
|
+
'cached-visitor',
|
|
62
273
|
);
|
|
63
|
-
expect(getVisitorDataMock).
|
|
274
|
+
expect(getVisitorDataMock).not.toHaveBeenCalled();
|
|
275
|
+
|
|
276
|
+
consoleError.mockRestore();
|
|
64
277
|
});
|
|
65
278
|
|
|
66
279
|
it('waits for the agent to load before identifying', async () => {
|
|
@@ -71,9 +284,8 @@ describe('fingerprintService readiness', () => {
|
|
|
71
284
|
finishInit = resolve;
|
|
72
285
|
}),
|
|
73
286
|
);
|
|
74
|
-
getVisitorDataMock.mockResolvedValue({ visitorId: 'visitor-123' });
|
|
75
287
|
|
|
76
|
-
const init = fingerprintService.initFingerprint('test-key');
|
|
288
|
+
const init = fingerprintService.initFingerprint('test-api-key');
|
|
77
289
|
|
|
78
290
|
expect(fingerprintService.isFingerprintReady()).toBe(false);
|
|
79
291
|
await expect(fingerprintService.getFingerprint()).resolves.toBe('');
|
|
@@ -127,7 +339,10 @@ describe('fingerprintService readiness', () => {
|
|
|
127
339
|
}),
|
|
128
340
|
)
|
|
129
341
|
.mockResolvedValueOnce(undefined);
|
|
130
|
-
getVisitorDataMock.mockResolvedValue({
|
|
342
|
+
getVisitorDataMock.mockResolvedValue({
|
|
343
|
+
...visitorData,
|
|
344
|
+
visitorId: 'new-visitor',
|
|
345
|
+
});
|
|
131
346
|
const consoleError = vi
|
|
132
347
|
.spyOn(console, 'error')
|
|
133
348
|
.mockImplementation(() => undefined);
|