@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.
@@ -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";
@@ -281,7 +281,10 @@ function requestPath(req) {
281
281
  }
282
282
  function requestRoute(req, route) {
283
283
  if (route) return route;
284
- return typeof req.route?.path === "string" ? req.route.path : void 0;
284
+ if (typeof req.route?.path !== "string") return void 0;
285
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
286
+ if (!base) return req.route.path;
287
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
285
288
  }
286
289
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
287
290
  function pathSegments(value) {
@@ -401,7 +404,7 @@ function createMiddleware(logger) {
401
404
  let context;
402
405
  try {
403
406
  const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
404
- context = { requestId, startedAt: performance.now(), req };
407
+ context = { requestId, startedAt: performance2.now(), req };
405
408
  res.setHeader("x-request-id", requestId);
406
409
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
407
410
  } catch {
@@ -415,6 +418,12 @@ function createMiddleware(logger) {
415
418
  // src/options.ts
416
419
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
417
420
  var LEVELS = ["debug", "info", "warn", "error"];
421
+ function resolveTraceFetch(value) {
422
+ if (!value) return { enabled: false, propagateTo: [] };
423
+ if (value === true) return { enabled: true, propagateTo: [] };
424
+ const targets2 = Array.isArray(value.propagateTo) ? value.propagateTo : [];
425
+ return { enabled: true, propagateTo: targets2.filter((target) => typeof target === "string" ? target.trim() !== "" : target instanceof RegExp) };
426
+ }
418
427
  function resolveOptions(options = {}, env = process.env) {
419
428
  const key = options.key?.trim() || void 0;
420
429
  return {
@@ -430,6 +439,7 @@ function resolveOptions(options = {}, env = process.env) {
430
439
  logRequests: options.logRequests ?? true,
431
440
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
432
441
  captureUnhandled: options.captureUnhandled ?? true,
442
+ traceFetch: resolveTraceFetch(options.traceFetch),
433
443
  debug: options.debug ?? false
434
444
  };
435
445
  }
@@ -497,6 +507,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
497
507
  var MAX_LINES_UP = 400;
498
508
  var MAX_HEADER_LINES = 6;
499
509
  var MAX_NAME_LENGTH = 100;
510
+ var MAX_COMMENT_LINES = 40;
500
511
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
501
512
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
502
513
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -509,8 +520,58 @@ var NAME_PATTERNS = [
509
520
  ];
510
521
  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
522
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
512
- function codeOnly(text) {
513
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
523
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
524
+ function lineCommentAt(text, from) {
525
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
526
+ if (index === 0 || text[index - 1] !== ":") return index;
527
+ }
528
+ return -1;
529
+ }
530
+ function blockCommentAt(text, from) {
531
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
532
+ if (index === 0 || text[index - 1] !== "\\") return index;
533
+ }
534
+ return -1;
535
+ }
536
+ function stripComments(text, insideComment, closesLater) {
537
+ let code = "";
538
+ let position = 0;
539
+ let inside = insideComment;
540
+ while (position < text.length) {
541
+ if (inside) {
542
+ const end = text.indexOf("*/", position);
543
+ if (end < 0) return { code, insideComment: true };
544
+ inside = false;
545
+ position = end + 2;
546
+ continue;
547
+ }
548
+ const block = blockCommentAt(text, position);
549
+ const line = lineCommentAt(text, position);
550
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
551
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
552
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
553
+ code += text.slice(position, block + 2);
554
+ position = block + 2;
555
+ continue;
556
+ }
557
+ code += text.slice(position, block);
558
+ inside = true;
559
+ position = block + 2;
560
+ }
561
+ return { code, insideComment: inside };
562
+ }
563
+ function codeLines(lines, from, start, column) {
564
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
565
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
566
+ const result = [];
567
+ let insideComment = false;
568
+ for (let index = 0; index < plain.length; index++) {
569
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
570
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
571
+ insideComment = stripped.insideComment;
572
+ result.push(stripped.code);
573
+ }
574
+ return result;
514
575
  }
515
576
  function parenBalance(text) {
516
577
  let balance = 0;
@@ -520,11 +581,11 @@ function parenBalance(text) {
520
581
  }
521
582
  return balance;
522
583
  }
523
- function headerOf(lines, index, prefix) {
584
+ function headerOf(code, index, prefix) {
524
585
  let header = prefix;
525
586
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
526
587
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
527
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
588
+ header = `${code[previous] ?? ""} ${header}`;
528
589
  }
529
590
  return header;
530
591
  }
@@ -539,11 +600,12 @@ function nameIn(header) {
539
600
  function enclosingFunctionName(lines, line, column) {
540
601
  const start = line - 1;
541
602
  if (start < 0 || start >= lines.length) return void 0;
603
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
604
+ const cleaned = codeLines(lines, from, start, column);
542
605
  let depth = 0;
543
606
  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);
607
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
608
+ const code = cleaned[index - from] ?? "";
547
609
  for (let position = code.length - 1; position >= 0; position--) {
548
610
  const char = code[position];
549
611
  if (char === "}") {
@@ -553,7 +615,7 @@ function enclosingFunctionName(lines, line, column) {
553
615
  depth -= 1;
554
616
  continue;
555
617
  }
556
- const header = headerOf(lines, index, code.slice(0, position));
618
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
557
619
  if (CLASS_BODY.test(header)) return void 0;
558
620
  if (CONTROL_BLOCK.test(header)) continue;
559
621
  const name = nameIn(header);
@@ -795,20 +857,110 @@ function safeStringify(value) {
795
857
  }
796
858
  }
797
859
 
798
- // src/transport.ts
799
- import { promisify } from "util";
800
- import { gzip } from "zlib";
801
-
802
860
  // src/util.ts
803
- import { performance as performance2 } from "perf_hooks";
861
+ import { performance as performance3 } from "perf_hooks";
804
862
  function describeError(error) {
805
863
  return error instanceof Error ? error.message : String(error);
806
864
  }
807
865
  function elapsedSince(startedAt) {
808
- return Math.round(performance2.now() - startedAt);
866
+ return Math.round(performance3.now() - startedAt);
867
+ }
868
+
869
+ // src/trace-fetch.ts
870
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
871
+ var REQUEST_ID_HEADER = "x-request-id";
872
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
873
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
874
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
875
+ function secretSegment(segment) {
876
+ if (UUID.test(segment)) return false;
877
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
878
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
879
+ }
880
+ function outboundPath(pathname, mode) {
881
+ if (mode === "none") return pathname;
882
+ return pathname.split("/").map((segment) => {
883
+ let decoded = segment;
884
+ try {
885
+ decoded = decodeURIComponent(segment);
886
+ } catch {
887
+ decoded = segment;
888
+ }
889
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
890
+ }).join("/");
891
+ }
892
+ function failureOf(error) {
893
+ const message = describeError(error);
894
+ const cause = error instanceof Error ? error.cause : void 0;
895
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
896
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
897
+ }
898
+ function urlOf(input) {
899
+ try {
900
+ if (typeof input === "string") return new URL(input);
901
+ if (input instanceof URL) return input;
902
+ return new URL(input.url);
903
+ } catch {
904
+ return null;
905
+ }
906
+ }
907
+ function originOf(value) {
908
+ try {
909
+ return new URL(value).origin;
910
+ } catch {
911
+ return null;
912
+ }
913
+ }
914
+ function targets(url, target) {
915
+ if (target instanceof RegExp) return target.test(url.host);
916
+ const wanted = target.trim().toLowerCase();
917
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
918
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
919
+ }
920
+ function isPlainObject(value) {
921
+ if (value === null || typeof value !== "object") return false;
922
+ const prototype = Object.getPrototypeOf(value);
923
+ return prototype === Object.prototype || prototype === null;
924
+ }
925
+ function withRequestId(input, init, requestId) {
926
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
927
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
928
+ headers.set(REQUEST_ID_HEADER, requestId);
929
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
930
+ return [new Request(input, init), { headers }];
931
+ }
932
+ function traceFetch(config) {
933
+ const current = globalThis.fetch;
934
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
935
+ const original = current;
936
+ const radarOrigin = originOf(config.endpoint);
937
+ const traced = async (input, init) => {
938
+ const url = urlOf(input);
939
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
940
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
941
+ const context = currentContext();
942
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
943
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
944
+ const started = performance.now();
945
+ try {
946
+ const response = await original(nextInput, nextInit);
947
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
948
+ return response;
949
+ } catch (error) {
950
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
951
+ throw error;
952
+ }
953
+ };
954
+ Object.defineProperty(traced, WRAPPED, { value: original });
955
+ globalThis.fetch = traced;
956
+ return () => {
957
+ if (globalThis.fetch === traced) globalThis.fetch = original;
958
+ };
809
959
  }
810
960
 
811
961
  // src/transport.ts
962
+ import { promisify } from "util";
963
+ import { gzip } from "zlib";
812
964
  var gzipAsync = promisify(gzip);
813
965
  var MAX_BATCH_BYTES = 9e5;
814
966
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1036,7 +1188,7 @@ function withTimeout(promise, ms) {
1036
1188
 
1037
1189
  // src/version.ts
1038
1190
  var SDK_NAME = "@oconde/radar";
1039
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1191
+ var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
1040
1192
 
1041
1193
  // src/client.ts
1042
1194
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1048,6 +1200,7 @@ var RadarClient = class {
1048
1200
  initialized = false;
1049
1201
  closed = false;
1050
1202
  captured = /* @__PURE__ */ new WeakSet();
1203
+ restoreFetch = null;
1051
1204
  get isEnabled() {
1052
1205
  return this.transport !== null && !this.closed;
1053
1206
  }
@@ -1075,6 +1228,14 @@ var RadarClient = class {
1075
1228
  });
1076
1229
  }
1077
1230
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1231
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1232
+ this.restoreFetch = traceFetch({
1233
+ endpoint: this.options.endpoint,
1234
+ propagateTo: this.options.traceFetch.propagateTo,
1235
+ redact: this.options.redact,
1236
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1237
+ });
1238
+ }
1078
1239
  } catch (error) {
1079
1240
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1080
1241
  }
@@ -1118,7 +1279,7 @@ var RadarClient = class {
1118
1279
  }
1119
1280
  async track(name, fn, data) {
1120
1281
  const execute = async () => {
1121
- const started = performance3.now();
1282
+ const started = performance4.now();
1122
1283
  try {
1123
1284
  const result = await fn();
1124
1285
  this.info(name, { ...data, durationMs: elapsedSince(started), ok: true });
@@ -1130,10 +1291,10 @@ var RadarClient = class {
1130
1291
  }
1131
1292
  };
1132
1293
  if (currentContext()) return execute();
1133
- return runWithContext({ requestId: randomUUID2(), startedAt: performance3.now() }, execute);
1294
+ return runWithContext({ requestId: randomUUID2(), startedAt: performance4.now() }, execute);
1134
1295
  }
1135
1296
  withContext(fn, options = {}) {
1136
- return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance3.now() }, fn);
1297
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance4.now() }, fn);
1137
1298
  }
1138
1299
  async checkIn(slug, status, options = {}) {
1139
1300
  try {
@@ -1161,7 +1322,7 @@ var RadarClient = class {
1161
1322
  }
1162
1323
  async cron(slug, fn) {
1163
1324
  const checkInId = randomUUID2();
1164
- const started = performance3.now();
1325
+ const started = performance4.now();
1165
1326
  const execute = async () => {
1166
1327
  await this.checkIn(slug, "in_progress", { checkInId });
1167
1328
  try {
@@ -1211,6 +1372,8 @@ var RadarClient = class {
1211
1372
  async close(timeoutMs = 2e3) {
1212
1373
  if (this.closed) return;
1213
1374
  await this.flush(timeoutMs);
1375
+ this.restoreFetch?.();
1376
+ this.restoreFetch = null;
1214
1377
  this.closed = true;
1215
1378
  this.transport?.stop();
1216
1379
  }