@oconde/radar 0.1.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/CHANGELOG.md +38 -0
- package/PROTOCOL.md +43 -7
- package/README.md +65 -61
- package/dist/bin/radar-sourcemaps.js +244 -0
- package/dist/browser/index.cjs +173 -0
- package/dist/browser/index.cjs.map +1 -0
- package/dist/browser/index.d.cts +30 -0
- package/dist/browser/index.d.ts +30 -0
- package/dist/browser/index.js +152 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/{client-ZpQHpeAu.d.ts → client-BB-to8cr.d.cts} +39 -2
- package/dist/{client-DbNex6tj.d.cts → client-BS798K3S.d.ts} +39 -2
- package/dist/index.cjs +469 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +470 -79
- package/dist/index.js.map +1 -1
- package/dist/nest/index.cjs +469 -78
- package/dist/nest/index.cjs.map +1 -1
- package/dist/nest/index.d.cts +1 -1
- package/dist/nest/index.d.ts +1 -1
- package/dist/nest/index.js +472 -81
- package/dist/nest/index.js.map +1 -1
- package/dist/protocol/index.cjs +16 -3
- package/dist/protocol/index.cjs.map +1 -1
- package/dist/protocol/index.d.cts +27 -2
- package/dist/protocol/index.d.ts +27 -2
- package/dist/protocol/index.js +11 -2
- package/dist/protocol/index.js.map +1 -1
- package/dist/sveltekit/index.cjs +2003 -0
- package/dist/sveltekit/index.cjs.map +1 -0
- package/dist/sveltekit/index.d.cts +28 -0
- package/dist/sveltekit/index.d.ts +28 -0
- package/dist/sveltekit/index.js +1973 -0
- package/dist/sveltekit/index.js.map +1 -0
- package/package.json +39 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Novo
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
|
|
9
|
+
### Correções
|
|
10
|
+
|
|
11
|
+
- 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).
|
|
12
|
+
|
|
13
|
+
## 0.2.0
|
|
14
|
+
|
|
15
|
+
### Novo
|
|
16
|
+
|
|
17
|
+
- `@oconde/radar/sveltekit`: `radarHandle` (contexto, `x-request-id`, log `http.request`; opção `requestId` para usar o id do app), `radarHandleError`, `radarClientErrors` (rota que recebe erros do navegador), `moveClientSourceMaps` e `composeServerSourceMaps`.
|
|
18
|
+
- `@oconde/radar/browser`: `reportClientError`, `handleErrorWithRadar` e `listenForClientErrors`, sem dependência de Node.
|
|
19
|
+
- Erros do navegador trazem o que a pessoa fez logo antes: `listenForClientErrors` guarda o último clique ou envio de formulário (elemento e texto visível, nunca o valor digitado; links com texto longo, números ou `@` vão sem o texto, e e-mails e números viram `<email>` e `<número>`) e a rota grava em `ui.action`, `ui.element`, `ui.label` e `ui.msBefore`. No modo `requestDetail: 'route'` o texto do elemento não é enviado. `captureActions: false` desliga.
|
|
20
|
+
- Frames do navegador ganham o nome da função do código original (inferido do source map), em vez de ficarem sem nome.
|
|
21
|
+
- Comando `radar-sourcemaps [buildDir]`: depois do `vite build`, tira os source maps de `build/client` e reescreve os caminhos para continuarem apontando para o código. Também junta os dois níveis de mapa do servidor (o do Vite e o do `adapter-node`), para a stack do servidor apontar direto para o arquivo em `src/`, e não para `.svelte-kit/output/server`.
|
|
22
|
+
- `radar.withContext`, `radar.checkIn` e `radar.cron` para workers e tarefas agendadas.
|
|
23
|
+
- `radar.captureException` para quem já tem a exceção montada.
|
|
24
|
+
|
|
25
|
+
### Mudanças de comportamento
|
|
26
|
+
|
|
27
|
+
- **Agrupamento de erros:** frames que, pelo source map, caem em `node_modules` deixam de contar como código do app, e arquivos gerados com `sourcemap: 'hidden'` passam a usar o `.map` ao lado. As duas coisas mudam a impressão digital de alguns erros: no deploy que atualizar o SDK, erros já conhecidos podem reaparecer como novos uma vez.
|
|
28
|
+
- `radarHandleError()` e `handleErrorWithRadar()` sem handler interno imprimem o erro no console e deixam o SvelteKit usar a mensagem padrão (antes devolviam "Internal Error", inclusive no 404).
|
|
29
|
+
- Um `x-request-id` com caracteres fora do ASCII imprimível é trocado por um id gerado.
|
|
30
|
+
- Check-ins cortam `environment` em 64 caracteres, `release` em 128 e `durationMs` em 7 dias, como o servidor exige.
|
|
31
|
+
|
|
32
|
+
### Correções
|
|
33
|
+
|
|
34
|
+
- Express: segredos em parâmetros do caminho (como `/reset/:token`) eram enviados sem máscara quando a requisição terminava no middleware de erro do app.
|
|
35
|
+
|
|
36
|
+
## 0.1.0
|
|
37
|
+
|
|
38
|
+
Primeira versão: NestJS, Express, logs, erros com trecho de código, requisições e mascaramento de dados sensíveis.
|
package/PROTOCOL.md
CHANGED
|
@@ -11,7 +11,7 @@ Content-Type: application/json
|
|
|
11
11
|
Content-Encoding: gzip (opcional)
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Limites:
|
|
14
|
+
Limites: até 500 eventos por lote e 1 MB de corpo, tanto nos bytes recebidos quanto no JSON depois de descompactado. `Content-Encoding` aceita só `gzip` ou nenhum.
|
|
15
15
|
|
|
16
16
|
## Tipos
|
|
17
17
|
|
|
@@ -117,14 +117,48 @@ export type IngestError = { error: { code: IngestErrorCode; message: string } };
|
|
|
117
117
|
|
|
118
118
|
| Status | Corpo | Quando | O SDK faz |
|
|
119
119
|
|---|---|---|---|
|
|
120
|
-
| 202 | `IngestResponse` | lote aceito, mesmo que parcialmente: eventos inválidos
|
|
121
|
-
| 400 | `invalid_batch` | o envelope não é um `EventBatch` (sem `v: 1`, sem `events`) | descarta o lote, avisa uma vez |
|
|
120
|
+
| 202 | `IngestResponse` | lote aceito, mesmo que parcialmente: eventos inválidos, de `type` desconhecido ou acima da cota do dia entram em `dropped` | segue |
|
|
121
|
+
| 400 | `invalid_batch` | JSON inválido, `Content-Encoding` diferente de `gzip`, ou o envelope não é um `EventBatch` (sem `v: 1`, sem `sdk`, sem `events`) | descarta o lote, avisa uma vez |
|
|
122
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 | `
|
|
123
|
+
| 403 | `account_disabled` | conta desativada ou projeto apagado | igual ao 401 |
|
|
124
|
+
| 413 | `payload_too_large` | acima de 1 MB ou de 500 eventos | descarta o lote |
|
|
125
|
+
| 429 | `rate_limited`, com `Retry-After: 1` | excesso de requisições da mesma chave | devolve o lote à fila e espera o `Retry-After` |
|
|
126
|
+
| 429 | `quota_exceeded`, com `Retry-After` até a virada do dia | a cota do dia acabou e nenhum evento do lote coube | devolve o lote à fila e espera o `Retry-After` |
|
|
126
127
|
| 5xx / rede | — | servidor fora | devolve o lote à fila e tenta com espera crescente |
|
|
127
128
|
|
|
129
|
+
Quando a cota do dia acaba, o Radar avisa uma vez por dia em todos os canais ativos da conta.
|
|
130
|
+
|
|
131
|
+
## Check-in de tarefa agendada
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
POST https://radar-ingest.oconde.dev/api/v1/checkins/<slug>
|
|
135
|
+
Authorization: Bearer rk_<40 caracteres>
|
|
136
|
+
Content-Type: application/json
|
|
137
|
+
|
|
138
|
+
{ "status": "in_progress" | "ok" | "error", "checkInId"?: string, "durationMs"?: number, "environment"?: string, "release"?: string }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- `slug`: `^[a-z0-9][a-z0-9-]{0,63}$`, o mesmo do monitor criado no Radar, no projeto da chave.
|
|
142
|
+
- `checkInId` (até 64 caracteres) liga o `in_progress` ao `ok` ou `error` da mesma execução. Sem ele, `ok` e `error` fecham a execução aberta mais recente.
|
|
143
|
+
- `202 { "accepted": true }`. `404 unknown_monitor` quando o slug não existe no projeto da chave, `400 invalid_checkin` para corpo fora do formato, e `401`, `403` e `429` como no envio de eventos.
|
|
144
|
+
- O SDK manda na hora, sem fila, e nunca lança: `radar.checkIn(slug, status, { checkInId, durationMs })` ou `radar.cron(slug, fn)`.
|
|
145
|
+
|
|
146
|
+
Exemplo para um script sem o SDK:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
curl -fsS -X POST -H "Authorization: Bearer $RADAR_KEY" -H 'content-type: application/json' \
|
|
150
|
+
-d '{"status":"ok"}' https://radar-ingest.oconde.dev/api/v1/checkins/walg-archive-check
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Resumo de release
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
GET https://radar-ingest.oconde.dev/api/v1/releases/<release>
|
|
157
|
+
Authorization: Bearer rk_<40 caracteres>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`200 { "release", "firstSeen": iso | null, "newIssues": n, "events": n, "issues": [{ "title", "culprit", "count", "url" }] }`, com até 5 erros (mais ocorrências primeiro), só do projeto da chave. Serve para o pipeline de deploy olhar a release logo depois de publicar.
|
|
161
|
+
|
|
128
162
|
## Compatibilidade
|
|
129
163
|
|
|
130
164
|
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.
|
|
@@ -132,7 +166,9 @@ O v1 só cresce: campo opcional novo pode; mudar o significado, tornar obrigató
|
|
|
132
166
|
|
|
133
167
|
## Limites recomendados no SDK
|
|
134
168
|
|
|
135
|
-
O servidor corta o
|
|
169
|
+
O servidor também corta, com regras um pouco diferentes: a `message` do log em 2000 bytes (UTF-8), a mensagem da exceção em 2000 caracteres, `attrs`, `query` e headers em 16 KB, `params` em 4 KB e `body` em 16 KB. Um campo JSON acima do limite vira `{ "_truncated": "<começo do JSON>" }`. Abaixo de 64 níveis de profundidade o servidor troca o valor por `null`; o SDK corta em 6.
|
|
170
|
+
|
|
171
|
+
O SDK deve cortar antes de enviar:
|
|
136
172
|
|
|
137
173
|
| Item | Limite |
|
|
138
174
|
|---|---|
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @oconde/radar
|
|
2
2
|
|
|
3
|
-
SDK do [Radar](https://radar.oconde.dev) para Node.js e
|
|
3
|
+
SDK do [Radar](https://radar.oconde.dev) para Node.js, NestJS e SvelteKit: logs, erros com trecho de código, contexto de requisição, tarefas agendadas e erros do navegador. Sem dependências em runtime.
|
|
4
|
+
|
|
5
|
+
**Documentação completa: https://radar.oconde.dev/docs/**
|
|
4
6
|
|
|
5
7
|
## Instalação
|
|
6
8
|
|
|
@@ -8,7 +10,7 @@ SDK do [Radar](https://radar.oconde.dev) para Node.js e NestJS: logs, erros com
|
|
|
8
10
|
npm install @oconde/radar
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
Node 20 ou mais novo. Funciona em ESM e CommonJS, do NestJS 10 em diante.
|
|
13
|
+
Node 20 ou mais novo. Funciona em ESM e CommonJS, do NestJS 10 em diante. Gere a chave do projeto no painel do Radar e ponha em `RADAR_KEY`.
|
|
12
14
|
|
|
13
15
|
## NestJS
|
|
14
16
|
|
|
@@ -22,103 +24,105 @@ import { RadarModule } from '@oconde/radar/nest';
|
|
|
22
24
|
export class AppModule {}
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
O módulo registra o contexto por requisição, um log `http.request` por requisição e a captura dos erros 5xx. Nos services:
|
|
26
28
|
|
|
27
29
|
```ts
|
|
28
|
-
|
|
29
|
-
inject: [ConfigService],
|
|
30
|
-
useFactory: (config: ConfigService) => ({ key: config.get('RADAR_KEY') }),
|
|
31
|
-
});
|
|
32
|
-
```
|
|
30
|
+
import { radar } from '@oconde/radar';
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
radar.info('webhook.olx.lead', { leadId });
|
|
33
|
+
```
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
## Express
|
|
37
36
|
|
|
38
37
|
```ts
|
|
39
38
|
import { radar } from '@oconde/radar';
|
|
40
39
|
|
|
41
|
-
radar.
|
|
40
|
+
radar.init({ key: process.env.RADAR_KEY });
|
|
41
|
+
app.use(radar.middleware());
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
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:
|
|
44
|
+
Depois das rotas, para os erros dos handlers chegarem ao Radar:
|
|
47
45
|
|
|
48
46
|
```ts
|
|
49
|
-
import {
|
|
50
|
-
import { RadarExceptionFilter } from '@oconde/radar/nest';
|
|
47
|
+
import type { ErrorRequestHandler } from 'express';
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
const reportErrors: ErrorRequestHandler = (error, req, res, next) => {
|
|
50
|
+
radar.captureError(error);
|
|
51
|
+
next(error);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
app.use(reportErrors);
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
Exemplo completo em [Express e Node](https://radar.oconde.dev/docs/sdk/express/).
|
|
56
58
|
|
|
57
|
-
##
|
|
59
|
+
## SvelteKit
|
|
58
60
|
|
|
59
61
|
```ts
|
|
62
|
+
import { sequence } from '@sveltejs/kit/hooks';
|
|
60
63
|
import { radar } from '@oconde/radar';
|
|
64
|
+
import { radarHandle, radarHandleError } from '@oconde/radar/sveltekit';
|
|
61
65
|
|
|
62
|
-
radar.init({ key: process.env.RADAR_KEY });
|
|
63
|
-
|
|
66
|
+
radar.init({ key: process.env.RADAR_KEY, release: process.env.GIT_SHA });
|
|
67
|
+
|
|
68
|
+
export const handle = sequence(radarHandle(), yourHandle);
|
|
69
|
+
export const handleError = radarHandleError();
|
|
64
70
|
```
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
Se o app já gera o próprio id de requisição, passe-o e coloque o `radarHandle` depois do handle que o cria: `radarHandle({ requestId: (event) => event.locals.requestId })`.
|
|
67
73
|
|
|
68
|
-
|
|
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 |
|
|
74
|
+
Erros do navegador vão por uma rota do próprio app, sem expor a chave. Ela limita tamanho e frequência por visitante e no total; atrás de proxy, configure `ADDRESS_HEADER`/`XFF_DEPTH` do adapter-node para o IP do visitante ser o real:
|
|
79
75
|
|
|
80
|
-
|
|
76
|
+
```ts title="src/routes/api/radar/client-errors/+server.ts"
|
|
77
|
+
import { radarClientErrors } from '@oconde/radar/sveltekit';
|
|
81
78
|
|
|
82
|
-
|
|
79
|
+
export const POST = radarClientErrors();
|
|
80
|
+
```
|
|
83
81
|
|
|
84
|
-
|
|
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 |
|
|
82
|
+
```ts
|
|
83
|
+
import { handleErrorWithRadar, listenForClientErrors } from '@oconde/radar/browser';
|
|
98
84
|
|
|
99
|
-
|
|
85
|
+
listenForClientErrors();
|
|
86
|
+
export const handleError = handleErrorWithRadar();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
O erro do navegador chega com o que a pessoa fez logo antes (por exemplo, "clicou no botão Aplicar cupom"): o último clique ou envio de formulário até 10 s antes, com o texto visível do elemento e nunca o que foi digitado. `listenForClientErrors({ captureActions: false })` desliga.
|
|
100
90
|
|
|
101
|
-
Com `
|
|
91
|
+
Com `build: { sourcemap: 'hidden' }` no Vite e `radar-sourcemaps` depois do build, os mapas saem de `build/client`, os do servidor passam a apontar direto para `src/`, e os erros mostram o código original. Detalhes em [SvelteKit](https://radar.oconde.dev/docs/sdk/sveltekit/) e [Erros do navegador](https://radar.oconde.dev/docs/sdk/browser/).
|
|
102
92
|
|
|
103
|
-
|
|
104
|
-
- qualquer valor com cara de credencial (`Bearer …`, `Basic …`, JWT), seja qual for a chave.
|
|
93
|
+
## Tarefas agendadas
|
|
105
94
|
|
|
106
|
-
|
|
95
|
+
```ts
|
|
96
|
+
await radar.cron('nightly-report', () => buildReport());
|
|
97
|
+
```
|
|
98
|
+
|
|
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
|
+
|
|
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
|
+
```
|
|
107
106
|
|
|
108
|
-
`
|
|
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.
|
|
109
108
|
|
|
110
|
-
##
|
|
109
|
+
## Erros com o seu código
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
Ligue no `tsconfig` para os erros mostrarem as linhas do TypeScript:
|
|
113
112
|
|
|
114
113
|
```json
|
|
115
114
|
{ "compilerOptions": { "sourceMap": true, "inlineSources": true } }
|
|
116
115
|
```
|
|
117
116
|
|
|
118
|
-
|
|
117
|
+
## Na documentação
|
|
119
118
|
|
|
120
|
-
|
|
119
|
+
- [Começar em 5 minutos](https://radar.oconde.dev/docs/start/getting-started/)
|
|
120
|
+
- [Todas as opções](https://radar.oconde.dev/docs/sdk/options/)
|
|
121
|
+
- [Dados sensíveis e `requestDetail: 'route'`](https://radar.oconde.dev/docs/sdk/sensitive-data/)
|
|
122
|
+
- [Garantias: fila, lotes e o que acontece quando o Radar cai](https://radar.oconde.dev/docs/sdk/guarantees/)
|
|
123
|
+
- [Referência da API](https://radar.oconde.dev/docs/reference/sdk-api/)
|
|
124
|
+
- [Protocolo de envio](https://radar.oconde.dev/docs/reference/protocol/) (também em [PROTOCOL.md](PROTOCOL.md), para quem escreve um SDK em outra linguagem)
|
|
121
125
|
|
|
122
|
-
|
|
126
|
+
## Licença
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
MIT
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/sveltekit/sourcemaps.ts
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
6
|
+
|
|
7
|
+
// src/sveltekit/vlq.ts
|
|
8
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
9
|
+
var DIGITS = new Map([...ALPHABET].map((char, index) => [char, index]));
|
|
10
|
+
var FIELDS = 5;
|
|
11
|
+
function decodeGroup(group) {
|
|
12
|
+
const values = [];
|
|
13
|
+
let value = 0;
|
|
14
|
+
let factor = 1;
|
|
15
|
+
for (const char of group) {
|
|
16
|
+
const digit = DIGITS.get(char);
|
|
17
|
+
if (digit === void 0) throw new Error(`invalid source map mapping: ${group}`);
|
|
18
|
+
value += digit % 32 * factor;
|
|
19
|
+
if (digit >= 32) {
|
|
20
|
+
factor *= 32;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const magnitude = Math.floor(value / 2);
|
|
24
|
+
values.push(value % 2 === 1 ? -magnitude : magnitude);
|
|
25
|
+
value = 0;
|
|
26
|
+
factor = 1;
|
|
27
|
+
}
|
|
28
|
+
return values;
|
|
29
|
+
}
|
|
30
|
+
function encodeValue(delta) {
|
|
31
|
+
let value = delta < 0 ? -delta * 2 + 1 : delta * 2;
|
|
32
|
+
let encoded = "";
|
|
33
|
+
do {
|
|
34
|
+
const digit = value % 32;
|
|
35
|
+
value = Math.floor(value / 32);
|
|
36
|
+
encoded += ALPHABET[value > 0 ? digit + 32 : digit];
|
|
37
|
+
} while (value > 0);
|
|
38
|
+
return encoded;
|
|
39
|
+
}
|
|
40
|
+
function decodeMappings(mappings) {
|
|
41
|
+
const state = new Array(FIELDS).fill(0);
|
|
42
|
+
return mappings.split(";").map((line) => {
|
|
43
|
+
state[0] = 0;
|
|
44
|
+
const segments = [];
|
|
45
|
+
for (const group of line.split(",")) {
|
|
46
|
+
if (!group) continue;
|
|
47
|
+
const values = decodeGroup(group).slice(0, FIELDS);
|
|
48
|
+
segments.push(values.map((delta, field) => state[field] = (state[field] ?? 0) + delta));
|
|
49
|
+
}
|
|
50
|
+
return segments;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function encodeMappings(lines) {
|
|
54
|
+
const state = new Array(FIELDS).fill(0);
|
|
55
|
+
return lines.map((segments) => {
|
|
56
|
+
state[0] = 0;
|
|
57
|
+
return segments.map(
|
|
58
|
+
(segment) => segment.map((value, field) => {
|
|
59
|
+
const delta = value - (state[field] ?? 0);
|
|
60
|
+
state[field] = value;
|
|
61
|
+
return encodeValue(delta);
|
|
62
|
+
}).join("")
|
|
63
|
+
).join(",");
|
|
64
|
+
}).join(";");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/sveltekit/sourcemaps.ts
|
|
68
|
+
function filesUnder(directory) {
|
|
69
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
70
|
+
const full = join(directory, entry.name);
|
|
71
|
+
if (entry.isDirectory()) return filesUnder(full);
|
|
72
|
+
return entry.isFile() ? [full] : [];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function rebase(source, fromDir, root, toDir) {
|
|
76
|
+
if (typeof source !== "string" || isAbsolute(source) || source.includes("://")) return source;
|
|
77
|
+
return relative(toDir, resolve(fromDir, root, source)).split(sep).join("/");
|
|
78
|
+
}
|
|
79
|
+
function moveMap(file, target, originalDir) {
|
|
80
|
+
try {
|
|
81
|
+
const map = JSON.parse(readFileSync(file, "utf8"));
|
|
82
|
+
if (Array.isArray(map.sources)) {
|
|
83
|
+
const root = typeof map.sourceRoot === "string" ? map.sourceRoot : "";
|
|
84
|
+
map.sources = map.sources.map((source) => rebase(source, originalDir, root, dirname(target)));
|
|
85
|
+
delete map.sourceRoot;
|
|
86
|
+
}
|
|
87
|
+
writeFileSync(target, JSON.stringify(map));
|
|
88
|
+
unlinkSync(file);
|
|
89
|
+
} catch {
|
|
90
|
+
renameSync(file, target);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function moveClientSourceMaps(buildDir2 = "build") {
|
|
94
|
+
const clientDir = resolve(buildDir2, "client");
|
|
95
|
+
const mapsDir = resolve(buildDir2, "client-maps");
|
|
96
|
+
const generatedDir = resolve(buildDir2, "..", ".svelte-kit/output/client");
|
|
97
|
+
if (!existsSync(clientDir)) return -1;
|
|
98
|
+
let moved2 = 0;
|
|
99
|
+
for (const file of filesUnder(clientDir)) {
|
|
100
|
+
if (file.endsWith(".map.gz") || file.endsWith(".map.br")) {
|
|
101
|
+
unlinkSync(file);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!file.endsWith(".map")) continue;
|
|
105
|
+
const path = relative(clientDir, file);
|
|
106
|
+
const target = join(mapsDir, path);
|
|
107
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
108
|
+
const generated = join(generatedDir, path);
|
|
109
|
+
moveMap(file, target, dirname(existsSync(generated) ? generated : file));
|
|
110
|
+
moved2 += 1;
|
|
111
|
+
}
|
|
112
|
+
return moved2;
|
|
113
|
+
}
|
|
114
|
+
function parseSourceMap(file) {
|
|
115
|
+
try {
|
|
116
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
117
|
+
if (typeof raw.mappings !== "string" || !Array.isArray(raw.sources)) return null;
|
|
118
|
+
const root = typeof raw.sourceRoot === "string" ? raw.sourceRoot : "";
|
|
119
|
+
return {
|
|
120
|
+
raw,
|
|
121
|
+
lines: decodeMappings(raw.mappings),
|
|
122
|
+
sources: raw.sources.map((source) => typeof source === "string" ? source : ""),
|
|
123
|
+
contents: Array.isArray(raw.sourcesContent) ? raw.sourcesContent.map((content) => typeof content === "string" ? content : null) : [],
|
|
124
|
+
names: Array.isArray(raw.names) ? raw.names.map(String) : [],
|
|
125
|
+
baseDir: resolve(dirname(file), root)
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function isUrl(source) {
|
|
132
|
+
return source.includes("://");
|
|
133
|
+
}
|
|
134
|
+
function isInside(directory, path) {
|
|
135
|
+
const rel = relative(directory, path);
|
|
136
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
137
|
+
}
|
|
138
|
+
function traceSegment(map, line, column) {
|
|
139
|
+
const segments = map.lines[line];
|
|
140
|
+
if (!segments || segments.length === 0) return null;
|
|
141
|
+
let low = 0;
|
|
142
|
+
let high = segments.length - 1;
|
|
143
|
+
let found = -1;
|
|
144
|
+
while (low <= high) {
|
|
145
|
+
const middle = low + high >> 1;
|
|
146
|
+
if ((segments[middle]?.[0] ?? 0) <= column) {
|
|
147
|
+
found = middle;
|
|
148
|
+
low = middle + 1;
|
|
149
|
+
} else {
|
|
150
|
+
high = middle - 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const segment = segments[Math.max(found, 0)];
|
|
154
|
+
return segment && segment.length >= 4 ? segment : null;
|
|
155
|
+
}
|
|
156
|
+
var ComposedTable = class {
|
|
157
|
+
constructor(outputDir) {
|
|
158
|
+
this.outputDir = outputDir;
|
|
159
|
+
}
|
|
160
|
+
outputDir;
|
|
161
|
+
sources = [];
|
|
162
|
+
contents = [];
|
|
163
|
+
names = [];
|
|
164
|
+
sourceIndex = /* @__PURE__ */ new Map();
|
|
165
|
+
nameIndex = /* @__PURE__ */ new Map();
|
|
166
|
+
source(baseDir, source, content) {
|
|
167
|
+
const key = isUrl(source) || isAbsolute(source) ? source : relative(this.outputDir, resolve(baseDir, source)).split(sep).join("/");
|
|
168
|
+
const existing = this.sourceIndex.get(key);
|
|
169
|
+
if (existing !== void 0) return existing;
|
|
170
|
+
this.sourceIndex.set(key, this.sources.length);
|
|
171
|
+
this.sources.push(key);
|
|
172
|
+
this.contents.push(content);
|
|
173
|
+
return this.sources.length - 1;
|
|
174
|
+
}
|
|
175
|
+
name(value) {
|
|
176
|
+
const existing = this.nameIndex.get(value);
|
|
177
|
+
if (existing !== void 0) return existing;
|
|
178
|
+
this.nameIndex.set(value, this.names.length);
|
|
179
|
+
this.names.push(value);
|
|
180
|
+
return this.names.length - 1;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
function composeMap(file, generatedDir, generatedMaps) {
|
|
184
|
+
const outer = parseSourceMap(file);
|
|
185
|
+
if (!outer) return false;
|
|
186
|
+
const inners = outer.sources.map((source) => {
|
|
187
|
+
if (isUrl(source)) return null;
|
|
188
|
+
const path = resolve(outer.baseDir, source);
|
|
189
|
+
if (!isInside(generatedDir, path)) return null;
|
|
190
|
+
if (!generatedMaps.has(path)) generatedMaps.set(path, parseSourceMap(`${path}.map`));
|
|
191
|
+
return generatedMaps.get(path) ?? null;
|
|
192
|
+
});
|
|
193
|
+
if (inners.every((inner) => inner === null)) return false;
|
|
194
|
+
const table = new ComposedTable(dirname(file));
|
|
195
|
+
let traced = 0;
|
|
196
|
+
const lines = outer.lines.map(
|
|
197
|
+
(segments) => segments.map((segment) => {
|
|
198
|
+
const [column = 0, sourceIndex = 0, line = 0, originalColumn = 0, nameIndex] = segment;
|
|
199
|
+
if (segment.length < 4) return [column];
|
|
200
|
+
const inner = inners[sourceIndex];
|
|
201
|
+
const original = inner ? traceSegment(inner, line, originalColumn) : null;
|
|
202
|
+
if (inner && original) {
|
|
203
|
+
traced += 1;
|
|
204
|
+
const [, innerSource = 0, innerLine = 0, innerColumn = 0, innerName] = original;
|
|
205
|
+
const source2 = table.source(inner.baseDir, inner.sources[innerSource] ?? "", inner.contents[innerSource] ?? null);
|
|
206
|
+
const name2 = innerName !== void 0 ? inner.names[innerName] : nameIndex !== void 0 ? outer.names[nameIndex] : void 0;
|
|
207
|
+
return name2 === void 0 ? [column, source2, innerLine, innerColumn] : [column, source2, innerLine, innerColumn, table.name(name2)];
|
|
208
|
+
}
|
|
209
|
+
const source = table.source(outer.baseDir, outer.sources[sourceIndex] ?? "", outer.contents[sourceIndex] ?? null);
|
|
210
|
+
const name = nameIndex !== void 0 ? outer.names[nameIndex] : void 0;
|
|
211
|
+
return name === void 0 ? [column, source, line, originalColumn] : [column, source, line, originalColumn, table.name(name)];
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
if (traced === 0) return false;
|
|
215
|
+
const { sourceRoot: _root, ignoreList: _ignore, x_google_ignoreList: _googleIgnore, ...rest } = outer.raw;
|
|
216
|
+
const composed2 = { ...rest, sources: table.sources, sourcesContent: table.contents, names: table.names, mappings: encodeMappings(lines) };
|
|
217
|
+
writeFileSync(file, JSON.stringify(composed2));
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
function composeServerSourceMaps(buildDir2 = "build") {
|
|
221
|
+
const serverDir = resolve(buildDir2, "server");
|
|
222
|
+
const generatedDir = resolve(buildDir2, "..", ".svelte-kit/output/server");
|
|
223
|
+
if (!existsSync(serverDir) || !existsSync(generatedDir)) return 0;
|
|
224
|
+
const generatedMaps = /* @__PURE__ */ new Map();
|
|
225
|
+
let composed2 = 0;
|
|
226
|
+
for (const file of filesUnder(serverDir)) {
|
|
227
|
+
if (file.endsWith(".map") && composeMap(file, generatedDir, generatedMaps)) composed2 += 1;
|
|
228
|
+
}
|
|
229
|
+
return composed2;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/bin/radar-sourcemaps.ts
|
|
233
|
+
var buildDir = process.argv[2] ?? "build";
|
|
234
|
+
var moved = moveClientSourceMaps(buildDir);
|
|
235
|
+
if (moved < 0) {
|
|
236
|
+
process.stderr.write(`radar: ${buildDir}/client not found; run it after vite build
|
|
237
|
+
`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
process.stdout.write(`radar: ${moved} source maps moved from ${buildDir}/client to ${buildDir}/client-maps
|
|
241
|
+
`);
|
|
242
|
+
var composed = composeServerSourceMaps(buildDir);
|
|
243
|
+
process.stdout.write(`radar: ${composed} server source maps in ${buildDir}/server now point to the original sources
|
|
244
|
+
`);
|