@reservamos/browser-analytics 1.1.1 → 1.1.2-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/dist/browser-analytics.cjs +2 -2
- package/dist/browser-analytics.cjs.map +1 -1
- package/dist/browser-analytics.esm.js +40 -23
- 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 +1 -1
- package/src/init.ts +1 -1
- package/src/services/geolocation.test.ts +101 -23
- package/src/services/geolocation.ts +61 -24
- package/src/track.ts +2 -2
package/package.json
CHANGED
package/src/init.ts
CHANGED
|
@@ -49,7 +49,7 @@ async function init(config: InitConfig) {
|
|
|
49
49
|
console.error('Error initializing identification service:', error);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
geolocationService.init();
|
|
52
|
+
await geolocationService.init();
|
|
53
53
|
|
|
54
54
|
configService.setEnvironment(isSandbox ? 'sandbox' : 'prod');
|
|
55
55
|
coreApi.setConfig(configService.getCoreAPIConfig());
|
|
@@ -101,8 +101,8 @@ describe('geolocationService', () => {
|
|
|
101
101
|
'Geolocation Updated At': updatedAt,
|
|
102
102
|
'Geolocation Status': 'granted',
|
|
103
103
|
'Geolocation Accuracy': 12,
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
latitude: 19.4326,
|
|
105
|
+
longitude: -99.1332,
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
expect(coordinates).toEqual({ lat: 19.4326, long: -99.1332 });
|
|
@@ -113,8 +113,8 @@ describe('geolocationService', () => {
|
|
|
113
113
|
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
114
114
|
'Geolocation Updated At': updatedAt,
|
|
115
115
|
'Geolocation Status': 'granted',
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
latitude: 19.4326,
|
|
117
|
+
longitude: -99.1332,
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
vi.useRealTimers();
|
|
@@ -217,8 +217,8 @@ describe('geolocationService', () => {
|
|
|
217
217
|
'Geolocation Updated At': updatedAt,
|
|
218
218
|
'Geolocation Status': 'skipped_cache',
|
|
219
219
|
'Geolocation Accuracy': 25,
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
latitude: 19.4326,
|
|
221
|
+
longitude: -99.1332,
|
|
222
222
|
};
|
|
223
223
|
|
|
224
224
|
expect(coordinates).toEqual({ lat: 19.4326, long: -99.1332 });
|
|
@@ -229,33 +229,111 @@ describe('geolocationService', () => {
|
|
|
229
229
|
expect(setPeoplePropertiesMock).toHaveBeenCalledWith({
|
|
230
230
|
'Geolocation Updated At': updatedAt,
|
|
231
231
|
'Geolocation Status': 'skipped_cache',
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
latitude: 19.4326,
|
|
233
|
+
longitude: -99.1332,
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
it('
|
|
238
|
-
const
|
|
239
|
-
|
|
237
|
+
it('repairs legacy storage by clearing permission timestamp when coordinates are missing', async () => {
|
|
238
|
+
const updatedAt = '2026-05-07T21:19:00.000Z';
|
|
239
|
+
vi.useFakeTimers();
|
|
240
|
+
vi.setSystemTime(new Date(updatedAt));
|
|
240
241
|
|
|
241
|
-
localStorage.setItem('geolocationPermissionTimestamp',
|
|
242
|
+
localStorage.setItem('geolocationPermissionTimestamp', Date.now().toString());
|
|
242
243
|
localStorage.setItem('geolocationCache', '{ invalid json');
|
|
243
244
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
mockGeolocationSuccess({
|
|
246
|
+
coords: { latitude: 19.4326, longitude: -99.1332, accuracy: 15 },
|
|
247
|
+
} as GeolocationPosition);
|
|
248
|
+
|
|
249
|
+
const geolocationService = (await import('./geolocation')).default;
|
|
250
|
+
await geolocationService.init();
|
|
251
|
+
|
|
252
|
+
expect(localStorage.getItem('geolocationPermissionTimestamp')).toBe(
|
|
253
|
+
Date.parse(updatedAt).toString(),
|
|
254
|
+
);
|
|
255
|
+
expect(JSON.parse(localStorage.getItem('geolocationCache') ?? '')).toEqual({
|
|
256
|
+
lat: 19.4326,
|
|
257
|
+
long: -99.1332,
|
|
258
|
+
updatedAt,
|
|
259
|
+
accuracy: 15,
|
|
250
260
|
});
|
|
251
261
|
|
|
262
|
+
vi.useRealTimers();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('requests fresh geolocation when the permission cache is valid but coordinates are missing', async () => {
|
|
266
|
+
const updatedAt = '2026-05-07T21:19:00.000Z';
|
|
267
|
+
vi.useFakeTimers();
|
|
268
|
+
vi.setSystemTime(new Date(updatedAt));
|
|
269
|
+
|
|
270
|
+
localStorage.setItem('geolocationPermissionTimestamp', Date.now().toString());
|
|
271
|
+
localStorage.setItem('geolocationCache', '{ invalid json');
|
|
272
|
+
|
|
273
|
+
mockGeolocationSuccess({
|
|
274
|
+
coords: { latitude: 19.4326, longitude: -99.1332, accuracy: 15 },
|
|
275
|
+
} as GeolocationPosition);
|
|
276
|
+
|
|
252
277
|
const geolocationService = (await import('./geolocation')).default;
|
|
253
278
|
const coordinates = await geolocationService.init();
|
|
254
279
|
|
|
255
|
-
expect(coordinates).
|
|
256
|
-
expect(
|
|
257
|
-
'Geolocation
|
|
258
|
-
|
|
259
|
-
|
|
280
|
+
expect(coordinates).toEqual({ lat: 19.4326, long: -99.1332 });
|
|
281
|
+
expect(trackCustomEventMock).toHaveBeenCalledWith(
|
|
282
|
+
'Geolocation Requested',
|
|
283
|
+
{
|
|
284
|
+
'Geolocation Updated At': updatedAt,
|
|
285
|
+
'Geolocation Status': 'granted',
|
|
286
|
+
'Geolocation Accuracy': 15,
|
|
287
|
+
latitude: 19.4326,
|
|
288
|
+
longitude: -99.1332,
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
vi.useRealTimers();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('requests fresh geolocation when the permission cache is older than one hour', async () => {
|
|
296
|
+
const updatedAt = '2026-05-07T21:19:00.000Z';
|
|
297
|
+
vi.useFakeTimers();
|
|
298
|
+
vi.setSystemTime(new Date(updatedAt));
|
|
299
|
+
|
|
300
|
+
const expiredPermissionTimestamp = (
|
|
301
|
+
Date.parse(updatedAt) - 60 * 60 * 1000 - 1
|
|
302
|
+
).toString();
|
|
303
|
+
|
|
304
|
+
localStorage.setItem(
|
|
305
|
+
'geolocationPermissionTimestamp',
|
|
306
|
+
expiredPermissionTimestamp,
|
|
307
|
+
);
|
|
308
|
+
localStorage.setItem(
|
|
309
|
+
'geolocationCache',
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
lat: 19.4326,
|
|
312
|
+
long: -99.1332,
|
|
313
|
+
updatedAt: '2026-05-07T20:00:00.000Z',
|
|
314
|
+
accuracy: 25,
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
mockGeolocationSuccess({
|
|
319
|
+
coords: { latitude: 20.9674, longitude: -89.5926, accuracy: 8 },
|
|
320
|
+
} as GeolocationPosition);
|
|
321
|
+
|
|
322
|
+
const geolocationService = (await import('./geolocation')).default;
|
|
323
|
+
const coordinates = await geolocationService.init();
|
|
324
|
+
|
|
325
|
+
expect(coordinates).toEqual({ lat: 20.9674, long: -89.5926 });
|
|
326
|
+
expect(trackCustomEventMock).toHaveBeenCalledWith(
|
|
327
|
+
'Geolocation Requested',
|
|
328
|
+
{
|
|
329
|
+
'Geolocation Updated At': updatedAt,
|
|
330
|
+
'Geolocation Status': 'granted',
|
|
331
|
+
'Geolocation Accuracy': 8,
|
|
332
|
+
latitude: 20.9674,
|
|
333
|
+
longitude: -89.5926,
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
vi.useRealTimers();
|
|
260
338
|
});
|
|
261
339
|
});
|
|
@@ -17,6 +17,8 @@ interface GeolocationCache {
|
|
|
17
17
|
interface GeolocationTrackingData {
|
|
18
18
|
status: GeolocationStatus;
|
|
19
19
|
updatedAt: string;
|
|
20
|
+
lat?: number;
|
|
21
|
+
long?: number;
|
|
20
22
|
accuracy?: number;
|
|
21
23
|
errorCode?: number;
|
|
22
24
|
}
|
|
@@ -28,7 +30,7 @@ interface GeolocationTrackingData {
|
|
|
28
30
|
* - `granted` — The browser returned fresh coordinates successfully.
|
|
29
31
|
* - `denied` — The user blocked location access (PERMISSION_DENIED).
|
|
30
32
|
* - `error` — The request failed for another reason (timeout, position unavailable, or geolocation unsupported).
|
|
31
|
-
* - `skipped_cache` — Permission was granted within the last
|
|
33
|
+
* - `skipped_cache` — Permission was granted within the last hour; cached coordinates were reused without a new browser request.
|
|
32
34
|
*/
|
|
33
35
|
type GeolocationStatus = 'granted' | 'denied' | 'error' | 'skipped_cache';
|
|
34
36
|
|
|
@@ -37,7 +39,7 @@ let currentCoordinates: Coordinates | null = null;
|
|
|
37
39
|
const GEOLOCATION_PERMISSION_KEY = 'geolocationPermissionTimestamp';
|
|
38
40
|
const GEOLOCATION_CACHE_KEY = 'geolocationCache';
|
|
39
41
|
const GEOLOCATION_TRACK_EVENT = 'Geolocation Requested';
|
|
40
|
-
const
|
|
42
|
+
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
|
|
41
43
|
/** Max wait for a high-accuracy position fix before the browser reports TIMEOUT. */
|
|
42
44
|
const GEOLOCATION_REQUEST_TIMEOUT_MS = 10_000;
|
|
43
45
|
const GEOLOCATION_OPTIONS: PositionOptions = {
|
|
@@ -72,6 +74,18 @@ function safeSetItem(key: string, value: string): void {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Removes a value from localStorage, ignoring failures when storage is unavailable.
|
|
79
|
+
* @param {string} key - Storage key to remove.
|
|
80
|
+
*/
|
|
81
|
+
function safeRemoveItem(key: string): void {
|
|
82
|
+
try {
|
|
83
|
+
localStorage.removeItem(key);
|
|
84
|
+
} catch {
|
|
85
|
+
// Storage may be blocked (private mode, quota exceeded, etc.)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
/**
|
|
76
90
|
* Converts a Unix timestamp to an ISO 8601 string.
|
|
77
91
|
* @param {number} timestamp - Unix timestamp in milliseconds.
|
|
@@ -163,7 +177,7 @@ function isPermissionTimestampValid(
|
|
|
163
177
|
): boolean {
|
|
164
178
|
if (!lastPermissionTimestamp) return false;
|
|
165
179
|
const now = Date.now();
|
|
166
|
-
return now - parseInt(lastPermissionTimestamp, 10) <
|
|
180
|
+
return now - parseInt(lastPermissionTimestamp, 10) < ONE_HOUR_IN_MS;
|
|
167
181
|
}
|
|
168
182
|
|
|
169
183
|
/**
|
|
@@ -174,6 +188,21 @@ function updatePermissionTimestamp(timestamp: number): void {
|
|
|
174
188
|
safeSetItem(GEOLOCATION_PERMISSION_KEY, timestamp.toString());
|
|
175
189
|
}
|
|
176
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Repairs inconsistent localStorage left by older SDK versions that persisted a
|
|
193
|
+
* permission timestamp even when coordinates were never cached (e.g. after deny or error).
|
|
194
|
+
* Runs automatically on init so users do not need to clear storage after upgrading.
|
|
195
|
+
*/
|
|
196
|
+
function repairLegacyGeolocationStorage(): void {
|
|
197
|
+
if (!safeGetItem(GEOLOCATION_PERMISSION_KEY)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!loadCoordinatesFromStorage()) {
|
|
202
|
+
safeRemoveItem(GEOLOCATION_PERMISSION_KEY);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
177
206
|
/**
|
|
178
207
|
* Maps a GeolocationPositionError to a geolocation status.
|
|
179
208
|
* Permission denial maps to `denied`; timeout and position unavailable map to `error`.
|
|
@@ -207,10 +236,13 @@ function buildGeolocationEventProperties(
|
|
|
207
236
|
properties['Geolocation Error Code'] = data.errorCode;
|
|
208
237
|
}
|
|
209
238
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
239
|
+
if (
|
|
240
|
+
(data.status === 'granted' || data.status === 'skipped_cache') &&
|
|
241
|
+
data.lat !== undefined &&
|
|
242
|
+
data.long !== undefined
|
|
243
|
+
) {
|
|
244
|
+
properties.latitude = data.lat;
|
|
245
|
+
properties.longitude = data.long;
|
|
214
246
|
}
|
|
215
247
|
|
|
216
248
|
return properties;
|
|
@@ -230,10 +262,13 @@ function buildGeolocationProfileProperties(
|
|
|
230
262
|
'Geolocation Status': data.status,
|
|
231
263
|
};
|
|
232
264
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
265
|
+
if (
|
|
266
|
+
(data.status === 'granted' || data.status === 'skipped_cache') &&
|
|
267
|
+
data.lat !== undefined &&
|
|
268
|
+
data.long !== undefined
|
|
269
|
+
) {
|
|
270
|
+
properties.latitude = data.lat;
|
|
271
|
+
properties.longitude = data.long;
|
|
237
272
|
}
|
|
238
273
|
|
|
239
274
|
return properties;
|
|
@@ -260,6 +295,7 @@ function syncGeolocationToMixpanel(data: GeolocationTrackingData): void {
|
|
|
260
295
|
* @returns {Promise<Coordinates | null>} Cached or fresh coordinates, or null on failure.
|
|
261
296
|
*/
|
|
262
297
|
function init(): Promise<Coordinates | null> {
|
|
298
|
+
repairLegacyGeolocationStorage();
|
|
263
299
|
restoreCoordinatesFromStorage();
|
|
264
300
|
|
|
265
301
|
return new Promise((resolve) => {
|
|
@@ -267,18 +303,18 @@ function init(): Promise<Coordinates | null> {
|
|
|
267
303
|
|
|
268
304
|
if (isPermissionTimestampValid(lastPermissionTimestamp)) {
|
|
269
305
|
const cache = loadCoordinatesFromStorage();
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
306
|
+
|
|
307
|
+
if (cache) {
|
|
308
|
+
syncGeolocationToMixpanel({
|
|
309
|
+
status: 'skipped_cache',
|
|
310
|
+
updatedAt: cache.updatedAt,
|
|
311
|
+
lat: cache.lat,
|
|
312
|
+
long: cache.long,
|
|
313
|
+
accuracy: cache.accuracy,
|
|
314
|
+
});
|
|
315
|
+
resolve(currentCoordinates);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
282
318
|
}
|
|
283
319
|
|
|
284
320
|
const now = Date.now();
|
|
@@ -300,13 +336,14 @@ function init(): Promise<Coordinates | null> {
|
|
|
300
336
|
syncGeolocationToMixpanel({
|
|
301
337
|
status: 'granted',
|
|
302
338
|
updatedAt,
|
|
339
|
+
lat,
|
|
340
|
+
long,
|
|
303
341
|
accuracy,
|
|
304
342
|
});
|
|
305
343
|
resolve(currentCoordinates);
|
|
306
344
|
},
|
|
307
345
|
(error) => {
|
|
308
346
|
const status = getGeolocationStatus(error);
|
|
309
|
-
updatePermissionTimestamp(now);
|
|
310
347
|
|
|
311
348
|
syncGeolocationToMixpanel({
|
|
312
349
|
status,
|
package/src/track.ts
CHANGED
|
@@ -126,8 +126,8 @@ export async function trackEvent(
|
|
|
126
126
|
const defaultCoordinates: Record<string, string | number> = {};
|
|
127
127
|
const coordinates = geolocationService.getCoordinates();
|
|
128
128
|
if (coordinates) {
|
|
129
|
-
defaultCoordinates
|
|
130
|
-
defaultCoordinates
|
|
129
|
+
defaultCoordinates.latitude = coordinates.lat;
|
|
130
|
+
defaultCoordinates.longitude = coordinates.long;
|
|
131
131
|
}
|
|
132
132
|
const defaultProperties = {
|
|
133
133
|
'User Fingerprint': fingerprint,
|