@reservamos/browser-analytics 1.1.7-alpha.0 → 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.
- package/CHANGELOG.md +5 -0
- 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 +1737 -1477
- 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 +1 -1
- 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 +127 -0
- package/src/services/botDetectionRelay.ts +64 -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,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
|
+
});
|
package/src/services/mixpanel.ts
CHANGED
|
@@ -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
|
-
*
|
|
15
|
+
* SDK metadata sent on every Mixpanel event, including native identification events.
|
|
14
16
|
*/
|
|
15
|
-
const
|
|
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({ ...
|
|
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(
|
|
54
|
+
function identify(
|
|
55
|
+
userId: string,
|
|
56
|
+
properties: Record<string, unknown>,
|
|
57
|
+
report?: IdentifyReporter,
|
|
58
|
+
): void {
|
|
52
59
|
mixpanel.identify(userId);
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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 };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import mixpanelService from '@/services/mixpanel';
|
|
2
|
+
import { trackEventError } from '@/track';
|
|
3
|
+
|
|
4
|
+
type IdentifyStage =
|
|
5
|
+
| 'readiness'
|
|
6
|
+
| 'validation'
|
|
7
|
+
| 'profile'
|
|
8
|
+
| 'cache'
|
|
9
|
+
| 'mixpanel'
|
|
10
|
+
| 'people'
|
|
11
|
+
| 'fingerprint'
|
|
12
|
+
| 'datalake';
|
|
13
|
+
type IdentifyReason =
|
|
14
|
+
| 'failed'
|
|
15
|
+
| 'timeout'
|
|
16
|
+
| 'unobserved'
|
|
17
|
+
| 'unconfirmed'
|
|
18
|
+
| 'unavailable'
|
|
19
|
+
| 'invalid_input'
|
|
20
|
+
| 'http_error';
|
|
21
|
+
|
|
22
|
+
export type IdentifyReporter = (
|
|
23
|
+
stage: IdentifyStage,
|
|
24
|
+
reason: IdentifyReason,
|
|
25
|
+
httpStatus?: number,
|
|
26
|
+
) => void;
|
|
27
|
+
export interface IdentifyStageContext {
|
|
28
|
+
isActive: () => boolean;
|
|
29
|
+
report: IdentifyReporter;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Reports only SDK-controlled diagnostics; original errors and inputs never enter telemetry. */
|
|
33
|
+
export function createIdentifyReporter(): IdentifyReporter {
|
|
34
|
+
const reported = new Set<string>();
|
|
35
|
+
return (stage, reason, httpStatus) => {
|
|
36
|
+
const key = `${stage}:${reason}`;
|
|
37
|
+
if (reported.has(key)) return;
|
|
38
|
+
reported.add(key);
|
|
39
|
+
const payload = {
|
|
40
|
+
stage,
|
|
41
|
+
reason,
|
|
42
|
+
...(httpStatus !== undefined ? { httpStatus } : {}),
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
if (mixpanelService.isReady()) {
|
|
46
|
+
trackEventError(
|
|
47
|
+
'Identify',
|
|
48
|
+
new Error(`Identify ${stage}: ${reason}`),
|
|
49
|
+
payload,
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
console.error('Identify diagnostic', payload);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
console.error('Identify diagnostic', payload);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Waits only for this identify attempt, without changing the shared tracking queue. */
|
|
61
|
+
export function waitForIdentifyReady(
|
|
62
|
+
report: IdentifyReporter,
|
|
63
|
+
): Promise<boolean> {
|
|
64
|
+
if (mixpanelService.isReady()) return Promise.resolve(true);
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
let active = true;
|
|
67
|
+
const finish = (ready: boolean) => {
|
|
68
|
+
if (!active) return;
|
|
69
|
+
active = false;
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
window.removeEventListener('Tracker Ready', onReady);
|
|
72
|
+
if (!ready) report('readiness', 'timeout');
|
|
73
|
+
resolve(ready);
|
|
74
|
+
};
|
|
75
|
+
const onReady = () => {
|
|
76
|
+
if (mixpanelService.isReady()) finish(true);
|
|
77
|
+
};
|
|
78
|
+
const timer = setTimeout(() => finish(false), 5000);
|
|
79
|
+
window.addEventListener('Tracker Ready', onReady);
|
|
80
|
+
onReady();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Closes optional work at its deadline and consumes late fulfillment or rejection. */
|
|
85
|
+
export function waitForIdentifyStage<T>(
|
|
86
|
+
stage: 'profile' | 'fingerprint',
|
|
87
|
+
timeout: number,
|
|
88
|
+
run: (context: IdentifyStageContext) => Promise<T>,
|
|
89
|
+
report: IdentifyReporter,
|
|
90
|
+
): Promise<T | undefined> {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
let active = true;
|
|
93
|
+
const finish = (value?: T) => {
|
|
94
|
+
if (!active) return;
|
|
95
|
+
active = false;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
resolve(value);
|
|
98
|
+
};
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
report(stage, 'timeout');
|
|
101
|
+
finish();
|
|
102
|
+
}, timeout);
|
|
103
|
+
const context: IdentifyStageContext = {
|
|
104
|
+
isActive: () => active,
|
|
105
|
+
report: (...args) => {
|
|
106
|
+
if (active) report(...args);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
void Promise.resolve()
|
|
110
|
+
.then(() => run(context))
|
|
111
|
+
.then(
|
|
112
|
+
(value) => finish(value),
|
|
113
|
+
() => {
|
|
114
|
+
if (active) report(stage, 'failed');
|
|
115
|
+
finish();
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Observes remote outcomes separately from the public identify promise. */
|
|
122
|
+
export function observeIdentifyResult(
|
|
123
|
+
stage: 'people' | 'datalake',
|
|
124
|
+
report: IdentifyReporter,
|
|
125
|
+
onTimeout?: () => void,
|
|
126
|
+
) {
|
|
127
|
+
let active = true;
|
|
128
|
+
const complete = (reason?: IdentifyReason, httpStatus?: number): void => {
|
|
129
|
+
if (!active) return;
|
|
130
|
+
active = false;
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
if (reason) report(stage, reason, httpStatus);
|
|
133
|
+
};
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
complete(stage === 'people' ? 'unobserved' : 'timeout');
|
|
136
|
+
onTimeout?.();
|
|
137
|
+
}, 5000);
|
|
138
|
+
return { complete };
|
|
139
|
+
}
|