@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.
@@ -50,6 +50,33 @@ var import_rxjs = require("rxjs");
50
50
 
51
51
  // src/context.ts
52
52
  var import_node_async_hooks = require("async_hooks");
53
+ var import_node_crypto = require("crypto");
54
+
55
+ // src/protocol/index.ts
56
+ var INGEST_PATH = "/api/v1/events";
57
+ var CHECKIN_PATH = "/api/v1/checkins/";
58
+ var CHECKIN_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
59
+ var CHECKIN_STATUSES = ["in_progress", "ok", "error"];
60
+ var LIMITS = {
61
+ batchBytes: 1e6,
62
+ batchEvents: 500,
63
+ messageLength: 2e3,
64
+ stringLength: 2e3,
65
+ attrsBytes: 16384,
66
+ bodyBytes: 16384,
67
+ headers: 50,
68
+ frames: 50,
69
+ contextFrames: 10,
70
+ contextLines: 5,
71
+ contextLineLength: 300,
72
+ causeDepth: 5,
73
+ depth: 6,
74
+ arrayItems: 50,
75
+ requestIdLength: 128,
76
+ checkInIdLength: 64
77
+ };
78
+
79
+ // src/context.ts
53
80
  var STATE_KEY = /* @__PURE__ */ Symbol.for("@oconde/radar");
