@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.
@@ -303,9 +303,18 @@ function requestPath(req) {
303
303
  const end = url.indexOf("?");
304
304
  return end >= 0 ? url.slice(0, end) : url;
305
305
  }
306
+ function matchesRequest(pattern, path, route) {
307
+ const candidates = route === void 0 ? [path] : [path, route];
308
+ if (pattern instanceof RegExp) return candidates.some((value) => value.search(pattern) !== -1);
309
+ const prefix = pattern.endsWith("/") ? pattern : `${pattern}/`;
310
+ return candidates.some((value) => value === pattern || value.startsWith(prefix));
311
+ }
306
312
  function requestRoute(req, route) {
307
313
  if (route) return route;
308
- return typeof req.route?.path === "string" ? req.route.path : void 0;
314
+ if (typeof req.route?.path !== "string") return void 0;
315
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
316
+ if (!base) return req.route.path;
317
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
309
318
  }
310
319
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
311
320
  function pathSegments(value) {
@@ -439,6 +448,25 @@ function createMiddleware(logger) {
439
448
  // src/options.ts
440
449
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
441
450
  var LEVELS = ["debug", "info", "warn", "error"];
451
+ function patterns(value) {
452
+ if (!Array.isArray(value)) return [];
453
+ return value.flatMap((item) => {
454
+ if (item instanceof RegExp) return [item];
455
+ if (typeof item === "string" && item.trim() !== "") return [item.trim()];
456
+ return [];
457
+ });
458
+ }
459
+ function resolveTraceFetch(value) {
460
+ if (!value) return { enabled: false, propagateTo: [], ignore: [] };
461
+ if (value === true) return { enabled: true, propagateTo: [], ignore: [] };
462
+ return { enabled: true, propagateTo: patterns(value.propagateTo), ignore: patterns(value.ignore) };
463
+ }
464
+ function resolveLogRequests(value) {
465
+ if (value === false) return { enabled: false, include: [], exclude: [], sample: 1 };
466
+ if (value === void 0 || value === true || typeof value !== "object" || value === null) return { enabled: true, include: [], exclude: [], sample: 1 };
467
+ const sample = typeof value.sample === "number" && Number.isFinite(value.sample) && value.sample >= 0 && value.sample <= 1 ? value.sample : 1;
468
+ return { enabled: true, include: patterns(value.include), exclude: patterns(value.exclude), sample };
469
+ }
442
470
  function resolveOptions(options = {}, env = process.env) {
443
471
  const key = options.key?.trim() || void 0;
444
472
  return {
@@ -451,9 +479,10 @@ function resolveOptions(options = {}, env = process.env) {
451
479
  minLevel: options.minLevel && LEVELS.includes(options.minLevel) ? options.minLevel : "info",
452
480
  redact: options.redact === "none" ? "none" : "mask",
453
481
  requestDetail: options.requestDetail === "route" ? "route" : "full",
454
- logRequests: options.logRequests ?? true,
482
+ logRequests: resolveLogRequests(options.logRequests),
455
483
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
456
484
  captureUnhandled: options.captureUnhandled ?? true,
485
+ traceFetch: resolveTraceFetch(options.traceFetch),
457
486
  debug: options.debug ?? false
458
487
  };
459
488
  }
@@ -521,6 +550,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
521
550
  var MAX_LINES_UP = 400;
522
551
  var MAX_HEADER_LINES = 6;
523
552
  var MAX_NAME_LENGTH = 100;
553
+ var MAX_COMMENT_LINES = 40;
524
554
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
525
555
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
526
556
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -533,8 +563,58 @@ var NAME_PATTERNS = [
533
563
  ];
534
564
  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
565
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
536
- function codeOnly(text) {
537
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
566
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
567
+ function lineCommentAt(text, from) {
568
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
569
+ if (index === 0 || text[index - 1] !== ":") return index;
570
+ }
571
+ return -1;
572
+ }
573
+ function blockCommentAt(text, from) {
574
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
575
+ if (index === 0 || text[index - 1] !== "\\") return index;
576
+ }
577
+ return -1;
578
+ }
579
+ function stripComments(text, insideComment, closesLater) {
580
+ let code = "";
581
+ let position = 0;
582
+ let inside = insideComment;
583
+ while (position < text.length) {
584
+ if (inside) {
585
+ const end = text.indexOf("*/", position);
586
+ if (end < 0) return { code, insideComment: true };
587
+ inside = false;
588
+ position = end + 2;
589
+ continue;
590
+ }
591
+ const block = blockCommentAt(text, position);
592
+ const line = lineCommentAt(text, position);
593
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
594
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
595
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
596
+ code += text.slice(position, block + 2);
597
+ position = block + 2;
598
+ continue;
599
+ }
600
+ code += text.slice(position, block);
601
+ inside = true;
602
+ position = block + 2;
603
+ }
604
+ return { code, insideComment: inside };
605
+ }
606
+ function codeLines(lines, from, start, column) {
607
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
608
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
609
+ const result = [];
610
+ let insideComment = false;
611
+ for (let index = 0; index < plain.length; index++) {
612
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
613
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
614
+ insideComment = stripped.insideComment;
615
+ result.push(stripped.code);
616
+ }
617
+ return result;
538
618
  }
539
619
  function parenBalance(text) {
540
620
  let balance = 0;
@@ -544,17 +624,17 @@ function parenBalance(text) {
544
624
  }
545
625
  return balance;
546
626
  }
547
- function headerOf(lines, index, prefix) {
627
+ function headerOf(code, index, prefix) {
548
628
  let header = prefix;
549
629
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
550
630
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
551
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
631
+ header = `${code[previous] ?? ""} ${header}`;
552
632
  }
553
633
  return header;
554
634
  }
555
635
  function nameIn(header) {
556
- const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
557
- for (const pattern of patterns) {
636
+ const patterns2 = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
637
+ for (const pattern of patterns2) {
558
638
  const name = pattern.exec(header)?.[1];
559
639
  if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
560
640
  }
@@ -563,11 +643,12 @@ function nameIn(header) {
563
643
  function enclosingFunctionName(lines, line, column) {
564
644
  const start = line - 1;
565
645
  if (start < 0 || start >= lines.length) return void 0;
646
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
647
+ const cleaned = codeLines(lines, from, start, column);
566
648
  let depth = 0;
567
649
  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);
650
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
651
+ const code = cleaned[index - from] ?? "";
571
652
  for (let position = code.length - 1; position >= 0; position--) {
572
653
  const char = code[position];
573
654
  if (char === "}") {
@@ -577,7 +658,7 @@ function enclosingFunctionName(lines, line, column) {
577
658
  depth -= 1;
578
659
  continue;
579
660
  }
580
- const header = headerOf(lines, index, code.slice(0, position));
661
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
581
662
  if (CLASS_BODY.test(header)) return void 0;
582
663
  if (CONTROL_BLOCK.test(header)) continue;
583
664
  const name = nameIn(header);
@@ -819,10 +900,6 @@ function safeStringify(value) {
819
900
  }
820
901
  }
821
902
 
822
- // src/transport.ts
823
- var import_node_util2 = require("util");
824
- var import_node_zlib = require("zlib");
825
-
826
903
  // src/util.ts
827
904
  var import_node_perf_hooks2 = require("perf_hooks");
828
905
  function describeError(error) {
@@ -832,7 +909,109 @@ function elapsedSince(startedAt) {
832
909
  return Math.round(import_node_perf_hooks2.performance.now() - startedAt);
833
910
  }
834
911
 
912
+ // src/trace-fetch.ts
913
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
914
+ var REQUEST_ID_HEADER = "x-request-id";
915
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
916
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
917
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
918
+ function secretSegment(segment) {
919
+ if (UUID.test(segment)) return false;
920
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
921
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
922
+ }
923
+ function outboundPath(pathname, mode) {
924
+ if (mode === "none") return pathname;
925
+ return pathname.split("/").map((segment) => {
926
+ let decoded = segment;
927
+ try {
928
+ decoded = decodeURIComponent(segment);
929
+ } catch {
930
+ decoded = segment;
931
+ }
932
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
933
+ }).join("/");
934
+ }
935
+ function failureOf(error) {
936
+ const message = describeError(error);
937
+ const cause = error instanceof Error ? error.cause : void 0;
938
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
939
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
940
+ }
941
+ function urlOf(input) {
942
+ try {
943
+ if (typeof input === "string") return new URL(input);
944
+ if (input instanceof URL) return input;
945
+ return new URL(input.url);
946
+ } catch {
947
+ return null;
948
+ }
949
+ }
950
+ function originOf(value) {
951
+ try {
952
+ return new URL(value).origin;
953
+ } catch {
954
+ return null;
955
+ }
956
+ }
957
+ function hostMatches(url, host) {
958
+ const wanted = host.trim().toLowerCase();
959
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
960
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
961
+ }
962
+ function targets(url, target) {
963
+ if (target instanceof RegExp) return url.host.search(target) !== -1;
964
+ return hostMatches(url, target);
965
+ }
966
+ function ignored(url, target) {
967
+ if (target instanceof RegExp) return `${url.host}${url.pathname}`.search(target) !== -1;
968
+ return hostMatches(url, target);
969
+ }
970
+ function isPlainObject(value) {
971
+ if (value === null || typeof value !== "object") return false;
972
+ const prototype = Object.getPrototypeOf(value);
973
+ return prototype === Object.prototype || prototype === null;
974
+ }
975
+ function withRequestId(input, init, requestId) {
976
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
977
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
978
+ headers.set(REQUEST_ID_HEADER, requestId);
979
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
980
+ return [new Request(input, init), { headers }];
981
+ }
982
+ function traceFetch(config) {
983
+ const current = globalThis.fetch;
984
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
985
+ const original = current;
986
+ const radarOrigin = originOf(config.endpoint);
987
+ const traced = async (input, init) => {
988
+ const url = urlOf(input);
989
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
990
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
991
+ const context = currentContext();
992
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
993
+ if (config.ignore?.some((target) => ignored(url, target))) return original(nextInput, nextInit);
994
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
995
+ const started = performance.now();
996
+ try {
997
+ const response = await original(nextInput, nextInit);
998
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
999
+ return response;
1000
+ } catch (error) {
1001
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
1002
+ throw error;
1003
+ }
1004
+ };
1005
+ Object.defineProperty(traced, WRAPPED, { value: original });
1006
+ globalThis.fetch = traced;
1007
+ return () => {
1008
+ if (globalThis.fetch === traced) globalThis.fetch = original;
1009
+ };
1010
+ }
1011
+
835
1012
  // src/transport.ts
1013
+ var import_node_util2 = require("util");
1014
+ var import_node_zlib = require("zlib");
836
1015
  var gzipAsync = (0, import_node_util2.promisify)(import_node_zlib.gzip);
837
1016
  var MAX_BATCH_BYTES = 9e5;
838
1017
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1060,7 +1239,7 @@ function withTimeout(promise, ms) {
1060
1239
 
1061
1240
  // src/version.ts
1062
1241
  var SDK_NAME = "@oconde/radar";
1063
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1242
+ var SDK_VERSION = true ? "0.4.1" : "0.0.0-dev";
1064
1243
 
1065
1244
  // src/client.ts
1066
1245
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1072,6 +1251,7 @@ var RadarClient = class {
1072
1251
  initialized = false;
1073
1252
  closed = false;
1074
1253
  captured = /* @__PURE__ */ new WeakSet();
1254
+ restoreFetch = null;
1075
1255
  get isEnabled() {
1076
1256
  return this.transport !== null && !this.closed;
1077
1257
  }
@@ -1099,6 +1279,15 @@ var RadarClient = class {
1099
1279
  });
1100
1280
  }
1101
1281
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1282
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1283
+ this.restoreFetch = traceFetch({
1284
+ endpoint: this.options.endpoint,
1285
+ propagateTo: this.options.traceFetch.propagateTo,
1286
+ ignore: this.options.traceFetch.ignore,
1287
+ redact: this.options.redact,
1288
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1289
+ });
1290
+ }
1102
1291
  } catch (error) {
1103
1292
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1104
1293
  }
@@ -1205,22 +1394,28 @@ var RadarClient = class {
1205
1394
  return createMiddleware(this);
1206
1395
  }
1207
1396
  shouldLogRequest(req) {
1208
- return this.options.logRequests && !this.options.ignorePaths.includes(requestPath(req));
1397
+ return this.options.logRequests.enabled && !this.options.ignorePaths.includes(requestPath(req));
1209
1398
  }
1210
1399
  logHttpRequest(context, status) {
1211
1400
  this.safely(() => {
1212
1401
  const req = context.req;
1213
1402
  if (!req) return;
1214
- const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1215
1403
  const route = requestRoute(req, context.route);
1404
+ const path = requestPath(req);
1405
+ const { include, exclude, sample } = this.options.logRequests;
1406
+ if (include.length > 0 && !include.some((pattern) => matchesRequest(pattern, path, route))) return;
1407
+ if (exclude.some((pattern) => matchesRequest(pattern, path, route))) return;
1408
+ if (sample < 1 && Math.random() >= sample) return;
1409
+ const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1216
1410
  runWithContext(
1217
1411
  context,
1218
1412
  () => this.emitLog(level, "http.request", {
1219
1413
  method: (req.method ?? "GET").toUpperCase(),
1220
1414
  ...route ? { route } : {},
1221
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
1415
+ ...this.options.requestDetail === "full" ? { path: redactPath(path, pathParams(req, path), this.options.redact) } : {},
1222
1416
  status,
1223
- durationMs: elapsedSince(context.startedAt)
1417
+ durationMs: elapsedSince(context.startedAt),
1418
+ ...sample < 1 ? { sampleRate: sample } : {}
1224
1419
  })
1225
1420
  );
1226
1421
  });
@@ -1235,6 +1430,8 @@ var RadarClient = class {
1235
1430
  async close(timeoutMs = 2e3) {
1236
1431
  if (this.closed) return;
1237
1432
  await this.flush(timeoutMs);
1433
+ this.restoreFetch?.();
1434
+ this.restoreFetch = null;
1238
1435
  this.closed = true;
1239
1436
  this.transport?.stop();
1240
1437
  }