@faststats/web 0.2.6 → 0.2.8
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 +25 -0
- package/dist/chunks/analytics-Cmawx5f9.js +1 -0
- package/dist/chunks/api-urls-BrkcoElX.js +1 -0
- package/dist/{error-Df3cDcoP.js → chunks/error-1K0dai1m.js} +2 -2
- package/dist/chunks/feature-flags-6rZlmhfu.d.ts +18 -0
- package/dist/chunks/feature-flags-BPRTgvIh.js +1 -0
- package/dist/chunks/replay-CtMtn0C-.js +1 -0
- package/dist/chunks/replay-IhsP2Ab4.d.ts +77 -0
- package/dist/chunks/rolldown-runtime-MP-BAFHD.js +1 -0
- package/dist/chunks/types-C3vW7XGe.js +1 -0
- package/dist/chunks/web-vitals-BooBPWJC.js +1 -0
- package/dist/error.d.ts +44 -0
- package/dist/error.js +1 -0
- package/dist/feature-flags.d.ts +2 -0
- package/dist/feature-flags.js +1 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +1 -0
- package/dist/replay.d.ts +2 -0
- package/dist/replay.js +1 -0
- package/dist/web-vitals.d.ts +27 -0
- package/dist/web-vitals.js +1 -0
- package/package.json +32 -11
- package/scripts/check-bundle-size.mjs +90 -9
- package/src/analytics.ts +47 -41
- package/src/entries/error.ts +6 -0
- package/src/entries/feature-flags.ts +5 -0
- package/src/entries/main.ts +21 -0
- package/src/entries/replay.ts +1 -0
- package/src/entries/web-vitals.ts +1 -0
- package/src/env.d.ts +2 -0
- package/src/error.ts +5 -0
- package/src/replay.ts +110 -62
- package/src/sdk.ts +8 -0
- package/src/utils/types.ts +1 -1
- package/src/web-vitals.ts +32 -14
- package/tests/replay.test.ts +178 -16
- package/tsdown.config.ts +16 -1
- package/dist/analytics-D_dgxSVU.js +0 -1
- package/dist/module.d.ts +0 -531
- package/dist/module.js +0 -1
- package/dist/replay-alCILAnP.js +0 -1
- package/dist/types-Df70G0eM.js +0 -1
- package/dist/web-vitals-gcUR-TX_.js +0 -1
- package/src/module.ts +0 -25
package/src/analytics.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type ErrorTracker from "./error";
|
|
2
|
-
import {
|
|
3
|
-
type FeatureFlagEvaluation,
|
|
4
|
-
fetchFeatureFlagEvaluation,
|
|
5
|
-
} from "./feature-flags";
|
|
2
|
+
import type { FeatureFlagEvaluation } from "./feature-flags";
|
|
6
3
|
import type ReplayTracker from "./replay";
|
|
7
4
|
import type { ReplayTrackerOptions } from "./replay";
|
|
8
5
|
import {
|
|
@@ -41,6 +38,7 @@ interface SamplingOptions {
|
|
|
41
38
|
|
|
42
39
|
interface WebVitalsConfig {
|
|
43
40
|
sampling?: SamplingOptions;
|
|
41
|
+
attribution?: boolean;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
interface SessionReplayConfig {
|
|
@@ -77,6 +75,10 @@ export interface WebAnalyticsOptions {
|
|
|
77
75
|
webVitals?: WebVitalsConfig;
|
|
78
76
|
sessionReplays?: SessionReplayConfig;
|
|
79
77
|
replayOptions?: Partial<ReplayTrackerOptions>;
|
|
78
|
+
/** @internal */
|
|
79
|
+
sdkName?: string;
|
|
80
|
+
/** @internal */
|
|
81
|
+
sdkVersion?: string;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
function hasReplaySamplingConfig(options: WebAnalyticsOptions): boolean {
|
|
@@ -112,11 +114,16 @@ export function resolveReplayTrackerOptions(
|
|
|
112
114
|
};
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
const moduleState: {
|
|
118
|
+
instance: WebAnalytics | null;
|
|
119
|
+
pendingConsentMode: ConsentMode | undefined;
|
|
120
|
+
} = {
|
|
121
|
+
instance: null,
|
|
122
|
+
pendingConsentMode: undefined,
|
|
123
|
+
};
|
|
117
124
|
|
|
118
125
|
export function getInstance(): WebAnalytics | null {
|
|
119
|
-
return
|
|
126
|
+
return moduleState.instance;
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
export function trackEvent(
|
|
@@ -124,7 +131,7 @@ export function trackEvent(
|
|
|
124
131
|
properties?: Record<string, unknown>,
|
|
125
132
|
): void {
|
|
126
133
|
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
127
|
-
|
|
134
|
+
moduleState.instance?.track(eventName, properties ?? {});
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
export function identify(
|
|
@@ -133,20 +140,20 @@ export function identify(
|
|
|
133
140
|
options?: IdentifyOptions,
|
|
134
141
|
): void {
|
|
135
142
|
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
136
|
-
|
|
143
|
+
moduleState.instance?.identify(externalId, email, options ?? {});
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
export function logout(resetAnonymousIdentity = true): void {
|
|
140
147
|
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
141
|
-
|
|
148
|
+
moduleState.instance?.logout(resetAnonymousIdentity);
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
export function setConsentMode(mode: ConsentMode): void {
|
|
145
|
-
if (
|
|
146
|
-
|
|
152
|
+
if (moduleState.instance) {
|
|
153
|
+
moduleState.instance.setConsentMode(mode);
|
|
147
154
|
return;
|
|
148
155
|
}
|
|
149
|
-
pendingConsentMode = mode;
|
|
156
|
+
moduleState.pendingConsentMode = mode;
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
export function optIn(): void {
|
|
@@ -159,7 +166,7 @@ export function optOut(): void {
|
|
|
159
166
|
|
|
160
167
|
export function reportError(error: Error): void {
|
|
161
168
|
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
162
|
-
|
|
169
|
+
moduleState.instance?.reportError(error);
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
export function isTrackingDisabled(): boolean {
|
|
@@ -190,9 +197,9 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
|
190
197
|
data instanceof Blob
|
|
191
198
|
? data
|
|
192
199
|
: typeof Blob !== "undefined"
|
|
193
|
-
? new Blob([data], { type: contentType })
|
|
200
|
+
? new Blob([data as BlobPart], { type: contentType })
|
|
194
201
|
: data;
|
|
195
|
-
if (navigator.sendBeacon(url, beaconBody)) {
|
|
202
|
+
if (navigator.sendBeacon(url, beaconBody as BodyInit)) {
|
|
196
203
|
if (debug) {
|
|
197
204
|
console.log(`${debugPrefix} Sent via beacon`);
|
|
198
205
|
}
|
|
@@ -211,7 +218,7 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
|
211
218
|
try {
|
|
212
219
|
const response = await fetch(url, {
|
|
213
220
|
method: "POST",
|
|
214
|
-
body: data,
|
|
221
|
+
body: data as BodyInit,
|
|
215
222
|
headers: {
|
|
216
223
|
"Content-Type": contentType,
|
|
217
224
|
...headers,
|
|
@@ -311,9 +318,9 @@ export class WebAnalytics {
|
|
|
311
318
|
this.consentMode = options.consent?.mode ?? "granted";
|
|
312
319
|
this.cookielessWhilePending =
|
|
313
320
|
options.consent?.cookielessWhilePending ?? true;
|
|
314
|
-
if (pendingConsentMode !== undefined) {
|
|
315
|
-
this.consentMode = pendingConsentMode;
|
|
316
|
-
pendingConsentMode = undefined;
|
|
321
|
+
if (moduleState.pendingConsentMode !== undefined) {
|
|
322
|
+
this.consentMode = moduleState.pendingConsentMode;
|
|
323
|
+
moduleState.pendingConsentMode = undefined;
|
|
317
324
|
}
|
|
318
325
|
if (options.autoTrack ?? true) this.init();
|
|
319
326
|
}
|
|
@@ -328,7 +335,7 @@ export class WebAnalytics {
|
|
|
328
335
|
this.log("disabled");
|
|
329
336
|
return;
|
|
330
337
|
}
|
|
331
|
-
|
|
338
|
+
moduleState.instance = this;
|
|
332
339
|
setCookielessMode(this.isCookielessMode());
|
|
333
340
|
setTimeout(() => void this.start(), 0);
|
|
334
341
|
}
|
|
@@ -408,7 +415,7 @@ export class WebAnalytics {
|
|
|
408
415
|
}
|
|
409
416
|
|
|
410
417
|
private registerChildTracker(tracker: ChildTracker): boolean {
|
|
411
|
-
if (!this.started || this.destroyed ||
|
|
418
|
+
if (!this.started || this.destroyed || moduleState.instance !== this) {
|
|
412
419
|
tracker.stop?.();
|
|
413
420
|
return false;
|
|
414
421
|
}
|
|
@@ -420,12 +427,15 @@ export class WebAnalytics {
|
|
|
420
427
|
private async startErrorTracker(): Promise<void> {
|
|
421
428
|
try {
|
|
422
429
|
const { default: ErrorTrackerClass } = await import("./error");
|
|
423
|
-
if (!this.started || this.destroyed ||
|
|
430
|
+
if (!this.started || this.destroyed || moduleState.instance !== this)
|
|
431
|
+
return;
|
|
424
432
|
|
|
425
433
|
const errorTracker: ErrorTracker = new ErrorTrackerClass({
|
|
426
434
|
siteKey: this.options.siteKey,
|
|
427
435
|
baseUrl: this.baseUrl,
|
|
428
436
|
debug: this.debug,
|
|
437
|
+
sdkName: this.options.sdkName,
|
|
438
|
+
sdkVersion: this.options.sdkVersion,
|
|
429
439
|
});
|
|
430
440
|
errorTracker.start();
|
|
431
441
|
if (!this.registerChildTracker(errorTracker)) return;
|
|
@@ -444,7 +454,8 @@ export class WebAnalytics {
|
|
|
444
454
|
private async startWebVitalsTracker(): Promise<void> {
|
|
445
455
|
try {
|
|
446
456
|
const { default: WebVitalsTrackerClass } = await import("./web-vitals");
|
|
447
|
-
if (!this.started || this.destroyed ||
|
|
457
|
+
if (!this.started || this.destroyed || moduleState.instance !== this)
|
|
458
|
+
return;
|
|
448
459
|
|
|
449
460
|
const webVitalsTracker: WebVitalsTracker = new WebVitalsTrackerClass({
|
|
450
461
|
siteKey: this.options.siteKey,
|
|
@@ -453,6 +464,7 @@ export class WebAnalytics {
|
|
|
453
464
|
samplingPercentage: normalizeSamplingPercentage(
|
|
454
465
|
this.options.webVitals?.sampling?.percentage,
|
|
455
466
|
),
|
|
467
|
+
attribution: this.options.webVitals?.attribution ?? false,
|
|
456
468
|
});
|
|
457
469
|
webVitalsTracker.start();
|
|
458
470
|
if (!this.registerChildTracker(webVitalsTracker)) return;
|
|
@@ -468,7 +480,8 @@ export class WebAnalytics {
|
|
|
468
480
|
): Promise<void> {
|
|
469
481
|
try {
|
|
470
482
|
const { default: ReplayTrackerClass } = await import("./replay");
|
|
471
|
-
if (!this.started || this.destroyed ||
|
|
483
|
+
if (!this.started || this.destroyed || moduleState.instance !== this)
|
|
484
|
+
return;
|
|
472
485
|
|
|
473
486
|
const replayTracker: ReplayTracker = new ReplayTrackerClass(
|
|
474
487
|
replayOptions,
|
|
@@ -484,7 +497,7 @@ export class WebAnalytics {
|
|
|
484
497
|
|
|
485
498
|
async start(): Promise<void> {
|
|
486
499
|
if (this.started || this.destroyed || typeof window === "undefined") return;
|
|
487
|
-
if (
|
|
500
|
+
if (moduleState.instance && moduleState.instance !== this) {
|
|
488
501
|
this.log("already started by another instance");
|
|
489
502
|
return;
|
|
490
503
|
}
|
|
@@ -494,7 +507,7 @@ export class WebAnalytics {
|
|
|
494
507
|
}
|
|
495
508
|
|
|
496
509
|
this.started = true;
|
|
497
|
-
|
|
510
|
+
moduleState.instance = this;
|
|
498
511
|
setCookielessMode(this.isCookielessMode());
|
|
499
512
|
getOrCreateSessionId();
|
|
500
513
|
|
|
@@ -551,8 +564,8 @@ export class WebAnalytics {
|
|
|
551
564
|
}
|
|
552
565
|
|
|
553
566
|
this.stopChildTrackers();
|
|
554
|
-
if (
|
|
555
|
-
|
|
567
|
+
if (moduleState.instance === this) {
|
|
568
|
+
moduleState.instance = null;
|
|
556
569
|
}
|
|
557
570
|
this.started = false;
|
|
558
571
|
this.destroyed = true;
|
|
@@ -646,23 +659,16 @@ export class WebAnalytics {
|
|
|
646
659
|
return { value: "false" };
|
|
647
660
|
}
|
|
648
661
|
const externalId = opts?.externalId?.trim();
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
baseUrl: this.featureFlagsBaseUrl,
|
|
652
|
-
projectToken: this.options.siteKey,
|
|
653
|
-
externalId,
|
|
654
|
-
attributes,
|
|
655
|
-
signal: opts?.signal,
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
const identifier = this.getAnonymousId();
|
|
659
|
-
if (!identifier) {
|
|
662
|
+
const identifier = externalId ? undefined : this.getAnonymousId();
|
|
663
|
+
if (!externalId && !identifier) {
|
|
660
664
|
return { value: "false" };
|
|
661
665
|
}
|
|
666
|
+
|
|
667
|
+
const { fetchFeatureFlagEvaluation } = await import("./feature-flags");
|
|
662
668
|
return fetchFeatureFlagEvaluation(key, {
|
|
663
669
|
baseUrl: this.featureFlagsBaseUrl,
|
|
664
670
|
projectToken: this.options.siteKey,
|
|
665
|
-
identifier,
|
|
671
|
+
...(externalId ? { externalId } : { identifier }),
|
|
666
672
|
attributes,
|
|
667
673
|
signal: opts?.signal,
|
|
668
674
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type ConsentMode,
|
|
3
|
+
getInstance,
|
|
4
|
+
type IdentifyOptions,
|
|
5
|
+
identify,
|
|
6
|
+
isTrackingDisabled,
|
|
7
|
+
logout,
|
|
8
|
+
optIn,
|
|
9
|
+
optOut,
|
|
10
|
+
reportError,
|
|
11
|
+
sendData,
|
|
12
|
+
setConsentMode,
|
|
13
|
+
trackEvent,
|
|
14
|
+
WebAnalytics,
|
|
15
|
+
type WebAnalyticsOptions,
|
|
16
|
+
} from "../analytics";
|
|
17
|
+
export type {
|
|
18
|
+
FeatureFlagCheckContext,
|
|
19
|
+
FeatureFlagEvaluation,
|
|
20
|
+
} from "../feature-flags";
|
|
21
|
+
export { fetchFeatureFlagEvaluation } from "../feature-flags";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, type ReplayTrackerOptions } from "../replay";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, type WebVitalsOptions } from "../web-vitals";
|
package/src/env.d.ts
ADDED
package/src/error.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sendData } from "./analytics";
|
|
2
|
+
import { SDK_NAME, SDK_VERSION } from "./sdk";
|
|
2
3
|
import { webEventsUrl } from "./utils/api-urls";
|
|
3
4
|
import { getAnonymousId, getOrCreateSessionId } from "./utils/identifiers";
|
|
4
5
|
|
|
@@ -33,6 +34,8 @@ export interface ErrorTrackingOptions {
|
|
|
33
34
|
debug?: boolean;
|
|
34
35
|
flushInterval?: number;
|
|
35
36
|
maxQueueSize?: number;
|
|
37
|
+
sdkName?: string;
|
|
38
|
+
sdkVersion?: string;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const EXTENSION_URL =
|
|
@@ -256,6 +259,8 @@ export default class ErrorTracker {
|
|
|
256
259
|
...(identifier ? { userId: identifier } : {}),
|
|
257
260
|
sessionId: getOrCreateSessionId(),
|
|
258
261
|
...(buildId ? { buildId } : {}),
|
|
262
|
+
sdkName: this.options.sdkName ?? SDK_NAME,
|
|
263
|
+
sdkVersion: this.options.sdkVersion ?? SDK_VERSION,
|
|
259
264
|
data: {
|
|
260
265
|
url: location.href,
|
|
261
266
|
page: location.pathname,
|
package/src/replay.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
EventType,
|
|
3
|
+
type eventWithTime,
|
|
4
|
+
type listenerHandler,
|
|
5
|
+
} from "@rrweb/types";
|
|
6
|
+
import { record } from "rrweb";
|
|
5
7
|
import type { recordOptions } from "rrweb/typings/types";
|
|
6
8
|
import type { SlimDOMOptions } from "rrweb-snapshot";
|
|
7
9
|
import { sendData } from "./analytics";
|
|
@@ -15,6 +17,7 @@ import { normalizeSamplingPercentage } from "./utils/types";
|
|
|
15
17
|
|
|
16
18
|
const RRWEB_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
|
|
17
19
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
20
|
+
const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2 * 1024 * 1024;
|
|
18
21
|
|
|
19
22
|
export interface ReplayTrackerOptions {
|
|
20
23
|
siteKey: string;
|
|
@@ -25,6 +28,7 @@ export interface ReplayTrackerOptions {
|
|
|
25
28
|
flushInterval?: number;
|
|
26
29
|
maxEvents?: number;
|
|
27
30
|
maxPendingBatches?: number;
|
|
31
|
+
maxQueueSizeBytes?: number;
|
|
28
32
|
minReplayLengthMs?: number;
|
|
29
33
|
sampling?: recordOptions<eventWithTime>["sampling"];
|
|
30
34
|
slimDOMOptions?: SlimDOMOptions;
|
|
@@ -77,7 +81,9 @@ export default class ReplayTracker {
|
|
|
77
81
|
private readonly sampled: boolean;
|
|
78
82
|
private readonly events: eventWithTime[] = [];
|
|
79
83
|
private readonly pending: ReplayBatch[] = [];
|
|
84
|
+
private pendingSizeBytes = 0;
|
|
80
85
|
|
|
86
|
+
private sessionId: string | undefined;
|
|
81
87
|
private started = false;
|
|
82
88
|
private startTime = 0;
|
|
83
89
|
private sequence = 0;
|
|
@@ -111,12 +117,16 @@ export default class ReplayTracker {
|
|
|
111
117
|
return this.options.maxPendingBatches ?? 30;
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
private get maxQueueSizeBytes(): number {
|
|
121
|
+
return this.options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
private get minReplayLengthMs(): number {
|
|
115
125
|
return this.options.minReplayLengthMs ?? 3000;
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
private get shouldCompress(): boolean {
|
|
119
|
-
return this.options.compress ??
|
|
129
|
+
return this.options.compress ?? true;
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
private log(...args: unknown[]): void {
|
|
@@ -127,9 +137,40 @@ export default class ReplayTracker {
|
|
|
127
137
|
if (this.started || typeof window === "undefined" || !this.sampled) return;
|
|
128
138
|
|
|
129
139
|
this.started = true;
|
|
130
|
-
getOrCreateSessionId();
|
|
140
|
+
this.sessionId = getOrCreateSessionId();
|
|
131
141
|
this.startTime = getSessionStart();
|
|
132
142
|
|
|
143
|
+
void this.beginRecording();
|
|
144
|
+
|
|
145
|
+
this.intervalId = setInterval(this.requestFlush, this.flushInterval);
|
|
146
|
+
|
|
147
|
+
window.addEventListener("beforeunload", this.onUnload);
|
|
148
|
+
window.addEventListener("pagehide", this.onUnload);
|
|
149
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
150
|
+
|
|
151
|
+
this.log("Recording started");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async beginRecording(): Promise<void> {
|
|
155
|
+
const wantsConsole = this.options.recordConsole ?? true;
|
|
156
|
+
|
|
157
|
+
const [{ getRecordSequentialIdPlugin }, consolePluginModule] =
|
|
158
|
+
await Promise.all([
|
|
159
|
+
import("@rrweb/rrweb-plugin-sequential-id-record"),
|
|
160
|
+
wantsConsole
|
|
161
|
+
? import("@rrweb/rrweb-plugin-console-record")
|
|
162
|
+
: Promise.resolve(null),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
if (!this.started) return;
|
|
166
|
+
|
|
167
|
+
const plugins = [
|
|
168
|
+
getRecordSequentialIdPlugin({ key: RRWEB_SEQUENTIAL_ID_KEY }),
|
|
169
|
+
];
|
|
170
|
+
if (consolePluginModule) {
|
|
171
|
+
plugins.push(consolePluginModule.getRecordConsolePlugin());
|
|
172
|
+
}
|
|
173
|
+
|
|
133
174
|
this.stopRecording = record({
|
|
134
175
|
emit: this.onEvent,
|
|
135
176
|
sampling: this.options.sampling ?? defaultSampling,
|
|
@@ -146,21 +187,8 @@ export default class ReplayTracker {
|
|
|
146
187
|
maskTextSelector: this.options.maskTextSelector,
|
|
147
188
|
checkoutEveryNms: this.options.checkoutEveryNms ?? 60_000,
|
|
148
189
|
checkoutEveryNth: this.options.checkoutEveryNth,
|
|
149
|
-
plugins
|
|
150
|
-
getRecordSequentialIdPlugin({ key: RRWEB_SEQUENTIAL_ID_KEY }),
|
|
151
|
-
...((this.options.recordConsole ?? true)
|
|
152
|
-
? [getRecordConsolePlugin()]
|
|
153
|
-
: []),
|
|
154
|
-
],
|
|
190
|
+
plugins,
|
|
155
191
|
});
|
|
156
|
-
|
|
157
|
-
this.intervalId = setInterval(this.requestFlush, this.flushInterval);
|
|
158
|
-
|
|
159
|
-
window.addEventListener("beforeunload", this.onUnload);
|
|
160
|
-
window.addEventListener("pagehide", this.onUnload);
|
|
161
|
-
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
162
|
-
|
|
163
|
-
this.log("Recording started");
|
|
164
192
|
}
|
|
165
193
|
|
|
166
194
|
stop(): void {
|
|
@@ -186,6 +214,7 @@ export default class ReplayTracker {
|
|
|
186
214
|
|
|
187
215
|
if (!this.hasReachedMinLength()) {
|
|
188
216
|
this.events.length = 0;
|
|
217
|
+
this.sessionId = undefined;
|
|
189
218
|
this.log(
|
|
190
219
|
`Session too short (${Date.now() - this.startTime}ms), discarding events`,
|
|
191
220
|
);
|
|
@@ -193,11 +222,12 @@ export default class ReplayTracker {
|
|
|
193
222
|
}
|
|
194
223
|
|
|
195
224
|
void this.flush(true);
|
|
225
|
+
this.sessionId = undefined;
|
|
196
226
|
this.log("Recording stopped");
|
|
197
227
|
}
|
|
198
228
|
|
|
199
229
|
getSessionId(): string | undefined {
|
|
200
|
-
return getOrCreateSessionId();
|
|
230
|
+
return this.sessionId ?? getOrCreateSessionId();
|
|
201
231
|
}
|
|
202
232
|
|
|
203
233
|
private onEvent = (event: eventWithTime, isCheckout?: boolean): void => {
|
|
@@ -272,7 +302,7 @@ export default class ReplayTracker {
|
|
|
272
302
|
|
|
273
303
|
return {
|
|
274
304
|
token: this.options.siteKey,
|
|
275
|
-
sessionId:
|
|
305
|
+
sessionId: this.getSessionId(),
|
|
276
306
|
...(identifier ? { identifier } : {}),
|
|
277
307
|
sequence: this.sequence++,
|
|
278
308
|
timestamp: Date.now(),
|
|
@@ -281,13 +311,52 @@ export default class ReplayTracker {
|
|
|
281
311
|
};
|
|
282
312
|
}
|
|
283
313
|
|
|
314
|
+
private getBatchSizeBytes(batch: ReplayBatch): number {
|
|
315
|
+
return new TextEncoder().encode(JSON.stringify(batch)).byteLength;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private dropOldestPendingBatch(reason: string): void {
|
|
319
|
+
const dropped = this.pending.shift();
|
|
320
|
+
if (!dropped) return;
|
|
321
|
+
|
|
322
|
+
this.pendingSizeBytes = Math.max(
|
|
323
|
+
0,
|
|
324
|
+
this.pendingSizeBytes - this.getBatchSizeBytes(dropped),
|
|
325
|
+
);
|
|
326
|
+
this.log(`${reason}, dropping batch ${dropped.sequence}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private enqueueBatch(batch: ReplayBatch): void {
|
|
330
|
+
const batchSizeBytes = this.getBatchSizeBytes(batch);
|
|
331
|
+
|
|
332
|
+
if (batchSizeBytes > this.maxQueueSizeBytes) {
|
|
333
|
+
this.log(
|
|
334
|
+
`Replay batch ${batch.sequence} is ${batchSizeBytes}B, exceeding ${this.maxQueueSizeBytes}B queue limit; dropping`,
|
|
335
|
+
);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
while (
|
|
340
|
+
this.pending.length > 0 &&
|
|
341
|
+
this.pendingSizeBytes + batchSizeBytes > this.maxQueueSizeBytes
|
|
342
|
+
) {
|
|
343
|
+
this.dropOldestPendingBatch("Pending queue size limit reached");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
while (this.pending.length >= this.maxPendingBatches) {
|
|
347
|
+
this.dropOldestPendingBatch("Pending batch limit reached");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.pending.push(batch);
|
|
351
|
+
this.pendingSizeBytes += batchSizeBytes;
|
|
352
|
+
}
|
|
353
|
+
|
|
284
354
|
private async encodeBatch(
|
|
285
355
|
batch: ReplayBatch,
|
|
286
|
-
|
|
287
|
-
): Promise<{ data: string | Blob; isCompressed: boolean }> {
|
|
356
|
+
): Promise<{ data: string | Uint8Array; isCompressed: boolean }> {
|
|
288
357
|
const json = JSON.stringify(batch);
|
|
289
358
|
|
|
290
|
-
if (!this.shouldCompress || !this.compressionSupported
|
|
359
|
+
if (!this.shouldCompress || !this.compressionSupported) {
|
|
291
360
|
return {
|
|
292
361
|
data: json,
|
|
293
362
|
isCompressed: false,
|
|
@@ -295,14 +364,18 @@ export default class ReplayTracker {
|
|
|
295
364
|
}
|
|
296
365
|
|
|
297
366
|
try {
|
|
367
|
+
const compressed = await this.compress(json);
|
|
368
|
+
this.log(
|
|
369
|
+
`Compressed ${json.length}B -> ${compressed.byteLength}B (${Math.round((compressed.byteLength / json.length) * 100)}%)`,
|
|
370
|
+
);
|
|
298
371
|
return {
|
|
299
|
-
data:
|
|
372
|
+
data: compressed,
|
|
300
373
|
isCompressed: true,
|
|
301
374
|
};
|
|
302
375
|
} catch {
|
|
303
376
|
this.log("Compression failed, using uncompressed");
|
|
304
377
|
return {
|
|
305
|
-
data:
|
|
378
|
+
data: json,
|
|
306
379
|
isCompressed: false,
|
|
307
380
|
};
|
|
308
381
|
}
|
|
@@ -316,7 +389,7 @@ export default class ReplayTracker {
|
|
|
316
389
|
this.scheduleMinLengthFlush();
|
|
317
390
|
} else {
|
|
318
391
|
const batch = this.createBatch(this.events.splice(0));
|
|
319
|
-
this.
|
|
392
|
+
this.enqueueBatch(batch);
|
|
320
393
|
}
|
|
321
394
|
}
|
|
322
395
|
|
|
@@ -328,7 +401,7 @@ export default class ReplayTracker {
|
|
|
328
401
|
const batch = this.pending[0];
|
|
329
402
|
if (!batch) break;
|
|
330
403
|
|
|
331
|
-
const encoded = await this.encodeBatch(batch
|
|
404
|
+
const encoded = await this.encodeBatch(batch);
|
|
332
405
|
const ok = await this.send(
|
|
333
406
|
encoded.data,
|
|
334
407
|
encoded.isCompressed,
|
|
@@ -336,15 +409,11 @@ export default class ReplayTracker {
|
|
|
336
409
|
);
|
|
337
410
|
|
|
338
411
|
if (!ok) {
|
|
339
|
-
if (this.pending.length >= this.maxPendingBatches) {
|
|
340
|
-
this.log(`Pending buffer full, dropping batch ${batch.sequence}`);
|
|
341
|
-
this.pending.shift();
|
|
342
|
-
}
|
|
343
412
|
this.log(`Failed to send replay batch ${batch.sequence}, retrying`);
|
|
344
413
|
this.scheduleRetry();
|
|
345
414
|
break;
|
|
346
415
|
}
|
|
347
|
-
this.
|
|
416
|
+
this.dropOldestPendingBatch("Sent replay batch");
|
|
348
417
|
lowLatency = false;
|
|
349
418
|
}
|
|
350
419
|
} finally {
|
|
@@ -360,37 +429,16 @@ export default class ReplayTracker {
|
|
|
360
429
|
}, 1000);
|
|
361
430
|
}
|
|
362
431
|
|
|
363
|
-
private async compress(data: string): Promise<
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
await writer.close();
|
|
370
|
-
|
|
371
|
-
const chunks: Uint8Array[] = [];
|
|
372
|
-
const reader = cs.readable.getReader();
|
|
373
|
-
|
|
374
|
-
while (true) {
|
|
375
|
-
const { done, value } = await reader.read();
|
|
376
|
-
if (done) break;
|
|
377
|
-
if (value) chunks.push(value);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const size = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
381
|
-
const output = new Uint8Array(size);
|
|
382
|
-
|
|
383
|
-
let offset = 0;
|
|
384
|
-
for (const chunk of chunks) {
|
|
385
|
-
output.set(chunk, offset);
|
|
386
|
-
offset += chunk.length;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return new Blob([output], { type: "application/octet-stream" });
|
|
432
|
+
private async compress(data: string): Promise<Uint8Array> {
|
|
433
|
+
const stream = new Blob([data])
|
|
434
|
+
.stream()
|
|
435
|
+
.pipeThrough(new CompressionStream("gzip"));
|
|
436
|
+
const buffer = await new Response(stream).arrayBuffer();
|
|
437
|
+
return new Uint8Array(buffer);
|
|
390
438
|
}
|
|
391
439
|
|
|
392
440
|
private send(
|
|
393
|
-
data: string |
|
|
441
|
+
data: string | Uint8Array,
|
|
394
442
|
isCompressed: boolean,
|
|
395
443
|
lowLatency: boolean,
|
|
396
444
|
): Promise<boolean> {
|
package/src/sdk.ts
ADDED