@oconde/radar 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -289,7 +289,10 @@ function requestPath(req) {
289
289
  }
290
290
  function requestRoute(req, route) {
291
291
  if (route) return route;
292
- return typeof req.route?.path === "string" ? req.route.path : void 0;
292
+ if (typeof req.route?.path !== "string") return void 0;
293
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
294
+ if (!base) return req.route.path;
295
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
293
296
  }
294
297
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
295
298
  function pathSegments(value) {
@@ -423,6 +426,12 @@ function createMiddleware(logger) {
423
426
  // src/options.ts
424
427
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
425
428
  var LEVELS = ["debug", "info", "warn", "error"];
429
+ function resolveTraceFetch(value) {
430
+ if (!value) return { enabled: false, propagateTo: [] };
431
+ if (value === true) return { enabled: true, propagateTo: [] };
432
+ const targets2 = Array.isArray(value.propagateTo) ? value.propagateTo : [];
433
+ return { enabled: true, propagateTo: targets2.filter((target) => typeof target === "string" ? target.trim() !== "" : target instanceof RegExp) };
434
+ }
426
435
  function resolveOptions(options = {}, env = process.env) {
427
436
  const key = options.key?.trim() || void 0;
428
437
  return {
@@ -438,6 +447,7 @@ function resolveOptions(options = {}, env = process.env) {
438
447
  logRequests: options.logRequests ?? true,
439
448
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
440
449
  captureUnhandled: options.captureUnhandled ?? true,
450
+ traceFetch: resolveTraceFetch(options.traceFetch),
441
451
  debug: options.debug ?? false
442
452
  };
443
453
  }
@@ -505,6 +515,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
505
515
  var MAX_LINES_UP = 400;
506
516
  var MAX_HEADER_LINES = 6;
507
517
  var MAX_NAME_LENGTH = 100;
518
+ var MAX_COMMENT_LINES = 40;
508
519
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
509
520
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
510
521
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -517,8 +528,58 @@ var NAME_PATTERNS = [
517
528
  ];
518
529
  var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
519
530
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
520
- function codeOnly(text) {
521
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
531
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
532
+ function lineCommentAt(text, from) {
533
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
534
+ if (index === 0 || text[index - 1] !== ":") return index;
535
+ }
536
+ return -1;
537
+ }
538
+ function blockCommentAt(text, from) {
539
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
540
+ if (index === 0 || text[index - 1] !== "\\") return index;
541
+ }
542
+ return -1;
543
+ }
544
+ function stripComments(text, insideComment, closesLater) {
545
+ let code = "";
546
+ let position = 0;
547
+ let inside = insideComment;
548
+ while (position < text.length) {
549
+ if (inside) {
550
+ const end = text.indexOf("*/", position);
551
+ if (end < 0) return { code, insideComment: true };
552
+ inside = false;
553
+ position = end + 2;
554
+ continue;
555
+ }
556
+ const block = blockCommentAt(text, position);
557
+ const line = lineCommentAt(text, position);
558
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
559
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
560
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
561
+ code += text.slice(position, block + 2);
562
+ position = block + 2;
563
+ continue;
564
+ }
565
+ code += text.slice(position, block);
566
+ inside = true;
567
+ position = block + 2;
568
+ }
569
+ return { code, insideComment: inside };
570
+ }
571
+ function codeLines(lines, from, start, column) {
572
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
573
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
574
+ const result = [];
575
+ let insideComment = false;
576
+ for (let index = 0; index < plain.length; index++) {
577
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
578
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
579
+ insideComment = stripped.insideComment;
580
+ result.push(stripped.code);
581
+ }
582
+ return result;
522
583
  }
523
584
  function parenBalance(text) {
524
585
  let balance = 0;
@@ -528,11 +589,11 @@ function parenBalance(text) {
528
589
  }
529
590
  return balance;
530
591
  }
531
- function headerOf(lines, index, prefix) {
592
+ function headerOf(code, index, prefix) {
532
593
  let header = prefix;
533
594
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
534
595
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
535
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
596
+ header = `${code[previous] ?? ""} ${header}`;
536
597
  }
537
598
  return header;
538
599
  }
@@ -547,11 +608,12 @@ function nameIn(header) {
547
608
  function enclosingFunctionName(lines, line, column) {
548
609
  const start = line - 1;
549
610
  if (start < 0 || start >= lines.length) return void 0;
611
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
612
+ const cleaned = codeLines(lines, from, start, column);
550
613
  let depth = 0;
551
614
  for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
552
- const raw = lines[index] ?? "";
553
- if (index < start && MARKUP_BOUNDARY.test(raw)) return void 0;
554
- const code = codeOnly(index === start && column ? raw.slice(0, Math.max(0, column - 1)) : raw);
615
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
616
+ const code = cleaned[index - from] ?? "";
555
617
  for (let position = code.length - 1; position >= 0; position--) {
556
618
  const char = code[position];
557
619
  if (char === "}") {
@@ -561,7 +623,7 @@ function enclosingFunctionName(lines, line, column) {
561
623
  depth -= 1;
562
624
  continue;
563
625
  }
564
- const header = headerOf(lines, index, code.slice(0, position));
626
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
565
627
  if (CLASS_BODY.test(header)) return void 0;
566
628
  if (CONTROL_BLOCK.test(header)) continue;
567
629
  const name = nameIn(header);
@@ -826,10 +888,6 @@ function safeStringify(value) {
826
888
  }
827
889
  }
828
890
 
829
- // src/transport.ts
830
- var import_node_util2 = require("util");
831
- var import_node_zlib = require("zlib");
832
-
833
891
  // src/util.ts
834
892
  var import_node_perf_hooks2 = require("perf_hooks");
835
893
  function describeError(error) {
@@ -839,7 +897,101 @@ function elapsedSince(startedAt) {
839
897
  return Math.round(import_node_perf_hooks2.performance.now() - startedAt);
840
898
  }
841
899
 
900
+ // src/trace-fetch.ts
901
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
902
+ var REQUEST_ID_HEADER = "x-request-id";
903
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
904
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
905
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
906
+ function secretSegment(segment) {
907
+ if (UUID.test(segment)) return false;
908
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
909
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
910
+ }
911
+ function outboundPath(pathname, mode) {
912
+ if (mode === "none") return pathname;
913
+ return pathname.split("/").map((segment) => {
914
+ let decoded = segment;
915
+ try {
916
+ decoded = decodeURIComponent(segment);
917
+ } catch {
918
+ decoded = segment;
919
+ }
920
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
921
+ }).join("/");
922
+ }
923
+ function failureOf(error) {
924
+ const message = describeError(error);
925
+ const cause = error instanceof Error ? error.cause : void 0;
926
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
927
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
928
+ }
929
+ function urlOf(input) {
930
+ try {
931
+ if (typeof input === "string") return new URL(input);
932
+ if (input instanceof URL) return input;
933
+ return new URL(input.url);
934
+ } catch {
935
+ return null;
936
+ }
937
+ }
938
+ function originOf(value) {
939
+ try {
940
+ return new URL(value).origin;
941
+ } catch {
942
+ return null;
943
+ }
944
+ }
945
+ function targets(url, target) {
946
+ if (target instanceof RegExp) return target.test(url.host);
947
+ const wanted = target.trim().toLowerCase();
948
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
949
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
950
+ }
951
+ function isPlainObject(value) {
952
+ if (value === null || typeof value !== "object") return false;
953
+ const prototype = Object.getPrototypeOf(value);
954
+ return prototype === Object.prototype || prototype === null;
955
+ }
956
+ function withRequestId(input, init, requestId) {
957
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
958
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
959
+ headers.set(REQUEST_ID_HEADER, requestId);
960
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
961
+ return [new Request(input, init), { headers }];
962
+ }
963
+ function traceFetch(config) {
964
+ const current = globalThis.fetch;
965
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
966
+ const original = current;
967
+ const radarOrigin = originOf(config.endpoint);
968
+ const traced = async (input, init) => {
969
+ const url = urlOf(input);
970
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
971
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
972
+ const context = currentContext();
973
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
974
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
975
+ const started = performance.now();
976
+ try {
977
+ const response = await original(nextInput, nextInit);
978
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
979
+ return response;
980
+ } catch (error) {
981
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
982
+ throw error;
983
+ }
984
+ };
985
+ Object.defineProperty(traced, WRAPPED, { value: original });
986
+ globalThis.fetch = traced;
987
+ return () => {
988
+ if (globalThis.fetch === traced) globalThis.fetch = original;
989
+ };
990
+ }
991
+
842
992
  // src/transport.ts
993
+ var import_node_util2 = require("util");
994
+ var import_node_zlib = require("zlib");
843
995
  var gzipAsync = (0, import_node_util2.promisify)(import_node_zlib.gzip);
844
996
  var MAX_BATCH_BYTES = 9e5;
845
997
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1067,7 +1219,7 @@ function withTimeout(promise, ms) {
1067
1219
 
1068
1220
  // src/version.ts
1069
1221
  var SDK_NAME = "@oconde/radar";
1070
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1222
+ var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
1071
1223
 
1072
1224
  // src/client.ts
1073
1225
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1079,6 +1231,7 @@ var RadarClient = class {
1079
1231
  initialized = false;
1080
1232
  closed = false;
1081
1233
  captured = /* @__PURE__ */ new WeakSet();
1234
+ restoreFetch = null;
1082
1235
  get isEnabled() {
1083
1236
  return this.transport !== null && !this.closed;
1084
1237
  }
@@ -1106,6 +1259,14 @@ var RadarClient = class {
1106
1259
  });
1107
1260
  }
1108
1261
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1262
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1263
+ this.restoreFetch = traceFetch({
1264
+ endpoint: this.options.endpoint,
1265
+ propagateTo: this.options.traceFetch.propagateTo,
1266
+ redact: this.options.redact,
1267
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1268
+ });
1269
+ }
1109
1270
  } catch (error) {
1110
1271
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1111
1272
  }
@@ -1242,6 +1403,8 @@ var RadarClient = class {
1242
1403
  async close(timeoutMs = 2e3) {
1243
1404
  if (this.closed) return;
1244
1405
  await this.flush(timeoutMs);
1406
+ this.restoreFetch?.();
1407
+ this.restoreFetch = null;
1245
1408
  this.closed = true;
1246
1409
  this.transport?.stop();
1247
1410
  }
@@ -1625,7 +1788,7 @@ function contextFor2(event, chooseRequestId) {
1625
1788
  const chosen = chooseRequestId?.(event);
1626
1789
  return { requestId: requestIdFrom(chosen || event.request.headers.get("x-request-id")), startedAt: import_node_perf_hooks4.performance.now(), req, ...route ? { route } : {} };
1627
1790
  }
1628
- function withRequestId(response, requestId) {
1791
+ function withRequestId2(response, requestId) {
1629
1792
  try {
1630
1793
  response.headers.set("x-request-id", requestId);
1631
1794
  return response;
@@ -1657,7 +1820,7 @@ function radarHandle(options = {}) {
1657
1820
  if (client.shouldLogRequest(req)) client.logHttpRequest(context, 500);
1658
1821
  throw error;
1659
1822
  }
1660
- const tagged = withRequestId(response, context.requestId);
1823
+ const tagged = withRequestId2(response, context.requestId);
1661
1824
  if (client.shouldLogRequest(req)) client.logHttpRequest(context, tagged.status);
1662
1825
  return tagged;
1663
1826
  });