@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.
package/dist/index.js CHANGED
@@ -5,29 +5,13 @@ import { performance as performance3 } 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 } 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([
@@ -246,6 +257,40 @@ function requestRoute(req, route) {
246
257
  if (route) return route;
247
258
  return typeof req.route?.path === "string" ? req.route.path : void 0;
248
259
  }
260
+ var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
261
+ function pathSegments(value) {
262
+ const segments = value.split("/");
263
+ while (segments.length > 1 && segments[segments.length - 1] === "") segments.pop();
264
+ return segments;
265
+ }
266
+ function decodeSegment(value) {
267
+ try {
268
+ return decodeURIComponent(value);
269
+ } catch {
270
+ return value;
271
+ }
272
+ }
273
+ function routeParams(req, path) {
274
+ if (typeof req.route?.path !== "string") return {};
275
+ const pattern = pathSegments(`${typeof req.baseUrl === "string" ? req.baseUrl : ""}${req.route.path}`).filter((segment, index) => index > 0 || segment !== "");
276
+ const values = pathSegments(path);
277
+ if (pattern.length > values.length) return {};
278
+ const offset = values.length - pattern.length;
279
+ const result = {};
280
+ for (let index = 0; index < pattern.length; index++) {
281
+ const value = values[offset + index];
282
+ const param = ROUTE_PARAM.exec(pattern[index]);
283
+ if (!param) {
284
+ if (pattern[index] !== value) return {};
285
+ continue;
286
+ }
287
+ if (value) result[param[1]] = decodeSegment(value);
288
+ }
289
+ return result;
290
+ }
291
+ function pathParams(req, path) {
292
+ return { ...routeParams(req, path), ...req.params ?? {} };
293
+ }
249
294
  function redactPath(path, params, mode) {
250
295
  if (mode === "none" || !params) return path;
251
296
  let result = path;
@@ -261,7 +306,7 @@ function safeUrl(req, mode) {
261
306
  const queryStart = url.indexOf("?");
262
307
  const path = queryStart >= 0 ? url.slice(0, queryStart) : url;
263
308
  const query = queryStart >= 0 ? url.slice(queryStart) : "";
264
- return redactUrl(redactPath(path, req.params, mode) + query, mode);
309
+ return redactUrl(redactPath(path, pathParams(req, path), mode) + query, mode);
265
310
  }
266
311
  function redactUrl(url, mode) {
267
312
  const queryStart = url.indexOf("?");
@@ -329,8 +374,7 @@ function createMiddleware(logger) {
329
374
  return (req, res, next) => {
330
375
  let context;
331
376
  try {
332
- const incoming = headerValue(req.headers["x-request-id"])?.trim();
333
- const requestId = incoming && incoming.length <= LIMITS.requestIdLength ? incoming : randomUUID();
377
+ const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
334
378
  context = { requestId, startedAt: performance.now(), req };
335
379
  res.setHeader("x-request-id", requestId);
336
380
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
@@ -421,16 +465,92 @@ import { readFile, stat } from "fs/promises";
421
465
  import { SourceMap } from "module";
422
466
  import { dirname, isAbsolute, relative, resolve, sep } from "path";
423
467
  import { fileURLToPath } from "url";
468
+
469
+ // src/function-name.ts
470
+ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
471
+ var MAX_LINES_UP = 400;
472
+ var MAX_HEADER_LINES = 6;
473
+ var MAX_NAME_LENGTH = 100;
474
+ var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
475
+ var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
476
+ var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
477
+ var KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "await", "else", "do", "try"]);
478
+ var NAME_PATTERNS = [
479
+ new RegExp(`\\bfunction\\s*\\*?\\s*(${IDENTIFIER})\\s*\\(`),
480
+ new RegExp(`(${IDENTIFIER})\\s*[:=]\\s*(?:async\\s+)?function\\b`),
481
+ new RegExp(`(${IDENTIFIER})\\s*(?::\\s*[^=]+)?=\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`),
482
+ new RegExp(`(?:^|[{,(]\\s*)(${IDENTIFIER})\\s*:\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`)
483
+ ];
484
+ var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
485
+ var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
486
+ function codeOnly(text) {
487
+ return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
488
+ }
489
+ function parenBalance(text) {
490
+ let balance = 0;
491
+ for (const char of text) {
492
+ if (char === "(") balance += 1;
493
+ else if (char === ")") balance -= 1;
494
+ }
495
+ return balance;
496
+ }
497
+ function headerOf(lines, index, prefix) {
498
+ let header = prefix;
499
+ for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
500
+ if (header.trim() !== "" && parenBalance(header) >= 0) break;
501
+ header = `${codeOnly(lines[previous] ?? "")} ${header}`;
502
+ }
503
+ return header;
504
+ }
505
+ function nameIn(header) {
506
+ const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
507
+ for (const pattern of patterns) {
508
+ const name = pattern.exec(header)?.[1];
509
+ if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
510
+ }
511
+ return void 0;
512
+ }
513
+ function enclosingFunctionName(lines, line, column) {
514
+ const start = line - 1;
515
+ if (start < 0 || start >= lines.length) return void 0;
516
+ let depth = 0;
517
+ for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
518
+ const raw = lines[index] ?? "";
519
+ if (index < start && MARKUP_BOUNDARY.test(raw)) return void 0;
520
+ const code = codeOnly(index === start && column ? raw.slice(0, Math.max(0, column - 1)) : raw);
521
+ for (let position = code.length - 1; position >= 0; position--) {
522
+ const char = code[position];
523
+ if (char === "}") {
524
+ depth += 1;
525
+ } else if (char === "{") {
526
+ if (depth > 0) {
527
+ depth -= 1;
528
+ continue;
529
+ }
530
+ const header = headerOf(lines, index, code.slice(0, position));
531
+ if (CLASS_BODY.test(header)) return void 0;
532
+ if (CONTROL_BLOCK.test(header)) continue;
533
+ const name = nameIn(header);
534
+ if (name) return name;
535
+ }
536
+ }
537
+ }
538
+ return void 0;
539
+ }
540
+
541
+ // src/source.ts
424
542
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
425
543
  var MAX_CACHED_FILES = 50;
426
544
  var MAPPING_URL = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
427
545
  var cache = /* @__PURE__ */ new Map();
428
- async function enrichFrames(frames, cwd = process.cwd()) {
546
+ async function enrichFrames(frames, cwd = process.cwd(), options = {}) {
429
547
  const result = [];
430
548
  let withContext = 0;
431
549
  for (const frame of frames) {
432
550
  try {
433
- const { frame: mapped, lines } = await mapFrame(frame);
551
+ const { frame: located, lines, original } = await mapFrame(frame, options.mapLocator);
552
+ const named = options.inferFunctionNames && original && !located.fn && lines && located.line ? withInferredName(located, lines) : located;
553
+ const mapped = named.inApp && isLibraryPath(named.file) ? { ...named, inApp: false } : named;
434
554
  let next = mapped;
435
555
  if (mapped.inApp && lines && mapped.line && withContext < LIMITS.contextFrames) {
436
556
  const context = contextFor(lines, mapped.line);
@@ -446,29 +566,36 @@ async function enrichFrames(frames, cwd = process.cwd()) {
446
566
  }
447
567
  return result;
448
568
  }
449
- async function enrichException(exception, cwd = process.cwd()) {
450
- const frames = await enrichFrames(exception.frames, cwd);
569
+ async function enrichException(exception, cwd = process.cwd(), options = {}) {
570
+ const frames = await enrichFrames(exception.frames, cwd, options);
451
571
  if (!exception.cause) return { ...exception, frames };
452
- return { ...exception, frames, cause: await enrichException(exception.cause, cwd) };
572
+ return { ...exception, frames, cause: await enrichException(exception.cause, cwd, options) };
573
+ }
574
+ function withInferredName(frame, lines) {
575
+ const fn = enclosingFunctionName(lines, frame.line ?? 0, frame.col);
576
+ return fn ? { ...frame, fn } : frame;
577
+ }
578
+ function isLibraryPath(file) {
579
+ return file.includes("/node_modules/") || file.includes("\\node_modules\\");
453
580
  }
454
581
  function relativize(file, cwd) {
455
582
  if (!isAbsolute(file)) return file;
456
583
  const path = relative(cwd, file);
457
584
  return path.startsWith("..") || isAbsolute(path) ? file : path.split(sep).join("/");
458
585
  }
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 };
586
+ async function mapFrame(frame, locator) {
587
+ if (!frame.line || !isAbsolute(frame.file)) return { frame, lines: null, original: false };
588
+ const file = await loadFile(frame.file, locator);
589
+ if (!file) return { frame, lines: null, original: false };
590
+ if (!file.map) return { frame, lines: file.lines, original: false };
464
591
  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 };
592
+ if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines, original: false };
466
593
  const originalPath = resolveSource(file.map, entry.originalSource);
467
594
  const mapped = { ...frame, file: originalPath, line: entry.originalLine + 1, col: (entry.originalColumn ?? 0) + 1 };
468
595
  const lines = embeddedLines(file.map, file.map.sources.indexOf(entry.originalSource));
469
- if (lines) return { frame: mapped, lines };
596
+ if (lines) return { frame: mapped, lines, original: true };
470
597
  const original = await loadFile(originalPath);
471
- return { frame: mapped, lines: original?.lines ?? null };
598
+ return { frame: mapped, lines: original?.lines ?? null, original: true };
472
599
  }
473
600
  function embeddedLines(map, index) {
474
601
  if (index < 0) return null;
@@ -500,26 +627,27 @@ function resolveSource(map, source) {
500
627
  }
501
628
  return isAbsolute(source) ? source : resolve(map.baseDir, source);
502
629
  }
503
- function loadFile(path) {
504
- const cached = cache.get(path);
630
+ function loadFile(path, locator) {
631
+ const key = locator ? `${path}\0located` : path;
632
+ const cached = cache.get(key);
505
633
  if (cached) {
506
- cache.delete(path);
507
- cache.set(path, cached);
634
+ cache.delete(key);
635
+ cache.set(key, cached);
508
636
  return cached;
509
637
  }
510
- const pending = readSource(path).catch(() => null);
511
- cache.set(path, pending);
638
+ const pending = readSource(path, locator).catch(() => null);
639
+ cache.set(key, pending);
512
640
  if (cache.size > MAX_CACHED_FILES) {
513
641
  const oldest = cache.keys().next().value;
514
642
  if (oldest !== void 0) cache.delete(oldest);
515
643
  }
516
644
  return pending;
517
645
  }
518
- async function readSource(path) {
646
+ async function readSource(path, locator) {
519
647
  const text = await readText(path);
520
648
  if (text === null) return null;
521
649
  const lines = text.split(/\r?\n/);
522
- return { lines, map: await loadMap(path, lines) };
650
+ return { lines, map: await loadMap(path, lines, locator) };
523
651
  }
524
652
  async function readText(path) {
525
653
  try {
@@ -530,7 +658,7 @@ async function readText(path) {
530
658
  return null;
531
659
  }
532
660
  }
533
- async function loadMap(path, lines) {
661
+ async function loadMap(path, lines, locator) {
534
662
  let url;
535
663
  for (let index = lines.length - 1; index >= 0 && index >= lines.length - 5; index--) {
536
664
  const match = MAPPING_URL.exec(lines[index] ?? "");
@@ -539,10 +667,15 @@ async function loadMap(path, lines) {
539
667
  break;
540
668
  }
541
669
  }
542
- if (!url) return null;
543
670
  let raw;
544
671
  let mapDir;
545
- if (url.startsWith("data:")) {
672
+ const located = url ? null : locator ? locator(path) : `${path}.map`;
673
+ if (located) {
674
+ raw = await readText(located);
675
+ mapDir = dirname(located);
676
+ } else if (!url) {
677
+ return null;
678
+ } else if (url.startsWith("data:")) {
546
679
  const comma = url.indexOf(",");
547
680
  const encoded = url.slice(comma + 1);
548
681
  raw = url.slice(0, comma).endsWith(";base64") ? Buffer.from(encoded, "base64").toString("utf8") : decodeURIComponent(encoded);
@@ -571,12 +704,13 @@ async function loadMap(path, lines) {
571
704
  import { fileURLToPath as fileURLToPath2 } from "url";
572
705
  var WITH_FUNCTION = /^at (?:async )?(.+?) \((.+)\)$/;
573
706
  var LOCATION = /^(.+):(\d+):(\d+)$/;
707
+ var MAX_FRAME_LINE = 1e3;
574
708
  function parseStack(stack) {
575
709
  if (!stack) return [];
576
710
  const frames = [];
577
711
  for (const raw of stack.split("\n")) {
578
712
  const line = raw.trim();
579
- if (!line.startsWith("at ")) continue;
713
+ if (line.length > MAX_FRAME_LINE || !line.startsWith("at ")) continue;
580
714
  const withFunction = WITH_FUNCTION.exec(line);
581
715
  const fn = withFunction?.[1];
582
716
  const location = withFunction ? withFunction[2] ?? "" : line.replace(/^at (?:async )?/, "");
@@ -671,6 +805,7 @@ var Transport = class {
671
805
  inFlight = null;
672
806
  nextAttemptAt = 0;
673
807
  backoffMs = 0;
808
+ unauthorizedUntil = 0;
674
809
  stopped = false;
675
810
  warned = /* @__PURE__ */ new Set();
676
811
  timer;
@@ -760,10 +895,8 @@ var Transport = class {
760
895
  this.nextAttemptAt = Date.now() + Math.max(this.backoffMs, outcome.retryAfterMs ?? 0);
761
896
  return false;
762
897
  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);
898
+ this.droppedCount += events.length;
899
+ this.pauseUnauthorized(outcome.status);
767
900
  return false;
768
901
  case "rejected":
769
902
  this.droppedCount += events.length;
@@ -771,6 +904,47 @@ var Transport = class {
771
904
  return true;
772
905
  }
773
906
  }
907
+ async checkIn(slug, payload) {
908
+ if (this.stopped || !this.config.checkInBaseUrl || Date.now() < this.unauthorizedUntil) return;
909
+ let response;
910
+ try {
911
+ response = await fetch(this.config.checkInBaseUrl + encodeURIComponent(slug), {
912
+ method: "POST",
913
+ headers: this.headers(),
914
+ body: JSON.stringify(payload),
915
+ redirect: "error",
916
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? 5e3)
917
+ });
918
+ } catch (error) {
919
+ this.warn("checkin_network", `check-in no Radar falhou: ${describeError(error)}`, false);
920
+ return;
921
+ }
922
+ await response.arrayBuffer().catch(() => void 0);
923
+ if (response.ok) return;
924
+ if (response.status === 401 || response.status === 403) {
925
+ this.pauseUnauthorized(response.status);
926
+ return;
927
+ }
928
+ if (response.status === 404) {
929
+ this.warn(`checkin_unknown_${slug}`, `tarefa agendada "${slug}" n\xE3o existe no projeto da chave; crie o monitor com esse slug no Radar`, true);
930
+ return;
931
+ }
932
+ this.warn(`checkin_rejected_${response.status}`, `check-in recusado pelo Radar (${response.status})`, false);
933
+ }
934
+ pauseUnauthorized(status) {
935
+ this.droppedCount += this.queue.length;
936
+ this.queue = [];
937
+ this.unauthorizedUntil = Date.now() + UNAUTHORIZED_PAUSE_MS;
938
+ this.nextAttemptAt = Math.max(this.nextAttemptAt, this.unauthorizedUntil);
939
+ this.warn("unauthorized", `chave do Radar recusada (${status}); envios pausados por 10 minutos`, true);
940
+ }
941
+ headers() {
942
+ return {
943
+ "content-type": "application/json",
944
+ authorization: `Bearer ${this.config.key}`,
945
+ "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
946
+ };
947
+ }
774
948
  requeue(events) {
775
949
  this.queue.unshift(...events);
776
950
  const overflow = this.queue.length - this.maxQueue;
@@ -782,11 +956,7 @@ var Transport = class {
782
956
  async post(batch) {
783
957
  const json = JSON.stringify(batch);
784
958
  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
- };
959
+ const headers = this.headers();
790
960
  if (compress) headers["content-encoding"] = "gzip";
791
961
  let response;
792
962
  try {
@@ -840,9 +1010,12 @@ function withTimeout(promise, ms) {
840
1010
 
841
1011
  // src/version.ts
842
1012
  var SDK_NAME = "@oconde/radar";
843
- var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
1013
+ var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
844
1014
 
845
1015
  // src/client.ts
1016
+ var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
1017
+ var MAX_CHECK_IN_ENVIRONMENT = 64;
1018
+ var MAX_CHECK_IN_RELEASE = 128;
846
1019
  var RadarClient = class {
847
1020
  options = resolveOptions({}, {});
848
1021
  transport = null;
@@ -871,6 +1044,7 @@ var RadarClient = class {
871
1044
  sdk: { name: SDK_NAME, version: SDK_VERSION },
872
1045
  environment: this.options.environment,
873
1046
  ...this.options.release ? { release: this.options.release } : {},
1047
+ checkInBaseUrl: this.options.endpoint + CHECKIN_PATH,
874
1048
  onWarning: (message, always) => always ? this.writeWarning(message) : this.debugWarn(message)
875
1049
  });
876
1050
  }
@@ -932,6 +1106,51 @@ var RadarClient = class {
932
1106
  if (currentContext()) return execute();
933
1107
  return runWithContext({ requestId: randomUUID2(), startedAt: performance3.now() }, execute);
934
1108
  }
1109
+ withContext(fn, options = {}) {
1110
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance3.now() }, fn);
1111
+ }
1112
+ async checkIn(slug, status, options = {}) {
1113
+ try {
1114
+ if (!this.transport || this.closed) return;
1115
+ if (typeof slug !== "string" || !CHECKIN_SLUG.test(slug)) {
1116
+ this.debugWarn(`slug de tarefa agendada inv\xE1lido: ${String(slug).slice(0, 80)}`);
1117
+ return;
1118
+ }
1119
+ if (!CHECKIN_STATUSES.includes(status)) return;
1120
+ const checkInId = typeof options.checkInId === "string" && options.checkInId ? options.checkInId.slice(0, LIMITS.checkInIdLength) : void 0;
1121
+ 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;
1122
+ const environment = this.options.environment.slice(0, MAX_CHECK_IN_ENVIRONMENT) || "production";
1123
+ const release = this.options.release?.slice(0, MAX_CHECK_IN_RELEASE);
1124
+ const payload = {
1125
+ status,
1126
+ ...checkInId ? { checkInId } : {},
1127
+ ...durationMs !== void 0 ? { durationMs } : {},
1128
+ environment,
1129
+ ...release ? { release } : {}
1130
+ };
1131
+ await this.transport.checkIn(slug, payload);
1132
+ } catch (error) {
1133
+ this.debugWarn(`check-in falhou: ${describeError(error)}`);
1134
+ }
1135
+ }
1136
+ async cron(slug, fn) {
1137
+ const checkInId = randomUUID2();
1138
+ const started = performance3.now();
1139
+ const execute = async () => {
1140
+ await this.checkIn(slug, "in_progress", { checkInId });
1141
+ try {
1142
+ const result = await fn();
1143
+ await this.checkIn(slug, "ok", { checkInId, durationMs: elapsedSince(started) });
1144
+ return result;
1145
+ } catch (error) {
1146
+ this.captureError(error, { cron: slug });
1147
+ await this.checkIn(slug, "error", { checkInId, durationMs: elapsedSince(started) });
1148
+ throw error;
1149
+ }
1150
+ };
1151
+ if (currentContext()) return execute();
1152
+ return this.withContext(execute);
1153
+ }
935
1154
  middleware() {
936
1155
  return createMiddleware(this);
937
1156
  }
@@ -949,7 +1168,7 @@ var RadarClient = class {
949
1168
  () => this.emitLog(level, "http.request", {
950
1169
  method: (req.method ?? "GET").toUpperCase(),
951
1170
  ...route ? { route } : {},
952
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), req.params, this.options.redact) } : {},
1171
+ ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
953
1172
  status,
954
1173
  durationMs: elapsedSince(context.startedAt)
955
1174
  })
@@ -986,31 +1205,40 @@ var RadarClient = class {
986
1205
  this.print(event);
987
1206
  this.transport?.enqueue(event);
988
1207
  }
1208
+ captureException(exception, options = {}) {
1209
+ this.safely(() => this.emitException(exception, options));
1210
+ }
989
1211
  emitError(error, data, level, handled) {
990
1212
  if (this.closed) return;
991
1213
  if (typeof error === "object" && error !== null) {
992
1214
  if (this.captured.has(error)) return;
993
1215
  this.captured.add(error);
994
1216
  }
1217
+ this.emitException(exceptionInfo(error), { level, handled, ...data !== void 0 ? { attrs: data } : {} }, error instanceof Error ? error.stack : void 0);
1218
+ }
1219
+ emitException(exception, options, stack) {
1220
+ if (this.closed) return;
995
1221
  const context = currentContext();
996
1222
  const mode = this.options.redact;
1223
+ const request = options.request === null ? void 0 : options.request ?? (context?.req ? requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) : void 0);
1224
+ const runtime = options.runtime === void 0 ? runtimeInfo() : options.runtime;
997
1225
  const event = {
998
1226
  type: "error",
999
1227
  ts: Date.now(),
1000
- level,
1001
- handled,
1002
- exception: exceptionInfo(error),
1228
+ level: options.level ?? "error",
1229
+ handled: options.handled ?? true,
1230
+ exception,
1003
1231
  ...context ? { requestId: context.requestId } : {},
1004
- ...context?.req ? { request: requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) } : {},
1232
+ ...request ? { request } : {},
1005
1233
  ...context?.user ? { user: context.user } : {},
1006
- runtime: runtimeInfo(),
1007
- ...data !== void 0 ? { attrs: prepareAttrs(data, mode) } : {}
1234
+ ...runtime ? { runtime } : {},
1235
+ ...options.attrs !== void 0 ? { attrs: prepareAttrs(options.attrs, mode) } : {}
1008
1236
  };
1009
- this.print(event, error instanceof Error ? error.stack : void 0);
1237
+ this.print(event, stack);
1010
1238
  if (!this.transport) return;
1011
1239
  this.transport.enqueue(
1012
- () => enrichException(event.exception).then(
1013
- (exception) => ({ ...event, exception }),
1240
+ () => enrichException(event.exception, process.cwd(), options.enrich).then(
1241
+ (enriched) => ({ ...event, exception: enriched }),
1014
1242
  () => event
1015
1243
  )
1016
1244
  );