@oconde/radar 0.0.0-stage → 0.1.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/index.cjs ADDED
@@ -0,0 +1,1107 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ RadarClient: () => RadarClient,
24
+ radar: () => radar
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+
28
+ // src/client.ts
29
+ var import_node_crypto3 = require("crypto");
30
+ var import_node_os = require("os");
31
+ var import_node_perf_hooks3 = require("perf_hooks");
32
+
33
+ // src/context.ts
34
+ var import_node_async_hooks = require("async_hooks");
35
+ var STATE_KEY = /* @__PURE__ */ Symbol.for("@oconde/radar");
36
+ function globalState() {
37
+ const holder = globalThis;
38
+ let state2 = holder[STATE_KEY];
39
+ if (!state2) {
40
+ state2 = { storage: new import_node_async_hooks.AsyncLocalStorage() };
41
+ holder[STATE_KEY] = state2;
42
+ }
43
+ return state2;
44
+ }
45
+ function currentContext() {
46
+ return globalState().storage.getStore();
47
+ }
48
+ function runWithContext(context, fn) {
49
+ return globalState().storage.run(context, fn);
50
+ }
51
+
52
+ // src/express.ts
53
+ var import_node_crypto2 = require("crypto");
54
+ var import_node_perf_hooks = require("perf_hooks");
55
+
56
+ // src/protocol/index.ts
57
+ var INGEST_PATH = "/api/v1/events";
58
+ var LIMITS = {
59
+ batchBytes: 1e6,
60
+ batchEvents: 500,
61
+ messageLength: 2e3,
62
+ stringLength: 2e3,
63
+ attrsBytes: 16384,
64
+ bodyBytes: 16384,
65
+ headers: 50,
66
+ frames: 50,
67
+ contextFrames: 10,
68
+ contextLines: 5,
69
+ contextLineLength: 300,
70
+ causeDepth: 5,
71
+ depth: 6,
72
+ arrayItems: 50,
73
+ requestIdLength: 128
74
+ };
75
+
76
+ // src/redact.ts
77
+ var import_node_crypto = require("crypto");
78
+ var SECRET_WORDS = /* @__PURE__ */ new Set([
79
+ "pass",
80
+ "passwd",
81
+ "password",
82
+ "passwords",
83
+ "senha",
84
+ "secret",
85
+ "secrets",
86
+ "token",
87
+ "tokens",
88
+ "authorization",
89
+ "auth",
90
+ "cookie",
91
+ "cookies",
92
+ "apikey",
93
+ "privatekey",
94
+ "cpf",
95
+ "card",
96
+ "cvv",
97
+ "cvc",
98
+ "pin",
99
+ "otp",
100
+ "signature",
101
+ "jwt",
102
+ "session",
103
+ "sessionid",
104
+ "credential",
105
+ "credentials"
106
+ ]);
107
+ var SECRET_PAIRS = [
108
+ ["api", "key"],
109
+ ["private", "key"],
110
+ ["access", "key"],
111
+ ["secret", "key"]
112
+ ];
113
+ var LOW_ENTROPY_WORDS = /* @__PURE__ */ new Set(["pass", "passwd", "password", "passwords", "senha", "cpf", "card", "cvv", "cvc", "pin", "otp"]);
114
+ var CUSTOM_SECRET_HEADER = /^x-.+-(key|secret|token)$/i;
115
+ var AUTH_SCHEME = /^(Bearer|Basic|Token|Digest|ApiKey)\s+/i;
116
+ var JWT_PREFIX = /^eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\./;
117
+ var MAX_DEPTH = 8;
118
+ var KEY_SLOT = /* @__PURE__ */ Symbol.for("@oconde/radar/fingerprint-key");
119
+ function fingerprintSlot() {
120
+ const holder = globalThis;
121
+ let slot = holder[KEY_SLOT];
122
+ if (!slot) {
123
+ slot = { key: (0, import_node_crypto.randomBytes)(32) };
124
+ holder[KEY_SLOT] = slot;
125
+ }
126
+ return slot;
127
+ }
128
+ function setFingerprintSecret(secret) {
129
+ fingerprintSlot().key = secret ? (0, import_node_crypto.createHash)("sha256").update(`@oconde/radar:${secret}`).digest() : (0, import_node_crypto.randomBytes)(32);
130
+ }
131
+ function fingerprint(value) {
132
+ return (0, import_node_crypto.createHmac)("sha256", fingerprintSlot().key).update(value).digest("hex").slice(0, 8);
133
+ }
134
+ function words(name) {
135
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
136
+ }
137
+ function isSensitiveKey(key) {
138
+ const parts = words(key);
139
+ if (parts.length === 1 && parts[0] === "key") return true;
140
+ if (parts.some((part) => SECRET_WORDS.has(part))) return true;
141
+ return SECRET_PAIRS.some(([first, second]) => parts.some((part, index) => part === first && parts[index + 1] === second));
142
+ }
143
+ function isLowEntropyKey(key) {
144
+ return words(key).some((part) => LOW_ENTROPY_WORDS.has(part));
145
+ }
146
+ function isSensitiveHeader(name) {
147
+ return isSensitiveKey(name) || CUSTOM_SECRET_HEADER.test(name);
148
+ }
149
+ function looksLikeCredential(value) {
150
+ const head = value.slice(0, 64);
151
+ return AUTH_SCHEME.test(head.slice(0, 16)) || JWT_PREFIX.test(head);
152
+ }
153
+ function maskValue(value, revealEdges = true) {
154
+ const text = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
155
+ const scheme = AUTH_SCHEME.exec(text.slice(0, 16));
156
+ const prefix = scheme ? `${scheme[1]} ` : "";
157
+ const secret = scheme ? text.slice(scheme[0].length) : text;
158
+ const visible = revealEdges && secret.length > 12 ? `${secret.slice(0, 4)}\u2026${secret.slice(-4)}` : "\u2022\u2022\u2022";
159
+ return `${prefix}${visible} #${fingerprint(text)}`;
160
+ }
161
+ function redactDeep(value, mode, depth = 0) {
162
+ if (mode === "none" || depth > MAX_DEPTH || value === null) return value;
163
+ if (typeof value === "string") return looksLikeCredential(value) ? maskValue(value) : value;
164
+ if (typeof value !== "object") return value;
165
+ if (Array.isArray(value)) return value.map((item) => redactDeep(item, mode, depth + 1));
166
+ const result = {};
167
+ for (const [key, item] of Object.entries(value)) {
168
+ const hasValue = item !== null && item !== void 0 && item !== "";
169
+ result[key] = isSensitiveKey(key) && hasValue ? maskValue(item, !isLowEntropyKey(key)) : redactDeep(item, mode, depth + 1);
170
+ }
171
+ return result;
172
+ }
173
+
174
+ // src/serialize.ts
175
+ var TRUNCATED = "\u2026[cortado]";
176
+ var LIMIT_REACHED = "\u2026[limite]";
177
+ var MAX_NODES = 2e3;
178
+ function truncate(value, max = LIMITS.stringLength) {
179
+ return value.length > max ? value.slice(0, max) + TRUNCATED : value;
180
+ }
181
+ function sanitize(value, depth = 0, seen = /* @__PURE__ */ new WeakSet(), budget = { remaining: MAX_NODES }) {
182
+ if (value === null || value === void 0) return value;
183
+ budget.remaining -= 1;
184
+ switch (typeof value) {
185
+ case "string":
186
+ return truncate(value);
187
+ case "number":
188
+ return Number.isFinite(value) ? value : String(value);
189
+ case "boolean":
190
+ return value;
191
+ case "bigint":
192
+ return value.toString();
193
+ case "function":
194
+ case "symbol":
195
+ return void 0;
196
+ }
197
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
198
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return `[bin\xE1rio ${value.byteLength} bytes]`;
199
+ if (value instanceof Error) return { name: value.name, message: truncate(value.message) };
200
+ const object = value;
201
+ if (seen.has(object)) return "[circular]";
202
+ if (depth >= LIMITS.depth) return "[profundidade]";
203
+ if (budget.remaining <= 0) return LIMIT_REACHED;
204
+ seen.add(object);
205
+ try {
206
+ if (Array.isArray(object)) {
207
+ const items = [];
208
+ for (const item of object.slice(0, LIMITS.arrayItems)) {
209
+ if (budget.remaining <= 0) {
210
+ items.push(LIMIT_REACHED);
211
+ break;
212
+ }
213
+ items.push(sanitize(item, depth + 1, seen, budget) ?? null);
214
+ }
215
+ if (object.length > LIMITS.arrayItems) items.push(`\u2026[mais ${object.length - LIMITS.arrayItems}]`);
216
+ return items;
217
+ }
218
+ if (object instanceof Map) return sanitize(Object.fromEntries(object.entries()), depth + 1, seen, budget);
219
+ if (object instanceof Set) return sanitize([...object], depth + 1, seen, budget);
220
+ const result = {};
221
+ for (const key in object) {
222
+ if (!Object.prototype.hasOwnProperty.call(object, key)) continue;
223
+ if (budget.remaining <= 0) {
224
+ result["\u2026"] = LIMIT_REACHED;
225
+ break;
226
+ }
227
+ const clean = sanitize(object[key], depth + 1, seen, budget);
228
+ if (clean !== void 0) result[key] = clean;
229
+ }
230
+ return result;
231
+ } finally {
232
+ seen.delete(object);
233
+ }
234
+ }
235
+ function limitBytes(value, maxBytes) {
236
+ const json = JSON.stringify(value);
237
+ if (json === void 0 || Buffer.byteLength(json) <= maxBytes) return value;
238
+ return { _truncated: truncate(json, maxBytes) };
239
+ }
240
+ function prepareAttrs(data, mode) {
241
+ const clean = redactDeep(sanitize(data), mode);
242
+ const record = isRecord(clean) ? clean : { value: clean };
243
+ return limitBytes(record, LIMITS.attrsBytes);
244
+ }
245
+ function isRecord(value) {
246
+ return typeof value === "object" && value !== null && !Array.isArray(value);
247
+ }
248
+
249
+ // src/request.ts
250
+ function headerValue(value) {
251
+ if (value === void 0) return void 0;
252
+ return Array.isArray(value) ? value.join(", ") : String(value);
253
+ }
254
+ function requestHeaders(headers, mode) {
255
+ const result = {};
256
+ let count = 0;
257
+ for (const [name, raw] of Object.entries(headers)) {
258
+ const value = headerValue(raw);
259
+ if (value === void 0) continue;
260
+ if (count >= LIMITS.headers) break;
261
+ const key = name.toLowerCase();
262
+ result[key] = mode === "mask" && (isSensitiveHeader(key) || looksLikeCredential(value)) ? maskValue(value) : truncate(value);
263
+ count++;
264
+ }
265
+ return result;
266
+ }
267
+ function requestPath(req) {
268
+ const url = req.originalUrl ?? req.url ?? "";
269
+ const end = url.indexOf("?");
270
+ return end >= 0 ? url.slice(0, end) : url;
271
+ }
272
+ function requestRoute(req, route) {
273
+ if (route) return route;
274
+ return typeof req.route?.path === "string" ? req.route.path : void 0;
275
+ }
276
+ function redactPath(path, params, mode) {
277
+ if (mode === "none" || !params) return path;
278
+ let result = path;
279
+ for (const [key, value] of Object.entries(params)) {
280
+ if (typeof value !== "string" || value.length < 4 || !isSensitiveKey(key)) continue;
281
+ const masked = maskValue(value, !isLowEntropyKey(key));
282
+ for (const form of /* @__PURE__ */ new Set([value, encodeURIComponent(value)])) result = result.split(form).join(masked);
283
+ }
284
+ return result;
285
+ }
286
+ function safeUrl(req, mode) {
287
+ const url = req.originalUrl ?? req.url ?? "";
288
+ const queryStart = url.indexOf("?");
289
+ const path = queryStart >= 0 ? url.slice(0, queryStart) : url;
290
+ const query = queryStart >= 0 ? url.slice(queryStart) : "";
291
+ return redactUrl(redactPath(path, req.params, mode) + query, mode);
292
+ }
293
+ function redactUrl(url, mode) {
294
+ const queryStart = url.indexOf("?");
295
+ if (mode === "none" || queryStart < 0) return url;
296
+ const params = new URLSearchParams(url.slice(queryStart + 1));
297
+ let changed = false;
298
+ for (const [key, value] of [...params.entries()]) {
299
+ if (isSensitiveKey(key) && value !== "") {
300
+ params.set(key, maskValue(value, !isLowEntropyKey(key)));
301
+ changed = true;
302
+ }
303
+ }
304
+ return changed ? `${url.slice(0, queryStart)}?${params.toString()}` : url;
305
+ }
306
+ function requestInfo(req, mode, extra = {}, detail = "full") {
307
+ const route = requestRoute(req, extra.route);
308
+ if (detail === "route") {
309
+ return {
310
+ method: (req.method ?? "GET").toUpperCase(),
311
+ url: route ?? "",
312
+ ...route ? { route } : {},
313
+ ...extra.status !== void 0 ? { status: extra.status } : {},
314
+ ...extra.durationMs !== void 0 ? { durationMs: extra.durationMs } : {}
315
+ };
316
+ }
317
+ const params = prepareRecord(req.params, mode);
318
+ const query = prepareRecord(req.query, mode);
319
+ const body = prepareBody(req.body, mode);
320
+ const userAgent = headerValue(req.headers["user-agent"]);
321
+ const ip = req.ip ?? req.socket?.remoteAddress;
322
+ return {
323
+ method: (req.method ?? "GET").toUpperCase(),
324
+ url: truncate(safeUrl(req, mode)),
325
+ ...route ? { route } : {},
326
+ ...params ? { params } : {},
327
+ ...query ? { query } : {},
328
+ headers: requestHeaders(req.headers, mode),
329
+ ...body !== void 0 ? { body } : {},
330
+ ...ip ? { ip } : {},
331
+ ...userAgent ? { userAgent: truncate(userAgent, 500) } : {},
332
+ ...extra.status !== void 0 ? { status: extra.status } : {},
333
+ ...extra.durationMs !== void 0 ? { durationMs: extra.durationMs } : {}
334
+ };
335
+ }
336
+ function isEmptyRecord(value) {
337
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
338
+ }
339
+ function prepareRecord(value, mode) {
340
+ if (typeof value !== "object" || value === null) return void 0;
341
+ const clean = prepare(value, mode);
342
+ return isEmptyRecord(clean) || typeof clean !== "object" || clean === null ? void 0 : clean;
343
+ }
344
+ function prepare(value, mode) {
345
+ return redactDeep(sanitize(value), mode);
346
+ }
347
+ function prepareBody(body, mode) {
348
+ if (body === void 0 || body === null || body === "") return void 0;
349
+ const clean = prepare(body, mode);
350
+ if (isEmptyRecord(clean)) return void 0;
351
+ return limitBytes(clean, LIMITS.bodyBytes);
352
+ }
353
+
354
+ // src/express.ts
355
+ function createMiddleware(logger) {
356
+ return (req, res, next) => {
357
+ let context;
358
+ try {
359
+ const incoming = headerValue(req.headers["x-request-id"])?.trim();
360
+ const requestId = incoming && incoming.length <= LIMITS.requestIdLength ? incoming : (0, import_node_crypto2.randomUUID)();
361
+ context = { requestId, startedAt: import_node_perf_hooks.performance.now(), req };
362
+ res.setHeader("x-request-id", requestId);
363
+ if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
364
+ } catch {
365
+ next();
366
+ return;
367
+ }
368
+ runWithContext(context, () => next());
369
+ };
370
+ }
371
+
372
+ // src/options.ts
373
+ var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
374
+ var LEVELS = ["debug", "info", "warn", "error"];
375
+ function resolveOptions(options = {}, env = process.env) {
376
+ const key = options.key?.trim() || void 0;
377
+ return {
378
+ key,
379
+ endpoint: (options.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, ""),
380
+ environment: options.environment ?? env.NODE_ENV ?? "production",
381
+ release: options.release || void 0,
382
+ enabled: options.enabled ?? Boolean(key),
383
+ console: options.console ?? false,
384
+ minLevel: options.minLevel && LEVELS.includes(options.minLevel) ? options.minLevel : "info",
385
+ redact: options.redact === "none" ? "none" : "mask",
386
+ requestDetail: options.requestDetail === "route" ? "route" : "full",
387
+ logRequests: options.logRequests ?? true,
388
+ ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
389
+ captureUnhandled: options.captureUnhandled ?? true,
390
+ debug: options.debug ?? false
391
+ };
392
+ }
393
+ function levelEnabled(level, minLevel) {
394
+ return LEVELS.indexOf(level) >= LEVELS.indexOf(minLevel);
395
+ }
396
+
397
+ // src/process-handlers.ts
398
+ var import_node_util = require("util");
399
+ var INSTALLED = /* @__PURE__ */ Symbol.for("@oconde/radar/process-handlers");
400
+ var MODE_FLAG = /^--unhandled-rejections=(\S+)$/;
401
+ function unhandledRejectionsMode(execArgv = process.execArgv, nodeOptions = process.env.NODE_OPTIONS ?? "") {
402
+ for (const arg of [...nodeOptions.split(/\s+/), ...execArgv]) {
403
+ const match = MODE_FLAG.exec(arg);
404
+ if (match?.[1]) return match[1];
405
+ }
406
+ return "throw";
407
+ }
408
+ function asUncaught(reason) {
409
+ if (reason instanceof Error) return reason;
410
+ const error = new Error(
411
+ `This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "${(0, import_node_util.inspect)(reason)}".`
412
+ );
413
+ error.code = "ERR_UNHANDLED_REJECTION";
414
+ return error;
415
+ }
416
+ function installProcessHandlers(capturer) {
417
+ const holder = process;
418
+ if (holder[INSTALLED]) return;
419
+ holder[INSTALLED] = true;
420
+ process.on("unhandledRejection", (reason) => {
421
+ const mode = unhandledRejectionsMode();
422
+ const othersListen = process.listenerCount("unhandledRejection") > 1;
423
+ if (othersListen || mode !== "throw") {
424
+ capturer.captureUnhandled(reason, "error");
425
+ if (!othersListen && mode === "warn-with-error-code") process.exitCode = 1;
426
+ return;
427
+ }
428
+ const uncaught = asUncaught(reason);
429
+ setImmediate(() => {
430
+ throw uncaught;
431
+ });
432
+ });
433
+ process.on("uncaughtException", (error) => {
434
+ capturer.captureUnhandled(error, "fatal");
435
+ if (process.listenerCount("uncaughtException") > 1) return;
436
+ try {
437
+ process.stderr.write(`${(0, import_node_util.inspect)(error)}
438
+ `);
439
+ } catch {
440
+ process.exitCode = 1;
441
+ }
442
+ void capturer.flush(2e3).finally(() => process.exit(1));
443
+ });
444
+ }
445
+
446
+ // src/source.ts
447
+ var import_promises = require("fs/promises");
448
+ var import_node_module = require("module");
449
+ var import_node_path = require("path");
450
+ var import_node_url = require("url");
451
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
452
+ var MAX_CACHED_FILES = 50;
453
+ var MAPPING_URL = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
454
+ var cache = /* @__PURE__ */ new Map();
455
+ async function enrichFrames(frames, cwd = process.cwd()) {
456
+ const result = [];
457
+ let withContext = 0;
458
+ for (const frame of frames) {
459
+ try {
460
+ const { frame: mapped, lines } = await mapFrame(frame);
461
+ let next = mapped;
462
+ if (mapped.inApp && lines && mapped.line && withContext < LIMITS.contextFrames) {
463
+ const context = contextFor(lines, mapped.line);
464
+ if (context) {
465
+ next = { ...mapped, context };
466
+ withContext++;
467
+ }
468
+ }
469
+ result.push({ ...next, file: relativize(next.file, cwd) });
470
+ } catch {
471
+ result.push({ ...frame, file: relativize(frame.file, cwd) });
472
+ }
473
+ }
474
+ return result;
475
+ }
476
+ async function enrichException(exception, cwd = process.cwd()) {
477
+ const frames = await enrichFrames(exception.frames, cwd);
478
+ if (!exception.cause) return { ...exception, frames };
479
+ return { ...exception, frames, cause: await enrichException(exception.cause, cwd) };
480
+ }
481
+ function relativize(file, cwd) {
482
+ if (!(0, import_node_path.isAbsolute)(file)) return file;
483
+ const path = (0, import_node_path.relative)(cwd, file);
484
+ return path.startsWith("..") || (0, import_node_path.isAbsolute)(path) ? file : path.split(import_node_path.sep).join("/");
485
+ }
486
+ async function mapFrame(frame) {
487
+ if (!frame.line || !(0, import_node_path.isAbsolute)(frame.file)) return { frame, lines: null };
488
+ const file = await loadFile(frame.file);
489
+ if (!file) return { frame, lines: null };
490
+ if (!file.map) return { frame, lines: file.lines };
491
+ const entry = file.map.map.findEntry(frame.line - 1, (frame.col ?? 1) - 1);
492
+ if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines };
493
+ const originalPath = resolveSource(file.map, entry.originalSource);
494
+ const mapped = { ...frame, file: originalPath, line: entry.originalLine + 1, col: (entry.originalColumn ?? 0) + 1 };
495
+ const lines = embeddedLines(file.map, file.map.sources.indexOf(entry.originalSource));
496
+ if (lines) return { frame: mapped, lines };
497
+ const original = await loadFile(originalPath);
498
+ return { frame: mapped, lines: original?.lines ?? null };
499
+ }
500
+ function embeddedLines(map, index) {
501
+ if (index < 0) return null;
502
+ const cached = map.lines.get(index);
503
+ if (cached) return cached;
504
+ const content = map.contents[index];
505
+ if (typeof content !== "string") return null;
506
+ const lines = content.split(/\r?\n/);
507
+ map.lines.set(index, lines);
508
+ return lines;
509
+ }
510
+ function contextFor(lines, line) {
511
+ const index = line - 1;
512
+ if (index < 0 || index >= lines.length) return void 0;
513
+ const clip = (text) => text.length > LIMITS.contextLineLength ? `${text.slice(0, LIMITS.contextLineLength)}\u2026` : text;
514
+ return {
515
+ pre: lines.slice(Math.max(0, index - LIMITS.contextLines), index).map(clip),
516
+ line: clip(lines[index] ?? ""),
517
+ post: lines.slice(index + 1, index + 1 + LIMITS.contextLines).map(clip)
518
+ };
519
+ }
520
+ function resolveSource(map, source) {
521
+ if (source.startsWith("file://")) {
522
+ try {
523
+ return (0, import_node_url.fileURLToPath)(source);
524
+ } catch {
525
+ return source;
526
+ }
527
+ }
528
+ return (0, import_node_path.isAbsolute)(source) ? source : (0, import_node_path.resolve)(map.baseDir, source);
529
+ }
530
+ function loadFile(path) {
531
+ const cached = cache.get(path);
532
+ if (cached) {
533
+ cache.delete(path);
534
+ cache.set(path, cached);
535
+ return cached;
536
+ }
537
+ const pending = readSource(path).catch(() => null);
538
+ cache.set(path, pending);
539
+ if (cache.size > MAX_CACHED_FILES) {
540
+ const oldest = cache.keys().next().value;
541
+ if (oldest !== void 0) cache.delete(oldest);
542
+ }
543
+ return pending;
544
+ }
545
+ async function readSource(path) {
546
+ const text = await readText(path);
547
+ if (text === null) return null;
548
+ const lines = text.split(/\r?\n/);
549
+ return { lines, map: await loadMap(path, lines) };
550
+ }
551
+ async function readText(path) {
552
+ try {
553
+ const info = await (0, import_promises.stat)(path);
554
+ if (!info.isFile() || info.size > MAX_FILE_BYTES) return null;
555
+ return await (0, import_promises.readFile)(path, "utf8");
556
+ } catch {
557
+ return null;
558
+ }
559
+ }
560
+ async function loadMap(path, lines) {
561
+ let url;
562
+ for (let index = lines.length - 1; index >= 0 && index >= lines.length - 5; index--) {
563
+ const match = MAPPING_URL.exec(lines[index] ?? "");
564
+ if (match) {
565
+ url = match[1];
566
+ break;
567
+ }
568
+ }
569
+ if (!url) return null;
570
+ let raw;
571
+ let mapDir;
572
+ if (url.startsWith("data:")) {
573
+ const comma = url.indexOf(",");
574
+ const encoded = url.slice(comma + 1);
575
+ raw = url.slice(0, comma).endsWith(";base64") ? Buffer.from(encoded, "base64").toString("utf8") : decodeURIComponent(encoded);
576
+ mapDir = (0, import_node_path.dirname)(path);
577
+ } else {
578
+ const mapPath = url.startsWith("file://") ? (0, import_node_url.fileURLToPath)(url) : (0, import_node_path.resolve)((0, import_node_path.dirname)(path), decodeURIComponent(url));
579
+ raw = await readText(mapPath);
580
+ mapDir = (0, import_node_path.dirname)(mapPath);
581
+ }
582
+ if (!raw) return null;
583
+ try {
584
+ const payload = JSON.parse(raw);
585
+ return {
586
+ map: new import_node_module.SourceMap(payload),
587
+ baseDir: (0, import_node_path.resolve)(mapDir, payload.sourceRoot ?? ""),
588
+ sources: payload.sources ?? [],
589
+ contents: payload.sourcesContent ?? [],
590
+ lines: /* @__PURE__ */ new Map()
591
+ };
592
+ } catch {
593
+ return null;
594
+ }
595
+ }
596
+
597
+ // src/stack.ts
598
+ var import_node_url2 = require("url");
599
+ var WITH_FUNCTION = /^at (?:async )?(.+?) \((.+)\)$/;
600
+ var LOCATION = /^(.+):(\d+):(\d+)$/;
601
+ function parseStack(stack) {
602
+ if (!stack) return [];
603
+ const frames = [];
604
+ for (const raw of stack.split("\n")) {
605
+ const line = raw.trim();
606
+ if (!line.startsWith("at ")) continue;
607
+ const withFunction = WITH_FUNCTION.exec(line);
608
+ const fn = withFunction?.[1];
609
+ const location = withFunction ? withFunction[2] ?? "" : line.replace(/^at (?:async )?/, "");
610
+ const parsed = LOCATION.exec(location);
611
+ if (parsed) {
612
+ const file = toPath(parsed[1] ?? "");
613
+ frames.push({ ...fn ? { fn } : {}, file, line: Number(parsed[2]), col: Number(parsed[3]), inApp: isInApp(file) });
614
+ } else {
615
+ frames.push({ ...fn ? { fn } : {}, file: location, inApp: false });
616
+ }
617
+ if (frames.length >= LIMITS.frames) break;
618
+ }
619
+ return frames;
620
+ }
621
+ function isInApp(file) {
622
+ if (file.includes("/node_modules/") || file.includes("\\node_modules\\")) return false;
623
+ if (file.startsWith("node:") || file.startsWith("internal/")) return false;
624
+ return file.startsWith("/") || /^[A-Za-z]:[\\/]/.test(file);
625
+ }
626
+ function exceptionInfo(error, depth = 0) {
627
+ if (error instanceof Error) {
628
+ const info = {
629
+ type: errorType(error),
630
+ message: truncate(error.message ?? "", LIMITS.messageLength),
631
+ frames: parseStack(error.stack)
632
+ };
633
+ const cause = error.cause;
634
+ if (cause !== void 0 && cause !== null && depth < LIMITS.causeDepth) info.cause = exceptionInfo(cause, depth + 1);
635
+ return info;
636
+ }
637
+ if (typeof error === "object" && error !== null) {
638
+ const record = error;
639
+ const message = typeof record.message === "string" ? record.message : safeStringify(error);
640
+ return { type: typeof record.name === "string" ? record.name : "Object", message: truncate(message, LIMITS.messageLength), frames: [] };
641
+ }
642
+ return { type: "Error", message: truncate(String(error), LIMITS.messageLength), frames: [] };
643
+ }
644
+ function toPath(location) {
645
+ if (!location.startsWith("file://")) return location;
646
+ try {
647
+ return (0, import_node_url2.fileURLToPath)(location);
648
+ } catch {
649
+ return location;
650
+ }
651
+ }
652
+ function errorType(error) {
653
+ if (error.name && error.name !== "Error") return error.name;
654
+ const constructorName = error.constructor?.name;
655
+ return constructorName && constructorName !== "Object" ? constructorName : "Error";
656
+ }
657
+ function safeStringify(value) {
658
+ try {
659
+ return JSON.stringify(value) ?? String(value);
660
+ } catch {
661
+ return String(value);
662
+ }
663
+ }
664
+
665
+ // src/transport.ts
666
+ var import_node_util2 = require("util");
667
+ var import_node_zlib = require("zlib");
668
+
669
+ // src/util.ts
670
+ var import_node_perf_hooks2 = require("perf_hooks");
671
+ function describeError(error) {
672
+ return error instanceof Error ? error.message : String(error);
673
+ }
674
+ function elapsedSince(startedAt) {
675
+ return Math.round(import_node_perf_hooks2.performance.now() - startedAt);
676
+ }
677
+
678
+ // src/transport.ts
679
+ var gzipAsync = (0, import_node_util2.promisify)(import_node_zlib.gzip);
680
+ var MAX_BATCH_BYTES = 9e5;
681
+ var GZIP_THRESHOLD_BYTES = 8192;
682
+ var MAX_BACKOFF_MS = 3e4;
683
+ var MAX_RETRY_AFTER_MS = 864e5;
684
+ var UNAUTHORIZED_PAUSE_MS = 10 * 6e4;
685
+ var Transport = class {
686
+ constructor(config) {
687
+ this.config = config;
688
+ this.maxQueue = config.maxQueue ?? 1e3;
689
+ this.batchSize = Math.min(config.batchSize ?? 100, LIMITS.batchEvents);
690
+ this.timer = setInterval(() => {
691
+ void this.send();
692
+ }, config.flushIntervalMs ?? 2e3);
693
+ this.timer.unref();
694
+ }
695
+ config;
696
+ queue = [];
697
+ droppedCount = 0;
698
+ inFlight = null;
699
+ nextAttemptAt = 0;
700
+ backoffMs = 0;
701
+ stopped = false;
702
+ warned = /* @__PURE__ */ new Set();
703
+ timer;
704
+ maxQueue;
705
+ batchSize;
706
+ get pending() {
707
+ return this.queue.length;
708
+ }
709
+ get dropped() {
710
+ return this.droppedCount;
711
+ }
712
+ enqueue(event) {
713
+ if (this.stopped) return;
714
+ if (this.queue.length >= this.maxQueue) {
715
+ this.droppedCount += 1;
716
+ this.warn("queue_full", "fila do Radar cheia; eventos est\xE3o sendo descartados", false);
717
+ return;
718
+ }
719
+ this.queue.push(event);
720
+ if (this.queue.length >= this.batchSize) void this.send();
721
+ }
722
+ send() {
723
+ if (this.inFlight) return this.inFlight;
724
+ if (this.stopped || this.queue.length === 0 || Date.now() < this.nextAttemptAt) return Promise.resolve(false);
725
+ const attempt = this.sendBatch().catch((error) => {
726
+ this.warn("internal", `envio ao Radar falhou: ${describeError(error)}`, false);
727
+ return false;
728
+ });
729
+ this.inFlight = attempt.finally(() => {
730
+ this.inFlight = null;
731
+ });
732
+ return this.inFlight;
733
+ }
734
+ async flush(timeoutMs) {
735
+ const deadline = Date.now() + timeoutMs;
736
+ while (this.queue.length > 0 || this.inFlight) {
737
+ const remaining = deadline - Date.now();
738
+ if (remaining <= 0) return;
739
+ const progressed = await withTimeout(this.send(), remaining);
740
+ if (progressed !== true && !this.inFlight) return;
741
+ }
742
+ }
743
+ stop() {
744
+ this.stopped = true;
745
+ clearInterval(this.timer);
746
+ }
747
+ async sendBatch() {
748
+ const items = this.queue.splice(0, this.batchSize);
749
+ const resolved = await Promise.all(items.map((item) => Promise.resolve().then(() => typeof item === "function" ? item() : item).then((event) => event, () => null)));
750
+ const events = [];
751
+ const leftovers = [];
752
+ let bytes = 0;
753
+ for (const event of resolved) {
754
+ if (!event) {
755
+ this.droppedCount += 1;
756
+ continue;
757
+ }
758
+ const size = Buffer.byteLength(JSON.stringify(event));
759
+ if (events.length > 0 && bytes + size > MAX_BATCH_BYTES) {
760
+ leftovers.push(event);
761
+ continue;
762
+ }
763
+ events.push(event);
764
+ bytes += size;
765
+ }
766
+ if (leftovers.length > 0) this.queue.unshift(...leftovers);
767
+ if (events.length === 0) return true;
768
+ const dropped = this.droppedCount;
769
+ const batch = {
770
+ v: 1,
771
+ sdk: this.config.sdk,
772
+ environment: this.config.environment,
773
+ ...this.config.release ? { release: this.config.release } : {},
774
+ ...dropped > 0 ? { dropped } : {},
775
+ events
776
+ };
777
+ const outcome = await this.post(batch);
778
+ switch (outcome.kind) {
779
+ case "ok":
780
+ this.droppedCount -= dropped;
781
+ this.backoffMs = 0;
782
+ this.nextAttemptAt = 0;
783
+ return true;
784
+ case "retry":
785
+ this.requeue(events);
786
+ this.backoffMs = Math.min(this.backoffMs > 0 ? this.backoffMs * 2 : 1e3, MAX_BACKOFF_MS);
787
+ this.nextAttemptAt = Date.now() + Math.max(this.backoffMs, outcome.retryAfterMs ?? 0);
788
+ return false;
789
+ case "unauthorized":
790
+ this.droppedCount += events.length + this.queue.length;
791
+ this.queue = [];
792
+ this.nextAttemptAt = Date.now() + UNAUTHORIZED_PAUSE_MS;
793
+ this.warn("unauthorized", `chave do Radar recusada (${outcome.status}); envios pausados por 10 minutos`, true);
794
+ return false;
795
+ case "rejected":
796
+ this.droppedCount += events.length;
797
+ this.warn(`rejected_${outcome.status}`, `lote recusado pelo Radar (${outcome.status}); eventos descartados`, true);
798
+ return true;
799
+ }
800
+ }
801
+ requeue(events) {
802
+ this.queue.unshift(...events);
803
+ const overflow = this.queue.length - this.maxQueue;
804
+ if (overflow > 0) {
805
+ this.queue.splice(this.maxQueue, overflow);
806
+ this.droppedCount += overflow;
807
+ }
808
+ }
809
+ async post(batch) {
810
+ const json = JSON.stringify(batch);
811
+ const compress = Buffer.byteLength(json) > GZIP_THRESHOLD_BYTES;
812
+ const headers = {
813
+ "content-type": "application/json",
814
+ authorization: `Bearer ${this.config.key}`,
815
+ "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
816
+ };
817
+ if (compress) headers["content-encoding"] = "gzip";
818
+ let response;
819
+ try {
820
+ response = await fetch(this.config.url, {
821
+ method: "POST",
822
+ headers,
823
+ body: compress ? await gzipAsync(json) : json,
824
+ redirect: "error",
825
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? 5e3)
826
+ });
827
+ } catch (error) {
828
+ this.warn("network", `Radar indispon\xEDvel: ${describeError(error)}`, false);
829
+ return { kind: "retry" };
830
+ }
831
+ await response.arrayBuffer().catch(() => void 0);
832
+ if (response.ok) return { kind: "ok" };
833
+ if (response.status === 401 || response.status === 403) return { kind: "unauthorized", status: response.status };
834
+ if (response.status === 429) return { kind: "retry", retryAfterMs: retryAfterMs(response.headers.get("retry-after")) };
835
+ if (response.status >= 500) return { kind: "retry" };
836
+ return { kind: "rejected", status: response.status };
837
+ }
838
+ warn(key, message, always) {
839
+ if (this.warned.has(key)) return;
840
+ this.warned.add(key);
841
+ this.config.onWarning?.(message, always);
842
+ }
843
+ };
844
+ function retryAfterMs(header) {
845
+ if (!header) return void 0;
846
+ const seconds = Number(header);
847
+ if (Number.isFinite(seconds)) return Math.min(Math.max(0, seconds * 1e3), MAX_RETRY_AFTER_MS);
848
+ const date = Date.parse(header);
849
+ return Number.isNaN(date) ? void 0 : Math.min(Math.max(0, date - Date.now()), MAX_RETRY_AFTER_MS);
850
+ }
851
+ function withTimeout(promise, ms) {
852
+ return new Promise((resolve2) => {
853
+ const timer = setTimeout(() => resolve2(void 0), ms);
854
+ timer.unref();
855
+ promise.then(
856
+ (value) => {
857
+ clearTimeout(timer);
858
+ resolve2(value);
859
+ },
860
+ () => {
861
+ clearTimeout(timer);
862
+ resolve2(void 0);
863
+ }
864
+ );
865
+ });
866
+ }
867
+
868
+ // src/version.ts
869
+ var SDK_NAME = "@oconde/radar";
870
+ var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
871
+
872
+ // src/client.ts
873
+ var RadarClient = class {
874
+ options = resolveOptions({}, {});
875
+ transport = null;
876
+ initialized = false;
877
+ closed = false;
878
+ captured = /* @__PURE__ */ new WeakSet();
879
+ get isEnabled() {
880
+ return this.transport !== null && !this.closed;
881
+ }
882
+ get settings() {
883
+ return this.options;
884
+ }
885
+ init(options = {}) {
886
+ try {
887
+ if (this.initialized) {
888
+ this.debugWarn("radar.init chamado mais de uma vez; mantida a primeira configura\xE7\xE3o");
889
+ return;
890
+ }
891
+ this.initialized = true;
892
+ this.options = resolveOptions(options);
893
+ setFingerprintSecret(this.options.key);
894
+ if (this.options.enabled && this.options.key) {
895
+ this.transport = new Transport({
896
+ url: this.options.endpoint + INGEST_PATH,
897
+ key: this.options.key,
898
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
899
+ environment: this.options.environment,
900
+ ...this.options.release ? { release: this.options.release } : {},
901
+ onWarning: (message, always) => always ? this.writeWarning(message) : this.debugWarn(message)
902
+ });
903
+ }
904
+ if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
905
+ } catch (error) {
906
+ this.debugWarn(`radar.init falhou: ${describeError(error)}`);
907
+ }
908
+ }
909
+ debug(message, data) {
910
+ this.safely(() => this.emitLog("debug", message, data));
911
+ }
912
+ info(message, data) {
913
+ this.safely(() => this.emitLog("info", message, data));
914
+ }
915
+ warn(message, data) {
916
+ this.safely(() => this.emitLog("warn", message, data));
917
+ }
918
+ error(message, data) {
919
+ this.safely(() => this.emitLog("error", message, data));
920
+ }
921
+ logRequest(title, data, options = {}) {
922
+ this.safely(() => {
923
+ const context = currentContext();
924
+ const mode = options.redact ?? this.options.redact;
925
+ const request = context?.req ? requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) : void 0;
926
+ this.emitLog(options.level ?? "info", title, data, { request, mode });
927
+ });
928
+ }
929
+ captureError(error, data) {
930
+ this.safely(() => this.emitError(error, data, "error", true));
931
+ }
932
+ captureUnhandled(error, level) {
933
+ this.safely(() => this.emitError(error, void 0, level, false));
934
+ }
935
+ setUser(user) {
936
+ this.safely(() => {
937
+ const context = currentContext();
938
+ if (!context) return;
939
+ context.user = {
940
+ ...user.id !== void 0 && user.id !== null ? { id: truncate(String(user.id), 200) } : {},
941
+ ...user.email ? { email: truncate(String(user.email), 200) } : {},
942
+ ...user.name ? { name: truncate(String(user.name), 200) } : {}
943
+ };
944
+ });
945
+ }
946
+ async track(name, fn, data) {
947
+ const execute = async () => {
948
+ const started = import_node_perf_hooks3.performance.now();
949
+ try {
950
+ const result = await fn();
951
+ this.info(name, { ...data, durationMs: elapsedSince(started), ok: true });
952
+ return result;
953
+ } catch (error) {
954
+ this.error(name, { ...data, durationMs: elapsedSince(started), ok: false });
955
+ this.captureError(error, data);
956
+ throw error;
957
+ }
958
+ };
959
+ if (currentContext()) return execute();
960
+ return runWithContext({ requestId: (0, import_node_crypto3.randomUUID)(), startedAt: import_node_perf_hooks3.performance.now() }, execute);
961
+ }
962
+ middleware() {
963
+ return createMiddleware(this);
964
+ }
965
+ shouldLogRequest(req) {
966
+ return this.options.logRequests && !this.options.ignorePaths.includes(requestPath(req));
967
+ }
968
+ logHttpRequest(context, status) {
969
+ this.safely(() => {
970
+ const req = context.req;
971
+ if (!req) return;
972
+ const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
973
+ const route = requestRoute(req, context.route);
974
+ runWithContext(
975
+ context,
976
+ () => this.emitLog(level, "http.request", {
977
+ method: (req.method ?? "GET").toUpperCase(),
978
+ ...route ? { route } : {},
979
+ ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), req.params, this.options.redact) } : {},
980
+ status,
981
+ durationMs: elapsedSince(context.startedAt)
982
+ })
983
+ );
984
+ });
985
+ }
986
+ async flush(timeoutMs = 2e3) {
987
+ try {
988
+ await this.transport?.flush(timeoutMs);
989
+ } catch (error) {
990
+ this.debugWarn(`flush falhou: ${describeError(error)}`);
991
+ }
992
+ }
993
+ async close(timeoutMs = 2e3) {
994
+ if (this.closed) return;
995
+ await this.flush(timeoutMs);
996
+ this.closed = true;
997
+ this.transport?.stop();
998
+ }
999
+ emitLog(level, message, data, extras = {}) {
1000
+ if (this.closed || !levelEnabled(level, this.options.minLevel)) return;
1001
+ const context = currentContext();
1002
+ const mode = extras.mode ?? this.options.redact;
1003
+ const event = {
1004
+ type: "log",
1005
+ ts: Date.now(),
1006
+ level,
1007
+ message: truncate(String(message), LIMITS.messageLength),
1008
+ ...context ? { requestId: context.requestId } : {},
1009
+ ...data !== void 0 ? { attrs: prepareAttrs(data, mode) } : {},
1010
+ ...extras.request ? { request: extras.request } : {},
1011
+ ...context?.user ? { user: context.user } : {}
1012
+ };
1013
+ this.print(event);
1014
+ this.transport?.enqueue(event);
1015
+ }
1016
+ emitError(error, data, level, handled) {
1017
+ if (this.closed) return;
1018
+ if (typeof error === "object" && error !== null) {
1019
+ if (this.captured.has(error)) return;
1020
+ this.captured.add(error);
1021
+ }
1022
+ const context = currentContext();
1023
+ const mode = this.options.redact;
1024
+ const event = {
1025
+ type: "error",
1026
+ ts: Date.now(),
1027
+ level,
1028
+ handled,
1029
+ exception: exceptionInfo(error),
1030
+ ...context ? { requestId: context.requestId } : {},
1031
+ ...context?.req ? { request: requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) } : {},
1032
+ ...context?.user ? { user: context.user } : {},
1033
+ runtime: runtimeInfo(),
1034
+ ...data !== void 0 ? { attrs: prepareAttrs(data, mode) } : {}
1035
+ };
1036
+ this.print(event, error instanceof Error ? error.stack : void 0);
1037
+ if (!this.transport) return;
1038
+ this.transport.enqueue(
1039
+ () => enrichException(event.exception).then(
1040
+ (exception) => ({ ...event, exception }),
1041
+ () => event
1042
+ )
1043
+ );
1044
+ }
1045
+ print(event, stack) {
1046
+ if (!this.options.console) return;
1047
+ try {
1048
+ const requestId = event.requestId ? { requestId: event.requestId } : {};
1049
+ if (event.type === "log") {
1050
+ const line2 = { ...event.attrs, ts: new Date(event.ts).toISOString(), level: event.level, message: event.message, ...requestId };
1051
+ (event.level === "error" ? process.stderr : process.stdout).write(`${JSON.stringify(line2)}
1052
+ `);
1053
+ return;
1054
+ }
1055
+ const line = {
1056
+ ...event.attrs,
1057
+ ts: new Date(event.ts).toISOString(),
1058
+ level: event.level,
1059
+ message: `${event.exception.type}: ${event.exception.message}`,
1060
+ ...requestId,
1061
+ ...stack ? { stack } : {}
1062
+ };
1063
+ process.stderr.write(`${JSON.stringify(line)}
1064
+ `);
1065
+ } catch {
1066
+ return;
1067
+ }
1068
+ }
1069
+ safely(action) {
1070
+ try {
1071
+ action();
1072
+ } catch (error) {
1073
+ this.debugWarn(`falha interna do Radar: ${describeError(error)}`);
1074
+ }
1075
+ }
1076
+ debugWarn(message) {
1077
+ if (this.options.debug) this.writeWarning(message);
1078
+ }
1079
+ writeWarning(message) {
1080
+ try {
1081
+ process.stderr.write(`[radar] ${message}
1082
+ `);
1083
+ } catch {
1084
+ return;
1085
+ }
1086
+ }
1087
+ };
1088
+ function runtimeInfo() {
1089
+ return {
1090
+ name: "node",
1091
+ version: process.versions.node,
1092
+ host: (0, import_node_os.hostname)(),
1093
+ pid: process.pid,
1094
+ memoryMb: Math.round(process.memoryUsage.rss() / 1048576),
1095
+ uptimeS: Math.round(process.uptime())
1096
+ };
1097
+ }
1098
+
1099
+ // src/radar.ts
1100
+ var state = globalState();
1101
+ var radar = state.client ?? (state.client = new RadarClient());
1102
+ // Annotate the CommonJS export names for ESM import in node:
1103
+ 0 && (module.exports = {
1104
+ RadarClient,
1105
+ radar
1106
+ });
1107
+ //# sourceMappingURL=index.cjs.map