@mostly-good-metrics/javascript 0.4.4 → 0.5.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.
@@ -258,7 +258,7 @@ describe('MostlyGoodMetrics', () => {
258
258
  await new Promise((resolve) => setTimeout(resolve, 10));
259
259
 
260
260
  const events = await storage.fetchEvents(1);
261
- expect(events[0].userId).toBe('user_456');
261
+ expect(events[0].user_id).toBe('user_456');
262
262
  });
263
263
 
264
264
  it('should not set empty userId', () => {
@@ -266,6 +266,97 @@ describe('MostlyGoodMetrics', () => {
266
266
  MostlyGoodMetrics.identify('');
267
267
  expect(MostlyGoodMetrics.shared?.userId).toBe('user_123');
268
268
  });
269
+
270
+ it('should send $identify event with email', async () => {
271
+ MostlyGoodMetrics.resetIdentity(); // Clear any previous identify state
272
+ MostlyGoodMetrics.identify('user_123', { email: 'test@example.com' });
273
+
274
+ await new Promise((resolve) => setTimeout(resolve, 10));
275
+
276
+ const events = await storage.fetchEvents(10);
277
+ const identifyEvent = events.find((e) => e.name === '$identify');
278
+ expect(identifyEvent).toBeDefined();
279
+ expect(identifyEvent?.properties?.email).toBe('test@example.com');
280
+ expect(identifyEvent?.properties?.name).toBeUndefined();
281
+ });
282
+
283
+ it('should send $identify event with name', async () => {
284
+ MostlyGoodMetrics.resetIdentity(); // Clear any previous identify state
285
+ MostlyGoodMetrics.identify('user_123', { name: 'John Doe' });
286
+
287
+ await new Promise((resolve) => setTimeout(resolve, 10));
288
+
289
+ const events = await storage.fetchEvents(10);
290
+ const identifyEvent = events.find((e) => e.name === '$identify');
291
+ expect(identifyEvent).toBeDefined();
292
+ expect(identifyEvent?.properties?.name).toBe('John Doe');
293
+ expect(identifyEvent?.properties?.email).toBeUndefined();
294
+ });
295
+
296
+ it('should send $identify event with both email and name', async () => {
297
+ MostlyGoodMetrics.resetIdentity(); // Clear any previous identify state
298
+ MostlyGoodMetrics.identify('user_123', { email: 'test@example.com', name: 'John Doe' });
299
+
300
+ await new Promise((resolve) => setTimeout(resolve, 10));
301
+
302
+ const events = await storage.fetchEvents(10);
303
+ const identifyEvent = events.find((e) => e.name === '$identify');
304
+ expect(identifyEvent).toBeDefined();
305
+ expect(identifyEvent?.properties?.email).toBe('test@example.com');
306
+ expect(identifyEvent?.properties?.name).toBe('John Doe');
307
+ });
308
+
309
+ it('should not send $identify event without profile data', async () => {
310
+ MostlyGoodMetrics.resetIdentity(); // Clear any previous identify state
311
+ MostlyGoodMetrics.identify('user_123');
312
+
313
+ await new Promise((resolve) => setTimeout(resolve, 10));
314
+
315
+ const events = await storage.fetchEvents(10);
316
+ const identifyEvent = events.find((e) => e.name === '$identify');
317
+ expect(identifyEvent).toBeUndefined();
318
+ });
319
+
320
+ it('should debounce $identify event with same profile data', async () => {
321
+ MostlyGoodMetrics.resetIdentity(); // Clear any previous identify state
322
+
323
+ MostlyGoodMetrics.identify('user_123', { email: 'debounce-test@example.com' });
324
+ await new Promise((resolve) => setTimeout(resolve, 10));
325
+
326
+ // Call again with same data - should not send another event
327
+ MostlyGoodMetrics.identify('user_123', { email: 'debounce-test@example.com' });
328
+ await new Promise((resolve) => setTimeout(resolve, 10));
329
+
330
+ const events = await storage.fetchEvents(10);
331
+ const identifyEvents = events.filter((e) => e.name === '$identify');
332
+ expect(identifyEvents.length).toBe(1);
333
+ });
334
+
335
+ it('should send new $identify event when profile data changes', async () => {
336
+ MostlyGoodMetrics.resetIdentity(); // Clear any previous identify state
337
+
338
+ MostlyGoodMetrics.identify('user_123', { email: 'change-test@example.com' });
339
+ await new Promise((resolve) => setTimeout(resolve, 10));
340
+
341
+ // Call with different email - should send new event
342
+ MostlyGoodMetrics.identify('user_123', { email: 'changed@example.com' });
343
+ await new Promise((resolve) => setTimeout(resolve, 10));
344
+
345
+ const events = await storage.fetchEvents(10);
346
+ const identifyEvents = events.filter((e) => e.name === '$identify');
347
+ expect(identifyEvents.length).toBe(2);
348
+ });
349
+
350
+ it('should not send $identify event with empty profile', async () => {
351
+ MostlyGoodMetrics.resetIdentity(); // Clear any previous identify state
352
+ MostlyGoodMetrics.identify('user_123', {});
353
+
354
+ await new Promise((resolve) => setTimeout(resolve, 10));
355
+
356
+ const events = await storage.fetchEvents(10);
357
+ const identifyEvent = events.find((e) => e.name === '$identify');
358
+ expect(identifyEvent).toBeUndefined();
359
+ });
269
360
  });
