@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.
@@ -50,6 +50,33 @@ var import_rxjs = require("rxjs");
50
50
 
51
51
  // src/context.ts
52
52
  var import_node_async_hooks = require("async_hooks");
53
+ var import_node_crypto = require("crypto");
54
+
55
+ // src/protocol/index.ts
56
+ var INGEST_PATH = "/api/v1/events";
57
+ var CHECKIN_PATH = "/api/v1/checkins/";
58
+ var CHECKIN_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
59
+ var CHECKIN_STATUSES = ["in_progress", "ok", "error"];
60
+ var LIMITS = {
61
+ batchBytes: 1e6,
62
+ batchEvents: 500,
63
+ messageLength: 2e3,
64
+ stringLength: 2e3,
65
+ attrsBytes: 16384,
66
+ bodyBytes: 16384,
67
+ headers: 50,
68
+ frames: 50,
69
+ contextFrames: 10,
70
+ contextLines: 5,
71
+ contextLineLength: 300,
72
+ causeDepth: 5,
73
+ depth: 6,
74
+ arrayItems: 50,
75
+ requestIdLength: 128,
76
+ checkInIdLength: 64
77
+ };
78
+
79
+ // src/context.ts
53
80
  var STATE_KEY = /* @__PURE__ */ Symbol.for("@oconde/radar");
