@faststats/web 0.2.13 → 0.2.14
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/CHANGELOG.md +7 -0
- package/REPLAY_PAYLOAD.md +64 -0
- package/dist/chunks/{error-CDVOMiI9.js → error-Cd9PTS5v.js} +2 -2
- package/dist/chunks/{replay-BuX3_0xs.d.ts → replay-DvJYurEC.d.ts} +10 -2
- package/dist/chunks/replay-rTqcjOo2.js +1 -0
- package/dist/chunks/send-data-B2fYGj6v.js +1 -0
- package/dist/chunks/session-manager-Cy63ptPF.js +1 -0
- package/dist/chunks/web-vitals-Be-Cg4Po.js +1 -0
- package/dist/error.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/replay.d.ts +1 -1
- package/dist/replay.js +1 -1
- package/dist/web-vitals.js +1 -1
- package/package.json +4 -7
- package/src/analytics.ts +21 -10
- package/src/error.ts +6 -1
- package/src/replay.ts +177 -50
- package/src/utils/identifiers.ts +30 -72
- package/src/utils/session-manager.ts +416 -0
- package/src/web-vitals.ts +1 -1
- package/tests/analytics.test.ts +7 -4
- package/tests/identifiers.test.ts +15 -32
- package/tests/replay.test.ts +180 -10
- package/tests/session-manager.test.ts +161 -0
- package/dist/chunks/identifiers-CQeWm7wi.js +0 -1
- package/dist/chunks/replay-CHFW2q4E.js +0 -1
- package/dist/chunks/send-data-DL_GlsQw.js +0 -1
- package/dist/chunks/web-vitals-GwPlL7Zy.js +0 -1
- package/wrangler.toml +0 -8
package/src/replay.ts
CHANGED
|
@@ -7,23 +7,28 @@ import { record } from "rrweb";
|
|
|
7
7
|
import type { recordOptions } from "rrweb/typings/types";
|
|
8
8
|
import type { SlimDOMOptions } from "rrweb-snapshot";
|
|
9
9
|
import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
|
|
10
|
+
import { getAnonymousId } from "./utils/identifiers";
|
|
11
|
+
import { sendData } from "./utils/send-data";
|
|
10
12
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
createId,
|
|
14
|
+
getSessionContext,
|
|
13
15
|
getSessionStart,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
onSessionRotated,
|
|
17
|
+
type SessionContext,
|
|
18
|
+
touchActivity,
|
|
19
|
+
} from "./utils/session-manager";
|
|
16
20
|
import { normalizeSamplingPercentage } from "./utils/types";
|
|
17
21
|
|
|
18
|
-
const RRWEB_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
|
|
19
22
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
20
23
|
const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
const VIEW_META_EVENT_TYPE = 5;
|
|
21
25
|
|
|
22
26
|
export interface ReplayTrackerOptions {
|
|
23
27
|
siteKey: string;
|
|
24
28
|
baseUrl?: string;
|
|
25
29
|
debug?: boolean;
|
|
26
30
|
compress?: boolean;
|
|
31
|
+
cookieless?: boolean;
|
|
27
32
|
samplingPercentage?: number;
|
|
28
33
|
flushInterval?: number;
|
|
29
34
|
maxEvents?: number;
|
|
@@ -45,12 +50,16 @@ export interface ReplayTrackerOptions {
|
|
|
45
50
|
|
|
46
51
|
type ReplayBatch = {
|
|
47
52
|
token: string;
|
|
48
|
-
sessionId: string
|
|
53
|
+
sessionId: string;
|
|
54
|
+
windowId: string;
|
|
55
|
+
viewId: string;
|
|
56
|
+
sessionStart: number;
|
|
49
57
|
identifier?: string;
|
|
50
58
|
batchId: string;
|
|
51
59
|
sequence: number;
|
|
52
60
|
timestamp: number;
|
|
53
61
|
url: string;
|
|
62
|
+
isFinal?: boolean;
|
|
54
63
|
events: eventWithTime[];
|
|
55
64
|
};
|
|
56
65
|
|
|
@@ -62,17 +71,53 @@ const defaultSampling: recordOptions<eventWithTime>["sampling"] = {
|
|
|
62
71
|
input: "last",
|
|
63
72
|
};
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
}
|
|
76
121
|
|
|
77
122
|
export default class ReplayTracker {
|
|
78
123
|
private readonly endpoint: string;
|
|
@@ -84,7 +129,8 @@ export default class ReplayTracker {
|
|
|
84
129
|
private readonly pending: ReplayBatch[] = [];
|
|
85
130
|
private pendingSizeBytes = 0;
|
|
86
131
|
|
|
87
|
-
private
|
|
132
|
+
private viewId = createId();
|
|
133
|
+
private readonly windowId = createId();
|
|
88
134
|
private started = false;
|
|
89
135
|
private startTime = 0;
|
|
90
136
|
private sequence = 0;
|
|
@@ -94,12 +140,19 @@ export default class ReplayTracker {
|
|
|
94
140
|
private minLengthFlushTask: ReturnType<typeof setTimeout> | null = null;
|
|
95
141
|
private stopRecording?: listenerHandler;
|
|
96
142
|
private sending = false;
|
|
143
|
+
private unsubscribeRotation?: () => void;
|
|
97
144
|
|
|
98
145
|
constructor(private readonly options: ReplayTrackerOptions) {
|
|
99
146
|
this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.replay}`;
|
|
100
|
-
this.sampled =
|
|
101
|
-
|
|
102
|
-
|
|
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;
|
|
103
156
|
}
|
|
104
157
|
|
|
105
158
|
private get debug(): boolean {
|
|
@@ -134,12 +187,20 @@ export default class ReplayTracker {
|
|
|
134
187
|
if (this.debug) console.log("[Replay]", ...args);
|
|
135
188
|
}
|
|
136
189
|
|
|
190
|
+
private sessionContext(): SessionContext {
|
|
191
|
+
return getSessionContext(this.options.siteKey, this.cookieless);
|
|
192
|
+
}
|
|
193
|
+
|
|
137
194
|
start(): void {
|
|
138
195
|
if (this.started || typeof window === "undefined" || !this.sampled) return;
|
|
139
196
|
|
|
140
197
|
this.started = true;
|
|
141
|
-
this.
|
|
142
|
-
this.startTime = getSessionStart();
|
|
198
|
+
this.viewId = createId();
|
|
199
|
+
this.startTime = getSessionStart(this.cookieless);
|
|
200
|
+
|
|
201
|
+
this.unsubscribeRotation = onSessionRotated((prev) => {
|
|
202
|
+
this.onSessionRotated(prev);
|
|
203
|
+
});
|
|
143
204
|
|
|
144
205
|
void this.beginRecording();
|
|
145
206
|
|
|
@@ -152,22 +213,31 @@ export default class ReplayTracker {
|
|
|
152
213
|
this.log("Recording started");
|
|
153
214
|
}
|
|
154
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
|
+
|
|
155
231
|
private async beginRecording(): Promise<void> {
|
|
156
232
|
const wantsConsole = this.options.recordConsole ?? true;
|
|
157
233
|
|
|
158
|
-
const
|
|
159
|
-
await
|
|
160
|
-
|
|
161
|
-
wantsConsole
|
|
162
|
-
? import("@rrweb/rrweb-plugin-console-record")
|
|
163
|
-
: Promise.resolve(null),
|
|
164
|
-
]);
|
|
234
|
+
const consolePluginModule = wantsConsole
|
|
235
|
+
? await import("@rrweb/rrweb-plugin-console-record")
|
|
236
|
+
: null;
|
|
165
237
|
|
|
166
238
|
if (!this.started) return;
|
|
167
239
|
|
|
168
|
-
const plugins = [
|
|
169
|
-
getRecordSequentialIdPlugin({ key: RRWEB_SEQUENTIAL_ID_KEY }),
|
|
170
|
-
];
|
|
240
|
+
const plugins = [];
|
|
171
241
|
if (consolePluginModule) {
|
|
172
242
|
plugins.push(consolePluginModule.getRecordConsolePlugin());
|
|
173
243
|
}
|
|
@@ -175,7 +245,17 @@ export default class ReplayTracker {
|
|
|
175
245
|
this.stopRecording = record({
|
|
176
246
|
emit: this.onEvent,
|
|
177
247
|
sampling: this.options.sampling ?? defaultSampling,
|
|
178
|
-
slimDOMOptions: this.options.slimDOMOptions ??
|
|
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
|
+
},
|
|
179
259
|
maskAllInputs: this.options.maskAllInputs ?? true,
|
|
180
260
|
maskInputOptions: this.options.maskInputOptions ?? {
|
|
181
261
|
password: true,
|
|
@@ -196,6 +276,9 @@ export default class ReplayTracker {
|
|
|
196
276
|
if (!this.started) return;
|
|
197
277
|
this.started = false;
|
|
198
278
|
|
|
279
|
+
this.unsubscribeRotation?.();
|
|
280
|
+
this.unsubscribeRotation = undefined;
|
|
281
|
+
|
|
199
282
|
this.stopRecording?.();
|
|
200
283
|
this.stopRecording = undefined;
|
|
201
284
|
|
|
@@ -215,20 +298,37 @@ export default class ReplayTracker {
|
|
|
215
298
|
|
|
216
299
|
if (!this.hasReachedMinLength()) {
|
|
217
300
|
this.events.length = 0;
|
|
218
|
-
this.sessionId = undefined;
|
|
219
301
|
this.log(
|
|
220
302
|
`Session too short (${Date.now() - this.startTime}ms), discarding events`,
|
|
221
303
|
);
|
|
222
304
|
return;
|
|
223
305
|
}
|
|
224
306
|
|
|
225
|
-
void this.flush(true);
|
|
226
|
-
this.sessionId = undefined;
|
|
307
|
+
void this.flush(true, undefined, true);
|
|
227
308
|
this.log("Recording stopped");
|
|
228
309
|
}
|
|
229
310
|
|
|
230
|
-
getSessionId(): string
|
|
231
|
-
return this.
|
|
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
|
+
}
|
|
232
332
|
}
|
|
233
333
|
|
|
234
334
|
private onEvent = (event: eventWithTime, isCheckout?: boolean): void => {
|
|
@@ -247,12 +347,12 @@ export default class ReplayTracker {
|
|
|
247
347
|
};
|
|
248
348
|
|
|
249
349
|
private onUnload = (): void => {
|
|
250
|
-
void this.flush(true);
|
|
350
|
+
void this.flush(true, undefined, true);
|
|
251
351
|
};
|
|
252
352
|
|
|
253
353
|
private onVisibilityChange = (): void => {
|
|
254
354
|
if (document.visibilityState === "hidden") {
|
|
255
|
-
void this.flush(true);
|
|
355
|
+
void this.flush(true, undefined, true);
|
|
256
356
|
}
|
|
257
357
|
};
|
|
258
358
|
|
|
@@ -298,28 +398,36 @@ export default class ReplayTracker {
|
|
|
298
398
|
}, timeoutMs);
|
|
299
399
|
}
|
|
300
400
|
|
|
301
|
-
private createBatch(
|
|
302
|
-
|
|
401
|
+
private createBatch(
|
|
402
|
+
events: eventWithTime[],
|
|
403
|
+
context: SessionContext,
|
|
404
|
+
isFinal?: boolean,
|
|
405
|
+
): ReplayBatch {
|
|
406
|
+
const identifier = getAnonymousId(this.cookieless);
|
|
303
407
|
const sequence = this.sequence++;
|
|
304
408
|
|
|
305
409
|
return {
|
|
306
410
|
token: this.options.siteKey,
|
|
307
|
-
sessionId:
|
|
411
|
+
sessionId: context.sessionId,
|
|
412
|
+
windowId: this.windowId,
|
|
413
|
+
viewId: this.viewId,
|
|
414
|
+
sessionStart: context.sessionStart,
|
|
308
415
|
...(identifier ? { identifier } : {}),
|
|
309
|
-
batchId: this.createBatchId(sequence),
|
|
416
|
+
batchId: this.createBatchId(context.sessionId, sequence),
|
|
310
417
|
sequence,
|
|
311
418
|
timestamp: Date.now(),
|
|
312
419
|
url: window.location.href,
|
|
420
|
+
...(isFinal ? { isFinal: true } : {}),
|
|
313
421
|
events,
|
|
314
422
|
};
|
|
315
423
|
}
|
|
316
424
|
|
|
317
|
-
private createBatchId(sequence: number): string {
|
|
425
|
+
private createBatchId(sessionId: string, sequence: number): string {
|
|
318
426
|
const random =
|
|
319
427
|
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
320
428
|
? crypto.randomUUID()
|
|
321
429
|
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
322
|
-
return `${
|
|
430
|
+
return `${sessionId}-${sequence}-${random}`;
|
|
323
431
|
}
|
|
324
432
|
|
|
325
433
|
private getBatchSizeBytes(batch: ReplayBatch): number {
|
|
@@ -392,14 +500,33 @@ export default class ReplayTracker {
|
|
|
392
500
|
}
|
|
393
501
|
}
|
|
394
502
|
|
|
395
|
-
private async flush(
|
|
503
|
+
private async flush(
|
|
504
|
+
lowLatency: boolean,
|
|
505
|
+
contextOverride?: SessionContext,
|
|
506
|
+
isFinal?: boolean,
|
|
507
|
+
): Promise<void> {
|
|
396
508
|
if (this.sending) return;
|
|
397
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
|
+
|
|
398
525
|
if (this.events.length > 0) {
|
|
399
526
|
if (!this.hasReachedMinLength()) {
|
|
400
527
|
this.scheduleMinLengthFlush();
|
|
401
528
|
} else {
|
|
402
|
-
const batch = this.createBatch(this.events.splice(0));
|
|
529
|
+
const batch = this.createBatch(this.events.splice(0), context, isFinal);
|
|
403
530
|
this.enqueueBatch(batch);
|
|
404
531
|
}
|
|
405
532
|
}
|
|
@@ -461,7 +588,7 @@ export default class ReplayTracker {
|
|
|
461
588
|
? "application/octet-stream"
|
|
462
589
|
: "application/json",
|
|
463
590
|
debug: false,
|
|
464
|
-
useBeacon:
|
|
591
|
+
useBeacon: lowLatency,
|
|
465
592
|
keepalive: lowLatency,
|
|
466
593
|
}) ?? Promise.resolve(false)
|
|
467
594
|
);
|
package/src/utils/identifiers.ts
CHANGED
|
@@ -1,89 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
const ANONYMOUS_ID_KEY = "faststats_anon_id";
|
|
3
|
-
const SESSION_ID_KEY = "session_id";
|
|
4
|
-
const SESSION_TIMESTAMP_KEY = "session_timestamp";
|
|
5
|
-
const SESSION_START_KEY = "session_start";
|
|
1
|
+
import { createId, isCookielessMode } from "./session-manager";
|
|
6
2
|
|
|
7
|
-
|
|
3
|
+
const ANONYMOUS_ID_KEY = "faststats_anon_id";
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
function getLocalStorage(): Storage | undefined {
|
|
6
|
+
try {
|
|
7
|
+
return globalThis.localStorage;
|
|
8
|
+
} catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function getAnonymousId(cookieless?: boolean): string {
|
|
14
|
-
|
|
15
|
-
if (useCookieless) return "";
|
|
14
|
+
if (cookieless ?? isCookielessMode()) return "";
|
|
16
15
|
return getOrCreateAnonymousId();
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export function getOrCreateAnonymousId(): string {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|
|
23
27
|
|
|
24
|
-
const newId =
|
|
25
|
-
|
|
28
|
+
const newId = createId();
|
|
29
|
+
try {
|
|
30
|
+
storage.setItem(ANONYMOUS_ID_KEY, newId);
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
26
34
|
return newId;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export function resetAnonymousId(cookieless?: boolean): string {
|
|
30
38
|
if (cookieless) return "";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export function getOrCreateSessionId(): string {
|
|
39
|
-
if (!sessionStorage) return "";
|
|
40
|
-
|
|
41
|
-
const existingId = sessionStorage.getItem(SESSION_ID_KEY);
|
|
42
|
-
const sessionTimestamp = sessionStorage.getItem(SESSION_TIMESTAMP_KEY);
|
|
43
|
-
|
|
44
|
-
if (existingId && sessionTimestamp) {
|
|
45
|
-
const sessionAge = Date.now() - Number.parseInt(sessionTimestamp, 10);
|
|
46
|
-
if (sessionAge < SESSION_TIMEOUT) {
|
|
47
|
-
sessionStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
|
|
48
|
-
return existingId;
|
|
49
|
-
}
|
|
50
|
-
clearSession(sessionStorage);
|
|
39
|
+
try {
|
|
40
|
+
const storage = getLocalStorage();
|
|
41
|
+
if (!storage) return "";
|
|
42
|
+
storage.removeItem(ANONYMOUS_ID_KEY);
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
51
45
|
}
|
|
52
|
-
|
|
53
|
-
const now = Date.now().toString();
|
|
54
|
-
const newId = crypto.randomUUID();
|
|
55
|
-
sessionStorage.setItem(SESSION_ID_KEY, newId);
|
|
56
|
-
sessionStorage.setItem(SESSION_TIMESTAMP_KEY, now);
|
|
57
|
-
sessionStorage.setItem(SESSION_START_KEY, now);
|
|
58
|
-
return newId;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function resetSessionId(): string {
|
|
62
|
-
if (!sessionStorage) return "";
|
|
63
|
-
|
|
64
|
-
clearSession(sessionStorage);
|
|
65
|
-
return getOrCreateSessionId();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function refreshSessionTimestamp(): void {
|
|
69
|
-
if (!sessionStorage) return;
|
|
70
|
-
|
|
71
|
-
if (sessionStorage.getItem(SESSION_ID_KEY)) {
|
|
72
|
-
sessionStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function getSessionStart(): number {
|
|
77
|
-
if (!sessionStorage) return Date.now();
|
|
78
|
-
|
|
79
|
-
const start = sessionStorage.getItem(SESSION_START_KEY);
|
|
80
|
-
if (start) return Number.parseInt(start, 10);
|
|
81
|
-
const ts = sessionStorage.getItem(SESSION_TIMESTAMP_KEY);
|
|
82
|
-
return ts ? Number.parseInt(ts, 10) : Date.now();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function clearSession(storage: Storage): void {
|
|
86
|
-
storage.removeItem(SESSION_ID_KEY);
|
|
87
|
-
storage.removeItem(SESSION_TIMESTAMP_KEY);
|
|
88
|
-
storage.removeItem(SESSION_START_KEY);
|
|
46
|
+
return getOrCreateAnonymousId();
|
|
89
47
|
}
|