@oconde/radar 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fernando Conde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PROTOCOL.md ADDED
@@ -0,0 +1,149 @@
1
+ # Protocolo Radar v1
2
+
3
+ Este documento descreve o envio de eventos ao Radar para quem escreve um SDK em outra linguagem. A fonte da verdade dos tipos é `src/protocol/index.ts`.
4
+
5
+ ## Envio
6
+
7
+ ```
8
+ POST https://radar-ingest.oconde.dev/api/v1/events
9
+ Authorization: Bearer rk_<40 caracteres>
10
+ Content-Type: application/json
11
+ Content-Encoding: gzip (opcional)
12
+ ```
13
+
14
+ Limites: corpo de até 1 MB depois de descompactado, até 500 eventos por lote.
15
+
16
+ ## Tipos
17
+
18
+ ```ts
19
+ export const PROTOCOL_VERSION = 1;
20
+
21
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
22
+
23
+ export type EventBatch = {
24
+ v: 1;
25
+ sdk: { name: string; version: string };
26
+ environment?: string;
27
+ release?: string;
28
+ dropped?: number;
29
+ events: RadarEvent[];
30
+ };
31
+
32
+ export type RadarEvent = LogEvent | ErrorEvent;
33
+
34
+ export type LogEvent = {
35
+ type: 'log';
36
+ ts: number;
37
+ level: LogLevel;
38
+ message: string;
39
+ requestId?: string;
40
+ attrs?: Record<string, unknown>;
41
+ request?: RequestInfo;
42
+ user?: UserInfo;
43
+ };
44
+
45
+ export type ErrorEvent = {
46
+ type: 'error';
47
+ ts: number;
48
+ level: 'error' | 'fatal';
49
+ handled: boolean;
50
+ requestId?: string;
51
+ exception: ExceptionInfo;
52
+ request?: RequestInfo;
53
+ user?: UserInfo;
54
+ runtime?: RuntimeInfo;
55
+ attrs?: Record<string, unknown>;
56
+ };
57
+
58
+ export type ExceptionInfo = {
59
+ type: string;
60
+ message: string;
61
+ frames: StackFrame[];
62
+ cause?: ExceptionInfo;
63
+ };
64
+
65
+ export type StackFrame = {
66
+ fn?: string;
67
+ file: string;
68
+ line?: number;
69
+ col?: number;
70
+ inApp: boolean;
71
+ context?: { pre: string[]; line: string; post: string[] };
72
+ };
73
+
74
+ export type RequestInfo = {
75
+ method: string;
76
+ url: string;
77
+ route?: string;
78
+ params?: Record<string, string>;
79
+ query?: Record<string, unknown>;
80
+ headers?: Record<string, string>;
81
+ body?: unknown;
82
+ ip?: string;
83
+ userAgent?: string;
84
+ status?: number;
85
+ durationMs?: number;
86
+ };
87
+
88
+ export type UserInfo = { id?: string; email?: string; name?: string };
89
+
90
+ export type RuntimeInfo = {
91
+ name: 'node';
92
+ version: string;
93
+ host?: string;
94
+ pid?: number;
95
+ memoryMb?: number;
96
+ uptimeS?: number;
97
+ };
98
+
99
+ export type IngestResponse = { accepted: number; dropped: number };
100
+
101
+ export type IngestErrorCode =
102
+ | 'invalid_key'
103
+ | 'account_disabled'
104
+ | 'payload_too_large'
105
+ | 'invalid_batch'
106
+ | 'quota_exceeded'
107
+ | 'rate_limited';
108
+
109
+ export type IngestError = { error: { code: IngestErrorCode; message: string } };
110
+ ```
111
+
112
+ - `ts` em milissegundos desde a época. Se estiver mais de 24 h longe do relógio do servidor, vale a hora de recebimento.
113
+ - `message` do log é a chave do evento. Convenção: mensagem estável (`webhook.olx.lead`, `http.request`, `api listening`) e o que varia vai em `attrs`. A regra de ausência compara a `message` exata.
114
+ - `dropped` é quantos eventos o SDK descartou desde o último lote aceito (fila cheia); o servidor soma em `usage_daily.dropped` e a tela do projeto mostra.
115
+
116
+ ## Respostas
117
+
118
+ | Status | Corpo | Quando | O SDK faz |
119
+ |---|---|---|---|
120
+ | 202 | `IngestResponse` | lote aceito, mesmo que parcialmente: eventos inválidos ou de `type` desconhecido entram em `dropped` | segue |
121
+ | 400 | `invalid_batch` | o envelope não é um `EventBatch` (sem `v: 1`, sem `events`) | descarta o lote, avisa uma vez |
122
+ | 401 | `invalid_key` | chave ausente, inexistente ou revogada | pausa os envios por 10 min, descarta a fila, avisa uma vez |
123
+ | 403 | `account_disabled` | conta desativada | igual ao 401 |
124
+ | 413 | `payload_too_large` | acima de 1 MB ou 500 eventos | descarta o lote |
125
+ | 429 | `quota_exceeded` / `rate_limited`, com `Retry-After` em segundos | cota do dia ou excesso de requisições | devolve o lote à fila e espera o `Retry-After` |
126
+ | 5xx / rede | — | servidor fora | devolve o lote à fila e tenta com espera crescente |
127
+
128
+ ## Compatibilidade
129
+
130
+ O v1 só cresce: campo opcional novo pode; mudar o significado, tornar obrigatório ou remover não pode. O servidor ignora campos desconhecidos. Mudança que quebra vira `/api/v2/events`, com o v1 mantido.
131
+
132
+
133
+ ## Limites recomendados no SDK
134
+
135
+ O servidor corta o que passar destes limites; o SDK deve cortar antes de enviar.
136
+
137
+ | Item | Limite |
138
+ |---|---|
139
+ | `message` | 2000 caracteres |
140
+ | strings em `attrs`, `query`, `body` | 2000 caracteres |
141
+ | `attrs` serializado | 16 KB |
142
+ | `body` serializado | 16 KB |
143
+ | headers | 50 |
144
+ | frames por exceção | 50 |
145
+ | frames com `context` | 10 do próprio app, 5 linhas antes e depois, 300 caracteres por linha |
146
+ | cadeia de `cause` | 5 níveis |
147
+ | profundidade de objetos | 6 |
148
+ | itens por array | 50 |
149
+ | `requestId` | 128 caracteres |
package/README.md CHANGED
@@ -1,3 +1,124 @@
1
- # Temporary Holding Version
1
+ # @oconde/radar
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ SDK do [Radar](https://radar.oconde.dev) para Node.js e NestJS: logs, erros com trecho de código e contexto de requisição. Sem dependências em runtime.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm install @oconde/radar
9
+ ```
10
+
11
+ Node 20 ou mais novo. Funciona em ESM e CommonJS, do NestJS 10 em diante.
12
+
13
+ ## NestJS
14
+
15
+ ```ts
16
+ import { Module } from '@nestjs/common';
17
+ import { RadarModule } from '@oconde/radar/nest';
18
+
19
+ @Module({
20
+ imports: [RadarModule.forRoot({ key: process.env.RADAR_KEY, release: process.env.GIT_SHA })],
21
+ })
22
+ export class AppModule {}
23
+ ```
24
+
25
+ Com `ConfigService`:
26
+
27
+ ```ts
28
+ RadarModule.forRootAsync({
29
+ inject: [ConfigService],
30
+ useFactory: (config: ConfigService) => ({ key: config.get('RADAR_KEY') }),
31
+ });
32
+ ```
33
+
34
+ O módulo registra sozinho o contexto por requisição, um log `http.request` por requisição e a captura dos erros 5xx lançados nos handlers. Chame `app.enableShutdownHooks()` para o Radar enviar a fila ao desligar.
35
+
36
+ Nos services, nada precisa ser injetado:
37
+
38
+ ```ts
39
+ import { radar } from '@oconde/radar';
40
+
41
+ radar.info('webhook.olx.lead', { leadId });
42
+ ```
43
+
44
+ Quem prefere injeção usa `RadarService`, que tem os mesmos métodos.
45
+
46
+ Erros lançados em guards e pipes não passam pelo interceptor. Se o app não tem filtro de exceção próprio, registre o do Radar:
47
+
48
+ ```ts
49
+ import { APP_FILTER } from '@nestjs/core';
50
+ import { RadarExceptionFilter } from '@oconde/radar/nest';
51
+
52
+ providers: [{ provide: APP_FILTER, useClass: RadarExceptionFilter }];
53
+ ```
54
+
55
+ Se já tem, acrescente no `catch` dele: `if (status >= 500) radar.captureError(exception)`. O mesmo erro nunca é enviado duas vezes.
56
+
57
+ ## Express
58
+
59
+ ```ts
60
+ import { radar } from '@oconde/radar';
61
+
62
+ radar.init({ key: process.env.RADAR_KEY });
63
+ app.use(radar.middleware());
64
+ ```
65
+
66
+ ## API
67
+
68
+ | Função | O que faz |
69
+ |---|---|
70
+ | `radar.init(options)` | configura uma vez; chamadas seguintes são ignoradas |
71
+ | `radar.debug/info/warn/error(message, data?)` | log com `data` como atributos; dentro de uma requisição sai com o `requestId` e o usuário |
72
+ | `radar.logRequest(title, data?, { level?, redact? }?)` | log com a requisição atual inteira: método, URL, rota, params, query, headers, body, IP |
73
+ | `radar.captureError(error, data?)` | erro com stack, trecho de código, requisição, usuário e runtime |
74
+ | `radar.setUser({ id, email, name })` | usuário da requisição atual |
75
+ | `radar.track(name, fn, data?)` | mede `fn`, registra sucesso ou falha com `durationMs`, captura e relança o erro |
76
+ | `radar.middleware()` | middleware Express de contexto (`x-request-id`) |
77
+ | `radar.flush(timeoutMs?)` / `radar.close(timeoutMs?)` | envia a fila; `close` também desliga |
78
+ | `radar.settings` | opções em uso (só leitura); `radar.settings.console` diz se o Radar já imprime no stdout, útil para um logger próprio não duplicar linhas |
79
+
80
+ Mensagens de log são chaves estáveis (`webhook.olx.lead`); o que varia vai em `data`.
81
+
82
+ ## Opções
83
+
84
+ | Opção | Padrão | |
85
+ |---|---|---|
86
+ | `key` | — | sem chave nada é enviado |
87
+ | `endpoint` | `https://radar-ingest.oconde.dev` | |
88
+ | `environment` | `NODE_ENV` ou `production` | |
89
+ | `release` | — | ex.: o SHA do commit |
90
+ | `console` | `false` | imprime cada log como JSON no stdout, mesmo sem chave |
91
+ | `minLevel` | `info` | |
92
+ | `redact` | `mask` | `none` guarda tokens e senhas inteiros |
93
+ | `requestDetail` | `full` | `route` manda da requisição só o método e a rota (`/students/:studentId`), com status e duração: sem URL concreta, query, params, headers, corpo, IP nem user agent. Para apps com dado sensível (saúde, LGPD) |
94
+ | `logRequests` | `true` | log `http.request` automático |
95
+ | `ignorePaths` | `['/healthz', '/health']` | |
96
+ | `captureUnhandled` | `true` | `uncaughtException` e `unhandledRejection`; o processo cai como cairia sem o Radar |
97
+ | `debug` | `false` | mostra problemas internos do SDK |
98
+
99
+ ## Dados sensíveis
100
+
101
+ Com `redact: 'mask'` (padrão), o SDK mascara antes de enviar:
102
+
103
+ - headers e chaves (no corpo, na query, nos parâmetros da rota, no caminho da URL e nos atributos) cujo nome tenha uma destas palavras: `password`, `senha`, `secret`, `token`, `auth`, `authorization`, `cookie`, `apikey`, `api key`, `private key`, `access key`, `signature`, `jwt`, `session`, `credential`, `cpf`, `card`, `cvv`, `cvc`, `pin`, `otp`, além da chave `key` sozinha e dos headers `x-…-key`, `x-…-secret` e `x-…-token`. A comparação é por palavra: `cardio` e `passos` não são mascarados;
104
+ - qualquer valor com cara de credencial (`Bearer …`, `Basic …`, JWT), seja qual for a chave.
105
+
106
+ Tokens e chaves saem como `Bearer eyJh…5x9Q #a1b2c3d4`: dá para ver se veio, qual era e se dois valores são iguais. Senhas, CPF, cartão, CVV, PIN e OTP saem só como `••• #a1b2c3d4`, sem nenhum pedaço do valor. A impressão digital é um HMAC com chave derivada da chave do projeto, então não dá para descobrir o valor por força bruta sem ela.
107
+
108
+ `redact: 'none'` (no `init` ou por chamada de `logRequest`) guarda os valores inteiros.
109
+
110
+ ## Trecho de código nos erros
111
+
112
+ O SDK lê os source maps sozinho. Em projetos TypeScript, ligue no `tsconfig`:
113
+
114
+ ```json
115
+ { "compilerOptions": { "sourceMap": true, "inlineSources": true } }
116
+ ```
117
+
118
+ Com `inlineSources`, o código vai dentro do `.map` e a imagem de produção não precisa conter `src/`.
119
+
120
+ ## Garantias
121
+
122
+ Nenhuma função lança erro para o app (exceto `track`, que relança o erro de `fn`). A fila fica em memória (até 1000 eventos), é enviada a cada 2 s ou 100 eventos e tenta de novo com espera crescente se o Radar estiver fora. O SDK não registra `SIGTERM`.
123
+
124
+ Formato dos dados enviados: [PROTOCOL.md](PROTOCOL.md).
@@ -0,0 +1,104 @@
1
+ import { UserInfo, LogLevel } from './protocol/index.cjs';
2
+
3
+ type HeaderValue = string | string[] | number | undefined;
4
+ type RequestLike = {
5
+ method?: string;
6
+ url?: string;
7
+ originalUrl?: string;
8
+ route?: {
9
+ path?: unknown;
10
+ };
11
+ params?: Record<string, unknown>;
12
+ query?: unknown;
13
+ headers: Record<string, HeaderValue>;
14
+ body?: unknown;
15
+ ip?: string;
16
+ socket?: {
17
+ remoteAddress?: string;
18
+ };
19
+ };
20
+ type ResponseLike = {
21
+ statusCode: number;
22
+ setHeader(name: string, value: string): unknown;
23
+ once(event: 'finish', listener: () => void): unknown;
24
+ };
25
+ type RadarContext = {
26
+ requestId: string;
27
+ startedAt: number;
28
+ req?: RequestLike;
29
+ route?: string;
30
+ user?: UserInfo;
31
+ };
32
+
33
+ type RadarMiddleware = (req: RequestLike, res: ResponseLike, next: (error?: unknown) => void) => void;
34
+
35
+ type RedactMode = 'mask' | 'none';
36
+ type RequestDetail = 'full' | 'route';
37
+ type RadarOptions = {
38
+ key?: string;
39
+ endpoint?: string;
40
+ environment?: string;
41
+ release?: string;
42
+ enabled?: boolean;
43
+ console?: boolean;
44
+ minLevel?: LogLevel;
45
+ redact?: RedactMode;
46
+ requestDetail?: RequestDetail;
47
+ logRequests?: boolean;
48
+ ignorePaths?: string[];
49
+ captureUnhandled?: boolean;
50
+ debug?: boolean;
51
+ };
52
+ type ResolvedOptions = {
53
+ key: string | undefined;
54
+ endpoint: string;
55
+ environment: string;
56
+ release: string | undefined;
57
+ enabled: boolean;
58
+ console: boolean;
59
+ minLevel: LogLevel;
60
+ redact: RedactMode;
61
+ requestDetail: RequestDetail;
62
+ logRequests: boolean;
63
+ ignorePaths: string[];
64
+ captureUnhandled: boolean;
65
+ debug: boolean;
66
+ };
67
+
68
+ type Attributes = Record<string, unknown>;
69
+ type LogRequestOptions = {
70
+ level?: LogLevel;
71
+ redact?: RedactMode;
72
+ };
73
+ declare class RadarClient {
74
+ private options;
75
+ private transport;
76
+ private initialized;
77
+ private closed;
78
+ private readonly captured;
79
+ get isEnabled(): boolean;
80
+ get settings(): Readonly<ResolvedOptions>;
81
+ init(options?: RadarOptions): void;
82
+ debug(message: string, data?: Attributes): void;
83
+ info(message: string, data?: Attributes): void;
84
+ warn(message: string, data?: Attributes): void;
85
+ error(message: string, data?: Attributes): void;
86
+ logRequest(title: string, data?: Attributes, options?: LogRequestOptions): void;
87
+ captureError(error: unknown, data?: Attributes): void;
88
+ captureUnhandled(error: unknown, level: 'error' | 'fatal'): void;
89
+ setUser(user: UserInfo): void;
90
+ track<T>(name: string, fn: () => T | Promise<T>, data?: Attributes): Promise<T>;
91
+ middleware(): RadarMiddleware;
92
+ shouldLogRequest(req: RequestLike): boolean;
93
+ logHttpRequest(context: RadarContext, status: number): void;
94
+ flush(timeoutMs?: number): Promise<void>;
95
+ close(timeoutMs?: number): Promise<void>;
96
+ private emitLog;
97
+ private emitError;
98
+ private print;
99
+ private safely;
100
+ private debugWarn;
101
+ private writeWarning;
102
+ }
103
+
104
+ export { type Attributes as A, type LogRequestOptions as L, RadarClient as R, type RadarMiddleware as a, type RadarOptions as b, type RedactMode as c, type RequestLike as d, type ResponseLike as e };
@@ -0,0 +1,104 @@
1
+ import { UserInfo, LogLevel } from './protocol/index.js';
2
+
3
+ type HeaderValue = string | string[] | number | undefined;
4
+ type RequestLike = {
5
+ method?: string;
6
+ url?: string;
7
+ originalUrl?: string;
8
+ route?: {
9
+ path?: unknown;
10
+ };
11
+ params?: Record<string, unknown>;
12
+ query?: unknown;
13
+ headers: Record<string, HeaderValue>;
14
+ body?: unknown;
15
+ ip?: string;
16
+ socket?: {
17
+ remoteAddress?: string;
18
+ };
19
+ };
20
+ type ResponseLike = {
21
+ statusCode: number;
22
+ setHeader(name: string, value: string): unknown;
23
+ once(event: 'finish', listener: () => void): unknown;
24
+ };
25
+ type RadarContext = {
26
+ requestId: string;
27
+ startedAt: number;
28
+ req?: RequestLike;
29
+ route?: string;
30
+ user?: UserInfo;
31
+ };
32
+
33
+ type RadarMiddleware = (req: RequestLike, res: ResponseLike, next: (error?: unknown) => void) => void;
34
+
35
+ type RedactMode = 'mask' | 'none';
36
+ type RequestDetail = 'full' | 'route';
37
+ type RadarOptions = {
38
+ key?: string;
39
+ endpoint?: string;
40
+ environment?: string;
41
+ release?: string;
42
+ enabled?: boolean;
43
+ console?: boolean;
44
+ minLevel?: LogLevel;
45
+ redact?: RedactMode;
46
+ requestDetail?: RequestDetail;
47
+ logRequests?: boolean;
48
+ ignorePaths?: string[];
49
+ captureUnhandled?: boolean;
50
+ debug?: boolean;
51
+ };
52
+ type ResolvedOptions = {
53
+ key: string | undefined;
54
+ endpoint: string;
55
+ environment: string;
56
+ release: string | undefined;
57
+ enabled: boolean;
58
+ console: boolean;
59
+ minLevel: LogLevel;
60
+ redact: RedactMode;
61
+ requestDetail: RequestDetail;
62
+ logRequests: boolean;
63
+ ignorePaths: string[];
64
+ captureUnhandled: boolean;
65
+ debug: boolean;
66
+ };
67
+
68
+ type Attributes = Record<string, unknown>;
69
+ type LogRequestOptions = {
70
+ level?: LogLevel;
71
+ redact?: RedactMode;
72
+ };
73
+ declare class RadarClient {
74
+ private options;
75
+ private transport;
76
+ private initialized;
77
+ private closed;
78
+ private readonly captured;
79
+ get isEnabled(): boolean;
80
+ get settings(): Readonly<ResolvedOptions>;
81
+ init(options?: RadarOptions): void;
82
+ debug(message: string, data?: Attributes): void;
83
+ info(message: string, data?: Attributes): void;
84
+ warn(message: string, data?: Attributes): void;
85
+ error(message: string, data?: Attributes): void;
86
+ logRequest(title: string, data?: Attributes, options?: LogRequestOptions): void;
87
+ captureError(error: unknown, data?: Attributes): void;
88
+ captureUnhandled(error: unknown, level: 'error' | 'fatal'): void;
89
+ setUser(user: UserInfo): void;
90
+ track<T>(name: string, fn: () => T | Promise<T>, data?: Attributes): Promise<T>;
91
+ middleware(): RadarMiddleware;
92
+ shouldLogRequest(req: RequestLike): boolean;
93
+ logHttpRequest(context: RadarContext, status: number): void;
94
+ flush(timeoutMs?: number): Promise<void>;
95
+ close(timeoutMs?: number): Promise<void>;
96
+ private emitLog;
97
+ private emitError;
98
+ private print;
99
+ private safely;
100
+ private debugWarn;
101
+ private writeWarning;
102
+ }
103
+
104
+ export { type Attributes as A, type LogRequestOptions as L, RadarClient as R, type RadarMiddleware as a, type RadarOptions as b, type RedactMode as c, type RequestLike as d, type ResponseLike as e };