build(deps): Sentry 11 com coleta restrita e scrub provado pelo SDK

O Sentry 11 trocou `sendDefaultPii` por `dataCollection`, com default AMPLO
(IP, cookies, corpo de requisição/resposta, entrada/saída de IA, dados de
query, filas). Omitir a opção, que era o conserto óbvio para o TS2353, faria o
DSN da comunidade receber dado de titular de terceiros (issue #100).

- lib/sentry/privacidade.ts: ponto único com `dataCollection` restritivo
  explícito (base "v10" do MIGRATION.md + stackFrameVariables: false) e os
  hooks de scrub; espalhado nos 4 Sentry.init (server, edge, client, worker).
- lib/sentry/scrub.ts: beforeSendSpan no formato streamed (name/attributes),
  limpando também `sentry.segment.name`, headers-credencial/IP, corpo, user.* e
  client.address; beforeSend apaga request.data, request.cookies e user e passa
  a mensagem por scrubUrl (o token do path saía na mensagem do erro);
  beforeSendTransaction removido (no-op no 11, confirmado pelo guia).
- lib/sentry/privacidade.sdk.test.ts: prova pelo envelope do SDK real, com
  transporte de captura e controles positivos; 4 sabotagens ficam vermelhas.
- withSentryConfig de @sentry/nextjs/config; enableLogs removido; AGENTS.md
  cita Sentry 11; fragmento em .changes/.

Substitui #1743.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
melgarafael
2026-09-26 16:57:31 -03:00
co-authored by Claude Opus 5.5
parent b2342d6b0d
commit ca1da775ea
11 changed files with 350 additions and 48 deletions
+22
View File
@@ -0,0 +1,22 @@
---
impacto: capacidade_nova
secao: alterado
titulo: Telemetria atualizada para o Sentry 11, com a coleta de dados pessoais travada no mínimo
---
O componente que envia relatórios de erro foi atualizado para a versão 11 do
Sentry. A versão nova passaria a coletar, por padrão, IP, cookies, corpo das
requisições e o texto trocado com a IA. Aqui essa coleta continua desligada, de
forma explícita, e a limpeza de dados pessoais (e-mail, CPF, telefone, IP,
tokens de webhook e de convite) agora é conferida no pacote que de fato sai do
servidor. Quem usa o padrão (Sentry da comunidade) ou desligou a telemetria
(`SENTRY_DSN=off`) não precisa fazer nada.
Se você aponta `SENTRY_DSN` para o seu próprio Sentry, o rastreamento de
desempenho passa a ser enviado em fluxo contínuo, sem o antigo limite de 1.000
trechos por requisição. Alguns atributos mudaram de nome (por exemplo,
`http.method` virou `http.request.method` e `db.statement` virou
`db.query.text`), então alertas e painéis que filtram pelos nomes antigos
precisam ser revistos. Se o seu Sentry é auto-hospedado, o SDK novo só dá
suporte à versão 26.4.2 ou mais nova. Os relatórios de erro continuam chegando
como antes.
+3 -2
View File
@@ -22,7 +22,7 @@ Stack canônica (major; a versão exata é o `package.json`):
Next.js 16 (App Router, Turbopack) · React 19 · TypeScript 6 estrito · Tailwind 4 (config em CSS) ·
shadcn/ui (`new-york`) · Supabase (Postgres + Auth + Realtime + Storage) · Zod 4 · Vitest 4 ·
Playwright 1 · Sentry 10 · WAHA 2026.7.2 (engine NOWEB) · Upstash Redis · Vercel AI Gateway
Playwright 1 · Sentry 11 · WAHA 2026.7.2 (engine NOWEB) · Upstash Redis · Vercel AI Gateway
(`@ai-sdk/anthropic|openai|google`).
> As majors acima são verificadas contra o `package.json` por
@@ -304,7 +304,8 @@ server; segredo em query string; `throw` cru na borda da API.
`react-hooks`, `typescript-eslint`. `next lint` foi removido no Next 16 — o script chama o CLI.
- **Prettier** com `prettier-plugin-tailwindcss`; classes Tailwind em ordem canônica.
- **Tailwind 4** — configuração em CSS (`app/globals.css`), não em `tailwind.config.js`.
- **Sentry** — `beforeSend` higieniza PII; `tunnelRoute: "/monitoring"` evita ad-blocker.
- **Sentry** — coleta restrita (`dataCollection`) + scrub num ponto só, `lib/sentry/privacidade.ts`,
provado pelo envelope do SDK em `privacidade.sdk.test.ts`; `tunnelRoute: "/monitoring"` evita ad-blocker.
- **Packaging (não-negociável; lei em [`docs/doctrine/packaging.md`](docs/doctrine/packaging.md))** —
nenhum serviço de `docker-compose.prod.yml` constrói na máquina do cliente: todo serviço declara
`image:` de imagem publicada, e `build:` existe só ao lado, como escape. Serviço `build:`-only é
+3 -5
View File
@@ -4,7 +4,7 @@
import * as Sentry from "@sentry/nextjs";
import { resolveSentryDsn, isCommunityDsn, integracoesDoCliente } from "./lib/sentry/dsn";
import { sentryScrubHooks } from "./lib/sentry/scrub";
import { opcoesDePrivacidade } from "./lib/sentry/privacidade";
const sentryDsn = resolveSentryDsn(
typeof window !== "undefined" ? window.__PUBLIC_ENV__?.SENTRY_DSN : undefined,
@@ -28,14 +28,12 @@ Sentry.init({
// ERRO continua, porque é o que explica o stack trace — e o replayIntegration()
// sem argumentos já aplica maskAllText/blockAllMedia.
tracesSampleRate: community ? 0 : 1,
enableLogs: true,
replaysSessionSampleRate: community ? 0 : 0.1,
replaysOnErrorSampleRate: 1.0,
sendDefaultPii: false,
...sentryScrubHooks,
// Coleta restrita + scrub, num ponto só (Sentry 11 coleta amplo por default).
...opcoesDePrivacidade,
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
+158
View File
@@ -0,0 +1,158 @@
// @vitest-environment node
/**
* PROVA PELO SDK, não pela função.
*
* `scrub.test.ts` chama o hook direto: fica verde mesmo que o `Sentry.init`
* nunca o chame, ou que o SDK mude o formato do que entrega a ele — foi o que o
* Sentry 11 fez com o span (`description`→`name`, `data`→`attributes`), e o hook
* antigo continuaria sendo chamado sem limpar nada.
*
* Aqui o `@sentry/nextjs` de verdade (entrada de servidor, a que o app e o
* worker usam) roda com as MESMAS opções dos quatro `Sentry.init`, e um
* transporte que captura o envelope no lugar da rede. O que se afirma é sobre o
* envelope: o que sairia da VPS.
*/
import * as Sentry from "@sentry/nextjs";
import { describe, expect, it } from "vitest";
import { opcoesDePrivacidade } from "./privacidade";
import { sentryScrubHooks } from "./scrub";
// Os literais ficam AQUI, longe das linhas que disparam: o evento carrega o
// código-fonte em volta de cada frame (`context_line`), e um literal escrito na
// linha do `expect` apareceria no envelope como se fosse vazamento.
const TOKEN = "wht_9f3a1c8b2e4d6a0f";
const EMAIL = "joao.titular@exemplo.com";
const CPF = "123.456.789-09";
const TELEFONE = "(11) 98765-4321";
const IP = "203.0.113.77";
const ASSINATURA = "ASSINATURA9";
const COOKIE = "valor-do-cookie-preferencia";
const CORPO = "corpo-da-mensagem-do-paciente";
const URL_WEBHOOK = `https://crm.exemplo.com/api/v1/webhooks/canal-x/${TOKEN}?sig=${ASSINATURA}`;
type Opcoes = Parameters<typeof Sentry.init>[0];
async function envelopeDe(opcoes: Opcoes, disparar: () => void): Promise<string> {
const enviados: unknown[] = [];
// O `init` do @sentry/nextjs é no-op se já houver cliente (`sdkAlreadyInitialized`),
// o que em produção é o certo — um init por processo — e aqui faria todo caso
// depois do primeiro medir as opções do primeiro. Solta o cliente anterior.
Sentry.getCurrentScope().setClient(undefined);
Sentry.init({
dsn: "https://chave@o0.ingest.sentry.io/0",
tracesSampleRate: 1,
transport: () => ({
send: async (envelope: unknown) => {
enviados.push(envelope);
return {};
},
flush: async () => true,
}),
...opcoes,
});
Sentry.withIsolationScope((escopo) => {
// O que a instrumentação HTTP do SDK põe no escopo a cada requisição: é
// daqui que o `requestDataIntegration` monta `request`, `cookies` e o IP.
escopo.setSDKProcessingMetadata({
normalizedRequest: {
url: URL_WEBHOOK,
method: "POST",
query_string: `sig=${ASSINATURA}`,
headers: {
"x-forwarded-for": IP,
authorization: `Bearer ${TOKEN}`,
cookie: `preferencia=${COOKIE}`,
"content-type": "application/json",
},
cookies: { preferencia: COOKIE },
data: JSON.stringify({ texto: CORPO, email: EMAIL, cpf: CPF }),
},
ipAddress: IP,
});
disparar();
});
await Sentry.close(2000);
// Sem isto, um transporte nunca chamado deixaria verde todo caso que só
// afirma ausência — a sonda cega lê igual a "nada vazou".
expect(enviados.length, "o SDK não entregou envelope nenhum ao transporte").toBeGreaterThan(0);
return JSON.stringify(enviados);
}
const erroComDadoDoTitular = () => {
Sentry.captureException(
new Error(`falha para ${EMAIL}, cpf ${CPF}, tel ${TELEFONE} em ${URL_WEBHOOK}`),
);
};
const spanDeWebhook = () => {
Sentry.startSpan(
{
name: `POST /api/v1/webhooks/canal-x/${TOKEN}`,
attributes: {
// Como a instrumentação HTTP marca a rota não parametrizada. Com `url`, o
// SDK tira o nome do cabeçalho do envelope (`trace.transaction`), que o
// `beforeSendSpan` não alcança; ver o NÃO MEDIDO do PR.
"sentry.segment.name.source": "url",
"url.full": URL_WEBHOOK,
"http.request.header.x-canal-api-key": ["segredo-do-canal"],
},
},
() => undefined,
);
};
describe("o que sai da VPS, medido no envelope do SDK", () => {
it("controle: sem a nossa configuração, o dado do titular SAI — a sonda enxerga", async () => {
// Sem este caso, um envelope vazio (transporte que nunca é chamado) deixaria
// todos os outros verdes pelo motivo errado.
const envelope = await envelopeDe({}, erroComDadoDoTitular);
expect(envelope).toContain(EMAIL);
expect(envelope).toContain(TOKEN);
expect(envelope).toContain(CORPO);
expect(envelope).toContain(COOKIE);
expect(envelope).toContain(IP);
});
it("erro com as opções dos quatro inits: nenhum dado do titular nem credencial", async () => {
const envelope = await envelopeDe(opcoesDePrivacidade, erroComDadoDoTitular);
expect(envelope).toContain("[EMAIL]"); // o evento saiu, e saiu limpo
for (const vazamento of [EMAIL, CPF, TELEFONE, TOKEN, ASSINATURA, IP, COOKIE, CORPO]) {
expect(envelope, `vazou ${vazamento}`).not.toContain(vazamento);
}
});
it("span de webhook com as opções dos quatro inits: nem token nem header-credencial", async () => {
const envelope = await envelopeDe(opcoesDePrivacidade, spanDeWebhook);
expect(envelope).toContain("[TOKEN]"); // o span saiu, e saiu limpo
expect(envelope).not.toContain(TOKEN);
expect(envelope).not.toContain(ASSINATURA);
expect(envelope).not.toContain("segredo-do-canal");
});
it("controle do span: sem o beforeSendSpan, o token do path sai", async () => {
const envelope = await envelopeDe({}, spanDeWebhook);
expect(envelope).toContain(TOKEN);
});
// As duas camadas são provadas SEPARADAS: com as duas juntas, apagar uma
// deixaria o caso de cima verde pela outra.
it("só a coleta restrita, sem scrub: o SDK não anexa cookie nem IP", async () => {
const envelope = await envelopeDe(
{ dataCollection: opcoesDePrivacidade.dataCollection },
erroComDadoDoTitular,
);
expect(envelope).toContain(EMAIL); // controle: sem scrub a mensagem sai crua
expect(envelope).not.toContain(COOKIE);
expect(envelope).not.toContain(IP);
});
it("só o scrub, com a coleta AMPLA do default: corpo, cookie e IP não saem", async () => {
// Medido: o `requestDataIntegration` anexa o corpo que está no escopo sem
// olhar `httpBodies`, então o corpo passa da coleta e é o scrub que o apaga.
const envelope = await envelopeDe(sentryScrubHooks, erroComDadoDoTitular);
for (const vazamento of [CORPO, COOKIE, IP, EMAIL, TOKEN]) {
expect(envelope, `vazou ${vazamento}`).not.toContain(vazamento);
}
});
});
+51
View File
@@ -0,0 +1,51 @@
/**
* O que o Sentry COLETA, num ponto só, para os quatro `Sentry.init` (servidor,
* edge, cliente e worker).
*
* No Sentry 10 bastava `sendDefaultPii: false`: omitir era restritivo. O 11
* removeu a opção e trocou por `dataCollection`, cujo default é o CONTRÁRIO —
* omitir coleta IP, cookies, corpo de requisição e de resposta, entrada e saída
* de IA, dados de query do banco e argumentos de fila (MIGRATION.md 11.0.0,
* "`sendDefaultPii` is replaced by `dataCollection`"). Numa instalação padrão o
* DSN é o Sentry da COMUNIDADE, então isso mandaria conversa de WhatsApp e dado
* de paciente de terceiros da VPS do cliente para a nossa conta (issue #100).
*
* Por isso a coleta é declarada inteira aqui, e não deixada ao default de
* nenhum SDK. A base é a que o guia documenta como "o comportamento do v10",
* com um aperto: `stackFrameVariables: false` (no v10 o default era `true`; hoje
* o `includeLocalVariables` segue desligado e nada muda, mas se alguém o ligar
* as variáveis locais — texto de mensagem, telefone — não saem).
*
* O scrub (`./scrub`) continua sendo a segunda camada: o `requestData` do SDK
* anexa o corpo que já estiver no escopo independentemente de `httpBodies`
* (que só barra a ESCRITA), e é o `beforeSend` que o apaga.
*/
import { sentryScrubHooks } from "./scrub";
// Lista do próprio guia para o "default do v10": casa por trecho do nome.
const CABECALHOS_DE_ENDERECO = ["forwarded", "-ip", "remote-", "via", "-user"];
const COLETA_RESTRITA = {
userInfo: false,
cookies: false,
httpHeaders: {
request: { deny: CABECALHOS_DE_ENDERECO },
response: { deny: CABECALHOS_DE_ENDERECO },
},
httpBodies: [],
urlQueryParams: { deny: CABECALHOS_DE_ENDERECO },
genAI: { inputs: false, outputs: false },
databaseQueryData: false,
queues: false,
graphQL: { document: false, variables: false },
stackFrameVariables: false,
};
/**
* Espalhe no `Sentry.init` de cada runtime. Coleta e scrub andam juntos de
* propósito: um init que tenha um sem o outro é o buraco que o #1743 teria aberto.
*/
export const opcoesDePrivacidade = {
dataCollection: COLETA_RESTRITA,
...sentryScrubHooks,
};
+27 -8
View File
@@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest";
import { scrubMessage, scrubUrl, sentryScrubHooks } from "./scrub";
// Issue #100. O que estes testes travam: com `tracesSampleRate: 1` e sem
// `beforeSendTransaction`/`beforeSendSpan`/`beforeBreadcrumb`, a URL crua saía do
// hooks de transação/span/breadcrumb, a URL crua saía do
// servidor do self-hoster em 6 campos (transaction, request.url, url.full,
// http.url, url.path, http.target). As rotas de webhook por tenant têm CREDENCIAL
// no path, e na instalação padrão esse token é a credencial inteira da rota,
@@ -198,8 +198,10 @@ describe("sentryScrubHooks", () => {
expect(JSON.stringify(event)).not.toContain(TOKEN);
});
it("beforeSendTransaction limpa os atributos de trace — o canal que não tinha guarda", () => {
const event = sentryScrubHooks.beforeSendTransaction({
// No Sentry 11 não há mais evento de transação (o hook é no-op), mas o evento
// de ERRO ainda carrega `transaction` e `contexts.trace.data`.
it("beforeSend limpa o nome da transação e os atributos de trace do evento de erro", () => {
const event = sentryScrubHooks.beforeSend({
transaction: `GET /api/v1/webhooks/in/${TOKEN}`,
request: { url: urlComToken },
contexts: {
@@ -219,13 +221,30 @@ describe("sentryScrubHooks", () => {
expect(JSON.stringify(event)).not.toContain("deadbeef");
});
it("beforeSendSpan limpa description e data", () => {
it("beforeSendSpan limpa name e attributes — o formato do span no Sentry 11", () => {
const span = sentryScrubHooks.beforeSendSpan({
description: `GET ${urlComToken}`,
data: { "url.full": urlComToken },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
name: `GET ${urlComToken}`,
is_segment: true,
attributes: {
"url.full": urlComToken,
"http.request.header.x-canal-novo-api-key": ["chave-de-integracao-futura"],
"http.request.header.user-agent": ["Mozilla/5.0"],
},
});
expect(JSON.stringify(span)).not.toContain(TOKEN);
expect(JSON.stringify(span)).not.toContain("chave-de-integracao-futura");
// Header que não é credencial fica: serve para depurar.
expect(span.attributes).toHaveProperty("http.request.header.user-agent");
});
it("beforeSend apaga corpo, cookies e usuário do evento", () => {
const event = sentryScrubHooks.beforeSend({
request: { url: "https://crm.exemplo.com/x", data: { cpf: "123" }, cookies: { a: "b" } },
user: { ip_address: "203.0.113.9" },
});
expect(event.request).not.toHaveProperty("data");
expect(event.request).not.toHaveProperty("cookies");
expect(event).not.toHaveProperty("user");
});
it("beforeBreadcrumb limpa a URL — o README prometia isso sem mecanismo", () => {
+74 -19
View File
@@ -14,7 +14,7 @@
/**
* Tipos estruturais mínimos, em vez de importar de `@sentry/core`.
*
* `SpanJSON` e `TransactionEvent` não são reexportados por `@sentry/nextjs`, e o
* `StreamedSpanJSON` e `Event` não são reexportados por `@sentry/nextjs`, e o
* `@sentry/core` é dependência TRANSITIVA — sob o node_modules estrito do pnpm ele
* não resolve a partir da raiz. Importar dele funcionaria na máquina de quem tem
* hoisting e quebraria no CI. Declarar só os campos que este arquivo toca mantém os
@@ -25,13 +25,21 @@ type EventLike = {
// `unknown` de propósito nos campos que o Sentry tipa mais largo que string
// (`query_string` é `string | Record<string,string> | Array<[string,string]>`).
// A checagem de `typeof === "string"` acontece em runtime, logo abaixo.
request?: { url?: unknown; query_string?: unknown; headers?: unknown };
request?: {
url?: unknown;
query_string?: unknown;
headers?: unknown;
data?: unknown;
cookies?: unknown;
};
user?: unknown;
transaction?: string;
contexts?: { trace?: { data?: Record<string, unknown> } };
message?: string;
exception?: { values?: Array<{ value?: string }> };
};
type SpanLike = { description?: string; data?: Record<string, unknown> };
/** Formato do span no Sentry 11 (streaming): `description` virou `name`, `data` virou `attributes`. */
type SpanLike = { name?: string; attributes?: Record<string, unknown> };
type BreadcrumbLike = { message?: string; data?: Record<string, unknown> };
/**
@@ -42,8 +50,13 @@ type BreadcrumbLike = { message?: string; data?: Record<string, unknown> };
* lista, e o arquivo passa a nomear provider — o que a doutrina de restrição de canal
* proíbe fora de `lib/channels/` (`docs/doctrine/restricao-de-canal.md`). Casar pelo
* que torna o header sensível cobre os dois casos de uma vez.
*
* Sensível é credencial OU endereço do titular: `x-forwarded-for`, `x-real-ip`,
* `cf-connecting-ip` e afins carregam o IP de quem acessou (a mesma lista que o
* guia do Sentry 11 usa como "default do v10": forwarded, -ip, remote-, via).
*/
const SENSITIVE_HEADER = /authorization|cookie|api[-_]?key|token|secret|password|credential/i;
const SENSITIVE_HEADER =
/authorization|cookie|api[-_]?key|token|secret|password|credential|forwarded|-ip\b|remote-|^via$/i;
export function isSensitiveHeader(name: string): boolean {
return SENSITIVE_HEADER.test(name);
@@ -133,8 +146,13 @@ export function scrubUrl(input: string): string {
return scrubMessage(withoutQueryValues);
}
/** Atributos de span/trace que carregam URL crua na convenção OpenTelemetry. */
/**
* Atributos de span/trace que carregam URL crua na convenção OpenTelemetry.
* `sentry.segment.name` é a cópia do nome do span raiz que o Sentry 11 põe em
* TODO span — limpar só o `name` deixava o token sair por ela (medido pelo SDK).
*/
const URL_ATTRIBUTES = [
"sentry.segment.name",
"url.full",
"url.path",
"url.query",
@@ -151,6 +169,30 @@ function scrubAttributes(data: Record<string, unknown> | undefined): void {
}
}
/**
* No span, header vira atributo `http.request.header.<nome>` (valor em array no
* Sentry 11). O mesmo padrão de `isSensitiveHeader` decide quais saem.
*/
const HEADER_ATTRIBUTE = /^http\.(request|response)\.header\.(.+)$/;
/**
* Atributo de span que é dado do titular, e não metadado: o corpo (o
* `requestData` anexa `http.request.body.data` com o que houver no escopo, sem
* olhar `httpBodies` — medido pelo SDK), o usuário e o IP de quem acessou.
*/
const TITULAR_ATTRIBUTE = /^(http\.(request|response)\.body\.data|user\..+|client\.address)$/;
function scrubSpanAttributes(attributes: Record<string, unknown> | undefined): void {
if (!attributes) return;
scrubAttributes(attributes);
for (const key of Object.keys(attributes)) {
const header = HEADER_ATTRIBUTE.exec(key)?.[2];
if ((header && isSensitiveHeader(header)) || TITULAR_ATTRIBUTE.test(key)) {
delete attributes[key];
}
}
}
function scrubHeaders(headers: unknown): void {
if (!headers || typeof headers !== "object") return;
const record = headers as Record<string, string>;
@@ -160,8 +202,9 @@ function scrubHeaders(headers: unknown): void {
}
/**
* Limpa os campos que carregam URL em QUALQUER evento — erro ou transação.
* O nome da transação entra aqui porque o `@sentry/node` puro não parametriza a
* Limpa os campos do evento de erro que carregam URL ou dado do titular. O
* evento de erro ainda traz `transaction` e `contexts.trace` no Sentry 11, e o
* nome da transação entra aqui porque o `@sentry/node` puro não parametriza a
* rota; só o wrapper do Next parametriza, e nem todo caminho passa por ele.
*/
function scrubEventUrls<T extends EventLike>(event: T): T {
@@ -173,7 +216,15 @@ function scrubEventUrls<T extends EventLike>(event: T): T {
if (typeof event.request.query_string === "string") {
event.request.query_string = scrubUrl(event.request.query_string);
}
// Corpo e cookies não saem, nem se o SDK os anexar: o `requestData` do
// Sentry 11 anexa o corpo que estiver no escopo sem olhar `httpBodies`
// (que só barra a escrita) — medido em `privacidade.sdk.test.ts`.
delete event.request.data;
delete event.request.cookies;
}
// Não chamamos `setUser`; o que chega aqui é o que o SDK INFERIU (IP). Se um
// dia for preciso identificar usuário no Sentry, é decisão de LGPD, não default.
delete event.user;
if (typeof event.transaction === "string") {
event.transaction = scrubUrl(event.transaction);
}
@@ -182,33 +233,37 @@ function scrubEventUrls<T extends EventLike>(event: T): T {
}
/**
* Os quatro hooks, prontos para espalhar dentro do `Sentry.init` de cada runtime.
* Espalhar o objeto inteiro é o ponto: adicionar um hook aqui cobre servidor, edge
* e cliente de uma vez, sem depender de alguém lembrar dos três arquivos.
* Os hooks, prontos para espalhar dentro do `Sentry.init` de cada runtime (via
* `opcoesDePrivacidade`, em `./privacidade`). Espalhar o objeto inteiro é o
* ponto: adicionar um hook aqui cobre todos os runtimes de uma vez.
*
* Não há `beforeSendTransaction`: no Sentry 11 o span é transmitido em
* streaming, não existe mais evento de transação, e o hook é no-op (MIGRATION.md
* 11.0.0, "Replacing `beforeSendTransaction`"). O que ele limpava — o nome da
* transação e a URL nos atributos — é o `name` e os `attributes` do span raiz,
* e o `beforeSendSpan` limpa todo span, raiz (`is_segment`) inclusive.
*/
export const sentryScrubHooks = {
beforeSend<T extends EventLike>(event: T): T {
scrubEventUrls(event);
// `scrubUrl`, não só `scrubMessage`: mensagem de erro carrega URL (erro de
// fetch, de rota), e o token do path saía nela — medido pelo SDK.
if (typeof event.message === "string") {
event.message = scrubMessage(event.message);
event.message = scrubUrl(event.message);
}
if (event.exception?.values) {
for (const ex of event.exception.values) {
if (ex.value) ex.value = scrubMessage(ex.value);
if (ex.value) ex.value = scrubUrl(ex.value);
}
}
return event;
},
beforeSendTransaction<T extends EventLike>(event: T): T {
return scrubEventUrls(event);
},
beforeSendSpan<T extends SpanLike>(span: T): T {
if (typeof span.description === "string") {
span.description = scrubUrl(span.description);
if (typeof span.name === "string") {
span.name = scrubUrl(span.name);
}
scrubAttributes(span.data);
scrubSpanAttributes(span.attributes);
return span;
},
+1 -1
View File
@@ -1,4 +1,4 @@
import { withSentryConfig } from "@sentry/nextjs";
import { withSentryConfig } from "@sentry/nextjs/config";
import type { NextConfig } from "next";
/** Performance budget (EPIC-12 §S-12.05):
+3 -4
View File
@@ -3,7 +3,7 @@
import * as Sentry from "@sentry/nextjs";
import { resolveSentryDsn, isCommunityDsn } from "./lib/sentry/dsn";
import { sentryScrubHooks } from "./lib/sentry/scrub";
import { opcoesDePrivacidade } from "./lib/sentry/privacidade";
const sentryDsn = resolveSentryDsn(process.env.SENTRY_DSN);
@@ -12,8 +12,7 @@ Sentry.init({
// No Sentry da comunidade, só erro (issue #100). Ver isCommunityDsn().
tracesSampleRate: isCommunityDsn(sentryDsn) ? 0 : 1,
enableLogs: true,
sendDefaultPii: false,
...sentryScrubHooks,
// Coleta restrita + scrub, num ponto só (Sentry 11 coleta amplo por default).
...opcoesDePrivacidade,
});
+3 -4
View File
@@ -4,7 +4,7 @@
import * as Sentry from "@sentry/nextjs";
import { resolveSentryDsn, isCommunityDsn, DEFAULT_SENTRY_DSN } from "./lib/sentry/dsn";
import { sentryScrubHooks } from "./lib/sentry/scrub";
import { opcoesDePrivacidade } from "./lib/sentry/privacidade";
const sentryDsn = resolveSentryDsn(process.env.SENTRY_DSN);
const community = isCommunityDsn(sentryDsn);
@@ -14,10 +14,9 @@ Sentry.init({
// No Sentry da comunidade, só erro (issue #100). Ver isCommunityDsn().
tracesSampleRate: community ? 0 : 1,
enableLogs: true,
sendDefaultPii: false,
...sentryScrubHooks,
// Coleta restrita + scrub, num ponto só (Sentry 11 coleta amplo por default).
...opcoesDePrivacidade,
});
// Transparência de telemetria: uma linha no boot dizendo o que está ativo e como
+5 -5
View File
@@ -32,12 +32,13 @@ import { StaleServiceBoundaryError } from "@/lib/atendimento/fronteira";
// Mesma lógica de `sentry.server.config.ts`/`sentry.edge.config.ts`
// (reaproveitada, não duplicada): DSN resolvido por `resolveSentryDsn`,
// amostragem de trace condicionada ao Sentry da comunidade via
// `isCommunityDsn` (issue #100), e os hooks de scrub de `lib/sentry/scrub.ts`.
// `isCommunityDsn` (issue #100), e a coleta restrita + scrub de
// `lib/sentry/privacidade.ts`.
// O `@sentry/nextjs` funciona fora do Next — aqui é só `Sentry.init` puro,
// sem `instrumentation.ts` porque o worker não é um processo Next.
import * as Sentry from "@sentry/nextjs";
import { resolveSentryDsn, isCommunityDsn, DEFAULT_SENTRY_DSN } from "@/lib/sentry/dsn";
import { sentryScrubHooks } from "@/lib/sentry/scrub";
import { opcoesDePrivacidade } from "@/lib/sentry/privacidade";
const sentryDsn = resolveSentryDsn(process.env.SENTRY_DSN);
const sentryCommunity = isCommunityDsn(sentryDsn);
@@ -47,10 +48,9 @@ Sentry.init({
// No Sentry da comunidade, só erro (issue #100). Ver isCommunityDsn().
tracesSampleRate: sentryCommunity ? 0 : 1,
enableLogs: true,
sendDefaultPii: false,
...sentryScrubHooks,
// Coleta restrita + scrub, num ponto só (Sentry 11 coleta amplo por default).
...opcoesDePrivacidade,
});
// Transparência de telemetria (mesma mensagem de sentry.server.config.ts,