@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.js CHANGED
@@ -1,33 +1,17 @@
1
1
  // src/client.ts
2
2
  import { randomUUID as randomUUID2 } from "crypto";
3
3
  import { hostname } from "os";
4
- import { performance as performance3 } from "perf_hooks";
4
+ import { performance as performance4 } from "perf_hooks";
5
5
 
6
6
  // src/context.ts
7
7
  import { AsyncLocalStorage } from "async_hooks";
8
- var STATE_KEY = /* @__PURE__ */ Symbol.for("@oconde/radar");
9
- function globalState() {
10
- const holder = globalThis;
11
- let state2 = holder[STATE_KEY];
12
- if (!state2) {
13
- state2 = { storage: new AsyncLocalStorage() };
14
- holder[STATE_KEY] = state2;
15
- }
16
- return state2;
17
- }
18
- function currentContext() {
19
- return globalState().storage.getStore();
20
- }
21
- function runWithContext(context, fn) {
22
- return globalState().storage.run(context, fn);
23
- }
24
-
25
- // src/express.ts
26
8
  import { randomUUID } from "crypto";
27
- import { performance } from "perf_hooks";
28
9
 
29
10
  // src/protocol/index.ts
30
11
  var INGEST_PATH = "/api/v1/events";
12
+ var CHECKIN_PATH = "/api/v1/checkins/";
13
+ var CHECKIN_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
14
+ var CHECKIN_STATUSES = ["in_progress", "ok", "error"];
31
15
  var LIMITS = {
32
16
  batchBytes: 1e6,
33
17
  batchEvents: 500,
@@ -43,9 +27,36 @@ var LIMITS = {
43
27
  causeDepth: 5,
44
28
  depth: 6,
45
29
  arrayItems: 50,
46
- requestIdLength: 128
30
+ requestIdLength: 128,
31
+ checkInIdLength: 64
47
32
  };
48
33
 
34
+ // src/context.ts
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 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
+ var SAFE_REQUEST_ID = /^[\x21-\x7e]+$/;
52
+ function requestIdFrom(incoming) {
53
+ const value = incoming?.trim();
54
+ return value && value.length <= LIMITS.requestIdLength && SAFE_REQUEST_ID.test(value) ? value : randomUUID();
55
+ }
56
+
57
+ // src/express.ts
58
+ import { performance as performance2 } from "perf_hooks";
59
+
49
60
  // src/redact.ts
50
61
  import { createHash, createHmac, randomBytes } from "crypto";
51
62
  var SECRET_WORDS = /* @__PURE__ */ new Set([
@@ -244,7 +255,44 @@ function requestPath(req) {
244
255
  }
245
256
  function requestRoute(req, route) {
246
257
  if (route) return route;
247
- return typeof req.route?.path === "string" ? req.route.path : void 0;
258
+ if (typeof req.route?.path !== "string") return void 0;
259
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
260
+ if (!base) return req.route.path;
261
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
262
+ }
263
+ var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
264
+ function pathSegments(value) {
265
+ const segments = value.split("/");
266
+ while (segments.length > 1 && segments[segments.length - 1] === "") segments.pop();
267
+ return segments;
268
+ }
269
+ function decodeSegment(value) {
270
+ try {
271
+ return decodeURIComponent(value);
272
+ } catch {
273
+ return value;
274
+ }
275
+ }
276
+ function routeParams(req, path) {
277
+ if (typeof req.route?.path !== "string") return {};
278
+ const pattern = pathSegments(`${typeof req.baseUrl === "string" ? req.baseUrl : ""}${req.route.path}`).filter((segment, index) => index > 0 || segment !== "");
279
+ const values = pathSegments(path);
280
+ if (pattern.length > values.length) return {};
281
+ const offset = values.length - pattern.length;
282
+ const result = {};
283
+ for (let index = 0; index < pattern.length; index++) {
284
+ const value = values[offset + index];
285
+ const param = ROUTE_PARAM.exec(pattern[index]);
286
+ if (!param) {
287
+ if (pattern[index] !== value) return {};
288
+ continue;
289
+ }
290
+ if (value) result[param[1]] = decodeSegment(value);
291
+ }
292
+ return result;
293
+ }
294
+ function pathParams(req, path) {
295
+ return { ...routeParams(req, path), ...req.params ?? {} };
248
296
  }
249
297
  function redactPath(path, params, mode) {
250
298
  if (mode === "none" || !params) return path;
@@ -261,7 +309,7 @@ function safeUrl(req, mode) {
261
309
  const queryStart = url.indexOf("?");
262
310
  const path = queryStart >= 0 ? url.slice(0, queryStart) : url;
263
311
  const query = queryStart >= 0 ? url.slice(queryStart) : "";
264
- return redactUrl(redactPath(path, req.params, mode) + query, mode);
312
+ return redactUrl(redactPath(path, pathParams(req, path), mode) + query, mode);
265
313
  }
266
314
  function redactUrl(url, mode) {
267
315
  const queryStart = url.indexOf("?");
@@ -329,9 +377,8 @@ function createMiddleware(logger) {
329
377
  return (req, res, next) => {
330
378
  let context;
331
379
  try {
332
- const incoming = headerValue(req.headers["x-request-id"])?.trim();
333
- const requestId = incoming && incoming.length <= LIMITS.requestIdLength ? incoming : randomUUID();
334
- context = { requestId, startedAt: performance.now(), req };
380
+ const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
381
+ context = { requestId, startedAt: performance2.now(), req };
335
382
  res.setHeader("x-request-id", requestId);
336
383
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
337
384
  } catch {
@@ -345,6 +392,12 @@ function createMiddleware(logger) {
345
392
  // src/options.ts
346
393
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
347
394
  var LEVELS = ["debug", "info", "warn", "error"];
395
+ function resolveTraceFetch(value) {
396
+ if (!value) return { enabled: false, propagateTo: [] };
397
+ if (value === true) return { enabled: true, propagateTo: [] };
398
+ const targets2 = Array.isArray(value.propagateTo) ? value.propagateTo : [];
399
+ return { enabled: true, propagateTo: targets2.filter((target) => typeof target === "string" ? target.trim() !== "" : target instanceof RegExp) };
400
+ }
348
401
  function resolveOptions(options = {}, env = process.env) {
349
402
  const key = options.key?.trim() || void 0;
350
403
  return {
@@ -360,6 +413,7 @@ function resolveOptions(options = {}, env = process.env) {
360
413
  logRequests: options.logRequests ?? true,
361
414
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
362
415
  captureUnhandled: options.captureUnhandled ?? true,
416
+ traceFetch: resolveTraceFetch(options.traceFetch),
363
417
  debug: options.debug ?? false
364
418
  };
365
419
  }
@@ -421,16 +475,144 @@ import { readFile, stat } from "fs/promises";
421
475
  import { SourceMap } from "module";
422
476
  import { dirname, isAbsolute, relative, resolve, sep } from "path";
423
477
  import { fileURLToPath } from "url";
478
+
479
+ // src/function-name.ts
480
+ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
481
+ var MAX_LINES_UP = 400;
482
+ var MAX_HEADER_LINES = 6;
483
+ var MAX_NAME_LENGTH = 100;
484
+ var MAX_COMMENT_LINES = 40;
485
+ var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
486
+ var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
487
+ var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
488
+ var KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "await", "else", "do", "try"]);
489
+ var NAME_PATTERNS = [
490
+ new RegExp(`\\bfunction\\s*\\*?\\s*(${IDENTIFIER})\\s*\\(`),
491
+ new RegExp(`(${IDENTIFIER})\\s*[:=]\\s*(?:async\\s+)?function\\b`),
492
+ new RegExp(`(${IDENTIFIER})\\s*(?::\\s*[^=]+)?=\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`),
493
+ new RegExp(`(?:^|[{,(]\\s*)(${IDENTIFIER})\\s*:\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`)
494
+ ];
495
+ var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
496
+ var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
497
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
498
+ function lineCommentAt(text, from) {
499
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
500
+ if (index === 0 || text[index - 1] !== ":") return index;
501
+ }
502
+ return -1;
503
+ }
504
+ function blockCommentAt(text, from) {
505
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
506
+ if (index === 0 || text[index - 1] !== "\\") return index;
507
+ }
508
+ return -1;
509
+ }
510
+ function stripComments(text, insideComment, closesLater) {
511
+ let code = "";
512
+ let position = 0;
513
+ let inside = insideComment;
514
+ while (position < text.length) {
515
+ if (inside) {
516
+ const end = text.indexOf("*/", position);
517
+ if (end < 0) return { code, insideComment: true };
518
+ inside = false;
519
+ position = end + 2;
520
+ continue;
521
+ }
522
+ const block = blockCommentAt(text, position);
523
+ const line = lineCommentAt(text, position);
524
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
525
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
526
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
527
+ code += text.slice(position, block + 2);
528
+ position = block + 2;
529
+ continue;
530
+ }
531
+ code += text.slice(position, block);
532
+ inside = true;
533
+ position = block + 2;
534
+ }
535
+ return { code, insideComment: inside };
536
+ }
537
+ function codeLines(lines, from, start, column) {
538
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
539
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
540
+ const result = [];
541
+ let insideComment = false;
542
+ for (let index = 0; index < plain.length; index++) {
543
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
544
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
545
+ insideComment = stripped.insideComment;
546
+ result.push(stripped.code);
547
+ }
548
+ return result;
549
+ }
550
+ function parenBalance(text) {
551
+ let balance = 0;
552
+ for (const char of text) {
553
+ if (char === "(") balance += 1;
554
+ else if (char === ")") balance -= 1;
555
+ }
556
+ return balance;
557
+ }
558
+ function headerOf(code, index, prefix) {
559
+ let header = prefix;
560
+ for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
561
+ if (header.trim() !== "" && parenBalance(header) >= 0) break;
562
+ header = `${code[previous] ?? ""} ${header}`;
563
+ }
564
+ return header;
565
+ }
566
+ function nameIn(header) {
567
+ const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
568
+ for (const pattern of patterns) {
569
+ const name = pattern.exec(header)?.[1];
570
+ if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
571
+ }
572
+ return void 0;
573
+ }
574
+ function enclosingFunctionName(lines, line, column) {
575
+ const start = line - 1;
576
+ if (start < 0 || start >= lines.length) return void 0;
577
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
578
+ const cleaned = codeLines(lines, from, start, column);
579
+ let depth = 0;
580
+ for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
581
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
582
+ const code = cleaned[index - from] ?? "";
583
+ for (let position = code.length - 1; position >= 0; position--) {
584
+ const char = code[position];
585
+ if (char === "}") {
586
+ depth += 1;
587
+ } else if (char === "{") {
588
+ if (depth > 0) {
589
+ depth -= 1;
590
+ continue;
591
+ }
592
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
593
+ if (CLASS_BODY.test(header)) return void 0;
594
+ if (CONTROL_BLOCK.test(header)) continue;
595
+ const name = nameIn(header);
596
+ if (name) return name;
597
+ }
598
+ }
599
+ }
600
+ return void 0;
601
+ }
602
+
603
+ // src/source.ts
424
604
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
425
605
  var MAX_CACHED_FILES = 50;
426
606
  var MAPPING_URL = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
427
607
  var cache = /* @__PURE__ */ new Map();
428
- async function enrichFrames(frames, cwd = process.cwd()) {
608
+ async function enrichFrames(frames, cwd = process.cwd(), options = {}) {
429
609
  const result = [];
430
610
  let withContext = 0;
431
611
  for (const frame of frames) {
432
612
  try {
433
- const { frame: mapped, lines } = await mapFrame(frame);
613
+ const { frame: located, lines, original } = await mapFrame(frame, options.mapLocator);
614
+ const named = options.inferFunctionNames && original && !located.fn && lines && located.line ? withInferredName(located, lines) : located;
615
+ const mapped = named.inApp && isLibraryPath(named.file) ? { ...named, inApp: false } : named;
434
616
  let next = mapped;
435
617
  if (mapped.inApp && lines && mapped.line && withContext < LIMITS.contextFrames) {
436
618
  const context = contextFor(lines, mapped.line);
@@ -446,29 +628,36 @@ async function enrichFrames(frames, cwd = process.cwd()) {
446
628
  }
447
629
  return result;
448
630
  }
449
- async function enrichException(exception, cwd = process.cwd()) {
450
- const frames = await enrichFrames(exception.frames, cwd);
631
+ async function enrichException(exception, cwd = process.cwd(), options = {}) {
632
+ const frames = await enrichFrames(exception.frames, cwd, options);
451
633
  if (!exception.cause) return { ...exception, frames };
452
- return { ...exception, frames, cause: await enrichException(exception.cause, cwd) };
634
+ return { ...exception, frames, cause: await enrichException(exception.cause, cwd, options) };
635
+ }
636
+ function withInferredName(frame, lines) {
637
+ const fn = enclosingFunctionName(lines, frame.line ?? 0, frame.col);
638
+ return fn ? { ...frame, fn } : frame;
639
+ }
640
+ function isLibraryPath(file) {
641
+ return file.includes("/node_modules/") || file.includes("\\node_modules\\");
453
642
  }
454
643
  function relativize(file, cwd) {
455
644
  if (!isAbsolute(file)) return file;
456
645
  const path = relative(cwd, file);
457
646
  return path.startsWith("..") || isAbsolute(path) ? file : path.split(sep).join("/");
458
647
  }
459
- async function mapFrame(frame) {
460
- if (!frame.line || !isAbsolute(frame.file)) return { frame, lines: null };
461
- const file = await loadFile(frame.file);
462
- if (!file) return { frame, lines: null };
463
- if (!file.map) return { frame, lines: file.lines };
648
+ async function mapFrame(frame, locator) {
649
+ if (!frame.line || !isAbsolute(frame.file)) return { frame, lines: null, original: false };
650
+ const file = await loadFile(frame.file, locator);
651
+ if (!file) return { frame, lines: null, original: false };
652
+ if (!file.map) return { frame, lines: file.lines, original: false };
464
653
  const entry = file.map.map.findEntry(frame.line - 1, (frame.col ?? 1) - 1);
465
- if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines };
654
+ if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines, original: false };
466
655
  const originalPath = resolveSource(file.map, entry.originalSource);
467
656
  const mapped = { ...frame, file: originalPath, line: entry.originalLine + 1, col: (entry.originalColumn ?? 0) + 1 };
468
657
  const lines = embeddedLines(file.map, file.map.sources.indexOf(entry.originalSource));
469
- if (lines) return { frame: mapped, lines };
658
+ if (lines) return { frame: mapped, lines, original: true };
470
659
  const original = await loadFile(originalPath);
471
- return { frame: mapped, lines: original?.lines ?? null };
660
+ return { frame: mapped, lines: original?.lines ?? null, original: true };
472
661
  }
473
662
  function embeddedLines(map, index) {
474
663
  if (index < 0) return null;
@@ -500,26 +689,27 @@ function resolveSource(map, source) {
500
689
  }
501
690
  return isAbsolute(source) ? source : resolve(map.baseDir, source);
502
691
  }
503
- function loadFile(path) {
504
- const cached = cache.get(path);
692
+ function loadFile(path, locator) {
693
+ const key = locator ? `${path}\0located` : path;
694
+ const cached = cache.get(key);
505
695
  if (cached) {
506
- cache.delete(path);
507
- cache.set(path, cached);
696
+ cache.delete(key);
697
+ cache.set(key, cached);
508
698
  return cached;
509
699
  }
510
- const pending = readSource(path).catch(() => null);
511
- cache.set(path, pending);
700
+ const pending = readSource(path, locator).catch(() => null);
701
+ cache.set(key, pending);
512
702
  if (cache.size > MAX_CACHED_FILES) {
513
703
  const oldest = cache.keys().next().value;
514
704
  if (oldest !== void 0) cache.delete(oldest);
515
705
  }
516
706
  return pending;
517
707
  }
518
- async function readSource(path) {
708
+ async function readSource(path, locator) {
519
709
  const text = await readText(path);
520
710
  if (text === null) return null;
521
711
  const lines = text.split(/\r?\n/);
522
- return { lines, map: await loadMap(path, lines) };
712
+ return { lines, map: await loadMap(path, lines, locator) };
523
713
  }
524
714
  async function readText(path) {
525
715
  try {
@@ -530,7 +720,7 @@ async function readText(path) {
530
720
  return null;
531
721
  }
532
722
  }
533
- async function loadMap(path, lines) {
723
+ async function loadMap(path, lines, locator) {
534
724
  let url;
535
725
  for (let index = lines.length - 1; index >= 0 && index >= lines.length - 5; index--) {
536
726
  const match = MAPPING_URL.exec(lines[index] ?? "");
@@ -539,10 +729,15 @@ async function loadMap(path, lines) {
539
729
  break;
540
730
  }
541
731
  }
542
- if (!url) return null;
543
732
  let raw;
544
733
  let mapDir;
545
- if (url.startsWith("data:")) {
734
+ const located = url ? null : locator ? locator(path) : `${path}.map`;
735
+ if (located) {
736
+ raw = await readText(located);
737
+ mapDir = dirname(located);
738
+ } else if (!url) {
739
+ return null;
740
+ } else if (url.startsWith("data:")) {
546
741
  const comma = url.indexOf(",");
547
742
  const encoded = url.slice(comma + 1);
548
743
  raw = url.slice(0, comma).endsWith(";base64") ? Buffer.from(encoded, "base64").toString("utf8") : decodeURIComponent(encoded);
@@ -571,12 +766,13 @@ async function loadMap(path, lines) {
571
766
  import { fileURLToPath as fileURLToPath2 } from "url";
572
767
  var WITH_FUNCTION = /^at (?:async )?(.+?) \((.+)\)$/;
573
768
  var LOCATION = /^(.+):(\d+):(\d+)$/;
769
+ var MAX_FRAME_LINE = 1e3;
574
770
  function parseStack(stack) {
575
771
  if (!stack) return [];
576
772
  const frames = [];
577
773
  for (const raw of stack.split("\n")) {
578
774
  const line = raw.trim();
579
- if (!line.startsWith("at ")) continue;
775
+ if (line.length > MAX_FRAME_LINE || !line.startsWith("at ")) continue;
580
776
  const withFunction = WITH_FUNCTION.exec(line);
581
777
  const fn = withFunction?.[1];
582
778
  const location = withFunction ? withFunction[2] ?? "" : line.replace(/^at (?:async )?/, "");
@@ -635,20 +831,110 @@ function safeStringify(value) {
635
831
  }
636
832
  }
637
833
 
638
- // src/transport.ts
639
- import { promisify } from "util";
640
- import { gzip } from "zlib";
641
-
642
834
  // src/util.ts
643
- import { performance as performance2 } from "perf_hooks";
835
+ import { performance as performance3 } from "perf_hooks";
644
836
  function describeError(error) {
645
837
  return error instanceof Error ? error.message : String(error);
646
838
  }
647
839
  function elapsedSince(startedAt) {
648
- return Math.round(performance2.now() - startedAt);
840
+ return Math.round(performance3.now() - startedAt);
841
+ }
842
+
843
+ // src/trace-fetch.ts
844
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
845
+ var REQUEST_ID_HEADER = "x-request-id";
846
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
847
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
848
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
849
+ function secretSegment(segment) {
850
+ if (UUID.test(segment)) return false;
851
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
852
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
853
+ }
854
+ function outboundPath(pathname, mode) {
855
+ if (mode === "none") return pathname;
856
+ return pathname.split("/").map((segment) => {
857
+ let decoded = segment;
858
+ try {
859
+ decoded = decodeURIComponent(segment);
860
+ } catch {
861
+ decoded = segment;
862
+ }
863
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
864
+ }).join("/");
865
+ }
866
+ function failureOf(error) {
867
+ const message = describeError(error);
868
+ const cause = error instanceof Error ? error.cause : void 0;
869
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
870
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
871
+ }
872
+ function urlOf(input) {
873
+ try {
874
+ if (typeof input === "string") return new URL(input);
875
+ if (input instanceof URL) return input;
876
+ return new URL(input.url);
877
+ } catch {
878
+ return null;
879
+ }
880
+ }
881
+ function originOf(value) {
882
+ try {
883
+ return new URL(value).origin;
884
+ } catch {
885
+ return null;
886
+ }
887
+ }
888
+ function targets(url, target) {
889
+ if (target instanceof RegExp) return target.test(url.host);
890
+ const wanted = target.trim().toLowerCase();
891
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
892
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
893
+ }
894
+ function isPlainObject(value) {
895
+ if (value === null || typeof value !== "object") return false;
896
+ const prototype = Object.getPrototypeOf(value);
897
+ return prototype === Object.prototype || prototype === null;
898
+ }
899
+ function withRequestId(input, init, requestId) {
900
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
901
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
902
+ headers.set(REQUEST_ID_HEADER, requestId);
903
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
904
+ return [new Request(input, init), { headers }];
905
+ }
906
+ function traceFetch(config) {
907
+ const current = globalThis.fetch;
908
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
909
+ const original = current;
910
+ const radarOrigin = originOf(config.endpoint);
911
+ const traced = async (input, init) => {
912
+ const url = urlOf(input);
913
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
914
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
915
+ const context = currentContext();
916
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
917
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
918
+ const started = performance.now();
919
+ try {
920
+ const response = await original(nextInput, nextInit);
921
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
922
+ return response;
923
+ } catch (error) {
924
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
925
+ throw error;
926
+ }
927
+ };
928
+ Object.defineProperty(traced, WRAPPED, { value: original });
929
+ globalThis.fetch = traced;
930
+ return () => {
931
+ if (globalThis.fetch === traced) globalThis.fetch = original;
932
+ };
649
933
  }
650
934
 
651
935
  // src/transport.ts
936
+ import { promisify } from "util";
937
+ import { gzip } from "zlib";
652
938
  var gzipAsync = promisify(gzip);
653
939
  var MAX_BATCH_BYTES = 9e5;
654
940
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -671,6 +957,7 @@ var Transport = class {
671
957
  inFlight = null;
672
958
  nextAttemptAt = 0;
673
959
  backoffMs = 0;
960
+ unauthorizedUntil = 0;
674
961
  stopped = false;
675
962
  warned = /* @__PURE__ */ new Set();
676
963
  timer;
@@ -760,10 +1047,8 @@ var Transport = class {
760
1047
  this.nextAttemptAt = Date.now() + Math.max(this.backoffMs, outcome.retryAfterMs ?? 0);
761
1048
  return false;
762
1049
  case "unauthorized":
763
- this.droppedCount += events.length + this.queue.length;
764
- this.queue = [];
765
- this.nextAttemptAt = Date.now() + UNAUTHORIZED_PAUSE_MS;
766
- this.warn("unauthorized", `chave do Radar recusada (${outcome.status}); envios pausados por 10 minutos`, true);
1050
+ this.droppedCount += events.length;
1051
+ this.pauseUnauthorized(outcome.status);
767
1052
  return false;
768
1053
  case "rejected":
769
1054
  this.droppedCount += events.length;
@@ -771,6 +1056,47 @@ var Transport = class {
771
1056
  return true;
772
1057
  }
773
1058
  }
1059
+ async checkIn(slug, payload) {
1060
+ if (this.stopped || !this.config.checkInBaseUrl || Date.now() < this.unauthorizedUntil) return;
1061
+ let response;
1062
+ try {
1063
+ response = await fetch(this.config.checkInBaseUrl + encodeURIComponent(slug), {
1064
+ method: "POST",
1065
+ headers: this.headers(),
1066
+ body: JSON.stringify(payload),
1067
+ redirect: "error",
1068
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? 5e3)
1069
+ });
1070
+ } catch (error) {
1071
+ this.warn("checkin_network", `check-in no Radar falhou: ${describeError(error)}`, false);
1072
+ return;
1073
+ }
1074
+ await response.arrayBuffer().catch(() => void 0);
1075
+ if (response.ok) return;
1076
+ if (response.status === 401 || response.status === 403) {
1077
+ this.pauseUnauthorized(response.status);
1078
+ return;
1079
+ }
1080
+ if (response.status === 404) {
1081
+ this.warn(`checkin_unknown_${slug}`, `tarefa agendada "${slug}" n\xE3o existe no projeto da chave; crie o monitor com esse slug no Radar`, true);
1082
+ return;
1083
+ }
1084
+ this.warn(`checkin_rejected_${response.status}`, `check-in recusado pelo Radar (${response.status})`, false);
1085
+ }
1086
+ pauseUnauthorized(status) {
1087
+ this.droppedCount += this.queue.length;
1088
+ this.queue = [];
1089
+ this.unauthorizedUntil = Date.now() + UNAUTHORIZED_PAUSE_MS;
1090
+ this.nextAttemptAt = Math.max(this.nextAttemptAt, this.unauthorizedUntil);
1091
+ this.warn("unauthorized", `chave do Radar recusada (${status}); envios pausados por 10 minutos`, true);
1092
+ }
1093
+ headers() {
1094
+ return {
1095
+ "content-type": "application/json",
1096
+ authorization: `Bearer ${this.config.key}`,
1097
+ "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
1098
+ };
1099
+ }
774
1100
  requeue(events) {
775
1101
  this.queue.unshift(...events);
776
1102
  const overflow = this.queue.length - this.maxQueue;
@@ -782,11 +1108,7 @@ var Transport = class {
782
1108
  async post(batch) {
783
1109
  const json = JSON.stringify(batch);
784
1110
  const compress = Buffer.byteLength(json) > GZIP_THRESHOLD_BYTES;
785
- const headers = {
786
- "content-type": "application/json",
787
- authorization: `Bearer ${this.config.key}`,
788
- "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
789
- };
1111
+ const headers = this.headers();
790
1112
  if (compress) headers["content-encoding"] = "gzip";
791
1113
  let response;
792
1114
  try {
@@ -840,15 +1162,19 @@ function withTimeout(promise, ms) {
840
1162
 
841
1163
  // src/version.ts
842
1164
  var SDK_NAME = "@oconde/radar";
843
- var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
1165
+ var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
844
1166
 
845
1167
  // src/client.ts
1168
+ var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
1169
+ var MAX_CHECK_IN_ENVIRONMENT = 64;
1170
+ var MAX_CHECK_IN_RELEASE = 128;
846
1171
  var RadarClient = class {
847
1172
  options = resolveOptions({}, {});
848
1173
  transport = null;
849
1174
  initialized = false;
850
1175
  closed = false;
851
1176
  captured = /* @__PURE__ */ new WeakSet();
1177
+ restoreFetch = null;
852
1178
  get isEnabled() {
853
1179
  return this.transport !== null && !this.closed;
854
1180
  }
@@ -871,10 +1197,19 @@ var RadarClient = class {
871
1197
  sdk: { name: SDK_NAME, version: SDK_VERSION },
872
1198
  environment: this.options.environment,
873
1199
  ...this.options.release ? { release: this.options.release } : {},
1200
+ checkInBaseUrl: this.options.endpoint + CHECKIN_PATH,
874
1201
  onWarning: (message, always) => always ? this.writeWarning(message) : this.debugWarn(message)
875
1202
  });
876
1203
  }
877
1204
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1205
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1206
+ this.restoreFetch = traceFetch({
1207
+ endpoint: this.options.endpoint,
1208
+ propagateTo: this.options.traceFetch.propagateTo,
1209
+ redact: this.options.redact,
1210
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1211
+ });
1212
+ }
878
1213
  } catch (error) {
879
1214
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
880
1215
  }
@@ -918,7 +1253,7 @@ var RadarClient = class {
918
1253
  }
919
1254
  async track(name, fn, data) {
920
1255
  const execute = async () => {
921
- const started = performance3.now();
1256
+ const started = performance4.now();
922
1257
  try {
923
1258
  const result = await fn();
924
1259
  this.info(name, { ...data, durationMs: elapsedSince(started), ok: true });
@@ -930,7 +1265,52 @@ var RadarClient = class {
930
1265
  }
931
1266
  };
932
1267
  if (currentContext()) return execute();
933
- return runWithContext({ requestId: randomUUID2(), startedAt: performance3.now() }, execute);
1268
+ return runWithContext({ requestId: randomUUID2(), startedAt: performance4.now() }, execute);
1269
+ }
1270
+ withContext(fn, options = {}) {
1271
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance4.now() }, fn);
1272
+ }
1273
+ async checkIn(slug, status, options = {}) {
1274
+ try {
1275
+ if (!this.transport || this.closed) return;
1276
+ if (typeof slug !== "string" || !CHECKIN_SLUG.test(slug)) {
1277
+ this.debugWarn(`slug de tarefa agendada inv\xE1lido: ${String(slug).slice(0, 80)}`);
1278
+ return;
1279
+ }
1280
+ if (!CHECKIN_STATUSES.includes(status)) return;
1281
+ const checkInId = typeof options.checkInId === "string" && options.checkInId ? options.checkInId.slice(0, LIMITS.checkInIdLength) : void 0;
1282
+ 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;
1283
+ const environment = this.options.environment.slice(0, MAX_CHECK_IN_ENVIRONMENT) || "production";
1284
+ const release = this.options.release?.slice(0, MAX_CHECK_IN_RELEASE);
1285
+ const payload = {
1286
+ status,
1287
+ ...checkInId ? { checkInId } : {},
1288
+ ...durationMs !== void 0 ? { durationMs } : {},
1289
+ environment,
1290
+ ...release ? { release } : {}
1291
+ };
1292
+ await this.transport.checkIn(slug, payload);
1293
+ } catch (error) {
1294
+ this.debugWarn(`check-in falhou: ${describeError(error)}`);
1295
+ }
1296
+ }
1297
+ async cron(slug, fn) {
1298
+ const checkInId = randomUUID2();
1299
+ const started = performance4.now();
1300
+ const execute = async () => {
1301
+ await this.checkIn(slug, "in_progress", { checkInId });
1302
+ try {
1303
+ const result = await fn();
1304
+ await this.checkIn(slug, "ok", { checkInId, durationMs: elapsedSince(started) });
1305
+ return result;
1306
+ } catch (error) {
1307
+ this.captureError(error, { cron: slug });
1308
+ await this.checkIn(slug, "error", { checkInId, durationMs: elapsedSince(started) });
1309
+ throw error;
1310
+ }
1311
+ };
1312
+ if (currentContext()) return execute();
1313
+ return this.withContext(execute);
934
1314
  }
935
1315
  middleware() {
936
1316
  return createMiddleware(this);
@@ -949,7 +1329,7 @@ var RadarClient = class {
949
1329
  () => this.emitLog(level, "http.request", {
950
1330
  method: (req.method ?? "GET").toUpperCase(),
951
1331
  ...route ? { route } : {},
952
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), req.params, this.options.redact) } : {},
1332
+ ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
953
1333
  status,
954
1334
  durationMs: elapsedSince(context.startedAt)
955
1335
  })
@@ -966,6 +1346,8 @@ var RadarClient = class {
966
1346
  async close(timeoutMs = 2e3) {
967
1347
  if (this.closed) return;
968
1348
  await this.flush(timeoutMs);
1349
+ this.restoreFetch?.();
1350
+ this.restoreFetch = null;
969
1351
  this.closed = true;
970
1352
  this.transport?.stop();
971
1353
  }
@@ -986,31 +1368,40 @@ var RadarClient = class {
986
1368
  this.print(event);
987
1369
  this.transport?.enqueue(event);
988
1370
  }
1371
+ captureException(exception, options = {}) {
1372
+ this.safely(() => this.emitException(exception, options));
1373
+ }
989
1374
  emitError(error, data, level, handled) {
990
1375
  if (this.closed) return;
991
1376
  if (typeof error === "object" && error !== null) {
992
1377
  if (this.captured.has(error)) return;
993
1378
  this.captured.add(error);
994
1379
  }
1380
+ this.emitException(exceptionInfo(error), { level, handled, ...data !== void 0 ? { attrs: data } : {} }, error instanceof Error ? error.stack : void 0);
1381
+ }
1382
+ emitException(exception, options, stack) {
1383
+ if (this.closed) return;
995
1384
  const context = currentContext();
996
1385
  const mode = this.options.redact;
1386
+ const request = options.request === null ? void 0 : options.request ?? (context?.req ? requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) : void 0);
1387
+ const runtime = options.runtime === void 0 ? runtimeInfo() : options.runtime;
997
1388
  const event = {
998
1389
  type: "error",
999
1390
  ts: Date.now(),
1000
- level,
1001
- handled,
1002
- exception: exceptionInfo(error),
1391
+ level: options.level ?? "error",
1392
+ handled: options.handled ?? true,
1393
+ exception,
1003
1394
  ...context ? { requestId: context.requestId } : {},
1004
- ...context?.req ? { request: requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) } : {},
1395
+ ...request ? { request } : {},
1005
1396
  ...context?.user ? { user: context.user } : {},
1006
- runtime: runtimeInfo(),
1007
- ...data !== void 0 ? { attrs: prepareAttrs(data, mode) } : {}
1397
+ ...runtime ? { runtime } : {},
1398
+ ...options.attrs !== void 0 ? { attrs: prepareAttrs(options.attrs, mode) } : {}
1008
1399
  };
1009
- this.print(event, error instanceof Error ? error.stack : void 0);
1400
+ this.print(event, stack);
1010
1401
  if (!this.transport) return;
1011
1402
  this.transport.enqueue(
1012
- () => enrichException(event.exception).then(
1013
- (exception) => ({ ...event, exception }),
1403
+ () => enrichException(event.exception, process.cwd(), options.enrich).then(
1404
+ (enriched) => ({ ...event, exception: enriched }),
1014
1405
  () => event
1015
1406
  )
1016
1407
  );