@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reservamos/browser-analytics",
3
- "version": "1.0.14",
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.2",
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
+ });