@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.
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";
@@ -253,9 +253,18 @@ function requestPath(req) {
253
253
  const end = url.indexOf("?");
254
254
  return end >= 0 ? url.slice(0, end) : url;
255
255
  }
256
+ function matchesRequest(pattern, path, route) {
257
+ const candidates = route === void 0 ? [path] : [path, route];
258
+ if (pattern instanceof RegExp) return candidates.some((value) => value.search(pattern) !== -1);
259
+ const prefix = pattern.endsWith("/") ? pattern : `${pattern}/`;
260
+ return candidates.some((value) => value === pattern || value.startsWith(prefix));
261
+ }
256
262
  function requestRoute(req, route) {
257
263
  if (route) return route;
258
- return typeof req.route?.path === "string" ? req.route.path : void 0;
264
+ if (typeof req.route?.path !== "string") return void 0;
265
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
266
+ if (!base) return req.route.path;
267
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
259
268
  }
260
269
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
261
270
  function pathSegments(value) {
@@ -375,7 +384,7 @@ function createMiddleware(logger) {
375
384
  let context;
376
385
  try {
377
386
  const requestId = requestIdFrom(headerValue(req.headers["x-request-id"]));
378
- context = { requestId, startedAt: performance.now(), req };
387
+ context = { requestId, startedAt: performance2.now(), req };
379
388
  res.setHeader("x-request-id", requestId);
380
389
  if (logger.shouldLogRequest(req)) res.once("finish", () => logger.logHttpRequest(context, res.statusCode));
381
390
  } catch {
@@ -389,6 +398,25 @@ function createMiddleware(logger) {
389
398
  // src/options.ts
390
399
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
391
400
  var LEVELS = ["debug", "info", "warn", "error"];
401
+ function patterns(value) {
402
+ if (!Array.isArray(value)) return [];
403
+ return value.flatMap((item) => {
404
+ if (item instanceof RegExp) return [item];
405
+ if (typeof item === "string" && item.trim() !== "") return [item.trim()];
406
+ return [];
407
+ });
408
+ }
409
+ function resolveTraceFetch(value) {
410
+ if (!value) return { enabled: false, propagateTo: [], ignore: [] };
411
+ if (value === true) return { enabled: true, propagateTo: [], ignore: [] };
412
+ return { enabled: true, propagateTo: patterns(value.propagateTo), ignore: patterns(value.ignore) };
413
+ }
414
+ function resolveLogRequests(value) {
415
+ if (value === false) return { enabled: false, include: [], exclude: [], sample: 1 };
416
+ if (value === void 0 || value === true || typeof value !== "object" || value === null) return { enabled: true, include: [], exclude: [], sample: 1 };
417
+ const sample = typeof value.sample === "number" && Number.isFinite(value.sample) && value.sample >= 0 && value.sample <= 1 ? value.sample : 1;
418
+ return { enabled: true, include: patterns(value.include), exclude: patterns(value.exclude), sample };
419
+ }
392
420
  function resolveOptions(options = {}, env = process.env) {
393
421
  const key = options.key?.trim() || void 0;
394
422
  return {
@@ -401,9 +429,10 @@ function resolveOptions(options = {}, env = process.env) {
401
429
  minLevel: options.minLevel && LEVELS.includes(options.minLevel) ? options.minLevel : "info",
402
430
  redact: options.redact === "none" ? "none" : "mask",
403
431
  requestDetail: options.requestDetail === "route" ? "route" : "full",
404
- logRequests: options.logRequests ?? true,
432
+ logRequests: resolveLogRequests(options.logRequests),
405
433
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
406
434
  captureUnhandled: options.captureUnhandled ?? true,
435
+ traceFetch: resolveTraceFetch(options.traceFetch),
407
436
  debug: options.debug ?? false
408
437
  };
409
438
  }
@@ -471,6 +500,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
471
500
  var MAX_LINES_UP = 400;
472
501
  var MAX_HEADER_LINES = 6;
473
502
  var MAX_NAME_LENGTH = 100;
503
+ var MAX_COMMENT_LINES = 40;
474
504
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
475
505
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
476
506
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -483,8 +513,58 @@ var NAME_PATTERNS = [
483
513
  ];
484
514
  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
515
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
486
- function codeOnly(text) {
487
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
516
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
517
+ function lineCommentAt(text, from) {
518
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
519
+ if (index === 0 || text[index - 1] !== ":") return index;
520
+ }
521
+ return -1;
522
+ }
523
+ function blockCommentAt(text, from) {
524
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
525
+ if (index === 0 || text[index - 1] !== "\\") return index;
526
+ }
527
+ return -1;
528
+ }
529
+ function stripComments(text, insideComment, closesLater) {
530
+ let code = "";
531
+ let position = 0;
532
+ let inside = insideComment;
533
+ while (position < text.length) {
534
+ if (inside) {
535
+ const end = text.indexOf("*/", position);
536
+ if (end < 0) return { code, insideComment: true };
537
+ inside = false;
538
+ position = end + 2;
539
+ continue;
540
+ }
541
+ const block = blockCommentAt(text, position);
542
+ const line = lineCommentAt(text, position);
543
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
544
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
545
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
546
+ code += text.slice(position, block + 2);
547
+ position = block + 2;
548
+ continue;
549
+ }
550
+ code += text.slice(position, block);
551
+ inside = true;
552
+ position = block + 2;
553
+ }
554
+ return { code, insideComment: inside };
555
+ }
556
+ function codeLines(lines, from, start, column) {
557
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
558
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
559
+ const result = [];
560
+ let insideComment = false;
561
+ for (let index = 0; index < plain.length; index++) {
562
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
563
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
564
+ insideComment = stripped.insideComment;
565
+ result.push(stripped.code);
566
+ }
567
+ return result;
488
568
  }
489
569
  function parenBalance(text) {
490
570
  let balance = 0;
@@ -494,17 +574,17 @@ function parenBalance(text) {
494
574
  }
495
575
  return balance;
496
576
  }
497
- function headerOf(lines, index, prefix) {
577
+ function headerOf(code, index, prefix) {
498
578
  let header = prefix;
499
579
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
500
580
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
501
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
581
+ header = `${code[previous] ?? ""} ${header}`;
502
582
  }
503
583
  return header;
504
584
  }
505
585
  function nameIn(header) {
506
- const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
507
- for (const pattern of patterns) {
586
+ const patterns2 = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
587
+ for (const pattern of patterns2) {
508
588
  const name = pattern.exec(header)?.[1];
509
589
  if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
510
590
  }
@@ -513,11 +593,12 @@ function nameIn(header) {
513
593
  function enclosingFunctionName(lines, line, column) {
514
594
  const start = line - 1;
515
595
  if (start < 0 || start >= lines.length) return void 0;
596
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
597
+ const cleaned = codeLines(lines, from, start, column);
516
598
  let depth = 0;
517
599
  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);
600
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
601
+ const code = cleaned[index - from] ?? "";
521
602
  for (let position = code.length - 1; position >= 0; position--) {
522
603
  const char = code[position];
523
604
  if (char === "}") {
@@ -527,7 +608,7 @@ function enclosingFunctionName(lines, line, column) {
527
608
  depth -= 1;
528
609
  continue;
529
610
  }
530
- const header = headerOf(lines, index, code.slice(0, position));
611
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
531
612
  if (CLASS_BODY.test(header)) return void 0;
532
613
  if (CONTROL_BLOCK.test(header)) continue;
533
614
  const name = nameIn(header);
@@ -769,20 +850,118 @@ function safeStringify(value) {
769
850
  }
770
851
  }
771
852
 
772
- // src/transport.ts
773
- import { promisify } from "util";
774
- import { gzip } from "zlib";
775
-
776
853
  // src/util.ts
777
- import { performance as performance2 } from "perf_hooks";
854
+ import { performance as performance3 } from "perf_hooks";
778
855
  function describeError(error) {
779
856
  return error instanceof Error ? error.message : String(error);
780
857
  }
781
858
  function elapsedSince(startedAt) {
782
- return Math.round(performance2.now() - startedAt);
859
+ return Math.round(performance3.now() - startedAt);
860
+ }
861
+
862
+ // src/trace-fetch.ts
863
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
864
+ var REQUEST_ID_HEADER = "x-request-id";
865
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
866
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
867
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
868
+ function secretSegment(segment) {
869
+ if (UUID.test(segment)) return false;
870
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
871
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
872
+ }
873
+ function outboundPath(pathname, mode) {
874
+ if (mode === "none") return pathname;
875
+ return pathname.split("/").map((segment) => {
876
+ let decoded = segment;
877
+ try {
878
+ decoded = decodeURIComponent(segment);
879
+ } catch {
880
+ decoded = segment;
881
+ }
882
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
883
+ }).join("/");
884
+ }
885
+ function failureOf(error) {
886
+ const message = describeError(error);
887
+ const cause = error instanceof Error ? error.cause : void 0;
888
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
889
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
890
+ }
891
+ function urlOf(input) {
892
+ try {
893
+ if (typeof input === "string") return new URL(input);
894
+ if (input instanceof URL) return input;
895
+ return new URL(input.url);
896
+ } catch {
897
+ return null;
898
+ }
899
+ }
900
+ function originOf(value) {
901
+ try {
902
+ return new URL(value).origin;
903
+ } catch {
904
+ return null;
905
+ }
906
+ }
907
+ function hostMatches(url, host) {
908
+ const wanted = host.trim().toLowerCase();
909
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
910
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
911
+ }
912
+ function targets(url, target) {
913
+ if (target instanceof RegExp) return url.host.search(target) !== -1;
914
+ return hostMatches(url, target);
915
+ }
916
+ function ignored(url, target) {
917
+ if (target instanceof RegExp) return `${url.host}${url.pathname}`.search(target) !== -1;
918
+ return hostMatches(url, target);
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
+ if (config.ignore?.some((target) => ignored(url, target))) return original(nextInput, nextInit);
944
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
945
+ const started = performance.now();
946
+ try {
947
+ const response = await original(nextInput, nextInit);
948
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
949
+ return response;
950
+ } catch (error) {
951
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
952
+ throw error;
953
+ }
954
+ };
955
+ Object.defineProperty(traced, WRAPPED, { value: original });
956
+ globalThis.fetch = traced;
957
+ return () => {
958
+ if (globalThis.fetch === traced) globalThis.fetch = original;
959
+ };
783
960
  }
784
961
 
785
962
  // src/transport.ts
963
+ import { promisify } from "util";
964
+ import { gzip } from "zlib";
786
965
  var gzipAsync = promisify(gzip);
787
966
  var MAX_BATCH_BYTES = 9e5;
788
967
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1010,7 +1189,7 @@ function withTimeout(promise, ms) {
1010
1189
 
1011
1190
  // src/version.ts
1012
1191
  var SDK_NAME = "@oconde/radar";
1013
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1192
+ var SDK_VERSION = true ? "0.4.1" : "0.0.0-dev";
1014
1193
 
1015
1194
  // src/client.ts
1016
1195
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1022,6 +1201,7 @@ var RadarClient = class {
1022
1201
  initialized = false;
1023
1202
  closed = false;
1024
1203
  captured = /* @__PURE__ */ new WeakSet();
1204
+ restoreFetch = null;
1025
1205
  get isEnabled() {
1026
1206
  return this.transport !== null && !this.closed;
1027
1207
  }
@@ -1049,6 +1229,15 @@ var RadarClient = class {
1049
1229
  });
1050
1230
  }
1051
1231
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1232
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1233
+ this.restoreFetch = traceFetch({
1234
+ endpoint: this.options.endpoint,
1235
+ propagateTo: this.options.traceFetch.propagateTo,
1236
+ ignore: this.options.traceFetch.ignore,
1237
+ redact: this.options.redact,
1238
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1239
+ });
1240
+ }
1052
1241
  } catch (error) {
1053
1242
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1054
1243
  }
@@ -1092,7 +1281,7 @@ var RadarClient = class {
1092
1281
  }
1093
1282
  async track(name, fn, data) {
1094
1283
  const execute = async () => {
1095
- const started = performance3.now();
1284
+ const started = performance4.now();
1096
1285
  try {
1097
1286
  const result = await fn();
1098
1287
  this.info(name, { ...data, durationMs: elapsedSince(started), ok: true });
@@ -1104,10 +1293,10 @@ var RadarClient = class {
1104
1293
  }
1105
1294
  };
1106
1295
  if (currentContext()) return execute();
1107
- return runWithContext({ requestId: randomUUID2(), startedAt: performance3.now() }, execute);
1296
+ return runWithContext({ requestId: randomUUID2(), startedAt: performance4.now() }, execute);
1108
1297
  }
1109
1298
  withContext(fn, options = {}) {
1110
- return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance3.now() }, fn);
1299
+ return runWithContext({ requestId: requestIdFrom(options.requestId), startedAt: performance4.now() }, fn);
1111
1300
  }
1112
1301
  async checkIn(slug, status, options = {}) {
1113
1302
  try {
@@ -1135,7 +1324,7 @@ var RadarClient = class {
1135
1324
  }
1136
1325
  async cron(slug, fn) {
1137
1326
  const checkInId = randomUUID2();
1138
- const started = performance3.now();
1327
+ const started = performance4.now();
1139
1328
  const execute = async () => {
1140
1329
  await this.checkIn(slug, "in_progress", { checkInId });
1141
1330
  try {
@@ -1155,22 +1344,28 @@ var RadarClient = class {
1155
1344
  return createMiddleware(this);
1156
1345
  }
1157
1346
  shouldLogRequest(req) {
1158
- return this.options.logRequests && !this.options.ignorePaths.includes(requestPath(req));
1347
+ return this.options.logRequests.enabled && !this.options.ignorePaths.includes(requestPath(req));
1159
1348
  }
1160
1349
  logHttpRequest(context, status) {
1161
1350
  this.safely(() => {
1162
1351
  const req = context.req;
1163
1352
  if (!req) return;
1164
- const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1165
1353
  const route = requestRoute(req, context.route);
1354
+ const path = requestPath(req);
1355
+ const { include, exclude, sample } = this.options.logRequests;
1356
+ if (include.length > 0 && !include.some((pattern) => matchesRequest(pattern, path, route))) return;
1357
+ if (exclude.some((pattern) => matchesRequest(pattern, path, route))) return;
1358
+ if (sample < 1 && Math.random() >= sample) return;
1359
+ const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1166
1360
  runWithContext(
1167
1361
  context,
1168
1362
  () => this.emitLog(level, "http.request", {
1169
1363
  method: (req.method ?? "GET").toUpperCase(),
1170
1364
  ...route ? { route } : {},
1171
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
1365
+ ...this.options.requestDetail === "full" ? { path: redactPath(path, pathParams(req, path), this.options.redact) } : {},
1172
1366
  status,
1173
- durationMs: elapsedSince(context.startedAt)
1367
+ durationMs: elapsedSince(context.startedAt),
1368
+ ...sample < 1 ? { sampleRate: sample } : {}
1174
1369
  })
1175
1370
  );
1176
1371
  });
@@ -1185,6 +1380,8 @@ var RadarClient = class {
1185
1380
  async close(timeoutMs = 2e3) {
1186
1381
  if (this.closed) return;
1187
1382
  await this.flush(timeoutMs);
1383
+ this.restoreFetch?.();
1384
+ this.restoreFetch = null;
1188
1385
  this.closed = true;
1189
1386
  this.transport?.stop();
1190
1387
  }