270
361
 
271
362
  describe('resetIdentity', () => {
@@ -283,6 +374,86 @@ describe('MostlyGoodMetrics', () => {
283
374
  MostlyGoodMetrics.resetIdentity();
284
375
  expect(MostlyGoodMetrics.shared?.userId).toBeNull();
285
376
  });
377
+
378
+ it('should keep anonymousId unchanged', () => {
379
+ const originalAnonymousId = MostlyGoodMetrics.shared?.anonymousId;
380
+ MostlyGoodMetrics.resetIdentity();
381
+ const newAnonymousId = MostlyGoodMetrics.shared?.anonymousId;
382
+ expect(newAnonymousId).toBe(originalAnonymousId);
383
+ });
384
+ });
385
+
386
+ describe('anonymousId', () => {
387
+ it('should auto-generate anonymousId on init', () => {
388
+ MostlyGoodMetrics.configure({
389
+ apiKey: 'test-key',
390
+ storage,
391
+ networkClient,
392
+ trackAppLifecycleEvents: false,
393
+ });
394
+ expect(MostlyGoodMetrics.shared?.anonymousId).toBeDefined();
395
+ expect(MostlyGoodMetrics.shared?.anonymousId.length).toBeGreaterThan(0);
396
+ });
397
+
398
+ it('should include anonymousId as user_id in events when not identified', async () => {
399
+ MostlyGoodMetrics.configure({
400
+ apiKey: 'test-key',
401
+ storage,
402
+ networkClient,
403
+ trackAppLifecycleEvents: false,
404
+ });
405
+ MostlyGoodMetrics.track('test_event');
406
+
407
+ await new Promise((resolve) => setTimeout(resolve, 10));
408
+
409
+ const events = await storage.fetchEvents(1);
410
+ expect(events[0].user_id).toBe(MostlyGoodMetrics.shared?.anonymousId);
411
+ });
412
+
413
+ it('should allow wrapper SDK to override anonymousId', () => {
414
+ const customAnonymousId = 'react-native-device-id-12345';
415
+ MostlyGoodMetrics.configure({
416
+ apiKey: 'test-key',
417
+ storage,
418
+ networkClient,
419
+ trackAppLifecycleEvents: false,
420
+ anonymousId: customAnonymousId,
421
+ });
422
+ expect(MostlyGoodMetrics.shared?.anonymousId).toBe(customAnonymousId);
423
+ });
424
+
425
+ it('should include custom anonymousId as user_id in events', async () => {
426
+ const customAnonymousId = 'react-native-device-id-12345';
427
+ MostlyGoodMetrics.configure({
428
+ apiKey: 'test-key',
429
+ storage,
430
+ networkClient,
431
+ trackAppLifecycleEvents: false,
432
+ anonymousId: customAnonymousId,
433
+ });
434
+ MostlyGoodMetrics.track('test_event');
435
+
436
+ await new Promise((resolve) => setTimeout(resolve, 10));
437
+
438
+ const events = await storage.fetchEvents(1);
439
+ expect(events[0].user_id).toBe(customAnonymousId);
440
+ });
441
+
442
+ it('should use identified userId over anonymousId', async () => {
443
+ MostlyGoodMetrics.configure({
444
+ apiKey: 'test-key',
445
+ storage,
446
+ networkClient,
447
+ trackAppLifecycleEvents: false,
448
+ });
449
+ MostlyGoodMetrics.identify('identified_user_123');
450
+ MostlyGoodMetrics.track('test_event');
451
+
452
+ await new Promise((resolve) => setTimeout(resolve, 10));
453
+
454
+ const events = await storage.fetchEvents(1);
455
+ expect(events[0].user_id).toBe('identified_user_123');
456
+ });
286
457
  });
