@mostly-good-metrics/javascript 0.4.3 → 0.5.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/README.md +50 -4
- package/dist/cjs/client.js +34 -16
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/network.js +1 -1
- package/dist/cjs/network.js.map +1 -1
- package/dist/cjs/storage.js +139 -1
- package/dist/cjs/storage.js.map +1 -1
- package/dist/cjs/types.js +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/cjs/utils.js +30 -0
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/client.js +35 -17
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/network.js +1 -1
- package/dist/esm/network.js.map +1 -1
- package/dist/esm/storage.js +139 -1
- package/dist/esm/storage.js.map +1 -1
- package/dist/esm/types.js +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils.js +29 -0
- package/dist/esm/utils.js.map +1 -1
- package/dist/types/client.d.ts +5 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/storage.d.ts +36 -1
- package/dist/types/storage.d.ts.map +1 -1
- package/dist/types/types.d.ts +42 -15
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +5 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.test.ts +153 -1
- package/src/client.ts +45 -16
- package/src/index.ts +1 -0
- package/src/network.test.ts +3 -3
- package/src/network.ts +1 -1
- package/src/storage.test.ts +126 -0
- package/src/storage.ts +153 -1
- package/src/types.ts +52 -15
- package/src/utils.test.ts +21 -0
- package/src/utils.ts +30 -0
package/src/storage.test.ts
CHANGED
|
@@ -322,3 +322,129 @@ describe('PersistenceManager super properties', () => {
|
|
|
322
322
|
expect(props).toEqual({});
|
|
323
323
|
});
|
|
324
324
|
});
|
|
325
|
+
|
|
326
|
+
describe('PersistenceManager anonymous ID (localStorage)', () => {
|
|
327
|
+
beforeEach(() => {
|
|
328
|
+
jest.clearAllMocks();
|
|
329
|
+
(localStorage.getItem as jest.Mock).mockReturnValue(null);
|
|
330
|
+
// Disable cookies for these tests to test localStorage behavior
|
|
331
|
+
persistence.configureCookies(undefined, true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const generateMockUUID = () => 'mock-uuid-12345';
|
|
335
|
+
|
|
336
|
+
it('should generate anonymous ID if none exists', () => {
|
|
337
|
+
const id = persistence.initializeAnonymousId(undefined, generateMockUUID);
|
|
338
|
+
expect(id).toBe('mock-uuid-12345');
|
|
339
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
340
|
+
'mostlygoodmetrics_anonymous_id',
|
|
341
|
+
'mock-uuid-12345'
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should return existing anonymous ID from localStorage', () => {
|
|
346
|
+
(localStorage.getItem as jest.Mock).mockReturnValueOnce('existing-anonymous-id');
|
|
347
|
+
const id = persistence.initializeAnonymousId(undefined, generateMockUUID);
|
|
348
|
+
expect(id).toBe('existing-anonymous-id');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should use override ID from wrapper SDK', () => {
|
|
352
|
+
const overrideId = 'react-native-device-id';
|
|
353
|
+
const id = persistence.initializeAnonymousId(overrideId, generateMockUUID);
|
|
354
|
+
expect(id).toBe(overrideId);
|
|
355
|
+
expect(localStorage.setItem).toHaveBeenCalledWith('mostlygoodmetrics_anonymous_id', overrideId);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should prefer override ID over existing persisted ID', () => {
|
|
359
|
+
(localStorage.getItem as jest.Mock).mockReturnValueOnce('existing-anonymous-id');
|
|
360
|
+
const overrideId = 'react-native-device-id';
|
|
361
|
+
const id = persistence.initializeAnonymousId(overrideId, generateMockUUID);
|
|
362
|
+
expect(id).toBe(overrideId);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should reset anonymous ID with new UUID', () => {
|
|
366
|
+
let callCount = 0;
|
|
367
|
+
const generateNewUUID = () => {
|
|
368
|
+
callCount++;
|
|
369
|
+
return `new-uuid-${callCount}`;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const id1 = persistence.resetAnonymousId(generateNewUUID);
|
|
373
|
+
expect(id1).toBe('new-uuid-1');
|
|
374
|
+
|
|
375
|
+
const id2 = persistence.resetAnonymousId(generateNewUUID);
|
|
376
|
+
expect(id2).toBe('new-uuid-2');
|
|
377
|
+
expect(id2).not.toBe(id1);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should persist anonymous ID to localStorage', () => {
|
|
381
|
+
persistence.setAnonymousId('test-anonymous-id');
|
|
382
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
383
|
+
'mostlygoodmetrics_anonymous_id',
|
|
384
|
+
'test-anonymous-id'
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should get anonymous ID from localStorage', () => {
|
|
389
|
+
(localStorage.getItem as jest.Mock).mockReturnValueOnce('stored-anonymous-id');
|
|
390
|
+
const id = persistence.getAnonymousId();
|
|
391
|
+
expect(id).toBe('stored-anonymous-id');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe('PersistenceManager anonymous ID (cookies)', () => {
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
jest.clearAllMocks();
|
|
398
|
+
(localStorage.getItem as jest.Mock).mockReturnValue(null);
|
|
399
|
+
// Clear document.cookie
|
|
400
|
+
document.cookie = 'mostlygoodmetrics_anonymous_id=; path=/; max-age=0';
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const generateMockUUID = () => 'mock-uuid-12345';
|
|
404
|
+
|
|
405
|
+
it('should use cookies when enabled', () => {
|
|
406
|
+
persistence.configureCookies(undefined, false);
|
|
407
|
+
persistence.setAnonymousId('cookie-test-id');
|
|
408
|
+
expect(document.cookie).toContain('mostlygoodmetrics_anonymous_id=cookie-test-id');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should read from cookies before localStorage', () => {
|
|
412
|
+
persistence.configureCookies(undefined, false);
|
|
413
|
+
document.cookie = 'mostlygoodmetrics_anonymous_id=cookie-id; path=/';
|
|
414
|
+
(localStorage.getItem as jest.Mock).mockReturnValue('localStorage-id');
|
|
415
|
+
|
|
416
|
+
const id = persistence.getAnonymousId();
|
|
417
|
+
expect(id).toBe('cookie-id');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should fall back to localStorage when cookie is not set', () => {
|
|
421
|
+
persistence.configureCookies(undefined, false);
|
|
422
|
+
(localStorage.getItem as jest.Mock).mockReturnValue('localStorage-id');
|
|
423
|
+
|
|
424
|
+
const id = persistence.getAnonymousId();
|
|
425
|
+
expect(id).toBe('localStorage-id');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should set cookie with custom domain for cross-subdomain support', () => {
|
|
429
|
+
persistence.configureCookies('.example.com', false);
|
|
430
|
+
persistence.setAnonymousId('cross-domain-id');
|
|
431
|
+
// Note: jsdom rejects cookies for non-matching domains, so we verify localStorage fallback
|
|
432
|
+
// In real browsers, the cookie would be set with domain=.example.com
|
|
433
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
434
|
+
'mostlygoodmetrics_anonymous_id',
|
|
435
|
+
'cross-domain-id'
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should not use cookies when disabled', () => {
|
|
440
|
+
persistence.configureCookies(undefined, true);
|
|
441
|
+
persistence.setAnonymousId('no-cookie-id');
|
|
442
|
+
// Cookie should not contain our ID (cleared in beforeEach)
|
|
443
|
+
expect(document.cookie).not.toContain('mostlygoodmetrics_anonymous_id=no-cookie-id');
|
|
444
|
+
// But localStorage should have it
|
|
445
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
446
|
+
'mostlygoodmetrics_anonymous_id',
|
|
447
|
+
'no-cookie-id'
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Constraints, EventProperties, IEventStorage, MGMError, MGMEvent } from
|
|
|
3
3
|
|
|
4
4
|
const STORAGE_KEY = 'mostlygoodmetrics_events';
|
|
5
5
|
const USER_ID_KEY = 'mostlygoodmetrics_user_id';
|
|
6
|
+
const ANONYMOUS_ID_KEY = 'mostlygoodmetrics_anonymous_id';
|
|
6
7
|
const APP_VERSION_KEY = 'mostlygoodmetrics_app_version';
|
|
7
8
|
const SUPER_PROPERTIES_KEY = 'mostlygoodmetrics_super_properties';
|
|
8
9
|
|
|
@@ -23,6 +24,59 @@ function isLocalStorageAvailable(): boolean {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Check if cookies are available in this environment.
|
|
29
|
+
*/
|
|
30
|
+
function isCookieAvailable(): boolean {
|
|
31
|
+
try {
|
|
32
|
+
if (typeof document === 'undefined' || typeof document.cookie === 'undefined') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
// Test if we can actually set a cookie
|
|
36
|
+
const testKey = '__mgm_cookie_test__';
|
|
37
|
+
document.cookie = `${testKey}=test; path=/; max-age=60`;
|
|
38
|
+
const hasTest = document.cookie.indexOf(testKey) !== -1;
|
|
39
|
+
// Clean up test cookie
|
|
40
|
+
document.cookie = `${testKey}=; path=/; max-age=0`;
|
|
41
|
+
return hasTest;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get a cookie value by name.
|
|
49
|
+
*/
|
|
50
|
+
function getCookie(name: string): string | null {
|
|
51
|
+
if (!isCookieAvailable()) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const cookies = document.cookie.split(';');
|
|
55
|
+
for (const cookie of cookies) {
|
|
56
|
+
const [cookieName, cookieValue] = cookie.trim().split('=');
|
|
57
|
+
if (cookieName === name) {
|
|
58
|
+
return decodeURIComponent(cookieValue);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set a cookie with optional domain for cross-subdomain support.
|
|
66
|
+
* Uses a 1-year expiry by default.
|
|
67
|
+
*/
|
|
68
|
+
function setCookie(name: string, value: string, domain?: string): void {
|
|
69
|
+
if (!isCookieAvailable()) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const maxAge = 365 * 24 * 60 * 60; // 1 year in seconds
|
|
73
|
+
let cookieString = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
|
|
74
|
+
if (domain) {
|
|
75
|
+
cookieString += `; domain=${domain}`;
|
|
76
|
+
}
|
|
77
|
+
document.cookie = cookieString;
|
|
78
|
+
}
|
|
79
|
+
|
|
26
80
|
/**
|
|
27
81
|
* In-memory event storage implementation.
|
|
28
82
|
* Used as a fallback when localStorage is not available,
|
|
@@ -173,12 +227,32 @@ export function createDefaultStorage(maxEvents: number): IEventStorage {
|
|
|
173
227
|
|
|
174
228
|
/**
|
|
175
229
|
* Persistence helpers for user ID and app version.
|
|
176
|
-
*
|
|
230
|
+
* Uses cookies first (for cross-subdomain support), then localStorage as fallback.
|
|
177
231
|
*/
|
|
178
232
|
class PersistenceManager {
|
|
179
233
|
private inMemoryUserId: string | null = null;
|
|
234
|
+
private inMemoryAnonymousId: string | null = null;
|
|
180
235
|
private inMemoryAppVersion: string | null = null;
|
|
181
236
|
private inMemorySuperProperties: EventProperties = {};
|
|
237
|
+
private cookieDomain: string | undefined = undefined;
|
|
238
|
+
private disableCookies = false;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Configure cookie settings.
|
|
242
|
+
* @param cookieDomain Domain for cross-subdomain cookies (e.g., '.example.com')
|
|
243
|
+
* @param disableCookies If true, only use localStorage (no cookies)
|
|
244
|
+
*/
|
|
245
|
+
configureCookies(cookieDomain?: string, disableCookies?: boolean): void {
|
|
246
|
+
this.cookieDomain = cookieDomain;
|
|
247
|
+
this.disableCookies = disableCookies ?? false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if cookies should be used.
|
|
252
|
+
*/
|
|
253
|
+
private shouldUseCookies(): boolean {
|
|
254
|
+
return !this.disableCookies && isCookieAvailable();
|
|
255
|
+
}
|
|
182
256
|
|
|
183
257
|
/**
|
|
184
258
|
* Get the persisted user ID.
|
|
@@ -204,6 +278,84 @@ class PersistenceManager {
|
|
|
204
278
|
this.inMemoryUserId = userId;
|
|
205
279
|
}
|
|
206
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Get the anonymous ID (auto-generated UUID).
|
|
283
|
+
* Checks cookies first, then localStorage, then in-memory.
|
|
284
|
+
*/
|
|
285
|
+
getAnonymousId(): string | null {
|
|
286
|
+
// Try cookies first (for cross-subdomain support)
|
|
287
|
+
if (this.shouldUseCookies()) {
|
|
288
|
+
const cookieId = getCookie(ANONYMOUS_ID_KEY);
|
|
289
|
+
if (cookieId) {
|
|
290
|
+
return cookieId;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Fall back to localStorage
|
|
295
|
+
if (isLocalStorageAvailable()) {
|
|
296
|
+
return localStorage.getItem(ANONYMOUS_ID_KEY);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return this.inMemoryAnonymousId;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Set the anonymous ID (persists across sessions).
|
|
304
|
+
* Saves to both cookies and localStorage for redundancy.
|
|
305
|
+
*/
|
|
306
|
+
setAnonymousId(anonymousId: string): void {
|
|
307
|
+
// Save to cookies if enabled
|
|
308
|
+
if (this.shouldUseCookies()) {
|
|
309
|
+
setCookie(ANONYMOUS_ID_KEY, anonymousId, this.cookieDomain);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Also save to localStorage as fallback
|
|
313
|
+
if (isLocalStorageAvailable()) {
|
|
314
|
+
localStorage.setItem(ANONYMOUS_ID_KEY, anonymousId);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.inMemoryAnonymousId = anonymousId;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Initialize the anonymous ID. If an override is provided, use it.
|
|
322
|
+
* Otherwise, use existing persisted ID or generate a new UUID.
|
|
323
|
+
* @param overrideId Optional ID from wrapper SDK (e.g., React Native device ID)
|
|
324
|
+
* @param generateUUID Function to generate a UUID
|
|
325
|
+
*/
|
|
326
|
+
initializeAnonymousId(overrideId: string | undefined, generateUUID: () => string): string {
|
|
327
|
+
// If wrapper SDK provides an override, always use it
|
|
328
|
+
if (overrideId) {
|
|
329
|
+
this.setAnonymousId(overrideId);
|
|
330
|
+
return overrideId;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check for existing persisted anonymous ID
|
|
334
|
+
const existingId = this.getAnonymousId();
|
|
335
|
+
if (existingId) {
|
|
336
|
+
// Ensure it's saved to cookies if we have cookie support now
|
|
337
|
+
if (this.shouldUseCookies() && !getCookie(ANONYMOUS_ID_KEY)) {
|
|
338
|
+
setCookie(ANONYMOUS_ID_KEY, existingId, this.cookieDomain);
|
|
339
|
+
}
|
|
340
|
+
return existingId;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Generate and persist a new anonymous ID
|
|
344
|
+
const newId = generateUUID();
|
|
345
|
+
this.setAnonymousId(newId);
|
|
346
|
+
return newId;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Reset the anonymous ID to a new UUID.
|
|
351
|
+
* @param generateUUID Function to generate a UUID
|
|
352
|
+
*/
|
|
353
|
+
resetAnonymousId(generateUUID: () => string): string {
|
|
354
|
+
const newId = generateUUID();
|
|
355
|
+
this.setAnonymousId(newId);
|
|
356
|
+
return newId;
|
|
357
|
+
}
|
|
358
|
+
|
|
207
359
|
/**
|
|
208
360
|
* Get the persisted app version (for detecting updates).
|
|
209
361
|
*/
|
package/src/types.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface MGMConfiguration {
|
|
|
50
50
|
/**
|
|
51
51
|
* Whether to automatically track app lifecycle events
|
|
52
52
|
* ($app_opened, $app_backgrounded, $app_installed, $app_updated).
|
|
53
|
-
* @default
|
|
53
|
+
* @default false
|
|
54
54
|
*/
|
|
55
55
|
trackAppLifecycleEvents?: boolean;
|
|
56
56
|
|
|
@@ -91,6 +91,28 @@ export interface MGMConfiguration {
|
|
|
91
91
|
*/
|
|
92
92
|
sdkVersion?: string;
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Override the auto-generated anonymous user ID.
|
|
96
|
+
* Wrapper SDKs (e.g., React Native) can pass a device-specific ID here.
|
|
97
|
+
* If not provided, a UUID will be auto-generated and persisted.
|
|
98
|
+
*/
|
|
99
|
+
anonymousId?: string;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Cookie domain for cross-subdomain tracking.
|
|
103
|
+
* Set to '.yourdomain.com' to share anonymous ID across subdomains.
|
|
104
|
+
* Example: '.example.com' allows sharing between app.example.com and www.example.com
|
|
105
|
+
*/
|
|
106
|
+
cookieDomain?: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Disable cookie storage entirely.
|
|
110
|
+
* When true, only localStorage will be used (no cross-subdomain tracking).
|
|
111
|
+
* Useful for GDPR compliance or privacy-focused applications.
|
|
112
|
+
* @default false
|
|
113
|
+
*/
|
|
114
|
+
disableCookies?: boolean;
|
|
115
|
+
|
|
94
116
|
/**
|
|
95
117
|
* Custom storage adapter. If not provided, uses localStorage in browsers
|
|
96
118
|
* or in-memory storage in non-browser environments.
|
|
@@ -101,16 +123,27 @@ export interface MGMConfiguration {
|
|
|
101
123
|
* Custom network client. If not provided, uses fetch-based client.
|
|
102
124
|
*/
|
|
103
125
|
networkClient?: INetworkClient;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Callback invoked when a network error occurs.
|
|
129
|
+
* Useful for reporting errors to external services like Sentry.
|
|
130
|
+
* @param error The error that occurred
|
|
131
|
+
*/
|
|
132
|
+
onError?: (error: MGMError) => void;
|
|
104
133
|
}
|
|
105
134
|
|
|
106
135
|
/**
|
|
107
136
|
* Internal resolved configuration with all defaults applied.
|
|
108
137
|
*/
|
|
109
138
|
export interface ResolvedConfiguration extends Required<
|
|
110
|
-
Omit<
|
|
139
|
+
Omit<
|
|
140
|
+
MGMConfiguration,
|
|
141
|
+
'storage' | 'networkClient' | 'onError' | 'anonymousId' | 'cookieDomain' | 'disableCookies'
|
|
142
|
+
>
|
|
111
143
|
> {
|
|
112
144
|
storage?: IEventStorage;
|
|
113
145
|
networkClient?: INetworkClient;
|
|
146
|
+
onError?: (error: MGMError) => void;
|
|
114
147
|
}
|
|
115
148
|
|
|
116
149
|
/**
|
|
@@ -152,13 +185,15 @@ export interface MGMEvent {
|
|
|
152
185
|
|
|
153
186
|
/**
|
|
154
187
|
* The user ID associated with this event.
|
|
188
|
+
* Uses identified user if set, otherwise falls back to anonymous UUID.
|
|
155
189
|
*/
|
|
156
|
-
|
|
190
|
+
|
|
191
|
+
user_id: string;
|
|
157
192
|
|
|
158
193
|
/**
|
|
159
194
|
* The session ID associated with this event.
|
|
160
195
|
*/
|
|
161
|
-
|
|
196
|
+
session_id?: string;
|
|
162
197
|
|
|
163
198
|
/**
|
|
164
199
|
* The platform this event was generated from.
|
|
@@ -168,17 +203,17 @@ export interface MGMEvent {
|
|
|
168
203
|
/**
|
|
169
204
|
* The app version string.
|
|
170
205
|
*/
|
|
171
|
-
|
|
206
|
+
app_version?: string;
|
|
172
207
|
|
|
173
208
|
/**
|
|
174
209
|
* The app build number (separate from version).
|
|
175
210
|
*/
|
|
176
|
-
|
|
211
|
+
app_build_number?: string;
|
|
177
212
|
|
|
178
213
|
/**
|
|
179
214
|
* The OS version string.
|
|
180
215
|
*/
|
|
181
|
-
|
|
216
|
+
os_version?: string;
|
|
182
217
|
|
|
183
218
|
/**
|
|
184
219
|
* The environment name.
|
|
@@ -188,7 +223,7 @@ export interface MGMEvent {
|
|
|
188
223
|
/**
|
|
189
224
|
* The device manufacturer (e.g., "Apple", "Samsung").
|
|
190
225
|
*/
|
|
191
|
-
|
|
226
|
+
device_manufacturer?: string;
|
|
192
227
|
|
|
193
228
|
/**
|
|
194
229
|
* The user's locale (e.g., "en-US").
|
|
@@ -212,13 +247,15 @@ export interface MGMEvent {
|
|
|
212
247
|
*/
|
|
213
248
|
export interface MGMEventContext {
|
|
214
249
|
platform: Platform;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
250
|
+
app_version?: string;
|
|
251
|
+
app_build_number?: string;
|
|
252
|
+
os_version?: string;
|
|
253
|
+
|
|
254
|
+
user_id: string;
|
|
255
|
+
|
|
256
|
+
session_id?: string;
|
|
220
257
|
environment: string;
|
|
221
|
-
|
|
258
|
+
device_manufacturer?: string;
|
|
222
259
|
locale?: string;
|
|
223
260
|
timezone?: string;
|
|
224
261
|
}
|
|
@@ -375,7 +412,7 @@ export const DefaultConfiguration = {
|
|
|
375
412
|
flushInterval: 30,
|
|
376
413
|
maxStoredEvents: 10000,
|
|
377
414
|
enableDebugLogging: false,
|
|
378
|
-
trackAppLifecycleEvents:
|
|
415
|
+
trackAppLifecycleEvents: false,
|
|
379
416
|
} as const;
|
|
380
417
|
|
|
381
418
|
/**
|
package/src/utils.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
generateAnonymousId,
|
|
2
3
|
generateUUID,
|
|
3
4
|
getISOTimestamp,
|
|
4
5
|
isValidEventName,
|
|
@@ -26,6 +27,26 @@ describe('generateUUID', () => {
|
|
|
26
27
|
});
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
describe('generateAnonymousId', () => {
|
|
31
|
+
it('should generate an ID with $anon_ prefix', () => {
|
|
32
|
+
const id = generateAnonymousId();
|
|
33
|
+
expect(id).toMatch(/^\$anon_[a-z0-9]{12}$/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should generate unique IDs', () => {
|
|
37
|
+
const ids = new Set<string>();
|
|
38
|
+
for (let i = 0; i < 100; i++) {
|
|
39
|
+
ids.add(generateAnonymousId());
|
|
40
|
+
}
|
|
41
|
+
expect(ids.size).toBe(100);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should be 18 characters total ($anon_ + 12 random)', () => {
|
|
45
|
+
const id = generateAnonymousId();
|
|
46
|
+
expect(id.length).toBe(18);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
29
50
|
describe('getISOTimestamp', () => {
|
|
30
51
|
it('should return a valid ISO8601 timestamp', () => {
|
|
31
52
|
const timestamp = getISOTimestamp();
|
package/src/utils.ts
CHANGED
|
@@ -29,6 +29,35 @@ export function generateUUID(): string {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Generate a short random string for anonymous IDs.
|
|
34
|
+
* Uses base36 (0-9, a-z) for URL-safe, readable IDs.
|
|
35
|
+
*/
|
|
36
|
+
function generateRandomString(length: number): string {
|
|
37
|
+
const chars = '0123456789abcdefghijklmnopqrstuvwxyz';
|
|
38
|
+
let result = '';
|
|
39
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
40
|
+
const array = new Uint8Array(length);
|
|
41
|
+
crypto.getRandomValues(array);
|
|
42
|
+
for (let i = 0; i < length; i++) {
|
|
43
|
+
result += chars[array[i] % chars.length];
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
for (let i = 0; i < length; i++) {
|
|
47
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate an anonymous user ID with $anon_ prefix.
|
|
55
|
+
* Format: $anon_xxxxxxxxxxxx (12 random chars)
|
|
56
|
+
*/
|
|
57
|
+
export function generateAnonymousId(): string {
|
|
58
|
+
return `$anon_${generateRandomString(12)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
/**
|
|
33
62
|
* Get the current timestamp in ISO8601 format.
|
|
34
63
|
*/
|
|
@@ -183,6 +212,7 @@ export function resolveConfiguration(config: MGMConfiguration): ResolvedConfigur
|
|
|
183
212
|
sdkVersion: config.sdkVersion ?? '',
|
|
184
213
|
storage: config.storage,
|
|
185
214
|
networkClient: config.networkClient,
|
|
215
|
+
onError: config.onError,
|
|
186
216
|
};
|
|
187
217
|
}
|
|
188
218
|
|