@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.
@@ -78,10 +78,10 @@ function requestIdFrom(incoming) {
78
78
  // src/client.ts
79
79
  import { randomUUID as randomUUID2 } from "crypto";
80
80
  import { hostname } from "os";
81
- import { performance as performance3 } from "perf_hooks";
81
+ import { performance as performance4 } from "perf_hooks";
82
82
 
83
83
  // src/express.ts
84
- import { performance } from "perf_hooks";
84
+ import { performance as performance2 } from "perf_hooks";
85
85
 
86
86
  // src/redact.ts
87
87
  import { createHash, createHmac, randomBytes } from "crypto";
@@ -279,9 +279,18 @@ function requestPath(req) {
279
279
  const end = url.indexOf("?");
280
280
  return end >= 0 ? url.slice(0, end) : url;
281
281
  }
282
+ function matchesRequest(pattern, path, route) {
283
+ const candidates = route === void 0 ? [path] : [path, route];
284
+ if (pattern instanceof RegExp) return candidates.some((value) => value.search(pattern) !== -1);
285
+ const prefix = pattern.endsWith("/") ? pattern : `${pattern}/`;
286
+ return candidates.some((value) => value === pattern || value.startsWith(prefix));
287
+ }
282
288
  function requestRoute(req, route) {
283
289
  if (route) return route;
284
- return typeof req.route?.path === "string" ? req.route.path : void 0;
290
+ if (typeof req.route?.path !== "string") return void 0;
291
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
292
+ if (!base) return req.route.path;
293
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
285
294
  }
286
295
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
287
296
  function pathSegments(value) {
@@ -401,7 +410,7 @@ function createMiddleware(logger) {
401
410
  let context;
402
411
  try {
403
412
  const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
404
- context = { requestId, startedAt: performance.now(), req };
413
+ context = { requestId, startedAt: performance2.now(), req };
405
414
  res.setHeader("x-request-id", requestId);
406
415
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
407
416
  } catch {
@@ -415,6 +424,25 @@ function createMiddleware(logger) {
415
424
  // src/options.ts
416
425
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
417
426
  var LEVELS = ["debug", "info", "warn", "error"];
427
+ function patterns(value) {
428
+ if (!Array.isArray(value)) return [];
429
+ return value.flatMap((item) => {
430
+ if (item instanceof RegExp) return [item];
431
+ if (typeof item === "string" && item.trim() !== "") return [item.trim()];
432
+ return [];
433
+ });
434
+ }
435
+ function resolveTraceFetch(value) {
436
+ if (!value) return { enabled: false, propagateTo: [], ignore: [] };
437
+ if (value === true) return { enabled: true, propagateTo: [], ignore: [] };
438
+ return { enabled: true, propagateTo: patterns(value.propagateTo), ignore: patterns(value.ignore) };
439
+ }
440
+ function resolveLogRequests(value) {
441
+ if (value === false) return { enabled: false, include: [], exclude: [], sample: 1 };
442
+ if (value === void 0 || value === true || typeof value !== "object" || value === null) return { enabled: true, include: [], exclude: [], sample: 1 };
443
+ const sample = typeof value.sample === "number" && Number.isFinite(value.sample) && value.sample >= 0 && value.sample <= 1 ? value.sample : 1;
444
+ return { enabled: true, include: patterns(value.include), exclude: patterns(value.exclude), sample };
445
+ }
418
446
  function resolveOptions(options = {}, env = process.env) {
419
447
  const key = options.key?.trim() || void 0;
420
448
  return {
@@ -427,9 +455,10 @@ function resolveOptions(options = {}, env = process.env) {
427
455
  minLevel: options.minLevel && LEVELS.includes(options.minLevel) ? options.minLevel : "info",
428
456
  redact: options.redact === "none" ? "none" : "mask",
429
457
  requestDetail: options.requestDetail === "route" ? "route" : "full",
430
- logRequests: options.logRequests ?? true,
458
+ logRequests: resolveLogRequests(options.logRequests),
431
459
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
432
460
  captureUnhandled: options.captureUnhandled ?? true,
461
+ traceFetch: resolveTraceFetch(options.traceFetch),
433
462
  debug: options.debug ?? false
434
463
  };
435
464
  }
@@ -497,6 +526,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
497
526
  var MAX_LINES_UP = 400;
498
527
  var MAX_HEADER_LINES = 6;
499
528
  var MAX_NAME_LENGTH = 100;
529
+ var MAX_COMMENT_LINES = 40;
500
530
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
501
531
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
502
532
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -509,8 +539,58 @@ var NAME_PATTERNS = [
509
539
  ];
510
540
  var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
511
541
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
512
- function codeOnly(text) {
513
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
542
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
543
+ function lineCommentAt(text, from) {
544
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
545
+ if (index === 0 || text[index - 1] !== ":") return index;
546
+ }
547
+ return -1;
548
+ }
549
+ function blockCommentAt(text, from) {
550
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
551
+ if (index === 0 || text[index - 1] !== "\\") return index;
552
+ }
553
+ return -1;
554
+ }
555
+ function stripComments(text, insideComment, closesLater) {
556
+ let code = "";
557
+ let position = 0;
558
+ let inside = insideComment;
559
+ while (position < text.length) {
560
+ if (inside) {
561
+ const end = text.indexOf("*/", position);
562
+ if (end < 0) return { code, insideComment: true };
563
+ inside = false;
564
+ position = end + 2;
565
+ continue;
566
+ }
567
+ const block = blockCommentAt(text, position);
568
+ const line = lineCommentAt(text, position);
569
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
570
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
571
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
572
+ code += text.slice(position, block + 2);
573
+ position = block + 2;
574
+ continue;
575
+ }
576
+ code += text.slice(position, block);
577
+ inside = true;
578
+ position = block + 2;
579
+ }
580
+ return { code, insideComment: inside };
581
+ }
582
+ function codeLines(lines, from, start, column) {
583
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
584
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
585
+ const result = [];
586
+ let insideComment = false;
587
+ for (let index = 0; index < plain.length; index++) {
588
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
589
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
590
+ insideComment = stripped.insideComment;
591
+ result.push(stripped.code);
592
+ }
593
+ return result;
514
594
  }
515
595
  function parenBalance(text) {
516
596
  let balance = 0;
@@ -520,17 +600,17 @@ function parenBalance(text) {
520
600
  }
521
601
  return balance;
522
602
  }
523
- function headerOf(lines, index, prefix) {
603
+ function headerOf(code, index, prefix) {
524
604
  let header = prefix;
525
605
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
526
606
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
527
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
607
+ header = `${code[previous] ?? ""} ${header}`;
528
608
  }
529
609
  return header;
530
610
  }
531
611
  function nameIn(header) {
532
- const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
533
- for (const pattern of patterns) {
612
+ const patterns2 = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
613
+ for (const pattern of patterns2) {
534
614
  const name = pattern.exec(header)?.[1];
535
615
  if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
536
616
  }
@@ -539,11 +619,12 @@ function nameIn(header) {
539
619
  function enclosingFunctionName(lines, line, column) {
540
620
  const start = line - 1;
541
621
  if (start < 0 || start >= lines.length) return void 0;
622
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
623
+ const cleaned = codeLines(lines, from, start, column);
542
624
  let depth = 0;
543
625
  for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
544
- const raw = lines[index] ?? "";
545
- if (index < start && MARKUP_BOUNDARY.test(raw)) return void 0;
546
- const code = codeOnly(index === start && column ? raw.slice(0, Math.max(0, column - 1)) : raw);
626
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
627
+ const code = cleaned[index - from] ?? "";
547
628
  for (let position = code.length - 1; position >= 0; position--) {
548
629
  const char = code[position];
549
630
  if (char === "}") {
@@ -553,7 +634,7 @@ function enclosingFunctionName(lines, line, column) {
553
634
  depth -= 1;
554
635
  continue;
555
636
  }
556
- const header = headerOf(lines, index, code.slice(0, position));
637
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
557
638
  if (CLASS_BODY.test(header)) return void 0;
558
639
  if (CONTROL_BLOCK.test(header)) continue;
559
640
  const name = nameIn(header);
@@ -795,20 +876,118 @@ function safeStringify(value) {
795
876
  }
796
877
  }
797
878
 
798
- // src/transport.ts
799
- import { promisify } from "util";
800
- import { gzip } from "zlib";
801
-
802
879
  // src/util.ts
803
- import { performance as performance2 } from "perf_hooks";
880
+ import { performance as performance3 } from "perf_hooks";
804
881
  function describeError(error) {
805
882
  return error instanceof Error ? error.message : String(error);
806
883
  }
807
884
  function elapsedSince(startedAt) {
808
- return Math.round(performance2.now() - startedAt);
885
+ return Math.round(performance3.now() - startedAt);
886
+ }
887
+
888
+ // src/trace-fetch.ts
889
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
890
+ var REQUEST_ID_HEADER = "x-request-id";
891
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
892
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
893
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
894
+ function secretSegment(segment) {
895
+ if (UUID.test(segment)) return false;
896
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
897
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
898
+ }
899
+ function outboundPath(pathname, mode) {
900
+ if (mode === "none") return pathname;
901
+ return pathname.split("/").map((segment) => {
902
+ let decoded = segment;
903
+ try {
904
+ decoded = decodeURIComponent(segment);
905
+ } catch {
906
+ decoded = segment;
907
+ }
908
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
909
+ }).join("/");
910
+ }
911
+ function failureOf(error) {
912
+ const message = describeError(error);
913
+ const cause = error instanceof Error ? error.cause : void 0;
914
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
915
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
916
+ }
917
+ function urlOf(input) {
918
+ try {
919
+ if (typeof input === "string") return new URL(input);
920
+ if (input instanceof URL) return input;
921
+ return new URL(input.url);
922
+ } catch {
923
+ return null;
924
+ }
925
+ }
926
+ function originOf(value) {
927
+ try {
928
+ return new URL(value).origin;
929
+ } catch {
930
+ return null;
931
+ }
932
+ }
933
+ function hostMatches(url, host) {
934
+ const wanted = host.trim().toLowerCase();
935
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
936
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
937
+ }
938
+ function targets(url, target) {
939
+ if (target instanceof RegExp) return url.host.search(target) !== -1;
940
+ return hostMatches(url, target);
941
+ }
942
+ function ignored(url, target) {
943
+ if (target instanceof RegExp) return `${url.host}${url.pathname}`.search(target) !== -1;
944
+ return hostMatches(url, target);
945
+ }
946
+ function isPlainObject(value) {
947
+ if (value === null || typeof value !== "object") return false;
948
+ const prototype = Object.getPrototypeOf(value);
949
+ return prototype === Object.prototype || prototype === null;
950
+ }
951
+ function withRequestId(input, init, requestId) {
952
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
953
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
954
+ headers.set(REQUEST_ID_HEADER, requestId);
955
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
956
+ return [new Request(input, init), { headers }];
957
+ }
958
+ function traceFetch(config) {
959
+ const current = globalThis.fetch;
960
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
961
+ const original = current;
962
+ const radarOrigin = originOf(config.endpoint);
963
+ const traced = async (input, init) => {
964
+ const url = urlOf(input);
965
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
966
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
967
+ const context = currentContext();
968
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
969
+ if (config.ignore?.some((target) => ignored(url, target))) return original(nextInput, nextInit);
970
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
971
+ const started = performance.now();
972
+ try {
973
+ const response = await original(nextInput, nextInit);
974
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
975
+ return response;
976
+ } catch (error) {
977
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
978
+ throw error;
979
+ }
980
+ };
981
+ Object.defineProperty(traced, WRAPPED, { value: original });
982
+ globalThis.fetch = traced;
983
+ return () => {
984
+ if (globalThis.fetch === traced) globalThis.fetch = original;
985
+ };
809
986
  }
810
987
 
811
988
  // src/transport.ts
989
+ import { promisify } from "util";
990
+ import { gzip } from "zlib";
812
991
  var gzipAsync = promisify(gzip);
813
992
  var MAX_BATCH_BYTES = 9e5;
814
993
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1036,7 +1215,7 @@ function withTimeout(promise, ms) {
1036
1215
 
1037
1216
  // src/version.ts
1038
1217
  var SDK_NAME = "@oconde/radar";
1039
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1218
+ var SDK_VERSION = true ? "0.4.1" : "0.0.0-dev";
1040
1219
 
1041
1220
  // src/client.ts
1042
1221
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1048,6 +1227,7 @@ var RadarClient = class {
1048
1227
  initialized = false;
1049
1228
  closed = false;
1050
1229
  captured = /* @__PURE__ */ new WeakSet();
1230
+ restoreFetch = null;
1051
1231
  get isEnabled() {
1052
1232
  return this.transport !== null && !this.closed;
1053
1233
  }
@@ -1075,6 +1255,15 @@ var RadarClient = class {
1075
1255
  });
1076
1256
  }
1077
1257
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1258
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1259
+ this.restoreFetch = traceFetch({
1260
+ endpoint: this.options.endpoint,
1261
+ propagateTo: this.options.traceFetch.propagateTo,
1262
+ ignore: this.options.traceFetch.ignore,
1263
+ redact: this.options.redact,
1264
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1265
+ });
1266
+ }
1078
1267
  } catch (error) {
1079
1268
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1080
1269
  }
@@ -1118,7 +1307,7 @@ var RadarClient = class {
1118
1307
  }
1119
1308
  async track(name, fn, data) {
1120
1309
  const execute = async () => {
1121
- const started = performance3.now();
1310
+ const started = performance4.now();
1122
1311
  try {
1123
1312
  const result = await fn();
1124
1313
  this.info(name, { ...data, durationMs: elapsedSince(started), ok: true });
@@ -1130,10 +1319,10 @@ var RadarClient = class {
1130
1319
  }
1131
1320
  };
1132
1321
  if (currentContext()) return execute();
1133
- return runWithContext({ requestId: randomUUID2(), startedAt: performance3.now() }, execute);
1322
+ return runWithContext({ requestId: randomUUID2(), startedAt: performance4.now() }, execute);
1134
1323
  }
1135
1324
  withContext(fn, options = {}) {
1136
- return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance3.now() }, fn);
1325
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance4.now() }, fn);
1137
1326
  }
1138
1327
  async checkIn(slug, status, options = {}) {
1139
1328
  try {
@@ -1161,7 +1350,7 @@ var RadarClient = class {
1161
1350
  }
1162
1351
  async cron(slug, fn) {
1163
1352
  const checkInId = randomUUID2();
1164
- const started = performance3.now();
1353
+ const started = performance4.now();
1165
1354
  const execute = async () => {
1166
1355
  await this.checkIn(slug, "in_progress", { checkInId });
1167
1356
  try {
@@ -1181,22 +1370,28 @@ var RadarClient = class {
1181
1370
  return createMiddleware(this);
1182
1371
  }
1183
1372
  shouldLogRequest(req) {
1184
- return this.options.logRequests && !this.options.ignorePaths.includes(requestPath(req));
1373
+ return this.options.logRequests.enabled && !this.options.ignorePaths.includes(requestPath(req));
1185
1374
  }
1186
1375
  logHttpRequest(context, status) {
1187
1376
  this.safely(() => {
1188
1377
  const req = context.req;
1189
1378
  if (!req) return;
1190
- const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1191
1379
  const route = requestRoute(req, context.route);
1380
+ const path = requestPath(req);
1381
+ const { include, exclude, sample } = this.options.logRequests;
1382
+ if (include.length > 0 && !include.some((pattern) => matchesRequest(pattern, path, route))) return;
1383
+ if (exclude.some((pattern) => matchesRequest(pattern, path, route))) return;
1384
+ if (sample < 1 && Math.random() >= sample) return;
1385
+ const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1192
1386
  runWithContext(
1193
1387
  context,
1194
1388
  () => this.emitLog(level, "http.request", {
1195
1389
  method: (req.method ?? "GET").toUpperCase(),
1196
1390
  ...route ? { route } : {},
1197
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
1391
+ ...this.options.requestDetail === "full" ? { path: redactPath(path, pathParams(req, path), this.options.redact) } : {},
1198
1392
  status,
1199
- durationMs: elapsedSince(context.startedAt)
1393
+ durationMs: elapsedSince(context.startedAt),
1394
+ ...sample < 1 ? { sampleRate: sample } : {}
1200
1395
  })
1201
1396
  );
1202
1397
  });
@@ -1211,6 +1406,8 @@ var RadarClient = class {
1211
1406
  async close(timeoutMs = 2e3) {
1212
1407
  if (this.closed) return;
1213
1408
  await this.flush(timeoutMs);
1409
+ this.restoreFetch?.();
1410
+ this.restoreFetch = null;
1214
1411
  this.closed = true;
1215
1412
  this.transport?.stop();
1216
1413
  }