@faststats/nitro 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{report-error-ldlQ3TdC.d.mts → options-D8H8Cnje.d.mts} +1 -1
- package/dist/plugin-hook-BMw88N35.mjs +318 -0
- package/dist/v2.d.mts +1 -1
- package/dist/v2.mjs +4 -12
- package/dist/v3.d.mts +1 -1
- package/dist/v3.mjs +4 -12
- package/package.json +1 -1
- package/dist/report-error-BGjRwWHB.mjs +0 -347
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { version } from "node:os";
|
|
4
|
+
//#region src/options.ts
|
|
5
|
+
const DEFAULT_ENDPOINT = "https://metrics.faststats.dev/v1/error";
|
|
6
|
+
const envInt = (key, fallback) => {
|
|
7
|
+
const value = Number.parseInt(process?.env?.[key] ?? "", 10);
|
|
8
|
+
return value > 0 ? value : fallback;
|
|
9
|
+
};
|
|
10
|
+
function resolveFaststatsNitroOptions(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
errorEndpoint: overrides.errorEndpoint ?? process?.env?.FASTSTATS_ERROR_ENDPOINT ?? DEFAULT_ENDPOINT,
|
|
13
|
+
debug: overrides.debug ?? process?.env?.FASTSTATS_DEBUG === "1",
|
|
14
|
+
token: overrides.token ?? process?.env?.FASTSTATS_TOKEN ?? process?.env?.FASTSTATS_API_TOKEN,
|
|
15
|
+
buildId: overrides.buildId ?? process?.env?.FASTSTATS_BUILD_ID,
|
|
16
|
+
sessionId: overrides.sessionId ?? process?.env?.FASTSTATS_SESSION_ID,
|
|
17
|
+
getContext: overrides.getContext,
|
|
18
|
+
pluginId: overrides.pluginId,
|
|
19
|
+
flushIntervalMs: overrides.flushIntervalMs ?? envInt("FASTSTATS_ERROR_FLUSH_MS", 5e3),
|
|
20
|
+
maxBatchHashes: overrides.maxBatchHashes ?? envInt("FASTSTATS_ERROR_MAX_HASHES", 50),
|
|
21
|
+
circuitBreakerFailures: overrides.circuitBreakerFailures ?? envInt("FASTSTATS_ERROR_CIRCUIT_FAILS", 5),
|
|
22
|
+
circuitBreakerPauseMs: overrides.circuitBreakerPauseMs ?? envInt("FASTSTATS_ERROR_CIRCUIT_PAUSE_MS", 6e4)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const SDK_NAME = "@faststats/nitro";
|
|
26
|
+
const SDK_VERSION = "0.5.0";
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/context.ts
|
|
29
|
+
function readPackageVersion(name) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(createRequire(import.meta.url).resolve(`${name}/package.json`), "utf8");
|
|
32
|
+
const pkg = JSON.parse(raw);
|
|
33
|
+
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function detectRuntime() {
|
|
39
|
+
if (typeof Bun !== "undefined" && Bun.version !== void 0) return {
|
|
40
|
+
runtime: "bun",
|
|
41
|
+
runtimeVersion: Bun.version
|
|
42
|
+
};
|
|
43
|
+
const deno = globalThis.Deno;
|
|
44
|
+
if (deno?.version !== void 0) {
|
|
45
|
+
const version = deno.version;
|
|
46
|
+
return {
|
|
47
|
+
runtime: "deno",
|
|
48
|
+
runtimeVersion: typeof version === "string" ? version : version.deno
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const node = globalThis.process?.versions?.node;
|
|
52
|
+
if (typeof node === "string") return {
|
|
53
|
+
runtime: "node",
|
|
54
|
+
runtimeVersion: node
|
|
55
|
+
};
|
|
56
|
+
const edgeRt = globalThis.EdgeRuntime;
|
|
57
|
+
if (edgeRt !== void 0) return {
|
|
58
|
+
runtime: "edge",
|
|
59
|
+
runtimeVersion: edgeRt
|
|
60
|
+
};
|
|
61
|
+
if ((globalThis.navigator?.userAgent ?? "").includes("Cloudflare-Workers")) return { runtime: "workerd" };
|
|
62
|
+
return { runtime: "unknown" };
|
|
63
|
+
}
|
|
64
|
+
function buildServerReportContext(extra = {}) {
|
|
65
|
+
const { runtime, runtimeVersion } = detectRuntime();
|
|
66
|
+
const process = globalThis.process;
|
|
67
|
+
return {
|
|
68
|
+
source: "@faststats/nitro",
|
|
69
|
+
...extra,
|
|
70
|
+
nitroVersion: readPackageVersion("nitropack") ?? readPackageVersion("nitro"),
|
|
71
|
+
nitroPreset: process?.env?.NITRO_PRESET,
|
|
72
|
+
runtime,
|
|
73
|
+
runtimeVersion,
|
|
74
|
+
os: process?.release?.name ?? "unknown",
|
|
75
|
+
osVersion: version() || "unknown",
|
|
76
|
+
platform: process?.platform ?? "unknown",
|
|
77
|
+
arch: process?.arch ?? "unknown",
|
|
78
|
+
...process?.cwd?.() ? { cwd: process.cwd() } : {}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/hash-error.ts
|
|
83
|
+
const EXTENSION_URL = /(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
|
|
84
|
+
function parseStack(stack) {
|
|
85
|
+
if (!stack) return;
|
|
86
|
+
return stack.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !EXTENSION_URL.test(line));
|
|
87
|
+
}
|
|
88
|
+
function serializeCause(cause) {
|
|
89
|
+
if (cause instanceof Error) return {
|
|
90
|
+
error: cause.name?.trim() || "Error",
|
|
91
|
+
message: cause.message,
|
|
92
|
+
stack: parseStack(cause.stack),
|
|
93
|
+
cause: serializeCause(cause.cause)
|
|
94
|
+
};
|
|
95
|
+
if (typeof cause === "string") return {
|
|
96
|
+
error: "Error",
|
|
97
|
+
message: cause
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function causeFingerprint(cause) {
|
|
101
|
+
if (!cause) return "";
|
|
102
|
+
return `${cause.error}\0${cause.message ?? ""}\0${causeFingerprint(cause.cause)}`;
|
|
103
|
+
}
|
|
104
|
+
function hashMessage(message, cause) {
|
|
105
|
+
const key = [
|
|
106
|
+
"error",
|
|
107
|
+
message,
|
|
108
|
+
"",
|
|
109
|
+
"",
|
|
110
|
+
causeFingerprint(cause)
|
|
111
|
+
].join("\0");
|
|
112
|
+
let a = 2166136261;
|
|
113
|
+
let b = 3598710387;
|
|
114
|
+
for (let i = 0; i < key.length; i++) {
|
|
115
|
+
const c = key.charCodeAt(i);
|
|
116
|
+
a = Math.imul(a ^ c, 16777619);
|
|
117
|
+
b = Math.imul(b ^ c, 2246822519);
|
|
118
|
+
}
|
|
119
|
+
return `err_${(a >>> 0).toString(16).padStart(8, "0")}${(b >>> 0).toString(16).padStart(8, "0")}`;
|
|
120
|
+
}
|
|
121
|
+
function hashServerError(error) {
|
|
122
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
const name = err.name?.trim() || "Error";
|
|
124
|
+
const cause = serializeCause(err.cause);
|
|
125
|
+
return {
|
|
126
|
+
hash: hashMessage(err.message, cause),
|
|
127
|
+
entry: {
|
|
128
|
+
error: name,
|
|
129
|
+
message: err.message,
|
|
130
|
+
stack: parseStack(err.stack),
|
|
131
|
+
...cause ? { cause } : {}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/error-reporter.ts
|
|
137
|
+
const defaultRuntime = {
|
|
138
|
+
fetch: (input, init) => fetch(input, init),
|
|
139
|
+
setTimeout: (callback, delay) => setTimeout(callback, delay),
|
|
140
|
+
clearTimeout: (timer) => clearTimeout(timer),
|
|
141
|
+
now: () => Date.now(),
|
|
142
|
+
randomUUID: () => crypto.randomUUID?.() ?? `sess-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
143
|
+
warn: (...args) => console.warn(...args)
|
|
144
|
+
};
|
|
145
|
+
function definedStrings(context) {
|
|
146
|
+
return Object.fromEntries(Object.entries(context).filter(([, value]) => value !== void 0 && value !== ""));
|
|
147
|
+
}
|
|
148
|
+
function mergeBuckets(target, incoming) {
|
|
149
|
+
for (const [hash, bucket] of incoming) {
|
|
150
|
+
const existing = target.get(hash);
|
|
151
|
+
if (!existing) {
|
|
152
|
+
target.set(hash, { ...bucket });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
existing.count += bucket.count;
|
|
156
|
+
if (bucket.path !== void 0) existing.path = bucket.path;
|
|
157
|
+
if (bucket.tags.length > 0) existing.tags = bucket.tags;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function buildPayload(buckets, options, sessionId) {
|
|
161
|
+
const errors = [...buckets.entries()].map(([hash, bucket]) => ({
|
|
162
|
+
...bucket.entry,
|
|
163
|
+
hash,
|
|
164
|
+
count: bucket.count
|
|
165
|
+
}));
|
|
166
|
+
const tags = [...new Set([...buckets.values()].flatMap((b) => b.tags))];
|
|
167
|
+
let route;
|
|
168
|
+
let routeCount = -1;
|
|
169
|
+
const custom = {};
|
|
170
|
+
for (const bucket of buckets.values()) {
|
|
171
|
+
if (bucket.path !== void 0 && bucket.count > routeCount) {
|
|
172
|
+
routeCount = bucket.count;
|
|
173
|
+
route = bucket.path;
|
|
174
|
+
}
|
|
175
|
+
for (const [key, value] of Object.entries(bucket.custom)) if (!(key in custom)) custom[key] = value;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
errors,
|
|
179
|
+
sessionId,
|
|
180
|
+
...options.buildId ? { buildId: options.buildId } : {},
|
|
181
|
+
sdkName: SDK_NAME,
|
|
182
|
+
sdkVersion: SDK_VERSION,
|
|
183
|
+
context: {
|
|
184
|
+
...definedStrings(buildServerReportContext({
|
|
185
|
+
route,
|
|
186
|
+
plugin: options.pluginId,
|
|
187
|
+
tags: tags.length > 0 ? tags.join(",") : void 0
|
|
188
|
+
})),
|
|
189
|
+
...custom
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function createNitroErrorReporter(options, runtime) {
|
|
194
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
195
|
+
let timer = null;
|
|
196
|
+
let flushing = false;
|
|
197
|
+
let consecutiveFailures = 0;
|
|
198
|
+
let pausedUntil = 0;
|
|
199
|
+
let sessionId = options.sessionId;
|
|
200
|
+
const getSessionId = () => {
|
|
201
|
+
sessionId ??= runtime.randomUUID();
|
|
202
|
+
return sessionId;
|
|
203
|
+
};
|
|
204
|
+
const clearTimer = () => {
|
|
205
|
+
if (timer !== null) {
|
|
206
|
+
runtime.clearTimeout(timer);
|
|
207
|
+
timer = null;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const scheduleFlush = () => {
|
|
211
|
+
if (timer !== null) return;
|
|
212
|
+
timer = runtime.setTimeout(() => {
|
|
213
|
+
timer = null;
|
|
214
|
+
flush();
|
|
215
|
+
}, options.flushIntervalMs);
|
|
216
|
+
};
|
|
217
|
+
const send = async (snapshot) => {
|
|
218
|
+
try {
|
|
219
|
+
const response = await runtime.fetch(options.errorEndpoint, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
Authorization: `Bearer ${options.token}`
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify(buildPayload(snapshot, options, getSessionId()))
|
|
226
|
+
});
|
|
227
|
+
if (response.ok) return true;
|
|
228
|
+
if (options.debug) runtime.warn("[faststats nitro] report failed", response.status, await response.text());
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (options.debug) runtime.warn("[faststats nitro] report error", error);
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
};
|
|
234
|
+
const flush = async () => {
|
|
235
|
+
clearTimer();
|
|
236
|
+
if (runtime.now() < pausedUntil) {
|
|
237
|
+
scheduleFlush();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (buckets.size === 0 || flushing) {
|
|
241
|
+
if (buckets.size > 0) scheduleFlush();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
flushing = true;
|
|
245
|
+
const snapshot = new Map(buckets);
|
|
246
|
+
buckets.clear();
|
|
247
|
+
const ok = await send(snapshot);
|
|
248
|
+
flushing = false;
|
|
249
|
+
if (ok) consecutiveFailures = 0;
|
|
250
|
+
else {
|
|
251
|
+
consecutiveFailures += 1;
|
|
252
|
+
mergeBuckets(buckets, snapshot);
|
|
253
|
+
if (consecutiveFailures >= options.circuitBreakerFailures) {
|
|
254
|
+
pausedUntil = runtime.now() + options.circuitBreakerPauseMs;
|
|
255
|
+
consecutiveFailures = 0;
|
|
256
|
+
if (options.debug) runtime.warn("[faststats nitro] circuit open; pausing error reports for", options.circuitBreakerPauseMs, "ms");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (buckets.size > 0) scheduleFlush();
|
|
260
|
+
};
|
|
261
|
+
return { async report(error, args) {
|
|
262
|
+
if (!options.token) {
|
|
263
|
+
if (options.debug) runtime.warn("[faststats nitro] FASTSTATS_TOKEN not set; skipping error report");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (runtime.now() < pausedUntil) return;
|
|
267
|
+
const { hash, entry } = hashServerError(error);
|
|
268
|
+
const existing = buckets.get(hash);
|
|
269
|
+
if (existing) {
|
|
270
|
+
existing.count += 1;
|
|
271
|
+
if (args.path !== void 0) existing.path = args.path;
|
|
272
|
+
if (args.tags.length > 0) existing.tags = args.tags;
|
|
273
|
+
} else {
|
|
274
|
+
let custom = {};
|
|
275
|
+
if (options.getContext) try {
|
|
276
|
+
custom = await options.getContext({
|
|
277
|
+
error,
|
|
278
|
+
...args
|
|
279
|
+
}) ?? {};
|
|
280
|
+
} catch {
|
|
281
|
+
custom = {};
|
|
282
|
+
}
|
|
283
|
+
buckets.set(hash, {
|
|
284
|
+
entry,
|
|
285
|
+
count: 1,
|
|
286
|
+
path: args.path,
|
|
287
|
+
tags: args.tags,
|
|
288
|
+
custom
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (buckets.size >= options.maxBatchHashes) {
|
|
292
|
+
clearTimer();
|
|
293
|
+
await flush();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
scheduleFlush();
|
|
297
|
+
} };
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/plugin-hook.ts
|
|
301
|
+
function extractPath(event) {
|
|
302
|
+
if (!event || typeof event !== "object") return;
|
|
303
|
+
const e = event;
|
|
304
|
+
if (e.path) return e.path;
|
|
305
|
+
const url = e.node?.req?.url;
|
|
306
|
+
return url !== void 0 ? String(url) : void 0;
|
|
307
|
+
}
|
|
308
|
+
function createNitroErrorHook(resolved) {
|
|
309
|
+
const reporter = createNitroErrorReporter(resolved, defaultRuntime);
|
|
310
|
+
return (error, context) => {
|
|
311
|
+
reporter.report(error, {
|
|
312
|
+
path: extractPath(context.event),
|
|
313
|
+
tags: Array.isArray(context.tags) ? context.tags : []
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
export { resolveFaststatsNitroOptions as n, createNitroErrorHook as t };
|
package/dist/v2.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as FaststatsNitroOptions } from "./
|
|
1
|
+
import { t as FaststatsNitroOptions } from "./options-D8H8Cnje.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/v2.d.ts
|
|
4
4
|
declare function createFaststatsNitroPluginV2(options?: FaststatsNitroOptions): import("nitropack").NitroAppPlugin;
|
package/dist/v2.mjs
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
import { n as resolveFaststatsNitroOptions, t as
|
|
1
|
+
import { n as resolveFaststatsNitroOptions, t as createNitroErrorHook } from "./plugin-hook-BMw88N35.mjs";
|
|
2
2
|
import { defineNitroPlugin } from "nitropack/runtime";
|
|
3
3
|
//#region src/v2.ts
|
|
4
4
|
function createFaststatsNitroPluginV2(options) {
|
|
5
|
-
const
|
|
5
|
+
const onError = createNitroErrorHook(resolveFaststatsNitroOptions({
|
|
6
6
|
...options,
|
|
7
7
|
pluginId: options?.pluginId ?? "nitro-v2"
|
|
8
|
-
});
|
|
8
|
+
}));
|
|
9
9
|
return defineNitroPlugin((nitroApp) => {
|
|
10
|
-
nitroApp.hooks.hook("error",
|
|
11
|
-
const event = context.event;
|
|
12
|
-
const tags = Array.isArray(context.tags) ? context.tags : [];
|
|
13
|
-
reportNitroErrorToFaststats(error, {
|
|
14
|
-
path: event?.path ?? (event?.node?.req?.url !== void 0 ? String(event.node.req.url) : void 0),
|
|
15
|
-
tags,
|
|
16
|
-
options: resolved
|
|
17
|
-
});
|
|
18
|
-
});
|
|
10
|
+
nitroApp.hooks.hook("error", onError);
|
|
19
11
|
});
|
|
20
12
|
}
|
|
21
13
|
var v2_default = createFaststatsNitroPluginV2();
|
package/dist/v3.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as FaststatsNitroOptions } from "./
|
|
1
|
+
import { t as FaststatsNitroOptions } from "./options-D8H8Cnje.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/v3.d.ts
|
|
4
4
|
declare function createFaststatsNitroPluginV3(options?: FaststatsNitroOptions): import("nitro/types").NitroAppPlugin;
|
package/dist/v3.mjs
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
import { n as resolveFaststatsNitroOptions, t as
|
|
1
|
+
import { n as resolveFaststatsNitroOptions, t as createNitroErrorHook } from "./plugin-hook-BMw88N35.mjs";
|
|
2
2
|
import { definePlugin } from "nitro";
|
|
3
3
|
//#region src/v3.ts
|
|
4
4
|
function createFaststatsNitroPluginV3(options) {
|
|
5
|
-
const
|
|
5
|
+
const onError = createNitroErrorHook(resolveFaststatsNitroOptions({
|
|
6
6
|
...options,
|
|
7
7
|
pluginId: options?.pluginId ?? "nitro-v3"
|
|
8
|
-
});
|
|
8
|
+
}));
|
|
9
9
|
return definePlugin((nitroApp) => {
|
|
10
|
-
nitroApp.hooks.hook("error",
|
|
11
|
-
const event = ctx.event;
|
|
12
|
-
const tags = Array.isArray(ctx.tags) ? ctx.tags : [];
|
|
13
|
-
reportNitroErrorToFaststats(error, {
|
|
14
|
-
path: event?.path ?? (event?.node?.req?.url !== void 0 ? String(event.node.req.url) : void 0),
|
|
15
|
-
tags,
|
|
16
|
-
options: resolved
|
|
17
|
-
});
|
|
18
|
-
});
|
|
10
|
+
nitroApp.hooks.hook("error", onError);
|
|
19
11
|
});
|
|
20
12
|
}
|
|
21
13
|
var v3_default = createFaststatsNitroPluginV3();
|
package/package.json
CHANGED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { version } from "node:os";
|
|
4
|
-
//#region src/hash-error.ts
|
|
5
|
-
const EXTENSION_URL = /(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
|
|
6
|
-
function parseStack(stack) {
|
|
7
|
-
if (!stack) return void 0;
|
|
8
|
-
return stack.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !EXTENSION_URL.test(line));
|
|
9
|
-
}
|
|
10
|
-
function serializeCause(cause) {
|
|
11
|
-
if (cause instanceof Error) return {
|
|
12
|
-
error: cause.name?.trim() || "Error",
|
|
13
|
-
message: cause.message,
|
|
14
|
-
stack: parseStack(cause.stack),
|
|
15
|
-
cause: serializeCause(cause.cause)
|
|
16
|
-
};
|
|
17
|
-
if (typeof cause === "string") return {
|
|
18
|
-
error: "Error",
|
|
19
|
-
message: cause
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function causeFingerprint(cause) {
|
|
23
|
-
if (!cause) return "";
|
|
24
|
-
return `${cause.error}\0${cause.message ?? ""}\0${causeFingerprint(cause.cause)}`;
|
|
25
|
-
}
|
|
26
|
-
function hashErrorData(data) {
|
|
27
|
-
const key = [
|
|
28
|
-
data.type,
|
|
29
|
-
data.message,
|
|
30
|
-
data.filename ?? "",
|
|
31
|
-
data.lineno ?? "",
|
|
32
|
-
causeFingerprint(data.cause)
|
|
33
|
-
].join("\0");
|
|
34
|
-
let a = 2166136261;
|
|
35
|
-
let b = 3598710387;
|
|
36
|
-
for (let i = 0; i < key.length; i++) {
|
|
37
|
-
const c = key.charCodeAt(i);
|
|
38
|
-
a = Math.imul(a ^ c, 16777619);
|
|
39
|
-
b = Math.imul(b ^ c, 2246822519);
|
|
40
|
-
}
|
|
41
|
-
return `err_${(a >>> 0).toString(16).padStart(8, "0")}${(b >>> 0).toString(16).padStart(8, "0")}`;
|
|
42
|
-
}
|
|
43
|
-
function hashServerError(error) {
|
|
44
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
45
|
-
const hash = hashErrorData({
|
|
46
|
-
type: "error",
|
|
47
|
-
message: err.message,
|
|
48
|
-
stack: err.stack,
|
|
49
|
-
cause: serializeCause(err.cause)
|
|
50
|
-
});
|
|
51
|
-
const name = err.name?.trim() || "Error";
|
|
52
|
-
const cause = serializeCause(err.cause);
|
|
53
|
-
return {
|
|
54
|
-
hash,
|
|
55
|
-
entry: {
|
|
56
|
-
error: name,
|
|
57
|
-
message: err.message,
|
|
58
|
-
stack: parseStack(err.stack),
|
|
59
|
-
...cause ? { cause } : {}
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
//#endregion
|
|
64
|
-
//#region src/runtime-context.ts
|
|
65
|
-
function readPackageVersion(pkg) {
|
|
66
|
-
try {
|
|
67
|
-
const raw = readFileSync(createRequire(import.meta.url).resolve(`${pkg}/package.json`), "utf8");
|
|
68
|
-
const j = JSON.parse(raw);
|
|
69
|
-
return typeof j.version === "string" ? j.version : void 0;
|
|
70
|
-
} catch {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function getNitroPackageVersion() {
|
|
75
|
-
return readPackageVersion("nitropack") ?? readPackageVersion("nitro");
|
|
76
|
-
}
|
|
77
|
-
function getRuntimeInfo() {
|
|
78
|
-
if (Bun.version !== void 0) return {
|
|
79
|
-
runtime: "bun",
|
|
80
|
-
runtimeVersion: Bun.version
|
|
81
|
-
};
|
|
82
|
-
if (Deno.version !== void 0) return {
|
|
83
|
-
runtime: "deno",
|
|
84
|
-
runtimeVersion: Deno.version
|
|
85
|
-
};
|
|
86
|
-
if (typeof process !== "undefined" && process.versions && typeof process.versions.node === "string") return {
|
|
87
|
-
runtime: "node",
|
|
88
|
-
runtimeVersion: process.versions.node
|
|
89
|
-
};
|
|
90
|
-
const edgeRt = EdgeRuntime;
|
|
91
|
-
if (edgeRt !== void 0) return {
|
|
92
|
-
runtime: "edge",
|
|
93
|
-
runtimeVersion: edgeRt
|
|
94
|
-
};
|
|
95
|
-
if (typeof globalThis !== "undefined" && "navigator" in globalThis && String(globalThis.navigator?.userAgent ?? "").includes("Cloudflare-Workers")) return { runtime: "workerd" };
|
|
96
|
-
return { runtime: "unknown" };
|
|
97
|
-
}
|
|
98
|
-
function getOsSlice() {
|
|
99
|
-
return {
|
|
100
|
-
os: globalThis.process?.release?.name || "unknown",
|
|
101
|
-
osVersion: version() || "unknown",
|
|
102
|
-
platform: globalThis.process?.platform || "unknown",
|
|
103
|
-
arch: globalThis.process?.arch || "unknown"
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
function buildServerReportContext(extra = {}) {
|
|
107
|
-
const { runtime, runtimeVersion } = getRuntimeInfo();
|
|
108
|
-
const osSlice = getOsSlice();
|
|
109
|
-
const nitroVersion = getNitroPackageVersion();
|
|
110
|
-
const nitroPreset = globalThis.process.env.NITRO_PRESET;
|
|
111
|
-
const cwd = globalThis.process?.cwd();
|
|
112
|
-
return {
|
|
113
|
-
source: "@faststats/nitro",
|
|
114
|
-
...extra,
|
|
115
|
-
nitroVersion,
|
|
116
|
-
nitroPreset,
|
|
117
|
-
runtime,
|
|
118
|
-
runtimeVersion,
|
|
119
|
-
...osSlice,
|
|
120
|
-
...cwd ? { cwd } : {}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
//#endregion
|
|
124
|
-
//#region src/sdk.ts
|
|
125
|
-
const SDK_NAME = "@faststats/nitro";
|
|
126
|
-
const SDK_VERSION = "0.4.2";
|
|
127
|
-
//#endregion
|
|
128
|
-
//#region src/error-reporter.ts
|
|
129
|
-
function pruneUndefined(context) {
|
|
130
|
-
const result = {};
|
|
131
|
-
for (const [key, value] of Object.entries(context)) if (value !== void 0 && value !== "") result[key] = value;
|
|
132
|
-
return result;
|
|
133
|
-
}
|
|
134
|
-
function mergeBucketsInto(target, incoming) {
|
|
135
|
-
for (const [hash, incomingBucket] of incoming) {
|
|
136
|
-
const existing = target.get(hash);
|
|
137
|
-
if (!existing) {
|
|
138
|
-
target.set(hash, { ...incomingBucket });
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
existing.count += incomingBucket.count;
|
|
142
|
-
if (incomingBucket.path !== void 0) existing.path = incomingBucket.path;
|
|
143
|
-
if (incomingBucket.tags.length > 0) existing.tags = incomingBucket.tags;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function selectRoute(buckets) {
|
|
147
|
-
let route;
|
|
148
|
-
let count = -1;
|
|
149
|
-
for (const bucket of buckets.values()) if (bucket.path !== void 0 && bucket.count > count) {
|
|
150
|
-
count = bucket.count;
|
|
151
|
-
route = bucket.path;
|
|
152
|
-
}
|
|
153
|
-
return route;
|
|
154
|
-
}
|
|
155
|
-
function mergeCustomContext(buckets) {
|
|
156
|
-
const merged = {};
|
|
157
|
-
for (const bucket of buckets.values()) for (const [key, value] of Object.entries(bucket.custom)) if (!(key in merged)) merged[key] = value;
|
|
158
|
-
return merged;
|
|
159
|
-
}
|
|
160
|
-
function createPayload(snapshot, options, sessionId) {
|
|
161
|
-
const errors = [...snapshot.entries()].map(([hash, bucket]) => ({
|
|
162
|
-
...bucket.entry,
|
|
163
|
-
hash,
|
|
164
|
-
count: bucket.count
|
|
165
|
-
}));
|
|
166
|
-
const tags = [...new Set([...snapshot.values()].flatMap((bucket) => bucket.tags))];
|
|
167
|
-
const baseContext = buildServerReportContext({
|
|
168
|
-
route: selectRoute(snapshot),
|
|
169
|
-
plugin: options.pluginId,
|
|
170
|
-
tags: tags.length > 0 ? tags.join(",") : void 0
|
|
171
|
-
});
|
|
172
|
-
return {
|
|
173
|
-
errors,
|
|
174
|
-
sessionId,
|
|
175
|
-
...options.buildId ? { buildId: options.buildId } : {},
|
|
176
|
-
sdkName: SDK_NAME,
|
|
177
|
-
sdkVersion: SDK_VERSION,
|
|
178
|
-
context: {
|
|
179
|
-
...pruneUndefined(baseContext),
|
|
180
|
-
...mergeCustomContext(snapshot)
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
async function resolveCustomContext(options, args) {
|
|
185
|
-
if (!options.getContext) return {};
|
|
186
|
-
try {
|
|
187
|
-
return await options.getContext(args) ?? {};
|
|
188
|
-
} catch {
|
|
189
|
-
return {};
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
function updateExistingBucket(bucket, args) {
|
|
193
|
-
bucket.count += 1;
|
|
194
|
-
if (args.path !== void 0) bucket.path = args.path;
|
|
195
|
-
if (args.tags.length > 0) bucket.tags = args.tags;
|
|
196
|
-
}
|
|
197
|
-
function createNitroErrorReporter(options, runtime) {
|
|
198
|
-
const state = {
|
|
199
|
-
buckets: /* @__PURE__ */ new Map(),
|
|
200
|
-
timer: null,
|
|
201
|
-
flushRunning: false,
|
|
202
|
-
consecutiveFailures: 0,
|
|
203
|
-
pausedUntil: 0
|
|
204
|
-
};
|
|
205
|
-
let sessionId = options.sessionId;
|
|
206
|
-
const getSessionId = () => {
|
|
207
|
-
sessionId ??= runtime.randomUUID();
|
|
208
|
-
return sessionId;
|
|
209
|
-
};
|
|
210
|
-
const clearFlushTimer = () => {
|
|
211
|
-
if (state.timer !== null) {
|
|
212
|
-
runtime.clearTimeout(state.timer);
|
|
213
|
-
state.timer = null;
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
const scheduleFlush = () => {
|
|
217
|
-
if (state.timer !== null) return;
|
|
218
|
-
state.timer = runtime.setTimeout(() => {
|
|
219
|
-
state.timer = null;
|
|
220
|
-
flushBatches();
|
|
221
|
-
}, options.flushIntervalMs);
|
|
222
|
-
};
|
|
223
|
-
const sendBatch = async (snapshot) => {
|
|
224
|
-
try {
|
|
225
|
-
const response = await runtime.fetch(options.errorEndpoint, {
|
|
226
|
-
method: "POST",
|
|
227
|
-
headers: {
|
|
228
|
-
"Content-Type": "application/json",
|
|
229
|
-
Authorization: `Bearer ${options.token}`
|
|
230
|
-
},
|
|
231
|
-
body: JSON.stringify(createPayload(snapshot, options, getSessionId()))
|
|
232
|
-
});
|
|
233
|
-
if (response.ok) return true;
|
|
234
|
-
if (options.debug) runtime.warn("[faststats nitro] report failed", response.status, await response.text());
|
|
235
|
-
return false;
|
|
236
|
-
} catch (error) {
|
|
237
|
-
if (options.debug) runtime.warn("[faststats nitro] report error", error);
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
const flushBatches = async () => {
|
|
242
|
-
clearFlushTimer();
|
|
243
|
-
if (runtime.now() < state.pausedUntil) {
|
|
244
|
-
scheduleFlush();
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (state.buckets.size === 0 || state.flushRunning) {
|
|
248
|
-
if (state.buckets.size > 0) scheduleFlush();
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
state.flushRunning = true;
|
|
252
|
-
const snapshot = new Map(state.buckets);
|
|
253
|
-
state.buckets.clear();
|
|
254
|
-
const sendOk = await sendBatch(snapshot);
|
|
255
|
-
state.flushRunning = false;
|
|
256
|
-
if (sendOk) state.consecutiveFailures = 0;
|
|
257
|
-
else {
|
|
258
|
-
state.consecutiveFailures += 1;
|
|
259
|
-
mergeBucketsInto(state.buckets, snapshot);
|
|
260
|
-
if (state.consecutiveFailures >= options.circuitBreakerFailures) {
|
|
261
|
-
state.pausedUntil = runtime.now() + options.circuitBreakerPauseMs;
|
|
262
|
-
state.consecutiveFailures = 0;
|
|
263
|
-
if (options.debug) runtime.warn("[faststats nitro] circuit open; pausing error reports for", options.circuitBreakerPauseMs, "ms");
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (state.buckets.size > 0) scheduleFlush();
|
|
267
|
-
};
|
|
268
|
-
return { async report(error, args) {
|
|
269
|
-
if (!options.token) {
|
|
270
|
-
if (options.debug) runtime.warn("[faststats nitro] FASTSTATS_TOKEN not set; skipping error report");
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (runtime.now() < state.pausedUntil) return;
|
|
274
|
-
const { hash, entry } = hashServerError(error);
|
|
275
|
-
const existing = state.buckets.get(hash);
|
|
276
|
-
if (existing) updateExistingBucket(existing, args);
|
|
277
|
-
else state.buckets.set(hash, {
|
|
278
|
-
entry,
|
|
279
|
-
count: 1,
|
|
280
|
-
path: args.path,
|
|
281
|
-
tags: args.tags,
|
|
282
|
-
custom: await resolveCustomContext(options, {
|
|
283
|
-
error,
|
|
284
|
-
path: args.path,
|
|
285
|
-
tags: args.tags
|
|
286
|
-
})
|
|
287
|
-
});
|
|
288
|
-
if (state.buckets.size >= options.maxBatchHashes) {
|
|
289
|
-
clearFlushTimer();
|
|
290
|
-
await flushBatches();
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
scheduleFlush();
|
|
294
|
-
} };
|
|
295
|
-
}
|
|
296
|
-
//#endregion
|
|
297
|
-
//#region src/report-error.ts
|
|
298
|
-
const DEFAULT_ENDPOINT = "https://metrics.faststats.dev/v1/error";
|
|
299
|
-
const reporters = /* @__PURE__ */ new WeakMap();
|
|
300
|
-
const envInt = (key, fallback) => {
|
|
301
|
-
const value = Number.parseInt(process?.env?.[key] ?? "", 10);
|
|
302
|
-
return value > 0 ? value : fallback;
|
|
303
|
-
};
|
|
304
|
-
function resolveFaststatsNitroOptions(overrides = {}) {
|
|
305
|
-
return {
|
|
306
|
-
errorEndpoint: overrides.errorEndpoint ?? process?.env?.FASTSTATS_ERROR_ENDPOINT ?? DEFAULT_ENDPOINT,
|
|
307
|
-
debug: overrides.debug ?? process?.env?.FASTSTATS_DEBUG === "1",
|
|
308
|
-
token: overrides.token ?? process?.env?.FASTSTATS_TOKEN ?? process?.env?.FASTSTATS_API_TOKEN,
|
|
309
|
-
buildId: overrides.buildId ?? process?.env?.FASTSTATS_BUILD_ID,
|
|
310
|
-
sessionId: overrides.sessionId ?? process?.env?.FASTSTATS_SESSION_ID,
|
|
311
|
-
getContext: overrides.getContext,
|
|
312
|
-
pluginId: overrides.pluginId,
|
|
313
|
-
flushIntervalMs: overrides.flushIntervalMs ?? envInt("FASTSTATS_ERROR_FLUSH_MS", 5e3),
|
|
314
|
-
maxBatchHashes: overrides.maxBatchHashes ?? envInt("FASTSTATS_ERROR_MAX_HASHES", 50),
|
|
315
|
-
circuitBreakerFailures: overrides.circuitBreakerFailures ?? envInt("FASTSTATS_ERROR_CIRCUIT_FAILS", 5),
|
|
316
|
-
circuitBreakerPauseMs: overrides.circuitBreakerPauseMs ?? envInt("FASTSTATS_ERROR_CIRCUIT_PAUSE_MS", 6e4)
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
function createDefaultReporter(options) {
|
|
320
|
-
return createNitroErrorReporter(options, {
|
|
321
|
-
fetch: (input, init) => fetch(input, init),
|
|
322
|
-
setTimeout: (callback, delay) => setTimeout(callback, delay),
|
|
323
|
-
clearTimeout: (timer) => clearTimeout(timer),
|
|
324
|
-
now: () => Date.now(),
|
|
325
|
-
randomUUID: () => {
|
|
326
|
-
if (crypto.randomUUID !== void 0) return crypto.randomUUID();
|
|
327
|
-
return `sess-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
328
|
-
},
|
|
329
|
-
warn: (...args) => console.warn(...args)
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
function getReporter(options) {
|
|
333
|
-
let reporter = reporters.get(options);
|
|
334
|
-
if (!reporter) {
|
|
335
|
-
reporter = createDefaultReporter(options);
|
|
336
|
-
reporters.set(options, reporter);
|
|
337
|
-
}
|
|
338
|
-
return reporter;
|
|
339
|
-
}
|
|
340
|
-
async function reportNitroErrorToFaststats(error, args) {
|
|
341
|
-
await getReporter(args.options).report(error, {
|
|
342
|
-
path: args.path,
|
|
343
|
-
tags: args.tags
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
//#endregion
|
|
347
|
-
export { resolveFaststatsNitroOptions as n, reportNitroErrorToFaststats as t };
|