54
81
  function globalState() {
55
82
  const holder = globalThis;
@@ -66,6 +93,11 @@ function currentContext() {
66
93
  function runWithContext(context, fn) {
67
94
  return globalState().storage.run(context, fn);
68
95
  }
96
+ var SAFE_REQUEST_ID = /^[\x21-\x7e]+$/;
97
+ function requestIdFrom(incoming) {
98
+ const value = incoming?.trim();
99
+ return value && value.length <= LIMITS.requestIdLength && SAFE_REQUEST_ID.test(value) ? value : (0, import_node_crypto.randomUUID)();
100
+ }
69
101
 
70
102
  // src/client.ts
71
103
  var import_node_crypto3 = require("crypto");
@@ -73,31 +105,10 @@ var import_node_os = require("os");
73
105
  var import_node_perf_hooks3 = require("perf_hooks");
74
106
 
75
107
  // src/express.ts
76
- var import_node_crypto2 = require("crypto");
77
108
  var import_node_perf_hooks = require("perf_hooks");
78
109
 
79
- // src/protocol/index.ts
80
- var INGEST_PATH = "/api/v1/events";
81
- var LIMITS = {
82
- batchBytes: 1e6,
83
- batchEvents: 500,
84
- messageLength: 2e3,
85
- stringLength: 2e3,
86
- attrsBytes: 16384,
87
- bodyBytes: 16384,
88
- headers: 50,
89
- frames: 50,
90
- contextFrames: 10,
91
- contextLines: 5,
92
- contextLineLength: 300,
93
- causeDepth: 5,
94
- depth: 6,
95
- arrayItems: 50,
96
- requestIdLength: 128
97
- };
98
-
99
110
  // src/redact.ts
100
- var import_node_crypto = require("crypto");
111
+ var import_node_crypto2 = require("crypto");
101
112
  var SECRET_WORDS = /* @__PURE__ */ new Set([
102
113
  "pass",
103
114
  "passwd",
@@ -143,16 +154,16 @@ function fingerprintSlot() {
143
154
  const holder = globalThis;
144
155
  let slot = holder[KEY_SLOT];
145
156
  if (!slot) {
146
- slot = { key: (0, import_node_crypto.randomBytes)(32) };
157
+ slot = { key: (0, import_node_crypto2.randomBytes)(32) };
147
158
  holder[KEY_SLOT] = slot;
148
159
  }
149
160
  return slot;
150
161
  }
151
162
  function setFingerprintSecret(secret) {
152
- fingerprintSlot().key = secret ? (0, import_node_crypto.createHash)("sha256").update(`@oconde/radar:${secret}`).digest() : (0, import_node_crypto.randomBytes)(32);
163
+ fingerprintSlot().key = secret ? (0, import_node_crypto2.createHash)("sha256").update(`@oconde/radar:${secret}`).digest() : (0, import_node_crypto2.randomBytes)(32);
153
164
  }
154
165
  function fingerprint(value) {
155
- return (0, import_node_crypto.createHmac)("sha256", fingerprintSlot().key).update(value).digest("hex").slice(0, 8);
166
+ return (0, import_node_crypto2.createHmac)("sha256", fingerprintSlot().key).update(value).digest("hex").slice(0, 8);
156
167
  }
157
168
  function words(name) {
158
169
  return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
@@ -296,6 +307,40 @@ function requestRoute(req, route) {
296
307
  if (route) return route;
297
308
  return typeof req.route?.path === "string" ? req.route.path : void 0;
298
309
  }
310
+ var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
311
+ function pathSegments(value) {
312
+ const segments = value.split("/");
313
+ while (segments.length > 1 && segments[segments.length - 1] === "") segments.pop();
314
+ return segments;
315
+ }
316
+ function decodeSegment(value) {
317
+ try {
318
+ return decodeURIComponent(value);
319
+ } catch {
320
+ return value;
321
+ }
322
+ }
323
+ function routeParams(req, path) {
324
+ if (typeof req.route?.path !== "string") return {};
325
+ const pattern = pathSegments(`${typeof req.baseUrl === "string" ? req.baseUrl : ""}${req.route.path}`).filter((segment, index) => index > 0 || segment !== "");
326
+ const values = pathSegments(path);
327
+ if (pattern.length > values.length) return {};
328
+ const offset = values.length - pattern.length;
329
+ const result = {};
330
+ for (let index = 0; index < pattern.length; index++) {
331
+ const value = values[offset + index];
332
+ const param = ROUTE_PARAM.exec(pattern[index]);
333
+ if (!param) {
334
+ if (pattern[index] !== value) return {};
335
+ continue;
336
+ }
337
+ if (value) result[param[1]] = decodeSegment(value);
338
+ }
339
+ return result;
340
+ }
341
+ function pathParams(req, path) {
342
+ return { ...routeParams(req, path), ...req.params ?? {} };
343
+ }
299
344
  function redactPath(path, params, mode) {
300
345
  if (mode === "none" || !params) return path;
301
346
  let result = path;
@@ -311,7 +356,7 @@ function safeUrl(req, mode) {
311
356
  const queryStart = url.indexOf("?");
312
357
  const path = queryStart >= 0 ? url.slice(0, queryStart) : url;
313
358
  const query = queryStart >= 0 ? url.slice(queryStart) : "";
314
- return redactUrl(redactPath(path, req.params, mode) + query, mode);
359
+ return redactUrl(redactPath(path, pathParams(req, path), mode) + query, mode);
315
360
  }
316
361
  function redactUrl(url, mode) {
317
362
  const queryStart = url.indexOf("?");
@@ -379,8 +424,7 @@ function createMiddleware(logger) {
379
424
  return (req, res, next) => {
380
425
  let context;
381
426
  try {
382
- const incoming = headerValue(req.headers["x-request-id"])?.trim();
383
- const requestId = incoming && incoming.length <= LIMITS.requestIdLength ? incoming : (0, import_node_crypto2.randomUUID)();
427
+ const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
384
428
  context = { requestId, startedAt: import_node_perf_hooks.performance.now(), req };
385
429
  res.setHeader("x-request-id", requestId);
386
430
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
@@ -471,16 +515,92 @@ var import_promises = require("fs/promises");
471
515
  var import_node_module = require("module");
472
516
  var import_node_path = require("path");
473
517
  var import_node_url = require("url");
518
+
519
+ // src/function-name.ts
520
+ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
521
+ var MAX_LINES_UP = 400;
522
+ var MAX_HEADER_LINES = 6;
523
+ var MAX_NAME_LENGTH = 100;
524
+ var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
525
+ var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
526
+ var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
527
+ var KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "await", "else", "do", "try"]);
528
+ var NAME_PATTERNS = [
529
+ new RegExp(`\\bfunction\\s*\\*?\\s*(${IDENTIFIER})\\s*\\(`),
530
+ new RegExp(`(${IDENTIFIER})\\s*[:=]\\s*(?:async\\s+)?function\\b`),
531
+ new RegExp(`(${IDENTIFIER})\\s*(?::\\s*[^=]+)?=\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`),
532
+ new RegExp(`(?:^|[{,(]\\s*)(${IDENTIFIER})\\s*:\\s*(?:async\\s+)?(?:\\([^]*\\)|${IDENTIFIER})\\s*(?::\\s*[^=]+)?=>\\s*$`)
533
+ ];
534
+ var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
535
+ var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
536
+ function codeOnly(text) {
537
+ return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
538
+ }
539
+ function parenBalance(text) {
540
+ let balance = 0;
541
+ for (const char of text) {
542
+ if (char === "(") balance += 1;
543
+ else if (char === ")") balance -= 1;
544
+ }
545
+ return balance;
546
+ }
547
+ function headerOf(lines, index, prefix) {
548
+ let header = prefix;
549
+ for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
550
+ if (header.trim() !== "" && parenBalance(header) >= 0) break;
551
+ header = `${codeOnly(lines[previous] ?? "")} ${header}`;
552
+ }
553
+ return header;
554
+ }
555
+ function nameIn(header) {
556
+ const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
557
+ for (const pattern of patterns) {
558
+ const name = pattern.exec(header)?.[1];
559
+ if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
560
+ }
561
+ return void 0;
562
+ }
563
+ function enclosingFunctionName(lines, line, column) {
564
+ const start = line - 1;
565
+ if (start < 0 || start >= lines.length) return void 0;
566
+ let depth = 0;
567
+ for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
568
+ const raw = lines[index] ?? "";
569
+ if (index < start && MARKUP_BOUNDARY.test(raw)) return void 0;
570
+ const code = codeOnly(index === start && column ? raw.slice(0, Math.max(0, column - 1)) : raw);
571
+ for (let position = code.length - 1; position >= 0; position--) {
572
+ const char = code[position];
573
+ if (char === "}") {
574
+ depth += 1;
575
+ } else if (char === "{") {
576
+ if (depth > 0) {
577
+ depth -= 1;
578
+ continue;
579
+ }
580
+ const header = headerOf(lines, index, code.slice(0, position));
581
+ if (CLASS_BODY.test(header)) return void 0;
582
+ if (CONTROL_BLOCK.test(header)) continue;
583
+ const name = nameIn(header);
584
+ if (name) return name;
585
+ }
586
+ }
587
+ }
588
+ return void 0;
589
+ }
590
+
591
+ // src/source.ts
474
592
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
475
593
  var MAX_CACHED_FILES = 50;
476
594
  var MAPPING_URL = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
477
595
  var cache = /* @__PURE__ */ new Map();
478
- async function enrichFrames(frames, cwd = process.cwd()) {
596
+ async function enrichFrames(frames, cwd = process.cwd(), options = {}) {
479
597
  const result = [];
480
598
  let withContext = 0;
481
599
  for (const frame of frames) {
482
600
  try {
483
- const { frame: mapped, lines } = await mapFrame(frame);
601
+ const { frame: located, lines, original } = await mapFrame(frame, options.mapLocator);
602
+ const named = options.inferFunctionNames && original && !located.fn && lines && located.line ? withInferredName(located, lines) : located;
603
+ const mapped = named.inApp && isLibraryPath(named.file) ? { ...named, inApp: false } : named;
484
604
  let next = mapped;
485
605
  if (mapped.inApp && lines && mapped.line && withContext < LIMITS.contextFrames) {
486
606
  const context = contextFor(lines, mapped.line);
@@ -496,29 +616,36 @@ async function enrichFrames(frames, cwd = process.cwd()) {
496
616
  }
497
617
  return result;
498
618
  }
499
- async function enrichException(exception, cwd = process.cwd()) {
500
- const frames = await enrichFrames(exception.frames, cwd);
619
+ async function enrichException(exception, cwd = process.cwd(), options = {}) {
620
+ const frames = await enrichFrames(exception.frames, cwd, options);
501
621
  if (!exception.cause) return { ...exception, frames };
502
- return { ...exception, frames, cause: await enrichException(exception.cause, cwd) };
622
+ return { ...exception, frames, cause: await enrichException(exception.cause, cwd, options) };
623
+ }
624
+ function withInferredName(frame, lines) {
625
+ const fn = enclosingFunctionName(lines, frame.line ?? 0, frame.col);
626
+ return fn ? { ...frame, fn } : frame;
627
+ }
628
+ function isLibraryPath(file) {
629
+ return file.includes("/node_modules/") || file.includes("\\node_modules\\");
503
630
  }
504
631
  function relativize(file, cwd) {
505
632
  if (!(0, import_node_path.isAbsolute)(file)) return file;
506
633
  const path = (0, import_node_path.relative)(cwd, file);
507
634
  return path.startsWith("..") || (0, import_node_path.isAbsolute)(path) ? file : path.split(import_node_path.sep).join("/");
508
635
  }
509
- async function mapFrame(frame) {
510
- if (!frame.line || !(0, import_node_path.isAbsolute)(frame.file)) return { frame, lines: null };
511
- const file = await loadFile(frame.file);
512
- if (!file) return { frame, lines: null };
513
- if (!file.map) return { frame, lines: file.lines };
636
+ async function mapFrame(frame, locator) {
637
+ if (!frame.line || !(0, import_node_path.isAbsolute)(frame.file)) return { frame, lines: null, original: false };
638
+ const file = await loadFile(frame.file, locator);
639
+ if (!file) return { frame, lines: null, original: false };
640
+ if (!file.map) return { frame, lines: file.lines, original: false };
514
641
  const entry = file.map.map.findEntry(frame.line - 1, (frame.col ?? 1) - 1);
515
- if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines };
642
+ if (typeof entry.originalSource !== "string" || typeof entry.originalLine !== "number") return { frame, lines: file.lines, original: false };
516
643
  const originalPath = resolveSource(file.map, entry.originalSource);
517
644
  const mapped = { ...frame, file: originalPath, line: entry.originalLine + 1, col: (entry.originalColumn ?? 0) + 1 };
518
645
  const lines = embeddedLines(file.map, file.map.sources.indexOf(entry.originalSource));
519
- if (lines) return { frame: mapped, lines };
646
+ if (lines) return { frame: mapped, lines, original: true };
520
647
  const original = await loadFile(originalPath);
521
- return { frame: mapped, lines: original?.lines ?? null };
648
+ return { frame: mapped, lines: original?.lines ?? null, original: true };
522
649
  }
523
650
  function embeddedLines(map, index) {
524
651
  if (index < 0) return null;
@@ -550,26 +677,27 @@ function resolveSource(map, source) {
550
677
  }
551
678
  return (0, import_node_path.isAbsolute)(source) ? source : (0, import_node_path.resolve)(map.baseDir, source);
552
679
  }
553
- function loadFile(path) {
554
- const cached = cache.get(path);
680
+ function loadFile(path, locator) {
681
+ const key = locator ? `${path}\0located` : path;
682
+ const cached = cache.get(key);
555
683
  if (cached) {
556
- cache.delete(path);
557
- cache.set(path, cached);
684
+ cache.delete(key);
685
+ cache.set(key, cached);
558
686
  return cached;
559
687
  }
560
- const pending = readSource(path).catch(() => null);
561
- cache.set(path, pending);
688
+ const pending = readSource(path, locator).catch(() => null);
689
+ cache.set(key, pending);
562
690
  if (cache.size > MAX_CACHED_FILES) {
563
691
  const oldest = cache.keys().next().value;
564
692
  if (oldest !== void 0) cache.delete(oldest);
565
693
  }
566
694
  return pending;
567
695
  }
568
- async function readSource(path) {
696
+ async function readSource(path, locator) {
569
697
  const text = await readText(path);
570
698
  if (text === null) return null;
571
699
  const lines = text.split(/\r?\n/);
572
- return { lines, map: await loadMap(path, lines) };
700
+ return { lines, map: await loadMap(path, lines, locator) };
573
701
  }
574
702
  async function readText(path) {
575
703
  try {
@@ -580,7 +708,7 @@ async function readText(path) {
580
708
  return null;
581
709
  }
582
710
  }
583
- async function loadMap(path, lines) {
711
+ async function loadMap(path, lines, locator) {
584
712
  let url;
585
713
  for (let index = lines.length - 1; index >= 0 && index >= lines.length - 5; index--) {
586
714
  const match = MAPPING_URL.exec(lines[index] ?? "");
@@ -589,10 +717,15 @@ async function loadMap(path, lines) {
589
717
  break;
590
718
  }
591
719
  }
592
- if (!url) return null;
593
720
  let raw;
594
721
  let mapDir;
595
- if (url.startsWith("data:")) {
722
+ const located = url ? null : locator ? locator(path) : `${path}.map`;
723
+ if (located) {
724
+ raw = await readText(located);
725
+ mapDir = (0, import_node_path.dirname)(located);
726
+ } else if (!url) {
727
+ return null;
728
+ } else if (url.startsWith("data:")) {
596
729
  const comma = url.indexOf(",");
597
730
  const encoded = url.slice(comma + 1);
598
731
  raw = url.slice(0, comma).endsWith(";base64") ? Buffer.from(encoded, "base64").toString("utf8") : decodeURIComponent(encoded);
@@ -621,12 +754,13 @@ async function loadMap(path, lines) {
621
754
  var import_node_url2 = require("url");
622
755
  var WITH_FUNCTION = /^at (?:async )?(.+?) \((.+)\)$/;
623
756
  var LOCATION = /^(.+):(\d+):(\d+)$/;
757
+ var MAX_FRAME_LINE = 1e3;
624
758
  function parseStack(stack) {
625
759
  if (!stack) return [];
626
760
  const frames = [];
627
761
  for (const raw of stack.split("\n")) {
628
762
  const line = raw.trim();
629
- if (!line.startsWith("at ")) continue;
763
+ if (line.length > MAX_FRAME_LINE || !line.startsWith("at ")) continue;
630
764
  const withFunction = WITH_FUNCTION.exec(line);
631
765
  const fn = withFunction?.[1];
632
766
  const location = withFunction ? withFunction[2] ?? "" : line.replace(/^at (?:async )?/, "");
@@ -721,6 +855,7 @@ var Transport = class {
721
855
  inFlight = null;
722
856
  nextAttemptAt = 0;
723
857
  backoffMs = 0;
858
+ unauthorizedUntil = 0;
724
859
  stopped = false;
725
860
  warned = /* @__PURE__ */ new Set();
726
861
  timer;
@@ -810,10 +945,8 @@ var Transport = class {
810
945
  this.nextAttemptAt = Date.now() + Math.max(this.backoffMs, outcome.retryAfterMs ?? 0);
811
946
  return false;
812
947
  case "unauthorized":
813
- this.droppedCount += events.length + this.queue.length;
814
- this.queue = [];
815
- this.nextAttemptAt = Date.now() + UNAUTHORIZED_PAUSE_MS;
816
- this.warn("unauthorized", `chave do Radar recusada (${outcome.status}); envios pausados por 10 minutos`, true);
948
+ this.droppedCount += events.length;
949
+ this.pauseUnauthorized(outcome.status);
817
950
  return false;
818
951
  case "rejected":
819
952
  this.droppedCount += events.length;
@@ -821,6 +954,47 @@ var Transport = class {
821
954
  return true;
822
955
  }
823
956
  }
957
+ async checkIn(slug, payload) {
958
+ if (this.stopped || !this.config.checkInBaseUrl || Date.now() < this.unauthorizedUntil) return;
959
+ let response;
960
+ try {
961
+ response = await fetch(this.config.checkInBaseUrl + encodeURIComponent(slug), {
962
+ method: "POST",
963
+ headers: this.headers(),
964
+ body: JSON.stringify(payload),
965
+ redirect: "error",
966
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? 5e3)
967
+ });
968
+ } catch (error) {
969
+ this.warn("checkin_network", `check-in no Radar falhou: ${describeError(error)}`, false);
970
+ return;
971
+ }
972
+ await response.arrayBuffer().catch(() => void 0);
973
+ if (response.ok) return;
974
+ if (response.status === 401 || response.status === 403) {
975
+ this.pauseUnauthorized(response.status);
976
+ return;
977
+ }
978
+ if (response.status === 404) {
979
+ this.warn(`checkin_unknown_${slug}`, `tarefa agendada "${slug}" n\xE3o existe no projeto da chave; crie o monitor com esse slug no Radar`, true);
980
+ return;
981
+ }
982
+ this.warn(`checkin_rejected_${response.status}`, `check-in recusado pelo Radar (${response.status})`, false);
983
+ }
984
+ pauseUnauthorized(status) {
985
+ this.droppedCount += this.queue.length;
986
+ this.queue = [];
987
+ this.unauthorizedUntil = Date.now() + UNAUTHORIZED_PAUSE_MS;
988
+ this.nextAttemptAt = Math.max(this.nextAttemptAt, this.unauthorizedUntil);
989
+ this.warn("unauthorized", `chave do Radar recusada (${status}); envios pausados por 10 minutos`, true);
990
+ }
991
+ headers() {
992
+ return {
993
+ "content-type": "application/json",
994
+ authorization: `Bearer ${this.config.key}`,
995
+ "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
996
+ };
997
+ }
824
998
  requeue(events) {
825
999
  this.queue.unshift(...events);
826
1000
  const overflow = this.queue.length - this.maxQueue;
@@ -832,11 +1006,7 @@ var Transport = class {
832
1006
  async post(batch) {
833
1007
  const json = JSON.stringify(batch);
834
1008
  const compress = Buffer.byteLength(json) > GZIP_THRESHOLD_BYTES;
835
- const headers = {
836
- "content-type": "application/json",
837
- authorization: `Bearer ${this.config.key}`,
838
- "user-agent": `${this.config.sdk.name}/${this.config.sdk.version}`
839
- };
1009
+ const headers = this.headers();
840
1010
  if (compress) headers["content-encoding"] = "gzip";
841
1011
  let response;
842
1012
  try {
@@ -890,9 +1060,12 @@ function withTimeout(promise, ms) {
890
1060
 
891
1061
  // src/version.ts
892
1062
  var SDK_NAME = "@oconde/radar";
893
- var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
1063
+ var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
894
1064
 
895
1065
  // src/client.ts
1066
+ var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
1067
+ var MAX_CHECK_IN_ENVIRONMENT = 64;
1068
+ var MAX_CHECK_IN_RELEASE = 128;
896
1069
  var RadarClient = class {
897
1070
  options = resolveOptions({}, {});
898
1071
  transport = null;
@@ -921,6 +1094,7 @@ var RadarClient = class {
921
1094
  sdk: { name: SDK_NAME, version: SDK_VERSION },
922
1095
  environment: this.options.environment,
923
1096
  ...this.options.release ? { release: this.options.release } : {},
1097
+ checkInBaseUrl: this.options.endpoint + CHECKIN_PATH,
924
1098
  onWarning: (message, always) => always ? this.writeWarning(message) : this.debugWarn(message)
925
1099
  });
926
1100
  }
@@ -982,6 +1156,51 @@ var RadarClient = class {
982
1156
  if (currentContext()) return execute();
983
1157
  return runWithContext({ requestId: (0, import_node_crypto3.randomUUID)(), startedAt: import_node_perf_hooks3.performance.now() }, execute);
984
1158
  }
1159
+ withContext(fn, options = {}) {
1160
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: import_node_perf_hooks3.performance.now() }, fn);
1161
+ }
1162
+ async checkIn(slug, status, options = {}) {
1163
+ try {
1164
+ if (!this.transport || this.closed) return;
1165
+ if (typeof slug !== "string" || !CHECKIN_SLUG.test(slug)) {
1166
+ this.debugWarn(`slug de tarefa agendada inv\xE1lido: ${String(slug).slice(0, 80)}`);
1167
+ return;
1168
+ }
1169
+ if (!CHECKIN_STATUSES.includes(status)) return;
1170
+ const checkInId = typeof options.checkInId === "string" && options.checkInId ? options.checkInId.slice(0, LIMITS.checkInIdLength) : void 0;
1171
+ 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;
1172
+ const environment = this.options.environment.slice(0, MAX_CHECK_IN_ENVIRONMENT) || "production";
1173
+ const release = this.options.release?.slice(0, MAX_CHECK_IN_RELEASE);
1174
+ const payload = {
1175
+ status,
1176
+ ...checkInId ? { checkInId } : {},
1177
+ ...durationMs !== void 0 ? { durationMs } : {},
1178
+ environment,
1179
+ ...release ? { release } : {}
1180
+ };
1181
+ await this.transport.checkIn(slug, payload);
1182
+ } catch (error) {
1183
+ this.debugWarn(`check-in falhou: ${describeError(error)}`);
1184
+ }
1185
+ }
1186
+ async cron(slug, fn) {
1187
+ const checkInId = (0, import_node_crypto3.randomUUID)();
1188
+ const started = import_node_perf_hooks3.performance.now();
1189
+ const execute = async () => {
1190
+ await this.checkIn(slug, "in_progress", { checkInId });
1191
+ try {
1192
+ const result = await fn();
1193
+ await this.checkIn(slug, "ok", { checkInId, durationMs: elapsedSince(started) });
1194
+ return result;
1195
+ } catch (error) {
1196
+ this.captureError(error, { cron: slug });
1197
+ await this.checkIn(slug, "error", { checkInId, durationMs: elapsedSince(started) });
1198
+ throw error;
1199
+ }
1200
+ };
1201
+ if (currentContext()) return execute();
1202
+ return this.withContext(execute);
1203
+ }
985
1204
  middleware() {
986
1205
  return createMiddleware(this);
987
1206
  }
@@ -999,7 +1218,7 @@ var RadarClient = class {
999
1218
  () => this.emitLog(level, "http.request", {
1000
1219
  method: (req.method ?? "GET").toUpperCase(),
1001
1220
  ...route ? { route } : {},
1002
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), req.params, this.options.redact) } : {},
1221
+ ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
1003
1222
  status,
1004
1223
  durationMs: elapsedSince(context.startedAt)
1005
1224
  })
@@ -1036,31 +1255,40 @@ var RadarClient = class {
1036
1255
  this.print(event);
1037
1256
  this.transport?.enqueue(event);
1038
1257
  }
1258
+ captureException(exception, options = {}) {
1259
+ this.safely(() => this.emitException(exception, options));
1260
+ }
1039
1261
  emitError(error, data, level, handled) {
1040
1262
  if (this.closed) return;
1041
1263
  if (typeof error === "object" && error !== null) {
1042
1264
  if (this.captured.has(error)) return;
1043
1265
  this.captured.add(error);
1044
1266
  }
1267
+ this.emitException(exceptionInfo(error), { level, handled, ...data !== void 0 ? { attrs: data } : {} }, error instanceof Error ? error.stack : void 0);
1268
+ }
1269
+ emitException(exception, options, stack) {
1270
+ if (this.closed) return;
1045
1271
  const context = currentContext();
1046
1272
  const mode = this.options.redact;
1273
+ const request = options.request === null ? void 0 : options.request ?? (context?.req ? requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) : void 0);
1274
+ const runtime = options.runtime === void 0 ? runtimeInfo() : options.runtime;
1047
1275
  const event = {
1048
1276
  type: "error",
1049
1277
  ts: Date.now(),
1050
- level,
1051
- handled,
1052
- exception: exceptionInfo(error),
1278
+ level: options.level ?? "error",
1279
+ handled: options.handled ?? true,
1280
+ exception,
1053
1281
  ...context ? { requestId: context.requestId } : {},
1054
- ...context?.req ? { request: requestInfo(context.req, mode, { route: context.route }, this.options.requestDetail) } : {},
1282
+ ...request ? { request } : {},
1055
1283
  ...context?.user ? { user: context.user } : {},
1056
- runtime: runtimeInfo(),
1057
- ...data !== void 0 ? { attrs: prepareAttrs(data, mode) } : {}
1284
+ ...runtime ? { runtime } : {},
1285
+ ...options.attrs !== void 0 ? { attrs: prepareAttrs(options.attrs, mode) } : {}
1058
1286
  };
1059
- this.print(event, error instanceof Error ? error.stack : void 0);
1287
+ this.print(event, stack);
1060
1288
  if (!this.transport) return;
1061
1289
  this.transport.enqueue(
1062
- () => enrichException(event.exception).then(
1063
- (exception) => ({ ...event, exception }),
1290
+ () => enrichException(event.exception, process.cwd(), options.enrich).then(
1291
+ (enriched) => ({ ...event, exception: enriched }),
1064
1292
  () => event
1065
1293
  )
1066
1294
  );