@faststats/web 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/REPLAY_PAYLOAD.md +64 -0
- package/dist/chunks/api-urls-DaeYkG0_.js +1 -0
- package/dist/chunks/error-Cd9PTS5v.js +2 -0
- package/dist/chunks/{feature-flags-BxZz_lNm.d.ts → feature-flags-BClx56v5.d.ts} +1 -0
- package/dist/chunks/feature-flags-DSOCIZHK.js +1 -0
- package/dist/chunks/{replay-BuX3_0xs.d.ts → replay-DvJYurEC.d.ts} +10 -2
- package/dist/chunks/replay-rTqcjOo2.js +1 -0
- package/dist/chunks/send-data-B2fYGj6v.js +1 -0
- package/dist/chunks/session-manager-Cy63ptPF.js +1 -0
- package/dist/chunks/web-vitals-Be-Cg4Po.js +1 -0
- package/dist/error.d.ts +5 -8
- package/dist/error.js +1 -1
- package/dist/feature-flags.d.ts +1 -1
- package/dist/feature-flags.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/replay.d.ts +1 -1
- package/dist/replay.js +1 -1
- package/dist/web-vitals.d.ts +7 -15
- package/dist/web-vitals.js +1 -1
- package/package.json +4 -7
- package/src/analytics.ts +25 -14
- package/src/error.ts +110 -158
- package/src/feature-flags.ts +5 -2
- package/src/replay.ts +179 -52
- package/src/utils/api-urls.ts +7 -20
- package/src/utils/identifiers.ts +30 -72
- package/src/utils/session-manager.ts +416 -0
- package/src/web-vitals.ts +66 -168
- package/tests/analytics.test.ts +8 -4
- package/tests/identifiers.test.ts +15 -32
- package/tests/replay.test.ts +180 -10
- package/tests/session-manager.test.ts +161 -0
- package/dist/chunks/api-urls-BrkcoElX.js +0 -1
- package/dist/chunks/error-CttYL43D.js +0 -2
- package/dist/chunks/feature-flags-CjnLZGxp.js +0 -1
- package/dist/chunks/identifiers-CQeWm7wi.js +0 -1
- package/dist/chunks/replay-BrMLCiBF.js +0 -1
- package/dist/chunks/send-data-DL_GlsQw.js +0 -1
- package/dist/chunks/web-vitals-CjA1bFLG.js +0 -1
- package/wrangler.toml +0 -8
package/src/error.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { SDK_NAME, SDK_VERSION } from "./sdk";
|
|
2
|
-
import {
|
|
3
|
-
import { getAnonymousId
|
|
2
|
+
import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
|
|
3
|
+
import { getAnonymousId } from "./utils/identifiers";
|
|
4
4
|
import { sendData } from "./utils/send-data";
|
|
5
|
+
import {
|
|
6
|
+
getOrCreateSessionId,
|
|
7
|
+
getOrCreateWindowId,
|
|
8
|
+
} from "./utils/session-manager";
|
|
5
9
|
|
|
6
10
|
export type ErrorEntry = {
|
|
7
11
|
error: string;
|
|
@@ -16,18 +20,6 @@ export type ErrorTracking = ErrorEntry & {
|
|
|
16
20
|
count: number;
|
|
17
21
|
};
|
|
18
22
|
|
|
19
|
-
type ErrorKind = "error" | "unhandledrejection";
|
|
20
|
-
|
|
21
|
-
type NormalizedError = {
|
|
22
|
-
kind: ErrorKind;
|
|
23
|
-
message: string;
|
|
24
|
-
stack?: string;
|
|
25
|
-
filename?: string;
|
|
26
|
-
lineno?: number;
|
|
27
|
-
handled: boolean;
|
|
28
|
-
cause?: ErrorEntry;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
23
|
export interface ErrorTrackingOptions {
|
|
32
24
|
siteKey: string;
|
|
33
25
|
baseUrl?: string;
|
|
@@ -38,214 +30,176 @@ export interface ErrorTrackingOptions {
|
|
|
38
30
|
sdkVersion?: string;
|
|
39
31
|
}
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
type Kind = "error" | "unhandledrejection";
|
|
34
|
+
|
|
35
|
+
const EXTENSION_RE =
|
|
42
36
|
/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
|
|
43
37
|
|
|
44
38
|
const parseStack = (stack?: string): string[] | undefined =>
|
|
45
39
|
stack
|
|
46
40
|
?.split("\n")
|
|
47
|
-
.map((
|
|
41
|
+
.map((l) => l.trim())
|
|
48
42
|
.filter(Boolean);
|
|
49
43
|
|
|
50
|
-
function serializeCause(
|
|
51
|
-
if (
|
|
44
|
+
function serializeCause(value: unknown): ErrorEntry | undefined {
|
|
45
|
+
if (value instanceof Error) {
|
|
52
46
|
return {
|
|
53
|
-
error:
|
|
54
|
-
message:
|
|
55
|
-
stack: parseStack(
|
|
56
|
-
cause: serializeCause(
|
|
47
|
+
error: value.name?.trim() || "Error",
|
|
48
|
+
message: value.message,
|
|
49
|
+
stack: parseStack(value.stack),
|
|
50
|
+
cause: serializeCause(value.cause),
|
|
57
51
|
};
|
|
58
52
|
}
|
|
59
|
-
|
|
60
|
-
if (typeof cause === "string") {
|
|
61
|
-
return { error: "Error", message: cause };
|
|
62
|
-
}
|
|
53
|
+
if (typeof value === "string") return { error: "Error", message: value };
|
|
63
54
|
}
|
|
64
55
|
|
|
65
56
|
function fingerprintCause(cause?: ErrorEntry): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
let s = "";
|
|
58
|
+
for (let c = cause; c; c = c.cause) {
|
|
59
|
+
s += `${c.error}\0${c.message ?? ""}\0`;
|
|
60
|
+
}
|
|
61
|
+
return s;
|
|
69
62
|
}
|
|
70
63
|
|
|
71
|
-
function
|
|
72
|
-
const key = [
|
|
73
|
-
data.kind,
|
|
74
|
-
data.handled ? "handled" : "unhandled",
|
|
75
|
-
data.message,
|
|
76
|
-
data.filename ?? "",
|
|
77
|
-
data.lineno ?? "",
|
|
78
|
-
fingerprintCause(data.cause),
|
|
79
|
-
].join("\0");
|
|
80
|
-
|
|
64
|
+
function hashKey(key: string): string {
|
|
81
65
|
let a = 2166136261;
|
|
82
66
|
let b = 3598710387;
|
|
83
|
-
|
|
84
67
|
for (let i = 0; i < key.length; i++) {
|
|
85
68
|
const c = key.charCodeAt(i);
|
|
86
69
|
a = Math.imul(a ^ c, 16777619);
|
|
87
70
|
b = Math.imul(b ^ c, 2246822519);
|
|
88
71
|
}
|
|
72
|
+
return `err_${(a >>> 0).toString(16).padStart(8, "0")}${(b >>> 0).toString(16).padStart(8, "0")}`;
|
|
73
|
+
}
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
75
|
+
function readBuildId(): string | undefined {
|
|
76
|
+
const v = (globalThis as { __SOURCEMAPS_BUILD__?: { buildId?: unknown } })
|
|
77
|
+
.__SOURCEMAPS_BUILD__?.buildId;
|
|
78
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
93
79
|
}
|
|
94
80
|
|
|
95
81
|
export default class ErrorTracker {
|
|
96
82
|
private readonly endpoint: string;
|
|
97
|
-
private readonly
|
|
83
|
+
private readonly seen = new WeakSet<object>();
|
|
98
84
|
private readonly queue = new Map<string, ErrorTracking>();
|
|
99
85
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
100
86
|
private started = false;
|
|
101
87
|
private flushing = false;
|
|
102
88
|
|
|
103
89
|
constructor(private readonly options: ErrorTrackingOptions) {
|
|
104
|
-
this.endpoint =
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private get debug(): boolean {
|
|
108
|
-
return this.options.debug ?? false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private get flushInterval(): number {
|
|
112
|
-
return this.options.flushInterval ?? 5000;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private get maxQueueSize(): number {
|
|
116
|
-
return this.options.maxQueueSize ?? 50;
|
|
90
|
+
this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.events}`;
|
|
117
91
|
}
|
|
118
92
|
|
|
119
93
|
private log(...args: unknown[]): void {
|
|
120
|
-
if (this.debug) console.log("[ErrorTracker]", ...args);
|
|
94
|
+
if (this.options.debug) console.log("[ErrorTracker]", ...args);
|
|
121
95
|
}
|
|
122
96
|
|
|
123
97
|
start(): void {
|
|
124
98
|
if (this.started || typeof window === "undefined") return;
|
|
125
99
|
this.started = true;
|
|
126
|
-
|
|
127
100
|
window.addEventListener("error", this.onError);
|
|
128
101
|
window.addEventListener("unhandledrejection", this.onRejection);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
102
|
+
window.addEventListener("pagehide", this.scheduleFlush);
|
|
103
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
104
|
+
this.timer = setInterval(
|
|
105
|
+
this.scheduleFlush,
|
|
106
|
+
this.options.flushInterval ?? 5000,
|
|
107
|
+
);
|
|
108
|
+
this.log("Started");
|
|
134
109
|
}
|
|
135
110
|
|
|
136
111
|
stop(): void {
|
|
137
112
|
if (!this.started || typeof window === "undefined") return;
|
|
138
113
|
this.started = false;
|
|
139
|
-
|
|
140
114
|
window.removeEventListener("error", this.onError);
|
|
141
115
|
window.removeEventListener("unhandledrejection", this.onRejection);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
116
|
+
window.removeEventListener("pagehide", this.scheduleFlush);
|
|
117
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
145
118
|
if (this.timer) clearInterval(this.timer);
|
|
146
119
|
this.timer = null;
|
|
147
|
-
|
|
148
|
-
this.
|
|
149
|
-
this.log("Stopped listening for errors");
|
|
120
|
+
this.scheduleFlush();
|
|
121
|
+
this.log("Stopped");
|
|
150
122
|
}
|
|
151
123
|
|
|
152
124
|
captureError(error: Error): void {
|
|
153
|
-
this.
|
|
154
|
-
{
|
|
155
|
-
kind: "error",
|
|
156
|
-
message: error.message,
|
|
157
|
-
stack: error.stack,
|
|
158
|
-
handled: true,
|
|
159
|
-
cause: serializeCause(error.cause),
|
|
160
|
-
},
|
|
161
|
-
error,
|
|
162
|
-
);
|
|
125
|
+
this.capture("error", error.message, true, error);
|
|
163
126
|
}
|
|
164
127
|
|
|
165
128
|
private onError = (event: ErrorEvent): void => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
lineno: event.lineno || undefined,
|
|
174
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
175
|
-
handled: false,
|
|
176
|
-
cause: err instanceof Error ? serializeCause(err.cause) : undefined,
|
|
177
|
-
},
|
|
178
|
-
err,
|
|
129
|
+
this.capture(
|
|
130
|
+
"error",
|
|
131
|
+
event.message || "Unknown error",
|
|
132
|
+
false,
|
|
133
|
+
event.error,
|
|
134
|
+
event.filename || "",
|
|
135
|
+
event.lineno,
|
|
179
136
|
);
|
|
180
137
|
};
|
|
181
138
|
|
|
182
139
|
private onRejection = (event: PromiseRejectionEvent): void => {
|
|
183
140
|
const reason = event.reason;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
cause: serializeCause(reason.cause),
|
|
192
|
-
},
|
|
193
|
-
reason,
|
|
194
|
-
);
|
|
141
|
+
const message =
|
|
142
|
+
reason instanceof Error
|
|
143
|
+
? reason.message
|
|
144
|
+
: typeof reason === "string"
|
|
145
|
+
? reason
|
|
146
|
+
: "Unhandled promise rejection";
|
|
147
|
+
this.capture("unhandledrejection", message, false, reason);
|
|
195
148
|
};
|
|
196
149
|
|
|
197
|
-
private
|
|
198
|
-
if (document.visibilityState === "hidden") this.
|
|
150
|
+
private onVisibility = (): void => {
|
|
151
|
+
if (document.visibilityState === "hidden") this.scheduleFlush();
|
|
199
152
|
};
|
|
200
153
|
|
|
201
|
-
private
|
|
154
|
+
private scheduleFlush = (): void => {
|
|
202
155
|
void this.flush();
|
|
203
156
|
};
|
|
204
157
|
|
|
205
|
-
private
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
158
|
+
private capture(
|
|
159
|
+
kind: Kind,
|
|
160
|
+
message: string,
|
|
161
|
+
handled: boolean,
|
|
162
|
+
source: unknown,
|
|
163
|
+
filename = "",
|
|
164
|
+
lineno: number | "" = "",
|
|
165
|
+
): void {
|
|
166
|
+
const stackText = source instanceof Error ? (source.stack ?? "") : "";
|
|
167
|
+
if (EXTENSION_RE.test(`${filename}\n${stackText}`)) return;
|
|
168
|
+
|
|
169
|
+
if (typeof source === "object" && source !== null) {
|
|
170
|
+
if (this.seen.has(source)) {
|
|
171
|
+
this.log("Skipping duplicate:", message);
|
|
218
172
|
return;
|
|
219
173
|
}
|
|
220
|
-
this.
|
|
174
|
+
this.seen.add(source);
|
|
221
175
|
}
|
|
222
176
|
|
|
223
|
-
|
|
224
|
-
|
|
177
|
+
const entry: ErrorEntry = {
|
|
178
|
+
error: kind === "unhandledrejection" ? "UnhandledRejection" : "Error",
|
|
179
|
+
message,
|
|
180
|
+
handled,
|
|
181
|
+
};
|
|
182
|
+
if (source instanceof Error) {
|
|
183
|
+
entry.stack = parseStack(source.stack);
|
|
184
|
+
const cause = serializeCause(source.cause);
|
|
185
|
+
if (cause) entry.cause = cause;
|
|
225
186
|
}
|
|
226
187
|
|
|
227
|
-
const
|
|
228
|
-
|
|
188
|
+
const key = hashKey(
|
|
189
|
+
`${kind}\0${handled ? "handled" : "unhandled"}\0${message}\0${filename}\0${lineno}\0${fingerprintCause(entry.cause)}`,
|
|
190
|
+
);
|
|
229
191
|
|
|
192
|
+
const existing = this.queue.get(key);
|
|
230
193
|
if (existing) {
|
|
231
|
-
existing.count
|
|
194
|
+
existing.count++;
|
|
232
195
|
} else {
|
|
233
|
-
this.queue.set(hash,
|
|
234
|
-
hash,
|
|
235
|
-
count: 1,
|
|
236
|
-
error:
|
|
237
|
-
data.kind === "unhandledrejection" ? "UnhandledRejection" : "Error",
|
|
238
|
-
message: data.message,
|
|
239
|
-
stack: parseStack(data.stack),
|
|
240
|
-
handled: data.handled,
|
|
241
|
-
...(data.cause ? { cause: data.cause } : {}),
|
|
242
|
-
});
|
|
196
|
+
this.queue.set(key, { hash: key, count: 1, ...entry });
|
|
243
197
|
}
|
|
244
198
|
|
|
245
|
-
this.log("Captured
|
|
199
|
+
this.log("Captured:", entry);
|
|
246
200
|
|
|
247
|
-
if (this.queue.size >= this.maxQueueSize) {
|
|
248
|
-
this.
|
|
201
|
+
if (this.queue.size >= (this.options.maxQueueSize ?? 50)) {
|
|
202
|
+
this.scheduleFlush();
|
|
249
203
|
}
|
|
250
204
|
}
|
|
251
205
|
|
|
@@ -256,23 +210,14 @@ export default class ErrorTracker {
|
|
|
256
210
|
this.queue.clear();
|
|
257
211
|
this.flushing = true;
|
|
258
212
|
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
globalThis as {
|
|
262
|
-
__SOURCEMAPS_BUILD__?: { buildId?: unknown };
|
|
263
|
-
}
|
|
264
|
-
).__SOURCEMAPS_BUILD__;
|
|
265
|
-
|
|
266
|
-
const buildId =
|
|
267
|
-
typeof sourcemapsBuild?.buildId === "string" &&
|
|
268
|
-
sourcemapsBuild.buildId.trim()
|
|
269
|
-
? sourcemapsBuild.buildId
|
|
270
|
-
: undefined;
|
|
213
|
+
const userId = getAnonymousId();
|
|
214
|
+
const buildId = readBuildId();
|
|
271
215
|
|
|
272
216
|
const payload = JSON.stringify({
|
|
273
217
|
token: this.options.siteKey,
|
|
274
|
-
...(
|
|
218
|
+
...(userId ? { userId } : {}),
|
|
275
219
|
sessionId: getOrCreateSessionId(),
|
|
220
|
+
windowId: getOrCreateWindowId(this.options.siteKey),
|
|
276
221
|
...(buildId ? { buildId } : {}),
|
|
277
222
|
sdkName: this.options.sdkName ?? SDK_NAME,
|
|
278
223
|
sdkVersion: this.options.sdkVersion ?? SDK_VERSION,
|
|
@@ -285,21 +230,28 @@ export default class ErrorTracker {
|
|
|
285
230
|
errors,
|
|
286
231
|
});
|
|
287
232
|
|
|
288
|
-
this.log("Flushing
|
|
289
|
-
this.log("Payload:", payload);
|
|
233
|
+
this.log("Flushing:", errors);
|
|
290
234
|
|
|
291
235
|
try {
|
|
292
236
|
const sent = await sendData({
|
|
293
237
|
url: this.endpoint,
|
|
294
238
|
data: payload,
|
|
295
|
-
debug: this.debug,
|
|
239
|
+
debug: this.options.debug,
|
|
296
240
|
debugPrefix: "[ErrorTracker]",
|
|
297
241
|
});
|
|
298
|
-
if (!sent) this.
|
|
242
|
+
if (!sent) this.requeue(errors);
|
|
299
243
|
} catch {
|
|
300
|
-
this.
|
|
244
|
+
this.requeue(errors);
|
|
301
245
|
} finally {
|
|
302
246
|
this.flushing = false;
|
|
303
247
|
}
|
|
304
248
|
}
|
|
249
|
+
|
|
250
|
+
private requeue(errors: ErrorTracking[]): void {
|
|
251
|
+
for (const e of errors) {
|
|
252
|
+
const existing = this.queue.get(e.hash);
|
|
253
|
+
if (existing) existing.count += e.count;
|
|
254
|
+
else this.queue.set(e.hash, e);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
305
257
|
}
|
package/src/feature-flags.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeFeatureFlagsBaseUrl, URLS } from "./utils/api-urls";
|
|
2
2
|
|
|
3
3
|
export type FeatureFlagEvaluation = {
|
|
4
4
|
value: string;
|
|
@@ -12,6 +12,7 @@ export type FeatureFlagCheckContext = {
|
|
|
12
12
|
projectId?: string;
|
|
13
13
|
identifier?: string;
|
|
14
14
|
externalId?: string;
|
|
15
|
+
sessionId?: string;
|
|
15
16
|
attributes?: Record<string, unknown>;
|
|
16
17
|
signal?: AbortSignal;
|
|
17
18
|
};
|
|
@@ -28,6 +29,7 @@ export async function fetchFeatureFlagEvaluation(
|
|
|
28
29
|
const projectId = context.projectId;
|
|
29
30
|
const identifier = context.identifier;
|
|
30
31
|
const externalId = context.externalId;
|
|
32
|
+
const sessionId = context.sessionId;
|
|
31
33
|
|
|
32
34
|
if (!projectToken && !projectId) {
|
|
33
35
|
throw new Error("feature flag check requires projectToken or projectId");
|
|
@@ -41,6 +43,7 @@ export async function fetchFeatureFlagEvaluation(
|
|
|
41
43
|
...(projectId ? { projectId } : {}),
|
|
42
44
|
...(identifier ? { identifier } : {}),
|
|
43
45
|
...(externalId ? { externalId } : {}),
|
|
46
|
+
...(sessionId ? { sessionId } : {}),
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
if (context.attributes && Object.keys(context.attributes).length > 0) {
|
|
@@ -54,7 +57,7 @@ export async function fetchFeatureFlagEvaluation(
|
|
|
54
57
|
headers.Authorization = `Bearer ${projectToken}`;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
const url =
|
|
60
|
+
const url = `${normalizeFeatureFlagsBaseUrl(context.baseUrl)}${URLS.flags}`;
|
|
58
61
|
const res = await fetch(url, {
|
|
59
62
|
method: "POST",
|
|
60
63
|
headers,
|