@faststats/web 0.10.3 → 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-BgGtAQdp.js → identifiers-BwGNL11R.js} +1 -1
- package/dist/chunks/{send-data-DAVUdfNm.js → send-data-D520oab1.js} +61 -13
- 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 +2 -2
- package/dist/replay.js +43 -61
- 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,17 +210,14 @@ 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
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
222
|
if (debug) console.log(debugPrefix, ok ? "Sent via fetch" : status);
|
|
175
223
|
return ok || !!settleRejected && status < 500 && status !== 429;
|
|
@@ -179,4 +227,4 @@ async function sendData(options) {
|
|
|
179
227
|
}
|
|
180
228
|
}
|
|
181
229
|
//#endregion
|
|
182
|
-
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-BgGtAQdp.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-BgGtAQdp.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";
|
|
@@ -96,6 +96,7 @@ export default class ReplayTracker {
|
|
|
96
96
|
constructor(options: ReplayTrackerOptions);
|
|
97
97
|
private log;
|
|
98
98
|
private clearFlushTimers;
|
|
99
|
+
private dropEvents;
|
|
99
100
|
private clearQueues;
|
|
100
101
|
private finalizeSession;
|
|
101
102
|
setCookielessMode(cookieless: boolean): void;
|
|
@@ -112,7 +113,6 @@ export default class ReplayTracker {
|
|
|
112
113
|
private beginReplayStream;
|
|
113
114
|
private resetSessionTiming;
|
|
114
115
|
private onEvent;
|
|
115
|
-
private onUnload;
|
|
116
116
|
private requestTerminalFlush;
|
|
117
117
|
private hasReachedMinLength;
|
|
118
118
|
private requestFlush;
|
package/dist/replay.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-BgGtAQdp.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,50 +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 };
|
|
65
70
|
}
|
|
66
71
|
const sequenceKey = (siteKey) => `faststats_replay_sequence_v1_${siteKey}`;
|
|
67
72
|
/** Whether this recording already sent batches, e.g. from an earlier page load. */
|
|
68
73
|
function hasReplaySequence(siteKey, context, cookieless) {
|
|
69
|
-
|
|
70
|
-
return [counters.get(siteKey), raw === null ? void 0 : readCounter(raw)].some((counter) => sameRecording(counter, context) && counter.next > 0);
|
|
74
|
+
return [counters.get(siteKey), readCounter(siteKey, cookieless).counter].some((counter) => sameRecording(counter, context) && counter.next > 0);
|
|
71
75
|
}
|
|
76
|
+
/** Next batch sequence for this recording, or undefined when it cannot be trusted. */
|
|
72
77
|
function nextReplaySequence(siteKey, context, cookieless) {
|
|
73
|
-
const key = sequenceKey(siteKey);
|
|
74
78
|
const cached = counters.get(siteKey);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const next = legacy === null ? 0 : Number(legacy);
|
|
84
|
-
if (!Number.isSafeInteger(next) || next < 0 || legacy !== null && !/^\d+$/.test(legacy)) return;
|
|
85
|
-
counter = {
|
|
86
|
-
sessionId: context.sessionId,
|
|
87
|
-
windowId: context.windowId,
|
|
88
|
-
next,
|
|
89
|
-
version: legacy === null ? 1 : 0
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
if (!Number.isSafeInteger(counter.next + 1)) return;
|
|
93
|
-
const sequence = counter.next;
|
|
94
|
-
const updated = {
|
|
95
|
-
...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,
|
|
96
87
|
next: sequence + 1
|
|
97
88
|
};
|
|
98
|
-
counters.set(siteKey,
|
|
99
|
-
if (!cookieless
|
|
100
|
-
return
|
|
101
|
-
sequence,
|
|
102
|
-
...counter.version === 1 ? { sequenceContractVersion: 1 } : {}
|
|
103
|
-
};
|
|
89
|
+
counters.set(siteKey, counter);
|
|
90
|
+
if (!cookieless) setStorageItem("sessionStorage", sequenceKey(siteKey), JSON.stringify(counter));
|
|
91
|
+
return sequence;
|
|
104
92
|
}
|
|
105
93
|
//#endregion
|
|
106
94
|
//#region src/replay.ts
|
|
@@ -183,7 +171,7 @@ var ReplayTracker = class {
|
|
|
183
171
|
constructor(options) {
|
|
184
172
|
this.options = options;
|
|
185
173
|
this.lastCookielessMode = options.cookieless ?? false;
|
|
186
|
-
this.endpoint = `${resolveBaseUrl(options.baseUrl
|
|
174
|
+
this.endpoint = `${resolveBaseUrl(options.baseUrl)}${URLS.replay}`;
|
|
187
175
|
this.flushInterval = boundedInteger(options.flushInterval, 15e3, 1, MAX_TIMEOUT_MS);
|
|
188
176
|
this.maxEvents = boundedInteger(options.maxEvents, 1e3, 1, 1e5);
|
|
189
177
|
this.maxBatchSizeBytes = boundedInteger(options.maxBatchSizeBytes, DEFAULT_MAX_BATCH_SIZE_BYTES, 1, 33554432);
|
|
@@ -205,10 +193,13 @@ var ReplayTracker = class {
|
|
|
205
193
|
this.retryAttempt = 0;
|
|
206
194
|
this.minLengthFlushTask = null;
|
|
207
195
|
}
|
|
208
|
-
|
|
209
|
-
this.needsFinalization = false;
|
|
196
|
+
dropEvents() {
|
|
210
197
|
this.events.length = 0;
|
|
211
198
|
this.queuedEventsSizeBytes = 0;
|
|
199
|
+
}
|
|
200
|
+
clearQueues() {
|
|
201
|
+
this.needsFinalization = false;
|
|
202
|
+
this.dropEvents();
|
|
212
203
|
this.pending.length = 0;
|
|
213
204
|
this.pendingSizeBytes = 0;
|
|
214
205
|
}
|
|
@@ -243,7 +234,6 @@ var ReplayTracker = class {
|
|
|
243
234
|
this.subscribeToSessionRotation();
|
|
244
235
|
this.beginRecording();
|
|
245
236
|
this.intervalId = setInterval(() => this.requestFlush("interval"), this.flushInterval);
|
|
246
|
-
window.addEventListener("beforeunload", this.onUnload);
|
|
247
237
|
this.log("Recording started");
|
|
248
238
|
}
|
|
249
239
|
trackPageChange(url) {
|
|
@@ -293,7 +283,7 @@ var ReplayTracker = class {
|
|
|
293
283
|
}));
|
|
294
284
|
if (this.options.recordNetwork === true) {
|
|
295
285
|
const options = this.options.networkOptions;
|
|
296
|
-
const base = resolveBaseUrl(this.options.baseUrl
|
|
286
|
+
const base = resolveBaseUrl(this.options.baseUrl);
|
|
297
287
|
const endpoints = new Set(Object.values(URLS).map((path) => {
|
|
298
288
|
const url = new URL(`${base}${path}`, window.location.href);
|
|
299
289
|
return `${url.origin}${url.pathname}`;
|
|
@@ -356,15 +346,13 @@ var ReplayTracker = class {
|
|
|
356
346
|
if (this.intervalId) clearInterval(this.intervalId);
|
|
357
347
|
this.clearFlushTimers();
|
|
358
348
|
this.intervalId = null;
|
|
359
|
-
window.removeEventListener("beforeunload", this.onUnload);
|
|
360
349
|
if (discard) {
|
|
361
350
|
this.clearQueues();
|
|
362
351
|
this.log("Recording discarded");
|
|
363
352
|
return;
|
|
364
353
|
}
|
|
365
354
|
if (!this.hasReachedMinLength()) {
|
|
366
|
-
this.
|
|
367
|
-
this.queuedEventsSizeBytes = 0;
|
|
355
|
+
this.dropEvents();
|
|
368
356
|
this.needsFinalization = false;
|
|
369
357
|
this.log(`Recording too short (${Date.now() - this.startTime}ms), discarding events`);
|
|
370
358
|
}
|
|
@@ -383,10 +371,7 @@ var ReplayTracker = class {
|
|
|
383
371
|
applySessionRotation(prev, next) {
|
|
384
372
|
if (this.activeSessionId === next.sessionId) return;
|
|
385
373
|
if (this.hasReachedMinLength()) this.enqueueEventsIfReady(prev, "sessionRotate");
|
|
386
|
-
else
|
|
387
|
-
this.events.length = 0;
|
|
388
|
-
this.queuedEventsSizeBytes = 0;
|
|
389
|
-
}
|
|
374
|
+
else this.dropEvents();
|
|
390
375
|
this.finalizeSession(prev, "sessionRotate");
|
|
391
376
|
this.needsFinalization = false;
|
|
392
377
|
this.activeSessionId = next.sessionId;
|
|
@@ -457,13 +442,9 @@ var ReplayTracker = class {
|
|
|
457
442
|
if (reason) this.requestFlush(reason);
|
|
458
443
|
else this.scheduleMinLengthFlush();
|
|
459
444
|
};
|
|
460
|
-
onUnload = () => {
|
|
461
|
-
this.flush(true, false, "unload");
|
|
462
|
-
};
|
|
463
445
|
requestTerminalFlush(reason) {
|
|
464
446
|
if (!this.hasReachedMinLength()) {
|
|
465
|
-
this.
|
|
466
|
-
this.queuedEventsSizeBytes = 0;
|
|
447
|
+
this.dropEvents();
|
|
467
448
|
this.needsFinalization = false;
|
|
468
449
|
return;
|
|
469
450
|
}
|
|
@@ -472,7 +453,7 @@ var ReplayTracker = class {
|
|
|
472
453
|
hasReachedMinLength() {
|
|
473
454
|
return this.resumed || this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
|
|
474
455
|
}
|
|
475
|
-
requestFlush
|
|
456
|
+
requestFlush(reason) {
|
|
476
457
|
if (this.flushTask || this.events.length === 0) return;
|
|
477
458
|
if (this.minLengthFlushTask) {
|
|
478
459
|
clearTimeout(this.minLengthFlushTask);
|
|
@@ -482,7 +463,7 @@ var ReplayTracker = class {
|
|
|
482
463
|
this.flushTask = null;
|
|
483
464
|
this.flush(false, false, reason);
|
|
484
465
|
}, 0);
|
|
485
|
-
}
|
|
466
|
+
}
|
|
486
467
|
scheduleMinLengthFlush() {
|
|
487
468
|
if (!this.initialFlushPending || this.events.length === 0 || this.minReplayLengthMs <= 0) return;
|
|
488
469
|
if (this.hasReachedMinLength()) {
|
|
@@ -498,7 +479,7 @@ var ReplayTracker = class {
|
|
|
498
479
|
this.requestFlush("minLength");
|
|
499
480
|
}, Math.min(remainingMs, MAX_TIMEOUT_MS));
|
|
500
481
|
}
|
|
501
|
-
enqueueEventsIfReady(context, flushReason
|
|
482
|
+
enqueueEventsIfReady(context, flushReason, maxBatchSizeBytes = this.maxBatchSizeBytes) {
|
|
502
483
|
if (this.events.length === 0) return;
|
|
503
484
|
if (!this.hasReachedMinLength()) {
|
|
504
485
|
this.scheduleMinLengthFlush();
|
|
@@ -532,10 +513,10 @@ var ReplayTracker = class {
|
|
|
532
513
|
if (current.length > 0) batches.push(current);
|
|
533
514
|
return batches;
|
|
534
515
|
}
|
|
535
|
-
createBatch(events, context, isFinal, flushReason
|
|
516
|
+
createBatch(events, context, isFinal, flushReason) {
|
|
536
517
|
const identifier = getAnonymousId(this.options.siteKey, this.lastCookielessMode);
|
|
537
518
|
const sequence = nextReplaySequence(this.options.siteKey, context, this.lastCookielessMode);
|
|
538
|
-
if (
|
|
519
|
+
if (sequence === void 0) {
|
|
539
520
|
this.stopCapture("Replay sequence state is invalid or exhausted");
|
|
540
521
|
return;
|
|
541
522
|
}
|
|
@@ -551,8 +532,9 @@ var ReplayTracker = class {
|
|
|
551
532
|
chunkStartedAt,
|
|
552
533
|
chunkEndedAt: now,
|
|
553
534
|
...identifier ? { identifier } : {},
|
|
554
|
-
batchId: `${context.sessionId}-${sequence
|
|
555
|
-
|
|
535
|
+
batchId: `${context.sessionId}-${sequence}-${chunkStartedAt}`,
|
|
536
|
+
sequence,
|
|
537
|
+
sequenceContractVersion: 1,
|
|
556
538
|
timestamp: now,
|
|
557
539
|
url: this.viewUrl || sanitizeUrl(window.location.href, this.options.trackHash ?? false),
|
|
558
540
|
...isFinal ? { isFinal: true } : {},
|
|
@@ -647,7 +629,7 @@ var ReplayTracker = class {
|
|
|
647
629
|
}
|
|
648
630
|
async sendBatch(batch, lowLatency) {
|
|
649
631
|
const json = JSON.stringify(batch);
|
|
650
|
-
const lowLatencySafe = lowLatency &&
|
|
632
|
+
const lowLatencySafe = lowLatency && this.batchSizeBytes(batch) <= DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES;
|
|
651
633
|
const sendOptions = {
|
|
652
634
|
debug: this.options.debug,
|
|
653
635
|
debugPrefix: "[Replay]",
|
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-DAVUdfNm.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 };
|