@oconde/radar 0.2.0 → 0.4.1

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.
@@ -287,9 +287,18 @@ function requestPath(req) {
287
287
  const end = url.indexOf("?");
288
288
  return end >= 0 ? url.slice(0, end) : url;
289
289
  }
290
+ function matchesRequest(pattern, path, route) {
291
+ const candidates = route === void 0 ? [path] : [path, route];
292
+ if (pattern instanceof RegExp) return candidates.some((value) => value.search(pattern) !== -1);
293
+ const prefix = pattern.endsWith("/") ? pattern : `${pattern}/`;
294
+ return candidates.some((value) => value === pattern || value.startsWith(prefix));
295
+ }
290
296
  function requestRoute(req, route) {
291
297
  if (route) return route;
292
- return typeof req.route?.path === "string" ? req.route.path : void 0;
298
+ if (typeof req.route?.path !== "string") return void 0;
299
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
300
+ if (!base) return req.route.path;
301
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
293
302
  }
294
303
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
295
304
  function pathSegments(value) {
@@ -423,6 +432,25 @@ function createMiddleware(logger) {
423
432
  // src/options.ts
424
433
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
425
434
  var LEVELS = ["debug", "info", "warn", "error"];
435
+ function patterns(value) {
436
+ if (!Array.isArray(value)) return [];
437
+ return value.flatMap((item) => {
438
+ if (item instanceof RegExp) return [item];
439
+ if (typeof item === "string" && item.trim() !== "") return [item.trim()];
440
+ return [];
441
+ });
442
+ }
443
+ function resolveTraceFetch(value) {
444
+ if (!value) return { enabled: false, propagateTo: [], ignore: [] };
445
+ if (value === true) return { enabled: true, propagateTo: [], ignore: [] };
446
+ return { enabled: true, propagateTo: patterns(value.propagateTo), ignore: patterns(value.ignore) };
447
+ }
448
+ function resolveLogRequests(value) {
449
+ if (value === false) return { enabled: false, include: [], exclude: [], sample: 1 };
450
+ if (value === void 0 || value === true || typeof value !== "object" || value === null) return { enabled: true, include: [], exclude: [], sample: 1 };
451
+ const sample = typeof value.sample === "number" && Number.isFinite(value.sample) && value.sample >= 0 && value.sample <= 1 ? value.sample : 1;
452
+ return { enabled: true, include: patterns(value.include), exclude: patterns(value.exclude), sample };
453
+ }
426
454
  function resolveOptions(options = {}, env = process.env) {
427
455
  const key = options.key?.trim() || void 0;
428
456
  return {
@@ -435,9 +463,10 @@ function resolveOptions(options = {}, env = process.env) {
435
463
  minLevel: options.minLevel && LEVELS.includes(options.minLevel) ? options.minLevel : "info",
436
464
  redact: options.redact === "none" ? "none" : "mask",
437
465
  requestDetail: options.requestDetail === "route" ? "route" : "full",
438
- logRequests: options.logRequests ?? true,
466
+ logRequests: resolveLogRequests(options.logRequests),
439
467
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
440
468
  captureUnhandled: options.captureUnhandled ?? true,
469
+ traceFetch: resolveTraceFetch(options.traceFetch),
441
470
  debug: options.debug ?? false
442
471
  };
443
472
  }
@@ -505,6 +534,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
505
534
  var MAX_LINES_UP = 400;
506
535
  var MAX_HEADER_LINES = 6;
507
536
  var MAX_NAME_LENGTH = 100;
537
+ var MAX_COMMENT_LINES = 40;
508
538
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
509
539
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
510
540
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -517,8 +547,58 @@ var NAME_PATTERNS = [
517
547
  ];
518
548
  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
549
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
520
- function codeOnly(text) {
521
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
550
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
551
+ function lineCommentAt(text, from) {
552
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
553
+ if (index === 0 || text[index - 1] !== ":") return index;
554
+ }
555
+ return -1;
556
+ }
557
+ function blockCommentAt(text, from) {
558
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
559
+ if (index === 0 || text[index - 1] !== "\\") return index;
560
+ }
561
+ return -1;
562
+ }
563
+ function stripComments(text, insideComment, closesLater) {
564
+ let code = "";
565
+ let position = 0;
566
+ let inside = insideComment;
567
+ while (position < text.length) {
568
+ if (inside) {
569
+ const end = text.indexOf("*/", position);
570
+ if (end < 0) return { code, insideComment: true };
571
+ inside = false;
572
+ position = end + 2;
573
+ continue;
574
+ }
575
+ const block = blockCommentAt(text, position);
576
+ const line = lineCommentAt(text, position);
577
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
578
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
579
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
580
+ code += text.slice(position, block + 2);
581
+ position = block + 2;
582
+ continue;
583
+ }
584
+ code += text.slice(position, block);
585
+ inside = true;
586
+ position = block + 2;
587
+ }
588
+ return { code, insideComment: inside };
589
+ }
590
+ function codeLines(lines, from, start, column) {
591
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
592
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
593
+ const result = [];
594
+ let insideComment = false;
595
+ for (let index = 0; index < plain.length; index++) {
596
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
597
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
598
+ insideComment = stripped.insideComment;
599
+ result.push(stripped.code);
600
+ }
601
+ return result;
522
602
  }
523
603
  function parenBalance(text) {
524
604
  let balance = 0;
@@ -528,17 +608,17 @@ function parenBalance(text) {
528
608
  }
529
609
  return balance;
530
610
  }
531
- function headerOf(lines, index, prefix) {
611
+ function headerOf(code, index, prefix) {
532
612
  let header = prefix;
533
613
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
534
614
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
535
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
615
+ header = `${code[previous] ?? ""} ${header}`;
536
616
  }
537
617
  return header;
538
618
  }
539
619
  function nameIn(header) {
540
- const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
541
- for (const pattern of patterns) {
620
+ const patterns2 = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
621
+ for (const pattern of patterns2) {
542
622
  const name = pattern.exec(header)?.[1];
543
623
  if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
544
624
  }
@@ -547,11 +627,12 @@ function nameIn(header) {
547
627
  function enclosingFunctionName(lines, line, column) {
548
628
  const start = line - 1;
549
629
  if (start < 0 || start >= lines.length) return void 0;
630
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
631
+ const cleaned = codeLines(lines, from, start, column);
550
632
  let depth = 0;
551
633
  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);
634
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
635
+ const code = cleaned[index - from] ?? "";
555
636
  for (let position = code.length - 1; position >= 0; position--) {
556
637
  const char = code[position];
557
638
  if (char === "}") {
@@ -561,7 +642,7 @@ function enclosingFunctionName(lines, line, column) {
561
642
  depth -= 1;
562
643
  continue;
563
644
  }
564
- const header = headerOf(lines, index, code.slice(0, position));
645
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
565
646
  if (CLASS_BODY.test(header)) return void 0;
566
647
  if (CONTROL_BLOCK.test(header)) continue;
567
648
  const name = nameIn(header);
@@ -826,10 +907,6 @@ function safeStringify(value) {
826
907
  }
827
908
  }
828
909
 
829
- // src/transport.ts
830
- var import_node_util2 = require("util");
831
- var import_node_zlib = require("zlib");
832
-
833
910
  // src/util.ts
834
911
  var import_node_perf_hooks2 = require("perf_hooks");
835
912
  function describeError(error) {
@@ -839,7 +916,109 @@ function elapsedSince(startedAt) {
839
916
  return Math.round(import_node_perf_hooks2.performance.now() - startedAt);
840
917
  }
841
918
 
919
+ // src/trace-fetch.ts
920
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
921
+ var REQUEST_ID_HEADER = "x-request-id";
922
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
923
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
924
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
925
+ function secretSegment(segment) {
926
+ if (UUID.test(segment)) return false;
927
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
928
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
929
+ }
930
+ function outboundPath(pathname, mode) {
931
+ if (mode === "none") return pathname;
932
+ return pathname.split("/").map((segment) => {
933
+ let decoded = segment;
934
+ try {
935
+ decoded = decodeURIComponent(segment);
936
+ } catch {
937
+ decoded = segment;
938
+ }
939
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
940
+ }).join("/");
941
+ }
942
+ function failureOf(error) {
943
+ const message = describeError(error);
944
+ const cause = error instanceof Error ? error.cause : void 0;
945
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
946
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
947
+ }
948
+ function urlOf(input) {
949
+ try {
950
+ if (typeof input === "string") return new URL(input);
951
+ if (input instanceof URL) return input;
952
+ return new URL(input.url);
953
+ } catch {
954
+ return null;
955
+ }
956
+ }
957
+ function originOf(value) {
958
+ try {
959
+ return new URL(value).origin;
960
+ } catch {
961
+ return null;
962
+ }
963
+ }
964
+ function hostMatches(url, host) {
965
+ const wanted = host.trim().toLowerCase();
966
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
967
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
968
+ }
969
+ function targets(url, target) {
970
+ if (target instanceof RegExp) return url.host.search(target) !== -1;
971
+ return hostMatches(url, target);
972
+ }
973
+ function ignored(url, target) {
974
+ if (target instanceof RegExp) return `${url.host}${url.pathname}`.search(target) !== -1;
975
+ return hostMatches(url, target);
976
+ }
977
+ function isPlainObject(value) {
978
+ if (value === null || typeof value !== "object") return false;
979
+ const prototype = Object.getPrototypeOf(value);
980
+ return prototype === Object.prototype || prototype === null;
981
+ }
982
+ function withRequestId(input, init, requestId) {
983
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
984
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
985
+ headers.set(REQUEST_ID_HEADER, requestId);
986
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
987
+ return [new Request(input, init), { headers }];
988
+ }
989
+ function traceFetch(config) {
990
+ const current = globalThis.fetch;
991
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
992
+ const original = current;
993
+ const radarOrigin = originOf(config.endpoint);
994
+ const traced = async (input, init) => {
995
+ const url = urlOf(input);
996
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
997
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
998
+ const context = currentContext();
999
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
1000
+ if (config.ignore?.some((target) => ignored(url, target))) return original(nextInput, nextInit);
1001
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
1002
+ const started = performance.now();
1003
+ try {
1004
+ const response = await original(nextInput, nextInit);
1005
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
1006
+ return response;
1007
+ } catch (error) {
1008
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
1009
+ throw error;
1010
+ }
1011
+ };
1012
+ Object.defineProperty(traced, WRAPPED, { value: original });
1013
+ globalThis.fetch = traced;
1014
+ return () => {
1015
+ if (globalThis.fetch === traced) globalThis.fetch = original;
1016
+ };
1017
+ }
1018
+
842
1019
  // src/transport.ts
1020
+ var import_node_util2 = require("util");
1021
+ var import_node_zlib = require("zlib");
843
1022
  var gzipAsync = (0, import_node_util2.promisify)(import_node_zlib.gzip);
844
1023
  var MAX_BATCH_BYTES = 9e5;
845
1024
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1067,7 +1246,7 @@ function withTimeout(promise, ms) {
1067
1246
 
1068
1247
  // src/version.ts
1069
1248
  var SDK_NAME = "@oconde/radar";
1070
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1249
+ var SDK_VERSION = true ? "0.4.1" : "0.0.0-dev";
1071
1250
 
1072
1251
  // src/client.ts
1073
1252
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1079,6 +1258,7 @@ var RadarClient = class {
1079
1258
  initialized = false;
1080
1259
  closed = false;
1081
1260
  captured = /* @__PURE__ */ new WeakSet();
1261
+ restoreFetch = null;
1082
1262
  get isEnabled() {
1083
1263
  return this.transport !== null && !this.closed;
1084
1264
  }
@@ -1106,6 +1286,15 @@ var RadarClient = class {
1106
1286
  });
1107
1287
  }
1108
1288
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1289
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1290
+ this.restoreFetch = traceFetch({
1291
+ endpoint: this.options.endpoint,
1292
+ propagateTo: this.options.traceFetch.propagateTo,
1293
+ ignore: this.options.traceFetch.ignore,
1294
+ redact: this.options.redact,
1295
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1296
+ });
1297
+ }
1109
1298
  } catch (error) {
1110
1299
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1111
1300
  }
@@ -1212,22 +1401,28 @@ var RadarClient = class {
1212
1401
  return createMiddleware(this);
1213
1402
  }
1214
1403
  shouldLogRequest(req) {
1215
- return this.options.logRequests && !this.options.ignorePaths.includes(requestPath(req));
1404
+ return this.options.logRequests.enabled && !this.options.ignorePaths.includes(requestPath(req));
1216
1405
  }
1217
1406
  logHttpRequest(context, status) {
1218
1407
  this.safely(() => {
1219
1408
  const req = context.req;
1220
1409
  if (!req) return;
1221
- const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1222
1410
  const route = requestRoute(req, context.route);
1411
+ const path = requestPath(req);
1412
+ const { include, exclude, sample } = this.options.logRequests;
1413
+ if (include.length > 0 && !include.some((pattern) => matchesRequest(pattern, path, route))) return;
1414
+ if (exclude.some((pattern) => matchesRequest(pattern, path, route))) return;
1415
+ if (sample < 1 && Math.random() >= sample) return;
1416
+ const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1223
1417
  runWithContext(
1224
1418
  context,
1225
1419
  () => this.emitLog(level, "http.request", {
1226
1420
  method: (req.method ?? "GET").toUpperCase(),
1227
1421
  ...route ? { route } : {},
1228
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
1422
+ ...this.options.requestDetail === "full" ? { path: redactPath(path, pathParams(req, path), this.options.redact) } : {},
1229
1423
  status,
1230
- durationMs: elapsedSince(context.startedAt)
1424
+ durationMs: elapsedSince(context.startedAt),
1425
+ ...sample < 1 ? { sampleRate: sample } : {}
1231
1426
  })
1232
1427
  );
1233
1428
  });
@@ -1242,6 +1437,8 @@ var RadarClient = class {
1242
1437
  async close(timeoutMs = 2e3) {
1243
1438
  if (this.closed) return;
1244
1439
  await this.flush(timeoutMs);
1440
+ this.restoreFetch?.();
1441
+ this.restoreFetch = null;
1245
1442
  this.closed = true;
1246
1443
  this.transport?.stop();
1247
1444
  }
@@ -1625,7 +1822,7 @@ function contextFor2(event, chooseRequestId) {
1625
1822
  const chosen = chooseRequestId?.(event);
1626
1823
  return { requestId: requestIdFrom(chosen || event.request.headers.get("x-request-id")), startedAt: import_node_perf_hooks4.performance.now(), req, ...route ? { route } : {} };
1627
1824
  }
1628
- function withRequestId(response, requestId) {
1825
+ function withRequestId2(response, requestId) {
1629
1826
  try {
1630
1827
  response.headers.set("x-request-id", requestId);
1631
1828
  return response;
@@ -1657,7 +1854,7 @@ function radarHandle(options = {}) {
1657
1854
  if (client.shouldLogRequest(req)) client.logHttpRequest(context, 500);
1658
1855
  throw error;
1659
1856
  }
1660
- const tagged = withRequestId(response, context.requestId);
1857
+ const tagged = withRequestId2(response, context.requestId);
1661
1858
  if (client.shouldLogRequest(req)) client.logHttpRequest(context, tagged.status);
1662
1859
  return tagged;
1663
1860
  });