@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.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/client.ts
2
2
  import { randomUUID as randomUUID2 } from "crypto";
3
3
  import { hostname } from "os";
4
- import { performance as performance3 } from "perf_hooks";
4
+ import { performance as performance4 } from "perf_hooks";
5
5
 
6
6
  // src/context.ts
7
7
  import { AsyncLocalStorage } from "async_hooks";
@@ -55,7 +55,7 @@ function requestIdFrom(incoming) {
55
55
  }
56
56
 
57
57
  // src/express.ts
58
- import { performance } from "perf_hooks";
58
+ import { performance as performance2 } from "perf_hooks";
59
59
 
60
60
  // src/redact.ts
61
61
  import { createHash, createHmac, randomBytes } from "crypto";
@@ -255,7 +255,10 @@ function requestPath(req) {
255
255
  }
256
256
  function requestRoute(req, route) {
257
257
  if (route) return route;
258
- return typeof req.route?.path === "string" ? req.route.path : void 0;
258
+ if (typeof req.route?.path !== "string") return void 0;
259
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
260
+ if (!base) return req.route.path;
261
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
259
262
  }
260
263
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
261
264
  function pathSegments(value) {
@@ -375,7 +378,7 @@ function createMiddleware(logger) {
375
378
  let context;
376
379
  try {
377
380
  const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
378
- context = { requestId, startedAt: performance.now(), req };
381
+ context = { requestId, startedAt: performance2.now(), req };
379
382
  res.setHeader("x-request-id", requestId);
380
383
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
381
384
  } catch {
@@ -389,6 +392,12 @@ function createMiddleware(logger) {
389
392
  // src/options.ts
390
393
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
391
394
  var LEVELS = ["debug", "info", "warn", "error"];
395
+ function resolveTraceFetch(value) {
396
+ if (!value) return { enabled: false, propagateTo: [] };
397
+ if (value === true) return { enabled: true, propagateTo: [] };
398
+ const targets2 = Array.isArray(value.propagateTo) ? value.propagateTo : [];
399
+ return { enabled: true, propagateTo: targets2.filter((target) => typeof target === "string" ? target.trim() !== "" : target instanceof RegExp) };
400
+ }
392
401
  function resolveOptions(options = {}, env = process.env) {
393
402
  const key = options.key?.trim() || void 0;
394
403
  return {
@@ -404,6 +413,7 @@ function resolveOptions(options = {}, env = process.env) {
404
413
  logRequests: options.logRequests ?? true,
405
414
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
406
415
  captureUnhandled: options.captureUnhandled ?? true,
416
+ traceFetch: resolveTraceFetch(options.traceFetch),
407
417
  debug: options.debug ?? false
408
418
  };
409
419
  }
@@ -471,6 +481,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
471
481
  var MAX_LINES_UP = 400;
472
482
  var MAX_HEADER_LINES = 6;
473
483
  var MAX_NAME_LENGTH = 100;
484
+ var MAX_COMMENT_LINES = 40;
474
485
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
475
486
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
476
487
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -483,8 +494,58 @@ var NAME_PATTERNS = [
483
494
  ];
484
495
  var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
485
496
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
486
- function codeOnly(text) {
487
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
497
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
498
+ function lineCommentAt(text, from) {
499
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
500
+ if (index === 0 || text[index - 1] !== ":") return index;
501
+ }
502
+ return -1;
503
+ }
504
+ function blockCommentAt(text, from) {
505
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
506
+ if (index === 0 || text[index - 1] !== "\\") return index;
507
+ }
508
+ return -1;
509
+ }
510
+ function stripComments(text, insideComment, closesLater) {
511
+ let code = "";
512
+ let position = 0;
513
+ let inside = insideComment;
514
+ while (position < text.length) {
515
+ if (inside) {
516
+ const end = text.indexOf("*/", position);
517
+ if (end < 0) return { code, insideComment: true };
518
+ inside = false;
519
+ position = end + 2;
520
+ continue;
521
+ }
522
+ const block = blockCommentAt(text, position);
523
+ const line = lineCommentAt(text, position);
524
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
525
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
526
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
527
+ code += text.slice(position, block + 2);
528
+ position = block + 2;
529
+ continue;
530
+ }
531
+ code += text.slice(position, block);
532
+ inside = true;
533
+ position = block + 2;
534
+ }
535
+ return { code, insideComment: inside };
536
+ }
537
+ function codeLines(lines, from, start, column) {
538
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
539
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
540
+ const result = [];
541
+ let insideComment = false;
542
+ for (let index = 0; index < plain.length; index++) {
543
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
544
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
545
+ insideComment = stripped.insideComment;
546
+ result.push(stripped.code);
547
+ }
548
+ return result;
488
549
  }
489
550
  function parenBalance(text) {
490
551
  let balance = 0;
@@ -494,11 +555,11 @@ function parenBalance(text) {
494
555
  }
495
556
  return balance;
496
557
  }
497
- function headerOf(lines, index, prefix) {
558
+ function headerOf(code, index, prefix) {
498
559
  let header = prefix;
499
560
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
500
561
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
501
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
562
+ header = `${code[previous] ?? ""} ${header}`;
502
563
  }
503
564
  return header;
504
565
  }
@@ -513,11 +574,12 @@ function nameIn(header) {
513
574
  function enclosingFunctionName(lines, line, column) {
514
575
  const start = line - 1;
515
576
  if (start < 0 || start >= lines.length) return void 0;
577
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
578
+ const cleaned = codeLines(lines, from, start, column);
516
579
  let depth = 0;
517
580
  for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
518
- const raw = lines[index] ?? "";
519
- if (index < start && MARKUP_BOUNDARY.test(raw)) return void 0;
520
- const code = codeOnly(index === start && column ? raw.slice(0, Math.max(0, column - 1)) : raw);
581
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
582
+ const code = cleaned[index - from] ?? "";
521
583
  for (let position = code.length - 1; position >= 0; position--) {
522
584
  const char = code[position];
523
585
  if (char === "}") {
@@ -527,7 +589,7 @@ function enclosingFunctionName(lines, line, column) {
527
589
  depth -= 1;
528
590
  continue;
529
591
  }
530
- const header = headerOf(lines, index, code.slice(0, position));
592
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
531
593
  if (CLASS_BODY.test(header)) return void 0;
532
594
  if (CONTROL_BLOCK.test(header)) continue;
533
595
  const name = nameIn(header);
@@ -769,20 +831,110 @@ function safeStringify(value) {
769
831
  }
770
832
  }
771
833
 
772
- // src/transport.ts
773
- import { promisify } from "util";
774
- import { gzip } from "zlib";
775
-
776
834
  // src/util.ts
777
- import { performance as performance2 } from "perf_hooks";
835
+ import { performance as performance3 } from "perf_hooks";
778
836
  function describeError(error) {
779
837
  return error instanceof Error ? error.message : String(error);
780
838
  }
781
839
  function elapsedSince(startedAt) {
782
- return Math.round(performance2.now() - startedAt);
840
+ return Math.round(performance3.now() - startedAt);
841
+ }
842
+
843
+ // src/trace-fetch.ts
844
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
845
+ var REQUEST_ID_HEADER = "x-request-id";
846
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
847
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
848
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
849
+ function secretSegment(segment) {
850
+ if (UUID.test(segment)) return false;
851
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
852
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
853
+ }
854
+ function outboundPath(pathname, mode) {
855
+ if (mode === "none") return pathname;
856
+ return pathname.split("/").map((segment) => {
857
+ let decoded = segment;
858
+ try {
859
+ decoded = decodeURIComponent(segment);
860
+ } catch {
861
+ decoded = segment;
862
+ }
863
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
864
+ }).join("/");
865
+ }
866
+ function failureOf(error) {
867
+ const message = describeError(error);
868
+ const cause = error instanceof Error ? error.cause : void 0;
869
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
870
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
871
+ }
872
+ function urlOf(input) {
873
+ try {
874
+ if (typeof input === "string") return new URL(input);
875
+ if (input instanceof URL) return input;
876
+ return new URL(input.url);
877
+ } catch {
878
+ return null;
879
+ }
880
+ }
881
+ function originOf(value) {
882
+ try {
883
+ return new URL(value).origin;
884
+ } catch {
885
+ return null;
886
+ }
887
+ }
888
+ function targets(url, target) {
889
+ if (target instanceof RegExp) return target.test(url.host);
890
+ const wanted = target.trim().toLowerCase();
891
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
892
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
893
+ }
894
+ function isPlainObject(value) {
895
+ if (value === null || typeof value !== "object") return false;
896
+ const prototype = Object.getPrototypeOf(value);
897
+ return prototype === Object.prototype || prototype === null;
898
+ }
899
+ function withRequestId(input, init, requestId) {
900
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
901
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
902
+ headers.set(REQUEST_ID_HEADER, requestId);
903
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
904
+ return [new Request(input, init), { headers }];
905
+ }
906
+ function traceFetch(config) {
907
+ const current = globalThis.fetch;
908
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
909
+ const original = current;
910
+ const radarOrigin = originOf(config.endpoint);
911
+ const traced = async (input, init) => {
912
+ const url = urlOf(input);
913
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
914
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
915
+ const context = currentContext();
916
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
917
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
918
+ const started = performance.now();
919
+ try {
920
+ const response = await original(nextInput, nextInit);
921
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
922
+ return response;
923
+ } catch (error) {
924
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
925
+ throw error;
926
+ }
927
+ };
928
+ Object.defineProperty(traced, WRAPPED, { value: original });
929
+ globalThis.fetch = traced;
930
+ return () => {
931
+ if (globalThis.fetch === traced) globalThis.fetch = original;
932
+ };
783
933
  }
784
934
 
785
935
  // src/transport.ts
936
+ import { promisify } from "util";
937
+ import { gzip } from "zlib";
786
938
  var gzipAsync = promisify(gzip);
787
939
  var MAX_BATCH_BYTES = 9e5;
788
940
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1010,7 +1162,7 @@ function withTimeout(promise, ms) {
1010
1162
 
1011
1163
  // src/version.ts
1012
1164
  var SDK_NAME = "@oconde/radar";
1013
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1165
+ var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
1014
1166
 
1015
1167
  // src/client.ts
1016
1168
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1022,6 +1174,7 @@ var RadarClient = class {
1022
1174
  initialized = false;
1023
1175
  closed = false;
1024
1176
  captured = /* @__PURE__ */ new WeakSet();
1177
+ restoreFetch = null;
1025
1178
  get isEnabled() {
1026
1179
  return this.transport !== null && !this.closed;
1027
1180
  }
@@ -1049,6 +1202,14 @@ var RadarClient = class {
1049
1202
  });
1050
1203
  }
1051
1204
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1205
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1206
+ this.restoreFetch = traceFetch({
1207
+ endpoint: this.options.endpoint,
1208
+ propagateTo: this.options.traceFetch.propagateTo,
1209
+ redact: this.options.redact,
1210
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1211
+ });
1212
+ }
1052
1213
  } catch (error) {
1053
1214
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1054
1215
  }
@@ -1092,7 +1253,7 @@ var RadarClient = class {
1092
1253
  }
1093
1254
  async track(name, fn, data) {
1094
1255
  const execute = async () => {
1095
- const started = performance3.now();
1256
+ const started = performance4.now();
1096
1257
  try {
1097
1258
  const result = await fn();
1098
1259
  this.info(name, { ...data, durationMs: elapsedSince(started), ok: true });
@@ -1104,10 +1265,10 @@ var RadarClient = class {
1104
1265
  }
1105
1266
  };
1106
1267
  if (currentContext()) return execute();
1107
- return runWithContext({ requestId: randomUUID2(), startedAt: performance3.now() }, execute);
1268
+ return runWithContext({ requestId: randomUUID2(), startedAt: performance4.now() }, execute);
1108
1269
  }
1109
1270
  withContext(fn, options = {}) {
1110
- return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance3.now() }, fn);
1271
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance4.now() }, fn);
1111
1272
  }
1112
1273
  async checkIn(slug, status, options = {}) {
1113
1274
  try {
@@ -1135,7 +1296,7 @@ var RadarClient = class {
1135
1296
  }
1136
1297
  async cron(slug, fn) {
1137
1298
  const checkInId = randomUUID2();
1138
- const started = performance3.now();
1299
+ const started = performance4.now();
1139
1300
  const execute = async () => {
1140
1301
  await this.checkIn(slug, "in_progress", { checkInId });
1141
1302
  try {
@@ -1185,6 +1346,8 @@ var RadarClient = class {
1185
1346
  async close(timeoutMs = 2e3) {
1186
1347
  if (this.closed) return;
1187
1348
  await this.flush(timeoutMs);
1349
+ this.restoreFetch?.();
1350
+ this.restoreFetch = null;
1188
1351
  this.closed = true;
1189
1352
  this.transport?.stop();
1190
1353
  }