@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.
- package/README.md +50 -4
- package/dist/cjs/client.js +91 -21
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/index.js +7 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/storage.js +185 -1
- package/dist/cjs/storage.js.map +1 -1
- package/dist/cjs/types.js +2 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/cjs/utils.js +29 -0
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/client.js +92 -22
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.js +6 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/storage.js +185 -1
- package/dist/esm/storage.js.map +1 -1
- package/dist/esm/types.js +2 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils.js +28 -0
- package/dist/esm/utils.js.map +1 -1
- package/dist/types/client.d.ts +28 -5
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/index.d.ts +7 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/storage.d.ts +56 -1
- package/dist/types/storage.d.ts.map +1 -1
- package/dist/types/types.d.ts +49 -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 +172 -1
- package/src/client.ts +113 -21
- package/src/index.ts +7 -2
- package/src/storage.test.ts +126 -0
- package/src/storage.ts +204 -1
- package/src/types.ts +60 -15
- package/src/utils.test.ts +21 -0
- package/src/utils.ts +29 -0
package/src/storage.ts
CHANGED
|
@@ -3,8 +3,11 @@ 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';
|
|
9
|
+
const IDENTIFY_HASH_KEY = 'mostlygoodmetrics_identify_hash';
|
|
10
|
+
const IDENTIFY_TIMESTAMP_KEY = 'mostlygoodmetrics_identify_timestamp';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Check if we're running in a browser environment with localStorage available.
|
|
@@ -23,6 +26,59 @@ function isLocalStorageAvailable(): boolean {
|
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Check if cookies are available in this environment.
|
|
31
|
+
*/
|
|
32
|
+
function isCookieAvailable(): boolean {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof document === 'undefined' || typeof document.cookie === 'undefined') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
// Test if we can actually set a cookie
|
|
38
|
+
const testKey = '__mgm_cookie_test__';
|
|
39
|
+
document.cookie = `${testKey}=test; path=/; max-age=60`;
|
|
40
|
+
const hasTest = document.cookie.indexOf(testKey) !== -1;
|
|
41
|
+
// Clean up test cookie
|
|
42
|
+
document.cookie = `${testKey}=; path=/; max-age=0`;
|
|
43
|
+
return hasTest;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a cookie value by name.
|
|
51
|
+
*/
|
|
52
|
+
function getCookie(name: string): string | null {
|
|
53
|
+
if (!isCookieAvailable()) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const cookies = document.cookie.split(';');
|
|
57
|
+
for (const cookie of cookies) {
|
|
58
|
+
const [cookieName, cookieValue] = cookie.trim().split('=');
|
|
59
|
+
if (cookieName === name) {
|
|
60
|
+
return decodeURIComponent(cookieValue);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set a cookie with optional domain for cross-subdomain support.
|
|
68
|
+
* Uses a 1-year expiry by default.
|
|
69
|
+
*/
|
|
70
|
+
function setCookie(name: string, value: string, domain?: string): void {
|
|
71
|
+
if (!isCookieAvailable()) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const maxAge = 365 * 24 * 60 * 60; // 1 year in seconds
|
|
75
|
+
let cookieString = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
|
|
76
|
+
if (domain) {
|
|
77
|
+
cookieString += `; domain=${domain}`;
|
|
78
|
+
}
|
|
79
|
+
document.cookie = cookieString;
|
|
80
|
+
}
|
|
81
|
+
|
|
26
82
|
/**
|
|
27
83
|
* In-memory event storage implementation.
|
|
28
84
|
* Used as a fallback when localStorage is not available,
|
|
@@ -173,12 +229,32 @@ export function createDefaultStorage(maxEvents: number): IEventStorage {
|
|
|
173
229
|
|
|
174
230
|
/**
|
|
175
231
|
* Persistence helpers for user ID and app version.
|
|
176
|
-
*
|
|
232
|
+
* Uses cookies first (for cross-subdomain support), then localStorage as fallback.
|
|
177
233
|
*/
|
|
178
234
|
class PersistenceManager {
|
|
179
235
|
private inMemoryUserId: string | null = null;
|
|
236
|
+
private inMemoryAnonymousId: string | null = null;
|
|
180
237
|
private inMemoryAppVersion: string | null = null;
|
|
181
238
|
private inMemorySuperProperties: EventProperties = {};
|
|
239
|
+
private cookieDomain: string | undefined = undefined;
|
|
240
|
+
private disableCookies = false;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Configure cookie settings.
|
|
244
|
+
* @param cookieDomain Domain for cross-subdomain cookies (e.g., '.example.com')
|
|
245
|
+
* @param disableCookies If true, only use localStorage (no cookies)
|
|
246
|
+
*/
|
|
247
|
+
configureCookies(cookieDomain?: string, disableCookies?: boolean): void {
|
|
248
|
+
this.cookieDomain = cookieDomain;
|
|
249
|
+
this.disableCookies = disableCookies ?? false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if cookies should be used.
|
|
254
|
+
*/
|
|
255
|
+
private shouldUseCookies(): boolean {
|
|
256
|
+
return !this.disableCookies && isCookieAvailable();
|
|
257
|
+
}
|
|
182
258
|
|
|
183
259
|
/**
|
|
184
260
|
* Get the persisted user ID.
|
|
@@ -204,6 +280,84 @@ class PersistenceManager {
|
|
|
204
280
|
this.inMemoryUserId = userId;
|
|
205
281
|
}
|
|
206
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Get the anonymous ID (auto-generated UUID).
|
|
285
|
+
* Checks cookies first, then localStorage, then in-memory.
|
|
286
|
+
*/
|
|
287
|
+
getAnonymousId(): string | null {
|
|
288
|
+
// Try cookies first (for cross-subdomain support)
|
|
289
|
+
if (this.shouldUseCookies()) {
|
|
290
|
+
const cookieId = getCookie(ANONYMOUS_ID_KEY);
|
|
291
|
+
if (cookieId) {
|
|
292
|
+
return cookieId;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Fall back to localStorage
|
|
297
|
+
if (isLocalStorageAvailable()) {
|
|
298
|
+
return localStorage.getItem(ANONYMOUS_ID_KEY);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return this.inMemoryAnonymousId;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Set the anonymous ID (persists across sessions).
|
|
306
|
+
* Saves to both cookies and localStorage for redundancy.
|
|
307
|
+
*/
|
|
308
|
+
setAnonymousId(anonymousId: string): void {
|
|
309
|
+
// Save to cookies if enabled
|
|
310
|
+
if (this.shouldUseCookies()) {
|
|
311
|
+
setCookie(ANONYMOUS_ID_KEY, anonymousId, this.cookieDomain);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Also save to localStorage as fallback
|
|
315
|
+
if (isLocalStorageAvailable()) {
|
|
316
|
+
localStorage.setItem(ANONYMOUS_ID_KEY, anonymousId);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.inMemoryAnonymousId = anonymousId;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Initialize the anonymous ID. If an override is provided, use it.
|
|
324
|
+
* Otherwise, use existing persisted ID or generate a new UUID.
|
|
325
|
+
* @param overrideId Optional ID from wrapper SDK (e.g., React Native device ID)
|
|
326
|
+
* @param generateUUID Function to generate a UUID
|
|
327
|
+
*/
|
|
328
|
+
initializeAnonymousId(overrideId: string | undefined, generateUUID: () => string): string {
|
|
329
|
+
// If wrapper SDK provides an override, always use it
|
|
330
|
+
if (overrideId) {
|
|
331
|
+
this.setAnonymousId(overrideId);
|
|
332
|
+
return overrideId;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Check for existing persisted anonymous ID
|
|
336
|
+
const existingId = this.getAnonymousId();
|
|
337
|
+
if (existingId) {
|
|
338
|
+
// Ensure it's saved to cookies if we have cookie support now
|
|
339
|
+
if (this.shouldUseCookies() && !getCookie(ANONYMOUS_ID_KEY)) {
|
|
340
|
+
setCookie(ANONYMOUS_ID_KEY, existingId, this.cookieDomain);
|
|
341
|
+
}
|
|
342
|
+
return existingId;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Generate and persist a new anonymous ID
|
|
346
|
+
const newId = generateUUID();
|
|
347
|
+
this.setAnonymousId(newId);
|
|
348
|
+
return newId;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Reset the anonymous ID to a new UUID.
|
|
353
|
+
* @param generateUUID Function to generate a UUID
|
|
354
|
+
*/
|
|
355
|
+
resetAnonymousId(generateUUID: () => string): string {
|
|
356
|
+
const newId = generateUUID();
|
|
357
|
+
this.setAnonymousId(newId);
|
|
358
|
+
return newId;
|
|
359
|
+
}
|
|
360
|
+
|
|
207
361
|
/**
|
|
208
362
|
* Get the persisted app version (for detecting updates).
|
|
209
363
|
*/
|
|
@@ -309,6 +463,55 @@ class PersistenceManager {
|
|
|
309
463
|
}
|
|
310
464
|
}
|
|
311
465
|
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get the stored identify hash (for debouncing).
|
|
469
|
+
*/
|
|
470
|
+
getIdentifyHash(): string | null {
|
|
471
|
+
if (isLocalStorageAvailable()) {
|
|
472
|
+
return localStorage.getItem(IDENTIFY_HASH_KEY);
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Set the identify hash.
|
|
479
|
+
*/
|
|
480
|
+
setIdentifyHash(hash: string): void {
|
|
481
|
+
if (isLocalStorageAvailable()) {
|
|
482
|
+
localStorage.setItem(IDENTIFY_HASH_KEY, hash);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Get the timestamp of the last identify event sent.
|
|
488
|
+
*/
|
|
489
|
+
getIdentifyLastSentAt(): number | null {
|
|
490
|
+
if (isLocalStorageAvailable()) {
|
|
491
|
+
const timestamp = localStorage.getItem(IDENTIFY_TIMESTAMP_KEY);
|
|
492
|
+
return timestamp ? parseInt(timestamp, 10) : null;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Set the timestamp of the last identify event sent.
|
|
499
|
+
*/
|
|
500
|
+
setIdentifyLastSentAt(timestamp: number): void {
|
|
501
|
+
if (isLocalStorageAvailable()) {
|
|
502
|
+
localStorage.setItem(IDENTIFY_TIMESTAMP_KEY, timestamp.toString());
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Clear identify debounce state (used when resetting identity).
|
|
508
|
+
*/
|
|
509
|
+
clearIdentifyState(): void {
|
|
510
|
+
if (isLocalStorageAvailable()) {
|
|
511
|
+
localStorage.removeItem(IDENTIFY_HASH_KEY);
|
|
512
|
+
localStorage.removeItem(IDENTIFY_TIMESTAMP_KEY);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
312
515
|
}
|
|
313
516
|
|
|
314
517
|
export const persistence = new PersistenceManager();
|
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.
|
|
@@ -114,7 +136,10 @@ export interface MGMConfiguration {
|
|
|
114
136
|
* Internal resolved configuration with all defaults applied.
|
|
115
137
|
*/
|
|
116
138
|
export interface ResolvedConfiguration extends Required<
|
|
117
|
-
Omit<
|
|
139
|
+
Omit<
|
|
140
|
+
MGMConfiguration,
|
|
141
|
+
'storage' | 'networkClient' | 'onError' | 'anonymousId' | 'cookieDomain' | 'disableCookies'
|
|
142
|
+
>
|
|
118
143
|
> {
|
|
119
144
|
storage?: IEventStorage;
|
|
120
145
|
networkClient?: INetworkClient;
|
|
@@ -160,13 +185,15 @@ export interface MGMEvent {
|
|
|
160
185
|
|
|
161
186
|
/**
|
|
162
187
|
* The user ID associated with this event.
|
|
188
|
+
* Uses identified user if set, otherwise falls back to anonymous UUID.
|
|
163
189
|
*/
|
|
164
|
-
|
|
190
|
+
|
|
191
|
+
user_id: string;
|
|
165
192
|
|
|
166
193
|
/**
|
|
167
194
|
* The session ID associated with this event.
|
|
168
195
|
*/
|
|
169
|
-
|
|
196
|
+
session_id?: string;
|
|
170
197
|
|
|
171
198
|
/**
|
|
172
199
|
* The platform this event was generated from.
|
|
@@ -176,17 +203,17 @@ export interface MGMEvent {
|
|
|
176
203
|
/**
|
|
177
204
|
* The app version string.
|
|
178
205
|
*/
|
|
179
|
-
|
|
206
|
+
app_version?: string;
|
|
180
207
|
|
|
181
208
|
/**
|
|
182
209
|
* The app build number (separate from version).
|
|
183
210
|
*/
|
|
184
|
-
|
|
211
|
+
app_build_number?: string;
|
|
185
212
|
|
|
186
213
|
/**
|
|
187
214
|
* The OS version string.
|
|
188
215
|
*/
|
|
189
|
-
|
|
216
|
+
os_version?: string;
|
|
190
217
|
|
|
191
218
|
/**
|
|
192
219
|
* The environment name.
|
|
@@ -196,7 +223,7 @@ export interface MGMEvent {
|
|
|
196
223
|
/**
|
|
197
224
|
* The device manufacturer (e.g., "Apple", "Samsung").
|
|
198
225
|
*/
|
|
199
|
-
|
|
226
|
+
device_manufacturer?: string;
|
|
200
227
|
|
|
201
228
|
/**
|
|
202
229
|
* The user's locale (e.g., "en-US").
|
|
@@ -220,13 +247,15 @@ export interface MGMEvent {
|
|
|
220
247
|
*/
|
|
221
248
|
export interface MGMEventContext {
|
|
222
249
|
platform: Platform;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
250
|
+
app_version?: string;
|
|
251
|
+
app_build_number?: string;
|
|
252
|
+
os_version?: string;
|
|
253
|
+
|
|
254
|
+
user_id: string;
|
|
255
|
+
|
|
256
|
+
session_id?: string;
|
|
228
257
|
environment: string;
|
|
229
|
-
|
|
258
|
+
device_manufacturer?: string;
|
|
230
259
|
locale?: string;
|
|
231
260
|
timezone?: string;
|
|
232
261
|
}
|
|
@@ -360,8 +389,24 @@ export const SystemEvents = {
|
|
|
360
389
|
APP_UPDATED: '$app_updated',
|
|
361
390
|
APP_OPENED: '$app_opened',
|
|
362
391
|
APP_BACKGROUNDED: '$app_backgrounded',
|
|
392
|
+
IDENTIFY: '$identify',
|
|
363
393
|
} as const;
|
|
364
394
|
|
|
395
|
+
/**
|
|
396
|
+
* User profile data for the identify() call.
|
|
397
|
+
*/
|
|
398
|
+
export interface UserProfile {
|
|
399
|
+
/**
|
|
400
|
+
* The user's email address.
|
|
401
|
+
*/
|
|
402
|
+
email?: string;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The user's display name.
|
|
406
|
+
*/
|
|
407
|
+
name?: string;
|
|
408
|
+
}
|
|
409
|
+
|
|
365
410
|
/**
|
|
366
411
|
* System property keys (prefixed with $).
|
|
367
412
|
*/
|
|
@@ -383,7 +428,7 @@ export const DefaultConfiguration = {
|
|
|
383
428
|
flushInterval: 30,
|
|
384
429
|
maxStoredEvents: 10000,
|
|
385
430
|
enableDebugLogging: false,
|
|
386
|
-
trackAppLifecycleEvents:
|
|
431
|
+
trackAppLifecycleEvents: false,
|
|
387
432
|
} as const;
|
|
388
433
|
|
|
389
434
|
/**
|
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
|
*/
|