@reservamos/browser-analytics 1.0.14 → 1.0.22-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 +12 -0
- package/dist/browser-analytics.cjs +3 -3
- package/dist/browser-analytics.cjs.map +1 -1
- package/dist/browser-analytics.d.ts +13 -0
- package/dist/browser-analytics.esm.js +536 -431
- 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 +2 -2
- package/src/events/geolocationSelected/geolocationSelectedSchema.ts +13 -0
- package/src/events/geolocationSelected/index.ts +7 -0
- package/src/events/geolocationSelected/trackGeolocationSelected.ts +21 -0
- package/src/index.ts +4 -0
- package/src/init.ts +2 -2
- package/src/services/fingerprint.ts +4 -0
- package/src/services/geolocation.test.ts +261 -0
- package/src/services/geolocation.ts +270 -24
- package/src/services/mixpanel.ts +13 -0
- package/src/services/validator/validator.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reservamos/browser-analytics",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22-alpha.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/reservamos/reservamos-browser-analytics.git"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"typings": "./dist/reservamos-browser-analytics.d.ts",
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@fingerprintjs/fingerprintjs-pro-spa": "^1.3.
|
|
64
|
+
"@fingerprintjs/fingerprintjs-pro-spa": "^1.3.3",
|
|
65
65
|
"mixpanel-browser": "^2.55.1",
|
|
66
66
|
"zod": "^3.23.8"
|
|
67
67
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const geolocationSelectedSchema = z.object({
|
|
4
|
+
"Selected City": z.string().min(1, 'Selected city is required'),
|
|
5
|
+
"Selected Country Code": z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1, 'Selected Country Code is required'),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
type GeolocationSelectedProps = z.infer<typeof geolocationSelectedSchema>;
|
|
11
|
+
|
|
12
|
+
export type { GeolocationSelectedProps };
|
|
13
|
+
export default geolocationSelectedSchema;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GeolocationSelectedProps } from './geolocationSelectedSchema';
|
|
2
|
+
import geolocationSelectedSchema from './geolocationSelectedSchema';
|
|
3
|
+
import trackGeolocationSelected from './trackGeolocationSelected';
|
|
4
|
+
|
|
5
|
+
export type { GeolocationSelectedProps };
|
|
6
|
+
export { geolocationSelectedSchema };
|
|
7
|
+
export default trackGeolocationSelected;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EventMetadata } from '@/types/eventData';
|
|
2
|
+
import type { GeolocationSelectedProps } from './geolocationSelectedSchema';
|
|
3
|
+
import mixpanelService from '@/services/mixpanel';
|
|
4
|
+
import { trackEvent } from '@/track';
|
|
5
|
+
|
|
6
|
+
const EVENT_NAME = 'Geolocation Selected';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tracks a Geolocation Selected event and updates the Mixpanel user profile.
|
|
10
|
+
* @param {GeolocationSelectedProps} eventData - The data associated with the geolocation selection.
|
|
11
|
+
* @param {EventMetadata} meta - Additional metadata to include in the event.
|
|
12
|
+
*/
|
|
13
|
+
function trackGeolocationSelected(
|
|
14
|
+
eventData: GeolocationSelectedProps,
|
|
15
|
+
meta: EventMetadata = {},
|
|
16
|
+
): void {
|
|
17
|
+
trackEvent(EVENT_NAME, eventData, meta);
|
|
18
|
+
mixpanelService.setPeopleProperties(eventData);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default trackGeolocationSelected;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CustomEventProps } from '@/events/customEvent';
|
|
2
|
+
import type { GeolocationSelectedProps } from '@/events/geolocationSelected';
|
|
2
3
|
import type { InterestInHomeProps } from '@/events/interestInHome';
|
|
3
4
|
import type { InterestInSearchProps } from '@/events/interestInSearch';
|
|
4
5
|
import type { PassengersCreatedProps } from '@/events/passengersCreated';
|
|
@@ -17,6 +18,7 @@ import type { GetRecommendedSeatsSchemaProps } from '@/recommendations/getRecomm
|
|
|
17
18
|
import type { GetRecommendedTripsProps } from '@/recommendations/getRecommendedTrips';
|
|
18
19
|
import type { EventData, EventMetadata } from '@/types/eventData';
|
|
19
20
|
import trackCustomEvent from '@/events/customEvent';
|
|
21
|
+
import trackGeolocationSelected from '@/events/geolocationSelected';
|
|
20
22
|
import identify from '@/events/identify';
|
|
21
23
|
import trackInterestInHome from '@/events/interestInHome';
|
|
22
24
|
import trackInterestInSearch from '@/events/interestInSearch';
|
|
@@ -72,6 +74,7 @@ const analytics = {
|
|
|
72
74
|
pickedDeparture: trackPickedDeparture,
|
|
73
75
|
customEvent: trackCustomEvent,
|
|
74
76
|
purchaseCanceled: trackPurchaseCanceled,
|
|
77
|
+
geolocationSelected: trackGeolocationSelected,
|
|
75
78
|
},
|
|
76
79
|
};
|
|
77
80
|
|
|
@@ -95,6 +98,7 @@ export type {
|
|
|
95
98
|
GetRecommendedTripsProps,
|
|
96
99
|
CreateFrequentPassengersSchemaProps,
|
|
97
100
|
DeleteFrequentPassengerProps,
|
|
101
|
+
GeolocationSelectedProps,
|
|
98
102
|
};
|
|
99
103
|
|
|
100
104
|
export default analytics;
|
package/src/init.ts
CHANGED
|
@@ -38,10 +38,8 @@ async function init(config: InitConfig) {
|
|
|
38
38
|
identifyProxyUrl,
|
|
39
39
|
} = config;
|
|
40
40
|
|
|
41
|
-
geolocationService.init();
|
|
42
41
|
await mixpanelService.init(mixpanelToken, debug, mixpanelProxyUrl);
|
|
43
42
|
|
|
44
|
-
// Only mixpanel is required to be ready to dispatch the 'Tracker Ready' event
|
|
45
43
|
try {
|
|
46
44
|
await fingerprintService.initFingerprint(
|
|
47
45
|
identificationKey,
|
|
@@ -51,6 +49,8 @@ async function init(config: InitConfig) {
|
|
|
51
49
|
console.error('Error initializing identification service:', error);
|
|
52
50
|
}
|
|
53
51
|
|
|
52
|
+
geolocationService.init();
|
|
53
|
+
|
|
54
54
|
configService.setEnvironment(isSandbox ? 'sandbox' : 'prod');
|
|
55
55
|
coreApi.setConfig(configService.getCoreAPIConfig());
|
|
56
56
|
|
|
@@ -127,6 +127,10 @@ async function initFingerprint(
|
|
|
127
127
|
*/
|
|
128
128
|
async function getFingerprint(cacheOnly = false): Promise<string> {
|
|
129
129
|
if (!window.fingerprintConfig) {
|
|
130
|
+
if (cacheOnly) {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
|
|
130
134
|
throw new Error('Fingerprint configuration is not initialized.');
|
|
131
135
|
}
|
|
132
136
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const trackCustomEventMock = vi.fn();
|
|
4
|
+
const isTrackerReadyMock = vi.fn();
|
|
5
|
+
const setPeoplePropertiesMock = vi.fn();
|
|
6
|
+
|
|
7
|
+
vi.mock('@/events/customEvent', () => ({
|
|
8
|
+
default: trackCustomEventMock,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@/init', () => ({
|
|
12
|
+
isTrackerReady: isTrackerReadyMock,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@/services/mixpanel', () => ({
|
|
16
|
+
default: {
|
|
17
|
+
setPeopleProperties: setPeoplePropertiesMock,
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('geolocationService', () => {
|
|
22
|
+
let storage: Record<string, string>;
|
|
23
|
+
|
|
24
|
+
const expectedGeolocationOptions = {
|
|
25
|
+
enableHighAccuracy: true,
|
|
26
|
+
timeout: 10_000,
|
|
27
|
+
maximumAge: 0,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
storage = {};
|
|
32
|
+
vi.stubGlobal('localStorage', {
|
|
33
|
+
getItem: (key: string) => storage[key] ?? null,
|
|
34
|
+
setItem: (key: string, value: string) => {
|
|
35
|
+
storage[key] = value;
|
|
36
|
+
},
|
|
37
|
+
removeItem: (key: string) => {
|
|
38
|
+
storage = Object.fromEntries(
|
|
39
|
+
Object.entries(storage).filter(([entryKey]) => entryKey !== key),
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
clear: () => {
|
|
43
|
+
storage = {};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
vi.resetModules();
|
|
48
|
+
trackCustomEventMock.mockReset();
|
|
49
|
+
isTrackerReadyMock.mockReset();
|
|
50
|
+
setPeoplePropertiesMock.mockReset();
|
|
51
|
+
isTrackerReadyMock.mockReturnValue(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.restoreAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const mockGeolocationSuccess = (position: GeolocationPosition) => {
|
|
59
|
+
vi.stubGlobal('navigator', {
|
|
60
|
+
geolocation: {
|
|
61
|
+
getCurrentPosition: (
|
|
62
|
+
success: PositionCallback,
|
|
63
|
+
_error?: PositionErrorCallback,
|
|
64
|
+
options?: PositionOptions,
|
|
65
|
+
) => {
|
|
66
|
+
expect(options).toEqual(expectedGeolocationOptions);
|
|
67
|
+
success(position);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const mockGeolocationError = (positionError: GeolocationPositionError) => {
|
|
74
|
+
vi.stubGlobal('navigator', {
|
|
75
|
+
geolocation: {
|
|
76
|
+
getCurrentPosition: (
|
|
77
|
+
_success: PositionCallback,
|
|
78
|
+
error?: PositionErrorCallback,
|
|
79
|
+
options?: PositionOptions,
|
|
80
|
+
) => {
|
|
81
|
+
expect(options).toEqual(expectedGeolocationOptions);
|
|
82
|
+
error?.(positionError);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
it('tracks and identifies granted geolocation with coordinates and accuracy', async () => {
|
|
89
|
+
const updatedAt = '2026-05-07T21:19:00.000Z';
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
vi.setSystemTime(new Date(updatedAt));
|
|
92
|
+
|
|
93
|
+
mockGeolocationSuccess({
|
|
94
|
+
coords: { latitude: 19.4326, longitude: -99.1332, accuracy: 12 },
|
|
95
|
+
} as GeolocationPosition);
|
|
96
|
+
|
|
97
|
+
const geolocationService = (await import('./geolocation')).default;
|
|
98
|
+
const coordinates = await geolocationService.init();
|
|
99
|
+
|
|
100
|
+
const expectedProperties = {
|
|
101
|
+
'Geolocation Updated At': updatedAt,
|
|
102
|
+
'Geolocation Status': 'granted',
|
|
103
|
+
'Geolocation Accuracy': 12,
|
|
104
|
+
$latitude: 19.4326,
|
|
105
|
+
$longitude: -99.1332,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
expect(coordinates).toEqual({ lat: 19.4326, long: -99.1332 });
|
|
109
|
+
expect(trackCustomEventMock).toHaveBeenCalledWith(
|
|
110
|
+
'Geolocation Requested',
|
|
111
|
+
expectedProperties,
|
|
112
|
+
);
|
|
113
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
114
|
+
'Geolocation Updated At': updatedAt,
|
|
115
|
+
'Geolocation Status': 'granted',
|
|
116
|
+
$latitude: 19.4326,
|
|
117
|
+
$longitude: -99.1332,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
vi.useRealTimers();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('tracks and identifies denied geolocation when permission is rejected', async () => {
|
|
124
|
+
const updatedAt = '2026-05-07T21:19:00.000Z';
|
|
125
|
+
vi.useFakeTimers();
|
|
126
|
+
vi.setSystemTime(new Date(updatedAt));
|
|
127
|
+
|
|
128
|
+
mockGeolocationError({
|
|
129
|
+
code: 1,
|
|
130
|
+
PERMISSION_DENIED: 1,
|
|
131
|
+
POSITION_UNAVAILABLE: 2,
|
|
132
|
+
TIMEOUT: 3,
|
|
133
|
+
} as GeolocationPositionError);
|
|
134
|
+
|
|
135
|
+
const geolocationService = (await import('./geolocation')).default;
|
|
136
|
+
const coordinates = await geolocationService.init();
|
|
137
|
+
|
|
138
|
+
const expectedProperties = {
|
|
139
|
+
'Geolocation Updated At': updatedAt,
|
|
140
|
+
'Geolocation Status': 'denied',
|
|
141
|
+
'Geolocation Error Code': 1,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
expect(coordinates).toBeNull();
|
|
145
|
+
expect(trackCustomEventMock).toHaveBeenCalledWith(
|
|
146
|
+
'Geolocation Requested',
|
|
147
|
+
expectedProperties,
|
|
148
|
+
);
|
|
149
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
150
|
+
'Geolocation Updated At': updatedAt,
|
|
151
|
+
'Geolocation Status': 'denied',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
vi.useRealTimers();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('tracks and identifies error geolocation when the request fails technically', async () => {
|
|
158
|
+
const updatedAt = '2026-05-07T21:19:00.000Z';
|
|
159
|
+
vi.useFakeTimers();
|
|
160
|
+
vi.setSystemTime(new Date(updatedAt));
|
|
161
|
+
|
|
162
|
+
mockGeolocationError({
|
|
163
|
+
code: 3,
|
|
164
|
+
PERMISSION_DENIED: 1,
|
|
165
|
+
POSITION_UNAVAILABLE: 2,
|
|
166
|
+
TIMEOUT: 3,
|
|
167
|
+
} as GeolocationPositionError);
|
|
168
|
+
|
|
169
|
+
const geolocationService = (await import('./geolocation')).default;
|
|
170
|
+
await geolocationService.init();
|
|
171
|
+
|
|
172
|
+
const expectedProperties = {
|
|
173
|
+
'Geolocation Updated At': updatedAt,
|
|
174
|
+
'Geolocation Status': 'error',
|
|
175
|
+
'Geolocation Error Code': 3,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
expect(trackCustomEventMock).toHaveBeenCalledWith(
|
|
179
|
+
'Geolocation Requested',
|
|
180
|
+
expectedProperties,
|
|
181
|
+
);
|
|
182
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
183
|
+
'Geolocation Updated At': updatedAt,
|
|
184
|
+
'Geolocation Status': 'error',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
vi.useRealTimers();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('tracks and identifies skipped_cache and restores coordinates from storage', async () => {
|
|
191
|
+
const updatedAt = '2026-05-07T21:19:00.000Z';
|
|
192
|
+
const permissionTimestamp = Date.now().toString();
|
|
193
|
+
|
|
194
|
+
localStorage.setItem('geolocationPermissionTimestamp', permissionTimestamp);
|
|
195
|
+
localStorage.setItem(
|
|
196
|
+
'geolocationCache',
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
lat: 19.4326,
|
|
199
|
+
long: -99.1332,
|
|
200
|
+
updatedAt,
|
|
201
|
+
accuracy: 25,
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
vi.stubGlobal('navigator', {
|
|
206
|
+
geolocation: {
|
|
207
|
+
getCurrentPosition: () => {
|
|
208
|
+
throw new Error('getCurrentPosition should not be called');
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const geolocationService = (await import('./geolocation')).default;
|
|
214
|
+
const coordinates = await geolocationService.init();
|
|
215
|
+
|
|
216
|
+
const expectedProperties = {
|
|
217
|
+
'Geolocation Updated At': updatedAt,
|
|
218
|
+
'Geolocation Status': 'skipped_cache',
|
|
219
|
+
'Geolocation Accuracy': 25,
|
|
220
|
+
$latitude: 19.4326,
|
|
221
|
+
$longitude: -99.1332,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
expect(coordinates).toEqual({ lat: 19.4326, long: -99.1332 });
|
|
225
|
+
expect(trackCustomEventMock).toHaveBeenCalledWith(
|
|
226
|
+
'Geolocation Requested',
|
|
227
|
+
expectedProperties,
|
|
228
|
+
);
|
|
229
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
230
|
+
'Geolocation Updated At': updatedAt,
|
|
231
|
+
'Geolocation Status': 'skipped_cache',
|
|
232
|
+
$latitude: 19.4326,
|
|
233
|
+
$longitude: -99.1332,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('ignores malformed geolocation cache entries', async () => {
|
|
238
|
+
const permissionTimestamp = Date.now().toString();
|
|
239
|
+
const expectedUpdatedAt = new Date(Number(permissionTimestamp)).toISOString();
|
|
240
|
+
|
|
241
|
+
localStorage.setItem('geolocationPermissionTimestamp', permissionTimestamp);
|
|
242
|
+
localStorage.setItem('geolocationCache', '{ invalid json');
|
|
243
|
+
|
|
244
|
+
vi.stubGlobal('navigator', {
|
|
245
|
+
geolocation: {
|
|
246
|
+
getCurrentPosition: () => {
|
|
247
|
+
throw new Error('getCurrentPosition should not be called');
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const geolocationService = (await import('./geolocation')).default;
|
|
253
|
+
const coordinates = await geolocationService.init();
|
|
254
|
+
|
|
255
|
+
expect(coordinates).toBeNull();
|
|
256
|
+
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
257
|
+
'Geolocation Updated At': expectedUpdatedAt,
|
|
258
|
+
'Geolocation Status': 'skipped_cache',
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|