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