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