54
81
  function globalState() {
55
82
  const holder = globalThis;
@@ -66,6 +93,11 @@ function currentContext() {
66
93
  function runWithContext(context, fn) {
67
94
  return globalState().storage.run(context, fn);
68
95
  }
96
+ var SAFE_REQUEST_ID = /^[\x21-\x7e]+$/;
97
+ function requestIdFrom(incoming) {
98
+ const value = incoming?.trim();
99
+ return value && value.length <= LIMITS.requestIdLength && SAFE_REQUEST_ID.test(value) ? value : (0, import_node_crypto.randomUUID)();
100
+ }
69
101
 
70
102
  // src/client.ts
71
103
  var import_node_crypto3 = require("crypto");
@@ -73,31 +105,10 @@ var import_node_os = require("os");
73
105
  var import_node_perf_hooks3 = require("perf_hooks");
74
106
 
75
107
  // src/express.ts
76
- var import_node_crypto2 = require("crypto");
77
108
  var import_node_perf_hooks = require("perf_hooks");
78
109
 
79
- // src/protocol/index.ts
80
- var INGEST_PATH = "/api/v1/events";
81
- var LIMITS = {
82
- batchBytes: 1e6,
83
- batchEvents: 500,
84
- messageLength: 2e3,
85
- stringLength: 2e3,
86
- attrsBytes: 16384,
87
- bodyBytes: 16384,
88
- headers: 50,
89
- frames: 50,
90
- contextFrames: 10,
91
- contextLines: 5,
92
- contextLineLength: 300,
93
- causeDepth: 5,
94
- depth: 6,
95
- arrayItems: 50,
96
- requestIdLength: 128
97
- };
98
-
99
110
  // src/redact.ts
100
- var import_node_crypto = require("crypto");
111
+ var import_node_crypto2 = require("crypto");
101
112
  var SECRET_WORDS = /* @__PURE__ */ new Set([
102
113
  "pass",
103
114
  "passwd",
@@ -143,16 +154,16 @@ function fingerprintSlot() {
143
154
  const holder = globalThis;
144
155
  let slot = holder[KEY_SLOT];
145
156
  if (!slot) {
146
- slot = { key: (0, import_node_crypto.randomBytes)(32) };
157
+ slot = { key: (0, import_node_crypto2.randomBytes)(32) };
147
158
  holder[KEY_SLOT] = slot;
148
159
  }
149
160
  return slot;
150
161
  }
151
162
  function setFingerprintSecret(secret) {
152
- fingerprintSlot().key = secret ? (0, import_node_crypto.createHash)("sha256").update(`@oconde/radar:${secret}`).digest() : (0, import_node_crypto.randomBytes)(32);
163
+ fingerprintSlot().key = secret ? (0, import_node_crypto2.createHash)("sha256").update(`@oconde/radar:${secret}`).digest() : (0, import_node_crypto2.randomBytes)(32);
153
164
  }
154
165
  function fingerprint(value) {
155
- return (0, import_node_crypto.createHmac)("sha256", fingerprintSlot().key).update(value).digest("hex").slice(0, 8);
166
+ return (0, import_node_crypto2.createHmac)("sha256", fingerprintSlot().key).update(value).digest("hex").slice(0, 8);
156
167
  }
157
168
  function words(name) {
158
169
  return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
@@ -294,7 +305,44 @@ function requestPath(req) {
294
305
  }
295
306
  function requestRoute(req, route) {
296
307
  if (route) return route;
297
- return typeof req.route?.path === "string" ? req.route.path : void 0;
308
+ if (typeof req.route?.path !== "string") return void 0;
309
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
310
+ if (!base) return req.route.path;
311
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
312
+ }
313
+ var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
314
+ function pathSegments(value) {
315
+ const segments = value.split("/");
316
+ while (segments.length > 1 && segments[segments.length - 1] === "") segments.pop();
317
+ return segments;
318
+ }
319
+ function decodeSegment(value) {
320
+ try {
321
+ return decodeURIComponent(value);
322
+ } catch {
323
+ return value;
324
+ }
325
+ }
326
+ function routeParams(req, path) {
327
+ if (typeof req.route?.path !== "string") return {};
328
+ const pattern = pathSegments(`${typeof req.baseUrl === "string" ? req.baseUrl : ""}${req.route.path}`).filter((segment, index) => index > 0 || segment !== "");
329
+ const values = pathSegments(path);
330
+ if (pattern.length > values.length) return {};
331
+ const offset = values.length - pattern.length;
332
+ const result = {};
333
+ for (let index = 0; index < pattern.length; index++) {
334
+ const value = values[offset + index];
335
+ const param = ROUTE_PARAM.exec(pattern[index]);
336
+ if (!param) {
337
+ if (pattern[index] !== value) return {};
338
+ continue;
339
+ }
340
+ if (value) result[param[1]] = decodeSegment(value);
341
+ }
342
+ return result;
343
+ }
344
+ function pathParams(req, path) {
345
+ return { ...routeParams(req, path), ...req.params ?? {} };
298
346
  }
299
347
  function redactPath(path, params, mode) {
300
348
  if (mode === "none" || !params) return path;
@@ -311,7 +359,7 @@ function safeUrl(req, mode) {
311
359
  const queryStart = url.indexOf("?");
312
360
  const path = queryStart >= 0 ? url.slice(0, queryStart) : url;
313
361
  const query = queryStart >= 0 ? url.slice(queryStart) : "";
314
- return redactUrl(redactPath(path, req.params, mode) + query, mode);
362
+ return redactUrl(redactPath(path, pathParams(req, path), mode) + query, mode);
315
363
  }
316
364
  function redactUrl(url, mode) {
317
365
  const queryStart = url.indexOf("?");
@@ -379,8 +427,7 @@ function createMiddleware(logger) {
379
427
  return (req, res, next) => {
380
428
  let context;
381
429
  try {
382
- const incoming = headerValue(req.headers["x-request-id"])?.trim();
383
- const requestId = incoming && incoming.length <= LIMITS.requestIdLength ? incoming : (0, import_node_crypto2.randomUUID)();
430
+ const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
384
431
  context = { requestId, startedAt: import_node_perf_hooks.performance.now(), req };
385
432
  res.setHeader("x-request-id", requestId);
386
433
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
@@ -395,6 +442,12 @@ function createMiddleware(logger) {
395
442
  // src/options.ts
396
443
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
397
444
  var LEVELS = ["debug", "info", "warn", "error"];
445
+ function resolveTraceFetch(value) {
446
+ if (!value) return { enabled: false, propagateTo: [] };
447
+ if (value === true) return { enabled: true, propagateTo: [] };
448
+ const targets2 = Array.isArray(value.propagateTo) ? value.propagateTo : [];
449
+ return { enabled: true, propagateTo: targets2.filter((target) => typeof target === "string" ? target.trim() !== "" : target instanceof RegExp) };
450
+ }
398
451
  function resolveOptions(options = {}, env = process.env) {
399
452
  const key = options.key?.trim() || void 0;
400
453
  return {
@@ -410,6 +463,7 @@ function resolveOptions(options = {}, env = process.env) {
410
463
  logRequests: options.logRequests ?? true,
411
464
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
412
465
  captureUnhandled: options.captureUnhandled ?? true,
466
+ traceFetch: resolveTraceFetch(options.traceFetch),
413
467
  debug: options.debug ?? false
414
468
  };
415
469
  }
@@ -471,16 +525,144 @@ var import_promises = require("fs/promises");
471
525
  var import_node_module = require("module");
472
526
  var import_node_path = require("path");
473
527
  var import_node_url = require("url");
528
+
529
+ // src/function-name.ts
530
+ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
531
+ var MAX_LINES_UP = 400;
532
+ var MAX_HEADER_LINES = 6;
533
+ var MAX_NAME_LENGTH = 100;
534
+ var MAX_COMMENT_LINES = 40;
535
+ var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
536
+ var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
537
+ var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
538
+ var KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "await", "else", "do", "try"]);
539
+ var NAME_PATTERNS = [
540
+ new RegExp(`\\bfunction\\s*\\*?\\s*(${IDENTIFIER})\\s*\\(`),
541
+ new RegExp(`(${IDENTIFIER})\\s*[:=]\\s*(?:async\\s+)?function\\b`),
542
+ new RegExp(`(${IDENTIFIER})\\s*(?::\\s*[^=]+)?=\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`),
543
+ new RegExp(`(?:^|[{,(]\\s*)(${IDENTIFIER})\\s*:\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`)
544
+ ];
545
+ var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
546
+ var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
547
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
548
+ function lineCommentAt(text, from) {
549
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
550
+ if (index === 0 || text[index - 1] !== ":") return index;
551
+ }
552
+ return -1;
553
+ }
554
+ function blockCommentAt(text, from) {
555
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
556
+ if (index === 0 || text[index - 1] !== "\\") return index;
557
+ }
558
+ return -1;
559
+ }
560
+ function stripComments(text, insideComment, closesLater) {
561
+ let code = "";
562
+ let position = 0;
563
+ let inside = insideComment;
564
+ while (position < text.length) {
565
+ if (inside) {
566
+ const end = text.indexOf("*/", position);
567
+ if (end < 0) return { code, insideComment: true };
568
+ inside = false;
569
+ position = end + 2;
570
+ continue;
571
+ }
572
+ const block = blockCommentAt(text, position);
573
+ const line = lineCommentAt(text, position);
574
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
575
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
576
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
577
+ code += text.slice(position, block + 2);
578
+ position = block + 2;
579
+ continue;
580
+ }
581
+ code += text.slice(position, block);
582
+ inside = true;
583
+ position = block + 2;
584
+ }
585
+ return { code, insideComment: inside };
586
+ }
587
+ function codeLines(lines, from, start, column) {
588
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
589
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
590
+ const result = [];
591
+ let insideComment = false;
592
+ for (let index = 0; index < plain.length; index++) {
593
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
594
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
595
+ insideComment = stripped.insideComment;
596
+ result.push(stripped.code);
597
+ }
598
+ return result;
599
+ }
600
+ function parenBalance(text) {
601
+ let balance = 0;
602
+ for (const char of text) {
603
+ if (char === "(") balance += 1;
604
+ else if (char === ")") balance -= 1;
605
+ }
606
+ return balance;
607
+ }
608
+ function headerOf(code, index, prefix) {
609
+ let header = prefix;
610
+ for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
611
+ if (header.trim() !== "" && parenBalance(header) >= 0) break;
612
+ header = `${code[previous] ?? ""} ${header}`;
613
+ }
614
+ return header;
615
+ }
616
+ function nameIn(header) {
617
+ const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
618
+ for (const pattern of patterns) {
619
+ const name = pattern.exec(header)?.[1];
620
+ if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
621
+ }
622
+ return void 0;
623
+ }
624
+ function enclosingFunctionName(lines, line, column) {
625
+ const start = line - 1;
626
+ if (start < 0 || start >= lines.length) return void 0;
627
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
628
+ const cleaned = codeLines(lines, from, start, column);
629
+ let depth = 0;
630
+ for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
631
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
632
+ const code = cleaned[index - from] ?? "";
633
+ for (let position = code.length - 1; position >= 0; position--) {
634
+ const char = code[position];
635
+ if (char === "}") {
636
+ depth += 1;
637
+ } else if (char === "{") {
638
+ if (depth > 0) {
639
+ depth -= 1;
640
+ continue;
641
+ }
642
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
643
+ if (CLASS_BODY.test(header)) return void 0;
644
+ if (CONTROL_BLOCK.test(header)) continue;
645
+ const name = nameIn(header);
646
+ if (name) return name;
647
+ }
648
+ }
649
+ }
650
+ return void 0;
651
+ }
652
+
653
+ // src/source.ts
474
654
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
475
655
  var MAX_CACHED_FILES = 50;
476
656
  var MAPPING_URL = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
477
657
  var cache = /* @__PURE__ */ new Map();
478
- async function enrichFrames(frames, cwd = process.cwd()) {
658
+ async function enrichFrames(frames, cwd = process.cwd(), options = {}) {
479
659
  const result = [];
480
660
  let withContext = 0;
481
661
  for (const frame of frames) {
482
662
  try {
483
- const { frame: mapped, lines } = await mapFrame(frame);
663
+ const { frame: located, lines, original } = await mapFrame(frame, options.mapLocator);
664
+ const named = options.inferFunctionNames && original && !located.fn && lines && located.line ? withInferredName(located, lines) : located;
665
+ const mapped = named.inApp && isLibraryPath(named.file) ? { ...named, inApp: false } : named;
484
666
  let next = mapped;
485
667
  if (mapped.inApp && lines && mapped.line && withContext < LIMITS.contextFrames) {
486
668
  const context = contextFor(lines, mapped.line);
@@ -496,29 +678,36 @@ async function enrichFrames(frames, cwd = process.cwd()) {
496
678
  }
497
679
  return result;
498
680
  }
499
- async function enrichException(exception, cwd = process.cwd()) {
500
- const frames = await enrichFrames(exception.frames, cwd);
681
+ async function enrichException(exception, cwd = process.cwd(), options = {}) {
682
+ const frames = await enrichFrames(exception.frames, cwd, options);
501
683
  if (!exception.cause) return { ...exception, frames };
502
- return { ...exception, frames, cause: await enrichException(exception.cause, cwd) };
684
+ return { ...exception, frames, cause: await enrichException(exception.cause, cwd, options) };
685
+ }
686
+ function withInferredName(frame, lines) {
687
+ const fn = enclosingFunctionName(lines, frame.line ?? 0, frame.col);
688
+ return fn ? { ...frame, fn } : frame;
689
+ }
690
+ function isLibraryPath(file) {
691
+ return file.includes("/node_modules/") || file.includes("\\node_modules\\");
503
692
  }
504
693
  function relativize(file, cwd) {
505
694
  if (!(0, import_node_path.isAbsolute)(file)) return file;
506
695
  const path = (0, import_node_path.relative)(cwd, file);
507
696
  return path.startsWith("..") || (0, import_node_path.isAbsolute)(path) ? file : path.split(import_node_path.sep).join("/");
508
697
  }
509
- async function mapFrame(frame) {
510
- if (!frame.line || !(0, import_node_path.isAbsolute)(frame.file)) return { frame, lines: null };
511
- const file = await loadFile(frame.file);
512
- if (!file) return { frame, lines: null };
513
- if (!file.map) return { frame, lines: file.lines };
698
+ async function mapFrame(frame, locator) {
699
+ if (!frame.line || !(0, import_node_path.isAbsolute)(frame.file)) return { frame, lines: null, original: false };
700
+ const file = await loadFile(frame.file, locator);
701
+ if (!file) return { frame, lines: null, original: false };
702
+ if (!file.map) return { frame, lines: file.lines, original: false };
514
703
  const entry = file.map.map.findEntry(frame.line - 1, (frame.col ?? 1) - 1);
515
- if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines };
704
+ if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines, original: false };
516
705
  const originalPath = resolveSource(file.map, entry.originalSource);
517
706
  const mapped = { ...frame, file: originalPath, line: entry.originalLine + 1, col: (entry.originalColumn ?? 0) + 1 };
518
707
  const lines = embeddedLines(file.map, file.map.sources.indexOf(entry.originalSource));
519
- if (lines) return { frame: mapped, lines };
708
+ if (lines) return { frame: mapped, lines, original: true };
520
709
  const original = await loadFile(originalPath);
521
- return { frame: mapped, lines: original?.lines ?? null };
710
+ return { frame: mapped, lines: original?.lines ?? null, original: true };
522
711
  }
523
712
  function embeddedLines(map, index) {
524
713
  if (index < 0) return null;
@@ -550,26 +739,27 @@ function resolveSource(map, source) {
550
739
  }
551
740
  return (0, import_node_path.isAbsolute)(source) ? source : (0, import_node_path.resolve)(map.baseDir, source);
552
741
  }
553
- function loadFile(path) {
554
- const cached = cache.get(path);
742
+ function loadFile(path, locator) {
743
+ const key = locator ? `${path}\0located` : path;
744
+ const cached = cache.get(key);
555
745
  if (cached) {
556
- cache.delete(path);
557
- cache.set(path, cached);
746
+ cache.delete(key);
747
+ cache.set(key, cached);
558
748
  return cached;
559
749
  }
560
- const pending = readSource(path).catch(() => null);
561
- cache.set(path, pending);
750
+ const pending = readSource(path, locator).catch(() => null);
751
+ cache.set(key, pending);
562
752
  if (cache.size > MAX_CACHED_FILES) {
563
753
  const oldest = cache.keys().next().value;
564
754
  if (oldest !== void 0) cache.delete(oldest);
565
755
  }
566
756
  return pending;
567
757
  }
568
- async function readSource(path) {
758
+ async function readSource(path, locator) {
569
759
  const text = await readText(path);
570
760
  if (text === null) return null;
571
761
  const lines = text.split(/\r?\n/);
572
- return { lines, map: await loadMap(path, lines) };
762
+ return { lines, map: await loadMap(path, lines, locator) };
573
763
  }
574
764
  async function readText(path) {
575
765
  try {
@@ -580,7 +770,7 @@ async function readText(path) {
580
770
  return null;
581
771
  }
582
772
  }
583
- async function loadMap(path, lines) {
773
+ async function loadMap(path, lines, locator) {
584
774
  let url;
585
775
  for (let index = lines.length - 1; index >= 0 && index >= lines.length - 5; index--) {
586
776
  const match = MAPPING_URL.exec(lines[index] ?? "");
@@ -589,10 +779,15 @@ async function loadMap(path, lines) {
589
779
  break;
590
780
  }
591
781
  }
592
- if (!url) return null;
593
782
  let raw;
594
783
  let mapDir;
595
- if (url.startsWith("data:")) {
784
+ const located = url ? null : locator ? locator(path) : `${path}.map`;
785
+ if (located) {
786
+ raw = await readText(located);
787
+ mapDir = (0, import_node_path.dirname)(located);
788
+ } else if (!url) {
789
+ return null;
790
+ } else if (url.startsWith("data:")) {
596
791
  const comma = url.indexOf(",");
597
792
  const encoded = url.slice(comma + 1);
598
793
  raw = url.slice(0, comma).endsWith(";base64") ? Buffer.from(encoded, "base64").toString("utf8") : decodeURIComponent(encoded);
@@ -621,12 +816,13 @@ async function loadMap(path, lines) {
621
816
  var import_node_url2 = require("url");
622
817
  var WITH_FUNCTION = /^at (?:async )?(.+?) \((.+)\)$/;
623
818
  var LOCATION = /^(.+):(\d+):(\d+)$/;
819
+ var MAX_FRAME_LINE = 1e3;
624
820
  function parseStack(stack) {
625
821
  if (!stack) return [];
626
822
  const frames = [];
627
823
  for (const raw of stack.split("\n")) {
628
824
  const line = raw.trim();
629
- if (!line.startsWith("at ")) continue;
825
+ if (line.length > MAX_FRAME_LINE || !line.startsWith("at ")) continue;
630
826
  const withFunction = WITH_FUNCTION.exec(line);
631
827
  const fn = withFunction?.[1];
632
828
  const location = withFunction ? withFunction[2] ?? "" : line.replace(/^at (?:async )?/, "");
@@ -685,10 +881,6 @@ function safeStringify(value) {
685
881
  }
686
882
  }
687
883
 
688
- // src/transport.ts
689
- var import_node_util2 = require("util");
690
- var import_node_zlib = require("zlib");
691
-
692
884
  // src/util.ts
693
885
  var import_node_perf_hooks2 = require("perf_hooks");
694
886
  function describeError(error) {
@@ -698,7 +890,101 @@ function elapsedSince(startedAt) {
698
890
  return Math.round(import_node_perf_hooks2.performance.now() - startedAt);
699
891
  }
700
892
 
893
+ // src/trace-fetch.ts
894
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
895
+ var REQUEST_ID_HEADER = "x-request-id";
896
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
897
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
898
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
899
+ function secretSegment(segment) {
900
+ if (UUID.test(segment)) return false;
901
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
902
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
903
+ }
904
+ function outboundPath(pathname, mode) {
905
+ if (mode === "none") return pathname;
906
+ return pathname.split("/").map((segment) => {
907
+ let decoded = segment;
908
+ try {
909
+ decoded = decodeURIComponent(segment);
910
+ } catch {
911
+ decoded = segment;
912
+ }
913
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
914
+ }).join("/");
915
+ }
916
+ function failureOf(error) {
917
+ const message = describeError(error);
918
+ const cause = error instanceof Error ? error.cause : void 0;
919
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
920
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
921
+ }
922
+ function urlOf(input) {
923
+ try {
924
+ if (typeof input === "string") return new URL(input);
925
+ if (input instanceof URL) return input;
926
+ return new URL(input.url);
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
931
+ function originOf(value) {
932
+ try {
933
+ return new URL(value).origin;
934
+ } catch {
935
+ return null;
936
+ }
937
+ }
938
+ function targets(url, target) {
939
+ if (target instanceof RegExp) return target.test(url.host);
940
+ const wanted = target.trim().toLowerCase();
941
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
942
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
943
+ }
944
+ function isPlainObject(value) {
945
+ if (value === null || typeof value !== "object") return false;
946
+ const prototype = Object.getPrototypeOf(value);
947
+ return prototype === Object.prototype || prototype === null;
948
+ }
949
+ function withRequestId(input, init, requestId) {
950
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
951
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
952
+ headers.set(REQUEST_ID_HEADER, requestId);
953
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
954
+ return [new Request(input, init), { headers }];
955
+ }
956
+ function traceFetch(config) {
957
+ const current = globalThis.fetch;
958
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
959
+ const original = current;
960
+ const radarOrigin = originOf(config.endpoint);
961
+ const traced = async (input, init) => {
962
+ const url = urlOf(input);
963
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
964
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
965
+ const context = currentContext();
966
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
967
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
968
+ const started = performance.now();
969
+ try {
970
+ const response = await original(nextInput, nextInit);
971
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
972
+ return response;
973
+ } catch (error) {
974
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
975
+ throw error;
976
+ }
977
+ };
978
+ Object.defineProperty(traced, WRAPPED, { value: original });
979
+ globalThis.fetch = traced;
980
+ return () => {
981
+ if (globalThis.fetch === traced) globalThis.fetch = original;
982
+ };
983
+ }
984
+
701
985
  // src/transport.ts
986
+ var import_node_util2 = require("util");
987
+ var import_node_zlib = require("zlib");
702
988
  var gzipAsync = (0, import_node_util2.promisify)(import_node_zlib.gzip);
703
989
  var MAX_BATCH_BYTES = 9e5;
704
990
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -721,6 +1007,7 @@ var Transport = class {
721
1007
  inFlight = null;
722
1008
  nextAttemptAt = 0;
723
1009
  backoffMs = 0;
1010
+ unauthorizedUntil = 0;
724
1011
  stopped = false;
725
1012
  warned = /* @__PURE__ */ new Set();
726
1013
  timer;
@@ -810,10 +1097,8 @@ var Transport = class {
810
1097
  this.nextAttemptAt = Date.now() + Math.max(this.backoffMs, outcome.retryAfterMs ?? 0);
811
1098
  return false;
812
1099
  case "unauthorized":
813
- this.droppedCount += events.length + this.queue.length;
814
- this.queue = [];
815
- this.nextAttemptAt = Date.now() + UNAUTHORIZED_PAUSE_MS;
816
- this.warn("unauthorized", `chave do Radar recusada (${outcome.status}); envios pausados por 10 minutos`, true);
1100
+ this.droppedCount += events.length;
1101
+ this.pauseUnauthorized(outcome.status);
817
1102
  return false;
818
1103
  case "rejected":
819
1104
  this.droppedCount += events.length;
@@ -821,6 +1106,47 @@ var Transport = class {
821
1106
  return true;
822
1107
  }
823
1108
  }
1109
+ async checkIn(slug, payload) {
1110
+ if (this.stopped || !this.config.checkInBaseUrl || Date.now() < this.unauthorizedUntil) return;
1111
+ let response;
1112
+ try {
1113
+ response = await fetch(this.config.checkInBaseUrl + encodeURIComponent(slug), {
1114
+ method: "POST",
1115
+ headers: this.headers(),
1116
+ body: JSON.stringify(payload),
1117
+ redirect: "error",
1118
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? 5e3)
1119
+ });
1120
+ } catch (error) {
1121
+ this.warn("checkin_network", `check-in no Radar falhou: ${describeError(error)}`, false);
1122
+ return;
1123
+ }
1124
+ await response.arrayBuffer().catch(() => void 0);
1125
+ if (response.ok) return;
1126
+ if (response.status === 401 || response.status === 403) {
1127
+ this.pauseUnauthorized(response.status);
1128
+ return;
1129
+ }
1130
+ if (response.status === 404) {
1131
+ this.warn(`checkin_unknown_${slug}`, `tarefa agendada "${slug}" n\xE3o existe no projeto da chave; crie o monitor com esse slug no Radar`, true);
1132
+ return;
1133
+ }
1134
+ this.warn(`checkin_rejected_${response.status}`, `check-in recusado pelo Radar (${response.status})`, false);
1135
+ }
1136
+ pauseUnauthorized(status) {
1137
+ this.droppedCount += this.queue.length;
1138
+ this.queue = [];
1139
+ this.unauthorizedUntil = Date.now() + UNAUTHORIZED_PAUSE_MS;
1140
+ this.nextAttemptAt = Math.max(this.nextAttemptAt, this.unauthorizedUntil);
1141
+ this.warn("unauthorized", `chave do Radar recusada (${status}); envios pausados por 10 minutos`, true);
1142
+ }
1143
+ headers() {
1144
+ return {
1145
+ "content-type": "application/json",
1146
+ authorization: `Bearer ${this.config.key}`,
1147
+ "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
1148
+ };
1149
+ }
824
1150
  requeue(events) {
825
1151
  this.queue.unshift(...events);
826
1152
  const overflow = this.queue.length - this.maxQueue;
@@ -832,11 +1158,7 @@ var Transport = class {
832
1158
  async post(batch) {
833
1159
  const json = JSON.stringify(batch);
834
1160
  const compress = Buffer.byteLength(json) > GZIP_THRESHOLD_BYTES;
835
- const headers = {
836
- "content-type": "application/json",
837
- authorization: `Bearer ${this.config.key}`,
838
- "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
839
- };
1161
+ const headers = this.headers();
840
1162
  if (compress) headers["content-encoding"] = "gzip";
841
1163
  let response;
842
1164
  try {
@@ -890,15 +1212,19 @@ function withTimeout(promise, ms) {
890
1212
 
891
1213
  // src/version.ts
892
1214
  var SDK_NAME = "@oconde/radar";
893
- var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
1215
+ var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
894
1216
 
895
1217
  // src/client.ts
1218
+ var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
1219
+ var MAX_CHECK_IN_ENVIRONMENT = 64;
1220
+ var MAX_CHECK_IN_RELEASE = 128;
896
1221
  var RadarClient = class {
897
1222
  options = resolveOptions({}, {});
898
1223
  transport = null;
899
1224
  initialized = false;
900
1225
  closed = false;
901
1226
  captured = /* @__PURE__ */ new WeakSet();
1227
+ restoreFetch = null;
902
1228
  get isEnabled() {
903
1229
  return this.transport !== null && !this.closed;
904
1230
  }
@@ -921,10 +1247,19 @@ var RadarClient = class {
921
1247
  sdk: { name: SDK_NAME, version: SDK_VERSION },
922
1248
  environment: this.options.environment,
923
1249
  ...this.options.release ? { release: this.options.release } : {},
1250
+ checkInBaseUrl: this.options.endpoint + CHECKIN_PATH,
924
1251
  onWarning: (message, always) => always ? this.writeWarning(message) : this.debugWarn(message)
925
1252
  });
926
1253
  }
927
1254
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1255
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1256
+ this.restoreFetch = traceFetch({
1257
+ endpoint: this.options.endpoint,
1258
+ propagateTo: this.options.traceFetch.propagateTo,
1259
+ redact: this.options.redact,
1260
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1261
+ });
1262
+ }
928
1263
  } catch (error) {
929
1264
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
930
1265
  }
@@ -982,6 +1317,51 @@ var RadarClient = class {
982
1317
  if (currentContext()) return execute();
983
1318
  return runWithContext({ requestId: (0, import_node_crypto3.randomUUID)(), startedAt: import_node_perf_hooks3.performance.now() }, execute);
984
1319
  }
1320
+ withContext(fn, options = {}) {
1321
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: import_node_perf_hooks3.performance.now() }, fn);
1322
+ }
1323
+ async checkIn(slug, status, options = {}) {
1324
+ try {
1325
+ if (!this.transport || this.closed) return;
1326
+ if (typeof slug !== "string" || !CHECKIN_SLUG.test(slug)) {
1327
+ this.debugWarn(`slug de tarefa agendada inv\xE1lido: ${String(slug).slice(0, 80)}`);
1328
+ return;
1329
+ }
1330
+ if (!CHECKIN_STATUSES.includes(status)) return;
1331
+ const checkInId = typeof options.checkInId === "string" && options.checkInId ? options.checkInId.slice(0, LIMITS.checkInIdLength) : void 0;
1332
+ 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;
1333
+ const environment = this.options.environment.slice(0, MAX_CHECK_IN_ENVIRONMENT) || "production";
1334
+ const release = this.options.release?.slice(0, MAX_CHECK_IN_RELEASE);
1335
+ const payload = {
1336
+ status,
1337
+ ...checkInId ? { checkInId } : {},
1338
+ ...durationMs !== void 0 ? { durationMs } : {},
1339
+ environment,
1340
+ ...release ? { release } : {}
1341
+ };
1342
+ await this.transport.checkIn(slug, payload);
1343
+ } catch (error) {
1344
+ this.debugWarn(`check-in falhou: ${describeError(error)}`);
1345
+ }
1346
+ }
1347
+ async cron(slug, fn) {
1348
+ const checkInId = (0, import_node_crypto3.randomUUID)();
1349
+ const started = import_node_perf_hooks3.performance.now();
1350
+ const execute = async () => {
1351
+ await this.checkIn(slug, "in_progress", { checkInId });
1352
+ try {
1353
+ const result = await fn();
1354
+ await this.checkIn(slug, "ok", { checkInId, durationMs: elapsedSince(started) });
1355
+ return result;
1356
+ } catch (error) {
1357
+ this.captureError(error, { cron: slug });
1358
+ await this.checkIn(slug, "error", { checkInId, durationMs: elapsedSince(started) });
1359
+ throw error;
1360
+ }
1361
+ };
1362
+ if (currentContext()) return execute();
1363
+ return this.withContext(execute);
1364
+ }
985
1365
  middleware() {
986
1366
  return createMiddleware(this);
987
1367
  }
@@ -999,7 +1379,7 @@ var RadarClient = class {
999
1379
  () => this.emitLog(level, "http.request", {
1000
1380
  method: (req.method ?? "GET").toUpperCase(),
1001
1381
  ...route ? { route } : {},
1002
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), req.params, this.options.redact) } : {},
1382
+ ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
1003
1383
  status,
1004
1384
  durationMs: elapsedSince(context.startedAt)
1005
1385
  })
@@ -1016,6 +1396,8 @@ var RadarClient = class {
1016
1396
  async close(timeoutMs = 2e3) {
1017
1397
  if (this.closed) return;
1018
1398
  await this.flush(timeoutMs);
1399
+ this.restoreFetch?.();
1400
+ this.restoreFetch = null;
1019
1401
  this.closed = true;
1020
1402
  this.transport?.stop();
1021
1403
  }
@@ -1036,31 +1418,40 @@ var RadarClient = class {
1036
1418
  this.print(event);
1037
1419
  this.transport?.enqueue(event);
1038
1420
  }
1421
+ captureException(exception, options = {}) {
1422
+ this.safely(() => this.emitException(exception, options));
1423
+ }
1039
1424
  emitError(error, data, level, handled) {
1040
1425
  if (this.closed) return;
1041
1426
  if (typeof error === "object" && error !== null) {
1042
1427
  if (this.captured.has(error)) return;
1043
1428
  this.captured.add(error);
1044
1429
  }
1430
+ this.emitException(exceptionInfo(error), { level, handled, ...data !== void 0 ? { attrs: data } : {} }, error instanceof Error ? error.stack : void 0);
1431
+ }
1432
+ emitException(exception, options, stack) {
1433
+ if (this.closed) return;
1045
1434
  const context = currentContext();
1046
1435
  const mode = this.options.redact;
1436
+ const request = options.request === null ? void 0 : options.request ?? (context?.req ? requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) : void 0);
1437
+ const runtime = options.runtime === void 0 ? runtimeInfo() : options.runtime;
1047
1438
  const event = {
1048
1439
  type: "error",
1049
1440
  ts: Date.now(),
1050
- level,
1051
- handled,
1052
- exception: exceptionInfo(error),
1441
+ level: options.level ?? "error",
1442
+ handled: options.handled ?? true,
1443
+ exception,
1053
1444
  ...context ? { requestId: context.requestId } : {},
1054
- ...context?.req ? { request: requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) } : {},
1445
+ ...request ? { request } : {},
1055
1446
  ...context?.user ? { user: context.user } : {},
1056
- runtime: runtimeInfo(),
1057
- ...data !== void 0 ? { attrs: prepareAttrs(data, mode) } : {}
1447
+ ...runtime ? { runtime } : {},
1448
+ ...options.attrs !== void 0 ? { attrs: prepareAttrs(options.attrs, mode) } : {}
1058
1449
  };
1059
- this.print(event, error instanceof Error ? error.stack : void 0);
1450
+ this.print(event, stack);
1060
1451
  if (!this.transport) return;
1061
1452
  this.transport.enqueue(
1062
- () => enrichException(event.exception).then(
1063
- (exception) => ({ ...event, exception }),
1453
+ () => enrichException(event.exception, process.cwd(), options.enrich).then(
1454
+ (enriched) => ({ ...event, exception: enriched }),
1064
1455
  () => event
1065
1456
  )
1066
1457
  );