@faststats/web 0.2.7 → 0.2.9
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-BlPwufvF.js +1 -0
- package/dist/chunks/api-urls-BrkcoElX.js +1 -0
- package/dist/{error-asweBGYd.js → chunks/error-C-fHJ_1D.js} +2 -2
- package/dist/chunks/feature-flags-6rZlmhfu.d.ts +18 -0
- package/dist/chunks/feature-flags-CqVtrpX2.js +1 -0
- package/dist/chunks/replay-CKrEvguu.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 +60 -47
- 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/feature-flags.ts +2 -4
- package/src/replay.ts +104 -59
- package/src/sdk.ts +8 -0
- package/src/utils/types.ts +1 -1
- package/src/web-vitals.ts +32 -14
- package/tests/analytics.test.ts +33 -2
- package/tests/replay.test.ts +150 -21
- package/tsdown.config.ts +16 -1
- package/dist/analytics-Ct-lghlf.js +0 -1
- package/dist/module.d.ts +0 -531
- package/dist/module.js +0 -1
- package/dist/replay-DUA5c3Jf.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,29 +131,34 @@ 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(
|
|
131
138
|
externalId: string,
|
|
132
139
|
email: string,
|
|
133
140
|
options?: IdentifyOptions,
|
|
134
|
-
):
|
|
135
|
-
if (typeof window === "undefined" || isTrackingDisabled())
|
|
136
|
-
|
|
141
|
+
): Promise<boolean> {
|
|
142
|
+
if (typeof window === "undefined" || isTrackingDisabled()) {
|
|
143
|
+
return Promise.resolve(false);
|
|
144
|
+
}
|
|
145
|
+
return (
|
|
146
|
+
moduleState.instance?.identify(externalId, email, options ?? {}) ??
|
|
147
|
+
Promise.resolve(false)
|
|
148
|
+
);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
export function logout(resetAnonymousIdentity = true): void {
|
|
140
152
|
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
141
|
-
|
|
153
|
+
moduleState.instance?.logout(resetAnonymousIdentity);
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
export function setConsentMode(mode: ConsentMode): void {
|
|
145
|
-
if (
|
|
146
|
-
|
|
157
|
+
if (moduleState.instance) {
|
|
158
|
+
moduleState.instance.setConsentMode(mode);
|
|
147
159
|
return;
|
|
148
160
|
}
|
|
149
|
-
pendingConsentMode = mode;
|
|
161
|
+
moduleState.pendingConsentMode = mode;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
export function optIn(): void {
|
|
@@ -159,7 +171,7 @@ export function optOut(): void {
|
|
|
159
171
|
|
|
160
172
|
export function reportError(error: Error): void {
|
|
161
173
|
if (typeof window === "undefined" || isTrackingDisabled()) return;
|
|
162
|
-
|
|
174
|
+
moduleState.instance?.reportError(error);
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
export function isTrackingDisabled(): boolean {
|
|
@@ -190,9 +202,9 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
|
190
202
|
data instanceof Blob
|
|
191
203
|
? data
|
|
192
204
|
: typeof Blob !== "undefined"
|
|
193
|
-
? new Blob([data], { type: contentType })
|
|
205
|
+
? new Blob([data as BlobPart], { type: contentType })
|
|
194
206
|
: data;
|
|
195
|
-
if (navigator.sendBeacon(url, beaconBody)) {
|
|
207
|
+
if (navigator.sendBeacon(url, beaconBody as BodyInit)) {
|
|
196
208
|
if (debug) {
|
|
197
209
|
console.log(`${debugPrefix} Sent via beacon`);
|
|
198
210
|
}
|
|
@@ -211,7 +223,7 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
|
211
223
|
try {
|
|
212
224
|
const response = await fetch(url, {
|
|
213
225
|
method: "POST",
|
|
214
|
-
body: data,
|
|
226
|
+
body: data as BodyInit,
|
|
215
227
|
headers: {
|
|
216
228
|
"Content-Type": contentType,
|
|
217
229
|
...headers,
|
|
@@ -311,9 +323,9 @@ export class WebAnalytics {
|
|
|
311
323
|
this.consentMode = options.consent?.mode ?? "granted";
|
|
312
324
|
this.cookielessWhilePending =
|
|
313
325
|
options.consent?.cookielessWhilePending ?? true;
|
|
314
|
-
if (pendingConsentMode !== undefined) {
|
|
315
|
-
this.consentMode = pendingConsentMode;
|
|
316
|
-
pendingConsentMode = undefined;
|
|
326
|
+
if (moduleState.pendingConsentMode !== undefined) {
|
|
327
|
+
this.consentMode = moduleState.pendingConsentMode;
|
|
328
|
+
moduleState.pendingConsentMode = undefined;
|
|
317
329
|
}
|
|
318
330
|
if (options.autoTrack ?? true) this.init();
|
|
319
331
|
}
|
|
@@ -328,7 +340,7 @@ export class WebAnalytics {
|
|
|
328
340
|
this.log("disabled");
|
|
329
341
|
return;
|
|
330
342
|
}
|
|
331
|
-
|
|
343
|
+
moduleState.instance = this;
|
|
332
344
|
setCookielessMode(this.isCookielessMode());
|
|
333
345
|
setTimeout(() => void this.start(), 0);
|
|
334
346
|
}
|
|
@@ -408,7 +420,7 @@ export class WebAnalytics {
|
|
|
408
420
|
}
|
|
409
421
|
|
|
410
422
|
private registerChildTracker(tracker: ChildTracker): boolean {
|
|
411
|
-
if (!this.started || this.destroyed ||
|
|
423
|
+
if (!this.started || this.destroyed || moduleState.instance !== this) {
|
|
412
424
|
tracker.stop?.();
|
|
413
425
|
return false;
|
|
414
426
|
}
|
|
@@ -420,12 +432,15 @@ export class WebAnalytics {
|
|
|
420
432
|
private async startErrorTracker(): Promise<void> {
|
|
421
433
|
try {
|
|
422
434
|
const { default: ErrorTrackerClass } = await import("./error");
|
|
423
|
-
if (!this.started || this.destroyed ||
|
|
435
|
+
if (!this.started || this.destroyed || moduleState.instance !== this)
|
|
436
|
+
return;
|
|
424
437
|
|
|
425
438
|
const errorTracker: ErrorTracker = new ErrorTrackerClass({
|
|
426
439
|
siteKey: this.options.siteKey,
|
|
427
440
|
baseUrl: this.baseUrl,
|
|
428
441
|
debug: this.debug,
|
|
442
|
+
sdkName: this.options.sdkName,
|
|
443
|
+
sdkVersion: this.options.sdkVersion,
|
|
429
444
|
});
|
|
430
445
|
errorTracker.start();
|
|
431
446
|
if (!this.registerChildTracker(errorTracker)) return;
|
|
@@ -444,7 +459,8 @@ export class WebAnalytics {
|
|
|
444
459
|
private async startWebVitalsTracker(): Promise<void> {
|
|
445
460
|
try {
|
|
446
461
|
const { default: WebVitalsTrackerClass } = await import("./web-vitals");
|
|
447
|
-
if (!this.started || this.destroyed ||
|
|
462
|
+
if (!this.started || this.destroyed || moduleState.instance !== this)
|
|
463
|
+
return;
|
|
448
464
|
|
|
449
465
|
const webVitalsTracker: WebVitalsTracker = new WebVitalsTrackerClass({
|
|
450
466
|
siteKey: this.options.siteKey,
|
|
@@ -453,6 +469,7 @@ export class WebAnalytics {
|
|
|
453
469
|
samplingPercentage: normalizeSamplingPercentage(
|
|
454
470
|
this.options.webVitals?.sampling?.percentage,
|
|
455
471
|
),
|
|
472
|
+
attribution: this.options.webVitals?.attribution ?? false,
|
|
456
473
|
});
|
|
457
474
|
webVitalsTracker.start();
|
|
458
475
|
if (!this.registerChildTracker(webVitalsTracker)) return;
|
|
@@ -468,7 +485,8 @@ export class WebAnalytics {
|
|
|
468
485
|
): Promise<void> {
|
|
469
486
|
try {
|
|
470
487
|
const { default: ReplayTrackerClass } = await import("./replay");
|
|
471
|
-
if (!this.started || this.destroyed ||
|
|
488
|
+
if (!this.started || this.destroyed || moduleState.instance !== this)
|
|
489
|
+
return;
|
|
472
490
|
|
|
473
491
|
const replayTracker: ReplayTracker = new ReplayTrackerClass(
|
|
474
492
|
replayOptions,
|
|
@@ -484,7 +502,7 @@ export class WebAnalytics {
|
|
|
484
502
|
|
|
485
503
|
async start(): Promise<void> {
|
|
486
504
|
if (this.started || this.destroyed || typeof window === "undefined") return;
|
|
487
|
-
if (
|
|
505
|
+
if (moduleState.instance && moduleState.instance !== this) {
|
|
488
506
|
this.log("already started by another instance");
|
|
489
507
|
return;
|
|
490
508
|
}
|
|
@@ -494,7 +512,7 @@ export class WebAnalytics {
|
|
|
494
512
|
}
|
|
495
513
|
|
|
496
514
|
this.started = true;
|
|
497
|
-
|
|
515
|
+
moduleState.instance = this;
|
|
498
516
|
setCookielessMode(this.isCookielessMode());
|
|
499
517
|
getOrCreateSessionId();
|
|
500
518
|
|
|
@@ -551,8 +569,8 @@ export class WebAnalytics {
|
|
|
551
569
|
}
|
|
552
570
|
|
|
553
571
|
this.stopChildTrackers();
|
|
554
|
-
if (
|
|
555
|
-
|
|
572
|
+
if (moduleState.instance === this) {
|
|
573
|
+
moduleState.instance = null;
|
|
556
574
|
}
|
|
557
575
|
this.started = false;
|
|
558
576
|
this.destroyed = true;
|
|
@@ -575,13 +593,13 @@ export class WebAnalytics {
|
|
|
575
593
|
externalId: string,
|
|
576
594
|
email: string,
|
|
577
595
|
options: IdentifyOptions = {},
|
|
578
|
-
):
|
|
579
|
-
if (!this.ensureStarted()) return;
|
|
580
|
-
if (this.isCookielessMode()) return;
|
|
596
|
+
): Promise<boolean> {
|
|
597
|
+
if (!this.ensureStarted()) return Promise.resolve(false);
|
|
598
|
+
if (this.isCookielessMode()) return Promise.resolve(false);
|
|
581
599
|
|
|
582
600
|
const trimmedExternalId = externalId.trim();
|
|
583
601
|
const trimmedEmail = email.trim();
|
|
584
|
-
if (!trimmedExternalId || !trimmedEmail) return;
|
|
602
|
+
if (!trimmedExternalId || !trimmedEmail) return Promise.resolve(false);
|
|
585
603
|
|
|
586
604
|
const identifyEndpoint = identifyEventsUrl(this.baseUrl);
|
|
587
605
|
const payload = JSON.stringify({
|
|
@@ -595,12 +613,13 @@ export class WebAnalytics {
|
|
|
595
613
|
traits: options.traits ?? {},
|
|
596
614
|
});
|
|
597
615
|
|
|
598
|
-
|
|
616
|
+
return sendData({
|
|
599
617
|
url: identifyEndpoint,
|
|
600
618
|
data: payload,
|
|
601
619
|
contentType: "text/plain",
|
|
602
620
|
debug: this.debug,
|
|
603
621
|
debugPrefix: "[Analytics] identify",
|
|
622
|
+
useBeacon: false,
|
|
604
623
|
});
|
|
605
624
|
}
|
|
606
625
|
|
|
@@ -646,23 +665,17 @@ export class WebAnalytics {
|
|
|
646
665
|
return { value: "false" };
|
|
647
666
|
}
|
|
648
667
|
const externalId = opts?.externalId?.trim();
|
|
649
|
-
if (externalId) {
|
|
650
|
-
return fetchFeatureFlagEvaluation(key, {
|
|
651
|
-
baseUrl: this.featureFlagsBaseUrl,
|
|
652
|
-
projectToken: this.options.siteKey,
|
|
653
|
-
externalId,
|
|
654
|
-
attributes,
|
|
655
|
-
signal: opts?.signal,
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
668
|
const identifier = this.getAnonymousId();
|
|
659
|
-
if (!identifier) {
|
|
669
|
+
if (!externalId && !identifier) {
|
|
660
670
|
return { value: "false" };
|
|
661
671
|
}
|
|
672
|
+
|
|
673
|
+
const { fetchFeatureFlagEvaluation } = await import("./feature-flags");
|
|
662
674
|
return fetchFeatureFlagEvaluation(key, {
|
|
663
675
|
baseUrl: this.featureFlagsBaseUrl,
|
|
664
676
|
projectToken: this.options.siteKey,
|
|
665
|
-
identifier,
|
|
677
|
+
...(identifier ? { identifier } : {}),
|
|
678
|
+
...(externalId ? { externalId } : {}),
|
|
666
679
|
attributes,
|
|
667
680
|
signal: opts?.signal,
|
|
668
681
|
});
|
|
@@ -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/feature-flags.ts
CHANGED
|
@@ -23,9 +23,6 @@ export async function fetchFeatureFlagEvaluation(
|
|
|
23
23
|
if (context.projectToken && context.projectId) {
|
|
24
24
|
throw new Error("provide either projectToken or projectId, not both");
|
|
25
25
|
}
|
|
26
|
-
if (context.identifier && context.externalId) {
|
|
27
|
-
throw new Error("provide either serverId or externalId, not both");
|
|
28
|
-
}
|
|
29
26
|
|
|
30
27
|
const projectToken = context.projectToken;
|
|
31
28
|
const projectId = context.projectId;
|
|
@@ -42,7 +39,8 @@ export async function fetchFeatureFlagEvaluation(
|
|
|
42
39
|
const body: Record<string, unknown> = {
|
|
43
40
|
key: flagKey,
|
|
44
41
|
...(projectId ? { projectId } : {}),
|
|
45
|
-
...(identifier ? { identifier } : {
|
|
42
|
+
...(identifier ? { identifier } : {}),
|
|
43
|
+
...(externalId ? { externalId } : {}),
|
|
46
44
|
};
|
|
47
45
|
|
|
48
46
|
if (context.attributes && Object.keys(context.attributes).length > 0) {
|
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,6 +81,7 @@ 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
|
|
|
81
86
|
private sessionId: string | undefined;
|
|
82
87
|
private started = false;
|
|
@@ -112,12 +117,16 @@ export default class ReplayTracker {
|
|
|
112
117
|
return this.options.maxPendingBatches ?? 30;
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
private get maxQueueSizeBytes(): number {
|
|
121
|
+
return this.options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
private get minReplayLengthMs(): number {
|
|
116
125
|
return this.options.minReplayLengthMs ?? 3000;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
private get shouldCompress(): boolean {
|
|
120
|
-
return this.options.compress ??
|
|
129
|
+
return this.options.compress ?? true;
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
private log(...args: unknown[]): void {
|
|
@@ -131,6 +140,37 @@ export default class ReplayTracker {
|
|
|
131
140
|
this.sessionId = getOrCreateSessionId();
|
|
132
141
|
this.startTime = getSessionStart();
|
|
133
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
|
+
|
|
134
174
|
this.stopRecording = record({
|
|
135
175
|
emit: this.onEvent,
|
|
136
176
|
sampling: this.options.sampling ?? defaultSampling,
|
|
@@ -147,21 +187,8 @@ export default class ReplayTracker {
|
|
|
147
187
|
maskTextSelector: this.options.maskTextSelector,
|
|
148
188
|
checkoutEveryNms: this.options.checkoutEveryNms ?? 60_000,
|
|
149
189
|
checkoutEveryNth: this.options.checkoutEveryNth,
|
|
150
|
-
plugins
|
|
151
|
-
getRecordSequentialIdPlugin({ key: RRWEB_SEQUENTIAL_ID_KEY }),
|
|
152
|
-
...((this.options.recordConsole ?? true)
|
|
153
|
-
? [getRecordConsolePlugin()]
|
|
154
|
-
: []),
|
|
155
|
-
],
|
|
190
|
+
plugins,
|
|
156
191
|
});
|
|
157
|
-
|
|
158
|
-
this.intervalId = setInterval(this.requestFlush, this.flushInterval);
|
|
159
|
-
|
|
160
|
-
window.addEventListener("beforeunload", this.onUnload);
|
|
161
|
-
window.addEventListener("pagehide", this.onUnload);
|
|
162
|
-
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
163
|
-
|
|
164
|
-
this.log("Recording started");
|
|
165
192
|
}
|
|
166
193
|
|
|
167
194
|
stop(): void {
|
|
@@ -284,13 +311,52 @@ export default class ReplayTracker {
|
|
|
284
311
|
};
|
|
285
312
|
}
|
|
286
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
|
+
|
|
287
354
|
private async encodeBatch(
|
|
288
355
|
batch: ReplayBatch,
|
|
289
|
-
|
|
290
|
-
): Promise<{ data: string | Blob; isCompressed: boolean }> {
|
|
356
|
+
): Promise<{ data: string | Uint8Array; isCompressed: boolean }> {
|
|
291
357
|
const json = JSON.stringify(batch);
|
|
292
358
|
|
|
293
|
-
if (!this.shouldCompress || !this.compressionSupported
|
|
359
|
+
if (!this.shouldCompress || !this.compressionSupported) {
|
|
294
360
|
return {
|
|
295
361
|
data: json,
|
|
296
362
|
isCompressed: false,
|
|
@@ -298,14 +364,18 @@ export default class ReplayTracker {
|
|
|
298
364
|
}
|
|
299
365
|
|
|
300
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
|
+
);
|
|
301
371
|
return {
|
|
302
|
-
data:
|
|
372
|
+
data: compressed,
|
|
303
373
|
isCompressed: true,
|
|
304
374
|
};
|
|
305
375
|
} catch {
|
|
306
376
|
this.log("Compression failed, using uncompressed");
|
|
307
377
|
return {
|
|
308
|
-
data:
|
|
378
|
+
data: json,
|
|
309
379
|
isCompressed: false,
|
|
310
380
|
};
|
|
311
381
|
}
|
|
@@ -319,7 +389,7 @@ export default class ReplayTracker {
|
|
|
319
389
|
this.scheduleMinLengthFlush();
|
|
320
390
|
} else {
|
|
321
391
|
const batch = this.createBatch(this.events.splice(0));
|
|
322
|
-
this.
|
|
392
|
+
this.enqueueBatch(batch);
|
|
323
393
|
}
|
|
324
394
|
}
|
|
325
395
|
|
|
@@ -331,7 +401,7 @@ export default class ReplayTracker {
|
|
|
331
401
|
const batch = this.pending[0];
|
|
332
402
|
if (!batch) break;
|
|
333
403
|
|
|
334
|
-
const encoded = await this.encodeBatch(batch
|
|
404
|
+
const encoded = await this.encodeBatch(batch);
|
|
335
405
|
const ok = await this.send(
|
|
336
406
|
encoded.data,
|
|
337
407
|
encoded.isCompressed,
|
|
@@ -339,15 +409,11 @@ export default class ReplayTracker {
|
|
|
339
409
|
);
|
|
340
410
|
|
|
341
411
|
if (!ok) {
|
|
342
|
-
if (this.pending.length >= this.maxPendingBatches) {
|
|
343
|
-
this.log(`Pending buffer full, dropping batch ${batch.sequence}`);
|
|
344
|
-
this.pending.shift();
|
|
345
|
-
}
|
|
346
412
|
this.log(`Failed to send replay batch ${batch.sequence}, retrying`);
|
|
347
413
|
this.scheduleRetry();
|
|
348
414
|
break;
|
|
349
415
|
}
|
|
350
|
-
this.
|
|
416
|
+
this.dropOldestPendingBatch("Sent replay batch");
|
|
351
417
|
lowLatency = false;
|
|
352
418
|
}
|
|
353
419
|
} finally {
|
|
@@ -363,37 +429,16 @@ export default class ReplayTracker {
|
|
|
363
429
|
}, 1000);
|
|
364
430
|
}
|
|
365
431
|
|
|
366
|
-
private async compress(data: string): Promise<
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
await writer.close();
|
|
373
|
-
|
|
374
|
-
const chunks: Uint8Array[] = [];
|
|
375
|
-
const reader = cs.readable.getReader();
|
|
376
|
-
|
|
377
|
-
while (true) {
|
|
378
|
-
const { done, value } = await reader.read();
|
|
379
|
-
if (done) break;
|
|
380
|
-
if (value) chunks.push(value);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const size = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
384
|
-
const output = new Uint8Array(size);
|
|
385
|
-
|
|
386
|
-
let offset = 0;
|
|
387
|
-
for (const chunk of chunks) {
|
|
388
|
-
output.set(chunk, offset);
|
|
389
|
-
offset += chunk.length;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
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);
|
|
393
438
|
}
|
|
394
439
|
|
|
395
440
|
private send(
|
|
396
|
-
data: string |
|
|
441
|
+
data: string | Uint8Array,
|
|
397
442
|
isCompressed: boolean,
|
|
398
443
|
lowLatency: boolean,
|
|
399
444
|
): Promise<boolean> {
|
package/src/sdk.ts
ADDED