@faststats/web 0.10.2 → 0.11.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/README.md +3 -4
- package/dist/chunks/{extensions-DW7tJY0T.d.ts → extensions-DZDQn8O8.d.ts} +0 -11
- package/dist/chunks/{identifiers-I5UnTXGR.js → identifiers-BwGNL11R.js} +1 -1
- package/dist/chunks/{send-data-Dplz7m4c.js → send-data-D520oab1.js} +65 -21
- package/dist/error.d.ts +5 -2
- package/dist/error.js +46 -32
- package/dist/index.d.ts +4 -2
- package/dist/index.js +81 -50
- package/dist/replay.d.ts +3 -2
- package/dist/replay.js +80 -109
- package/dist/web-vitals.d.ts +1 -1
- package/dist/web-vitals.js +4 -7
- package/package.json +1 -6
- package/dist/chunks/api-urls-DWk43fu6.js +0 -53
- package/dist/outbound-links.d.ts +0 -9
- package/dist/outbound-links.js +0 -62
package/README.md
CHANGED
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
> [!IMPORTANT]
|
|
4
4
|
> This repository is in early development and not intended for production use.
|
|
5
5
|
|
|
6
|
-
The base SDK
|
|
6
|
+
The base SDK handles analytics, identity, sessions, navigation, outbound link
|
|
7
|
+
clicks, and consent.
|
|
7
8
|
Error tracking, Web Vitals, and session replay are independent extensions, so
|
|
8
9
|
applications only download the products they use.
|
|
9
10
|
|
|
10
11
|
```ts
|
|
11
12
|
import { init, track } from "@faststats/web";
|
|
12
13
|
import { errorTracking } from "@faststats/web/error";
|
|
13
|
-
import { outboundLinks } from "@faststats/web/outbound-links";
|
|
14
14
|
import { sessionReplay } from "@faststats/web/replay";
|
|
15
15
|
import { webVitals } from "@faststats/web/web-vitals";
|
|
16
16
|
|
|
17
17
|
init({
|
|
18
18
|
siteKey: "site_123",
|
|
19
19
|
extensions: [
|
|
20
|
-
outboundLinks(),
|
|
21
20
|
errorTracking(),
|
|
22
21
|
webVitals(),
|
|
23
22
|
sessionReplay({ recordConsole: true }),
|
|
@@ -123,7 +122,7 @@ export function diagnostics(): AnalyticsExtension {
|
|
|
123
122
|
name: "diagnostics",
|
|
124
123
|
setup(context) {
|
|
125
124
|
const off = context.on("pageChange", ({ url }) => {
|
|
126
|
-
console.debug("page changed", url
|
|
125
|
+
console.debug("page changed", url);
|
|
127
126
|
});
|
|
128
127
|
|
|
129
128
|
return () => off();
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
//#region src/utils/session-manager.d.ts
|
|
2
|
-
type SessionContext = {
|
|
3
|
-
sessionId: string;
|
|
4
|
-
windowId: string;
|
|
5
|
-
sessionStart: number;
|
|
6
|
-
};
|
|
7
|
-
//#endregion
|
|
8
1
|
//#region src/extensions.d.ts
|
|
9
2
|
type ExtensionHooks = {
|
|
10
3
|
pageChange: {
|
|
@@ -34,11 +27,7 @@ interface AnalyticsExtensionContext {
|
|
|
34
27
|
readonly sdkName?: string;
|
|
35
28
|
readonly sdkVersion?: string;
|
|
36
29
|
isCookieless(): boolean;
|
|
37
|
-
getAnonymousId(): string;
|
|
38
|
-
getSession(): SessionContext;
|
|
39
|
-
capture(event: string, properties?: Record<string, unknown>): void;
|
|
40
30
|
on<K extends ExtensionHook>(hook: K, handler: ExtensionHandler<K>): () => void;
|
|
41
|
-
log(message: string, ...details: unknown[]): void;
|
|
42
31
|
}
|
|
43
32
|
interface AnalyticsExtension {
|
|
44
33
|
readonly name: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as removeStorageItem, l as setStorageItem, n as createId, s as getStorageItem } from "./send-data-
|
|
1
|
+
import { c as removeStorageItem, l as setStorageItem, n as createId, s as getStorageItem } from "./send-data-D520oab1.js";
|
|
2
2
|
//#region src/utils/identifiers.ts
|
|
3
3
|
const ANONYMOUS_ID_KEY = "faststats_anon_id_";
|
|
4
4
|
function getAnonymousId(siteKey, cookieless = false) {
|
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
//#region src/utils/api-urls.ts
|
|
2
|
+
function resolveBaseUrl(baseUrl) {
|
|
3
|
+
return baseUrl?.replace(/\/+$/, "") || "https://metrics.faststats.dev";
|
|
4
|
+
}
|
|
5
|
+
const URLS = {
|
|
6
|
+
events: "/v1/web",
|
|
7
|
+
identify: "/v1/identify",
|
|
8
|
+
replay: "/v1/replay",
|
|
9
|
+
vitals: "/v1/vitals"
|
|
10
|
+
};
|
|
11
|
+
function sanitizeUrl(url, includeHash = false) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(url, location.href);
|
|
14
|
+
return `${parsed.origin}${parsed.pathname}${includeHash ? parsed.hash : ""}`;
|
|
15
|
+
} catch {
|
|
16
|
+
return url.split(includeHash ? "?" : /[?#]/, 1)[0] ?? "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const UTM_KEYS = [
|
|
20
|
+
"utm_source",
|
|
21
|
+
"utm_medium",
|
|
22
|
+
"utm_campaign",
|
|
23
|
+
"utm_term",
|
|
24
|
+
"utm_content"
|
|
25
|
+
];
|
|
26
|
+
function getReferrer() {
|
|
27
|
+
const raw = document.referrer;
|
|
28
|
+
if (!raw) return null;
|
|
29
|
+
try {
|
|
30
|
+
const pageOrigin = new URL(location.href).origin;
|
|
31
|
+
if (new URL(raw, location.href).origin === pageOrigin) return null;
|
|
32
|
+
} catch {}
|
|
33
|
+
return sanitizeUrl(raw);
|
|
34
|
+
}
|
|
35
|
+
function getPageContext(includeHash = false) {
|
|
36
|
+
const hash = includeHash ? location.hash : "";
|
|
37
|
+
const ctx = {
|
|
38
|
+
page: `${location.pathname}${hash}`,
|
|
39
|
+
url: sanitizeUrl(location.href, includeHash),
|
|
40
|
+
referrer: getReferrer(),
|
|
41
|
+
title: document.title || ""
|
|
42
|
+
};
|
|
43
|
+
if (!location.search) return ctx;
|
|
44
|
+
const sp = new URLSearchParams(location.search);
|
|
45
|
+
for (const key of UTM_KEYS) {
|
|
46
|
+
const value = sp.get(key);
|
|
47
|
+
if (value) ctx[key] = value;
|
|
48
|
+
}
|
|
49
|
+
return ctx;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
1
52
|
//#region src/utils/storage.ts
|
|
2
53
|
function getStorageItem(name, key) {
|
|
3
54
|
try {
|
|
@@ -31,6 +82,7 @@ function removeStorageItem(name, key) {
|
|
|
31
82
|
const IDLE_MS = 18e5;
|
|
32
83
|
const MAX_AGE_MS = 864e5;
|
|
33
84
|
const SESSION_KEY = "faststats_session_";
|
|
85
|
+
const WINDOW_KEY = "faststats_window_id_";
|
|
34
86
|
const NO_STORAGE = {};
|
|
35
87
|
const memorySessions = /* @__PURE__ */ new Map();
|
|
36
88
|
const rotationListeners = /* @__PURE__ */ new Map();
|
|
@@ -64,7 +116,7 @@ function readSession(siteKey) {
|
|
|
64
116
|
if (!raw) return memory;
|
|
65
117
|
try {
|
|
66
118
|
const session = JSON.parse(raw);
|
|
67
|
-
|
|
119
|
+
if (typeof session?.id === "string" && session.id && Number.isFinite(session.start) && Number.isFinite(session.activity)) return session;
|
|
68
120
|
} catch {}
|
|
69
121
|
return memory;
|
|
70
122
|
}
|
|
@@ -104,7 +156,7 @@ function resolveSession(siteKey, cookieless, touch) {
|
|
|
104
156
|
}
|
|
105
157
|
function windowId(siteKey, cookieless, sessionId) {
|
|
106
158
|
if (cookieless) return sessionId;
|
|
107
|
-
const key =
|
|
159
|
+
const key = `${WINDOW_KEY}${siteKey}`;
|
|
108
160
|
const existing = getStorageItem("sessionStorage", key);
|
|
109
161
|
if (existing) return existing;
|
|
110
162
|
const id = createId();
|
|
@@ -128,7 +180,7 @@ function resetSession(siteKey, cookieless = false) {
|
|
|
128
180
|
memorySessions.delete(memoryKey(siteKey, cookieless));
|
|
129
181
|
if (!cookieless) {
|
|
130
182
|
removeStorageItem("localStorage", `${SESSION_KEY}${siteKey}`);
|
|
131
|
-
removeStorageItem("sessionStorage",
|
|
183
|
+
removeStorageItem("sessionStorage", `${WINDOW_KEY}${siteKey}`);
|
|
132
184
|
}
|
|
133
185
|
const next = getSessionContext(siteKey, cookieless);
|
|
134
186
|
if (previous.sessionId !== next.sessionId) emitRotation(siteKey, cookieless, previous, next);
|
|
@@ -150,8 +202,7 @@ function onSessionRotated(siteKey, cookieless, listener) {
|
|
|
150
202
|
//#endregion
|
|
151
203
|
//#region src/utils/send-data.ts
|
|
152
204
|
async function sendData(options) {
|
|
153
|
-
const { url, data, contentType = "application/json",
|
|
154
|
-
if (signal?.aborted) return false;
|
|
205
|
+
const { url, data, contentType = "application/json", debug = false, debugPrefix = "[Analytics]", useBeacon = true, keepalive = true, settleRejected } = options;
|
|
155
206
|
if (useBeacon && typeof globalThis.navigator?.sendBeacon === "function") try {
|
|
156
207
|
const beaconBody = data instanceof Blob || typeof Blob === "undefined" ? data : new Blob([data], { type: contentType });
|
|
157
208
|
if (globalThis.navigator.sendBeacon(url, beaconBody)) {
|
|
@@ -159,28 +210,21 @@ async function sendData(options) {
|
|
|
159
210
|
return true;
|
|
160
211
|
}
|
|
161
212
|
} catch {}
|
|
213
|
+
const size = typeof data === "string" ? data.length : data instanceof Blob ? data.size : data.byteLength;
|
|
162
214
|
try {
|
|
163
|
-
const
|
|
215
|
+
const { ok, status } = await fetch(url, {
|
|
164
216
|
method: "POST",
|
|
165
217
|
body: data,
|
|
166
|
-
headers: {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
},
|
|
170
|
-
keepalive,
|
|
171
|
-
credentials: "omit",
|
|
172
|
-
signal
|
|
218
|
+
headers: { "Content-Type": contentType },
|
|
219
|
+
keepalive: keepalive && size <= 61440,
|
|
220
|
+
credentials: "omit"
|
|
173
221
|
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const message = success ? `${debugPrefix} Sent via fetch` : `${debugPrefix} Failed: ${response.status}`;
|
|
177
|
-
console[success ? "log" : "warn"](message);
|
|
178
|
-
}
|
|
179
|
-
return success;
|
|
222
|
+
if (debug) console.log(debugPrefix, ok ? "Sent via fetch" : status);
|
|
223
|
+
return ok || !!settleRejected && status < 500 && status !== 429;
|
|
180
224
|
} catch {
|
|
181
|
-
if (debug) console.
|
|
225
|
+
if (debug) console.log(debugPrefix, "Failed to send");
|
|
182
226
|
return false;
|
|
183
227
|
}
|
|
184
228
|
}
|
|
185
229
|
//#endregion
|
|
186
|
-
export { resetSession as a, removeStorageItem as c, onSessionRotated as i, setStorageItem as l, createId as n, touchActivity as o, getSessionContext as r, getStorageItem as s, sendData as t };
|
|
230
|
+
export { resetSession as a, removeStorageItem as c, getPageContext as d, resolveBaseUrl as f, onSessionRotated as i, setStorageItem as l, createId as n, touchActivity as o, sanitizeUrl as p, getSessionContext as r, getStorageItem as s, sendData as t, URLS as u };
|
package/dist/error.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as AnalyticsExtension } from "./chunks/extensions-
|
|
1
|
+
import { t as AnalyticsExtension } from "./chunks/extensions-DZDQn8O8.js";
|
|
2
2
|
//#region src/error.d.ts
|
|
3
3
|
export type ErrorEntry = {
|
|
4
4
|
error: string;
|
|
@@ -17,7 +17,7 @@ export interface ErrorTrackingOptions {
|
|
|
17
17
|
sdkName?: string;
|
|
18
18
|
sdkVersion?: string;
|
|
19
19
|
}
|
|
20
|
-
export type ErrorTrackingExtensionOptions =
|
|
20
|
+
export type ErrorTrackingExtensionOptions = Pick<ErrorTrackingOptions, "flushInterval" | "maxQueueSize">;
|
|
21
21
|
export declare function errorTracking(options?: ErrorTrackingExtensionOptions): AnalyticsExtension;
|
|
22
22
|
export default class ErrorTracker {
|
|
23
23
|
private readonly options;
|
|
@@ -26,11 +26,14 @@ export default class ErrorTracker {
|
|
|
26
26
|
private timer;
|
|
27
27
|
private started;
|
|
28
28
|
private flushing;
|
|
29
|
+
private generation;
|
|
29
30
|
private cookieless;
|
|
31
|
+
private readonly maxQueueSize;
|
|
30
32
|
constructor(options: ErrorTrackingOptions);
|
|
31
33
|
setCookielessMode(cookieless: boolean): void;
|
|
32
34
|
start(): void;
|
|
33
35
|
stop(discard?: boolean): void;
|
|
36
|
+
private discard;
|
|
34
37
|
onPageHidden(persisted?: boolean): void;
|
|
35
38
|
captureError(error: Error): void;
|
|
36
39
|
private onError;
|
package/dist/error.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-I5UnTXGR.js";
|
|
1
|
+
import { d as getPageContext, f as resolveBaseUrl, r as getSessionContext, t as sendData, u as URLS } from "./chunks/send-data-D520oab1.js";
|
|
2
|
+
import { t as getAnonymousId } from "./chunks/identifiers-BwGNL11R.js";
|
|
4
3
|
//#region src/error.ts
|
|
5
4
|
const SDK_NAME = "@faststats/web";
|
|
6
|
-
const SDK_VERSION = "0.
|
|
5
|
+
const SDK_VERSION = "0.11.0";
|
|
7
6
|
function errorTracking(options = {}) {
|
|
8
7
|
return {
|
|
9
8
|
name: "error-tracking",
|
|
@@ -51,14 +50,17 @@ var ErrorTracker = class {
|
|
|
51
50
|
timer = null;
|
|
52
51
|
started = false;
|
|
53
52
|
flushing = false;
|
|
53
|
+
generation = 0;
|
|
54
54
|
cookieless;
|
|
55
|
+
maxQueueSize;
|
|
55
56
|
constructor(options) {
|
|
56
57
|
this.options = options;
|
|
57
|
-
this.
|
|
58
|
+
this.maxQueueSize = options.maxQueueSize ?? 50;
|
|
59
|
+
this.endpoint = `${resolveBaseUrl(options.baseUrl)}${URLS.events}`;
|
|
58
60
|
this.cookieless = options.cookieless ?? false;
|
|
59
61
|
}
|
|
60
62
|
setCookielessMode(cookieless) {
|
|
61
|
-
if (this.cookieless !== cookieless) this.
|
|
63
|
+
if (this.cookieless !== cookieless) this.discard();
|
|
62
64
|
this.cookieless = cookieless;
|
|
63
65
|
}
|
|
64
66
|
start() {
|
|
@@ -69,15 +71,19 @@ var ErrorTracker = class {
|
|
|
69
71
|
this.timer = setInterval(() => void this.flush(), this.options.flushInterval ?? 5e3);
|
|
70
72
|
}
|
|
71
73
|
stop(discard = false) {
|
|
72
|
-
if (!this.started
|
|
74
|
+
if (!this.started) return;
|
|
73
75
|
this.started = false;
|
|
74
76
|
window.removeEventListener("error", this.onError);
|
|
75
77
|
window.removeEventListener("unhandledrejection", this.onRejection);
|
|
76
78
|
if (this.timer) clearInterval(this.timer);
|
|
77
79
|
this.timer = null;
|
|
78
|
-
if (discard) this.
|
|
80
|
+
if (discard) this.discard();
|
|
79
81
|
else this.flush();
|
|
80
82
|
}
|
|
83
|
+
discard() {
|
|
84
|
+
this.generation++;
|
|
85
|
+
this.queue = [];
|
|
86
|
+
}
|
|
81
87
|
onPageHidden(persisted = false) {
|
|
82
88
|
if (!persisted) this.flush();
|
|
83
89
|
}
|
|
@@ -111,11 +117,12 @@ var ErrorTracker = class {
|
|
|
111
117
|
properties: {}
|
|
112
118
|
}
|
|
113
119
|
});
|
|
114
|
-
if (this.queue.length >=
|
|
120
|
+
if (this.queue.length >= this.maxQueueSize) this.flush();
|
|
115
121
|
}
|
|
116
122
|
async flush() {
|
|
117
123
|
if (this.flushing || !this.queue.length) return;
|
|
118
124
|
this.flushing = true;
|
|
125
|
+
const generation = this.generation;
|
|
119
126
|
const batch = this.queue;
|
|
120
127
|
this.queue = [];
|
|
121
128
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -128,29 +135,36 @@ var ErrorTracker = class {
|
|
|
128
135
|
group.items.push(item);
|
|
129
136
|
groups.set(key, group);
|
|
130
137
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
let failed = [];
|
|
139
|
+
try {
|
|
140
|
+
failed = (await Promise.all([...groups.values()].map(async (group) => {
|
|
141
|
+
const errors = /* @__PURE__ */ new Map();
|
|
142
|
+
for (const { error } of group.items) {
|
|
143
|
+
const key = JSON.stringify(error);
|
|
144
|
+
const existing = errors.get(key);
|
|
145
|
+
if (existing) existing.count++;
|
|
146
|
+
else errors.set(key, {
|
|
147
|
+
...error,
|
|
148
|
+
count: 1
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return await sendData({
|
|
152
|
+
url: this.endpoint,
|
|
153
|
+
data: JSON.stringify({
|
|
154
|
+
...group.context,
|
|
155
|
+
errors: [...errors.values()]
|
|
156
|
+
}),
|
|
157
|
+
debug: this.options.debug,
|
|
158
|
+
debugPrefix: "[ErrorTracker]",
|
|
159
|
+
settleRejected: true
|
|
160
|
+
}) ? [] : group.items;
|
|
161
|
+
}))).flat();
|
|
162
|
+
} finally {
|
|
163
|
+
this.flushing = false;
|
|
164
|
+
}
|
|
165
|
+
if (generation !== this.generation) return;
|
|
166
|
+
this.queue = [...failed, ...this.queue].slice(-this.maxQueueSize);
|
|
167
|
+
if (!this.started && !failed.length) this.flush();
|
|
154
168
|
}
|
|
155
169
|
};
|
|
156
170
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ExtensionHook, i as ExtensionHandler, n as AnalyticsExtensionContext, o as ExtensionHooks, r as ExtensionCleanup, t as AnalyticsExtension } from "./chunks/extensions-
|
|
1
|
+
import { a as ExtensionHook, i as ExtensionHandler, n as AnalyticsExtensionContext, o as ExtensionHooks, r as ExtensionCleanup, t as AnalyticsExtension } from "./chunks/extensions-DZDQn8O8.js";
|
|
2
2
|
//#region src/analytics.d.ts
|
|
3
3
|
type ConsentMode = "anonymous" | "granted" | "denied";
|
|
4
4
|
interface IdentifyOptions {
|
|
@@ -32,7 +32,7 @@ export declare class WebAnalytics {
|
|
|
32
32
|
private readonly baseUrl;
|
|
33
33
|
private readonly debug;
|
|
34
34
|
private readonly cleanup;
|
|
35
|
-
private
|
|
35
|
+
private state;
|
|
36
36
|
private heartbeatTimer;
|
|
37
37
|
private extensions;
|
|
38
38
|
private pageKey;
|
|
@@ -53,6 +53,8 @@ export declare class WebAnalytics {
|
|
|
53
53
|
private canTrack;
|
|
54
54
|
private isCookieless;
|
|
55
55
|
private isActive;
|
|
56
|
+
private session;
|
|
57
|
+
private context;
|
|
56
58
|
private activate;
|
|
57
59
|
private startExtensions;
|
|
58
60
|
private stopExtensions;
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-I5UnTXGR.js";
|
|
1
|
+
import { a as resetSession, d as getPageContext, f as resolveBaseUrl, n as createId, o as touchActivity, p as sanitizeUrl, r as getSessionContext, t as sendData, u as URLS } from "./chunks/send-data-D520oab1.js";
|
|
2
|
+
import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-BwGNL11R.js";
|
|
4
3
|
//#region src/extensions.ts
|
|
5
4
|
var ExtensionHost = class {
|
|
6
|
-
|
|
5
|
+
context;
|
|
6
|
+
log;
|
|
7
7
|
handlers = /* @__PURE__ */ new Map();
|
|
8
8
|
cleanups = [];
|
|
9
9
|
stopped = false;
|
|
10
|
-
constructor(extensions,
|
|
11
|
-
this.
|
|
10
|
+
constructor(extensions, context, log) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
this.log = log;
|
|
12
13
|
for (const extension of extensions) this.setup(extension);
|
|
13
14
|
}
|
|
14
15
|
emit(hook, event) {
|
|
15
16
|
if (this.stopped) return;
|
|
16
17
|
for (const handler of this.handlers.get(hook) ?? []) try {
|
|
17
|
-
handler(event)?.catch((error) => this.
|
|
18
|
+
handler(event)?.catch((error) => this.log(`${hook} failed`, error));
|
|
18
19
|
} catch (error) {
|
|
19
|
-
this.
|
|
20
|
+
this.log(`${hook} failed`, error);
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
stop(discard = false) {
|
|
@@ -25,7 +26,7 @@ var ExtensionHost = class {
|
|
|
25
26
|
for (const cleanup of this.cleanups.reverse()) try {
|
|
26
27
|
cleanup({ discard });
|
|
27
28
|
} catch (error) {
|
|
28
|
-
this.
|
|
29
|
+
this.log("cleanup failed", error);
|
|
29
30
|
}
|
|
30
31
|
this.cleanups.length = 0;
|
|
31
32
|
this.handlers.clear();
|
|
@@ -35,7 +36,7 @@ var ExtensionHost = class {
|
|
|
35
36
|
const subscriptions = [];
|
|
36
37
|
try {
|
|
37
38
|
const cleanup = await extension.setup({
|
|
38
|
-
...this.
|
|
39
|
+
...this.context,
|
|
39
40
|
on: (hook, handler) => {
|
|
40
41
|
if (this.stopped) return () => {};
|
|
41
42
|
let handlers = this.handlers.get(hook);
|
|
@@ -53,11 +54,40 @@ var ExtensionHost = class {
|
|
|
53
54
|
else if (cleanup) this.cleanups.push(cleanup);
|
|
54
55
|
} catch (error) {
|
|
55
56
|
for (const off of subscriptions) off();
|
|
56
|
-
this.
|
|
57
|
+
this.log(`${extension.name} setup failed`, error);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
};
|
|
60
61
|
//#endregion
|
|
62
|
+
//#region src/outbound-links.ts
|
|
63
|
+
const DEDUPE_MS = 500;
|
|
64
|
+
function outboundHref(event) {
|
|
65
|
+
for (const node of event.composedPath()) if (node instanceof HTMLAnchorElement && (node.protocol === "http:" || node.protocol === "https:") && node.host !== location.host) return node.href;
|
|
66
|
+
}
|
|
67
|
+
function trackOutboundLinks(report) {
|
|
68
|
+
let last;
|
|
69
|
+
const onClick = (event) => {
|
|
70
|
+
if (!(event instanceof MouseEvent) || event.defaultPrevented) return;
|
|
71
|
+
if (event.button !== (event.type === "click" ? 0 : 1)) return;
|
|
72
|
+
const href = outboundHref(event);
|
|
73
|
+
if (!href) return;
|
|
74
|
+
const key = `${event.button}:${event.clientX}:${event.clientY}:${href}`;
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const duplicate = last?.key === key && now - last.at <= DEDUPE_MS;
|
|
77
|
+
last = {
|
|
78
|
+
key,
|
|
79
|
+
at: now
|
|
80
|
+
};
|
|
81
|
+
if (!duplicate) report(sanitizeUrl(href));
|
|
82
|
+
};
|
|
83
|
+
document.addEventListener("click", onClick, true);
|
|
84
|
+
document.addEventListener("auxclick", onClick, true);
|
|
85
|
+
return () => {
|
|
86
|
+
document.removeEventListener("click", onClick, true);
|
|
87
|
+
document.removeEventListener("auxclick", onClick, true);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
61
91
|
//#region src/analytics.ts
|
|
62
92
|
const navigationListeners = /* @__PURE__ */ new Set();
|
|
63
93
|
const historyPatches = {};
|
|
@@ -95,12 +125,18 @@ function observeHistory(listener) {
|
|
|
95
125
|
}
|
|
96
126
|
};
|
|
97
127
|
}
|
|
128
|
+
const RESERVED_EVENTS = /* @__PURE__ */ new Set([
|
|
129
|
+
"pageview",
|
|
130
|
+
"page_leave",
|
|
131
|
+
"error",
|
|
132
|
+
"outbound_link"
|
|
133
|
+
]);
|
|
98
134
|
var WebAnalytics = class {
|
|
99
135
|
options;
|
|
100
136
|
baseUrl;
|
|
101
137
|
debug;
|
|
102
138
|
cleanup = [];
|
|
103
|
-
|
|
139
|
+
state = "idle";
|
|
104
140
|
heartbeatTimer = null;
|
|
105
141
|
extensions = null;
|
|
106
142
|
pageKey = "";
|
|
@@ -110,20 +146,20 @@ var WebAnalytics = class {
|
|
|
110
146
|
constructor(options) {
|
|
111
147
|
this.options = options;
|
|
112
148
|
if (!options.siteKey.trim()) throw new Error("siteKey is required");
|
|
113
|
-
this.baseUrl = resolveBaseUrl(options.baseUrl
|
|
149
|
+
this.baseUrl = resolveBaseUrl(options.baseUrl);
|
|
114
150
|
this.debug = options.debug ?? false;
|
|
115
151
|
this.consentMode = options.consent ?? "granted";
|
|
116
152
|
}
|
|
117
153
|
start() {
|
|
118
|
-
if (this.
|
|
119
|
-
this.
|
|
154
|
+
if (this.state !== "idle" || typeof window === "undefined") return;
|
|
155
|
+
this.state = "started";
|
|
120
156
|
this.installBrowserLifecycle();
|
|
121
157
|
if (this.canTrack()) this.activate("load");
|
|
122
158
|
}
|
|
123
159
|
destroy() {
|
|
124
|
-
if (this.
|
|
160
|
+
if (this.state === "destroyed") return;
|
|
125
161
|
this.leavePage();
|
|
126
|
-
this.
|
|
162
|
+
this.state = "destroyed";
|
|
127
163
|
this.pendingPageview = null;
|
|
128
164
|
for (const cleanup of this.cleanup.splice(0).reverse()) cleanup();
|
|
129
165
|
this.stopExtensions(false);
|
|
@@ -135,26 +171,22 @@ var WebAnalytics = class {
|
|
|
135
171
|
this.leavePage();
|
|
136
172
|
if (this.pageKey) this.extensions?.emit("pageChange", { url: getPageContext(this.options.trackHash).url ?? "" });
|
|
137
173
|
this.pageKey = key;
|
|
174
|
+
this.pendingPageview = null;
|
|
138
175
|
}
|
|
139
176
|
if (document.visibilityState !== "visible") {
|
|
140
|
-
this.pendingPageview
|
|
177
|
+
this.pendingPageview ??= properties;
|
|
141
178
|
return;
|
|
142
179
|
}
|
|
143
180
|
this.pendingPageview = null;
|
|
144
181
|
if (document.hasFocus()) touchActivity(this.options.siteKey, this.isCookieless());
|
|
145
|
-
const session =
|
|
146
|
-
const
|
|
147
|
-
if (this.visit?.session.sessionId === session.sessionId && this.visit.context.userId ===
|
|
182
|
+
const session = this.session();
|
|
183
|
+
const context = this.context(session);
|
|
184
|
+
if (this.visit?.session.sessionId === session.sessionId && this.visit.context.userId === context.userId) return;
|
|
148
185
|
this.leavePage();
|
|
149
186
|
this.visit = {
|
|
150
187
|
id: createId(),
|
|
151
188
|
session,
|
|
152
|
-
context
|
|
153
|
-
...getPageContext(this.options.trackHash),
|
|
154
|
-
userId: userId || void 0,
|
|
155
|
-
sessionId: session.sessionId,
|
|
156
|
-
windowId: session.windowId
|
|
157
|
-
},
|
|
189
|
+
context,
|
|
158
190
|
duration: 0,
|
|
159
191
|
startedAt: null,
|
|
160
192
|
reportedDuration: -1
|
|
@@ -168,7 +200,7 @@ var WebAnalytics = class {
|
|
|
168
200
|
track(name, properties = {}) {
|
|
169
201
|
if (!this.isActive()) return;
|
|
170
202
|
const eventName = name.trim();
|
|
171
|
-
if (!eventName || eventName
|
|
203
|
+
if (!eventName || RESERVED_EVENTS.has(eventName)) return;
|
|
172
204
|
this.pageview({ trigger: "session" });
|
|
173
205
|
this.send(eventName, properties);
|
|
174
206
|
}
|
|
@@ -233,7 +265,7 @@ var WebAnalytics = class {
|
|
|
233
265
|
if (wasTracking) {
|
|
234
266
|
this.extensions?.emit("consentChange", { cookieless: willBeCookieless });
|
|
235
267
|
if (wasCookieless !== willBeCookieless) this.pageview({ trigger: "consent" });
|
|
236
|
-
} else if (this.started) this.activate("consent");
|
|
268
|
+
} else if (this.state === "started") this.activate("consent");
|
|
237
269
|
}
|
|
238
270
|
getAnonymousId() {
|
|
239
271
|
return this.canTrack() ? getAnonymousId(this.options.siteKey, this.isCookieless()) : "";
|
|
@@ -249,7 +281,18 @@ var WebAnalytics = class {
|
|
|
249
281
|
return this.options.cookieless === true || this.consentMode === "anonymous";
|
|
250
282
|
}
|
|
251
283
|
isActive() {
|
|
252
|
-
return this.
|
|
284
|
+
return this.state === "started" && this.canTrack();
|
|
285
|
+
}
|
|
286
|
+
session() {
|
|
287
|
+
return getSessionContext(this.options.siteKey, this.isCookieless());
|
|
288
|
+
}
|
|
289
|
+
context(session = this.session()) {
|
|
290
|
+
return {
|
|
291
|
+
...getPageContext(this.options.trackHash),
|
|
292
|
+
userId: this.getAnonymousId() || void 0,
|
|
293
|
+
sessionId: session.sessionId,
|
|
294
|
+
windowId: session.windowId
|
|
295
|
+
};
|
|
253
296
|
}
|
|
254
297
|
activate(trigger) {
|
|
255
298
|
this.startExtensions();
|
|
@@ -257,22 +300,17 @@ var WebAnalytics = class {
|
|
|
257
300
|
}
|
|
258
301
|
startExtensions() {
|
|
259
302
|
if (this.extensions || !this.options.extensions?.length) return;
|
|
260
|
-
|
|
303
|
+
this.extensions = new ExtensionHost(this.options.extensions, {
|
|
261
304
|
siteKey: this.options.siteKey,
|
|
262
305
|
baseUrl: this.baseUrl,
|
|
263
306
|
debug: this.debug,
|
|
264
307
|
trackHash: this.options.trackHash ?? false,
|
|
265
308
|
sdkName: this.options.sdkName,
|
|
266
309
|
sdkVersion: this.options.sdkVersion,
|
|
267
|
-
isCookieless: () => this.isCookieless()
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
capture: (event, properties) => this.send(event, properties),
|
|
271
|
-
log: (...details) => {
|
|
272
|
-
if (this.debug) console.log(...details);
|
|
273
|
-
}
|
|
310
|
+
isCookieless: () => this.isCookieless()
|
|
311
|
+
}, (...details) => {
|
|
312
|
+
if (this.debug) console.log(...details);
|
|
274
313
|
});
|
|
275
|
-
this.extensions = host;
|
|
276
314
|
}
|
|
277
315
|
stopExtensions(discard) {
|
|
278
316
|
this.extensions?.stop(discard);
|
|
@@ -286,7 +324,7 @@ var WebAnalytics = class {
|
|
|
286
324
|
this.bind(window, "blur", () => this.pauseVisit());
|
|
287
325
|
this.bind(window, "popstate", this.onNavigate);
|
|
288
326
|
if (this.options.trackHash) this.bind(window, "hashchange", this.onNavigate);
|
|
289
|
-
this.cleanup.push(observeHistory(this.onNavigate));
|
|
327
|
+
this.cleanup.push(observeHistory(this.onNavigate), trackOutboundLinks((url) => this.send("outbound_link", { outbound_link: url })));
|
|
290
328
|
}
|
|
291
329
|
bind(target, type, listener) {
|
|
292
330
|
target.addEventListener(type, listener);
|
|
@@ -359,21 +397,14 @@ var WebAnalytics = class {
|
|
|
359
397
|
this.pauseVisit();
|
|
360
398
|
return;
|
|
361
399
|
}
|
|
400
|
+
const visit = this.visit;
|
|
362
401
|
this.pageview({ trigger: "session" });
|
|
363
|
-
this.checkpoint();
|
|
402
|
+
if (this.visit === visit) this.checkpoint();
|
|
364
403
|
}, 15e3);
|
|
365
404
|
}
|
|
366
405
|
send(event, properties = {}, context) {
|
|
367
406
|
if (!this.isActive()) return;
|
|
368
|
-
|
|
369
|
-
const session = getSessionContext(this.options.siteKey, this.isCookieless());
|
|
370
|
-
context = {
|
|
371
|
-
...getPageContext(this.options.trackHash),
|
|
372
|
-
userId: this.getAnonymousId() || void 0,
|
|
373
|
-
sessionId: session.sessionId,
|
|
374
|
-
windowId: session.windowId
|
|
375
|
-
};
|
|
376
|
-
}
|
|
407
|
+
context ??= this.context();
|
|
377
408
|
let data;
|
|
378
409
|
try {
|
|
379
410
|
data = JSON.stringify({
|
package/dist/replay.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as AnalyticsExtension } from "./chunks/extensions-
|
|
1
|
+
import { t as AnalyticsExtension } from "./chunks/extensions-DZDQn8O8.js";
|
|
2
2
|
import { record } from "@rrweb/record";
|
|
3
3
|
import { LogLevel } from "@rrweb/rrweb-plugin-console-record";
|
|
4
4
|
import { NetworkRecordOptions } from "@rrweb/rrweb-plugin-network-record";
|
|
@@ -89,12 +89,14 @@ export default class ReplayTracker {
|
|
|
89
89
|
private lastCookielessMode;
|
|
90
90
|
private needsFinalization;
|
|
91
91
|
private initialFlushPending;
|
|
92
|
+
private resumed;
|
|
92
93
|
private overflowed;
|
|
93
94
|
private replayEventSequence;
|
|
94
95
|
private readonly mutationThrottler;
|
|
95
96
|
constructor(options: ReplayTrackerOptions);
|
|
96
97
|
private log;
|
|
97
98
|
private clearFlushTimers;
|
|
99
|
+
private dropEvents;
|
|
98
100
|
private clearQueues;
|
|
99
101
|
private finalizeSession;
|
|
100
102
|
setCookielessMode(cookieless: boolean): void;
|
|
@@ -111,7 +113,6 @@ export default class ReplayTracker {
|
|
|
111
113
|
private beginReplayStream;
|
|
112
114
|
private resetSessionTiming;
|
|
113
115
|
private onEvent;
|
|
114
|
-
private onUnload;
|
|
115
116
|
private requestTerminalFlush;
|
|
116
117
|
private hasReachedMinLength;
|
|
117
118
|
private requestFlush;
|
package/dist/replay.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-I5UnTXGR.js";
|
|
1
|
+
import { f as resolveBaseUrl, i as onSessionRotated, l as setStorageItem, n as createId, o as touchActivity, p as sanitizeUrl, r as getSessionContext, s as getStorageItem, t as sendData, u as URLS } from "./chunks/send-data-D520oab1.js";
|
|
2
|
+
import { t as getAnonymousId } from "./chunks/identifiers-BwGNL11R.js";
|
|
4
3
|
import { record } from "@rrweb/record";
|
|
5
4
|
import { getRecordConsolePlugin } from "@rrweb/rrweb-plugin-console-record";
|
|
6
5
|
import { getRecordNetworkPlugin } from "@rrweb/rrweb-plugin-network-record";
|
|
@@ -57,44 +56,39 @@ const counters = /* @__PURE__ */ new Map();
|
|
|
57
56
|
function sameRecording(counter, context) {
|
|
58
57
|
return counter?.sessionId === context.sessionId && counter.windowId === context.windowId;
|
|
59
58
|
}
|
|
60
|
-
function readCounter(
|
|
59
|
+
function readCounter(siteKey, cookieless) {
|
|
60
|
+
const raw = cookieless ? null : getStorageItem("sessionStorage", sequenceKey(siteKey));
|
|
61
|
+
if (raw === null) return { corrupt: false };
|
|
61
62
|
try {
|
|
62
|
-
const parsed = JSON.parse(
|
|
63
|
-
if (parsed && typeof parsed.sessionId === "string" && typeof parsed.windowId === "string" && typeof parsed.next === "number" && Number.isSafeInteger(parsed.next) && parsed.next >= 0
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (parsed && typeof parsed.sessionId === "string" && typeof parsed.windowId === "string" && typeof parsed.next === "number" && Number.isSafeInteger(parsed.next) && parsed.next >= 0) return {
|
|
65
|
+
corrupt: false,
|
|
66
|
+
counter: parsed
|
|
67
|
+
};
|
|
64
68
|
} catch {}
|
|
69
|
+
return { corrupt: true };
|
|
70
|
+
}
|
|
71
|
+
const sequenceKey = (siteKey) => `faststats_replay_sequence_v1_${siteKey}`;
|
|
72
|
+
/** Whether this recording already sent batches, e.g. from an earlier page load. */
|
|
73
|
+
function hasReplaySequence(siteKey, context, cookieless) {
|
|
74
|
+
return [counters.get(siteKey), readCounter(siteKey, cookieless).counter].some((counter) => sameRecording(counter, context) && counter.next > 0);
|
|
65
75
|
}
|
|
76
|
+
/** Next batch sequence for this recording, or undefined when it cannot be trusted. */
|
|
66
77
|
function nextReplaySequence(siteKey, context, cookieless) {
|
|
67
|
-
const key = `faststats_replay_sequence_v1_${siteKey}`;
|
|
68
78
|
const cached = counters.get(siteKey);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const next = legacy === null ? 0 : Number(legacy);
|
|
78
|
-
if (!Number.isSafeInteger(next) || next < 0 || legacy !== null && !/^\d+$/.test(legacy)) return;
|
|
79
|
-
counter = {
|
|
80
|
-
sessionId: context.sessionId,
|
|
81
|
-
windowId: context.windowId,
|
|
82
|
-
next,
|
|
83
|
-
version: legacy === null ? 1 : 0
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
if (!Number.isSafeInteger(counter.next + 1)) return;
|
|
87
|
-
const sequence = counter.next;
|
|
88
|
-
const updated = {
|
|
89
|
-
...counter,
|
|
79
|
+
const stored = readCounter(siteKey, cookieless);
|
|
80
|
+
if (stored.corrupt && !sameRecording(cached, context)) return;
|
|
81
|
+
let sequence = sameRecording(cached, context) ? cached.next : 0;
|
|
82
|
+
if (sameRecording(stored.counter, context)) sequence = Math.max(sequence, stored.counter.next);
|
|
83
|
+
if (!Number.isSafeInteger(sequence + 1)) return;
|
|
84
|
+
const counter = {
|
|
85
|
+
sessionId: context.sessionId,
|
|
86
|
+
windowId: context.windowId,
|
|
90
87
|
next: sequence + 1
|
|
91
88
|
};
|
|
92
|
-
counters.set(siteKey,
|
|
93
|
-
if (!cookieless
|
|
94
|
-
return
|
|
95
|
-
sequence,
|
|
96
|
-
...counter.version === 1 ? { sequenceContractVersion: 1 } : {}
|
|
97
|
-
};
|
|
89
|
+
counters.set(siteKey, counter);
|
|
90
|
+
if (!cookieless) setStorageItem("sessionStorage", sequenceKey(siteKey), JSON.stringify(counter));
|
|
91
|
+
return sequence;
|
|
98
92
|
}
|
|
99
93
|
//#endregion
|
|
100
94
|
//#region src/replay.ts
|
|
@@ -170,13 +164,14 @@ var ReplayTracker = class {
|
|
|
170
164
|
lastCookielessMode;
|
|
171
165
|
needsFinalization = false;
|
|
172
166
|
initialFlushPending = true;
|
|
167
|
+
resumed = false;
|
|
173
168
|
overflowed = false;
|
|
174
169
|
replayEventSequence = 0;
|
|
175
170
|
mutationThrottler;
|
|
176
171
|
constructor(options) {
|
|
177
172
|
this.options = options;
|
|
178
173
|
this.lastCookielessMode = options.cookieless ?? false;
|
|
179
|
-
this.endpoint = `${resolveBaseUrl(options.baseUrl
|
|
174
|
+
this.endpoint = `${resolveBaseUrl(options.baseUrl)}${URLS.replay}`;
|
|
180
175
|
this.flushInterval = boundedInteger(options.flushInterval, 15e3, 1, MAX_TIMEOUT_MS);
|
|
181
176
|
this.maxEvents = boundedInteger(options.maxEvents, 1e3, 1, 1e5);
|
|
182
177
|
this.maxBatchSizeBytes = boundedInteger(options.maxBatchSizeBytes, DEFAULT_MAX_BATCH_SIZE_BYTES, 1, 33554432);
|
|
@@ -198,10 +193,13 @@ var ReplayTracker = class {
|
|
|
198
193
|
this.retryAttempt = 0;
|
|
199
194
|
this.minLengthFlushTask = null;
|
|
200
195
|
}
|
|
201
|
-
|
|
202
|
-
this.needsFinalization = false;
|
|
196
|
+
dropEvents() {
|
|
203
197
|
this.events.length = 0;
|
|
204
198
|
this.queuedEventsSizeBytes = 0;
|
|
199
|
+
}
|
|
200
|
+
clearQueues() {
|
|
201
|
+
this.needsFinalization = false;
|
|
202
|
+
this.dropEvents();
|
|
205
203
|
this.pending.length = 0;
|
|
206
204
|
this.pendingSizeBytes = 0;
|
|
207
205
|
}
|
|
@@ -218,8 +216,7 @@ var ReplayTracker = class {
|
|
|
218
216
|
this.clearFlushTimers();
|
|
219
217
|
const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
|
|
220
218
|
this.activeSessionId = context.sessionId;
|
|
221
|
-
this.
|
|
222
|
-
this.chunkStartedAt = Date.now();
|
|
219
|
+
this.resetSessionTiming(context);
|
|
223
220
|
this.beginReplayStream();
|
|
224
221
|
}
|
|
225
222
|
start() {
|
|
@@ -233,12 +230,10 @@ var ReplayTracker = class {
|
|
|
233
230
|
this.viewUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
|
|
234
231
|
const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
|
|
235
232
|
this.activeSessionId = context.sessionId;
|
|
236
|
-
this.
|
|
237
|
-
this.chunkStartedAt = Date.now();
|
|
233
|
+
this.resetSessionTiming(context);
|
|
238
234
|
this.subscribeToSessionRotation();
|
|
239
235
|
this.beginRecording();
|
|
240
236
|
this.intervalId = setInterval(() => this.requestFlush("interval"), this.flushInterval);
|
|
241
|
-
window.addEventListener("beforeunload", this.onUnload);
|
|
242
237
|
this.log("Recording started");
|
|
243
238
|
}
|
|
244
239
|
trackPageChange(url) {
|
|
@@ -288,7 +283,7 @@ var ReplayTracker = class {
|
|
|
288
283
|
}));
|
|
289
284
|
if (this.options.recordNetwork === true) {
|
|
290
285
|
const options = this.options.networkOptions;
|
|
291
|
-
const base = resolveBaseUrl(this.options.baseUrl
|
|
286
|
+
const base = resolveBaseUrl(this.options.baseUrl);
|
|
292
287
|
const endpoints = new Set(Object.values(URLS).map((path) => {
|
|
293
288
|
const url = new URL(`${base}${path}`, window.location.href);
|
|
294
289
|
return `${url.origin}${url.pathname}`;
|
|
@@ -351,15 +346,13 @@ var ReplayTracker = class {
|
|
|
351
346
|
if (this.intervalId) clearInterval(this.intervalId);
|
|
352
347
|
this.clearFlushTimers();
|
|
353
348
|
this.intervalId = null;
|
|
354
|
-
window.removeEventListener("beforeunload", this.onUnload);
|
|
355
349
|
if (discard) {
|
|
356
350
|
this.clearQueues();
|
|
357
351
|
this.log("Recording discarded");
|
|
358
352
|
return;
|
|
359
353
|
}
|
|
360
354
|
if (!this.hasReachedMinLength()) {
|
|
361
|
-
this.
|
|
362
|
-
this.queuedEventsSizeBytes = 0;
|
|
355
|
+
this.dropEvents();
|
|
363
356
|
this.needsFinalization = false;
|
|
364
357
|
this.log(`Recording too short (${Date.now() - this.startTime}ms), discarding events`);
|
|
365
358
|
}
|
|
@@ -377,14 +370,8 @@ var ReplayTracker = class {
|
|
|
377
370
|
}
|
|
378
371
|
applySessionRotation(prev, next) {
|
|
379
372
|
if (this.activeSessionId === next.sessionId) return;
|
|
380
|
-
if (this.hasReachedMinLength()) this.enqueueEventsIfReady(prev,
|
|
381
|
-
|
|
382
|
-
flushReason: "sessionRotate"
|
|
383
|
-
});
|
|
384
|
-
else {
|
|
385
|
-
this.events.length = 0;
|
|
386
|
-
this.queuedEventsSizeBytes = 0;
|
|
387
|
-
}
|
|
373
|
+
if (this.hasReachedMinLength()) this.enqueueEventsIfReady(prev, "sessionRotate");
|
|
374
|
+
else this.dropEvents();
|
|
388
375
|
this.finalizeSession(prev, "sessionRotate");
|
|
389
376
|
this.needsFinalization = false;
|
|
390
377
|
this.activeSessionId = next.sessionId;
|
|
@@ -411,9 +398,10 @@ var ReplayTracker = class {
|
|
|
411
398
|
}
|
|
412
399
|
}
|
|
413
400
|
}
|
|
414
|
-
resetSessionTiming(
|
|
401
|
+
resetSessionTiming(next) {
|
|
415
402
|
if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
|
|
416
403
|
this.minLengthFlushTask = null;
|
|
404
|
+
this.resumed = hasReplaySequence(this.options.siteKey, next, this.lastCookielessMode);
|
|
417
405
|
this.startTime = Date.now();
|
|
418
406
|
this.chunkStartedAt = Date.now();
|
|
419
407
|
this.initialFlushPending = true;
|
|
@@ -454,22 +442,18 @@ var ReplayTracker = class {
|
|
|
454
442
|
if (reason) this.requestFlush(reason);
|
|
455
443
|
else this.scheduleMinLengthFlush();
|
|
456
444
|
};
|
|
457
|
-
onUnload = () => {
|
|
458
|
-
this.flush(true, false, "unload");
|
|
459
|
-
};
|
|
460
445
|
requestTerminalFlush(reason) {
|
|
461
446
|
if (!this.hasReachedMinLength()) {
|
|
462
|
-
this.
|
|
463
|
-
this.queuedEventsSizeBytes = 0;
|
|
447
|
+
this.dropEvents();
|
|
464
448
|
this.needsFinalization = false;
|
|
465
449
|
return;
|
|
466
450
|
}
|
|
467
451
|
this.flush(true, true, reason);
|
|
468
452
|
}
|
|
469
453
|
hasReachedMinLength() {
|
|
470
|
-
return this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
|
|
454
|
+
return this.resumed || this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
|
|
471
455
|
}
|
|
472
|
-
requestFlush
|
|
456
|
+
requestFlush(reason) {
|
|
473
457
|
if (this.flushTask || this.events.length === 0) return;
|
|
474
458
|
if (this.minLengthFlushTask) {
|
|
475
459
|
clearTimeout(this.minLengthFlushTask);
|
|
@@ -479,7 +463,7 @@ var ReplayTracker = class {
|
|
|
479
463
|
this.flushTask = null;
|
|
480
464
|
this.flush(false, false, reason);
|
|
481
465
|
}, 0);
|
|
482
|
-
}
|
|
466
|
+
}
|
|
483
467
|
scheduleMinLengthFlush() {
|
|
484
468
|
if (!this.initialFlushPending || this.events.length === 0 || this.minReplayLengthMs <= 0) return;
|
|
485
469
|
if (this.hasReachedMinLength()) {
|
|
@@ -495,17 +479,20 @@ var ReplayTracker = class {
|
|
|
495
479
|
this.requestFlush("minLength");
|
|
496
480
|
}, Math.min(remainingMs, MAX_TIMEOUT_MS));
|
|
497
481
|
}
|
|
498
|
-
enqueueEventsIfReady(context,
|
|
499
|
-
const { isFinal = false, flushReason = "manual", maxBatchSizeBytes = this.maxBatchSizeBytes } = options;
|
|
482
|
+
enqueueEventsIfReady(context, flushReason, maxBatchSizeBytes = this.maxBatchSizeBytes) {
|
|
500
483
|
if (this.events.length === 0) return;
|
|
501
|
-
if (!
|
|
484
|
+
if (!this.hasReachedMinLength()) {
|
|
502
485
|
this.scheduleMinLengthFlush();
|
|
503
486
|
return;
|
|
504
487
|
}
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
488
|
+
for (const events of this.takeEventBatches(maxBatchSizeBytes)) {
|
|
489
|
+
if (this.pending.length >= this.maxPendingBatches) {
|
|
490
|
+
this.stopCapture("Replay queue limit reached");
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
const batch = this.createBatch(events, context, false, flushReason);
|
|
494
|
+
if (!batch) break;
|
|
495
|
+
this.enqueueBatch(batch);
|
|
509
496
|
}
|
|
510
497
|
this.queuedEventsSizeBytes = 0;
|
|
511
498
|
}
|
|
@@ -526,10 +513,10 @@ var ReplayTracker = class {
|
|
|
526
513
|
if (current.length > 0) batches.push(current);
|
|
527
514
|
return batches;
|
|
528
515
|
}
|
|
529
|
-
createBatch(events, context, isFinal, flushReason
|
|
516
|
+
createBatch(events, context, isFinal, flushReason) {
|
|
530
517
|
const identifier = getAnonymousId(this.options.siteKey, this.lastCookielessMode);
|
|
531
518
|
const sequence = nextReplaySequence(this.options.siteKey, context, this.lastCookielessMode);
|
|
532
|
-
if (
|
|
519
|
+
if (sequence === void 0) {
|
|
533
520
|
this.stopCapture("Replay sequence state is invalid or exhausted");
|
|
534
521
|
return;
|
|
535
522
|
}
|
|
@@ -545,8 +532,9 @@ var ReplayTracker = class {
|
|
|
545
532
|
chunkStartedAt,
|
|
546
533
|
chunkEndedAt: now,
|
|
547
534
|
...identifier ? { identifier } : {},
|
|
548
|
-
batchId: `${context.sessionId}-${sequence
|
|
549
|
-
|
|
535
|
+
batchId: `${context.sessionId}-${sequence}-${chunkStartedAt}`,
|
|
536
|
+
sequence,
|
|
537
|
+
sequenceContractVersion: 1,
|
|
550
538
|
timestamp: now,
|
|
551
539
|
url: this.viewUrl || sanitizeUrl(window.location.href, this.options.trackHash ?? false),
|
|
552
540
|
...isFinal ? { isFinal: true } : {},
|
|
@@ -578,45 +566,27 @@ var ReplayTracker = class {
|
|
|
578
566
|
this.stopRecording = void 0;
|
|
579
567
|
}
|
|
580
568
|
enqueueBatch(batch) {
|
|
581
|
-
const size = this.batchSizeBytes(batch);
|
|
582
|
-
if (size > this.maxQueueSizeBytes || this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) {
|
|
583
|
-
this.stopCapture(`Replay queue limit reached at batch ${batch.sequence}`);
|
|
584
|
-
return false;
|
|
585
|
-
}
|
|
586
569
|
this.needsFinalization = !batch.isFinal;
|
|
587
570
|
this.pending.push(batch);
|
|
588
|
-
this.pendingSizeBytes +=
|
|
589
|
-
return true;
|
|
571
|
+
this.pendingSizeBytes += this.batchSizeBytes(batch);
|
|
590
572
|
}
|
|
591
573
|
async flush(lowLatency, isFinal = false, flushReason = "manual") {
|
|
592
574
|
if (this.events.length === 0 && !isFinal) {
|
|
593
|
-
await this.sendPending(lowLatency
|
|
575
|
+
await this.sendPending(lowLatency);
|
|
594
576
|
return;
|
|
595
577
|
}
|
|
596
578
|
const rotation = touchActivity(this.options.siteKey, this.lastCookielessMode);
|
|
597
579
|
if (rotation) this.applySessionRotation(rotation.prev, rotation.next);
|
|
598
580
|
const context = getSessionContext(this.options.siteKey, this.lastCookielessMode);
|
|
599
|
-
this.enqueueEventsIfReady(context,
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (this.
|
|
606
|
-
await this.sendPending(lowLatency, lowLatency && isFinal);
|
|
607
|
-
}
|
|
608
|
-
async sendPending(lowLatency, bypassRetryDelay) {
|
|
609
|
-
if (this.retryTask) {
|
|
610
|
-
if (!bypassRetryDelay) return;
|
|
611
|
-
clearTimeout(this.retryTask);
|
|
612
|
-
this.retryTask = null;
|
|
613
|
-
}
|
|
581
|
+
this.enqueueEventsIfReady(context, flushReason, lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes);
|
|
582
|
+
if (!isFinal) return this.sendPending(lowLatency);
|
|
583
|
+
this.finalizeSession(context, flushReason);
|
|
584
|
+
for (const batch of [...this.pending].reverse()) this.sendBatch(batch, true).then((sent) => sent && this.removePending(batch));
|
|
585
|
+
}
|
|
586
|
+
async sendPending(lowLatency) {
|
|
587
|
+
if (this.retryTask) return;
|
|
614
588
|
if (this.sending) {
|
|
615
|
-
|
|
616
|
-
this.queuedFlush = {
|
|
617
|
-
lowLatency: (queued?.lowLatency ?? false) || lowLatency,
|
|
618
|
-
bypassRetryDelay: (queued?.bypassRetryDelay ?? false) || bypassRetryDelay
|
|
619
|
-
};
|
|
589
|
+
this.queuedFlush = !!this.queuedFlush || lowLatency;
|
|
620
590
|
return;
|
|
621
591
|
}
|
|
622
592
|
this.sending = true;
|
|
@@ -630,14 +600,14 @@ var ReplayTracker = class {
|
|
|
630
600
|
}
|
|
631
601
|
this.retryAttempt = 0;
|
|
632
602
|
this.removePending(batch);
|
|
633
|
-
if (this.queuedFlush
|
|
603
|
+
if (this.queuedFlush) break;
|
|
634
604
|
}
|
|
635
605
|
} finally {
|
|
636
606
|
this.sending = false;
|
|
637
|
-
if (this.queuedFlush) {
|
|
638
|
-
const
|
|
607
|
+
if (this.queuedFlush !== null) {
|
|
608
|
+
const lowLatency = this.queuedFlush;
|
|
639
609
|
this.queuedFlush = null;
|
|
640
|
-
this.sendPending(
|
|
610
|
+
this.sendPending(lowLatency);
|
|
641
611
|
}
|
|
642
612
|
}
|
|
643
613
|
}
|
|
@@ -649,7 +619,7 @@ var ReplayTracker = class {
|
|
|
649
619
|
this.retryTask = setTimeout(() => {
|
|
650
620
|
this.retryTask = null;
|
|
651
621
|
if (this.retryAttempt === RETRY_MAX_ATTEMPTS) this.retryAttempt = 0;
|
|
652
|
-
this.sendPending(false
|
|
622
|
+
this.sendPending(false);
|
|
653
623
|
}, delay);
|
|
654
624
|
}
|
|
655
625
|
retryDelay(attempt) {
|
|
@@ -659,12 +629,13 @@ var ReplayTracker = class {
|
|
|
659
629
|
}
|
|
660
630
|
async sendBatch(batch, lowLatency) {
|
|
661
631
|
const json = JSON.stringify(batch);
|
|
662
|
-
const lowLatencySafe = lowLatency &&
|
|
632
|
+
const lowLatencySafe = lowLatency && this.batchSizeBytes(batch) <= DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES;
|
|
663
633
|
const sendOptions = {
|
|
664
634
|
debug: this.options.debug,
|
|
665
635
|
debugPrefix: "[Replay]",
|
|
666
636
|
useBeacon: lowLatencySafe,
|
|
667
|
-
keepalive: lowLatencySafe
|
|
637
|
+
keepalive: lowLatencySafe,
|
|
638
|
+
settleRejected: true
|
|
668
639
|
};
|
|
669
640
|
if (!lowLatency && (this.options.compress ?? true) && this.compressionSupported) try {
|
|
670
641
|
const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
|
package/dist/web-vitals.d.ts
CHANGED
package/dist/web-vitals.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as getSessionContext, t as sendData } from "./chunks/send-data-Dplz7m4c.js";
|
|
1
|
+
import { f as resolveBaseUrl, p as sanitizeUrl, r as getSessionContext, t as sendData, u as URLS } from "./chunks/send-data-D520oab1.js";
|
|
3
2
|
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals/attribution";
|
|
4
3
|
//#region src/utils/web-vitals-collector.ts
|
|
5
4
|
const collectors = /* @__PURE__ */ new WeakMap();
|
|
@@ -72,7 +71,7 @@ var WebVitalsTracker = class {
|
|
|
72
71
|
cookieless;
|
|
73
72
|
constructor(options) {
|
|
74
73
|
this.options = options;
|
|
75
|
-
this.endpoint = `${resolveBaseUrl(options.baseUrl
|
|
74
|
+
this.endpoint = `${resolveBaseUrl(options.baseUrl)}${URLS.vitals}`;
|
|
76
75
|
this.cookieless = options.cookieless ?? false;
|
|
77
76
|
}
|
|
78
77
|
setCookielessMode(cookieless) {
|
|
@@ -104,10 +103,8 @@ var WebVitalsTracker = class {
|
|
|
104
103
|
});
|
|
105
104
|
}
|
|
106
105
|
onMetric = (metric) => {
|
|
107
|
-
if (!this.started) return;
|
|
108
|
-
|
|
109
|
-
if (!METRIC_NAMES.has(name) || !Number.isFinite(metric.value) || metric.value < 0) return;
|
|
110
|
-
this.metrics.set(name, metric);
|
|
106
|
+
if (!this.started || !METRIC_NAMES.has(metric.name) || !Number.isFinite(metric.value) || metric.value < 0) return;
|
|
107
|
+
this.metrics.set(metric.name, metric);
|
|
111
108
|
};
|
|
112
109
|
discard() {
|
|
113
110
|
this.generation++;
|
package/package.json
CHANGED
|
@@ -13,11 +13,6 @@
|
|
|
13
13
|
"import": "./dist/index.js",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
|
-
"./outbound-links": {
|
|
17
|
-
"types": "./dist/outbound-links.d.ts",
|
|
18
|
-
"import": "./dist/outbound-links.js",
|
|
19
|
-
"default": "./dist/outbound-links.js"
|
|
20
|
-
},
|
|
21
16
|
"./replay": {
|
|
22
17
|
"types": "./dist/replay.d.ts",
|
|
23
18
|
"import": "./dist/replay.js",
|
|
@@ -42,7 +37,7 @@
|
|
|
42
37
|
"publishConfig": {
|
|
43
38
|
"access": "public"
|
|
44
39
|
},
|
|
45
|
-
"version": "0.
|
|
40
|
+
"version": "0.11.0",
|
|
46
41
|
"scripts": {
|
|
47
42
|
"build": "tsdown && bun run check-size",
|
|
48
43
|
"dev": "bun run build",
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
//#region src/utils/api-urls.ts
|
|
2
|
-
const ANALYTICS_BASE = "https://metrics.faststats.dev";
|
|
3
|
-
function resolveBaseUrl(input, fallback) {
|
|
4
|
-
return input?.replace(/\/+$/, "") || fallback;
|
|
5
|
-
}
|
|
6
|
-
const URLS = {
|
|
7
|
-
events: "/v1/web",
|
|
8
|
-
identify: "/v1/identify",
|
|
9
|
-
replay: "/v1/replay",
|
|
10
|
-
vitals: "/v1/vitals"
|
|
11
|
-
};
|
|
12
|
-
function sanitizeUrl(url, includeHash = false) {
|
|
13
|
-
try {
|
|
14
|
-
const parsed = new URL(url, location.href);
|
|
15
|
-
return `${parsed.origin}${parsed.pathname}${includeHash ? parsed.hash : ""}`;
|
|
16
|
-
} catch {
|
|
17
|
-
return url.split(includeHash ? "?" : /[?#]/, 1)[0] ?? "";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const UTM_KEYS = [
|
|
21
|
-
"utm_source",
|
|
22
|
-
"utm_medium",
|
|
23
|
-
"utm_campaign",
|
|
24
|
-
"utm_term",
|
|
25
|
-
"utm_content"
|
|
26
|
-
];
|
|
27
|
-
function getReferrer() {
|
|
28
|
-
const raw = document.referrer;
|
|
29
|
-
if (!raw) return null;
|
|
30
|
-
try {
|
|
31
|
-
const pageOrigin = new URL(location.href).origin;
|
|
32
|
-
if (new URL(raw, location.href).origin === pageOrigin) return null;
|
|
33
|
-
} catch {}
|
|
34
|
-
return sanitizeUrl(raw);
|
|
35
|
-
}
|
|
36
|
-
function getPageContext(includeHash = false) {
|
|
37
|
-
const hash = includeHash ? location.hash : "";
|
|
38
|
-
const ctx = {
|
|
39
|
-
page: `${location.pathname}${hash}`,
|
|
40
|
-
url: sanitizeUrl(location.href, includeHash),
|
|
41
|
-
referrer: getReferrer(),
|
|
42
|
-
title: document.title || ""
|
|
43
|
-
};
|
|
44
|
-
if (!location.search) return ctx;
|
|
45
|
-
const sp = new URLSearchParams(location.search);
|
|
46
|
-
for (const key of UTM_KEYS) {
|
|
47
|
-
const value = sp.get(key);
|
|
48
|
-
if (value) ctx[key] = value;
|
|
49
|
-
}
|
|
50
|
-
return ctx;
|
|
51
|
-
}
|
|
52
|
-
//#endregion
|
|
53
|
-
export { sanitizeUrl as a, resolveBaseUrl as i, URLS as n, getPageContext as r, ANALYTICS_BASE as t };
|
package/dist/outbound-links.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
|
|
2
|
-
//#region src/outbound-links.d.ts
|
|
3
|
-
export declare function outboundLinks(): AnalyticsExtension;
|
|
4
|
-
export declare class OutboundLinkTracker {
|
|
5
|
-
private lastActivation;
|
|
6
|
-
getHref(event: Event): string | null;
|
|
7
|
-
private isDuplicate;
|
|
8
|
-
}
|
|
9
|
-
//#endregion
|
package/dist/outbound-links.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { a as sanitizeUrl } from "./chunks/api-urls-DWk43fu6.js";
|
|
2
|
-
//#region src/outbound-links.ts
|
|
3
|
-
function outboundLinks() {
|
|
4
|
-
return {
|
|
5
|
-
name: "outbound-links",
|
|
6
|
-
setup(context) {
|
|
7
|
-
const tracker = new OutboundLinkTracker();
|
|
8
|
-
const onClick = (event) => {
|
|
9
|
-
const href = tracker.getHref(event);
|
|
10
|
-
if (href) context.capture("outbound_link", { outbound_link: sanitizeUrl(href) });
|
|
11
|
-
};
|
|
12
|
-
document.addEventListener("click", onClick, true);
|
|
13
|
-
document.addEventListener("auxclick", onClick, true);
|
|
14
|
-
return () => {
|
|
15
|
-
document.removeEventListener("click", onClick, true);
|
|
16
|
-
document.removeEventListener("auxclick", onClick, true);
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
const DEDUPE_MS = 500;
|
|
22
|
-
var OutboundLinkTracker = class {
|
|
23
|
-
lastActivation = null;
|
|
24
|
-
getHref(event) {
|
|
25
|
-
if (!(event instanceof MouseEvent) || !isEligibleActivation(event)) return null;
|
|
26
|
-
const anchor = findOutboundAnchor(event);
|
|
27
|
-
if (!anchor || this.isDuplicate(event, anchor.href)) return null;
|
|
28
|
-
return anchor.href;
|
|
29
|
-
}
|
|
30
|
-
isDuplicate(event, href) {
|
|
31
|
-
const activation = {
|
|
32
|
-
button: event.button,
|
|
33
|
-
href,
|
|
34
|
-
clientX: event.clientX,
|
|
35
|
-
clientY: event.clientY,
|
|
36
|
-
at: Date.now()
|
|
37
|
-
};
|
|
38
|
-
const previous = this.lastActivation;
|
|
39
|
-
this.lastActivation = activation;
|
|
40
|
-
return !!(previous && previous.href === activation.href && previous.button === activation.button && previous.clientX === activation.clientX && previous.clientY === activation.clientY && activation.at - previous.at <= DEDUPE_MS);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
function isEligibleActivation(event) {
|
|
44
|
-
if (event.defaultPrevented) return false;
|
|
45
|
-
if (event.type === "click") return event.button === 0;
|
|
46
|
-
if (event.type === "auxclick") return event.button === 1;
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
function findOutboundAnchor(event) {
|
|
50
|
-
for (const node of event.composedPath()) if (isOutboundAnchor(node)) return node;
|
|
51
|
-
let node = event.target;
|
|
52
|
-
while (node) {
|
|
53
|
-
if (isOutboundAnchor(node)) return node;
|
|
54
|
-
node = node.parentNode;
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
function isOutboundAnchor(node) {
|
|
59
|
-
return node instanceof HTMLAnchorElement && (node.protocol === "http:" || node.protocol === "https:") && node.host !== location.host;
|
|
60
|
-
}
|
|
61
|
-
//#endregion
|
|
62
|
-
export { OutboundLinkTracker, outboundLinks };
|