@faststats/nitro 0.4.1 → 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.
@@ -1,4 +1,4 @@
1
- //#region src/report-error.d.ts
1
+ //#region src/options.d.ts
2
2
  type FaststatsNitroOptions = {
3
3
  token?: string;
4
4
  errorEndpoint?: string;
@@ -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 "./report-error-ldlQ3TdC.mjs";
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 reportNitroErrorToFaststats } from "./report-error-CSuDx52_.mjs";
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 resolved = resolveFaststatsNitroOptions({
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", async (error, context) => {
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 "./report-error-ldlQ3TdC.mjs";
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 reportNitroErrorToFaststats } from "./report-error-CSuDx52_.mjs";
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 resolved = resolveFaststatsNitroOptions({
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", async (error, ctx) => {
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
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "https://github.com/faststats-dev/faststats-javascript.git"
6
6
  },
7
- "version": "0.4.1",
7
+ "version": "0.5.0",
8
8
  "type": "module",
9
9
  "main": "./dist/v2.mjs",
10
10
  "module": "./dist/v2.mjs",
@@ -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.1";
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 };