@oconde/radar 0.1.0 → 0.2.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,6 +69,11 @@ 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";
@@ -49,29 +81,8 @@ import { hostname } from "os";
49
81
  import { performance as performance3 } from "perf_hooks";
50
82
 
51
83
  // src/express.ts
52
- import { randomUUID } from "crypto";
53
84
  import { performance } from "perf_hooks";
54
85
 
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
- };
74
-
75
86
  // src/redact.ts
76
87
  import { createHash, createHmac, randomBytes } from "crypto";
77
88
  var SECRET_WORDS = /* @__PURE__ */ new Set([
@@ -272,6 +283,40 @@ function requestRoute(req, route) {
272
283
  if (route) return route;
273
284
  return typeof req.route?.path === "string" ? req.route.path : void 0;
274
285
  }
286
+ var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
287
+ function pathSegments(value) {
288
+ const segments = value.split("/");
289
+ while (segments.length > 1 && segments[segments.length - 1] === "") segments.pop();
290
+ return segments;
291
+ }
292
+ function decodeSegment(value) {
293
+ try {
294
+ return decodeURIComponent(value);
295
+ } catch {
296
+ return value;
297
+ }
298
+ }
299
+ function routeParams(req, path) {
300
+ if (typeof req.route?.path !== "string") return {};
301
+ const pattern = pathSegments(`${typeof req.baseUrl === "string" ? req.baseUrl : ""}${req.route.path}`).filter((segment, index) => index > 0 || segment !== "");
302
+ const values = pathSegments(path);
303
+ if (pattern.length > values.length) return {};
304
+ const offset = values.length - pattern.length;
305
+ const result = {};
306
+ for (let index = 0; index < pattern.length; index++) {
307
+ const value = values[offset + index];
308
+ const param = ROUTE_PARAM.exec(pattern[index]);
309
+ if (!param) {
310
+ if (pattern[index] !== value) return {};
311
+ continue;
312
+ }
313
+ if (value) result[param[1]] = decodeSegment(value);
314
+ }
315
+ return result;
316
+ }
317
+ function pathParams(req, path) {
318
+ return { ...routeParams(req, path), ...req.params ?? {} };
319
+ }
275
320
  function redactPath(path, params, mode) {
276
321
  if (mode === "none" || !params) return path;
277
322
  let result = path;
@@ -287,7 +332,7 @@ function safeUrl(req, mode) {
287
332
  const queryStart = url.indexOf("?");
288
333
  const path = queryStart >= 0 ? url.slice(0, queryStart) : url;
289
334
  const query = queryStart >= 0 ? url.slice(queryStart) : "";
290
- return redactUrl(redactPath(path, req.params, mode) + query, mode);
335
+ return redactUrl(redactPath(path, pathParams(req, path), mode) + query, mode);
291
336
  }
292
337
  function redactUrl(url, mode) {
293
338
  const queryStart = url.indexOf("?");
@@ -355,8 +400,7 @@ function createMiddleware(logger) {
355
400
  return (req, res, next) => {
356
401
  let context;
357
402
  try {
358
- const incoming = headerValue(req.headers["x-request-id"])?.trim();
359
- const requestId = incoming && incoming.length <= LIMITS.requestIdLength ? incoming : randomUUID();
403
+ const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
360
404
  context = { requestId, startedAt: performance.now(), req };
361
405
  res.setHeader("x-request-id", requestId);
362
406
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
@@ -447,16 +491,92 @@ import { readFile, stat } from "fs/promises";
447
491
  import { SourceMap } from "module";
448
492
  import { dirname, isAbsolute, relative, resolve, sep } from "path";
449
493
  import { fileURLToPath } from "url";
494
+
495
+ // src/function-name.ts
496
+ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
497
+ var MAX_LINES_UP = 400;
498
+ var MAX_HEADER_LINES = 6;
499
+ var MAX_NAME_LENGTH = 100;
500
+ var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
501
+ var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
502
+ var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
503
+ var KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "await", "else", "do", "try"]);
504
+ var NAME_PATTERNS = [
505
+ new RegExp(`\\bfunction\\s*\\*?\\s*(${IDENTIFIER})\\s*\\(`),
506
+ new RegExp(`(${IDENTIFIER})\\s*[:=]\\s*(?:async\\s+)?function\\b`),
507
+ new RegExp(`(${IDENTIFIER})\\s*(?::\\s*[^=]+)?=\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`),
508
+ new RegExp(`(?:^|[{,(]\\s*)(${IDENTIFIER})\\s*:\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`)
509
+ ];
510
+ var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
511
+ var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
512
+ function codeOnly(text) {
513
+ return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
514
+ }
515
+ function parenBalance(text) {
516
+ let balance = 0;
517
+ for (const char of text) {
518
+ if (char === "(") balance += 1;
519
+ else if (char === ")") balance -= 1;
520
+ }
521
+ return balance;
522
+ }
523
+ function headerOf(lines, index, prefix) {
524
+ let header = prefix;
525
+ for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
526
+ if (header.trim() !== "" && parenBalance(header) >= 0) break;
527
+ header = `${codeOnly(lines[previous] ?? "")} ${header}`;
528
+ }
529
+ return header;
530
+ }
531
+ function nameIn(header) {
532
+ const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
533
+ for (const pattern of patterns) {
534
+ const name = pattern.exec(header)?.[1];
535
+ if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
536
+ }
537
+ return void 0;
538
+ }
539
+ function enclosingFunctionName(lines, line, column) {
540
+ const start = line - 1;
541
+ if (start < 0 || start >= lines.length) return void 0;
542
+ let depth = 0;
543
+ for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
544
+ const raw = lines[index] ?? "";
545
+ if (index < start && MARKUP_BOUNDARY.test(raw)) return void 0;
546
+ const code = codeOnly(index === start && column ? raw.slice(0, Math.max(0, column - 1)) : raw);
547
+ for (let position = code.length - 1; position >= 0; position--) {
548
+ const char = code[position];
549
+ if (char === "}") {
550
+ depth += 1;
551
+ } else if (char === "{") {
552
+ if (depth > 0) {
553
+ depth -= 1;
554
+ continue;
555
+ }
556
+ const header = headerOf(lines, index, code.slice(0, position));
557
+ if (CLASS_BODY.test(header)) return void 0;
558
+ if (CONTROL_BLOCK.test(header)) continue;
559
+ const name = nameIn(header);
560
+ if (name) return name;
561
+ }
562
+ }
563
+ }
564
+ return void 0;
565
+ }
566
+
567
+ // src/source.ts
450
568
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
451
569
  var MAX_CACHED_FILES = 50;
452
570
  var MAPPING_URL = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
453
571
  var cache = /* @__PURE__ */ new Map();
454
- async function enrichFrames(frames, cwd = process.cwd()) {
572
+ async function enrichFrames(frames, cwd = process.cwd(), options = {}) {
455
573
  const result = [];
456
574
  let withContext = 0;
457
575
  for (const frame of frames) {
458
576
  try {
459
- const { frame: mapped, lines } = await mapFrame(frame);
577
+ const { frame: located, lines, original } = await mapFrame(frame, options.mapLocator);
578
+ const named = options.inferFunctionNames && original && !located.fn && lines && located.line ? withInferredName(located, lines) : located;
579
+ const mapped = named.inApp && isLibraryPath(named.file) ? { ...named, inApp: false } : named;
460
580
  let next = mapped;
461
581
  if (mapped.inApp && lines && mapped.line && withContext < LIMITS.contextFrames) {
462
582
  const context = contextFor(lines, mapped.line);
@@ -472,29 +592,36 @@ async function enrichFrames(frames, cwd = process.cwd()) {
472
592
  }
473
593
  return result;
474
594
  }
475
- async function enrichException(exception, cwd = process.cwd()) {
476
- const frames = await enrichFrames(exception.frames, cwd);
595
+ async function enrichException(exception, cwd = process.cwd(), options = {}) {
596
+ const frames = await enrichFrames(exception.frames, cwd, options);
477
597
  if (!exception.cause) return { ...exception, frames };
478
- return { ...exception, frames, cause: await enrichException(exception.cause, cwd) };
598
+ return { ...exception, frames, cause: await enrichException(exception.cause, cwd, options) };
599
+ }
600
+ function withInferredName(frame, lines) {
601
+ const fn = enclosingFunctionName(lines, frame.line ?? 0, frame.col);
602
+ return fn ? { ...frame, fn } : frame;
603
+ }
604
+ function isLibraryPath(file) {
605
+ return file.includes("/node_modules/") || file.includes("\\node_modules\\");
479
606
  }
480
607
  function relativize(file, cwd) {
481
608
  if (!isAbsolute(file)) return file;
482
609
  const path = relative(cwd, file);
483
610
  return path.startsWith("..") || isAbsolute(path) ? file : path.split(sep).join("/");
484
611
  }
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 };
612
+ async function mapFrame(frame, locator) {
613
+ if (!frame.line || !isAbsolute(frame.file)) return { frame, lines: null, original: false };
614
+ const file = await loadFile(frame.file, locator);
615
+ if (!file) return { frame, lines: null, original: false };
616
+ if (!file.map) return { frame, lines: file.lines, original: false };
490
617
  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 };
618
+ if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines, original: false };
492
619
  const originalPath = resolveSource(file.map, entry.originalSource);
493
620
  const mapped = { ...frame, file: originalPath, line: entry.originalLine + 1, col: (entry.originalColumn ?? 0) + 1 };
494
621
  const lines = embeddedLines(file.map, file.map.sources.indexOf(entry.originalSource));
495
- if (lines) return { frame: mapped, lines };
622
+ if (lines) return { frame: mapped, lines, original: true };
496
623
  const original = await loadFile(originalPath);
497
- return { frame: mapped, lines: original?.lines ?? null };
624
+ return { frame: mapped, lines: original?.lines ?? null, original: true };
498
625
  }
499
626
  function embeddedLines(map, index) {
500
627
  if (index < 0) return null;
@@ -526,26 +653,27 @@ function resolveSource(map, source) {
526
653
  }
527
654
  return isAbsolute(source) ? source : resolve(map.baseDir, source);
528
655
  }
529
- function loadFile(path) {
530
- const cached = cache.get(path);
656
+ function loadFile(path, locator) {
657
+ const key = locator ? `${path}\0located` : path;
658
+ const cached = cache.get(key);
531
659
  if (cached) {
532
- cache.delete(path);
533
- cache.set(path, cached);
660
+ cache.delete(key);
661
+ cache.set(key, cached);
534
662
  return cached;
535
663
  }
536
- const pending = readSource(path).catch(() => null);
537
- cache.set(path, pending);
664
+ const pending = readSource(path, locator).catch(() => null);
665
+ cache.set(key, pending);
538
666
  if (cache.size > MAX_CACHED_FILES) {
539
667
  const oldest = cache.keys().next().value;
540
668
  if (oldest !== void 0) cache.delete(oldest);
541
669
  }
542
670
  return pending;
543
671
  }
544
- async function readSource(path) {
672
+ async function readSource(path, locator) {
545
673
  const text = await readText(path);
546
674
  if (text === null) return null;
547
675
  const lines = text.split(/\r?\n/);
548
- return { lines, map: await loadMap(path, lines) };
676
+ return { lines, map: await loadMap(path, lines, locator) };
549
677
  }
550
678
  async function readText(path) {
551
679
  try {
@@ -556,7 +684,7 @@ async function readText(path) {
556
684
  return null;
557
685
  }
558
686
  }
559
- async function loadMap(path, lines) {
687
+ async function loadMap(path, lines, locator) {
560
688
  let url;
561
689
  for (let index = lines.length - 1; index >= 0 && index >= lines.length - 5; index--) {
562
690
  const match = MAPPING_URL.exec(lines[index] ?? "");
@@ -565,10 +693,15 @@ async function loadMap(path, lines) {
565
693
  break;
566
694
  }
567
695
  }
568
- if (!url) return null;
569
696
  let raw;
570
697
  let mapDir;
571
- if (url.startsWith("data:")) {
698
+ const located = url ? null : locator ? locator(path) : `${path}.map`;
699
+ if (located) {
700
+ raw = await readText(located);
701
+ mapDir = dirname(located);
702
+ } else if (!url) {
703
+ return null;
704
+ } else if (url.startsWith("data:")) {
572
705
  const comma = url.indexOf(",");
573
706
  const encoded = url.slice(comma + 1);
574
707
  raw = url.slice(0, comma).endsWith(";base64") ? Buffer.from(encoded, "base64").toString("utf8") : decodeURIComponent(encoded);
@@ -597,12 +730,13 @@ async function loadMap(path, lines) {
597
730
  import { fileURLToPath as fileURLToPath2 } from "url";
598
731
  var WITH_FUNCTION = /^at (?:async )?(.+?) \((.+)\)$/;
599
732
  var LOCATION = /^(.+):(\d+):(\d+)$/;
733
+ var MAX_FRAME_LINE = 1e3;
600
734
  function parseStack(stack) {
601
735
  if (!stack) return [];
602
736
  const frames = [];
603
737
  for (const raw of stack.split("\n")) {
604
738
  const line = raw.trim();
605
- if (!line.startsWith("at ")) continue;
739
+ if (line.length > MAX_FRAME_LINE || !line.startsWith("at ")) continue;
606
740
  const withFunction = WITH_FUNCTION.exec(line);
607
741
  const fn = withFunction?.[1];
608
742
  const location = withFunction ? withFunction[2] ?? "" : line.replace(/^at (?:async )?/, "");
@@ -697,6 +831,7 @@ var Transport = class {
697
831
  inFlight = null;
698
832
  nextAttemptAt = 0;
699
833
  backoffMs = 0;
834
+ unauthorizedUntil = 0;
700
835
  stopped = false;
701
836
  warned = /* @__PURE__ */ new Set();
702
837
  timer;
@@ -786,10 +921,8 @@ var Transport = class {
786
921
  this.nextAttemptAt = Date.now() + Math.max(this.backoffMs, outcome.retryAfterMs ?? 0);
787
922
  return false;
788
923
  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);
924
+ this.droppedCount += events.length;
925
+ this.pauseUnauthorized(outcome.status);
793
926
  return false;
794
927
  case "rejected":
795
928
  this.droppedCount += events.length;
@@ -797,6 +930,47 @@ var Transport = class {
797
930
  return true;
798
931
  }
799
932
  }
933
+ async checkIn(slug, payload) {
934
+ if (this.stopped || !this.config.checkInBaseUrl || Date.now() < this.unauthorizedUntil) return;
935
+ let response;
936
+ try {
937
+ response = await fetch(this.config.checkInBaseUrl + encodeURIComponent(slug), {
938
+ method: "POST",
939
+ headers: this.headers(),
940
+ body: JSON.stringify(payload),
941
+ redirect: "error",
942
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? 5e3)
943
+ });
944
+ } catch (error) {
945
+ this.warn("checkin_network", `check-in no Radar falhou: ${describeError(error)}`, false);
946
+ return;
947
+ }
948
+ await response.arrayBuffer().catch(() => void 0);
949
+ if (response.ok) return;
950
+ if (response.status === 401 || response.status === 403) {
951
+ this.pauseUnauthorized(response.status);
952
+ return;
953
+ }
954
+ if (response.status === 404) {
955
+ this.warn(`checkin_unknown_${slug}`, `tarefa agendada "${slug}" n\xE3o existe no projeto da chave; crie o monitor com esse slug no Radar`, true);
956
+ return;
957
+ }
958
+ this.warn(`checkin_rejected_${response.status}`, `check-in recusado pelo Radar (${response.status})`, false);
959
+ }
960
+ pauseUnauthorized(status) {
961
+ this.droppedCount += this.queue.length;
962
+ this.queue = [];
963
+ this.unauthorizedUntil = Date.now() + UNAUTHORIZED_PAUSE_MS;
964
+ this.nextAttemptAt = Math.max(this.nextAttemptAt, this.unauthorizedUntil);
965
+ this.warn("unauthorized", `chave do Radar recusada (${status}); envios pausados por 10 minutos`, true);
966
+ }
967
+ headers() {
968
+ return {
969
+ "content-type": "application/json",
970
+ authorization: `Bearer ${this.config.key}`,
971
+ "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
972
+ };
973
+ }
800
974
  requeue(events) {
801
975
  this.queue.unshift(...events);
802
976
  const overflow = this.queue.length - this.maxQueue;
@@ -808,11 +982,7 @@ var Transport = class {
808
982
  async post(batch) {
809
983
  const json = JSON.stringify(batch);
810
984
  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
- };
985
+ const headers = this.headers();
816
986
  if (compress) headers["content-encoding"] = "gzip";
817
987
  let response;
818
988
  try {
@@ -866,9 +1036,12 @@ function withTimeout(promise, ms) {
866
1036
 
867
1037
  // src/version.ts
868
1038
  var SDK_NAME = "@oconde/radar";
869
- var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
1039
+ var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
870
1040
 
871
1041
  // src/client.ts
1042
+ var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
1043
+ var MAX_CHECK_IN_ENVIRONMENT = 64;
1044
+ var MAX_CHECK_IN_RELEASE = 128;
872
1045
  var RadarClient = class {
873
1046
  options = resolveOptions({}, {});
874
1047
  transport = null;
@@ -897,6 +1070,7 @@ var RadarClient = class {
897
1070
  sdk: { name: SDK_NAME, version: SDK_VERSION },
898
1071
  environment: this.options.environment,
899
1072
  ...this.options.release ? { release: this.options.release } : {},
1073
+ checkInBaseUrl: this.options.endpoint + CHECKIN_PATH,
900
1074
  onWarning: (message, always) => always ? this.writeWarning(message) : this.debugWarn(message)
901
1075
  });
902
1076
  }
@@ -958,6 +1132,51 @@ var RadarClient = class {
958
1132
  if (currentContext()) return execute();
959
1133
  return runWithContext({ requestId: randomUUID2(), startedAt: performance3.now() }, execute);
960
1134
  }
1135
+ withContext(fn, options = {}) {
1136
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance3.now() }, fn);
1137
+ }
1138
+ async checkIn(slug, status, options = {}) {
1139
+ try {
1140
+ if (!this.transport || this.closed) return;
1141
+ if (typeof slug !== "string" || !CHECKIN_SLUG.test(slug)) {
1142
+ this.debugWarn(`slug de tarefa agendada inv\xE1lido: ${String(slug).slice(0, 80)}`);
1143
+ return;
1144
+ }
1145
+ if (!CHECKIN_STATUSES.includes(status)) return;
1146
+ const checkInId = typeof options.checkInId === "string" && options.checkInId ? options.checkInId.slice(0, LIMITS.checkInIdLength) : void 0;
1147
+ 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;
1148
+ const environment = this.options.environment.slice(0, MAX_CHECK_IN_ENVIRONMENT) || "production";
1149
+ const release = this.options.release?.slice(0, MAX_CHECK_IN_RELEASE);
1150
+ const payload = {
1151
+ status,
1152
+ ...checkInId ? { checkInId } : {},
1153
+ ...durationMs !== void 0 ? { durationMs } : {},
1154
+ environment,
1155
+ ...release ? { release } : {}
1156
+ };
1157
+ await this.transport.checkIn(slug, payload);
1158
+ } catch (error) {
1159
+ this.debugWarn(`check-in falhou: ${describeError(error)}`);
1160
+ }
1161
+ }
1162
+ async cron(slug, fn) {
1163
+ const checkInId = randomUUID2();
1164
+ const started = performance3.now();
1165
+ const execute = async () => {
1166
+ await this.checkIn(slug, "in_progress", { checkInId });
1167
+ try {
1168
+ const result = await fn();
1169
+ await this.checkIn(slug, "ok", { checkInId, durationMs: elapsedSince(started) });
1170
+ return result;
1171
+ } catch (error) {
1172
+ this.captureError(error, { cron: slug });
1173
+ await this.checkIn(slug, "error", { checkInId, durationMs: elapsedSince(started) });
1174
+ throw error;
1175
+ }
1176
+ };
1177
+ if (currentContext()) return execute();
1178
+ return this.withContext(execute);
1179
+ }
961
1180
  middleware() {
962
1181
  return createMiddleware(this);
963
1182
  }
@@ -975,7 +1194,7 @@ var RadarClient = class {
975
1194
  () => this.emitLog(level, "http.request", {
976
1195
  method: (req.method ?? "GET").toUpperCase(),
977
1196
  ...route ? { route } : {},
978
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), req.params, this.options.redact) } : {},
1197
+ ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
979
1198
  status,
980
1199
  durationMs: elapsedSince(context.startedAt)
981
1200
  })
@@ -1012,31 +1231,40 @@ var RadarClient = class {
1012
1231
  this.print(event);
1013
1232
  this.transport?.enqueue(event);
1014
1233
  }
1234
+ captureException(exception, options = {}) {
1235
+ this.safely(() => this.emitException(exception, options));
1236
+ }
1015
1237
  emitError(error, data, level, handled) {
1016
1238
  if (this.closed) return;
1017
1239
  if (typeof error === "object" && error !== null) {
1018
1240
  if (this.captured.has(error)) return;
1019
1241
  this.captured.add(error);
1020
1242
  }
1243
+ this.emitException(exceptionInfo(error), { level, handled, ...data !== void 0 ? { attrs: data } : {} }, error instanceof Error ? error.stack : void 0);
1244
+ }
1245
+ emitException(exception, options, stack) {
1246
+ if (this.closed) return;
1021
1247
  const context = currentContext();
1022
1248
  const mode = this.options.redact;
1249
+ const request = options.request === null ? void 0 : options.request ?? (context?.req ? requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) : void 0);
1250
+ const runtime = options.runtime === void 0 ? runtimeInfo() : options.runtime;
1023
1251
  const event = {
1024
1252
  type: "error",
1025
1253
  ts: Date.now(),
1026
- level,
1027
- handled,
1028
- exception: exceptionInfo(error),
1254
+ level: options.level ?? "error",
1255
+ handled: options.handled ?? true,
1256
+ exception,
1029
1257
  ...context ? { requestId: context.requestId } : {},
1030
- ...context?.req ? { request: requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) } : {},
1258
+ ...request ? { request } : {},
1031
1259
  ...context?.user ? { user: context.user } : {},
1032
- runtime: runtimeInfo(),
1033
- ...data !== void 0 ? { attrs: prepareAttrs(data, mode) } : {}
1260
+ ...runtime ? { runtime } : {},
1261
+ ...options.attrs !== void 0 ? { attrs: prepareAttrs(options.attrs, mode) } : {}
1034
1262
  };
1035
- this.print(event, error instanceof Error ? error.stack : void 0);
1263
+ this.print(event, stack);
1036
1264
  if (!this.transport) return;
1037
1265
  this.transport.enqueue(
1038
- () => enrichException(event.exception).then(
1039
- (exception) => ({ ...event, exception }),
1266
+ () => enrichException(event.exception, process.cwd(), options.enrich).then(
1267
+ (enriched) => ({ ...event, exception: enriched }),
1040
1268
  () => event
1041
1269
  )
1042
1270
  );