287
458
 
288
459
  describe('flush', () => {
package/src/client.ts CHANGED
@@ -12,10 +12,12 @@ import {
12
12
  ResolvedConfiguration,
13
13
  SystemEvents,
14
14
  SystemProperties,
15
+ UserProfile,
15
16
  } from './types';
16
17
  import {
17
18
  delay,
18
19
  detectDeviceType,
20
+ generateAnonymousId,
19
21
  generateUUID,
20
22
  getDeviceModel,
21
23
  getISOTimestamp,
@@ -42,6 +44,7 @@ export class MostlyGoodMetrics {
42
44
  private flushTimer: ReturnType<typeof setInterval> | null = null;
43
45
  private isFlushingInternal = false;
44
46
  private sessionIdValue: string;
47
+ private anonymousIdValue: string;
45
48
  private lifecycleSetup = false;
46
49
 
47
50
  /**
@@ -51,6 +54,13 @@ export class MostlyGoodMetrics {
51
54
  this.config = resolveConfiguration(config);
52
55
  this.sessionIdValue = generateUUID();
53
56
 
57
+ // Configure cookie settings before initializing anonymous ID
58
+ persistence.configureCookies(config.cookieDomain, config.disableCookies);
59
+ this.anonymousIdValue = persistence.initializeAnonymousId(
60
+ config.anonymousId,
61
+ generateAnonymousId
62
+ );
63
+
54
64
  // Set up logging
55
65
  setDebugLogging(this.config.enableDebugLogging);
56
66
 
@@ -113,9 +123,9 @@ export class MostlyGoodMetrics {
113
123
  }
114
124
  }
115
125
 
116
- // ============================================================
126
+ // =====================================================
117
127
  // Static convenience methods (delegate to shared instance)
118
- // ============================================================
128
+ // =====================================================
119
129
 
120
130
  /**
121
131
  * Track an event with the given name and optional properties.
@@ -125,10 +135,12 @@ export class MostlyGoodMetrics {
125
135
  }
126
136
 
127
137
  /**
128
- * Identify the current user.
138
+ * Identify the current user with optional profile data.
139
+ * @param userId The user's unique identifier
140
+ * @param profile Optional profile data (email, name)
129
141
  */
130
- static identify(userId: string): void {
131
- MostlyGoodMetrics.instance?.identify(userId);
142
+ static identify(userId: string, profile?: UserProfile): void {
143
+ MostlyGoodMetrics.instance?.identify(userId, profile);
132
144
  }
133
145
 
134
146
  /**
@@ -201,9 +213,9 @@ export class MostlyGoodMetrics {
201
213
  return MostlyGoodMetrics.instance?.getSuperProperties() ?? {};
202
214
  }
203
215
 
204
- // ============================================================
216
+ // =====================================================
205
217
  // Instance properties
206
- // ============================================================
218
+ // =====================================================
207
219
 
208
220
  /**
209
221
  * Get the current user ID.
@@ -219,6 +231,13 @@ export class MostlyGoodMetrics {
219
231
  return this.sessionIdValue;
220
232
  }
221
233
 
234
+ /**
235
+ * Get the anonymous ID (auto-generated UUID, persisted across sessions).
236
+ */
237
+ get anonymousId(): string {
238
+ return this.anonymousIdValue;
239
+ }
240
+
222
241
  /**
223
242
  * Check if a flush operation is in progress.
224
243
  */
@@ -233,9 +252,9 @@ export class MostlyGoodMetrics {
233
252
  return { ...this.config };
234
253
  }
235
254
 
236
- // ============================================================
255
+ // =====================================================
237
256
  // Instance methods
238
- // ============================================================
257
+ // =====================================================
239
258
 
240
259
  /**
241
260
  * Track an event with the given name and optional properties.
@@ -265,11 +284,13 @@ export class MostlyGoodMetrics {
265
284
  name,
266
285
  client_event_id: generateUUID(),
267
286
  timestamp: getISOTimestamp(),
268
- userId: this.userId ?? undefined,
269
- sessionId: this.sessionIdValue,
287
+
288
+ user_id: this.userId ?? this.anonymousIdValue,
289
+
290
+ session_id: this.sessionIdValue,
270
291
  platform: this.config.platform,
271
- appVersion: this.config.appVersion || undefined,
272
- osVersion: this.config.osVersion || getOSVersion() || undefined,
292
+ app_version: this.config.appVersion || undefined,
293
+ os_version: this.config.osVersion || getOSVersion() || undefined,
273
294
  environment: this.config.environment,
274
295
  locale: getLocale(),
275
296
  timezone: getTimezone(),
@@ -288,9 +309,14 @@ export class MostlyGoodMetrics {
288
309
  }
289
310
 
290
311
  /**
291
- * Identify the current user.
312
+ * Identify the current user with optional profile data.
313
+ * Profile data is sent to the backend via the $identify event.
314
+ * Debouncing: only sends $identify if payload changed or >24h since last send.
315
+ *
316
+ * @param userId The user's unique identifier
317
+ * @param profile Optional profile data (email, name)
292
318
  */
293
- identify(userId: string): void {
319
+ identify(userId: string, profile?: UserProfile): void {
294
320
  if (!userId) {
295
321
  logger.warn('identify called with empty userId');
296
322
  return;
@@ -298,14 +324,78 @@ export class MostlyGoodMetrics {
298
324
 
299
325
  logger.debug(`Identifying user: ${userId}`);
300
326
  persistence.setUserId(userId);
327
+
328
+ // If profile data is provided, check if we should send $identify event
329
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional truthy check for non-empty strings
330
+ if (profile && (profile.email || profile.name)) {
331
+ this.sendIdentifyEventIfNeeded(userId, profile);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Send $identify event if debounce conditions are met.
337
+ * Only sends if: hash changed OR more than 24 hours since last send.
338
+ */
339
+ private sendIdentifyEventIfNeeded(userId: string, profile: UserProfile): void {
340
+ // Compute hash of the identify payload
341
+ const payloadString = JSON.stringify({ userId, email: profile.email, name: profile.name });
342
+ const currentHash = this.simpleHash(payloadString);
343
+
344
+ const storedHash = persistence.getIdentifyHash();
345
+ const lastSentAt = persistence.getIdentifyLastSentAt();
346
+ const now = Date.now();
347
+ const twentyFourHoursMs = 24 * 60 * 60 * 1000;
348
+
349
+ const hashChanged = storedHash !== currentHash;
350
+ const expiredTime = !lastSentAt || now - lastSentAt > twentyFourHoursMs;
351
+
352
+ if (hashChanged || expiredTime) {
353
+ logger.debug(
354
+ `Sending $identify event (hashChanged=${hashChanged}, expiredTime=${expiredTime})`
355
+ );
356
+
357
+ // Build properties object with only defined values
358
+ const properties: EventProperties = {};
359
+ if (profile.email) {
360
+ properties.email = profile.email;
361
+ }
362
+ if (profile.name) {
363
+ properties.name = profile.name;
364
+ }
365
+
366
+ // Track the $identify event
367
+ this.track(SystemEvents.IDENTIFY, properties);
368
+
369
+ // Update stored hash and timestamp
370
+ persistence.setIdentifyHash(currentHash);
371
+ persistence.setIdentifyLastSentAt(now);
372
+ } else {
373
+ logger.debug('Skipping $identify event (debounced)');
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Simple hash function for debouncing.
379
+ * Uses a basic string hash - not cryptographic, just for comparison.
380
+ */
381
+ private simpleHash(str: string): string {
382
+ let hash = 0;
383
+ for (let i = 0; i < str.length; i++) {
384
+ const char = str.charCodeAt(i);
385
+ hash = (hash << 5) - hash + char;
386
+ hash = hash & hash; // Convert to 32-bit integer
387
+ }
388
+ return hash.toString(16);
301
389
  }
302
390
 
303
391
  /**
304
392
  * Reset user identity.
393
+ * Clears the user ID and identify debounce state.
305
394
  */
306
395
  resetIdentity(): void {
307
396
  logger.debug('Resetting user identity');
308
397
  persistence.setUserId(null);
398
+ persistence.clearIdentifyState();
309
399
  }
310
400
 
311
401
  /**
@@ -398,9 +488,9 @@ export class MostlyGoodMetrics {
398
488
  logger.debug('MostlyGoodMetrics instance destroyed');
399
489
  }
400
490
 
401
- // ============================================================
491
+ // =====================================================
402
492
  // Private methods
403
- // ============================================================
493
+ // =====================================================
404
494
 
405
495
  private async checkBatchSize(): Promise<void> {
406
496
  const count = await this.storage.eventCount();
@@ -466,10 +556,12 @@ export class MostlyGoodMetrics {
466
556
  private buildPayload(events: MGMEvent[]): MGMEventsPayload {
467
557
  const context: MGMEventContext = {
468
558
  platform: this.config.platform,
469
- appVersion: this.config.appVersion || undefined,
470
- osVersion: this.config.osVersion || getOSVersion() || undefined,
471
- userId: this.userId ?? undefined,
472
- sessionId: this.sessionIdValue,
559
+ app_version: this.config.appVersion || undefined,
560
+ os_version: this.config.osVersion || getOSVersion() || undefined,
561
+
562
+ user_id: this.userId ?? this.anonymousIdValue,
563
+
564
+ session_id: this.sessionIdValue,
473
565
  environment: this.config.environment,
474
566
  locale: getLocale(),
475
567
  timezone: getTimezone(),
package/src/index.ts CHANGED
@@ -19,8 +19,11 @@
19
19
  * page: '/checkout',
20
20
  * });
21
21
  *
22
- * // Identify users
23
- * MostlyGoodMetrics.identify('user_123');
22
+ * // Identify users with profile data
23
+ * MostlyGoodMetrics.identify('user_123', {
24
+ * email: 'user@example.com',
25
+ * name: 'Jane Doe',
26
+ * });
24
27
  * ```
25
28
  */
26
29
 
@@ -43,6 +46,7 @@ export type {
43
46
  SendResult,
44
47
  IEventStorage,
45
48
  INetworkClient,
49
+ UserProfile,
46
50
  } from './types';
47
51
 
48
52
  // Error class
@@ -65,6 +69,7 @@ export { FetchNetworkClient, createDefaultNetworkClient } from './network';
65
69
 
66
70
  // Utilities (for advanced usage)
67
71
  export {
72
+ generateAnonymousId,
68
73
  generateUUID,
69
74
  getISOTimestamp,
70
75
  isValidEventName,
@@ -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
+ });