@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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Correções
6
+
7
+ - `logRequests: { sample: 0 }` não registra nenhuma requisição. Antes, 0 era tratado como valor inválido e virava 1, guardando todas.
8
+
9
+ ## 0.4.0
10
+
11
+ ### Novo
12
+
13
+ - `logRequests` aceita filtros e amostragem: `{ include, exclude, sample }`. `include` e `exclude` recebem caminhos ou rotas (`'/dashboard'` vale para `/dashboard` e tudo abaixo dele) ou RegExp, testados no caminho e na rota do framework. `sample` (de 0 a 1) guarda só essa fração das requisições, sorteada de forma uniforme, e cada log guardado leva `sampleRate`, que o Radar usa para estimar os totais. `logRequests: true` e `false` continuam valendo.
14
+ - `traceFetch.ignore`: hosts (o nome exato ou `.dominio`) ou RegExp testadas em `host + caminho` que não viram log `http.client`. As chamadas continuam saindo normalmente.
15
+
16
+ ### Correções
17
+
18
+ - Uma RegExp com a flag `g` em `propagateTo` deixava de mandar o `x-request-id` em chamadas alternadas.
19
+
20
+ ## 0.3.0
21
+
22
+ ### Novo
23
+
24
+ - `traceFetch`: cada `fetch` de saída vira um log `http.client` com método, host, caminho (sem a query, com trechos que parecem segredo mascarados), status e duração, ou o erro com a causa (`fetch failed (ECONNREFUSED)`). Para os hosts de `propagateTo`, o SDK manda o `x-request-id` da requisição atual, sem trocar um que o app já tenha posto. Chamadas para o próprio Radar ficam de fora. Desligado por padrão.
25
+
26
+ ### Correções
27
+
28
+ - O nome da função inferido do código original não se confunde mais com chaves e com a palavra `function` dentro de comentários de várias linhas (como os de JSDoc).
29
+
3
30
  ## 0.2.0
4
31
 
5
32
  ### Novo
package/README.md CHANGED
@@ -98,6 +98,27 @@ await radar.cron('nightly-report', () => buildReport());
98
98
 
