@faststats/web 0.2.14 → 0.3.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.
Files changed (53) hide show
  1. package/dist/chunks/api-urls-nKmJnyjD.js +1 -0
  2. package/dist/chunks/identifiers-DrjjHcKB.js +1 -0
  3. package/dist/chunks/{replay-DvJYurEC.d.ts → replay-DR_T0Iz4.d.ts} +12 -17
  4. package/dist/chunks/send-data-Cb4xlYF0.js +1 -0
  5. package/dist/error.d.ts +4 -12
  6. package/dist/error.js +2 -1
  7. package/dist/feature-flags.d.ts +19 -2
  8. package/dist/feature-flags.js +1 -1
  9. package/dist/index.d.ts +28 -59
  10. package/dist/index.js +1 -1
  11. package/dist/replay.d.ts +2 -2
  12. package/dist/replay.js +1 -1
  13. package/dist/web-vitals.d.ts +5 -8
  14. package/dist/web-vitals.js +1 -1
  15. package/package.json +7 -6
  16. package/CHANGELOG.md +0 -184
  17. package/REPLAY_PAYLOAD.md +0 -64
  18. package/dist/chunks/api-urls-DaeYkG0_.js +0 -1
  19. package/dist/chunks/error-Cd9PTS5v.js +0 -2
  20. package/dist/chunks/feature-flags-BClx56v5.d.ts +0 -19
  21. package/dist/chunks/feature-flags-DSOCIZHK.js +0 -1
  22. package/dist/chunks/replay-rTqcjOo2.js +0 -1
  23. package/dist/chunks/rolldown-runtime-MP-BAFHD.js +0 -1
  24. package/dist/chunks/send-data-B2fYGj6v.js +0 -1
  25. package/dist/chunks/session-manager-Cy63ptPF.js +0 -1
  26. package/dist/chunks/types-CYzR5xtT.js +0 -1
  27. package/dist/chunks/web-vitals-Be-Cg4Po.js +0 -1
  28. package/scripts/check-bundle-size.mjs +0 -96
  29. package/src/analytics.ts +0 -787
  30. package/src/entries/error.ts +0 -6
  31. package/src/entries/feature-flags.ts +0 -5
  32. package/src/entries/main.ts +0 -21
  33. package/src/entries/replay.ts +0 -1
  34. package/src/entries/web-vitals.ts +0 -1
  35. package/src/env.d.ts +0 -2
  36. package/src/error.ts +0 -257
  37. package/src/feature-flags.ts +0 -84
  38. package/src/replay.ts +0 -596
  39. package/src/sdk.ts +0 -8
  40. package/src/utils/api-urls.ts +0 -24
  41. package/src/utils/identifiers.ts +0 -47
  42. package/src/utils/send-data.ts +0 -52
  43. package/src/utils/session-manager.ts +0 -416
  44. package/src/utils/types.ts +0 -15
  45. package/src/web-vitals.ts +0 -159
  46. package/tests/analytics.test.ts +0 -453
  47. package/tests/identifiers.test.ts +0 -78
  48. package/tests/replay.test.ts +0 -582
  49. package/tests/session-manager.test.ts +0 -161
  50. package/tests/web-vitals.test.ts +0 -208
  51. package/tsconfig.json +0 -30
  52. package/tsdown.config.ts +0 -27
  53. package/worker/index.ts +0 -22
