@oconde/radar 0.1.0 → 0.3.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 CHANGED
@@ -32,29 +32,13 @@ var import_node_perf_hooks3 = require("perf_hooks");
32
32
 
33
33
  // src/context.ts
34
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");
35
+ var import_node_crypto = require("crypto");
55
36
 
56
37
  // src/protocol/index.ts
57
38
  var INGEST_PATH = "/api/v1/events";
39
+ var CHECKIN_PATH = "/api/v1/checkins/";
40
+ var CHECKIN_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
41
+ var CHECKIN_STATUSES = ["in_progress", "ok", "error"];
58
42
  var LIMITS = {
59
43
  batchBytes: 1e6,
60
44
  batchEvents: 500,
@@ -70,11 +54,38 @@ var LIMITS = {
70
54
  causeDepth: 5,
71
55
  depth: 6,
72
56
  arrayItems: 50,
73
- requestIdLength: 128
57
+ requestIdLength: 128,
58
+ checkInIdLength: 64
74
59
  };
75
60
 
61
+ // src/context.ts
62
+ var STATE_KEY = /* @__PURE__ */ Symbol.for("@oconde/radar");
63
+ function globalState() {
64
+ const holder = globalThis;
65
+ let state2 = holder[STATE_KEY];
66
+ if (!state2) {
67
+ state2 = { storage: new import_node_async_hooks.AsyncLocalStorage() };
68
+ holder[STATE_KEY] = state2;
69
+ }
70
+ return state2;
71
+ }
72
+ function currentContext() {
73
+ return globalState().storage.getStore();
74
+ }
75
+ function runWithContext(context, fn) {
76
+ return globalState().storage.run(context, fn);
77
+ }
78
+ var SAFE_REQUEST_ID = /^[\x21-\x7e]+$/;
79
+ function requestIdFrom(incoming) {
80
+ const value = incoming?.trim();
81
+ return value && value.length <= LIMITS.requestIdLength && SAFE_REQUEST_ID.test(value) ? value : (0, import_node_crypto.randomUUID)();
82
+ }
83
+
84
+ // src/express.ts
85
+ var import_node_perf_hooks = require("perf_hooks");
86
+
76
87
  // src/redact.ts
77
- var import_node_crypto = require("crypto");
88
+ var import_node_crypto2 = require("crypto");
78
89
  var SECRET_WORDS = /* @__PURE__ */ new Set([
79
90
  "pass",
80
91
  "passwd",
@@ -120,16 +131,16 @@ function fingerprintSlot() {
120
131
  const holder = globalThis;
121
132
  let slot = holder[KEY_SLOT];
122
133
  if (!slot) {
123
- slot = { key: (0, import_node_crypto.randomBytes)(32) };
134
+ slot = { key: (0, import_node_crypto2.randomBytes)(32) };
124
135
  holder[KEY_SLOT] = slot;
125
136
  }
126
137
  return slot;
127
138
  }
128
139
  function setFingerprintSecret(secret) {
129
- fingerprintSlot().key = secret ? (0, import_node_crypto.createHash)("sha256").update(`@oconde/radar:${secret}`).digest() : (0, import_node_crypto.randomBytes)(32);
140
+ fingerprintSlot().key = secret ? (0, import_node_crypto2.createHash)("sha256").update(`@oconde/radar:${secret}`).digest() : (0, import_node_crypto2.randomBytes)(32);
130
141
  }
131
142
  function fingerprint(value) {
132
- return (0, import_node_crypto.createHmac)("sha256", fingerprintSlot().key).update(value).digest("hex").slice(0, 8);
143
+ return (0, import_node_crypto2.createHmac)("sha256", fingerprintSlot().key).update(value).digest("hex").slice(0, 8);
133
144
  }
134
145
  function words(name) {
135
146
  return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
@@ -271,7 +282,44 @@ function requestPath(req) {
271
282
  }
272
283
  function requestRoute(req, route) {
273
284
  if (route) return route;
274
- return typeof req.route?.path === "string" ? req.route.path : void 0;
285
+ if (typeof req.route?.path !== "string") return void 0;
286
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
287
+ if (!base) return req.route.path;
288
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
289
+ }
290
+ var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
291
+ function pathSegments(value) {
292
+ const segments = value.split("/");
293
+ while (segments.length > 1 && segments[segments.length - 1] === "") segments.pop();
294
+ return segments;
295
+ }
296
+ function decodeSegment(value) {
297
+ try {
298
+ return decodeURIComponent(value);
299
+ } catch {
300
+ return value;
301
+ }
302
+ }
303
+ function routeParams(req, path) {
304
+ if (typeof req.route?.path !== "string") return {};
305
+ const pattern = pathSegments(`${typeof req.baseUrl === "string" ? req.baseUrl : ""}${req.route.path}`).filter((segment, index) => index > 0 || segment !== "");
306
+ const values = pathSegments(path);
307
+ if (pattern.length > values.length) return {};
308
+ const offset = values.length - pattern.length;
309
+ const result = {};
310
+ for (let index = 0; index < pattern.length; index++) {
311
+ const value = values[offset + index];
312
+ const param = ROUTE_PARAM.exec(pattern[index]);
313
+ if (!param) {
314
+ if (pattern[index] !== value) return {};
315
+ continue;
316
+ }
317
+ if (value) result[param[1]] = decodeSegment(value);
318
+ }
319
+ return result;
320
+ }
321
+ function pathParams(req, path) {
322
+ return { ...routeParams(req, path), ...req.params ?? {} };
275
323
  }
276
324
  function redactPath(path, params, mode) {
277
325
  if (mode === "none" || !params) return path;
@@ -288,7 +336,7 @@ function safeUrl(req, mode) {
288
336
  const queryStart = url.indexOf("?");
289
337
  const path = queryStart >= 0 ? url.slice(0, queryStart) : url;
290
338
  const query = queryStart >= 0 ? url.slice(queryStart) : "";
291
- return redactUrl(redactPath(path, req.params, mode) + query, mode);
339
+ return redactUrl(redactPath(path, pathParams(req, path), mode) + query, mode);
292
340
  }
293
341
  function redactUrl(url, mode) {
294
342
  const queryStart = url.indexOf("?");
@@ -356,8 +404,7 @@ function createMiddleware(logger) {
356
404
  return (req, res, next) => {
357
405
  let context;
358
406
  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)();
407
+ const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
361
408
  context = { requestId, startedAt: import_node_perf_hooks.performance.now(), req };
362
409
  res.setHeader("x-request-id", requestId);
363
410
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
@@ -372,6 +419,12 @@ function createMiddleware(logger) {
372
419
  // src/options.ts
373
420
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
374
421
  var LEVELS = ["debug", "info", "warn", "error"];
422
+ function resolveTraceFetch(value) {
423
+ if (!value) return { enabled: false, propagateTo: [] };
424
+ if (value === true) return { enabled: true, propagateTo: [] };
425
+ const targets2 = Array.isArray(value.propagateTo) ? value.propagateTo : [];
426
+ return { enabled: true, propagateTo: targets2.filter((target) => typeof target === "string" ? target.trim() !== "" : target instanceof RegExp) };
427
+ }
375
428
  function resolveOptions(options = {}, env = process.env) {
376
429
  const key = options.key?.trim() || void 0;
377
430
  return {
@@ -387,6 +440,7 @@ function resolveOptions(options = {}, env = process.env) {
387
440
  logRequests: options.logRequests ?? true,
388
441
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
389
442
  captureUnhandled: options.captureUnhandled ?? true,
443
+ traceFetch: resolveTraceFetch(options.traceFetch),
390
444
  debug: options.debug ?? false
391
445
  };
392
446
  }
@@ -448,16 +502,144 @@ var import_promises = require("fs/promises");
448
502
  var import_node_module = require("module");
449
503
  var import_node_path = require("path");
450
504
  var import_node_url = require("url");
505
+
506
+ // src/function-name.ts
507
+ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
508
+ var MAX_LINES_UP = 400;
509
+ var MAX_HEADER_LINES = 6;
510
+ var MAX_NAME_LENGTH = 100;
511
+ var MAX_COMMENT_LINES = 40;
512
+ var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
513
+ var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
514
+ var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
515
+ var KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "await", "else", "do", "try"]);
516
+ var NAME_PATTERNS = [
517
+ new RegExp(`\\bfunction\\s*\\*?\\s*(${IDENTIFIER})\\s*\\(`),
518
+ new RegExp(`(${IDENTIFIER})\\s*[:=]\\s*(?:async\\s+)?function\\b`),
519
+ new RegExp(`(${IDENTIFIER})\\s*(?::\\s*[^=]+)?=\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`),
520
+ new RegExp(`(?:^|[{,(]\\s*)(${IDENTIFIER})\\s*:\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`)
521
+ ];
522
+ var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
523
+ var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
524
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
525
+ function lineCommentAt(text, from) {
526
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
527
+ if (index === 0 || text[index - 1] !== ":") return index;
528
+ }
529
+ return -1;
530
+ }
531
+ function blockCommentAt(text, from) {
532
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
533
+ if (index === 0 || text[index - 1] !== "\\") return index;
534
+ }
535
+ return -1;
536
+ }
537
+ function stripComments(text, insideComment, closesLater) {
538
+ let code = "";
539
+ let position = 0;
540
+ let inside = insideComment;
541
+ while (position < text.length) {
542
+ if (inside) {
543
+ const end = text.indexOf("*/", position);
544
+ if (end < 0) return { code, insideComment: true };
545
+ inside = false;
546
+ position = end + 2;
547
+ continue;
548
+ }
549
+ const block = blockCommentAt(text, position);
550
+ const line = lineCommentAt(text, position);
551
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
552
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
553
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
554
+ code += text.slice(position, block + 2);
555
+ position = block + 2;
556
+ continue;
557
+ }
558
+ code += text.slice(position, block);
559
+ inside = true;
560
+ position = block + 2;
561
+ }
562
+ return { code, insideComment: inside };
563
+ }
564
+ function codeLines(lines, from, start, column) {
565
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
566
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
567
+ const result = [];
568
+ let insideComment = false;
569
+ for (let index = 0; index < plain.length; index++) {
570
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
571
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
572
+ insideComment = stripped.insideComment;
573
+ result.push(stripped.code);
574
+ }
575
+ return result;
576
+ }
577
+ function parenBalance(text) {
578
+ let balance = 0;
579
+ for (const char of text) {
580
+ if (char === "(") balance += 1;
581
+ else if (char === ")") balance -= 1;
582
+ }
583
+ return balance;
584
+ }
585
+ function headerOf(code, index, prefix) {
586
+ let header = prefix;
587
+ for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
588
+ if (header.trim() !== "" && parenBalance(header) >= 0) break;
589
+ header = `${code[previous] ?? ""} ${header}`;
590
+ }
591
+ return header;
592
+ }
593
+ function nameIn(header) {
594
+ const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
595
+ for (const pattern of patterns) {
596
+ const name = pattern.exec(header)?.[1];
597
+ if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
598
+ }
599
+ return void 0;
600
+ }
601
+ function enclosingFunctionName(lines, line, column) {
602
+ const start = line - 1;
603
+ if (start < 0 || start >= lines.length) return void 0;
604
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
605
+ const cleaned = codeLines(lines, from, start, column);
606
+ let depth = 0;
607
+ for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
608
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
609
+ const code = cleaned[index - from] ?? "";
610
+ for (let position = code.length - 1; position >= 0; position--) {
611
+ const char = code[position];
612
+ if (char === "}") {
613
+ depth += 1;
614
+ } else if (char === "{") {
615
+ if (depth > 0) {
616
+ depth -= 1;
617
+ continue;
618
+ }
619
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
620
+ if (CLASS_BODY.test(header)) return void 0;
621
+ if (CONTROL_BLOCK.test(header)) continue;
622
+ const name = nameIn(header);
623
+ if (name) return name;
624
+ }
625
+ }
626
+ }
627
+ return void 0;
628
+ }
629
+
630
+ // src/source.ts
451
631
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
452
632
  var MAX_CACHED_FILES = 50;
453
633
  var MAPPING_URL = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
454
634
  var cache = /* @__PURE__ */ new Map();
455
- async function enrichFrames(frames, cwd = process.cwd()) {
635
+ async function enrichFrames(frames, cwd = process.cwd(), options = {}) {
456
636
  const result = [];
457
637
  let withContext = 0;
458
638
  for (const frame of frames) {
459
639
  try {
460
- const { frame: mapped, lines } = await mapFrame(frame);
640
+ const { frame: located, lines, original } = await mapFrame(frame, options.mapLocator);
641
+ const named = options.inferFunctionNames && original && !located.fn && lines && located.line ? withInferredName(located, lines) : located;
642
+ const mapped = named.inApp && isLibraryPath(named.file) ? { ...named, inApp: false } : named;
461
643
  let next = mapped;
462
644
  if (mapped.inApp && lines && mapped.line && withContext < LIMITS.contextFrames) {
463
645
  const context = contextFor(lines, mapped.line);
@@ -473,29 +655,36 @@ async function enrichFrames(frames, cwd = process.cwd()) {
473
655
  }
474
656
  return result;
475
657
  }
476
- async function enrichException(exception, cwd = process.cwd()) {
477
- const frames = await enrichFrames(exception.frames, cwd);
658
+ async function enrichException(exception, cwd = process.cwd(), options = {}) {
659
+ const frames = await enrichFrames(exception.frames, cwd, options);
478
660
  if (!exception.cause) return { ...exception, frames };
479
- return { ...exception, frames, cause: await enrichException(exception.cause, cwd) };
661
+ return { ...exception, frames, cause: await enrichException(exception.cause, cwd, options) };
662
+ }
663
+ function withInferredName(frame, lines) {
664
+ const fn = enclosingFunctionName(lines, frame.line ?? 0, frame.col);
665
+ return fn ? { ...frame, fn } : frame;
666
+ }
667
+ function isLibraryPath(file) {
668
+ return file.includes("/node_modules/") || file.includes("\\node_modules\\");
480
669
  }
481
670
  function relativize(file, cwd) {
482
671
  if (!(0, import_node_path.isAbsolute)(file)) return file;
483
672
  const path = (0, import_node_path.relative)(cwd, file);
484
673
  return path.startsWith("..") || (0, import_node_path.isAbsolute)(path) ? file : path.split(import_node_path.sep).join("/");
485
674
  }
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 };
675
+ async function mapFrame(frame, locator) {
676
+ if (!frame.line || !(0, import_node_path.isAbsolute)(frame.file)) return { frame, lines: null, original: false };
677
+ const file = await loadFile(frame.file, locator);
678
+ if (!file) return { frame, lines: null, original: false };
679
+ if (!file.map) return { frame, lines: file.lines, original: false };
491
680
  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 };
681
+ if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines, original: false };
493
682
  const originalPath = resolveSource(file.map, entry.originalSource);
494
683
  const mapped = { ...frame, file: originalPath, line: entry.originalLine + 1, col: (entry.originalColumn ?? 0) + 1 };
495
684
  const lines = embeddedLines(file.map, file.map.sources.indexOf(entry.originalSource));
496
- if (lines) return { frame: mapped, lines };
685
+ if (lines) return { frame: mapped, lines, original: true };
497
686
  const original = await loadFile(originalPath);
498
- return { frame: mapped, lines: original?.lines ?? null };
687
+ return { frame: mapped, lines: original?.lines ?? null, original: true };
499
688
  }
500
689
  function embeddedLines(map, index) {
501
690
  if (index < 0) return null;
@@ -527,26 +716,27 @@ function resolveSource(map, source) {
527
716
  }
528
717
  return (0, import_node_path.isAbsolute)(source) ? source : (0, import_node_path.resolve)(map.baseDir, source);
529
718
  }
530
- function loadFile(path) {
531
- const cached = cache.get(path);
719
+ function loadFile(path, locator) {
720
+ const key = locator ? `${path}\0located` : path;
721
+ const cached = cache.get(key);
532
722
  if (cached) {
533
- cache.delete(path);
534
- cache.set(path, cached);
723
+ cache.delete(key);
724
+ cache.set(key, cached);
535
725
  return cached;
536
726
  }
537
- const pending = readSource(path).catch(() => null);
538
- cache.set(path, pending);
727
+ const pending = readSource(path, locator).catch(() => null);
728
+ cache.set(key, pending);
539
729
  if (cache.size > MAX_CACHED_FILES) {
540
730
  const oldest = cache.keys().next().value;
541
731
  if (oldest !== void 0) cache.delete(oldest);
542
732
  }
543
733
  return pending;
544
734
  }
545
- async function readSource(path) {
735
+ async function readSource(path, locator) {
546
736
  const text = await readText(path);
547
737
  if (text === null) return null;
548
738
  const lines = text.split(/\r?\n/);
549
- return { lines, map: await loadMap(path, lines) };
739
+ return { lines, map: await loadMap(path, lines, locator) };
550
740
  }
551
741
  async function readText(path) {
552
742
  try {
@@ -557,7 +747,7 @@ async function readText(path) {
557
747
  return null;
558
748
  }
559
749
  }
560
- async function loadMap(path, lines) {
750
+ async function loadMap(path, lines, locator) {
561
751
  let url;
562
752
  for (let index = lines.length - 1; index >= 0 && index >= lines.length - 5; index--) {
563
753
  const match = MAPPING_URL.exec(lines[index] ?? "");
@@ -566,10 +756,15 @@ async function loadMap(path, lines) {
566
756
  break;
567
757
  }
568
758
  }
569
- if (!url) return null;
570
759
  let raw;
571
760
  let mapDir;
572
- if (url.startsWith("data:")) {
761
+ const located = url ? null : locator ? locator(path) : `${path}.map`;
762
+ if (located) {
763
+ raw = await readText(located);
764
+ mapDir = (0, import_node_path.dirname)(located);
765
+ } else if (!url) {
766
+ return null;
767
+ } else if (url.startsWith("data:")) {
573
768
  const comma = url.indexOf(",");
574
769
  const encoded = url.slice(comma + 1);
575
770
  raw = url.slice(0, comma).endsWith(";base64") ? Buffer.from(encoded, "base64").toString("utf8") : decodeURIComponent(encoded);
@@ -598,12 +793,13 @@ async function loadMap(path, lines) {
598
793
  var import_node_url2 = require("url");
599
794
  var WITH_FUNCTION = /^at (?:async )?(.+?) \((.+)\)$/;
600
795
  var LOCATION = /^(.+):(\d+):(\d+)$/;
796
+ var MAX_FRAME_LINE = 1e3;
601
797
  function parseStack(stack) {
602
798
  if (!stack) return [];
603
799
  const frames = [];
604
800
  for (const raw of stack.split("\n")) {
605
801
  const line = raw.trim();
606
- if (!line.startsWith("at ")) continue;
802
+ if (line.length > MAX_FRAME_LINE || !line.startsWith("at ")) continue;
607
803
  const withFunction = WITH_FUNCTION.exec(line);
608
804
  const fn = withFunction?.[1];
609
805
  const location = withFunction ? withFunction[2] ?? "" : line.replace(/^at (?:async )?/, "");
@@ -662,10 +858,6 @@ function safeStringify(value) {
662
858
  }
663
859
  }
664
860
 
665
- // src/transport.ts
666
- var import_node_util2 = require("util");
667
- var import_node_zlib = require("zlib");
668
-
669
861
  // src/util.ts
670
862
  var import_node_perf_hooks2 = require("perf_hooks");
671
863
  function describeError(error) {
@@ -675,7 +867,101 @@ function elapsedSince(startedAt) {
675
867
  return Math.round(import_node_perf_hooks2.performance.now() - startedAt);
676
868
  }
677
869
 
870
+ // src/trace-fetch.ts
871
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
872
+ var REQUEST_ID_HEADER = "x-request-id";
873
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
874
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
875
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
876
+ function secretSegment(segment) {
877
+ if (UUID.test(segment)) return false;
878
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
879
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
880
+ }
881
+ function outboundPath(pathname, mode) {
882
+ if (mode === "none") return pathname;
883
+ return pathname.split("/").map((segment) => {
884
+ let decoded = segment;
885
+ try {
886
+ decoded = decodeURIComponent(segment);
887
+ } catch {
888
+ decoded = segment;
889
+ }
890
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
891
+ }).join("/");
892
+ }
893
+ function failureOf(error) {
894
+ const message = describeError(error);
895
+ const cause = error instanceof Error ? error.cause : void 0;
896
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
897
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
898
+ }
899
+ function urlOf(input) {
900
+ try {
901
+ if (typeof input === "string") return new URL(input);
902
+ if (input instanceof URL) return input;
903
+ return new URL(input.url);
904
+ } catch {
905
+ return null;
906
+ }
907
+ }
908
+ function originOf(value) {
909
+ try {
910
+ return new URL(value).origin;
911
+ } catch {
912
+ return null;
913
+ }
914
+ }
915
+ function targets(url, target) {
916
+ if (target instanceof RegExp) return target.test(url.host);
917
+ const wanted = target.trim().toLowerCase();
918
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
919
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
920
+ }
921
+ function isPlainObject(value) {
922
+ if (value === null || typeof value !== "object") return false;
923
+ const prototype = Object.getPrototypeOf(value);
924
+ return prototype === Object.prototype || prototype === null;
925
+ }
926
+ function withRequestId(input, init, requestId) {
927
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
928
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
929
+ headers.set(REQUEST_ID_HEADER, requestId);
930
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
931
+ return [new Request(input, init), { headers }];
932
+ }
933
+ function traceFetch(config) {
934
+ const current = globalThis.fetch;
935
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
936
+ const original = current;
937
+ const radarOrigin = originOf(config.endpoint);
938
+ const traced = async (input, init) => {
939
+ const url = urlOf(input);
940
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
941
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
942
+ const context = currentContext();
943
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
944
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
945
+ const started = performance.now();
946
+ try {
947
+ const response = await original(nextInput, nextInit);
948
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
949
+ return response;
950
+ } catch (error) {
951
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
952
+ throw error;
953
+ }
954
+ };
955
+ Object.defineProperty(traced, WRAPPED, { value: original });
956
+ globalThis.fetch = traced;
957
+ return () => {
958
+ if (globalThis.fetch === traced) globalThis.fetch = original;
959
+ };
960
+ }
961
+
678
962
  // src/transport.ts
963
+ var import_node_util2 = require("util");
964
+ var import_node_zlib = require("zlib");
679
965
  var gzipAsync = (0, import_node_util2.promisify)(import_node_zlib.gzip);
680
966
  var MAX_BATCH_BYTES = 9e5;
681
967
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -698,6 +984,7 @@ var Transport = class {
698
984
  inFlight = null;
699
985
  nextAttemptAt = 0;
700
986
  backoffMs = 0;
987
+ unauthorizedUntil = 0;
701
988
  stopped = false;
702
989
  warned = /* @__PURE__ */ new Set();
703
990
  timer;
@@ -787,10 +1074,8 @@ var Transport = class {
787
1074
  this.nextAttemptAt = Date.now() + Math.max(this.backoffMs, outcome.retryAfterMs ?? 0);
788
1075
  return false;
789
1076
  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);
1077
+ this.droppedCount += events.length;
1078
+ this.pauseUnauthorized(outcome.status);
794
1079
  return false;
795
1080
  case "rejected":
796
1081
  this.droppedCount += events.length;
@@ -798,6 +1083,47 @@ var Transport = class {
798
1083
  return true;
799
1084
  }
800
1085
  }
1086
+ async checkIn(slug, payload) {
1087
+ if (this.stopped || !this.config.checkInBaseUrl || Date.now() < this.unauthorizedUntil) return;
1088
+ let response;
1089
+ try {
1090
+ response = await fetch(this.config.checkInBaseUrl + encodeURIComponent(slug), {
1091
+ method: "POST",
1092
+ headers: this.headers(),
1093
+ body: JSON.stringify(payload),
1094
+ redirect: "error",
1095
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? 5e3)
1096
+ });
1097
+ } catch (error) {
1098
+ this.warn("checkin_network", `check-in no Radar falhou: ${describeError(error)}`, false);
1099
+ return;
1100
+ }
1101
+ await response.arrayBuffer().catch(() => void 0);
1102
+ if (response.ok) return;
1103
+ if (response.status === 401 || response.status === 403) {
1104
+ this.pauseUnauthorized(response.status);
1105
+ return;
1106
+ }
1107
+ if (response.status === 404) {
1108
+ this.warn(`checkin_unknown_${slug}`, `tarefa agendada "${slug}" n\xE3o existe no projeto da chave; crie o monitor com esse slug no Radar`, true);
1109
+ return;
1110
+ }
1111
+ this.warn(`checkin_rejected_${response.status}`, `check-in recusado pelo Radar (${response.status})`, false);
1112
+ }
1113
+ pauseUnauthorized(status) {
1114
+ this.droppedCount += this.queue.length;
1115
+ this.queue = [];
1116
+ this.unauthorizedUntil = Date.now() + UNAUTHORIZED_PAUSE_MS;
1117
+ this.nextAttemptAt = Math.max(this.nextAttemptAt, this.unauthorizedUntil);
1118
+ this.warn("unauthorized", `chave do Radar recusada (${status}); envios pausados por 10 minutos`, true);
1119
+ }
1120
+ headers() {
1121
+ return {
1122
+ "content-type": "application/json",
1123
+ authorization: `Bearer ${this.config.key}`,
1124
+ "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
1125
+ };
1126
+ }
801
1127
  requeue(events) {
802
1128
  this.queue.unshift(...events);
803
1129
  const overflow = this.queue.length - this.maxQueue;
@@ -809,11 +1135,7 @@ var Transport = class {
809
1135
  async post(batch) {
810
1136
  const json = JSON.stringify(batch);
811
1137
  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
- };
1138
+ const headers = this.headers();
817
1139
  if (compress) headers["content-encoding"] = "gzip";
818
1140
  let response;
819
1141
  try {
@@ -867,15 +1189,19 @@ function withTimeout(promise, ms) {
867
1189
 
868
1190
  // src/version.ts
869
1191
  var SDK_NAME = "@oconde/radar";
870
- var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
1192
+ var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
871
1193
 
872
1194
  // src/client.ts
1195
+ var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
1196
+ var MAX_CHECK_IN_ENVIRONMENT = 64;
1197
+ var MAX_CHECK_IN_RELEASE = 128;
873
1198
  var RadarClient = class {
874
1199
  options = resolveOptions({}, {});
875
1200
  transport = null;
876
1201
  initialized = false;
877
1202
  closed = false;
878
1203
  captured = /* @__PURE__ */ new WeakSet();
1204
+ restoreFetch = null;
879
1205
  get isEnabled() {
880
1206
  return this.transport !== null && !this.closed;
881
1207
  }
@@ -898,10 +1224,19 @@ var RadarClient = class {
898
1224
  sdk: { name: SDK_NAME, version: SDK_VERSION },
899
1225
  environment: this.options.environment,
900
1226
  ...this.options.release ? { release: this.options.release } : {},
1227
+ checkInBaseUrl: this.options.endpoint + CHECKIN_PATH,
901
1228
  onWarning: (message, always) => always ? this.writeWarning(message) : this.debugWarn(message)
902
1229
  });
903
1230
  }
904
1231
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1232
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1233
+ this.restoreFetch = traceFetch({
1234
+ endpoint: this.options.endpoint,
1235
+ propagateTo: this.options.traceFetch.propagateTo,
1236
+ redact: this.options.redact,
1237
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1238
+ });
1239
+ }
905
1240
  } catch (error) {
906
1241
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
907
1242
  }
@@ -959,6 +1294,51 @@ var RadarClient = class {
959
1294
  if (currentContext()) return execute();
960
1295
  return runWithContext({ requestId: (0, import_node_crypto3.randomUUID)(), startedAt: import_node_perf_hooks3.performance.now() }, execute);
961
1296
  }
1297
+ withContext(fn, options = {}) {
1298
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: import_node_perf_hooks3.performance.now() }, fn);
1299
+ }
1300
+ async checkIn(slug, status, options = {}) {
1301
+ try {
1302
+ if (!this.transport || this.closed) return;
1303
+ if (typeof slug !== "string" || !CHECKIN_SLUG.test(slug)) {
1304
+ this.debugWarn(`slug de tarefa agendada inv\xE1lido: ${String(slug).slice(0, 80)}`);
1305
+ return;
1306
+ }
1307
+ if (!CHECKIN_STATUSES.includes(status)) return;
1308
+ const checkInId = typeof options.checkInId === "string" && options.checkInId ? options.checkInId.slice(0, LIMITS.checkInIdLength) : void 0;
1309
+ const durationMs = typeof options.durationMs === "number" && Number.isFinite(options.durationMs) && options.durationMs >= 0 ? Math.min(Math.round(options.durationMs), MAX_CHECK_IN_DURATION_MS) : void 0;
1310
+ const environment = this.options.environment.slice(0, MAX_CHECK_IN_ENVIRONMENT) || "production";
1311
+ const release = this.options.release?.slice(0, MAX_CHECK_IN_RELEASE);
1312
+ const payload = {
1313
+ status,
1314
+ ...checkInId ? { checkInId } : {},
1315
+ ...durationMs !== void 0 ? { durationMs } : {},
1316
+ environment,
1317
+ ...release ? { release } : {}
1318
+ };
1319
+ await this.transport.checkIn(slug, payload);
1320
+ } catch (error) {
1321
+ this.debugWarn(`check-in falhou: ${describeError(error)}`);
1322
+ }
1323
+ }
1324
+ async cron(slug, fn) {
1325
+ const checkInId = (0, import_node_crypto3.randomUUID)();
1326
+ const started = import_node_perf_hooks3.performance.now();
1327
+ const execute = async () => {
1328
+ await this.checkIn(slug, "in_progress", { checkInId });
1329
+ try {
1330
+ const result = await fn();
1331
+ await this.checkIn(slug, "ok", { checkInId, durationMs: elapsedSince(started) });
1332
+ return result;
1333
+ } catch (error) {
1334
+ this.captureError(error, { cron: slug });
1335
+ await this.checkIn(slug, "error", { checkInId, durationMs: elapsedSince(started) });
1336
+ throw error;
1337
+ }
1338
+ };
1339
+ if (currentContext()) return execute();
1340
+ return this.withContext(execute);
1341
+ }
962
1342
  middleware() {
963
1343
  return createMiddleware(this);
964
1344
  }
@@ -976,7 +1356,7 @@ var RadarClient = class {
976
1356
  () => this.emitLog(level, "http.request", {
977
1357
  method: (req.method ?? "GET").toUpperCase(),
978
1358
  ...route ? { route } : {},
979
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), req.params, this.options.redact) } : {},
1359
+ ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
980
1360
  status,
981
1361
  durationMs: elapsedSince(context.startedAt)
982
1362
  })
@@ -993,6 +1373,8 @@ var RadarClient = class {
993
1373
  async close(timeoutMs = 2e3) {
994
1374
  if (this.closed) return;
995
1375
  await this.flush(timeoutMs);
1376
+ this.restoreFetch?.();
1377
+ this.restoreFetch = null;
996
1378
  this.closed = true;
997
1379
  this.transport?.stop();
998
1380
  }
@@ -1013,31 +1395,40 @@ var RadarClient = class {
1013
1395
  this.print(event);
1014
1396
  this.transport?.enqueue(event);
1015
1397
  }
1398
+ captureException(exception, options = {}) {
1399
+ this.safely(() => this.emitException(exception, options));
1400
+ }
1016
1401
  emitError(error, data, level, handled) {
1017
1402
  if (this.closed) return;
1018
1403
  if (typeof error === "object" && error !== null) {
1019
1404
  if (this.captured.has(error)) return;
1020
1405
  this.captured.add(error);
1021
1406
  }
1407
+ this.emitException(exceptionInfo(error), { level, handled, ...data !== void 0 ? { attrs: data } : {} }, error instanceof Error ? error.stack : void 0);
1408
+ }
1409
+ emitException(exception, options, stack) {
1410
+ if (this.closed) return;
1022
1411
  const context = currentContext();
1023
1412
  const mode = this.options.redact;
1413
+ const request = options.request === null ? void 0 : options.request ?? (context?.req ? requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) : void 0);
1414
+ const runtime = options.runtime === void 0 ? runtimeInfo() : options.runtime;
1024
1415
  const event = {
1025
1416
  type: "error",
1026
1417
  ts: Date.now(),
1027
- level,
1028
- handled,
1029
- exception: exceptionInfo(error),
1418
+ level: options.level ?? "error",
1419
+ handled: options.handled ?? true,
1420
+ exception,
1030
1421
  ...context ? { requestId: context.requestId } : {},
1031
- ...context?.req ? { request: requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) } : {},
1422
+ ...request ? { request } : {},
1032
1423
  ...context?.user ? { user: context.user } : {},
1033
- runtime: runtimeInfo(),
1034
- ...data !== void 0 ? { attrs: prepareAttrs(data, mode) } : {}
1424
+ ...runtime ? { runtime } : {},
1425
+ ...options.attrs !== void 0 ? { attrs: prepareAttrs(options.attrs, mode) } : {}
1035
1426
  };
1036
- this.print(event, error instanceof Error ? error.stack : void 0);
1427
+ this.print(event, stack);
1037
1428
  if (!this.transport) return;
1038
1429
  this.transport.enqueue(
1039
- () => enrichException(event.exception).then(
1040
- (exception) => ({ ...event, exception }),
1430
+ () => enrichException(event.exception, process.cwd(), options.enrich).then(
1431
+ (enriched) => ({ ...event, exception: enriched }),
1041
1432
  () => event
1042
1433
  )
1043
1434
  );