@faststats/web 0.6.0 → 0.8.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 +67 -0
- package/dist/chunks/api-urls-DWk43fu6.js +53 -0
- package/dist/chunks/extensions-DW7tJY0T.d.ts +51 -0
- package/dist/chunks/identifiers-KgQDX74Q.js +21 -0
- package/dist/chunks/send-data-DFZnsaL2.js +219 -0
- package/dist/error.d.ts +9 -4
- package/dist/error.js +89 -59
- package/dist/index.d.ts +54 -72
- package/dist/index.js +349 -353
- package/dist/outbound-links.d.ts +10 -0
- package/dist/outbound-links.js +62 -0
- package/dist/replay.d.ts +128 -2
- package/dist/replay.js +154 -62
- package/dist/web-vitals.d.ts +10 -5
- package/dist/web-vitals.js +56 -50
- package/package.json +11 -12
- package/dist/chunks/api-urls-CaNa8upY.js +0 -45
- package/dist/chunks/identifiers-cUymVK5H.js +0 -20
- package/dist/chunks/replay-DgodPHzA.d.ts +0 -104
- package/dist/chunks/send-data-DjdbGDDc.js +0 -196
- package/dist/feature-flags.d.ts +0 -18
- package/dist/feature-flags.js +0 -32
package/README.md
CHANGED
|
@@ -2,3 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
> [!IMPORTANT]
|
|
4
4
|
> This repository is in early development and not intended for production use.
|
|
5
|
+
|
|
6
|
+
The base SDK only handles analytics, identity, sessions, navigation, and consent.
|
|
7
|
+
Error tracking, Web Vitals, and session replay are independent extensions, so
|
|
8
|
+
applications only download the products they use.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { init, track } from "@faststats/web";
|
|
12
|
+
import { errorTracking } from "@faststats/web/error";
|
|
13
|
+
import { outboundLinks } from "@faststats/web/outbound-links";
|
|
14
|
+
import { sessionReplay } from "@faststats/web/replay";
|
|
15
|
+
import { webVitals } from "@faststats/web/web-vitals";
|
|
16
|
+
|
|
17
|
+
init({
|
|
18
|
+
siteKey: "site_123",
|
|
19
|
+
extensions: [
|
|
20
|
+
outboundLinks(),
|
|
21
|
+
errorTracking(),
|
|
22
|
+
webVitals({ attribution: true }),
|
|
23
|
+
sessionReplay({ recordConsole: true }),
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
track("checkout_completed", { total: 42 });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`init()` starts one shared client and is idempotent, making it safe to call from
|
|
31
|
+
application bootstrap code. Calls made before initialization are ignored rather
|
|
32
|
+
than buffered with stale context. Use `shutdown()` before initializing with new
|
|
33
|
+
options.
|
|
34
|
+
|
|
35
|
+
For multiple sites, tests, or framework integrations, create an independent
|
|
36
|
+
client with explicit lifecycle control:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { createClient } from "@faststats/web";
|
|
40
|
+
|
|
41
|
+
const analytics = createClient({ siteKey: "site_123" });
|
|
42
|
+
analytics.start();
|
|
43
|
+
analytics.track("checkout_completed", { total: 42 });
|
|
44
|
+
analytics.destroy();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Consent remains client-local: use `"anonymous"` for cookieless collection,
|
|
48
|
+
`"granted"` for normal collection, or `"denied"` to keep the client dormant.
|
|
49
|
+
|
|
50
|
+
### Custom extensions
|
|
51
|
+
|
|
52
|
+
An extension registers only the lifecycle hooks it needs. Hook failures are
|
|
53
|
+
isolated from analytics collection, asynchronous setup is safe during teardown,
|
|
54
|
+
and cleanup receives `discard: true` when consent is revoked.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import type { AnalyticsExtension } from "@faststats/web";
|
|
58
|
+
|
|
59
|
+
export function diagnostics(): AnalyticsExtension {
|
|
60
|
+
return {
|
|
61
|
+
name: "diagnostics",
|
|
62
|
+
setup(context) {
|
|
63
|
+
const off = context.on("pageChange", ({ url }) => {
|
|
64
|
+
console.debug("page changed", url, context.getSession());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return () => off();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
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 };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/utils/session-manager.d.ts
|
|
2
|
+
type SessionContext = {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
windowId: string;
|
|
5
|
+
sessionStart: number;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/extensions.d.ts
|
|
9
|
+
type ExtensionHooks = {
|
|
10
|
+
pageChange: {
|
|
11
|
+
url: string;
|
|
12
|
+
};
|
|
13
|
+
pageHide: {
|
|
14
|
+
persisted: boolean;
|
|
15
|
+
terminal: boolean;
|
|
16
|
+
};
|
|
17
|
+
pageShow: {
|
|
18
|
+
persisted: boolean;
|
|
19
|
+
};
|
|
20
|
+
consentChange: {
|
|
21
|
+
cookieless: boolean;
|
|
22
|
+
};
|
|
23
|
+
error: {
|
|
24
|
+
error: Error;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
type ExtensionHook = keyof ExtensionHooks;
|
|
28
|
+
type ExtensionHandler<K extends ExtensionHook> = (event: ExtensionHooks[K]) => void | Promise<void>;
|
|
29
|
+
interface AnalyticsExtensionContext {
|
|
30
|
+
readonly siteKey: string;
|
|
31
|
+
readonly baseUrl: string;
|
|
32
|
+
readonly debug: boolean;
|
|
33
|
+
readonly trackHash: boolean;
|
|
34
|
+
readonly sdkName?: string;
|
|
35
|
+
readonly sdkVersion?: string;
|
|
36
|
+
isCookieless(): boolean;
|
|
37
|
+
getAnonymousId(): string;
|
|
38
|
+
getSession(): SessionContext;
|
|
39
|
+
capture(event: string, properties?: Record<string, unknown>): void;
|
|
40
|
+
on<K extends ExtensionHook>(hook: K, handler: ExtensionHandler<K>): () => void;
|
|
41
|
+
log(message: string, ...details: unknown[]): void;
|
|
42
|
+
}
|
|
43
|
+
interface AnalyticsExtension {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
setup(context: AnalyticsExtensionContext): ExtensionCleanup | undefined | Promise<ExtensionCleanup | undefined>;
|
|
46
|
+
}
|
|
47
|
+
type ExtensionCleanup = (options: {
|
|
48
|
+
discard: boolean;
|
|
49
|
+
}) => void;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { ExtensionHook as a, ExtensionHandler as i, AnalyticsExtensionContext as n, ExtensionHooks as o, ExtensionCleanup as r, AnalyticsExtension as t };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { c as removeStorageItem, l as setStorageItem, n as createId, s as getStorageItem } from "./send-data-DFZnsaL2.js";
|
|
2
|
+
//#region src/utils/identifiers.ts
|
|
3
|
+
const ANONYMOUS_ID_KEY = "faststats_anon_id_";
|
|
4
|
+
function getOrCreateAnonymousId(siteKey) {
|
|
5
|
+
const key = `${ANONYMOUS_ID_KEY}${siteKey}`;
|
|
6
|
+
const existingId = getStorageItem("localStorage", key);
|
|
7
|
+
if (existingId) return existingId;
|
|
8
|
+
const newId = createId();
|
|
9
|
+
return setStorageItem("localStorage", key, newId) ? newId : "";
|
|
10
|
+
}
|
|
11
|
+
function getAnonymousId(siteKey, cookieless = false) {
|
|
12
|
+
if (cookieless) return "";
|
|
13
|
+
return getOrCreateAnonymousId(siteKey);
|
|
14
|
+
}
|
|
15
|
+
function resetAnonymousId(siteKey, cookieless) {
|
|
16
|
+
if (cookieless) return "";
|
|
17
|
+
if (!removeStorageItem("localStorage", `${ANONYMOUS_ID_KEY}${siteKey}`)) return "";
|
|
18
|
+
return getOrCreateAnonymousId(siteKey);
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { resetAnonymousId as n, getAnonymousId as t };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
//#region src/utils/storage.ts
|
|
2
|
+
function getStorageItem(name, key) {
|
|
3
|
+
try {
|
|
4
|
+
return globalThis[name]?.getItem(key) ?? null;
|
|
5
|
+
} catch {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function setStorageItem(name, key, value) {
|
|
10
|
+
try {
|
|
11
|
+
const storage = globalThis[name];
|
|
12
|
+
if (!storage) return false;
|
|
13
|
+
storage.setItem(key, value);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function removeStorageItem(name, key) {
|
|
20
|
+
try {
|
|
21
|
+
const storage = globalThis[name];
|
|
22
|
+
if (!storage) return false;
|
|
23
|
+
storage.removeItem(key);
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/utils/session-manager.ts
|
|
31
|
+
const IDLE_MS = 18e5;
|
|
32
|
+
const MAX_AGE_MS = 864e5;
|
|
33
|
+
const SESSION_KEY = "faststats_session_";
|
|
34
|
+
const NO_STORAGE = {};
|
|
35
|
+
const memorySessions = /* @__PURE__ */ new Map();
|
|
36
|
+
const rotationListeners = /* @__PURE__ */ new Map();
|
|
37
|
+
function memoryKey(siteKey, cookieless) {
|
|
38
|
+
return `${siteKey}:${cookieless ? "memory" : "persistent"}`;
|
|
39
|
+
}
|
|
40
|
+
function storageScope() {
|
|
41
|
+
try {
|
|
42
|
+
return globalThis.localStorage ?? NO_STORAGE;
|
|
43
|
+
} catch {
|
|
44
|
+
return NO_STORAGE;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function readMemory(siteKey, cookieless) {
|
|
48
|
+
const entry = memorySessions.get(memoryKey(siteKey, cookieless));
|
|
49
|
+
return entry?.scope === storageScope() ? entry.session : null;
|
|
50
|
+
}
|
|
51
|
+
function writeMemory(siteKey, cookieless, session) {
|
|
52
|
+
memorySessions.set(memoryKey(siteKey, cookieless), {
|
|
53
|
+
scope: storageScope(),
|
|
54
|
+
session
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function createId() {
|
|
58
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
|
|
59
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
60
|
+
}
|
|
61
|
+
function readSession(siteKey) {
|
|
62
|
+
const memory = readMemory(siteKey, false);
|
|
63
|
+
const raw = getStorageItem("localStorage", `${SESSION_KEY}${siteKey}`);
|
|
64
|
+
if (!raw) return memory;
|
|
65
|
+
try {
|
|
66
|
+
const session = JSON.parse(raw);
|
|
67
|
+
return session.id ? session : memory;
|
|
68
|
+
} catch {}
|
|
69
|
+
return memory;
|
|
70
|
+
}
|
|
71
|
+
function writeSession(siteKey, session) {
|
|
72
|
+
if (!setStorageItem("localStorage", `${SESSION_KEY}${siteKey}`, JSON.stringify(session))) writeMemory(siteKey, false, session);
|
|
73
|
+
}
|
|
74
|
+
function clearSession(siteKey, cookieless) {
|
|
75
|
+
memorySessions.delete(memoryKey(siteKey, cookieless));
|
|
76
|
+
if (!cookieless) removeStorageItem("localStorage", `${SESSION_KEY}${siteKey}`);
|
|
77
|
+
}
|
|
78
|
+
function expired(session, now) {
|
|
79
|
+
return now - session.activity >= IDLE_MS || now - session.start >= MAX_AGE_MS;
|
|
80
|
+
}
|
|
81
|
+
function emitRotation(siteKey, cookieless, prev, next) {
|
|
82
|
+
for (const listener of rotationListeners.get(memoryKey(siteKey, cookieless)) ?? []) listener(prev, next);
|
|
83
|
+
}
|
|
84
|
+
function resolveSession(siteKey, cookieless, touch) {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
if (cookieless) {
|
|
87
|
+
let session = readMemory(siteKey, true) ?? void 0;
|
|
88
|
+
if (!session) {
|
|
89
|
+
session = {
|
|
90
|
+
id: createId(),
|
|
91
|
+
start: now,
|
|
92
|
+
activity: now
|
|
93
|
+
};
|
|
94
|
+
writeMemory(siteKey, true, session);
|
|
95
|
+
} else if (touch) session.activity = now;
|
|
96
|
+
return {
|
|
97
|
+
session,
|
|
98
|
+
rotatedFrom: null
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const existing = readSession(siteKey);
|
|
102
|
+
if (existing && !expired(existing, now)) {
|
|
103
|
+
if (touch) {
|
|
104
|
+
existing.activity = now;
|
|
105
|
+
writeSession(siteKey, existing);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
session: existing,
|
|
109
|
+
rotatedFrom: null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const session = {
|
|
113
|
+
id: createId(),
|
|
114
|
+
start: now,
|
|
115
|
+
activity: now
|
|
116
|
+
};
|
|
117
|
+
writeSession(siteKey, session);
|
|
118
|
+
return {
|
|
119
|
+
session,
|
|
120
|
+
rotatedFrom: existing
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function windowId(siteKey, cookieless, sessionId) {
|
|
124
|
+
if (cookieless) return sessionId;
|
|
125
|
+
const key = `faststats_window_id_${siteKey}`;
|
|
126
|
+
const existing = getStorageItem("sessionStorage", key);
|
|
127
|
+
if (existing) return existing;
|
|
128
|
+
const id = createId();
|
|
129
|
+
return setStorageItem("sessionStorage", key, id) ? id : sessionId;
|
|
130
|
+
}
|
|
131
|
+
function toContext(siteKey, session, cookieless, windowIdOverride) {
|
|
132
|
+
return {
|
|
133
|
+
sessionId: session.id,
|
|
134
|
+
windowId: windowIdOverride ?? windowId(siteKey, cookieless, session.id),
|
|
135
|
+
sessionStart: session.start
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function getSessionContext(siteKey, cookieless = false) {
|
|
139
|
+
const { session, rotatedFrom } = resolveSession(siteKey, cookieless, false);
|
|
140
|
+
const next = toContext(siteKey, session, cookieless);
|
|
141
|
+
if (rotatedFrom) emitRotation(siteKey, cookieless, toContext(siteKey, rotatedFrom, cookieless, next.windowId), next);
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
function touchActivity(siteKey, cookieless = false) {
|
|
145
|
+
const { session, rotatedFrom } = resolveSession(siteKey, cookieless, true);
|
|
146
|
+
if (!rotatedFrom) return null;
|
|
147
|
+
const wid = windowId(siteKey, cookieless, session.id);
|
|
148
|
+
const rotation = {
|
|
149
|
+
prev: toContext(siteKey, rotatedFrom, cookieless, wid),
|
|
150
|
+
next: toContext(siteKey, session, cookieless, wid)
|
|
151
|
+
};
|
|
152
|
+
emitRotation(siteKey, cookieless, rotation.prev, rotation.next);
|
|
153
|
+
return rotation;
|
|
154
|
+
}
|
|
155
|
+
function resetSession(siteKey, cookieless = false) {
|
|
156
|
+
const previous = getSessionContext(siteKey, cookieless);
|
|
157
|
+
clearSession(siteKey, cookieless);
|
|
158
|
+
if (cookieless) {
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
writeMemory(siteKey, true, {
|
|
161
|
+
id: createId(),
|
|
162
|
+
start: now,
|
|
163
|
+
activity: now
|
|
164
|
+
});
|
|
165
|
+
} else removeStorageItem("sessionStorage", `faststats_window_id_${siteKey}`);
|
|
166
|
+
const next = getSessionContext(siteKey, cookieless);
|
|
167
|
+
if (previous.sessionId !== next.sessionId) emitRotation(siteKey, cookieless, previous, next);
|
|
168
|
+
return next;
|
|
169
|
+
}
|
|
170
|
+
function onSessionRotated(siteKey, cookieless, listener) {
|
|
171
|
+
const key = memoryKey(siteKey, cookieless);
|
|
172
|
+
let listeners = rotationListeners.get(key);
|
|
173
|
+
if (!listeners) {
|
|
174
|
+
listeners = /* @__PURE__ */ new Set();
|
|
175
|
+
rotationListeners.set(key, listeners);
|
|
176
|
+
}
|
|
177
|
+
listeners.add(listener);
|
|
178
|
+
return () => {
|
|
179
|
+
listeners?.delete(listener);
|
|
180
|
+
if (listeners?.size === 0) rotationListeners.delete(key);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/utils/send-data.ts
|
|
185
|
+
async function sendData(options) {
|
|
186
|
+
const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]", useBeacon = true, keepalive = true, signal } = options;
|
|
187
|
+
if (signal?.aborted) return false;
|
|
188
|
+
if (useBeacon && typeof globalThis.navigator?.sendBeacon === "function") try {
|
|
189
|
+
const beaconBody = data instanceof Blob || typeof Blob === "undefined" ? data : new Blob([data], { type: contentType });
|
|
190
|
+
if (globalThis.navigator.sendBeacon(url, beaconBody)) {
|
|
191
|
+
if (debug) console.log(`${debugPrefix} Sent via beacon`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(url, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
body: data,
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": contentType,
|
|
201
|
+
...headers
|
|
202
|
+
},
|
|
203
|
+
keepalive,
|
|
204
|
+
credentials: "omit",
|
|
205
|
+
signal
|
|
206
|
+
});
|
|
207
|
+
const success = response.ok;
|
|
208
|
+
if (debug) {
|
|
209
|
+
const message = success ? `${debugPrefix} Sent via fetch` : `${debugPrefix} Failed: ${response.status}`;
|
|
210
|
+
console[success ? "log" : "warn"](message);
|
|
211
|
+
}
|
|
212
|
+
return success;
|
|
213
|
+
} catch {
|
|
214
|
+
if (debug) console.warn(`${debugPrefix} Failed to send`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
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 };
|
package/dist/error.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
|
|
1
2
|
//#region src/error.d.ts
|
|
2
3
|
type ErrorEntry = {
|
|
3
4
|
error: string;
|
|
@@ -9,11 +10,15 @@ interface ErrorTrackingOptions {
|
|
|
9
10
|
siteKey: string;
|
|
10
11
|
baseUrl?: string;
|
|
11
12
|
debug?: boolean;
|
|
13
|
+
trackHash?: boolean;
|
|
14
|
+
cookieless?: boolean;
|
|
12
15
|
flushInterval?: number;
|
|
13
16
|
maxQueueSize?: number;
|
|
14
17
|
sdkName?: string;
|
|
15
18
|
sdkVersion?: string;
|
|
16
19
|
}
|
|
20
|
+
type ErrorTrackingExtensionOptions = Omit<ErrorTrackingOptions, "siteKey" | "baseUrl" | "debug" | "trackHash" | "cookieless" | "sdkName" | "sdkVersion">;
|
|
21
|
+
declare function errorTracking(options?: ErrorTrackingExtensionOptions): AnalyticsExtension;
|
|
17
22
|
declare class ErrorTracker {
|
|
18
23
|
private readonly options;
|
|
19
24
|
private readonly endpoint;
|
|
@@ -21,17 +26,17 @@ declare class ErrorTracker {
|
|
|
21
26
|
private timer;
|
|
22
27
|
private started;
|
|
23
28
|
private flushing;
|
|
29
|
+
private cookieless;
|
|
24
30
|
constructor(options: ErrorTrackingOptions);
|
|
25
|
-
|
|
31
|
+
setCookielessMode(cookieless: boolean): void;
|
|
26
32
|
start(): void;
|
|
27
|
-
stop(): void;
|
|
33
|
+
stop(discard?: boolean): void;
|
|
28
34
|
onPageHidden(persisted?: boolean): void;
|
|
29
35
|
captureError(error: Error): void;
|
|
30
36
|
private onError;
|
|
31
37
|
private onRejection;
|
|
32
|
-
private addToQueue;
|
|
33
38
|
private enqueue;
|
|
34
39
|
private flush;
|
|
35
40
|
}
|
|
36
41
|
//#endregion
|
|
37
|
-
export { ErrorEntry, ErrorTrackingOptions, ErrorTracker as default };
|
|
42
|
+
export { ErrorEntry, ErrorTrackingExtensionOptions, ErrorTrackingOptions, ErrorTracker as default, errorTracking };
|
package/dist/error.js
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as getSessionContext, t as sendData } from "./chunks/send-data-
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-
|
|
1
|
+
import { i as resolveBaseUrl, n as URLS, r as getPageContext, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
|
|
2
|
+
import { r as getSessionContext, t as sendData } from "./chunks/send-data-DFZnsaL2.js";
|
|
3
|
+
import { t as getAnonymousId } from "./chunks/identifiers-KgQDX74Q.js";
|
|
4
4
|
//#region src/error.ts
|
|
5
5
|
const SDK_NAME = "@faststats/web";
|
|
6
|
-
const SDK_VERSION = "0.
|
|
7
|
-
|
|
6
|
+
const SDK_VERSION = "0.8.0";
|
|
7
|
+
function errorTracking(options = {}) {
|
|
8
|
+
return {
|
|
9
|
+
name: "error-tracking",
|
|
10
|
+
setup(context) {
|
|
11
|
+
const tracker = new ErrorTracker({
|
|
12
|
+
...options,
|
|
13
|
+
siteKey: context.siteKey,
|
|
14
|
+
baseUrl: context.baseUrl,
|
|
15
|
+
debug: context.debug,
|
|
16
|
+
trackHash: context.trackHash,
|
|
17
|
+
cookieless: context.isCookieless(),
|
|
18
|
+
sdkName: context.sdkName,
|
|
19
|
+
sdkVersion: context.sdkVersion
|
|
20
|
+
});
|
|
21
|
+
context.on("pageHide", ({ persisted }) => tracker.onPageHidden(persisted));
|
|
22
|
+
context.on("error", ({ error }) => tracker.captureError(error));
|
|
23
|
+
context.on("consentChange", ({ cookieless }) => tracker.setCookielessMode(cookieless));
|
|
24
|
+
tracker.start();
|
|
25
|
+
return ({ discard }) => tracker.stop(discard);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const EXTENSION_URL = /(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
|
|
8
30
|
function toEntry(kind, source, handled, fallback) {
|
|
9
31
|
if (source instanceof Error) return {
|
|
10
|
-
error: kind,
|
|
32
|
+
error: kind === "UnhandledRejection" ? kind : source.name.trim() || kind,
|
|
11
33
|
message: source.message || fallback,
|
|
12
34
|
handled,
|
|
13
35
|
stack: source.stack?.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
@@ -18,26 +40,26 @@ function toEntry(kind, source, handled, fallback) {
|
|
|
18
40
|
handled
|
|
19
41
|
};
|
|
20
42
|
}
|
|
21
|
-
function entryKey({ error, message, handled }) {
|
|
22
|
-
return `${error}\0${message ?? ""}\0${handled}`;
|
|
23
|
-
}
|
|
24
43
|
function isExtensionError(filename, source) {
|
|
25
44
|
const stack = source instanceof Error ? source.stack ?? "" : "";
|
|
26
|
-
return
|
|
45
|
+
return EXTENSION_URL.test(filename) || EXTENSION_URL.test(stack);
|
|
27
46
|
}
|
|
28
47
|
var ErrorTracker = class {
|
|
29
48
|
options;
|
|
30
49
|
endpoint;
|
|
31
|
-
queue =
|
|
50
|
+
queue = [];
|
|
32
51
|
timer = null;
|
|
33
52
|
started = false;
|
|
34
53
|
flushing = false;
|
|
54
|
+
cookieless;
|
|
35
55
|
constructor(options) {
|
|
36
56
|
this.options = options;
|
|
37
57
|
this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.events}`;
|
|
58
|
+
this.cookieless = options.cookieless ?? false;
|
|
38
59
|
}
|
|
39
|
-
|
|
40
|
-
if (this.
|
|
60
|
+
setCookielessMode(cookieless) {
|
|
61
|
+
if (this.cookieless !== cookieless) this.queue = [];
|
|
62
|
+
this.cookieless = cookieless;
|
|
41
63
|
}
|
|
42
64
|
start() {
|
|
43
65
|
if (this.started || typeof window === "undefined") return;
|
|
@@ -45,21 +67,19 @@ var ErrorTracker = class {
|
|
|
45
67
|
window.addEventListener("error", this.onError);
|
|
46
68
|
window.addEventListener("unhandledrejection", this.onRejection);
|
|
47
69
|
this.timer = setInterval(() => void this.flush(), this.options.flushInterval ?? 5e3);
|
|
48
|
-
this.log("Started");
|
|
49
70
|
}
|
|
50
|
-
stop() {
|
|
71
|
+
stop(discard = false) {
|
|
51
72
|
if (!this.started || typeof window === "undefined") return;
|
|
52
73
|
this.started = false;
|
|
53
74
|
window.removeEventListener("error", this.onError);
|
|
54
75
|
window.removeEventListener("unhandledrejection", this.onRejection);
|
|
55
76
|
if (this.timer) clearInterval(this.timer);
|
|
56
77
|
this.timer = null;
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
78
|
+
if (discard) this.queue = [];
|
|
79
|
+
else this.flush();
|
|
59
80
|
}
|
|
60
81
|
onPageHidden(persisted = false) {
|
|
61
|
-
if (persisted)
|
|
62
|
-
this.flush();
|
|
82
|
+
if (!persisted) this.flush();
|
|
63
83
|
}
|
|
64
84
|
captureError(error) {
|
|
65
85
|
this.enqueue(toEntry("Error", error, true, error.message));
|
|
@@ -72,56 +92,66 @@ var ErrorTracker = class {
|
|
|
72
92
|
if (isExtensionError("", event.reason)) return;
|
|
73
93
|
this.enqueue(toEntry("UnhandledRejection", event.reason, false, "Unhandled promise rejection"));
|
|
74
94
|
};
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
enqueue(error) {
|
|
96
|
+
const buildId = globalThis.__SOURCEMAPS_BUILD__?.buildId;
|
|
97
|
+
const userId = getAnonymousId(this.options.siteKey, this.cookieless);
|
|
98
|
+
const ctx = getSessionContext(this.options.siteKey, this.cookieless);
|
|
99
|
+
this.queue.push({
|
|
100
|
+
error,
|
|
101
|
+
context: {
|
|
102
|
+
token: this.options.siteKey,
|
|
103
|
+
...userId ? { userId } : {},
|
|
104
|
+
sessionId: ctx.sessionId,
|
|
105
|
+
windowId: ctx.windowId,
|
|
106
|
+
...typeof buildId === "string" && buildId.trim() ? { buildId } : {},
|
|
107
|
+
sdkName: this.options.sdkName ?? SDK_NAME,
|
|
108
|
+
sdkVersion: this.options.sdkVersion ?? SDK_VERSION,
|
|
109
|
+
event: "error",
|
|
110
|
+
...getPageContext(this.options.trackHash ?? false),
|
|
111
|
+
properties: {}
|
|
112
|
+
}
|
|
85
113
|
});
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
enqueue(entry) {
|
|
89
|
-
if (this.addToQueue(entry)) this.log("Captured:", entry);
|
|
90
|
-
if (this.queue.size >= (this.options.maxQueueSize ?? 50)) this.flush();
|
|
114
|
+
if (this.queue.length >= (this.options.maxQueueSize ?? 50)) this.flush();
|
|
91
115
|
}
|
|
92
116
|
async flush() {
|
|
93
|
-
if (this.flushing || this.queue.
|
|
117
|
+
if (this.flushing || !this.queue.length) return;
|
|
94
118
|
this.flushing = true;
|
|
95
119
|
const batch = this.queue;
|
|
96
|
-
this.queue =
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
this.queue = [];
|
|
121
|
+
const groups = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const item of batch) {
|
|
123
|
+
const key = JSON.stringify(item.context);
|
|
124
|
+
const group = groups.get(key) ?? {
|
|
125
|
+
context: item.context,
|
|
126
|
+
items: []
|
|
127
|
+
};
|
|
128
|
+
group.items.push(item);
|
|
129
|
+
groups.set(key, group);
|
|
130
|
+
}
|
|
131
|
+
const failed = await Promise.all([...groups.values()].map(async (group) => {
|
|
132
|
+
const errors = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const { error } of group.items) {
|
|
134
|
+
const key = JSON.stringify(error);
|
|
135
|
+
const existing = errors.get(key);
|
|
136
|
+
if (existing) existing.count++;
|
|
137
|
+
else errors.set(key, {
|
|
138
|
+
...error,
|
|
139
|
+
count: 1
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return await sendData({
|
|
104
143
|
url: this.endpoint,
|
|
105
144
|
data: JSON.stringify({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
sessionId: ctx.sessionId,
|
|
109
|
-
windowId: ctx.windowId,
|
|
110
|
-
...typeof buildId === "string" && buildId.trim() ? { buildId } : {},
|
|
111
|
-
sdkName: this.options.sdkName ?? SDK_NAME,
|
|
112
|
-
sdkVersion: this.options.sdkVersion ?? SDK_VERSION,
|
|
113
|
-
event: "error",
|
|
114
|
-
...getPageContext(),
|
|
115
|
-
properties: {},
|
|
116
|
-
errors
|
|
145
|
+
...group.context,
|
|
146
|
+
errors: [...errors.values()]
|
|
117
147
|
}),
|
|
118
148
|
debug: this.options.debug,
|
|
119
149
|
debugPrefix: "[ErrorTracker]"
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
}) ? [] : group.items;
|
|
151
|
+
}));
|
|
152
|
+
this.queue.unshift(...failed.flat());
|
|
153
|
+
this.flushing = false;
|
|
124
154
|
}
|
|
125
155
|
};
|
|
126
156
|
//#endregion
|
|
127
|
-
export { ErrorTracker as default };
|
|
157
|
+
export { ErrorTracker as default, errorTracking };
|