@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.
@@ -305,7 +305,10 @@ function requestPath(req) {
305
305
  }
306
306
  function requestRoute(req, route) {
307
307
  if (route) return route;
308
- return typeof req.route?.path === "string" ? req.route.path : void 0;
308
+ if (typeof req.route?.path !== "string") return void 0;
309
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
310
+ if (!base) return req.route.path;
311
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
309
312
  }
310
313
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
311
314
  function pathSegments(value) {
@@ -439,6 +442,12 @@ function createMiddleware(logger) {
439
442
  // src/options.ts
440
443
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
441
444
  var LEVELS = ["debug", "info", "warn", "error"];
445
+ function resolveTraceFetch(value) {
446
+ if (!value) return { enabled: false, propagateTo: [] };
447
+ if (value === true) return { enabled: true, propagateTo: [] };
448
+ const targets2 = Array.isArray(value.propagateTo) ? value.propagateTo : [];
449
+ return { enabled: true, propagateTo: targets2.filter((target) => typeof target === "string" ? target.trim() !== "" : target instanceof RegExp) };
450
+ }
442
451
  function resolveOptions(options = {}, env = process.env) {
443
452
  const key = options.key?.trim() || void 0;
444
453
  return {
@@ -454,6 +463,7 @@ function resolveOptions(options = {}, env = process.env) {
454
463
  logRequests: options.logRequests ?? true,
455
464
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
456
465
  captureUnhandled: options.captureUnhandled ?? true,
466
+ traceFetch: resolveTraceFetch(options.traceFetch),
457
467
  debug: options.debug ?? false
458
468
  };
459
469
  }
@@ -521,6 +531,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
521
531
  var MAX_LINES_UP = 400;
522
532
  var MAX_HEADER_LINES = 6;
523
533
  var MAX_NAME_LENGTH = 100;
534
+ var MAX_COMMENT_LINES = 40;
524
535
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
525
536
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
526
537
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -533,8 +544,58 @@ var NAME_PATTERNS = [
533
544
  ];
534
545
  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
546
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
536
- function codeOnly(text) {
537
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
547
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
548
+ function lineCommentAt(text, from) {
549
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
550
+ if (index === 0 || text[index - 1] !== ":") return index;
551
+ }
552
+ return -1;
553
+ }
554
+ function blockCommentAt(text, from) {
555
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
556
+ if (index === 0 || text[index - 1] !== "\\") return index;
557
+ }
558
+ return -1;
559
+ }
560
+ function stripComments(text, insideComment, closesLater) {
561
+ let code = "";
562
+ let position = 0;
563
+ let inside = insideComment;
564
+ while (position < text.length) {
565
+ if (inside) {
566
+ const end = text.indexOf("*/", position);
567
+ if (end < 0) return { code, insideComment: true };
568
+ inside = false;
569
+ position = end + 2;
570
+ continue;
571
+ }
572
+ const block = blockCommentAt(text, position);
573
+ const line = lineCommentAt(text, position);
574
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
575
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
576
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
577
+ code += text.slice(position, block + 2);
578
+ position = block + 2;
579
+ continue;
580
+ }
581
+ code += text.slice(position, block);
582
+ inside = true;
583
+ position = block + 2;
584
+ }
585
+ return { code, insideComment: inside };
586
+ }
587
+ function codeLines(lines, from, start, column) {
588
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
589
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
590
+ const result = [];
591
+ let insideComment = false;
592
+ for (let index = 0; index < plain.length; index++) {
593
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
594
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
595
+ insideComment = stripped.insideComment;
596
+ result.push(stripped.code);
597
+ }
598
+ return result;
538
599
  }
539
600
  function parenBalance(text) {
540
601
  let balance = 0;
@@ -544,11 +605,11 @@ function parenBalance(text) {
544
605
  }
545
606
  return balance;
546
607
  }
547
- function headerOf(lines, index, prefix) {
608
+ function headerOf(code, index, prefix) {
548
609
  let header = prefix;
549
610
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
550
611
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
551
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
612
+ header = `${code[previous] ?? ""} ${header}`;
552
613
  }
553
614
  return header;
554
615
  }
@@ -563,11 +624,12 @@ function nameIn(header) {
563
624
  function enclosingFunctionName(lines, line, column) {
564
625
  const start = line - 1;
565
626
  if (start < 0 || start >= lines.length) return void 0;
627
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
628
+ const cleaned = codeLines(lines, from, start, column);
566
629
  let depth = 0;
567
630
  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);
631
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
632
+ const code = cleaned[index - from] ?? "";
571
633
  for (let position = code.length - 1; position >= 0; position--) {
572
634
  const char = code[position];
573
635
  if (char === "}") {
@@ -577,7 +639,7 @@ function enclosingFunctionName(lines, line, column) {
577
639
  depth -= 1;
578
640
  continue;
579
641
  }
580
- const header = headerOf(lines, index, code.slice(0, position));
642
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
581
643
  if (CLASS_BODY.test(header)) return void 0;
582
644
  if (CONTROL_BLOCK.test(header)) continue;
583
645
  const name = nameIn(header);
@@ -819,10 +881,6 @@ function safeStringify(value) {
819
881
  }
820
882
  }
821
883
 
822
- // src/transport.ts
823
- var import_node_util2 = require("util");
824
- var import_node_zlib = require("zlib");
825
-
826
884
  // src/util.ts
827
885
  var import_node_perf_hooks2 = require("perf_hooks");
828
886
  function describeError(error) {
@@ -832,7 +890,101 @@ function elapsedSince(startedAt) {
832
890
  return Math.round(import_node_perf_hooks2.performance.now() - startedAt);
833
891
  }
834
892
 
893
+ // src/trace-fetch.ts
894
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
895
+ var REQUEST_ID_HEADER = "x-request-id";
896
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
897
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
898
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
899
+ function secretSegment(segment) {
900
+ if (UUID.test(segment)) return false;
901
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
902
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
903
+ }
904
+ function outboundPath(pathname, mode) {
905
+ if (mode === "none") return pathname;
906
+ return pathname.split("/").map((segment) => {
907
+ let decoded = segment;
908
+ try {
909
+ decoded = decodeURIComponent(segment);
910
+ } catch {
911
+ decoded = segment;
912
+ }
913
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
914
+ }).join("/");
915
+ }
916
+ function failureOf(error) {
917
+ const message = describeError(error);
918
+ const cause = error instanceof Error ? error.cause : void 0;
919
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
920
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
921
+ }
922
+ function urlOf(input) {
923
+ try {
924
+ if (typeof input === "string") return new URL(input);
925
+ if (input instanceof URL) return input;
926
+ return new URL(input.url);
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
931
+ function originOf(value) {
932
+ try {
933
+ return new URL(value).origin;
934
+ } catch {
935
+ return null;
936
+ }
937
+ }
938
+ function targets(url, target) {
939
+ if (target instanceof RegExp) return target.test(url.host);
940
+ const wanted = target.trim().toLowerCase();
941
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
942
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
943
+ }
944
+ function isPlainObject(value) {
945
+ if (value === null || typeof value !== "object") return false;
946
+ const prototype = Object.getPrototypeOf(value);
947
+ return prototype === Object.prototype || prototype === null;
948
+ }
949
+ function withRequestId(input, init, requestId) {
950
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
951
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
952
+ headers.set(REQUEST_ID_HEADER, requestId);
953
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
954
+ return [new Request(input, init), { headers }];
955
+ }
956
+ function traceFetch(config) {
957
+ const current = globalThis.fetch;
958
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
959
+ const original = current;
960
+ const radarOrigin = originOf(config.endpoint);
961
+ const traced = async (input, init) => {
962
+ const url = urlOf(input);
963
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
964
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
965
+ const context = currentContext();
966
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
967
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
968
+ const started = performance.now();
969
+ try {
970
+ const response = await original(nextInput, nextInit);
971
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
972
+ return response;
973
+ } catch (error) {
974
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
975
+ throw error;
976
+ }
977
+ };
978
+ Object.defineProperty(traced, WRAPPED, { value: original });
979
+ globalThis.fetch = traced;
980
+ return () => {
981
+ if (globalThis.fetch === traced) globalThis.fetch = original;
982
+ };
983
+ }
984
+
835
985
  // src/transport.ts
986
+ var import_node_util2 = require("util");
987
+ var import_node_zlib = require("zlib");
836
988
  var gzipAsync = (0, import_node_util2.promisify)(import_node_zlib.gzip);
837
989
  var MAX_BATCH_BYTES = 9e5;
838
990
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1060,7 +1212,7 @@ function withTimeout(promise, ms) {
1060
1212
 
1061
1213
  // src/version.ts
1062
1214
  var SDK_NAME = "@oconde/radar";
1063
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1215
+ var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
1064
1216
 
1065
1217
  // src/client.ts
1066
1218
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1072,6 +1224,7 @@ var RadarClient = class {
1072
1224
  initialized = false;
1073
1225
  closed = false;
1074
1226
  captured = /* @__PURE__ */ new WeakSet();
1227
+ restoreFetch = null;
1075
1228
  get isEnabled() {
1076
1229
  return this.transport !== null && !this.closed;
1077
1230
  }
@@ -1099,6 +1252,14 @@ var RadarClient = class {
1099
1252
  });
1100
1253
  }
1101
1254
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1255
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1256
+ this.restoreFetch = traceFetch({
1257
+ endpoint: this.options.endpoint,
1258
+ propagateTo: this.options.traceFetch.propagateTo,
1259
+ redact: this.options.redact,
1260
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1261
+ });
1262
+ }
1102
1263
  } catch (error) {
1103
1264
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1104
1265
  }
@@ -1235,6 +1396,8 @@ var RadarClient = class {
1235
1396
  async close(timeoutMs = 2e3) {
1236
1397
  if (this.closed) return;
1237
1398
  await this.flush(timeoutMs);
1399
+ this.restoreFetch?.();
1400
+ this.restoreFetch = null;
1238
1401
  this.closed = true;
1239
1402
  this.transport?.stop();
1240
1403
  }