@oconde/radar 0.0.0-stage → 0.2.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 +28 -0
- package/LICENSE +21 -0
- package/PROTOCOL.md +185 -0
- package/README.md +119 -2
- 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-CZHtsQAd.d.cts +130 -0
- package/dist/client-DbBsXgpj.d.ts +130 -0
- package/dist/index.cjs +1335 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1307 -0
- package/dist/index.js.map +1 -0
- package/dist/nest/index.cjs +1495 -0
- package/dist/nest/index.cjs.map +1 -0
- package/dist/nest/index.d.cts +41 -0
- package/dist/nest/index.d.ts +41 -0
- package/dist/nest/index.js +1470 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/protocol/index.cjs +66 -0
- package/dist/protocol/index.cjs.map +1 -0
- package/dist/protocol/index.d.cts +136 -0
- package/dist/protocol/index.d.ts +136 -0
- package/dist/protocol/index.js +35 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/sveltekit/index.cjs +1840 -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 +1810 -0
- package/dist/sveltekit/index.js.map +1 -0
- package/package.json +142 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Novo
|
|
6
|
+
|
|
7
|
+
- `@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`.
|
|
8
|
+
- `@oconde/radar/browser`: `reportClientError`, `handleErrorWithRadar` e `listenForClientErrors`, sem dependência de Node.
|
|
9
|
+
- 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.
|
|
10
|
+
- Frames do navegador ganham o nome da função do código original (inferido do source map), em vez de ficarem sem nome.
|
|
11
|
+
- 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`.
|
|
12
|
+
- `radar.withContext`, `radar.checkIn` e `radar.cron` para workers e tarefas agendadas.
|
|
13
|
+
- `radar.captureException` para quem já tem a exceção montada.
|
|
14
|
+
|
|
15
|
+
### Mudanças de comportamento
|
|
16
|
+
|
|
17
|
+
- **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.
|
|
18
|
+
- `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).
|
|
19
|
+
- Um `x-request-id` com caracteres fora do ASCII imprimível é trocado por um id gerado.
|
|
20
|
+
- Check-ins cortam `environment` em 64 caracteres, `release` em 128 e `durationMs` em 7 dias, como o servidor exige.
|
|
21
|
+
|
|
22
|
+
### Correções
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
## 0.1.0
|
|
27
|
+
|
|
28
|
+
Primeira versão: NestJS, Express, logs, erros com trecho de código, requisições e mascaramento de dados sensíveis.
|
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,185 @@
|
|
|
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: 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
|
+
|
|
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, 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
|
+
| 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 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` |
|
|
127
|
+
| 5xx / rede | — | servidor fora | devolve o lote à fila e tenta com espera crescente |
|
|
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
|
+
|
|
162
|
+
## Compatibilidade
|
|
163
|
+
|
|
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.
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## Limites recomendados no SDK
|
|
168
|
+
|
|
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:
|
|
172
|
+
|
|
173
|
+
| Item | Limite |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `message` | 2000 caracteres |
|
|
176
|
+
| strings em `attrs`, `query`, `body` | 2000 caracteres |
|
|
177
|
+
| `attrs` serializado | 16 KB |
|
|
178
|
+
| `body` serializado | 16 KB |
|
|
179
|
+
| headers | 50 |
|
|
180
|
+
| frames por exceção | 50 |
|
|
181
|
+
| frames com `context` | 10 do próprio app, 5 linhas antes e depois, 300 caracteres por linha |
|
|
182
|
+
| cadeia de `cause` | 5 níveis |
|
|
183
|
+
| profundidade de objetos | 6 |
|
|
184
|
+
| itens por array | 50 |
|
|
185
|
+
| `requestId` | 128 caracteres |
|
package/README.md
CHANGED
|
@@ -1,3 +1,120 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @oconde/radar
|
|
2
2
|
|
|
3
|
-
|
|
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/**
|
|
6
|
+
|
|
7
|
+
## Instalação
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @oconde/radar
|
|
11
|
+
```
|
|
12
|
+
|
|
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`.
|
|
14
|
+
|
|
15
|
+
## NestJS
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Module } from '@nestjs/common';
|
|
19
|
+
import { RadarModule } from '@oconde/radar/nest';
|
|
20
|
+
|
|
21
|
+
@Module({
|
|
22
|
+
imports: [RadarModule.forRoot({ key: process.env.RADAR_KEY, release: process.env.GIT_SHA })],
|
|
23
|
+
})
|
|
24
|
+
export class AppModule {}
|
|
25
|
+
```
|
|
26
|
+
|
|
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:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { radar } from '@oconde/radar';
|
|
31
|
+
|
|
32
|
+
radar.info('webhook.olx.lead', { leadId });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Express
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { radar } from '@oconde/radar';
|
|
39
|
+
|
|
40
|
+
radar.init({ key: process.env.RADAR_KEY });
|
|
41
|
+
app.use(radar.middleware());
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Depois das rotas, para os erros dos handlers chegarem ao Radar:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import type { ErrorRequestHandler } from 'express';
|
|
48
|
+
|
|
49
|
+
const reportErrors: ErrorRequestHandler = (error, req, res, next) => {
|
|
50
|
+
radar.captureError(error);
|
|
51
|
+
next(error);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
app.use(reportErrors);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Exemplo completo em [Express e Node](https://radar.oconde.dev/docs/sdk/express/).
|
|
58
|
+
|
|
59
|
+
## SvelteKit
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { sequence } from '@sveltejs/kit/hooks';
|
|
63
|
+
import { radar } from '@oconde/radar';
|
|
64
|
+
import { radarHandle, radarHandleError } from '@oconde/radar/sveltekit';
|
|
65
|
+
|
|
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();
|
|
70
|
+
```
|
|
71
|
+
|
|
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 })`.
|
|
73
|
+
|
|
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:
|
|
75
|
+
|
|
76
|
+
```ts title="src/routes/api/radar/client-errors/+server.ts"
|
|
77
|
+
import { radarClientErrors } from '@oconde/radar/sveltekit';
|
|
78
|
+
|
|
79
|
+
export const POST = radarClientErrors();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { handleErrorWithRadar, listenForClientErrors } from '@oconde/radar/browser';
|
|
84
|
+
|
|
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.
|
|
90
|
+
|
|
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/).
|
|
92
|
+
|
|
93
|
+
## Tarefas agendadas
|
|
94
|
+
|
|
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
|
+
## Erros com o seu código
|
|
102
|
+
|
|
103
|
+
Ligue no `tsconfig` para os erros mostrarem as linhas do TypeScript:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "compilerOptions": { "sourceMap": true, "inlineSources": true } }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Na documentação
|
|
110
|
+
|
|
111
|
+
- [Começar em 5 minutos](https://radar.oconde.dev/docs/start/getting-started/)
|
|
112
|
+
- [Todas as opções](https://radar.oconde.dev/docs/sdk/options/)
|
|
113
|
+
- [Dados sensíveis e `requestDetail: 'route'`](https://radar.oconde.dev/docs/sdk/sensitive-data/)
|
|
114
|
+
- [Garantias: fila, lotes e o que acontece quando o Radar cai](https://radar.oconde.dev/docs/sdk/guarantees/)
|
|
115
|
+
- [Referência da API](https://radar.oconde.dev/docs/reference/sdk-api/)
|
|
116
|
+
- [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)
|
|
117
|
+
|
|
118
|
+
## Licença
|
|
119
|
+
|
|
120
|
+
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
|
+
`);
|