@faststats/web 0.6.0 → 0.7.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-CaNa8upY.js → api-urls-Dp2XNjRF.js} +7 -6
- package/dist/chunks/identifiers-BvRNbiwA.js +21 -0
- package/dist/chunks/{replay-DgodPHzA.d.ts → replay-BeC2XA4N.d.ts} +23 -3
- package/dist/chunks/{send-data-DjdbGDDc.js → send-data-DR1JUXka.js} +34 -29
- package/dist/error.d.ts +2 -2
- package/dist/error.js +60 -52
- package/dist/feature-flags.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +44 -26
- package/dist/replay.d.ts +1 -1
- package/dist/replay.js +110 -37
- package/dist/web-vitals.d.ts +3 -4
- package/dist/web-vitals.js +21 -42
- package/package.json +1 -1
- package/dist/chunks/identifiers-cUymVK5H.js +0 -20
|
@@ -11,12 +11,12 @@ const URLS = {
|
|
|
11
11
|
vitals: "/v1/vitals",
|
|
12
12
|
flags: "/v1/check"
|
|
13
13
|
};
|
|
14
|
-
function sanitizeUrl(url) {
|
|
14
|
+
function sanitizeUrl(url, includeHash = false) {
|
|
15
15
|
try {
|
|
16
16
|
const parsed = new URL(url, location.href);
|
|
17
|
-
return `${parsed.origin}${parsed.pathname}`;
|
|
17
|
+
return `${parsed.origin}${parsed.pathname}${includeHash ? parsed.hash : ""}`;
|
|
18
18
|
} catch {
|
|
19
|
-
return url.split(/[?#]/, 1)[0] ?? "";
|
|
19
|
+
return url.split(includeHash ? "?" : /[?#]/, 1)[0] ?? "";
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
const UTM_KEYS = [
|
|
@@ -26,10 +26,11 @@ const UTM_KEYS = [
|
|
|
26
26
|
"utm_term",
|
|
27
27
|
"utm_content"
|
|
28
28
|
];
|
|
29
|
-
function getPageContext() {
|
|
29
|
+
function getPageContext(includeHash = false) {
|
|
30
|
+
const hash = includeHash ? location.hash : "";
|
|
30
31
|
const ctx = {
|
|
31
|
-
page: location.pathname
|
|
32
|
-
url: sanitizeUrl(location.href),
|
|
32
|
+
page: `${location.pathname}${hash}`,
|
|
33
|
+
url: sanitizeUrl(location.href, includeHash),
|
|
33
34
|
referrer: document.referrer ? sanitizeUrl(document.referrer) : null,
|
|
34
35
|
title: document.title || ""
|
|
35
36
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { d as setStorageItem, i as isCookielessMode, l as getStorageItem, n as createId, u as removeStorageItem } from "./send-data-DR1JUXka.js";
|
|
2
|
+
//#region src/utils/identifiers.ts
|
|
3
|
+
const ANONYMOUS_ID_KEY = "faststats_anon_id_";
|
|
4
|
+
function getOrCreateAnonymousId(siteKey) {
|
|
5
|
+
const key = `${ANONYMOUS_ID_KEY}${siteKey}`;
|
|
6
|
+
const existingId = getStorageItem("localStorage", key);
|
|
7
|
+
if (existingId) return existingId;
|
|
8
|
+
const newId = createId();
|
|
9
|
+
return setStorageItem("localStorage", key, newId) ? newId : "";
|
|
10
|
+
}
|
|
11
|
+
function getAnonymousId(siteKey, cookieless) {
|
|
12
|
+
if (cookieless ?? isCookielessMode()) return "";
|
|
13
|
+
return getOrCreateAnonymousId(siteKey);
|
|
14
|
+
}
|
|
15
|
+
function resetAnonymousId(siteKey, cookieless) {
|
|
16
|
+
if (cookieless) return "";
|
|
17
|
+
if (!removeStorageItem("localStorage", `${ANONYMOUS_ID_KEY}${siteKey}`)) return "";
|
|
18
|
+
return getOrCreateAnonymousId(siteKey);
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { resetAnonymousId as n, getAnonymousId as t };
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { record } from "@rrweb/record";
|
|
2
2
|
import { LogLevel } from "@rrweb/rrweb-plugin-console-record";
|
|
3
3
|
|
|
4
|
+
//#region ../../node_modules/.bun/@rrweb+types@2.1.0/node_modules/@rrweb/types/dist/index.d.ts
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
FontFace: typeof FontFace;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/replay-mutation-throttler.d.ts
|
|
12
|
+
type ReplayMutationThrottleOptions = {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
bucketSize?: number;
|
|
15
|
+
refillRate?: number;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
4
18
|
//#region src/replay.d.ts
|
|
5
19
|
type RecordOptions = NonNullable<Parameters<typeof record>[0]>;
|
|
6
20
|
interface ReplayTrackerOptions {
|
|
7
21
|
siteKey: string;
|
|
8
22
|
baseUrl?: string;
|
|
9
23
|
debug?: boolean;
|
|
24
|
+
trackHash?: boolean;
|
|
10
25
|
compress?: boolean;
|
|
11
26
|
flushInterval?: number;
|
|
12
27
|
maxEvents?: number;
|
|
@@ -29,6 +44,7 @@ interface ReplayTrackerOptions {
|
|
|
29
44
|
consoleLevel?: LogLevel[];
|
|
30
45
|
consoleLengthThreshold?: number;
|
|
31
46
|
consoleStringLengthLimit?: number;
|
|
47
|
+
mutationThrottle?: ReplayMutationThrottleOptions;
|
|
32
48
|
}
|
|
33
49
|
declare class ReplayTracker {
|
|
34
50
|
private readonly options;
|
|
@@ -47,6 +63,7 @@ declare class ReplayTracker {
|
|
|
47
63
|
private queuedEventsSizeBytes;
|
|
48
64
|
private pendingSizeBytes;
|
|
49
65
|
private viewId;
|
|
66
|
+
private viewUrl;
|
|
50
67
|
private chunkStartedAt;
|
|
51
68
|
private started;
|
|
52
69
|
private disposed;
|
|
@@ -64,6 +81,9 @@ declare class ReplayTracker {
|
|
|
64
81
|
private queuedFlush;
|
|
65
82
|
private unsubscribeRotation?;
|
|
66
83
|
private lastCookielessMode;
|
|
84
|
+
private terminalFlushRequested;
|
|
85
|
+
private overflowed;
|
|
86
|
+
private readonly mutationThrottler;
|
|
67
87
|
constructor(options: ReplayTrackerOptions);
|
|
68
88
|
private log;
|
|
69
89
|
private clearFlushTimers;
|
|
@@ -75,15 +95,16 @@ declare class ReplayTracker {
|
|
|
75
95
|
setCookielessMode(cookieless: boolean): void;
|
|
76
96
|
start(): void;
|
|
77
97
|
trackPageChange(url?: string): void;
|
|
78
|
-
onPageHidden(persisted?: boolean): void;
|
|
98
|
+
onPageHidden(persisted?: boolean, terminal?: boolean): void;
|
|
79
99
|
onPageShow(persisted?: boolean): void;
|
|
80
100
|
private beginRecording;
|
|
81
|
-
stop(): void;
|
|
101
|
+
stop(discard?: boolean): void;
|
|
82
102
|
private onSessionRotated;
|
|
83
103
|
private applySessionRotation;
|
|
84
104
|
private resetSessionTiming;
|
|
85
105
|
private onEvent;
|
|
86
106
|
private onUnload;
|
|
107
|
+
private requestTerminalFlush;
|
|
87
108
|
private hasReachedMinLength;
|
|
88
109
|
private requestFlush;
|
|
89
110
|
private scheduleMinLengthFlush;
|
|
@@ -92,7 +113,6 @@ declare class ReplayTracker {
|
|
|
92
113
|
private createBatch;
|
|
93
114
|
private eventSizeBytes;
|
|
94
115
|
private batchSizeBytes;
|
|
95
|
-
private dropOldestPending;
|
|
96
116
|
private removePending;
|
|
97
117
|
private enqueueBatch;
|
|
98
118
|
private flush;
|
|
@@ -30,29 +30,30 @@ function removeStorageItem(name, key) {
|
|
|
30
30
|
//#region src/utils/session-manager.ts
|
|
31
31
|
const IDLE_MS = 1800 * 1e3;
|
|
32
32
|
const MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
33
|
-
const SESSION_KEY = "
|
|
33
|
+
const SESSION_KEY = "faststats_session_";
|
|
34
34
|
let cookielessMode = false;
|
|
35
|
-
|
|
35
|
+
const memorySessions = /* @__PURE__ */ new Map();
|
|
36
36
|
const rotationListeners = /* @__PURE__ */ new Set();
|
|
37
37
|
function createId() {
|
|
38
38
|
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
|
|
39
39
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
40
40
|
}
|
|
41
|
-
function readSession() {
|
|
42
|
-
const
|
|
43
|
-
|
|
41
|
+
function readSession(siteKey) {
|
|
42
|
+
const memory = memorySessions.get(siteKey) ?? null;
|
|
43
|
+
const raw = getStorageItem("localStorage", `${SESSION_KEY}${siteKey}`);
|
|
44
|
+
if (!raw) return memory;
|
|
44
45
|
try {
|
|
45
46
|
const session = JSON.parse(raw);
|
|
46
|
-
return session.id ? session :
|
|
47
|
+
return session.id ? session : memory;
|
|
47
48
|
} catch {}
|
|
48
|
-
return
|
|
49
|
+
return memory;
|
|
49
50
|
}
|
|
50
|
-
function writeSession(session) {
|
|
51
|
-
if (!setStorageItem("localStorage", SESSION_KEY
|
|
51
|
+
function writeSession(siteKey, session) {
|
|
52
|
+
if (!setStorageItem("localStorage", `${SESSION_KEY}${siteKey}`, JSON.stringify(session))) memorySessions.set(siteKey, session);
|
|
52
53
|
}
|
|
53
|
-
function clearSession() {
|
|
54
|
-
|
|
55
|
-
removeStorageItem("localStorage", SESSION_KEY);
|
|
54
|
+
function clearSession(siteKey) {
|
|
55
|
+
memorySessions.delete(siteKey);
|
|
56
|
+
removeStorageItem("localStorage", `${SESSION_KEY}${siteKey}`);
|
|
56
57
|
}
|
|
57
58
|
function expired(session, now) {
|
|
58
59
|
return now - session.activity >= IDLE_MS || now - session.start >= MAX_AGE_MS;
|
|
@@ -60,25 +61,28 @@ function expired(session, now) {
|
|
|
60
61
|
function emitRotation(prev, next) {
|
|
61
62
|
for (const listener of rotationListeners) listener(prev, next);
|
|
62
63
|
}
|
|
63
|
-
function resolveSession(cookieless, touch) {
|
|
64
|
+
function resolveSession(siteKey, cookieless, touch) {
|
|
64
65
|
const now = Date.now();
|
|
65
66
|
if (cookieless) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
let session = memorySessions.get(siteKey);
|
|
68
|
+
if (!session) {
|
|
69
|
+
session = {
|
|
70
|
+
id: createId(),
|
|
71
|
+
start: now,
|
|
72
|
+
activity: now
|
|
73
|
+
};
|
|
74
|
+
memorySessions.set(siteKey, session);
|
|
75
|
+
} else if (touch) session.activity = now;
|
|
72
76
|
return {
|
|
73
|
-
session
|
|
77
|
+
session,
|
|
74
78
|
rotatedFrom: null
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
|
-
const existing = readSession();
|
|
81
|
+
const existing = readSession(siteKey);
|
|
78
82
|
if (existing && !expired(existing, now)) {
|
|
79
83
|
if (touch) {
|
|
80
84
|
existing.activity = now;
|
|
81
|
-
writeSession(existing);
|
|
85
|
+
writeSession(siteKey, existing);
|
|
82
86
|
}
|
|
83
87
|
return {
|
|
84
88
|
session: existing,
|
|
@@ -90,7 +94,7 @@ function resolveSession(cookieless, touch) {
|
|
|
90
94
|
start: now,
|
|
91
95
|
activity: now
|
|
92
96
|
};
|
|
93
|
-
writeSession(session);
|
|
97
|
+
writeSession(siteKey, session);
|
|
94
98
|
return {
|
|
95
99
|
session,
|
|
96
100
|
rotatedFrom: existing
|
|
@@ -112,22 +116,23 @@ function toContext(siteKey, session, cookieless, windowIdOverride) {
|
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
118
|
function setCookielessMode(mode) {
|
|
119
|
+
if (cookielessMode === mode) return;
|
|
115
120
|
cookielessMode = mode;
|
|
116
|
-
|
|
121
|
+
memorySessions.clear();
|
|
117
122
|
}
|
|
118
123
|
function isCookielessMode() {
|
|
119
124
|
return cookielessMode;
|
|
120
125
|
}
|
|
121
126
|
function getSessionContext(siteKey, cookieless) {
|
|
122
127
|
const useCookieless = cookieless ?? cookielessMode;
|
|
123
|
-
const { session, rotatedFrom } = resolveSession(useCookieless, false);
|
|
128
|
+
const { session, rotatedFrom } = resolveSession(siteKey, useCookieless, false);
|
|
124
129
|
const next = toContext(siteKey, session, useCookieless);
|
|
125
130
|
if (rotatedFrom) emitRotation(toContext(siteKey, rotatedFrom, useCookieless, next.windowId), next);
|
|
126
131
|
return next;
|
|
127
132
|
}
|
|
128
133
|
function touchActivity(siteKey, cookieless) {
|
|
129
134
|
const useCookieless = cookieless ?? cookielessMode;
|
|
130
|
-
const { session, rotatedFrom } = resolveSession(useCookieless, true);
|
|
135
|
+
const { session, rotatedFrom } = resolveSession(siteKey, useCookieless, true);
|
|
131
136
|
if (!rotatedFrom) return null;
|
|
132
137
|
const wid = windowId(siteKey, useCookieless, session.id);
|
|
133
138
|
const rotation = {
|
|
@@ -140,14 +145,14 @@ function touchActivity(siteKey, cookieless) {
|
|
|
140
145
|
function resetSession(siteKey) {
|
|
141
146
|
const useCookieless = cookielessMode;
|
|
142
147
|
const previous = getSessionContext(siteKey, useCookieless);
|
|
143
|
-
clearSession();
|
|
148
|
+
clearSession(siteKey);
|
|
144
149
|
if (useCookieless) {
|
|
145
150
|
const now = Date.now();
|
|
146
|
-
|
|
151
|
+
memorySessions.set(siteKey, {
|
|
147
152
|
id: createId(),
|
|
148
153
|
start: now,
|
|
149
154
|
activity: now
|
|
150
|
-
};
|
|
155
|
+
});
|
|
151
156
|
} else removeStorageItem("sessionStorage", `faststats_window_id_${siteKey}`);
|
|
152
157
|
const next = getSessionContext(siteKey, useCookieless);
|
|
153
158
|
if (previous.sessionId !== next.sessionId) emitRotation(previous, next);
|
package/dist/error.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface ErrorTrackingOptions {
|
|
|
9
9
|
siteKey: string;
|
|
10
10
|
baseUrl?: string;
|
|
11
11
|
debug?: boolean;
|
|
12
|
+
trackHash?: boolean;
|
|
12
13
|
flushInterval?: number;
|
|
13
14
|
maxQueueSize?: number;
|
|
14
15
|
sdkName?: string;
|
|
@@ -24,12 +25,11 @@ declare class ErrorTracker {
|
|
|
24
25
|
constructor(options: ErrorTrackingOptions);
|
|
25
26
|
private log;
|
|
26
27
|
start(): void;
|
|
27
|
-
stop(): void;
|
|
28
|
+
stop(discard?: boolean): void;
|
|
28
29
|
onPageHidden(persisted?: boolean): void;
|
|
29
30
|
captureError(error: Error): void;
|
|
30
31
|
private onError;
|
|
31
32
|
private onRejection;
|
|
32
|
-
private addToQueue;
|
|
33
33
|
private enqueue;
|
|
34
34
|
private flush;
|
|
35
35
|
}
|
package/dist/error.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { a as resolveBaseUrl, i as getPageContext, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-
|
|
2
|
-
import { r as getSessionContext, t as sendData } from "./chunks/send-data-
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-
|
|
1
|
+
import { a as resolveBaseUrl, i as getPageContext, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-Dp2XNjRF.js";
|
|
2
|
+
import { r as getSessionContext, t as sendData } from "./chunks/send-data-DR1JUXka.js";
|
|
3
|
+
import { t as getAnonymousId } from "./chunks/identifiers-BvRNbiwA.js";
|
|
4
4
|
//#region src/error.ts
|
|
5
5
|
const SDK_NAME = "@faststats/web";
|
|
6
|
-
const SDK_VERSION = "0.
|
|
6
|
+
const SDK_VERSION = "0.7.0";
|
|
7
7
|
const EXTENSION_RE = /(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
|
|
8
8
|
function toEntry(kind, source, handled, fallback) {
|
|
9
9
|
if (source instanceof Error) return {
|
|
10
|
-
error: kind,
|
|
10
|
+
error: kind === "UnhandledRejection" ? kind : source.name.trim() || kind,
|
|
11
11
|
message: source.message || fallback,
|
|
12
12
|
handled,
|
|
13
13
|
stack: source.stack?.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
@@ -18,9 +18,6 @@ function toEntry(kind, source, handled, fallback) {
|
|
|
18
18
|
handled
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
function entryKey({ error, message, handled }) {
|
|
22
|
-
return `${error}\0${message ?? ""}\0${handled}`;
|
|
23
|
-
}
|
|
24
21
|
function isExtensionError(filename, source) {
|
|
25
22
|
const stack = source instanceof Error ? source.stack ?? "" : "";
|
|
26
23
|
return EXTENSION_RE.test(`${filename}\n${stack}`);
|
|
@@ -28,7 +25,7 @@ function isExtensionError(filename, source) {
|
|
|
28
25
|
var ErrorTracker = class {
|
|
29
26
|
options;
|
|
30
27
|
endpoint;
|
|
31
|
-
queue =
|
|
28
|
+
queue = [];
|
|
32
29
|
timer = null;
|
|
33
30
|
started = false;
|
|
34
31
|
flushing = false;
|
|
@@ -47,19 +44,19 @@ var ErrorTracker = class {
|
|
|
47
44
|
this.timer = setInterval(() => void this.flush(), this.options.flushInterval ?? 5e3);
|
|
48
45
|
this.log("Started");
|
|
49
46
|
}
|
|
50
|
-
stop() {
|
|
47
|
+
stop(discard = false) {
|
|
51
48
|
if (!this.started || typeof window === "undefined") return;
|
|
52
49
|
this.started = false;
|
|
53
50
|
window.removeEventListener("error", this.onError);
|
|
54
51
|
window.removeEventListener("unhandledrejection", this.onRejection);
|
|
55
52
|
if (this.timer) clearInterval(this.timer);
|
|
56
53
|
this.timer = null;
|
|
57
|
-
this.
|
|
54
|
+
if (discard) this.queue = [];
|
|
55
|
+
else this.flush();
|
|
58
56
|
this.log("Stopped");
|
|
59
57
|
}
|
|
60
58
|
onPageHidden(persisted = false) {
|
|
61
|
-
if (persisted)
|
|
62
|
-
this.flush();
|
|
59
|
+
if (!persisted) this.flush();
|
|
63
60
|
}
|
|
64
61
|
captureError(error) {
|
|
65
62
|
this.enqueue(toEntry("Error", error, true, error.message));
|
|
@@ -72,55 +69,66 @@ var ErrorTracker = class {
|
|
|
72
69
|
if (isExtensionError("", event.reason)) return;
|
|
73
70
|
this.enqueue(toEntry("UnhandledRejection", event.reason, false, "Unhandled promise rejection"));
|
|
74
71
|
};
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
enqueue(error) {
|
|
73
|
+
const buildId = globalThis.__SOURCEMAPS_BUILD__?.buildId;
|
|
74
|
+
const userId = getAnonymousId(this.options.siteKey);
|
|
75
|
+
const ctx = getSessionContext(this.options.siteKey);
|
|
76
|
+
this.log("Captured:", error);
|
|
77
|
+
this.queue.push({
|
|
78
|
+
error,
|
|
79
|
+
context: {
|
|
80
|
+
token: this.options.siteKey,
|
|
81
|
+
...userId ? { userId } : {},
|
|
82
|
+
sessionId: ctx.sessionId,
|
|
83
|
+
windowId: ctx.windowId,
|
|
84
|
+
...typeof buildId === "string" && buildId.trim() ? { buildId } : {},
|
|
85
|
+
sdkName: this.options.sdkName ?? SDK_NAME,
|
|
86
|
+
sdkVersion: this.options.sdkVersion ?? SDK_VERSION,
|
|
87
|
+
event: "error",
|
|
88
|
+
...getPageContext(this.options.trackHash ?? false),
|
|
89
|
+
properties: {}
|
|
90
|
+
}
|
|
85
91
|
});
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
enqueue(entry) {
|
|
89
|
-
if (this.addToQueue(entry)) this.log("Captured:", entry);
|
|
90
|
-
if (this.queue.size >= (this.options.maxQueueSize ?? 50)) this.flush();
|
|
92
|
+
if (this.queue.length >= (this.options.maxQueueSize ?? 50)) this.flush();
|
|
91
93
|
}
|
|
92
94
|
async flush() {
|
|
93
|
-
if (this.flushing || this.queue.
|
|
95
|
+
if (this.flushing || !this.queue.length) return;
|
|
94
96
|
this.flushing = true;
|
|
95
97
|
const batch = this.queue;
|
|
96
|
-
this.queue =
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
this.queue = [];
|
|
99
|
+
const groups = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const item of batch) {
|
|
101
|
+
const key = JSON.stringify(item.context);
|
|
102
|
+
const group = groups.get(key) ?? {
|
|
103
|
+
context: item.context,
|
|
104
|
+
items: []
|
|
105
|
+
};
|
|
106
|
+
group.items.push(item);
|
|
107
|
+
groups.set(key, group);
|
|
108
|
+
}
|
|
109
|
+
const failed = await Promise.all([...groups.values()].map(async (group) => {
|
|
110
|
+
const errors = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const { error } of group.items) {
|
|
112
|
+
const key = JSON.stringify(error);
|
|
113
|
+
const existing = errors.get(key);
|
|
114
|
+
if (existing) existing.count++;
|
|
115
|
+
else errors.set(key, {
|
|
116
|
+
...error,
|
|
117
|
+
count: 1
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return await sendData({
|
|
104
121
|
url: this.endpoint,
|
|
105
122
|
data: JSON.stringify({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
sessionId: ctx.sessionId,
|
|
109
|
-
windowId: ctx.windowId,
|
|
110
|
-
...typeof buildId === "string" && buildId.trim() ? { buildId } : {},
|
|
111
|
-
sdkName: this.options.sdkName ?? SDK_NAME,
|
|
112
|
-
sdkVersion: this.options.sdkVersion ?? SDK_VERSION,
|
|
113
|
-
event: "error",
|
|
114
|
-
...getPageContext(),
|
|
115
|
-
properties: {},
|
|
116
|
-
errors
|
|
123
|
+
...group.context,
|
|
124
|
+
errors: [...errors.values()]
|
|
117
125
|
}),
|
|
118
126
|
debug: this.options.debug,
|
|
119
127
|
debugPrefix: "[ErrorTracker]"
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
}) ? [] : group.items;
|
|
129
|
+
}));
|
|
130
|
+
this.queue.unshift(...failed.flat());
|
|
131
|
+
this.flushing = false;
|
|
124
132
|
}
|
|
125
133
|
};
|
|
126
134
|
//#endregion
|
package/dist/feature-flags.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as resolveBaseUrl, n as FEATURE_FLAGS_BASE, r as URLS } from "./chunks/api-urls-
|
|
1
|
+
import { a as resolveBaseUrl, n as FEATURE_FLAGS_BASE, r as URLS } from "./chunks/api-urls-Dp2XNjRF.js";
|
|
2
2
|
//#region src/feature-flags.ts
|
|
3
3
|
async function fetchFeatureFlagEvaluation(flagKey, context) {
|
|
4
4
|
if (!context.projectToken) throw new Error("feature flag check requires projectToken");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FeatureFlagEvaluation } from "./feature-flags.js";
|
|
2
|
-
import { n as ReplayTrackerOptions } from "./chunks/replay-
|
|
2
|
+
import { n as ReplayTrackerOptions } from "./chunks/replay-BeC2XA4N.js";
|
|
3
3
|
|
|
4
4
|
//#region src/analytics.d.ts
|
|
5
5
|
type ConsentMode = "pending" | "granted" | "denied";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as resolveBaseUrl, i as getPageContext, n as FEATURE_FLAGS_BASE, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-
|
|
2
|
-
import { c as touchActivity, l as getStorageItem, o as resetSession, r as getSessionContext, s as setCookielessMode, t as sendData } from "./chunks/send-data-
|
|
3
|
-
import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-
|
|
1
|
+
import { a as resolveBaseUrl, i as getPageContext, n as FEATURE_FLAGS_BASE, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-Dp2XNjRF.js";
|
|
2
|
+
import { c as touchActivity, l as getStorageItem, o as resetSession, r as getSessionContext, s as setCookielessMode, t as sendData } from "./chunks/send-data-DR1JUXka.js";
|
|
3
|
+
import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-BvRNbiwA.js";
|
|
4
4
|
//#region src/outbound-links.ts
|
|
5
5
|
const DEDUPE_MS = 500;
|
|
6
6
|
var OutboundLinkTracker = class {
|
|
@@ -40,7 +40,7 @@ function findOutboundAnchor(event) {
|
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
42
|
function isOutboundAnchor(node) {
|
|
43
|
-
return node instanceof HTMLAnchorElement &&
|
|
43
|
+
return node instanceof HTMLAnchorElement && (node.protocol === "http:" || node.protocol === "https:") && node.host !== location.host;
|
|
44
44
|
}
|
|
45
45
|
//#endregion
|
|
46
46
|
//#region src/analytics.ts
|
|
@@ -91,7 +91,7 @@ var WebAnalytics = class {
|
|
|
91
91
|
entry = {
|
|
92
92
|
path: "",
|
|
93
93
|
url: "",
|
|
94
|
-
|
|
94
|
+
search: ""
|
|
95
95
|
};
|
|
96
96
|
left = false;
|
|
97
97
|
visitDuration = 0;
|
|
@@ -130,13 +130,13 @@ var WebAnalytics = class {
|
|
|
130
130
|
return this.started && !this.destroyed && instance === this;
|
|
131
131
|
}
|
|
132
132
|
canTrack() {
|
|
133
|
-
return
|
|
133
|
+
return this.consentMode === "granted" || this.consentMode === "pending" && this.pendingBehavior === "anonymous";
|
|
134
134
|
}
|
|
135
135
|
canSendEvents() {
|
|
136
136
|
return !this.isTrackingBlocked() && this.canTrack();
|
|
137
137
|
}
|
|
138
138
|
cookieless() {
|
|
139
|
-
return !!this.options.cookieless || this.consentMode === "
|
|
139
|
+
return !!this.options.cookieless || this.consentMode === "pending" && this.pendingBehavior === "anonymous";
|
|
140
140
|
}
|
|
141
141
|
touch() {
|
|
142
142
|
if (this.canTrack()) touchActivity(this.options.siteKey, this.cookieless());
|
|
@@ -153,9 +153,9 @@ var WebAnalytics = class {
|
|
|
153
153
|
loadChild(name, create) {
|
|
154
154
|
const epoch = this.epoch;
|
|
155
155
|
create().then(async (tracker) => {
|
|
156
|
-
if (this.epoch !== epoch || !this.started) return tracker.stop?.();
|
|
156
|
+
if (this.epoch !== epoch || !this.started) return tracker.stop?.(true);
|
|
157
157
|
await tracker.start();
|
|
158
|
-
if (this.epoch !== epoch || !this.started) return tracker.stop?.();
|
|
158
|
+
if (this.epoch !== epoch || !this.started) return tracker.stop?.(true);
|
|
159
159
|
this.childTrackers.push(tracker);
|
|
160
160
|
this.log(`${name} loaded`);
|
|
161
161
|
}).catch((error) => this.log(`failed to initialize ${name} tracker: ${String(error)}`));
|
|
@@ -165,7 +165,8 @@ var WebAnalytics = class {
|
|
|
165
165
|
const base = {
|
|
166
166
|
siteKey: o.siteKey,
|
|
167
167
|
baseUrl: this.baseUrl,
|
|
168
|
-
debug: this.debug
|
|
168
|
+
debug: this.debug,
|
|
169
|
+
trackHash: o.trackHash ?? false
|
|
169
170
|
};
|
|
170
171
|
if (o.sessionReplays?.enabled) this.loadChild("replay", async () => {
|
|
171
172
|
const { default: ReplayTracker } = await import("./replay.js");
|
|
@@ -191,10 +192,12 @@ var WebAnalytics = class {
|
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
194
|
enterPage() {
|
|
195
|
+
const includeHash = this.options.trackHash ?? false;
|
|
196
|
+
const hash = includeHash ? location.hash : "";
|
|
194
197
|
this.entry = {
|
|
195
|
-
path: location.pathname
|
|
196
|
-
url: sanitizeUrl(location.href),
|
|
197
|
-
|
|
198
|
+
path: `${location.pathname}${hash}`,
|
|
199
|
+
url: sanitizeUrl(location.href, includeHash),
|
|
200
|
+
search: location.search
|
|
198
201
|
};
|
|
199
202
|
this.left = false;
|
|
200
203
|
this.startVisit();
|
|
@@ -212,13 +215,13 @@ var WebAnalytics = class {
|
|
|
212
215
|
}
|
|
213
216
|
onLinkClick = (event) => {
|
|
214
217
|
const href = this.outboundLinks.getHref(event);
|
|
215
|
-
if (href) this.
|
|
218
|
+
if (href) this.send("outbound_link", { outbound_link: sanitizeUrl(href) });
|
|
216
219
|
};
|
|
217
220
|
onVisibilityChange = () => {
|
|
218
221
|
this.touch();
|
|
219
222
|
if (document.visibilityState === "hidden") {
|
|
220
223
|
this.pauseVisit();
|
|
221
|
-
for (const tracker of this.childTrackers) tracker.onPageHidden?.();
|
|
224
|
+
for (const tracker of this.childTrackers) tracker.onPageHidden?.(false, false);
|
|
222
225
|
this.stopHeartbeat();
|
|
223
226
|
return;
|
|
224
227
|
}
|
|
@@ -231,7 +234,7 @@ var WebAnalytics = class {
|
|
|
231
234
|
onPageHide = (event) => {
|
|
232
235
|
const persisted = event.persisted;
|
|
233
236
|
this.pauseVisit();
|
|
234
|
-
for (const tracker of this.childTrackers) tracker.onPageHidden?.(persisted);
|
|
237
|
+
for (const tracker of this.childTrackers) tracker.onPageHidden?.(persisted, !persisted);
|
|
235
238
|
if (!persisted) this.leavePage();
|
|
236
239
|
};
|
|
237
240
|
onPageShow = (event) => {
|
|
@@ -291,7 +294,7 @@ var WebAnalytics = class {
|
|
|
291
294
|
pageview(extra = {}) {
|
|
292
295
|
if (!this.ensureActive()) return;
|
|
293
296
|
const trackHash = this.options.trackHash ?? false;
|
|
294
|
-
const key = `${location.pathname}|${trackHash ? location.hash : ""}`;
|
|
297
|
+
const key = `${location.pathname}|${location.search}|${trackHash ? location.hash : ""}`;
|
|
295
298
|
if (key === this.pageKey) return;
|
|
296
299
|
this.pageKey = key;
|
|
297
300
|
this.send("pageview", extra);
|
|
@@ -299,7 +302,7 @@ var WebAnalytics = class {
|
|
|
299
302
|
track(name, extra = {}) {
|
|
300
303
|
if (!this.ensureActive()) return;
|
|
301
304
|
const eventName = name.trim();
|
|
302
|
-
if (!eventName) return;
|
|
305
|
+
if (!eventName || eventName === "pageview" || eventName === "page_leave" || eventName === "error" || eventName === "outbound_link") return;
|
|
303
306
|
this.send(eventName, extra);
|
|
304
307
|
}
|
|
305
308
|
async identify(externalId, email, options = {}) {
|
|
@@ -327,28 +330,43 @@ var WebAnalytics = class {
|
|
|
327
330
|
}
|
|
328
331
|
logout(resetAnonymousIdentity = true) {
|
|
329
332
|
if (!this.ensureActive()) return;
|
|
330
|
-
if (resetAnonymousIdentity) resetAnonymousId(this.cookieless());
|
|
333
|
+
if (resetAnonymousIdentity) resetAnonymousId(this.options.siteKey, this.cookieless());
|
|
331
334
|
resetSession(this.options.siteKey);
|
|
332
335
|
}
|
|
333
336
|
setConsentMode(mode) {
|
|
334
|
-
|
|
337
|
+
if (mode === this.consentMode) return;
|
|
338
|
+
const wasTracking = this.canTrack();
|
|
335
339
|
this.consentMode = mode;
|
|
340
|
+
const tracking = this.canTrack();
|
|
336
341
|
const cookieless = this.cookieless();
|
|
337
342
|
setCookielessMode(cookieless);
|
|
343
|
+
if (!tracking) {
|
|
344
|
+
this.epoch++;
|
|
345
|
+
this.pauseVisit();
|
|
346
|
+
this.stopHeartbeat();
|
|
347
|
+
this.pendingPageviewTrigger = null;
|
|
348
|
+
for (const tracker of this.childTrackers.splice(0)) tracker.stop?.(true);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
338
351
|
for (const tracker of this.childTrackers) tracker.setCookielessMode?.(cookieless);
|
|
339
|
-
if (
|
|
352
|
+
if (!wasTracking && this.started) {
|
|
353
|
+
this.startHeartbeat();
|
|
354
|
+
this.beginTracking("consent");
|
|
355
|
+
}
|
|
340
356
|
}
|
|
341
357
|
getConsentMode() {
|
|
342
358
|
return this.consentMode;
|
|
343
359
|
}
|
|
344
360
|
getAnonymousId() {
|
|
345
361
|
if (!this.canTrack()) return "";
|
|
346
|
-
return getAnonymousId(this.cookieless());
|
|
362
|
+
return getAnonymousId(this.options.siteKey, this.cookieless());
|
|
347
363
|
}
|
|
348
364
|
getSessionId() {
|
|
365
|
+
if (!this.canTrack()) return "";
|
|
349
366
|
return getSessionContext(this.options.siteKey, this.cookieless()).sessionId;
|
|
350
367
|
}
|
|
351
368
|
getWindowId() {
|
|
369
|
+
if (!this.canTrack()) return "";
|
|
352
370
|
return getSessionContext(this.options.siteKey, this.cookieless()).windowId;
|
|
353
371
|
}
|
|
354
372
|
async checkFeatureFlag(key, attributes, opts) {
|
|
@@ -374,7 +392,7 @@ var WebAnalytics = class {
|
|
|
374
392
|
send(event, properties = {}, dims = {}) {
|
|
375
393
|
if (!this.canSendEvents()) return;
|
|
376
394
|
const cookieless = this.cookieless();
|
|
377
|
-
const id = getAnonymousId(cookieless);
|
|
395
|
+
const id = getAnonymousId(this.options.siteKey, cookieless);
|
|
378
396
|
const ctx = getSessionContext(this.options.siteKey, cookieless);
|
|
379
397
|
this.log(event);
|
|
380
398
|
sendData({
|
|
@@ -385,7 +403,7 @@ var WebAnalytics = class {
|
|
|
385
403
|
sessionId: ctx.sessionId,
|
|
386
404
|
windowId: ctx.windowId,
|
|
387
405
|
event,
|
|
388
|
-
...getPageContext(),
|
|
406
|
+
...getPageContext(this.options.trackHash ?? false),
|
|
389
407
|
...dims,
|
|
390
408
|
properties
|
|
391
409
|
}),
|
|
@@ -433,8 +451,8 @@ var WebAnalytics = class {
|
|
|
433
451
|
navigate() {
|
|
434
452
|
if (!this.started || this.destroyed) return;
|
|
435
453
|
const trackHash = this.options.trackHash ?? false;
|
|
436
|
-
if (location.pathname === this.entry.path &&
|
|
437
|
-
for (const tracker of this.childTrackers) tracker.trackPageChange?.(sanitizeUrl(location.href));
|
|
454
|
+
if (`${location.pathname}${trackHash ? location.hash : ""}` === this.entry.path && location.search === this.entry.search) return;
|
|
455
|
+
for (const tracker of this.childTrackers) tracker.trackPageChange?.(sanitizeUrl(location.href, trackHash));
|
|
438
456
|
this.leavePage();
|
|
439
457
|
this.enterPage();
|
|
440
458
|
this.pageview({ trigger: "navigation" });
|
package/dist/replay.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as ReplayTrackerOptions, t as ReplayTracker } from "./chunks/replay-
|
|
1
|
+
import { n as ReplayTrackerOptions, t as ReplayTracker } from "./chunks/replay-BeC2XA4N.js";
|
|
2
2
|
export { ReplayTrackerOptions, ReplayTracker as default };
|
package/dist/replay.js
CHANGED
|
@@ -1,11 +1,59 @@
|
|
|
1
|
-
import { a as resolveBaseUrl, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-
|
|
2
|
-
import { a as onSessionRotated, c as touchActivity, d as setStorageItem, i as isCookielessMode, l as getStorageItem, n as createId, r as getSessionContext, t as sendData } from "./chunks/send-data-
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-
|
|
1
|
+
import { a as resolveBaseUrl, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-Dp2XNjRF.js";
|
|
2
|
+
import { a as onSessionRotated, c as touchActivity, d as setStorageItem, i as isCookielessMode, l as getStorageItem, n as createId, r as getSessionContext, t as sendData } from "./chunks/send-data-DR1JUXka.js";
|
|
3
|
+
import { t as getAnonymousId } from "./chunks/identifiers-BvRNbiwA.js";
|
|
4
4
|
import { record } from "@rrweb/record";
|
|
5
|
+
//#region src/replay-mutation-throttler.ts
|
|
6
|
+
const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
|
|
7
|
+
const MUTATION_SOURCE_TYPE = 0;
|
|
8
|
+
const MAX_TRACKED_NODES = 1e4;
|
|
9
|
+
/** Bounds pathological per-node attribute churn without breaking the mutation chain. */
|
|
10
|
+
var ReplayMutationThrottler = class {
|
|
11
|
+
buckets = /* @__PURE__ */ new Map();
|
|
12
|
+
bucketSize;
|
|
13
|
+
refillRate;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.bucketSize = Math.max(1, options.bucketSize ?? 100);
|
|
16
|
+
this.refillRate = Math.max(1, options.refillRate ?? 10);
|
|
17
|
+
}
|
|
18
|
+
reset() {
|
|
19
|
+
this.buckets.clear();
|
|
20
|
+
}
|
|
21
|
+
throttle(event) {
|
|
22
|
+
if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE) return event;
|
|
23
|
+
const data = event.data;
|
|
24
|
+
if (data.source !== MUTATION_SOURCE_TYPE || !data.attributes?.length) return event;
|
|
25
|
+
const attributes = data.attributes.filter(({ id }) => this.consume(id));
|
|
26
|
+
if (attributes.length === data.attributes.length) return event;
|
|
27
|
+
if (attributes.length === 0 && (data.adds?.length ?? 0) === 0 && (data.removes?.length ?? 0) === 0 && (data.texts?.length ?? 0) === 0) return;
|
|
28
|
+
return {
|
|
29
|
+
...event,
|
|
30
|
+
data: {
|
|
31
|
+
...data,
|
|
32
|
+
attributes
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
consume(nodeId) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const bucket = this.buckets.get(nodeId) ?? {
|
|
39
|
+
tokens: this.bucketSize,
|
|
40
|
+
updatedAt: now
|
|
41
|
+
};
|
|
42
|
+
const elapsedSeconds = Math.max(0, now - bucket.updatedAt) / 1e3;
|
|
43
|
+
bucket.tokens = Math.min(this.bucketSize, bucket.tokens + elapsedSeconds * this.refillRate);
|
|
44
|
+
bucket.updatedAt = now;
|
|
45
|
+
if (!this.buckets.has(nodeId) && this.buckets.size >= MAX_TRACKED_NODES) this.buckets.delete(this.buckets.keys().next().value);
|
|
46
|
+
this.buckets.set(nodeId, bucket);
|
|
47
|
+
if (bucket.tokens < 1) return false;
|
|
48
|
+
bucket.tokens -= 1;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
5
53
|
//#region src/replay.ts
|
|
6
54
|
const MAX_TIMEOUT_MS = 2147483647;
|
|
7
55
|
const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2 * 1024 * 1024;
|
|
8
|
-
const DEFAULT_MAX_BATCH_SIZE_BYTES =
|
|
56
|
+
const DEFAULT_MAX_BATCH_SIZE_BYTES = 900 * 1024;
|
|
9
57
|
const DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES = 60 * 1024;
|
|
10
58
|
const RETRY_BASE_DELAY_MS = 2e3;
|
|
11
59
|
const RETRY_MAX_ATTEMPTS = 5;
|
|
@@ -33,6 +81,7 @@ var ReplayTracker = class {
|
|
|
33
81
|
queuedEventsSizeBytes = 0;
|
|
34
82
|
pendingSizeBytes = 0;
|
|
35
83
|
viewId = createId();
|
|
84
|
+
viewUrl = "";
|
|
36
85
|
chunkStartedAt = 0;
|
|
37
86
|
started = false;
|
|
38
87
|
disposed = false;
|
|
@@ -50,16 +99,20 @@ var ReplayTracker = class {
|
|
|
50
99
|
queuedFlush = null;
|
|
51
100
|
unsubscribeRotation;
|
|
52
101
|
lastCookielessMode = isCookielessMode();
|
|
102
|
+
terminalFlushRequested = false;
|
|
103
|
+
overflowed = false;
|
|
104
|
+
mutationThrottler;
|
|
53
105
|
constructor(options) {
|
|
54
106
|
this.options = options;
|
|
55
107
|
this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.replay}`;
|
|
56
|
-
this.flushInterval = options.flushInterval ??
|
|
108
|
+
this.flushInterval = options.flushInterval ?? 15e3;
|
|
57
109
|
this.maxEvents = options.maxEvents ?? 1e3;
|
|
58
110
|
this.maxBatchSizeBytes = options.maxBatchSizeBytes ?? DEFAULT_MAX_BATCH_SIZE_BYTES;
|
|
59
111
|
this.maxLowLatencyBatchSizeBytes = options.maxLowLatencyBatchSizeBytes ?? DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES;
|
|
60
112
|
this.maxPendingBatches = options.maxPendingBatches ?? 30;
|
|
61
113
|
this.maxQueueSizeBytes = options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
|
|
62
114
|
this.minReplayLengthMs = options.minReplayLengthMs ?? 3e3;
|
|
115
|
+
this.mutationThrottler = options.mutationThrottle?.enabled === false ? null : new ReplayMutationThrottler(options.mutationThrottle);
|
|
63
116
|
}
|
|
64
117
|
log(...args) {
|
|
65
118
|
if (this.options.debug) console.log("[Replay]", ...args);
|
|
@@ -113,10 +166,8 @@ var ReplayTracker = class {
|
|
|
113
166
|
setCookielessMode(cookieless) {
|
|
114
167
|
if (this.lastCookielessMode === cookieless) return;
|
|
115
168
|
this.lastCookielessMode = cookieless;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.clearFlushTimers();
|
|
119
|
-
}
|
|
169
|
+
this.clearQueues();
|
|
170
|
+
this.clearFlushTimers();
|
|
120
171
|
this.startTime = getSessionContext(this.options.siteKey).sessionStart;
|
|
121
172
|
this.chunkStartedAt = Date.now();
|
|
122
173
|
}
|
|
@@ -124,7 +175,10 @@ var ReplayTracker = class {
|
|
|
124
175
|
if (this.started || typeof window === "undefined") return;
|
|
125
176
|
this.started = true;
|
|
126
177
|
this.disposed = false;
|
|
178
|
+
this.terminalFlushRequested = false;
|
|
179
|
+
this.overflowed = false;
|
|
127
180
|
this.viewId = createId();
|
|
181
|
+
this.viewUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
|
|
128
182
|
this.startTime = getSessionContext(this.options.siteKey).sessionStart;
|
|
129
183
|
this.chunkStartedAt = Date.now();
|
|
130
184
|
this.unsubscribeRotation = onSessionRotated((prev, next) => {
|
|
@@ -138,13 +192,9 @@ var ReplayTracker = class {
|
|
|
138
192
|
}
|
|
139
193
|
trackPageChange(url) {
|
|
140
194
|
if (!this.started) return;
|
|
141
|
-
this.enqueueEventsIfReady(getSessionContext(this.options.siteKey), {
|
|
142
|
-
flushReason: "navigation",
|
|
143
|
-
sealBeforeMinLength: true
|
|
144
|
-
});
|
|
145
195
|
this.viewId = createId();
|
|
146
|
-
this.
|
|
147
|
-
|
|
196
|
+
const href = sanitizeUrl(url ?? window.location.href, this.options.trackHash ?? false);
|
|
197
|
+
this.viewUrl = href;
|
|
148
198
|
this.onEvent({
|
|
149
199
|
type: VIEW_META_EVENT_TYPE,
|
|
150
200
|
timestamp: Date.now(),
|
|
@@ -156,11 +206,14 @@ var ReplayTracker = class {
|
|
|
156
206
|
}
|
|
157
207
|
}
|
|
158
208
|
}, false);
|
|
159
|
-
if (this.pending.length > 0) queueMicrotask(() => void this.flush(false));
|
|
160
209
|
}
|
|
161
|
-
onPageHidden(persisted = false) {
|
|
210
|
+
onPageHidden(persisted = false, terminal = false) {
|
|
162
211
|
if (persisted) return;
|
|
163
|
-
|
|
212
|
+
if (terminal) {
|
|
213
|
+
this.requestTerminalFlush("pageHidden");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.flush(false, false, "pageHidden");
|
|
164
217
|
}
|
|
165
218
|
onPageShow(persisted = false) {
|
|
166
219
|
if (!persisted || !this.started) return;
|
|
@@ -214,7 +267,7 @@ var ReplayTracker = class {
|
|
|
214
267
|
blockSelector: this.options.blockSelector,
|
|
215
268
|
maskTextClass: this.options.maskTextClass,
|
|
216
269
|
maskTextSelector: this.options.maskTextSelector,
|
|
217
|
-
checkoutEveryNms: this.options.checkoutEveryNms ?? 6e4,
|
|
270
|
+
checkoutEveryNms: this.options.checkoutEveryNms ?? 5 * 6e4,
|
|
218
271
|
checkoutEveryNth: this.options.checkoutEveryNth,
|
|
219
272
|
plugins
|
|
220
273
|
});
|
|
@@ -224,7 +277,7 @@ var ReplayTracker = class {
|
|
|
224
277
|
}
|
|
225
278
|
this.stopRecording = stop;
|
|
226
279
|
}
|
|
227
|
-
stop() {
|
|
280
|
+
stop(discard = false) {
|
|
228
281
|
if (!this.started) return;
|
|
229
282
|
this.started = false;
|
|
230
283
|
this.disposed = true;
|
|
@@ -237,6 +290,11 @@ var ReplayTracker = class {
|
|
|
237
290
|
this.clearFlushTimers();
|
|
238
291
|
this.intervalId = null;
|
|
239
292
|
window.removeEventListener("beforeunload", this.onUnload);
|
|
293
|
+
if (discard) {
|
|
294
|
+
this.clearQueues();
|
|
295
|
+
this.log("Recording discarded");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
240
298
|
if (!this.hasReachedMinLength()) {
|
|
241
299
|
this.events.length = 0;
|
|
242
300
|
this.queuedEventsSizeBytes = 0;
|
|
@@ -266,7 +324,8 @@ var ReplayTracker = class {
|
|
|
266
324
|
this.startTime = next.sessionStart;
|
|
267
325
|
this.chunkStartedAt = Date.now();
|
|
268
326
|
}
|
|
269
|
-
onEvent = (event,
|
|
327
|
+
onEvent = (event, _isCheckout) => {
|
|
328
|
+
if (this.overflowed) return;
|
|
270
329
|
let capturedEvent = event;
|
|
271
330
|
if (event.type === RRWEB_EVENT_META) {
|
|
272
331
|
const data = event.data;
|
|
@@ -274,23 +333,38 @@ var ReplayTracker = class {
|
|
|
274
333
|
...event,
|
|
275
334
|
data: {
|
|
276
335
|
...data,
|
|
277
|
-
href: sanitizeUrl(data.href)
|
|
336
|
+
href: sanitizeUrl(data.href, this.options.trackHash ?? false)
|
|
278
337
|
}
|
|
279
338
|
};
|
|
280
339
|
}
|
|
340
|
+
if (capturedEvent.type === RRWEB_EVENT_FULL_SNAPSHOT) this.mutationThrottler?.reset();
|
|
341
|
+
else {
|
|
342
|
+
const throttled = this.mutationThrottler?.throttle(capturedEvent);
|
|
343
|
+
if (this.mutationThrottler && !throttled) return;
|
|
344
|
+
if (throttled) capturedEvent = throttled;
|
|
345
|
+
}
|
|
281
346
|
this.events.push(capturedEvent);
|
|
282
347
|
this.queuedEventsSizeBytes += this.eventSizeBytes(capturedEvent);
|
|
283
348
|
let reason;
|
|
284
|
-
if (
|
|
285
|
-
else if (this.events.length >= this.maxEvents) reason = "maxEvents";
|
|
349
|
+
if (this.events.length >= this.maxEvents) reason = "maxEvents";
|
|
286
350
|
else if (this.queuedEventsSizeBytes >= this.maxBatchSizeBytes) reason = "maxBytes";
|
|
287
|
-
else if (capturedEvent.type === RRWEB_EVENT_FULL_SNAPSHOT && this.hasReachedMinLength()) reason = "fullSnapshot";
|
|
288
351
|
if (reason) this.requestFlush(reason);
|
|
289
352
|
else this.scheduleMinLengthFlush();
|
|
290
353
|
};
|
|
291
354
|
onUnload = () => {
|
|
292
|
-
this.
|
|
355
|
+
this.requestTerminalFlush("unload");
|
|
293
356
|
};
|
|
357
|
+
requestTerminalFlush(reason) {
|
|
358
|
+
if (this.terminalFlushRequested) return;
|
|
359
|
+
this.terminalFlushRequested = true;
|
|
360
|
+
if (!this.hasReachedMinLength()) {
|
|
361
|
+
this.events.length = 0;
|
|
362
|
+
this.queuedEventsSizeBytes = 0;
|
|
363
|
+
this.dropMinLengthBlockedBatches();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.flush(true, true, reason);
|
|
367
|
+
}
|
|
294
368
|
hasReachedMinLength() {
|
|
295
369
|
return this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
|
|
296
370
|
}
|
|
@@ -347,7 +421,7 @@ var ReplayTracker = class {
|
|
|
347
421
|
return batches;
|
|
348
422
|
}
|
|
349
423
|
createBatch(events, context, isFinal, flushReason = "manual") {
|
|
350
|
-
const identifier = getAnonymousId();
|
|
424
|
+
const identifier = getAnonymousId(this.options.siteKey);
|
|
351
425
|
const sequence = this.nextBatchSequence(context);
|
|
352
426
|
const chunkStartedAt = this.chunkStartedAt;
|
|
353
427
|
const now = Date.now();
|
|
@@ -364,7 +438,7 @@ var ReplayTracker = class {
|
|
|
364
438
|
batchId: `${context.sessionId}-${sequence}-${chunkStartedAt}`,
|
|
365
439
|
sequence,
|
|
366
440
|
timestamp: now,
|
|
367
|
-
url: sanitizeUrl(window.location.href),
|
|
441
|
+
url: this.viewUrl || sanitizeUrl(window.location.href, this.options.trackHash ?? false),
|
|
368
442
|
...isFinal ? { isFinal: true } : {},
|
|
369
443
|
flushReason,
|
|
370
444
|
events
|
|
@@ -380,10 +454,6 @@ var ReplayTracker = class {
|
|
|
380
454
|
batchSizeCache.set(batch, size);
|
|
381
455
|
return size;
|
|
382
456
|
}
|
|
383
|
-
dropOldestPending(reason) {
|
|
384
|
-
const batch = this.pending[0];
|
|
385
|
-
if (batch) this.removePending(batch, reason);
|
|
386
|
-
}
|
|
387
457
|
removePending(batch, reason) {
|
|
388
458
|
const index = this.pending.indexOf(batch);
|
|
389
459
|
if (index < 0) return false;
|
|
@@ -394,11 +464,13 @@ var ReplayTracker = class {
|
|
|
394
464
|
}
|
|
395
465
|
enqueueBatch(batch) {
|
|
396
466
|
const size = this.batchSizeBytes(batch);
|
|
397
|
-
if (size > this.maxQueueSizeBytes) {
|
|
398
|
-
this.log(`Replay batch ${batch.sequence}
|
|
467
|
+
if (size > this.maxQueueSizeBytes || this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) {
|
|
468
|
+
this.log(`Replay queue limit reached at batch ${batch.sequence}; stopping recording at the last complete chain`);
|
|
469
|
+
this.overflowed = true;
|
|
470
|
+
this.stopRecording?.();
|
|
471
|
+
this.stopRecording = void 0;
|
|
399
472
|
return;
|
|
400
473
|
}
|
|
401
|
-
while ((this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) && this.pending.length > 0) this.dropOldestPending("Pending queue limit reached");
|
|
402
474
|
this.pending.push(batch);
|
|
403
475
|
this.pendingSizeBytes += size;
|
|
404
476
|
}
|
|
@@ -477,9 +549,10 @@ var ReplayTracker = class {
|
|
|
477
549
|
}
|
|
478
550
|
async sendBatch(batch, lowLatency) {
|
|
479
551
|
const json = JSON.stringify(batch);
|
|
552
|
+
const lowLatencySafe = lowLatency && textEncoder.encode(json).byteLength <= 60 * 1024;
|
|
480
553
|
const sendOptions = {
|
|
481
|
-
useBeacon:
|
|
482
|
-
keepalive:
|
|
554
|
+
useBeacon: lowLatencySafe,
|
|
555
|
+
keepalive: lowLatencySafe
|
|
483
556
|
};
|
|
484
557
|
if (!lowLatency && (this.options.compress ?? true) && this.compressionSupported) try {
|
|
485
558
|
const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
|
package/dist/web-vitals.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ interface WebVitalsOptions {
|
|
|
4
4
|
baseUrl?: string;
|
|
5
5
|
debug?: boolean;
|
|
6
6
|
attribution?: boolean;
|
|
7
|
+
trackHash?: boolean;
|
|
7
8
|
}
|
|
8
9
|
declare class WebVitalsTracker {
|
|
9
10
|
private readonly options;
|
|
@@ -12,14 +13,12 @@ declare class WebVitalsTracker {
|
|
|
12
13
|
private started;
|
|
13
14
|
private flushing;
|
|
14
15
|
private initialUrl;
|
|
15
|
-
private
|
|
16
|
+
private session;
|
|
16
17
|
constructor(options: WebVitalsOptions);
|
|
17
18
|
start(): Promise<void>;
|
|
18
|
-
stop(): void;
|
|
19
|
+
stop(discard?: boolean): void;
|
|
19
20
|
onPageHidden(persisted?: boolean): void;
|
|
20
|
-
trackPageChange(url?: string): void;
|
|
21
21
|
private onMetric;
|
|
22
|
-
private getMetricUrl;
|
|
23
22
|
private flush;
|
|
24
23
|
}
|
|
25
24
|
//#endregion
|
package/dist/web-vitals.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as resolveBaseUrl, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-
|
|
2
|
-
import { r as getSessionContext, t as sendData } from "./chunks/send-data-
|
|
1
|
+
import { a as resolveBaseUrl, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-Dp2XNjRF.js";
|
|
2
|
+
import { r as getSessionContext, t as sendData } from "./chunks/send-data-DR1JUXka.js";
|
|
3
3
|
//#region src/web-vitals.ts
|
|
4
4
|
const METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
5
5
|
"CLS",
|
|
@@ -15,7 +15,7 @@ var WebVitalsTracker = class {
|
|
|
15
15
|
started = false;
|
|
16
16
|
flushing = false;
|
|
17
17
|
initialUrl = "";
|
|
18
|
-
|
|
18
|
+
session = null;
|
|
19
19
|
constructor(options) {
|
|
20
20
|
this.options = options;
|
|
21
21
|
this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.vitals}`;
|
|
@@ -23,8 +23,8 @@ var WebVitalsTracker = class {
|
|
|
23
23
|
async start() {
|
|
24
24
|
if (this.started || typeof window === "undefined") return;
|
|
25
25
|
this.started = true;
|
|
26
|
-
this.initialUrl = sanitizeUrl(window.location.href);
|
|
27
|
-
this.
|
|
26
|
+
this.initialUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
|
|
27
|
+
this.session = getSessionContext(this.options.siteKey);
|
|
28
28
|
const mod = await (this.options.attribution ? import("web-vitals/attribution") : import("web-vitals"));
|
|
29
29
|
if (!this.started) return;
|
|
30
30
|
const changes = { reportAllChanges: true };
|
|
@@ -34,31 +34,24 @@ var WebVitalsTracker = class {
|
|
|
34
34
|
mod.onFCP(this.onMetric);
|
|
35
35
|
mod.onTTFB(this.onMetric);
|
|
36
36
|
}
|
|
37
|
-
stop() {
|
|
37
|
+
stop(discard = false) {
|
|
38
38
|
if (!this.started) return;
|
|
39
39
|
this.started = false;
|
|
40
|
-
this.
|
|
40
|
+
if (discard) this.metrics.clear();
|
|
41
|
+
else this.flush();
|
|
41
42
|
}
|
|
42
43
|
onPageHidden(persisted = false) {
|
|
43
44
|
if (persisted) return;
|
|
44
|
-
this.flush();
|
|
45
|
-
}
|
|
46
|
-
trackPageChange(url) {
|
|
47
|
-
if (!this.started) return;
|
|
48
|
-
const next = sanitizeUrl(url ?? window.location.href);
|
|
49
|
-
if (next === this.currentUrl) return;
|
|
50
|
-
this.currentUrl = next;
|
|
45
|
+
queueMicrotask(() => void this.flush());
|
|
51
46
|
}
|
|
52
47
|
onMetric = (metric) => {
|
|
53
48
|
if (!this.started) return;
|
|
54
49
|
const name = metric.name;
|
|
55
50
|
if (!METRIC_NAMES.has(name) || !Number.isFinite(metric.value) || metric.value < 0) return;
|
|
56
|
-
const url = this.getMetricUrl(name);
|
|
57
51
|
const { id, rating, delta, navigationType } = metric;
|
|
58
52
|
const attribution = "attribution" in metric && metric.attribution ? metric.attribution : void 0;
|
|
59
|
-
this.metrics.set(
|
|
53
|
+
this.metrics.set(name, {
|
|
60
54
|
metric: name,
|
|
61
|
-
url,
|
|
62
55
|
value: metric.value,
|
|
63
56
|
attributes: {
|
|
64
57
|
id,
|
|
@@ -69,46 +62,32 @@ var WebVitalsTracker = class {
|
|
|
69
62
|
}
|
|
70
63
|
});
|
|
71
64
|
};
|
|
72
|
-
getMetricUrl(metric) {
|
|
73
|
-
if (metric === "FCP" || metric === "LCP" || metric === "TTFB") return this.initialUrl;
|
|
74
|
-
return this.currentUrl || this.initialUrl;
|
|
75
|
-
}
|
|
76
65
|
async flush() {
|
|
77
66
|
if (!this.metrics.size || this.flushing) return;
|
|
78
67
|
this.flushing = true;
|
|
79
68
|
const batch = this.metrics;
|
|
80
69
|
this.metrics = /* @__PURE__ */ new Map();
|
|
81
|
-
const session = getSessionContext(this.options.siteKey);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
vitals.push(vital);
|
|
86
|
-
byUrl.set(vital.url, vitals);
|
|
87
|
-
}
|
|
88
|
-
const results = await Promise.all([...byUrl].map(async ([url, vitals]) => ({
|
|
89
|
-
ok: await sendData({
|
|
70
|
+
const session = this.session ?? getSessionContext(this.options.siteKey);
|
|
71
|
+
let sent = false;
|
|
72
|
+
try {
|
|
73
|
+
sent = await sendData({
|
|
90
74
|
url: this.endpoint,
|
|
91
75
|
data: JSON.stringify({
|
|
92
76
|
token: this.options.siteKey,
|
|
93
77
|
sessionId: session.sessionId,
|
|
94
78
|
windowId: session.windowId,
|
|
95
|
-
vitals:
|
|
96
|
-
metadata: { url }
|
|
79
|
+
vitals: [...batch.values()],
|
|
80
|
+
metadata: { url: this.initialUrl }
|
|
97
81
|
}),
|
|
98
82
|
debug: this.options.debug,
|
|
99
83
|
debugPrefix: "[WebVitals]"
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (const result of results) {
|
|
105
|
-
if (result.ok) continue;
|
|
106
|
-
for (const vital of result.vitals) {
|
|
107
|
-
const key = `${result.url}\n${vital.metric}`;
|
|
108
|
-
if (!this.metrics.has(key)) this.metrics.set(key, vital);
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
if (!sent) {
|
|
87
|
+
for (const [name, vital] of batch) if (!this.metrics.has(name)) this.metrics.set(name, vital);
|
|
109
88
|
}
|
|
89
|
+
this.flushing = false;
|
|
110
90
|
}
|
|
111
|
-
this.flushing = false;
|
|
112
91
|
}
|
|
113
92
|
};
|
|
114
93
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { d as setStorageItem, i as isCookielessMode, l as getStorageItem, n as createId, u as removeStorageItem } from "./send-data-DjdbGDDc.js";
|
|
2
|
-
//#region src/utils/identifiers.ts
|
|
3
|
-
const ANONYMOUS_ID_KEY = "faststats_anon_id";
|
|
4
|
-
function getOrCreateAnonymousId() {
|
|
5
|
-
const existingId = getStorageItem("localStorage", ANONYMOUS_ID_KEY);
|
|
6
|
-
if (existingId) return existingId;
|
|
7
|
-
const newId = createId();
|
|
8
|
-
return setStorageItem("localStorage", ANONYMOUS_ID_KEY, newId) ? newId : "";
|
|
9
|
-
}
|
|
10
|
-
function getAnonymousId(cookieless) {
|
|
11
|
-
if (cookieless ?? isCookielessMode()) return "";
|
|
12
|
-
return getOrCreateAnonymousId();
|
|
13
|
-
}
|
|
14
|
-
function resetAnonymousId(cookieless) {
|
|
15
|
-
if (cookieless) return "";
|
|
16
|
-
if (!removeStorageItem("localStorage", ANONYMOUS_ID_KEY)) return "";
|
|
17
|
-
return getOrCreateAnonymousId();
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
|
-
export { resetAnonymousId as n, getAnonymousId as t };
|