99
99
  `radar.cron` avisa o Radar no início, no fim e na falha, e o Radar alerta se a tarefa atrasar ou falhar. Em scripts sem o SDK, o check-in é um `POST` (veja o [PROTOCOL.md](PROTOCOL.md)). Para jobs e workers, `radar.withContext(fn)` dá a cada execução o próprio `requestId`. Detalhes em [Tarefas agendadas](https://radar.oconde.dev/docs/sdk/cron/).
100
100
 
101
+ ## Chamadas para outros serviços
102
+
103
+ ```ts
104
+ radar.init({ key: process.env.RADAR_KEY, traceFetch: { propagateTo: ['api.spinlab.dev', '.oconde.dev'] } });
105
+ ```
106
+
107
+ Com `traceFetch`, cada `fetch` que o app faz vira um log `http.client` com método, host, caminho (sem a query, e com trechos que parecem token mascarados), status e duração, ou o erro quando a chamada nem chega a responder. Dentro de uma requisição, o log entra na linha do tempo dela. Para os hosts de `propagateTo` (o nome exato, `.dominio` para os subdomínios ou uma RegExp), o SDK manda o `x-request-id` da requisição atual, e o Radar mostra a mesma requisição nos dois projetos. `traceFetch: true` registra as chamadas sem mandar o id para ninguém. Chamadas para o próprio Radar ficam de fora. Para não registrar chamadas barulhentas, como polling, use `ignore` com hosts ou RegExp testadas em `host + caminho`: `traceFetch: { ignore: ['api.telegram.org', /\/getUpdates$/] }`.
108
+
109
+ ## Quais requisições viram log
110
+
111
+ Por padrão, cada requisição vira um log `http.request`, que alimenta o Desempenho e os limites do Radar. Para guardar só uma parte:
112
+
113
+ ```ts
114
+ radar.init({
115
+ key: process.env.RADAR_KEY,
116
+ logRequests: { include: ['/dashboard', /^\/api\//], exclude: ['/dashboard/ping'], sample: 0.5 },
117
+ });
118
+ ```
119
+
120
+ `include` e `exclude` recebem caminhos ou rotas (`'/dashboard'` vale para `/dashboard` e tudo abaixo dele) ou RegExp. `sample` guarda só essa fração, sorteada de forma uniforme, e o Radar compensa a amostragem nos totais. `logRequests: false` desliga.
121
+
101
122
  ## Erros com o seu código
102
123
 
103
124
  Ligue no `tsconfig` para os erros mostrarem as linhas do TypeScript:
@@ -35,6 +35,28 @@ type RadarMiddleware = (req: RequestLike, res: ResponseLike, next: (error?: unkn
35
35
 
36
36
  type RedactMode = 'mask' | 'none';
37
37
  type RequestDetail = 'full' | 'route';
38
+ type FetchTraceTarget = string | RegExp;
39
+ type TraceFetchOption = boolean | {
40
+ propagateTo?: FetchTraceTarget[];
41
+ ignore?: FetchTraceTarget[];
42
+ };
43
+ type ResolvedTraceFetch = {
44
+ enabled: boolean;
45
+ propagateTo: FetchTraceTarget[];
46
+ ignore: FetchTraceTarget[];
47
+ };
48
+ type RequestPattern = string | RegExp;
49
+ type LogRequestsOption = boolean | {
50
+ include?: RequestPattern[];
51
+ exclude?: RequestPattern[];
52
+ sample?: number;
53
+ };
54
+ type ResolvedLogRequests = {
55
+ enabled: boolean;
56
+ include: RequestPattern[];
57
+ exclude: RequestPattern[];
58
+ sample: number;
59
+ };
38
60
  type RadarOptions = {
39
61
  key?: string;
40
62
  endpoint?: string;
@@ -45,9 +67,10 @@ type RadarOptions = {
45
67
  minLevel?: LogLevel;
46
68
  redact?: RedactMode;
47
69
  requestDetail?: RequestDetail;
48
- logRequests?: boolean;
70
+ logRequests?: LogRequestsOption;
49
71
  ignorePaths?: string[];
50
72
  captureUnhandled?: boolean;
73
+ traceFetch?: TraceFetchOption;
51
74
  debug?: boolean;
52
75
  };
53
76
  type ResolvedOptions = {
@@ -60,9 +83,10 @@ type ResolvedOptions = {
60
83
  minLevel: LogLevel;
61
84
  redact: RedactMode;
62
85
  requestDetail: RequestDetail;
63
- logRequests: boolean;
86
+ logRequests: ResolvedLogRequests;
64
87
  ignorePaths: string[];
65
88
  captureUnhandled: boolean;
89
+ traceFetch: ResolvedTraceFetch;
66
90
  debug: boolean;
67
91
  };
68
92
 
@@ -95,6 +119,7 @@ declare class RadarClient {
95
119
  private initialized;
96
120
  private closed;
97
121
  private readonly captured;
122
+ private restoreFetch;
98
123
  get isEnabled(): boolean;
99
124
  get settings(): Readonly<ResolvedOptions>;
100
125
  init(options?: RadarOptions): void;
@@ -35,6 +35,28 @@ type RadarMiddleware = (req: RequestLike, res: ResponseLike, next: (error?: unkn
35
35
 
36
36
  type RedactMode = 'mask' | 'none';
37
37
  type RequestDetail = 'full' | 'route';
38
+ type FetchTraceTarget = string | RegExp;
39
+ type TraceFetchOption = boolean | {
40
+ propagateTo?: FetchTraceTarget[];
41
+ ignore?: FetchTraceTarget[];
42
+ };
43
+ type ResolvedTraceFetch = {
44
+ enabled: boolean;
45
+ propagateTo: FetchTraceTarget[];
46
+ ignore: FetchTraceTarget[];
47
+ };
48
+ type RequestPattern = string | RegExp;
49
+ type LogRequestsOption = boolean | {
50
+ include?: RequestPattern[];
51
+ exclude?: RequestPattern[];
52
+ sample?: number;
53
+ };
54
+ type ResolvedLogRequests = {
55
+ enabled: boolean;
56
+ include: RequestPattern[];
57
+ exclude: RequestPattern[];
58
+ sample: number;
59
+ };
38
60
  type RadarOptions = {
39
61
  key?: string;
40
62
  endpoint?: string;
@@ -45,9 +67,10 @@ type RadarOptions = {
45
67
  minLevel?: LogLevel;
46
68
  redact?: RedactMode;
47
69
  requestDetail?: RequestDetail;
48
- logRequests?: boolean;
70
+ logRequests?: LogRequestsOption;
49
71
  ignorePaths?: string[];
50
72
  captureUnhandled?: boolean;
73
+ traceFetch?: TraceFetchOption;
51
74
  debug?: boolean;
52
75
  };
53
76
  type ResolvedOptions = {
@@ -60,9 +83,10 @@ type ResolvedOptions = {
60
83
  minLevel: LogLevel;
61
84
  redact: RedactMode;
62
85
  requestDetail: RequestDetail;
63
- logRequests: boolean;
86
+ logRequests: ResolvedLogRequests;
64
87
  ignorePaths: string[];
65
88
  captureUnhandled: boolean;
89
+ traceFetch: ResolvedTraceFetch;
66
90
  debug: boolean;
67
91
  };
68
92
 
@@ -95,6 +119,7 @@ declare class RadarClient {
95
119
  private initialized;
96
120
  private closed;
97
121
  private readonly captured;
122
+ private restoreFetch;
98
123
  get isEnabled(): boolean;
99
124
  get settings(): Readonly<ResolvedOptions>;
100
125
  init(options?: RadarOptions): void;
package/dist/index.cjs CHANGED
@@ -280,9 +280,18 @@ function requestPath(req) {
280
280
  const end = url.indexOf("?");
281
281
  return end >= 0 ? url.slice(0, end) : url;
282
282
  }
283
+ function matchesRequest(pattern, path, route) {
284
+ const candidates = route === void 0 ? [path] : [path, route];
285
+ if (pattern instanceof RegExp) return candidates.some((value) => value.search(pattern) !== -1);
286
+ const prefix = pattern.endsWith("/") ? pattern : `${pattern}/`;
287
+ return candidates.some((value) => value === pattern || value.startsWith(prefix));
288
+ }
283
289
  function requestRoute(req, route) {
284
290
  if (route) return route;
285
- return typeof req.route?.path === "string" ? req.route.path : void 0;
291
+ if (typeof req.route?.path !== "string") return void 0;
292
+ const base = typeof req.baseUrl === "string" ? req.baseUrl.replace(/\/+$/, "") : "";
293
+ if (!base) return req.route.path;
294
+ return req.route.path === "/" ? base : `${base}${req.route.path}`;
286
295
  }
287
296
  var ROUTE_PARAM = /^:([A-Za-z_$][\w$]*)/;
288
297
  function pathSegments(value) {
@@ -416,6 +425,25 @@ function createMiddleware(logger) {
416
425
  // src/options.ts
417
426
  var DEFAULT_ENDPOINT = "https://radar-ingest.oconde.dev";
418
427
  var LEVELS = ["debug", "info", "warn", "error"];
428
+ function patterns(value) {
429
+ if (!Array.isArray(value)) return [];
430
+ return value.flatMap((item) => {
431
+ if (item instanceof RegExp) return [item];
432
+ if (typeof item === "string" && item.trim() !== "") return [item.trim()];
433
+ return [];
434
+ });
435
+ }
436
+ function resolveTraceFetch(value) {
437
+ if (!value) return { enabled: false, propagateTo: [], ignore: [] };
438
+ if (value === true) return { enabled: true, propagateTo: [], ignore: [] };
439
+ return { enabled: true, propagateTo: patterns(value.propagateTo), ignore: patterns(value.ignore) };
440
+ }
441
+ function resolveLogRequests(value) {
442
+ if (value === false) return { enabled: false, include: [], exclude: [], sample: 1 };
443
+ if (value === void 0 || value === true || typeof value !== "object" || value === null) return { enabled: true, include: [], exclude: [], sample: 1 };
444
+ const sample = typeof value.sample === "number" && Number.isFinite(value.sample) && value.sample >= 0 && value.sample <= 1 ? value.sample : 1;
445
+ return { enabled: true, include: patterns(value.include), exclude: patterns(value.exclude), sample };
446
+ }
419
447
  function resolveOptions(options = {}, env = process.env) {
420
448
  const key = options.key?.trim() || void 0;
421
449
  return {
@@ -428,9 +456,10 @@ function resolveOptions(options = {}, env = process.env) {
428
456
  minLevel: options.minLevel && LEVELS.includes(options.minLevel) ? options.minLevel : "info",
429
457
  redact: options.redact === "none" ? "none" : "mask",
430
458
  requestDetail: options.requestDetail === "route" ? "route" : "full",
431
- logRequests: options.logRequests ?? true,
459
+ logRequests: resolveLogRequests(options.logRequests),
432
460
  ignorePaths: options.ignorePaths ?? ["/healthz", "/health"],
433
461
  captureUnhandled: options.captureUnhandled ?? true,
462
+ traceFetch: resolveTraceFetch(options.traceFetch),
434
463
  debug: options.debug ?? false
435
464
  };
436
465
  }
@@ -498,6 +527,7 @@ var IDENTIFIER = "[A-Za-z_$][\\w$]*";
498
527
  var MAX_LINES_UP = 400;
499
528
  var MAX_HEADER_LINES = 6;
500
529
  var MAX_NAME_LENGTH = 100;
530
+ var MAX_COMMENT_LINES = 40;
501
531
  var CONTROL_BLOCK = /^\s*(?:\}\s*)?(?:if|else|for|while|do|switch|try|catch|finally|with)\b/;
502
532
  var CLASS_BODY = new RegExp(`\\bclass\\s+${IDENTIFIER}`);
503
533
  var MARKUP_BOUNDARY = /<\/?(?:script|style|template)\b/i;
@@ -510,8 +540,58 @@ var NAME_PATTERNS = [
510
540
  ];
511
541
  var METHOD_PATTERN = new RegExp(`^\\s*(?:(?:export|default|public|private|protected|static|async|override|readonly|get|set)\\s+)*\\*?\\s*(${IDENTIFIER})\\s*(?:<[^>]*>)?\\s*\\([^]*\\)\\s*(?::\\s*[^{]+)?$`);
512
542
  var ANONYMOUS_FUNCTION = /\bfunction\b|=>/;
513
- function codeOnly(text) {
514
- return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""').replace(/\/\*.*?\*\//g, "").replace(/(^|[^:])\/\/.*$/, "$1");
543
+ var STRING_LITERAL = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
544
+ function lineCommentAt(text, from) {
545
+ for (let index = text.indexOf("//", from); index >= 0; index = text.indexOf("//", index + 1)) {
546
+ if (index === 0 || text[index - 1] !== ":") return index;
547
+ }
548
+ return -1;
549
+ }
550
+ function blockCommentAt(text, from) {
551
+ for (let index = text.indexOf("/*", from); index >= 0; index = text.indexOf("/*", index + 1)) {
552
+ if (index === 0 || text[index - 1] !== "\\") return index;
553
+ }
554
+ return -1;
555
+ }
556
+ function stripComments(text, insideComment, closesLater) {
557
+ let code = "";
558
+ let position = 0;
559
+ let inside = insideComment;
560
+ while (position < text.length) {
561
+ if (inside) {
562
+ const end = text.indexOf("*/", position);
563
+ if (end < 0) return { code, insideComment: true };
564
+ inside = false;
565
+ position = end + 2;
566
+ continue;
567
+ }
568
+ const block = blockCommentAt(text, position);
569
+ const line = lineCommentAt(text, position);
570
+ if (line >= 0 && (block < 0 || line < block)) return { code: code + text.slice(position, line), insideComment: false };
571
+ if (block < 0) return { code: code + text.slice(position), insideComment: false };
572
+ if (text.indexOf("*/", block + 2) < 0 && !closesLater()) {
573
+ code += text.slice(position, block + 2);
574
+ position = block + 2;
575
+ continue;
576
+ }
577
+ code += text.slice(position, block);
578
+ inside = true;
579
+ position = block + 2;
580
+ }
581
+ return { code, insideComment: inside };
582
+ }
583
+ function codeLines(lines, from, start, column) {
584
+ const plain = lines.slice(from, start + 1).map((line) => line.replace(STRING_LITERAL, '""'));
585
+ const closesWithin = (index) => plain.slice(index + 1, index + 1 + MAX_COMMENT_LINES).some((line) => line.includes("*/"));
586
+ const result = [];
587
+ let insideComment = false;
588
+ for (let index = 0; index < plain.length; index++) {
589
+ const text = index === plain.length - 1 && column ? (lines[start] ?? "").slice(0, Math.max(0, column - 1)).replace(STRING_LITERAL, '""') : plain[index] ?? "";
590
+ const stripped = stripComments(text, insideComment, () => closesWithin(index));
591
+ insideComment = stripped.insideComment;
592
+ result.push(stripped.code);
593
+ }
594
+ return result;
515
595
  }
516
596
  function parenBalance(text) {
517
597
  let balance = 0;
@@ -521,17 +601,17 @@ function parenBalance(text) {
521
601
  }
522
602
  return balance;
523
603
  }
524
- function headerOf(lines, index, prefix) {
604
+ function headerOf(code, index, prefix) {
525
605
  let header = prefix;
526
606
  for (let previous = index - 1; previous >= 0 && previous >= index - MAX_HEADER_LINES; previous--) {
527
607
  if (header.trim() !== "" && parenBalance(header) >= 0) break;
528
- header = `${codeOnly(lines[previous] ?? "")} ${header}`;
608
+ header = `${code[previous] ?? ""} ${header}`;
529
609
  }
530
610
  return header;
531
611
  }
532
612
  function nameIn(header) {
533
- const patterns = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
534
- for (const pattern of patterns) {
613
+ const patterns2 = ANONYMOUS_FUNCTION.test(header) ? NAME_PATTERNS : [...NAME_PATTERNS, METHOD_PATTERN];
614
+ for (const pattern of patterns2) {
535
615
  const name = pattern.exec(header)?.[1];
536
616
  if (name && !KEYWORDS.has(name)) return name.slice(0, MAX_NAME_LENGTH);
537
617
  }
@@ -540,11 +620,12 @@ function nameIn(header) {
540
620
  function enclosingFunctionName(lines, line, column) {
541
621
  const start = line - 1;
542
622
  if (start < 0 || start >= lines.length) return void 0;
623
+ const from = Math.max(0, start - MAX_LINES_UP - MAX_HEADER_LINES);
624
+ const cleaned = codeLines(lines, from, start, column);
543
625
  let depth = 0;
544
626
  for (let index = start; index >= 0 && index >= start - MAX_LINES_UP; index--) {
545
- const raw = lines[index] ?? "";
546
- if (index < start && MARKUP_BOUNDARY.test(raw)) return void 0;
547
- const code = codeOnly(index === start && column ? raw.slice(0, Math.max(0, column - 1)) : raw);
627
+ if (index < start && MARKUP_BOUNDARY.test(lines[index] ?? "")) return void 0;
628
+ const code = cleaned[index - from] ?? "";
548
629
  for (let position = code.length - 1; position >= 0; position--) {
549
630
  const char = code[position];
550
631
  if (char === "}") {
@@ -554,7 +635,7 @@ function enclosingFunctionName(lines, line, column) {
554
635
  depth -= 1;
555
636
  continue;
556
637
  }
557
- const header = headerOf(lines, index, code.slice(0, position));
638
+ const header = headerOf(cleaned, index - from, code.slice(0, position));
558
639
  if (CLASS_BODY.test(header)) return void 0;
559
640
  if (CONTROL_BLOCK.test(header)) continue;
560
641
  const name = nameIn(header);
@@ -796,10 +877,6 @@ function safeStringify(value) {
796
877
  }
797
878
  }
798
879
 
799
- // src/transport.ts
800
- var import_node_util2 = require("util");
801
- var import_node_zlib = require("zlib");
802
-
803
880
  // src/util.ts
804
881
  var import_node_perf_hooks2 = require("perf_hooks");
805
882
  function describeError(error) {
@@ -809,7 +886,109 @@ function elapsedSince(startedAt) {
809
886
  return Math.round(import_node_perf_hooks2.performance.now() - startedAt);
810
887
  }
811
888
 
889
+ // src/trace-fetch.ts
890
+ var WRAPPED = /* @__PURE__ */ Symbol.for("@oconde/radar.fetch");
891
+ var REQUEST_ID_HEADER = "x-request-id";
892
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
893
+ var TELEGRAM_BOT = /^bot\d{5,}:[\w-]{20,}$/;
894
+ var TOKEN_LIKE = /^[\w\-.~+=]{20,}$/;
895
+ function secretSegment(segment) {
896
+ if (UUID.test(segment)) return false;
897
+ if (TELEGRAM_BOT.test(segment) || looksLikeCredential(segment)) return true;
898
+ return TOKEN_LIKE.test(segment) && /[A-Za-z]/.test(segment) && /\d/.test(segment);
899
+ }
900
+ function outboundPath(pathname, mode) {
901
+ if (mode === "none") return pathname;
902
+ return pathname.split("/").map((segment) => {
903
+ let decoded = segment;
904
+ try {
905
+ decoded = decodeURIComponent(segment);
906
+ } catch {
907
+ decoded = segment;
908
+ }
909
+ return secretSegment(decoded) ? maskValue(decoded, false) : segment;
910
+ }).join("/");
911
+ }
912
+ function failureOf(error) {
913
+ const message = describeError(error);
914
+ const cause = error instanceof Error ? error.cause : void 0;
915
+ const detail = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : "";
916
+ return detail && !message.includes(detail) ? `${message} (${detail})` : message;
917
+ }
918
+ function urlOf(input) {
919
+ try {
920
+ if (typeof input === "string") return new URL(input);
921
+ if (input instanceof URL) return input;
922
+ return new URL(input.url);
923
+ } catch {
924
+ return null;
925
+ }
926
+ }
927
+ function originOf(value) {
928
+ try {
929
+ return new URL(value).origin;
930
+ } catch {
931
+ return null;
932
+ }
933
+ }
934
+ function hostMatches(url, host) {
935
+ const wanted = host.trim().toLowerCase();
936
+ if (wanted.startsWith(".")) return url.hostname.toLowerCase().endsWith(wanted);
937
+ return url.host.toLowerCase() === wanted || url.hostname.toLowerCase() === wanted;
938
+ }
939
+ function targets(url, target) {
940
+ if (target instanceof RegExp) return url.host.search(target) !== -1;
941
+ return hostMatches(url, target);
942
+ }
943
+ function ignored(url, target) {
944
+ if (target instanceof RegExp) return `${url.host}${url.pathname}`.search(target) !== -1;
945
+ return hostMatches(url, target);
946
+ }
947
+ function isPlainObject(value) {
948
+ if (value === null || typeof value !== "object") return false;
949
+ const prototype = Object.getPrototypeOf(value);
950
+ return prototype === Object.prototype || prototype === null;
951
+ }
952
+ function withRequestId(input, init, requestId) {
953
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
954
+ if (headers.has(REQUEST_ID_HEADER)) return [input, init];
955
+ headers.set(REQUEST_ID_HEADER, requestId);
956
+ if (init === void 0 || isPlainObject(init)) return [input, { ...init, headers }];
957
+ return [new Request(input, init), { headers }];
958
+ }
959
+ function traceFetch(config) {
960
+ const current = globalThis.fetch;
961
+ if (typeof current !== "function" || current[WRAPPED]) return () => void 0;
962
+ const original = current;
963
+ const radarOrigin = originOf(config.endpoint);
964
+ const traced = async (input, init) => {
965
+ const url = urlOf(input);
966
+ if (!url || url.origin === radarOrigin || url.protocol !== "http:" && url.protocol !== "https:") return original(input, init);
967
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
968
+ const context = currentContext();
969
+ const [nextInput, nextInit] = context && config.propagateTo.some((target) => targets(url, target)) ? withRequestId(input, init, context.requestId) : [input, init];
970
+ if (config.ignore?.some((target) => ignored(url, target))) return original(nextInput, nextInit);
971
+ const attrs = { method, host: url.host, path: outboundPath(url.pathname, config.redact) };
972
+ const started = performance.now();
973
+ try {
974
+ const response = await original(nextInput, nextInit);
975
+ config.record(response.status >= 500 ? "error" : response.status >= 400 ? "warn" : "info", { ...attrs, status: response.status, durationMs: elapsedSince(started) });
976
+ return response;
977
+ } catch (error) {
978
+ config.record("error", { ...attrs, error: failureOf(error), durationMs: elapsedSince(started) });
979
+ throw error;
980
+ }
981
+ };
982
+ Object.defineProperty(traced, WRAPPED, { value: original });
983
+ globalThis.fetch = traced;
984
+ return () => {
985
+ if (globalThis.fetch === traced) globalThis.fetch = original;
986
+ };
987
+ }
988
+
812
989
  // src/transport.ts
990
+ var import_node_util2 = require("util");
991
+ var import_node_zlib = require("zlib");
813
992
  var gzipAsync = (0, import_node_util2.promisify)(import_node_zlib.gzip);
814
993
  var MAX_BATCH_BYTES = 9e5;
815
994
  var GZIP_THRESHOLD_BYTES = 8192;
@@ -1037,7 +1216,7 @@ function withTimeout(promise, ms) {
1037
1216
 
1038
1217
  // src/version.ts
1039
1218
  var SDK_NAME = "@oconde/radar";
1040
- var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
1219
+ var SDK_VERSION = true ? "0.4.1" : "0.0.0-dev";
1041
1220
 
1042
1221
  // src/client.ts
1043
1222
  var MAX_CHECK_IN_DURATION_MS = 7 * 864e5;
@@ -1049,6 +1228,7 @@ var RadarClient = class {
1049
1228
  initialized = false;
1050
1229
  closed = false;
1051
1230
  captured = /* @__PURE__ */ new WeakSet();
1231
+ restoreFetch = null;
1052
1232
  get isEnabled() {
1053
1233
  return this.transport !== null && !this.closed;
1054
1234
  }
@@ -1076,6 +1256,15 @@ var RadarClient = class {
1076
1256
  });
1077
1257
  }
1078
1258
  if (this.options.captureUnhandled && (this.transport || this.options.console)) installProcessHandlers(this);
1259
+ if (this.options.traceFetch.enabled && (this.transport || this.options.console)) {
1260
+ this.restoreFetch = traceFetch({
1261
+ endpoint: this.options.endpoint,
1262
+ propagateTo: this.options.traceFetch.propagateTo,
1263
+ ignore: this.options.traceFetch.ignore,
1264
+ redact: this.options.redact,
1265
+ record: (level, attrs) => this.safely(() => this.emitLog(level, "http.client", attrs))
1266
+ });
1267
+ }
1079
1268
  } catch (error) {
1080
1269
  this.debugWarn(`radar.init falhou: ${describeError(error)}`);
1081
1270
  }
@@ -1182,22 +1371,28 @@ var RadarClient = class {
1182
1371
  return createMiddleware(this);
1183
1372
  }
1184
1373
  shouldLogRequest(req) {
1185
- return this.options.logRequests && !this.options.ignorePaths.includes(requestPath(req));
1374
+ return this.options.logRequests.enabled && !this.options.ignorePaths.includes(requestPath(req));
1186
1375
  }
1187
1376
  logHttpRequest(context, status) {
1188
1377
  this.safely(() => {
1189
1378
  const req = context.req;
1190
1379
  if (!req) return;
1191
- const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1192
1380
  const route = requestRoute(req, context.route);
1381
+ const path = requestPath(req);
1382
+ const { include, exclude, sample } = this.options.logRequests;
1383
+ if (include.length > 0 && !include.some((pattern) => matchesRequest(pattern, path, route))) return;
1384
+ if (exclude.some((pattern) => matchesRequest(pattern, path, route))) return;
1385
+ if (sample < 1 && Math.random() >= sample) return;
1386
+ const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
1193
1387
  runWithContext(
1194
1388
  context,
1195
1389
  () => this.emitLog(level, "http.request", {
1196
1390
  method: (req.method ?? "GET").toUpperCase(),
1197
1391
  ...route ? { route } : {},
1198
- ...this.options.requestDetail === "full" ? { path: redactPath(requestPath(req), pathParams(req, requestPath(req)), this.options.redact) } : {},
1392
+ ...this.options.requestDetail === "full" ? { path: redactPath(path, pathParams(req, path), this.options.redact) } : {},
1199
1393
  status,
1200
- durationMs: elapsedSince(context.startedAt)
1394
+ durationMs: elapsedSince(context.startedAt),
1395
+ ...sample < 1 ? { sampleRate: sample } : {}
1201
1396
  })
1202
1397
  );
1203
1398
  });
@@ -1212,6 +1407,8 @@ var RadarClient = class {
1212
1407
  async close(timeoutMs = 2e3) {
1213
1408
  if (this.closed) return;
1214
1409
  await this.flush(timeoutMs);
1410
+ this.restoreFetch?.();
1411
+ this.restoreFetch = null;
1215
1412
  this.closed = true;
1216
1413
  this.transport?.stop();
1217
1414
  }