package/src/replay.ts DELETED
@@ -1,596 +0,0 @@
1
- import {
2
- EventType,
3
- type eventWithTime,
4
- type listenerHandler,
5
- } from "@rrweb/types";
6
- import { record } from "rrweb";
7
- import type { recordOptions } from "rrweb/typings/types";
8
- import type { SlimDOMOptions } from "rrweb-snapshot";
9
- import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
10
- import { getAnonymousId } from "./utils/identifiers";
11
- import { sendData } from "./utils/send-data";
12
- import {
13
- createId,
14
- getSessionContext,
15
- getSessionStart,
16
- onSessionRotated,
17
- type SessionContext,
18
- touchActivity,
19
- } from "./utils/session-manager";
20
- import { normalizeSamplingPercentage } from "./utils/types";
21
-
22
- const MAX_TIMEOUT_MS = 2_147_483_647;
23
- const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2 * 1024 * 1024;
24
- const VIEW_META_EVENT_TYPE = 5;
25
-
26
- export interface ReplayTrackerOptions {
27
- siteKey: string;
28
- baseUrl?: string;
29
- debug?: boolean;
30
- compress?: boolean;
31
- cookieless?: boolean;
32
- samplingPercentage?: number;
33
- flushInterval?: number;
34
- maxEvents?: number;
35
- maxPendingBatches?: number;
36
- maxQueueSizeBytes?: number;
37
- minReplayLengthMs?: number;
38
- sampling?: recordOptions<eventWithTime>["sampling"];
39
- slimDOMOptions?: SlimDOMOptions;
40
- maskAllInputs?: boolean;
41
- maskInputOptions?: recordOptions<eventWithTime>["maskInputOptions"];
42
- blockClass?: string;
43
- blockSelector?: string;
44
- maskTextClass?: string;
45
- maskTextSelector?: string;
46
- checkoutEveryNms?: number;
47
- checkoutEveryNth?: number;
48
- recordConsole?: boolean;
49
- }
50
-
51
- type ReplayBatch = {
52
- token: string;
53
- sessionId: string;
54
- windowId: string;
55
- viewId: string;
56
- sessionStart: number;
57
- identifier?: string;
58
- batchId: string;
59
- sequence: number;
60
- timestamp: number;
61
- url: string;
62
- isFinal?: boolean;
63
- events: eventWithTime[];
64
- };
65
-
66
- const defaultSampling: recordOptions<eventWithTime>["sampling"] = {
67
- mousemove: 50,
68
- mouseInteraction: true,
69
- scroll: 150,
70
- media: 800,
71
- input: "last",
72
- };
73
-
74
- function replaySampledKey(siteKey: string): string {
75
- return `faststats_replay_sampled_${siteKey}`;
76
- }
77
-
78
- function hashSessionId(sessionId: string): number {
79
- let hash = 0;
80
- for (let i = 0; i < sessionId.length; i++) {
81
- hash = (hash << 5) - hash + sessionId.charCodeAt(i);
82
- hash |= 0;
83
- }
84
- return Math.abs(hash);
85
- }
86
-
87
- function resolveReplaySampled(
88
- siteKey: string,
89
- samplingPercentage: number,
90
- cookieless: boolean,
91
- ): boolean {
92
- const pct = normalizeSamplingPercentage(samplingPercentage);
93
- if (pct >= 100) return true;
94
- if (pct <= 0) return false;
95
-
96
- const sessionId = getSessionContext(siteKey, cookieless).sessionId;
97
- const storageKey = replaySampledKey(siteKey);
98
- const storedKey = `${sessionId}:${pct}`;
99
-
100
- try {
101
- const session = sessionStorage;
102
- if (session) {
103
- const stored = session.getItem(storageKey);
104
- if (stored === storedKey) return true;
105
- if (stored?.startsWith(`${sessionId}:`)) return false;
106
- }
107
- } catch {}
108
-
109
- const bucket = hashSessionId(sessionId) % 100;
110
- const sampled = bucket < pct;
111
-
112
- try {
113
- sessionStorage?.setItem(
114
- storageKey,
115
- sampled ? storedKey : `${sessionId}:out`,
116
- );
117
- } catch {}
118
-
119
- return sampled;
120
- }
121
-
122
- export default class ReplayTracker {
123
- private readonly endpoint: string;
124
- private readonly compressionSupported =
125
- typeof window !== "undefined" && "CompressionStream" in window;
126
-
127
- private readonly sampled: boolean;
128
- private readonly events: eventWithTime[] = [];
129
- private readonly pending: ReplayBatch[] = [];
130
- private pendingSizeBytes = 0;
131
-
132
- private viewId = createId();
133
- private readonly windowId = createId();
134
- private started = false;
135
- private startTime = 0;
136
- private sequence = 0;
137
- private intervalId: ReturnType<typeof setInterval> | null = null;
138
- private flushTask: ReturnType<typeof setTimeout> | null = null;
139
- private retryTask: ReturnType<typeof setTimeout> | null = null;
140
- private minLengthFlushTask: ReturnType<typeof setTimeout> | null = null;
141
- private stopRecording?: listenerHandler;
142
- private sending = false;
143
- private unsubscribeRotation?: () => void;
144
-
145
- constructor(private readonly options: ReplayTrackerOptions) {
146
- this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.replay}`;
147
- this.sampled = resolveReplaySampled(
148
- options.siteKey,
149
- options.samplingPercentage ?? 100,
150
- options.cookieless ?? false,
151
- );
152
- }
153
-
154
- private get cookieless(): boolean {
155
- return this.options.cookieless ?? false;
156
- }
157
-
158
- private get debug(): boolean {
159
- return this.options.debug ?? false;
160
- }
161
-
162
- private get flushInterval(): number {
163
- return this.options.flushInterval ?? 10_000;
164
- }
165
-
166
- private get maxEvents(): number {
167
- return this.options.maxEvents ?? 500;
168
- }
169
-
170
- private get maxPendingBatches(): number {
171
- return this.options.maxPendingBatches ?? 30;
172
- }
173
-
174
- private get maxQueueSizeBytes(): number {
175
- return this.options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
176
- }
177
-
178
- private get minReplayLengthMs(): number {
179
- return this.options.minReplayLengthMs ?? 3000;
180
- }
181
-
182
- private get shouldCompress(): boolean {
183
- return this.options.compress ?? true;
184
- }
185
-
186
- private log(...args: unknown[]): void {
187
- if (this.debug) console.log("[Replay]", ...args);
188
- }
189
-
190
- private sessionContext(): SessionContext {
191
- return getSessionContext(this.options.siteKey, this.cookieless);
192
- }
193
-
194
- start(): void {
195
- if (this.started || typeof window === "undefined" || !this.sampled) return;
196
-
197
- this.started = true;
198
- this.viewId = createId();
199
- this.startTime = getSessionStart(this.cookieless);
200
-
201
- this.unsubscribeRotation = onSessionRotated((prev) => {
202
- this.onSessionRotated(prev);
203
- });
204
-
205
- void this.beginRecording();
206
-
207
- this.intervalId = setInterval(this.requestFlush, this.flushInterval);
208
-
209
- window.addEventListener("beforeunload", this.onUnload);
210
- window.addEventListener("pagehide", this.onUnload);
211
- document.addEventListener("visibilitychange", this.onVisibilityChange);
212
-
213
- this.log("Recording started");
214
- }
215
-
216
- trackPageChange(url?: string): void {
217
- if (!this.started) return;
218
- const href = url ?? window.location.href;
219
- this.viewId = createId();
220
- const metaEvent = {
221
- type: VIEW_META_EVENT_TYPE,
222
- timestamp: Date.now(),
223
- data: {
224
- tag: "faststats:view",
225
- payload: { href, viewId: this.viewId },
226
- },
227
- } as eventWithTime;
228
- this.onEvent(metaEvent, false);
229
- }
230
-
231
- private async beginRecording(): Promise<void> {
232
- const wantsConsole = this.options.recordConsole ?? true;
233
-
234
- const consolePluginModule = wantsConsole
235
- ? await import("@rrweb/rrweb-plugin-console-record")
236
- : null;
237
-
238
- if (!this.started) return;
239
-
240
- const plugins = [];
241
- if (consolePluginModule) {
242
- plugins.push(consolePluginModule.getRecordConsolePlugin());
243
- }
244
-
245
- this.stopRecording = record({
246
- emit: this.onEvent,
247
- sampling: this.options.sampling ?? defaultSampling,
248
- slimDOMOptions: this.options.slimDOMOptions ?? {
249
- script: true,
250
- comment: true,
251
- headFavicon: true,
252
- headWhitespace: true,
253
- headMetaDescKeywords: true,
254
- headMetaSocial: true,
255
- headMetaRobots: true,
256
- headMetaHttpEquiv: true,
257
- headMetaAuthorship: true,
258
- },
259
- maskAllInputs: this.options.maskAllInputs ?? true,
260
- maskInputOptions: this.options.maskInputOptions ?? {
261
- password: true,
262
- email: true,
263
- tel: true,
264
- },
265
- blockClass: this.options.blockClass,
266
- blockSelector: this.options.blockSelector,
267
- maskTextClass: this.options.maskTextClass,
268
- maskTextSelector: this.options.maskTextSelector,
269
- checkoutEveryNms: this.options.checkoutEveryNms ?? 60_000,
270
- checkoutEveryNth: this.options.checkoutEveryNth,
271
- plugins,
272
- });
273
- }
274
-
275
- stop(): void {
276
- if (!this.started) return;
277
- this.started = false;
278
-
279
- this.unsubscribeRotation?.();
280
- this.unsubscribeRotation = undefined;
281
-
282
- this.stopRecording?.();
283
- this.stopRecording = undefined;
284
-
285
- if (this.intervalId) clearInterval(this.intervalId);
286
- if (this.flushTask) clearTimeout(this.flushTask);
287
- if (this.retryTask) clearTimeout(this.retryTask);
288
- if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
289
-
290
- this.intervalId = null;
291
- this.flushTask = null;
292
- this.retryTask = null;
293
- this.minLengthFlushTask = null;
294
-
295
- window.removeEventListener("beforeunload", this.onUnload);
296
- window.removeEventListener("pagehide", this.onUnload);
297
- document.removeEventListener("visibilitychange", this.onVisibilityChange);
298
-
299
- if (!this.hasReachedMinLength()) {
300
- this.events.length = 0;
301
- this.log(
302
- `Session too short (${Date.now() - this.startTime}ms), discarding events`,
303
- );
304
- return;
305
- }
306
-
307
- void this.flush(true, undefined, true);
308
- this.log("Recording stopped");
309
- }
310
-
311
- getSessionId(): string {
312
- return this.sessionContext().sessionId;
313
- }
314
-
315
- getWindowId(): string {
316
- return this.sessionContext().windowId;
317
- }
318
-
319
- private onSessionRotated(prev: SessionContext): void {
320
- if (!this.started) return;
321
- if (this.events.length > 0) {
322
- if (!this.hasReachedMinLength()) {
323
- this.scheduleMinLengthFlush();
324
- } else {
325
- const batch = this.createBatch(this.events.splice(0), prev, true);
326
- this.enqueueBatch(batch);
327
- }
328
- }
329
- if (this.pending.length > 0) {
330
- queueMicrotask(() => void this.flush(false));
331
- }
332
- }
333
-
334
- private onEvent = (event: eventWithTime, isCheckout?: boolean): void => {
335
- this.events.push(event);
336
-
337
- if (
338
- isCheckout ||
339
- this.events.length >= this.maxEvents ||
340
- (event.type === EventType.FullSnapshot && this.hasReachedMinLength())
341
- ) {
342
- this.requestFlush();
343
- return;
344
- }
345
-
346
- this.scheduleMinLengthFlush();
347
- };
348
-
349
- private onUnload = (): void => {
350
- void this.flush(true, undefined, true);
351
- };
352
-
353
- private onVisibilityChange = (): void => {
354
- if (document.visibilityState === "hidden") {
355
- void this.flush(true, undefined, true);
356
- }
357
- };
358
-
359
- private hasReachedMinLength(): boolean {
360
- return (
361
- this.minReplayLengthMs <= 0 ||
362
- Date.now() - this.startTime >= this.minReplayLengthMs
363
- );
364
- }
365
-
366
- private requestFlush = (): void => {
367
- if (this.flushTask || this.sending || this.events.length === 0) return;
368
-
369
- if (this.minLengthFlushTask) {
370
- clearTimeout(this.minLengthFlushTask);
371
- this.minLengthFlushTask = null;
372
- }
373
-
374
- this.flushTask = setTimeout(() => {
375
- this.flushTask = null;
376
- void this.flush(false);
377
- }, 0);
378
- };
379
-
380
- private scheduleMinLengthFlush(): void {
381
- if (
382
- this.minLengthFlushTask ||
383
- this.events.length === 0 ||
384
- this.hasReachedMinLength()
385
- ) {
386
- return;
387
- }
388
-
389
- const remainingMs = Math.max(
390
- 0,
391
- this.minReplayLengthMs - (Date.now() - this.startTime),
392
- );
393
- const timeoutMs = Math.min(remainingMs, MAX_TIMEOUT_MS);
394
-
395
- this.minLengthFlushTask = setTimeout(() => {
396
- this.minLengthFlushTask = null;
397
- this.requestFlush();
398
- }, timeoutMs);
399
- }
400
-
401
- private createBatch(
402
- events: eventWithTime[],
403
- context: SessionContext,
404
- isFinal?: boolean,
405
- ): ReplayBatch {
406
- const identifier = getAnonymousId(this.cookieless);
407
- const sequence = this.sequence++;
408
-
409
- return {
410
- token: this.options.siteKey,
411
- sessionId: context.sessionId,
412
- windowId: this.windowId,
413
- viewId: this.viewId,
414
- sessionStart: context.sessionStart,
415
- ...(identifier ? { identifier } : {}),
416
- batchId: this.createBatchId(context.sessionId, sequence),
417
- sequence,
418
- timestamp: Date.now(),
419
- url: window.location.href,
420
- ...(isFinal ? { isFinal: true } : {}),
421
- events,
422
- };
423
- }
424
-
425
- private createBatchId(sessionId: string, sequence: number): string {
426
- const random =
427
- typeof crypto !== "undefined" && "randomUUID" in crypto
428
- ? crypto.randomUUID()
429
- : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
430
- return `${sessionId}-${sequence}-${random}`;
431
- }
432
-
433
- private getBatchSizeBytes(batch: ReplayBatch): number {
434
- return new TextEncoder().encode(JSON.stringify(batch)).byteLength;
435
- }
436
-
437
- private dropOldestPendingBatch(reason: string): void {
438
- const dropped = this.pending.shift();
439
- if (!dropped) return;
440
-
441
- this.pendingSizeBytes = Math.max(
442
- 0,
443
- this.pendingSizeBytes - this.getBatchSizeBytes(dropped),
444
- );
445
- this.log(`${reason}, dropping batch ${dropped.sequence}`);
446
- }
447
-
448
- private enqueueBatch(batch: ReplayBatch): void {
449
- const batchSizeBytes = this.getBatchSizeBytes(batch);
450
-
451
- if (batchSizeBytes > this.maxQueueSizeBytes) {
452
- this.log(
453
- `Replay batch ${batch.sequence} is ${batchSizeBytes}B, exceeding ${this.maxQueueSizeBytes}B queue limit; dropping`,
454
- );
455
- return;
456
- }
457
-
458
- while (
459
- this.pending.length > 0 &&
460
- this.pendingSizeBytes + batchSizeBytes > this.maxQueueSizeBytes
461
- ) {
462
- this.dropOldestPendingBatch("Pending queue size limit reached");
463
- }
464
-
465
- while (this.pending.length >= this.maxPendingBatches) {
466
- this.dropOldestPendingBatch("Pending batch limit reached");
467
- }
468
-
469
- this.pending.push(batch);
470
- this.pendingSizeBytes += batchSizeBytes;
471
- }
472
-
473
- private async encodeBatch(
474
- batch: ReplayBatch,
475
- ): Promise<{ data: string | Uint8Array; isCompressed: boolean }> {
476
- const json = JSON.stringify(batch);
477
-
478
- if (!this.shouldCompress || !this.compressionSupported) {
479
- return {
480
- data: json,
481
- isCompressed: false,
482
- };
483
- }
484
-
485
- try {
486
- const compressed = await this.compress(json);
487
- this.log(
488
- `Compressed ${json.length}B -> ${compressed.byteLength}B (${Math.round((compressed.byteLength / json.length) * 100)}%)`,
489
- );
490
- return {
491
- data: compressed,
492
- isCompressed: true,
493
- };
494
- } catch {
495
- this.log("Compression failed, using uncompressed");
496
- return {
497
- data: json,
498
- isCompressed: false,
499
- };
500
- }
501
- }
502
-
503
- private async flush(
504
- lowLatency: boolean,
505
- contextOverride?: SessionContext,
506
- isFinal?: boolean,
507
- ): Promise<void> {
508
- if (this.sending) return;
509
-
510
- const rotation = contextOverride ? null : touchActivity(this.cookieless);
511
- if (rotation && this.events.length > 0) {
512
- if (!this.hasReachedMinLength()) {
513
- this.scheduleMinLengthFlush();
514
- } else {
515
- const batch = this.createBatch(
516
- this.events.splice(0),
517
- rotation.prev,
518
- true,
519
- );
520
- this.enqueueBatch(batch);
521
- }
522
- }
523
- const context = contextOverride ?? this.sessionContext();
524
-
525
- if (this.events.length > 0) {
526
- if (!this.hasReachedMinLength()) {
527
- this.scheduleMinLengthFlush();
528
- } else {
529
- const batch = this.createBatch(this.events.splice(0), context, isFinal);
530
- this.enqueueBatch(batch);
531
- }
532
- }
533
-
534
- if (this.pending.length === 0) return;
535
- this.sending = true;
536
-
537
- try {
538
- while (this.pending.length > 0) {
539
- const batch = this.pending[0];
540
- if (!batch) break;
541
-
542
- const encoded = await this.encodeBatch(batch);
543
- const ok = await this.send(
544
- encoded.data,
545
- encoded.isCompressed,
546
- lowLatency,
547
- );
548
-
549
- if (!ok) {
550
- this.log(`Failed to send replay batch ${batch.sequence}, retrying`);
551
- this.scheduleRetry();
552
- break;
553
- }
554
- this.dropOldestPendingBatch("Sent replay batch");
555
- lowLatency = false;
556
- }
557
- } finally {
558
- this.sending = false;
559
- }
560
- }
561
-
562
- private scheduleRetry(): void {
563
- if (this.retryTask) return;
564
- this.retryTask = setTimeout(() => {
565
- this.retryTask = null;
566
- void this.flush(false);
567
- }, 1000);
568
- }
569
-
570
- private async compress(data: string): Promise<Uint8Array> {
571
- const stream = new Blob([data])
572
- .stream()
573
- .pipeThrough(new CompressionStream("gzip"));
574
- const buffer = await new Response(stream).arrayBuffer();
575
- return new Uint8Array(buffer);
576
- }
577
-
578
- private send(
579
- data: string | Uint8Array,
580
- isCompressed: boolean,
581
- lowLatency: boolean,
582
- ): Promise<boolean> {
583
- return (
584
- sendData({
585
- url: isCompressed ? `${this.endpoint}?encoding=gzip` : this.endpoint,
586
- data,
587
- contentType: isCompressed
588
- ? "application/octet-stream"
589
- : "application/json",
590
- debug: false,
591
- useBeacon: lowLatency,
592
- keepalive: lowLatency,
593
- }) ?? Promise.resolve(false)
594
- );
595
- }
596
- }
package/src/sdk.ts DELETED
@@ -1,8 +0,0 @@
1
- export const SDK_NAME =
2
- typeof __FASTSTATS_SDK_NAME__ === "string"
3
- ? __FASTSTATS_SDK_NAME__
4
- : "@faststats/web";
5
- export const SDK_VERSION =
6
- typeof __FASTSTATS_SDK_VERSION__ === "string"
7
- ? __FASTSTATS_SDK_VERSION__
8
- : "0.2.7";
@@ -1,24 +0,0 @@
1
- const DEFAULT_BASE = "https://metrics.faststats.dev";
2
- const DEFAULT_FEATURE_FLAGS_BASE = "https://flags.faststats.dev";
3
-
4
- function stripTrailingSlashes(s: string): string {
5
- return s.replace(/\/+$/, "");
6
- }
7
-
8
- export function normalizeAnalyticsBaseUrl(input?: string): string {
9
- if (input === undefined || input === "") return DEFAULT_BASE;
10
- return stripTrailingSlashes(input) || DEFAULT_BASE;
11
- }
12
-
13
- export const URLS = {
14
- events: "/v1/web",
15
- identify: "/v1/identify",
16
- replay: "/v1/replay",
17
- vitals: "/v1/vitals",
18
- flags: "/v1/check",
19
- } as const;
20
-
21
- export function normalizeFeatureFlagsBaseUrl(input?: string): string {
22
- if (input === undefined || input === "") return DEFAULT_FEATURE_FLAGS_BASE;
23
- return stripTrailingSlashes(input) || DEFAULT_FEATURE_FLAGS_BASE;
24
- }
@@ -1,47 +0,0 @@
1
- import { createId, isCookielessMode } from "./session-manager";
2
-
3
- const ANONYMOUS_ID_KEY = "faststats_anon_id";
4
-
5
- function getLocalStorage(): Storage | undefined {
6
- try {
7
- return globalThis.localStorage;
8
- } catch {
9
- return undefined;
10
- }
11
- }
12
-
13
- export function getAnonymousId(cookieless?: boolean): string {
14
- if (cookieless ?? isCookielessMode()) return "";
15
- return getOrCreateAnonymousId();
16
- }
17
-
18
- export function getOrCreateAnonymousId(): string {
19
- const storage = getLocalStorage();
20
- if (!storage) return "";
21
- try {
22
- const existingId = storage.getItem(ANONYMOUS_ID_KEY);
23
- if (existingId) return existingId;
24
- } catch {
25
- return "";
26
- }
27
-
28
- const newId = createId();
29
- try {
30
- storage.setItem(ANONYMOUS_ID_KEY, newId);
31
- } catch {
32
- return "";
33
- }
34
- return newId;
35
- }
36
-
37
- export function resetAnonymousId(cookieless?: boolean): string {
38
- if (cookieless) return "";
39
- try {
40
- const storage = getLocalStorage();
41
- if (!storage) return "";
42
- storage.removeItem(ANONYMOUS_ID_KEY);
43
- } catch {
44
- return "";
45
- }
46
- return getOrCreateAnonymousId();
47
- }