@faststats/web 0.2.13 → 0.3.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/dist/chunks/api-urls-nKmJnyjD.js +1 -0
- package/dist/chunks/identifiers-wylBcW5K.js +1 -0
- package/dist/chunks/{replay-BuX3_0xs.d.ts → replay-Dpl1P6cw.d.ts} +17 -12
- package/dist/chunks/send-data-D6WdryL7.js +1 -0
- package/dist/error.d.ts +4 -12
- package/dist/error.js +2 -1
- package/dist/feature-flags.d.ts +19 -2
- package/dist/feature-flags.js +1 -1
- package/dist/index.d.ts +15 -37
- package/dist/index.js +1 -1
- package/dist/replay.d.ts +2 -2
- package/dist/replay.js +1 -1
- package/dist/web-vitals.d.ts +4 -7
- package/dist/web-vitals.js +1 -1
- package/package.json +9 -11
- package/CHANGELOG.md +0 -177
- package/dist/chunks/api-urls-DaeYkG0_.js +0 -1
- package/dist/chunks/error-CDVOMiI9.js +0 -2
- package/dist/chunks/feature-flags-BClx56v5.d.ts +0 -19
- package/dist/chunks/feature-flags-DSOCIZHK.js +0 -1
- package/dist/chunks/identifiers-CQeWm7wi.js +0 -1
- package/dist/chunks/replay-CHFW2q4E.js +0 -1
- package/dist/chunks/rolldown-runtime-MP-BAFHD.js +0 -1
- package/dist/chunks/send-data-DL_GlsQw.js +0 -1
- package/dist/chunks/types-CYzR5xtT.js +0 -1
- package/dist/chunks/web-vitals-GwPlL7Zy.js +0 -1
- package/scripts/check-bundle-size.mjs +0 -96
- package/src/analytics.ts +0 -776
- package/src/entries/error.ts +0 -6
- package/src/entries/feature-flags.ts +0 -5
- package/src/entries/main.ts +0 -21
- package/src/entries/replay.ts +0 -1
- package/src/entries/web-vitals.ts +0 -1
- package/src/env.d.ts +0 -2
- package/src/error.ts +0 -252
- package/src/feature-flags.ts +0 -84
- package/src/replay.ts +0 -469
- package/src/sdk.ts +0 -8
- package/src/utils/api-urls.ts +0 -24
- package/src/utils/identifiers.ts +0 -89
- package/src/utils/send-data.ts +0 -52
- package/src/utils/types.ts +0 -15
- package/src/web-vitals.ts +0 -159
- package/tests/analytics.test.ts +0 -450
- package/tests/identifiers.test.ts +0 -95
- package/tests/replay.test.ts +0 -412
- package/tests/web-vitals.test.ts +0 -208
- package/tsconfig.json +0 -30
- package/tsdown.config.ts +0 -27
- package/worker/index.ts +0 -22
- package/wrangler.toml +0 -8
package/src/replay.ts
DELETED
|
@@ -1,469 +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 {
|
|
11
|
-
getAnonymousId,
|
|
12
|
-
getOrCreateSessionId,
|
|
13
|
-
getSessionStart,
|
|
14
|
-
} from "./utils/identifiers";
|
|
15
|
-
import { sendData } from "./utils/send-data";
|
|
16
|
-
import { normalizeSamplingPercentage } from "./utils/types";
|
|
17
|
-
|
|
18
|
-
const RRWEB_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
|
|
19
|
-
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
20
|
-
const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2 * 1024 * 1024;
|
|
21
|
-
|
|
22
|
-
export interface ReplayTrackerOptions {
|
|
23
|
-
siteKey: string;
|
|
24
|
-
baseUrl?: string;
|
|
25
|
-
debug?: boolean;
|
|
26
|
-
compress?: boolean;
|
|
27
|
-
samplingPercentage?: number;
|
|
28
|
-
flushInterval?: number;
|
|
29
|
-
maxEvents?: number;
|
|
30
|
-
maxPendingBatches?: number;
|
|
31
|
-
maxQueueSizeBytes?: number;
|
|
32
|
-
minReplayLengthMs?: number;
|
|
33
|
-
sampling?: recordOptions<eventWithTime>["sampling"];
|
|
34
|
-
slimDOMOptions?: SlimDOMOptions;
|
|
35
|
-
maskAllInputs?: boolean;
|
|
36
|
-
maskInputOptions?: recordOptions<eventWithTime>["maskInputOptions"];
|
|
37
|
-
blockClass?: string;
|
|
38
|
-
blockSelector?: string;
|
|
39
|
-
maskTextClass?: string;
|
|
40
|
-
maskTextSelector?: string;
|
|
41
|
-
checkoutEveryNms?: number;
|
|
42
|
-
checkoutEveryNth?: number;
|
|
43
|
-
recordConsole?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type ReplayBatch = {
|
|
47
|
-
token: string;
|
|
48
|
-
sessionId: string | undefined;
|
|
49
|
-
identifier?: string;
|
|
50
|
-
batchId: string;
|
|
51
|
-
sequence: number;
|
|
52
|
-
timestamp: number;
|
|
53
|
-
url: string;
|
|
54
|
-
events: eventWithTime[];
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const defaultSampling: recordOptions<eventWithTime>["sampling"] = {
|
|
58
|
-
mousemove: 50,
|
|
59
|
-
mouseInteraction: true,
|
|
60
|
-
scroll: 150,
|
|
61
|
-
media: 800,
|
|
62
|
-
input: "last",
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const defaultSlimDOMOptions: SlimDOMOptions = {
|
|
66
|
-
script: true,
|
|
67
|
-
comment: true,
|
|
68
|
-
headFavicon: true,
|
|
69
|
-
headWhitespace: true,
|
|
70
|
-
headMetaDescKeywords: true,
|
|
71
|
-
headMetaSocial: true,
|
|
72
|
-
headMetaRobots: true,
|
|
73
|
-
headMetaHttpEquiv: true,
|
|
74
|
-
headMetaAuthorship: true,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export default class ReplayTracker {
|
|
78
|
-
private readonly endpoint: string;
|
|
79
|
-
private readonly compressionSupported =
|
|
80
|
-
typeof window !== "undefined" && "CompressionStream" in window;
|
|
81
|
-
|
|
82
|
-
private readonly sampled: boolean;
|
|
83
|
-
private readonly events: eventWithTime[] = [];
|
|
84
|
-
private readonly pending: ReplayBatch[] = [];
|
|
85
|
-
private pendingSizeBytes = 0;
|
|
86
|
-
|
|
87
|
-
private sessionId: string | undefined;
|
|
88
|
-
private started = false;
|
|
89
|
-
private startTime = 0;
|
|
90
|
-
private sequence = 0;
|
|
91
|
-
private intervalId: ReturnType<typeof setInterval> | null = null;
|
|
92
|
-
private flushTask: ReturnType<typeof setTimeout> | null = null;
|
|
93
|
-
private retryTask: ReturnType<typeof setTimeout> | null = null;
|
|
94
|
-
private minLengthFlushTask: ReturnType<typeof setTimeout> | null = null;
|
|
95
|
-
private stopRecording?: listenerHandler;
|
|
96
|
-
private sending = false;
|
|
97
|
-
|
|
98
|
-
constructor(private readonly options: ReplayTrackerOptions) {
|
|
99
|
-
this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.replay}`;
|
|
100
|
-
this.sampled =
|
|
101
|
-
Math.random() * 100 <
|
|
102
|
-
normalizeSamplingPercentage(options.samplingPercentage);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private get debug(): boolean {
|
|
106
|
-
return this.options.debug ?? false;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private get flushInterval(): number {
|
|
110
|
-
return this.options.flushInterval ?? 10_000;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private get maxEvents(): number {
|
|
114
|
-
return this.options.maxEvents ?? 500;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private get maxPendingBatches(): number {
|
|
118
|
-
return this.options.maxPendingBatches ?? 30;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private get maxQueueSizeBytes(): number {
|
|
122
|
-
return this.options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private get minReplayLengthMs(): number {
|
|
126
|
-
return this.options.minReplayLengthMs ?? 3000;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private get shouldCompress(): boolean {
|
|
130
|
-
return this.options.compress ?? true;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private log(...args: unknown[]): void {
|
|
134
|
-
if (this.debug) console.log("[Replay]", ...args);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
start(): void {
|
|
138
|
-
if (this.started || typeof window === "undefined" || !this.sampled) return;
|
|
139
|
-
|
|
140
|
-
this.started = true;
|
|
141
|
-
this.sessionId = getOrCreateSessionId();
|
|
142
|
-
this.startTime = getSessionStart();
|
|
143
|
-
|
|
144
|
-
void this.beginRecording();
|
|
145
|
-
|
|
146
|
-
this.intervalId = setInterval(this.requestFlush, this.flushInterval);
|
|
147
|
-
|
|
148
|
-
window.addEventListener("beforeunload", this.onUnload);
|
|
149
|
-
window.addEventListener("pagehide", this.onUnload);
|
|
150
|
-
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
151
|
-
|
|
152
|
-
this.log("Recording started");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async beginRecording(): Promise<void> {
|
|
156
|
-
const wantsConsole = this.options.recordConsole ?? true;
|
|
157
|
-
|
|
158
|
-
const [{ getRecordSequentialIdPlugin }, consolePluginModule] =
|
|
159
|
-
await Promise.all([
|
|
160
|
-
import("@rrweb/rrweb-plugin-sequential-id-record"),
|
|
161
|
-
wantsConsole
|
|
162
|
-
? import("@rrweb/rrweb-plugin-console-record")
|
|
163
|
-
: Promise.resolve(null),
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
if (!this.started) return;
|
|
167
|
-
|
|
168
|
-
const plugins = [
|
|
169
|
-
getRecordSequentialIdPlugin({ key: RRWEB_SEQUENTIAL_ID_KEY }),
|
|
170
|
-
];
|
|
171
|
-
if (consolePluginModule) {
|
|
172
|
-
plugins.push(consolePluginModule.getRecordConsolePlugin());
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
this.stopRecording = record({
|
|
176
|
-
emit: this.onEvent,
|
|
177
|
-
sampling: this.options.sampling ?? defaultSampling,
|
|
178
|
-
slimDOMOptions: this.options.slimDOMOptions ?? defaultSlimDOMOptions,
|
|
179
|
-
maskAllInputs: this.options.maskAllInputs ?? true,
|
|
180
|
-
maskInputOptions: this.options.maskInputOptions ?? {
|
|
181
|
-
password: true,
|
|
182
|
-
email: true,
|
|
183
|
-
tel: true,
|
|
184
|
-
},
|
|
185
|
-
blockClass: this.options.blockClass,
|
|
186
|
-
blockSelector: this.options.blockSelector,
|
|
187
|
-
maskTextClass: this.options.maskTextClass,
|
|
188
|
-
maskTextSelector: this.options.maskTextSelector,
|
|
189
|
-
checkoutEveryNms: this.options.checkoutEveryNms ?? 60_000,
|
|
190
|
-
checkoutEveryNth: this.options.checkoutEveryNth,
|
|
191
|
-
plugins,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
stop(): void {
|
|
196
|
-
if (!this.started) return;
|
|
197
|
-
this.started = false;
|
|
198
|
-
|
|
199
|
-
this.stopRecording?.();
|
|
200
|
-
this.stopRecording = undefined;
|
|
201
|
-
|
|
202
|
-
if (this.intervalId) clearInterval(this.intervalId);
|
|
203
|
-
if (this.flushTask) clearTimeout(this.flushTask);
|
|
204
|
-
if (this.retryTask) clearTimeout(this.retryTask);
|
|
205
|
-
if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
|
|
206
|
-
|
|
207
|
-
this.intervalId = null;
|
|
208
|
-
this.flushTask = null;
|
|
209
|
-
this.retryTask = null;
|
|
210
|
-
this.minLengthFlushTask = null;
|
|
211
|
-
|
|
212
|
-
window.removeEventListener("beforeunload", this.onUnload);
|
|
213
|
-
window.removeEventListener("pagehide", this.onUnload);
|
|
214
|
-
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
215
|
-
|
|
216
|
-
if (!this.hasReachedMinLength()) {
|
|
217
|
-
this.events.length = 0;
|
|
218
|
-
this.sessionId = undefined;
|
|
219
|
-
this.log(
|
|
220
|
-
`Session too short (${Date.now() - this.startTime}ms), discarding events`,
|
|
221
|
-
);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
void this.flush(true);
|
|
226
|
-
this.sessionId = undefined;
|
|
227
|
-
this.log("Recording stopped");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
getSessionId(): string | undefined {
|
|
231
|
-
return this.sessionId ?? getOrCreateSessionId();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private onEvent = (event: eventWithTime, isCheckout?: boolean): void => {
|
|
235
|
-
this.events.push(event);
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
isCheckout ||
|
|
239
|
-
this.events.length >= this.maxEvents ||
|
|
240
|
-
(event.type === EventType.FullSnapshot && this.hasReachedMinLength())
|
|
241
|
-
) {
|
|
242
|
-
this.requestFlush();
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
this.scheduleMinLengthFlush();
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
private onUnload = (): void => {
|
|
250
|
-
void this.flush(true);
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
private onVisibilityChange = (): void => {
|
|
254
|
-
if (document.visibilityState === "hidden") {
|
|
255
|
-
void this.flush(true);
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
private hasReachedMinLength(): boolean {
|
|
260
|
-
return (
|
|
261
|
-
this.minReplayLengthMs <= 0 ||
|
|
262
|
-
Date.now() - this.startTime >= this.minReplayLengthMs
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private requestFlush = (): void => {
|
|
267
|
-
if (this.flushTask || this.sending || this.events.length === 0) return;
|
|
268
|
-
|
|
269
|
-
if (this.minLengthFlushTask) {
|
|
270
|
-
clearTimeout(this.minLengthFlushTask);
|
|
271
|
-
this.minLengthFlushTask = null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
this.flushTask = setTimeout(() => {
|
|
275
|
-
this.flushTask = null;
|
|
276
|
-
void this.flush(false);
|
|
277
|
-
}, 0);
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
private scheduleMinLengthFlush(): void {
|
|
281
|
-
if (
|
|
282
|
-
this.minLengthFlushTask ||
|
|
283
|
-
this.events.length === 0 ||
|
|
284
|
-
this.hasReachedMinLength()
|
|
285
|
-
) {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const remainingMs = Math.max(
|
|
290
|
-
0,
|
|
291
|
-
this.minReplayLengthMs - (Date.now() - this.startTime),
|
|
292
|
-
);
|
|
293
|
-
const timeoutMs = Math.min(remainingMs, MAX_TIMEOUT_MS);
|
|
294
|
-
|
|
295
|
-
this.minLengthFlushTask = setTimeout(() => {
|
|
296
|
-
this.minLengthFlushTask = null;
|
|
297
|
-
this.requestFlush();
|
|
298
|
-
}, timeoutMs);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private createBatch(events: eventWithTime[]): ReplayBatch {
|
|
302
|
-
const identifier = getAnonymousId();
|
|
303
|
-
const sequence = this.sequence++;
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
token: this.options.siteKey,
|
|
307
|
-
sessionId: this.getSessionId(),
|
|
308
|
-
...(identifier ? { identifier } : {}),
|
|
309
|
-
batchId: this.createBatchId(sequence),
|
|
310
|
-
sequence,
|
|
311
|
-
timestamp: Date.now(),
|
|
312
|
-
url: window.location.href,
|
|
313
|
-
events,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private createBatchId(sequence: number): string {
|
|
318
|
-
const random =
|
|
319
|
-
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
320
|
-
? crypto.randomUUID()
|
|
321
|
-
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
322
|
-
return `${this.getSessionId() ?? "unknown"}-${sequence}-${random}`;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private getBatchSizeBytes(batch: ReplayBatch): number {
|
|
326
|
-
return new TextEncoder().encode(JSON.stringify(batch)).byteLength;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private dropOldestPendingBatch(reason: string): void {
|
|
330
|
-
const dropped = this.pending.shift();
|
|
331
|
-
if (!dropped) return;
|
|
332
|
-
|
|
333
|
-
this.pendingSizeBytes = Math.max(
|
|
334
|
-
0,
|
|
335
|
-
this.pendingSizeBytes - this.getBatchSizeBytes(dropped),
|
|
336
|
-
);
|
|
337
|
-
this.log(`${reason}, dropping batch ${dropped.sequence}`);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private enqueueBatch(batch: ReplayBatch): void {
|
|
341
|
-
const batchSizeBytes = this.getBatchSizeBytes(batch);
|
|
342
|
-
|
|
343
|
-
if (batchSizeBytes > this.maxQueueSizeBytes) {
|
|
344
|
-
this.log(
|
|
345
|
-
`Replay batch ${batch.sequence} is ${batchSizeBytes}B, exceeding ${this.maxQueueSizeBytes}B queue limit; dropping`,
|
|
346
|
-
);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
while (
|
|
351
|
-
this.pending.length > 0 &&
|
|
352
|
-
this.pendingSizeBytes + batchSizeBytes > this.maxQueueSizeBytes
|
|
353
|
-
) {
|
|
354
|
-
this.dropOldestPendingBatch("Pending queue size limit reached");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
while (this.pending.length >= this.maxPendingBatches) {
|
|
358
|
-
this.dropOldestPendingBatch("Pending batch limit reached");
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
this.pending.push(batch);
|
|
362
|
-
this.pendingSizeBytes += batchSizeBytes;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
private async encodeBatch(
|
|
366
|
-
batch: ReplayBatch,
|
|
367
|
-
): Promise<{ data: string | Uint8Array; isCompressed: boolean }> {
|
|
368
|
-
const json = JSON.stringify(batch);
|
|
369
|
-
|
|
370
|
-
if (!this.shouldCompress || !this.compressionSupported) {
|
|
371
|
-
return {
|
|
372
|
-
data: json,
|
|
373
|
-
isCompressed: false,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
const compressed = await this.compress(json);
|
|
379
|
-
this.log(
|
|
380
|
-
`Compressed ${json.length}B -> ${compressed.byteLength}B (${Math.round((compressed.byteLength / json.length) * 100)}%)`,
|
|
381
|
-
);
|
|
382
|
-
return {
|
|
383
|
-
data: compressed,
|
|
384
|
-
isCompressed: true,
|
|
385
|
-
};
|
|
386
|
-
} catch {
|
|
387
|
-
this.log("Compression failed, using uncompressed");
|
|
388
|
-
return {
|
|
389
|
-
data: json,
|
|
390
|
-
isCompressed: false,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
private async flush(lowLatency: boolean): Promise<void> {
|
|
396
|
-
if (this.sending) return;
|
|
397
|
-
|
|
398
|
-
if (this.events.length > 0) {
|
|
399
|
-
if (!this.hasReachedMinLength()) {
|
|
400
|
-
this.scheduleMinLengthFlush();
|
|
401
|
-
} else {
|
|
402
|
-
const batch = this.createBatch(this.events.splice(0));
|
|
403
|
-
this.enqueueBatch(batch);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (this.pending.length === 0) return;
|
|
408
|
-
this.sending = true;
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
while (this.pending.length > 0) {
|
|
412
|
-
const batch = this.pending[0];
|
|
413
|
-
if (!batch) break;
|
|
414
|
-
|
|
415
|
-
const encoded = await this.encodeBatch(batch);
|
|
416
|
-
const ok = await this.send(
|
|
417
|
-
encoded.data,
|
|
418
|
-
encoded.isCompressed,
|
|
419
|
-
lowLatency,
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
if (!ok) {
|
|
423
|
-
this.log(`Failed to send replay batch ${batch.sequence}, retrying`);
|
|
424
|
-
this.scheduleRetry();
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
this.dropOldestPendingBatch("Sent replay batch");
|
|
428
|
-
lowLatency = false;
|
|
429
|
-
}
|
|
430
|
-
} finally {
|
|
431
|
-
this.sending = false;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
private scheduleRetry(): void {
|
|
436
|
-
if (this.retryTask) return;
|
|
437
|
-
this.retryTask = setTimeout(() => {
|
|
438
|
-
this.retryTask = null;
|
|
439
|
-
void this.flush(false);
|
|
440
|
-
}, 1000);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
private async compress(data: string): Promise<Uint8Array> {
|
|
444
|
-
const stream = new Blob([data])
|
|
445
|
-
.stream()
|
|
446
|
-
.pipeThrough(new CompressionStream("gzip"));
|
|
447
|
-
const buffer = await new Response(stream).arrayBuffer();
|
|
448
|
-
return new Uint8Array(buffer);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
private send(
|
|
452
|
-
data: string | Uint8Array,
|
|
453
|
-
isCompressed: boolean,
|
|
454
|
-
lowLatency: boolean,
|
|
455
|
-
): Promise<boolean> {
|
|
456
|
-
return (
|
|
457
|
-
sendData({
|
|
458
|
-
url: isCompressed ? `${this.endpoint}?encoding=gzip` : this.endpoint,
|
|
459
|
-
data,
|
|
460
|
-
contentType: isCompressed
|
|
461
|
-
? "application/octet-stream"
|
|
462
|
-
: "application/json",
|
|
463
|
-
debug: false,
|
|
464
|
-
useBeacon: false,
|
|
465
|
-
keepalive: lowLatency,
|
|
466
|
-
}) ?? Promise.resolve(false)
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
}
|
package/src/sdk.ts
DELETED
package/src/utils/api-urls.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/identifiers.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
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";
|
|
6
|
-
|
|
7
|
-
let cookielessMode = false;
|
|
8
|
-
|
|
9
|
-
export function setCookielessMode(mode: boolean): void {
|
|
10
|
-
cookielessMode = mode;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getAnonymousId(cookieless?: boolean): string {
|
|
14
|
-
const useCookieless = cookieless ?? cookielessMode;
|
|
15
|
-
if (useCookieless) return "";
|
|
16
|
-
return getOrCreateAnonymousId();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getOrCreateAnonymousId(): string {
|
|
20
|
-
if (!localStorage) return "";
|
|
21
|
-
const existingId = localStorage.getItem(ANONYMOUS_ID_KEY);
|
|
22
|
-
if (existingId) return existingId;
|
|
23
|
-
|
|
24
|
-
const newId = crypto.randomUUID();
|
|
25
|
-
localStorage.setItem(ANONYMOUS_ID_KEY, newId);
|
|
26
|
-
return newId;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function resetAnonymousId(cookieless?: boolean): string {
|
|
30
|
-
if (cookieless) return "";
|
|
31
|
-
|
|
32
|
-
if (!localStorage) return "";
|
|
33
|
-
|
|
34
|
-
localStorage.removeItem(ANONYMOUS_ID_KEY);
|
|
35
|
-
return getOrCreateAnonymousId();
|
|
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);
|
|
51
|
-
}
|
|
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);
|
|
89
|
-
}
|
package/src/utils/send-data.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { SendDataOptions } from "./types";
|
|
2
|
-
|
|
3
|
-
export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
4
|
-
const {
|
|
5
|
-
url,
|
|
6
|
-
data,
|
|
7
|
-
contentType = "application/json",
|
|
8
|
-
headers = {},
|
|
9
|
-
debug = false,
|
|
10
|
-
debugPrefix = "[Analytics]",
|
|
11
|
-
useBeacon = true,
|
|
12
|
-
keepalive = true,
|
|
13
|
-
} = options;
|
|
14
|
-
|
|
15
|
-
if (useBeacon && typeof globalThis.navigator?.sendBeacon === "function") {
|
|
16
|
-
try {
|
|
17
|
-
const beaconBody =
|
|
18
|
-
data instanceof Blob
|
|
19
|
-
? data
|
|
20
|
-
: typeof Blob !== "undefined"
|
|
21
|
-
? new Blob([data as BlobPart], { type: contentType })
|
|
22
|
-
: data;
|
|
23
|
-
if (globalThis.navigator.sendBeacon(url, beaconBody as BodyInit)) {
|
|
24
|
-
if (debug) console.log(`${debugPrefix} Sent via beacon`);
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
} catch {}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const response = await fetch(url, {
|
|
32
|
-
method: "POST",
|
|
33
|
-
body: data as BodyInit,
|
|
34
|
-
headers: {
|
|
35
|
-
"Content-Type": contentType,
|
|
36
|
-
...headers,
|
|
37
|
-
},
|
|
38
|
-
keepalive,
|
|
39
|
-
});
|
|
40
|
-
const success = response.ok;
|
|
41
|
-
if (debug) {
|
|
42
|
-
const message = success
|
|
43
|
-
? `${debugPrefix} Sent via fetch`
|
|
44
|
-
: `${debugPrefix} Failed: ${response.status}`;
|
|
45
|
-
console[success ? "log" : "warn"](message);
|
|
46
|
-
}
|
|
47
|
-
return success;
|
|
48
|
-
} catch {
|
|
49
|
-
if (debug) console.warn(`${debugPrefix} Failed to send`);
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
package/src/utils/types.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface SendDataOptions {
|
|
2
|
-
url: string;
|
|
3
|
-
data: string | Blob | Uint8Array;
|
|
4
|
-
contentType?: string;
|
|
5
|
-
headers?: Record<string, string>;
|
|
6
|
-
debug?: boolean;
|
|
7
|
-
debugPrefix?: string;
|
|
8
|
-
useBeacon?: boolean;
|
|
9
|
-
keepalive?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function normalizeSamplingPercentage(value: number | undefined): number {
|
|
13
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 100;
|
|
14
|
-
return Math.max(0, Math.min(100, value));
|
|
15
|
-
}
|