@faststats/nitro 0.1.4

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.
@@ -0,0 +1,341 @@
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/error-reporter.ts
125
+ function pruneUndefined(context) {
126
+ const result = {};
127
+ for (const [key, value] of Object.entries(context)) if (value !== void 0 && value !== "") result[key] = value;
128
+ return result;
129
+ }
130
+ function mergeBucketsInto(target, incoming) {
131
+ for (const [hash, incomingBucket] of incoming) {
132
+ const existing = target.get(hash);
133
+ if (!existing) {
134
+ target.set(hash, { ...incomingBucket });
135
+ continue;
136
+ }
137
+ existing.count += incomingBucket.count;
138
+ if (incomingBucket.path !== void 0) existing.path = incomingBucket.path;
139
+ if (incomingBucket.tags.length > 0) existing.tags = incomingBucket.tags;
140
+ }
141
+ }
142
+ function selectRoute(buckets) {
143
+ let route;
144
+ let count = -1;
145
+ for (const bucket of buckets.values()) if (bucket.path !== void 0 && bucket.count > count) {
146
+ count = bucket.count;
147
+ route = bucket.path;
148
+ }
149
+ return route;
150
+ }
151
+ function mergeCustomContext(buckets) {
152
+ const merged = {};
153
+ for (const bucket of buckets.values()) for (const [key, value] of Object.entries(bucket.custom)) if (!(key in merged)) merged[key] = value;
154
+ return merged;
155
+ }
156
+ function createPayload(snapshot, options, sessionId) {
157
+ const errors = [...snapshot.entries()].map(([hash, bucket]) => ({
158
+ ...bucket.entry,
159
+ hash,
160
+ count: bucket.count
161
+ }));
162
+ const tags = [...new Set([...snapshot.values()].flatMap((bucket) => bucket.tags))];
163
+ const baseContext = buildServerReportContext({
164
+ route: selectRoute(snapshot),
165
+ plugin: options.pluginId,
166
+ tags: tags.length > 0 ? tags.join(",") : void 0
167
+ });
168
+ return {
169
+ errors,
170
+ sessionId,
171
+ ...options.buildId ? { buildId: options.buildId } : {},
172
+ context: {
173
+ ...pruneUndefined(baseContext),
174
+ ...mergeCustomContext(snapshot)
175
+ }
176
+ };
177
+ }
178
+ async function resolveCustomContext(options, args) {
179
+ if (!options.getContext) return {};
180
+ try {
181
+ return await options.getContext(args) ?? {};
182
+ } catch {
183
+ return {};
184
+ }
185
+ }
186
+ function updateExistingBucket(bucket, args) {
187
+ bucket.count += 1;
188
+ if (args.path !== void 0) bucket.path = args.path;
189
+ if (args.tags.length > 0) bucket.tags = args.tags;
190
+ }
191
+ function createNitroErrorReporter(options, runtime) {
192
+ const state = {
193
+ buckets: /* @__PURE__ */ new Map(),
194
+ timer: null,
195
+ flushRunning: false,
196
+ consecutiveFailures: 0,
197
+ pausedUntil: 0
198
+ };
199
+ let sessionId = options.sessionId;
200
+ const getSessionId = () => {
201
+ sessionId ??= runtime.randomUUID();
202
+ return sessionId;
203
+ };
204
+ const clearFlushTimer = () => {
205
+ if (state.timer !== null) {
206
+ runtime.clearTimeout(state.timer);
207
+ state.timer = null;
208
+ }
209
+ };
210
+ const scheduleFlush = () => {
211
+ if (state.timer !== null) return;
212
+ state.timer = runtime.setTimeout(() => {
213
+ state.timer = null;
214
+ flushBatches();
215
+ }, options.flushIntervalMs);
216
+ };
217
+ const sendBatch = 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(createPayload(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
+ return false;
230
+ } catch (error) {
231
+ if (options.debug) runtime.warn("[faststats nitro] report error", error);
232
+ return false;
233
+ }
234
+ };
235
+ const flushBatches = async () => {
236
+ clearFlushTimer();
237
+ if (runtime.now() < state.pausedUntil) {
238
+ scheduleFlush();
239
+ return;
240
+ }
241
+ if (state.buckets.size === 0 || state.flushRunning) {
242
+ if (state.buckets.size > 0) scheduleFlush();
243
+ return;
244
+ }
245
+ state.flushRunning = true;
246
+ const snapshot = new Map(state.buckets);
247
+ state.buckets.clear();
248
+ const sendOk = await sendBatch(snapshot);
249
+ state.flushRunning = false;
250
+ if (sendOk) state.consecutiveFailures = 0;
251
+ else {
252
+ state.consecutiveFailures += 1;
253
+ mergeBucketsInto(state.buckets, snapshot);
254
+ if (state.consecutiveFailures >= options.circuitBreakerFailures) {
255
+ state.pausedUntil = runtime.now() + options.circuitBreakerPauseMs;
256
+ state.consecutiveFailures = 0;
257
+ if (options.debug) runtime.warn("[faststats nitro] circuit open; pausing error reports for", options.circuitBreakerPauseMs, "ms");
258
+ }
259
+ }
260
+ if (state.buckets.size > 0) scheduleFlush();
261
+ };
262
+ return { async report(error, args) {
263
+ if (!options.token) {
264
+ if (options.debug) runtime.warn("[faststats nitro] FASTSTATS_TOKEN not set; skipping error report");
265
+ return;
266
+ }
267
+ if (runtime.now() < state.pausedUntil) return;
268
+ const { hash, entry } = hashServerError(error);
269
+ const existing = state.buckets.get(hash);
270
+ if (existing) updateExistingBucket(existing, args);
271
+ else state.buckets.set(hash, {
272
+ entry,
273
+ count: 1,
274
+ path: args.path,
275
+ tags: args.tags,
276
+ custom: await resolveCustomContext(options, {
277
+ error,
278
+ path: args.path,
279
+ tags: args.tags
280
+ })
281
+ });
282
+ if (state.buckets.size >= options.maxBatchHashes) {
283
+ clearFlushTimer();
284
+ await flushBatches();
285
+ return;
286
+ }
287
+ scheduleFlush();
288
+ } };
289
+ }
290
+ //#endregion
291
+ //#region src/report-error.ts
292
+ const DEFAULT_ENDPOINT = "https://metrics.faststats.dev/v1/error";
293
+ const reporters = /* @__PURE__ */ new WeakMap();
294
+ const envInt = (key, fallback) => {
295
+ const value = Number.parseInt(process?.env?.[key] ?? "", 10);
296
+ return value > 0 ? value : fallback;
297
+ };
298
+ function resolveFaststatsNitroOptions(overrides = {}) {
299
+ return {
300
+ errorEndpoint: overrides.errorEndpoint ?? process?.env?.FASTSTATS_ERROR_ENDPOINT ?? DEFAULT_ENDPOINT,
301
+ debug: overrides.debug ?? process?.env?.FASTSTATS_DEBUG === "1",
302
+ token: overrides.token ?? process?.env?.FASTSTATS_TOKEN ?? process?.env?.FASTSTATS_API_TOKEN,
303
+ buildId: overrides.buildId ?? process?.env?.FASTSTATS_BUILD_ID,
304
+ sessionId: overrides.sessionId ?? process?.env?.FASTSTATS_SESSION_ID,
305
+ getContext: overrides.getContext,
306
+ pluginId: overrides.pluginId,
307
+ flushIntervalMs: overrides.flushIntervalMs ?? envInt("FASTSTATS_ERROR_FLUSH_MS", 5e3),
308
+ maxBatchHashes: overrides.maxBatchHashes ?? envInt("FASTSTATS_ERROR_MAX_HASHES", 50),
309
+ circuitBreakerFailures: overrides.circuitBreakerFailures ?? envInt("FASTSTATS_ERROR_CIRCUIT_FAILS", 5),
310
+ circuitBreakerPauseMs: overrides.circuitBreakerPauseMs ?? envInt("FASTSTATS_ERROR_CIRCUIT_PAUSE_MS", 6e4)
311
+ };
312
+ }
313
+ function createDefaultReporter(options) {
314
+ return createNitroErrorReporter(options, {
315
+ fetch: (input, init) => fetch(input, init),
316
+ setTimeout: (callback, delay) => setTimeout(callback, delay),
317
+ clearTimeout: (timer) => clearTimeout(timer),
318
+ now: () => Date.now(),
319
+ randomUUID: () => {
320
+ if (crypto.randomUUID !== void 0) return crypto.randomUUID();
321
+ return `sess-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
322
+ },
323
+ warn: (...args) => console.warn(...args)
324
+ });
325
+ }
326
+ function getReporter(options) {
327
+ let reporter = reporters.get(options);
328
+ if (!reporter) {
329
+ reporter = createDefaultReporter(options);
330
+ reporters.set(options, reporter);
331
+ }
332
+ return reporter;
333
+ }
334
+ async function reportNitroErrorToFaststats(error, args) {
335
+ await getReporter(args.options).report(error, {
336
+ path: args.path,
337
+ tags: args.tags
338
+ });
339
+ }
340
+ //#endregion
341
+ export { resolveFaststatsNitroOptions as n, reportNitroErrorToFaststats as t };
@@ -0,0 +1,20 @@
1
+ //#region src/report-error.d.ts
2
+ type FaststatsNitroOptions = {
3
+ token?: string;
4
+ errorEndpoint?: string;
5
+ buildId?: string;
6
+ sessionId?: string;
7
+ debug?: boolean;
8
+ pluginId?: string;
9
+ flushIntervalMs?: number;
10
+ maxBatchHashes?: number;
11
+ circuitBreakerFailures?: number;
12
+ circuitBreakerPauseMs?: number;
13
+ getContext?: (args: {
14
+ error: unknown;
15
+ path?: string;
16
+ tags: string[];
17
+ }) => Record<string, unknown> | Promise<Record<string, unknown>>;
18
+ };
19
+ //#endregion
20
+ export { FaststatsNitroOptions as t };
package/dist/v2.d.mts ADDED
@@ -0,0 +1,8 @@
1
+ import { t as FaststatsNitroOptions } from "./report-error-CdwX_ZIm.mjs";
2
+ import * as nitropack from "nitropack";
3
+
4
+ //#region src/v2.d.ts
5
+ declare function createFaststatsNitroPluginV2(options?: FaststatsNitroOptions): nitropack.NitroAppPlugin;
6
+ declare const _default: nitropack.NitroAppPlugin;
7
+ //#endregion
8
+ export { createFaststatsNitroPluginV2, _default as default };
package/dist/v2.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import { n as resolveFaststatsNitroOptions, t as reportNitroErrorToFaststats } from "./report-error-Be0UMIs5.mjs";
2
+ import { defineNitroPlugin } from "nitropack/runtime";
3
+ //#region src/v2.ts
4
+ function createFaststatsNitroPluginV2(options) {
5
+ const resolved = resolveFaststatsNitroOptions({
6
+ ...options,
7
+ pluginId: options?.pluginId ?? "nitro-v2"
8
+ });
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
+ });
19
+ });
20
+ }
21
+ var v2_default = createFaststatsNitroPluginV2();
22
+ //#endregion
23
+ export { createFaststatsNitroPluginV2, v2_default as default };
package/dist/v3.d.mts ADDED
@@ -0,0 +1,8 @@
1
+ import { t as FaststatsNitroOptions } from "./report-error-CdwX_ZIm.mjs";
2
+ import * as nitro_types0 from "nitro/types";
3
+
4
+ //#region src/v3.d.ts
5
+ declare function createFaststatsNitroPluginV3(options?: FaststatsNitroOptions): nitro_types0.NitroAppPlugin;
6
+ declare const _default: nitro_types0.NitroAppPlugin;
7
+ //#endregion
8
+ export { createFaststatsNitroPluginV3, _default as default };
package/dist/v3.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import { n as resolveFaststatsNitroOptions, t as reportNitroErrorToFaststats } from "./report-error-Be0UMIs5.mjs";
2
+ import { definePlugin } from "nitro";
3
+ //#region src/v3.ts
4
+ function createFaststatsNitroPluginV3(options) {
5
+ const resolved = resolveFaststatsNitroOptions({
6
+ ...options,
7
+ pluginId: options?.pluginId ?? "nitro-v3"
8
+ });
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
+ });
19
+ });
20
+ }
21
+ var v3_default = createFaststatsNitroPluginV3();
22
+ //#endregion
23
+ export { createFaststatsNitroPluginV3, v3_default as default };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@faststats/nitro",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "https://github.com/faststats-dev/web-analytics.git"
6
+ },
7
+ "version": "0.1.4",
8
+ "type": "module",
9
+ "main": "./dist/v2.mjs",
10
+ "module": "./dist/v2.mjs",
11
+ "types": "./dist/v2.d.mts",
12
+ "exports": {
13
+ "./v2": {
14
+ "types": "./dist/v2.d.mts",
15
+ "import": "./dist/v2.mjs",
16
+ "default": "./dist/v2.mjs"
17
+ },
18
+ "./v3": {
19
+ "types": "./dist/v3.d.mts",
20
+ "import": "./dist/v3.mjs",
21
+ "default": "./dist/v3.mjs"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "peerDependencies": {
31
+ "nitropack": "^2.13.0",
32
+ "nitro": "3.0.260311-beta"
33
+ },
34
+ "devDependencies": {
35
+ "nitropack": "2.13.1",
36
+ "nitro": "3.0.260311-beta",
37
+ "tsdown": "^0.21.4",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "nitro": {
42
+ "optional": true
43
+ },
44
+ "nitropack": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "scripts": {
49
+ "prepare": "tsdown",
50
+ "build": "tsdown",
51
+ "typecheck": "tsc --noEmit -p tsconfig.json",
52
+ "test": "bun test"
53
+ }
54
+ }