Pular para o conteúdo
< samuelsantana.dev />
Voltar para o BlogDiagrama: navegador, API e Postgres; o caminho /vendas/9f1c… vira /vendas/:id, o IP vira BR · BR-SP e o user agent vira desktop · chrome · windows; nunca guardado: cookie, IP, user agent, e-mail, CPF.

Pyxis: análise de uso sem cookie, sem IP e sem dado pessoal (e por que eu fiz a minha)

Samuel Santana
Publicado em 13 de outubro de 2026
SecuritySoftware ArchitectureTypeScriptAnalyticsPrivacy

Enquanto você lê este texto, esta página está sendo medida. Quando ela abriu, saiu um page_view com o caminho da página (e, se ela foi a primeira da sua visita, o domínio de onde você veio) e um article_viewed com o post e o idioma. Quando você passar de um quarto, da metade, de três quartos e do fim, sai um article_read_25, _50, _75 e _100, cada um com quantos segundos a aba ficou visível até ali. Se você clicar num item do sumário ou copiar um bloco de código, sai mais um. É isso. A medição não gravou nenhum cookie no seu navegador (se você não entrou para comentar, o único que vai achar é o NEXT_LOCALE, que guarda o idioma do site), seu endereço IP não foi guardado em lugar nenhum, e quando você fechar esta aba nada vai ligar esta visita à próxima.

Dá para conferir. Abra as ferramentas do navegador na aba Rede e filtre por batch: os eventos saem em lotes, a cada cinco segundos ou quando a aba fica oculta, num POST para api.pyxis-analytics.dev/v1/batch com um corpo em texto que você pode ler inteiro. No rodapé do blog há uma chave que desliga a medição neste navegador. E se o seu navegador manda o sinal Global Privacy Control ou Do Not Track, nada disso sai.

A ferramenta que faz isso é o Pyxis, que eu escrevi: três repositórios públicos, em produção desde 06/10/2026, medindo o BolsoVerde e, desde 10/10, este blog, que trocou o Google Analytics por ele. Este post conta por que eu construí em vez de instalar, o que ele guarda, o que nunca guarda, e como cada promessa de privacidade virou uma função com teste. As versões citadas são as de hoje, 13/10/2026: o rastreador pyxis-analytics 0.2.1, a API 0.13.0 e o painel 0.15.0.

O problema: visitas de anúncio e nenhuma ideia do que elas faziam

O BolsoVerde é um app de controle de vendas, taxas e repasses para pequenos vendedores. Quando ele passou a receber visitas de anúncio, eu não tinha como saber o que essas pessoas faziam: se usavam a calculadora da página inicial, se abriam o cadastro, se paravam nos Termos, se estavam no celular. Dentro do app, a mesma cegueira esperava os primeiros usuários: quais telas são usadas, onde uma gravação falha, em que ponto alguém desiste.

Eram sete perguntas, todas de produto:

  1. Quem visitou cada página, e de onde veio (anúncio, busca, link direto)?
  2. Quantos chegaram a ver um resultado da calculadora?
  3. Quantos abriram o cadastro, quantos enviaram, quantos concluíram?
  4. Quais funções do app são usadas de verdade?
  5. Quais requisições dão erro, quantas vezes e em que tela?
  6. O que uma pessoa fez, em ordem?
  7. Celular ou computador, qual navegador, qual sistema?

Por que construir em vez de instalar

A resposta padrão é instalar o Google Analytics, ou uma ferramenta de análise de produto como o PostHog. As duas respondem a quase tudo isso. O custo vem junto: por padrão, elas guardam no navegador um identificador que reconhece a pessoa por meses, e os dados vão para outra empresa. Para um app que lida com o dinheiro de pequenos vendedores, isso significava mais uma empresa na Política de Privacidade e uma medição que, na leitura mais comum da lei, dependeria de consentimento: só contaria quem aceitasse o banner, e eu ficaria olhando para uma amostra que se escolheu sozinha.

Também existem ferramentas de análise sem cookie, algumas de código aberto, para hospedar no próprio servidor. Eu poderia ter usado uma delas. Construí o Pyxis por três motivos:

  1. Perguntas de produto, não só de tráfego. Eu queria a falha de uma gravação junto com a tela de onde ela veio, o funil do anúncio até a primeira venda, e a linha do tempo de uma pessoa começando na visita anônima de antes do cadastro. E queria que tudo isso fosse apagado sozinho quando a conta é excluída.
  2. Nenhuma empresa nova recebendo dados. O Pyxis roda na mesma AWS, no mesmo Neon e na mesma Vercel que o BolsoVerde já usava e que a Política já citava.
  3. Eu queria decidir cada regra. O que sai do navegador, por quanto tempo fica guardado, o que nunca é lido. Numa ferramenta pronta, essas regras são de outra pessoa. Aqui elas são funções que eu escrevi, testei e publiquei, e qualquer um pode auditar.

E há um quarto, que é o mais honesto: eu queria saber construir isso, e queria que o resultado fosse público.

O que o Pyxis guarda, e o que nunca sai do navegador

O ponto de partida foi uma lista do que não pode existir no banco. O resto do desenho saiu dela.

O quêO que acontece
CookieNenhum. A visita é um id aleatório no sessionStorage: morre quando a aba fecha e é trocado depois de 30 minutos sem uso. Duas abas são duas visitas
Endereço da páginaReduzido a um modelo no navegador: /vendas/9f1c… vira /vendas/:id, e e-mails e sequências longas de dígitos nunca saem. A API refaz o modelo do lado dela
Query stringDescartada, menos utm_source, utm_medium e utm_campaign. O código de clique de anúncio vira só from_ad_click: true
Endereço IPLido na hora para limitar abuso, e descartado. Nada é gravado, nem um hash. País e estado vêm de cabeçalhos que o CloudFront deduz do IP; a cidade nunca é lida
User agentClassificado em tipo de aparelho, família do navegador e sistema, e descartado. Robôs que se anunciam são recusados
Propriedades de eventoUma barreira no servidor descarta qualquer valor com cara de e-mail, telefone, CPF ou CNPJ
"Não rastrear"Global Privacy Control e Do Not Track desligam o rastreador antes de qualquer coisa entrar na fila
Opt-outoptOut() grava um único marcador no localStorage, a única coisa que o rastreador guarda além da aba
Prazo13 meses. Um job diário apaga o que passou
Pessoa identificadaSó pelo id interno do site, nunca pelo e-mail. Uma chamada apaga os eventos dela, inclusive os da visita anônima em que ela entrou, e outra exporta

Esse conjunto tem um preço, e eu prefiro dizer qual: o Pyxis não sabe que a pessoa que usou a calculadora ontem é a mesma que criou a conta hoje. Sem um identificador que dure, duas visitas anônimas nunca se ligam. Uma pessoa só passa a ter histórico quando faz login, e a partir daí pelo id interno do site. Foi uma escolha: a alternativa era um id que dura meses no navegador, que é exatamente o que eu não queria guardar.

As regras são código, não texto de política

Uma política de privacidade promete. Quem cumpre é o código, e dá para mostrar que cumpre quando cada regra mora numa função pequena, pura e testada. A que decide se um trecho do caminho é um identificador, no rastreador, é esta:

const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const ALL_DIGITS = /^\d+$/;
const DIGIT = /\d/g;
const ENCODED_AT_SIGN = /%40/i;
const MIN_ID_DIGITS = 10;

function isIdentifier(segment: string): boolean {
  return (
    UUID_PATTERN.test(segment) ||
    ALL_DIGITS.test(segment) ||
    (segment.match(DIGIT)?.length ?? 0) >= MIN_ID_DIGITS ||
    segment.includes('@') ||
    ENCODED_AT_SIGN.test(segment)
  );
}

templatePath aplica isso a cada trecho e troca o que for identificador por :id, depois de tentar as regras que o site declarou (pathRules: ['/blog/:slug']). Os testes dizem a regra em voz alta: turns /vendas/9f1c… into /vendas/:id, turns /u/joao@x.com into /u/:id, handles a path made of thousands of slashes in linear time. O último existe porque o caminho é entrada de quem visita: uma regra de privacidade que trava com uma URL maliciosa também é um problema.

O "não rastrear" é a primeira coisa que o rastreador consulta:

export function trackingStatusOf(signals: PrivacySignals): TrackingStatus {
  if (signals.globalPrivacyControl || signals.doNotTrack) {
    return 'blocked-by-browser';
  }
  return signals.optedOut ? 'opted-out' : 'on';
}

O sinal do navegador vence a escolha feita na página, de propósito: uma chave no rodapé de um site não pode religar um "não rastrear" que a pessoa ligou no navegador inteiro.

O rastreador é só a primeira camada. A chave pública de um projeto está no HTML do site, como toda chave pública, e qualquer um pode mandar um lote direto para a API sem passar pelo rastreador. Por isso a API refaz o modelo de cada caminho com as mesmas regras, descarta propriedades com cara de dado pessoal e recusa o evento inteiro quando o id de usuário parece um e-mail ou um documento. O navegador é educado; o servidor desconfia.

Os três repositórios estão com 100% de cobertura de instruções, ramos, funções e linhas, e o CI falha abaixo disso. Cobertura não prova que um teste está certo, mas garante que nenhuma regra ficou sem ninguém olhando para ela.

Como é por dentro

navegador                    AWS, São Paulo                       painel
pyxis-analytics ── lote ──▶  CloudFront ◀───── consultas ──────── Next.js na Vercel
caminho em modelo                │                                login por código
sem cookie                       ▼                                sessão opaca
                             Lambda (NestJS)
                             valida cada evento,
                             descarta IP e user agent
                                 │
                                 ▼
                             Postgres (Neon)

O rastreador (pyxis-analytics no npm) tem cerca de 4,8 KB com gzip na versão 0.2.1, dentro de um orçamento de 5 KB que o CI mede a cada push, e nenhuma dependência. As regras ficam num núcleo puro, sem window nem document; o navegador entra por adaptadores pequenos (transporte, armazenamento, History API, relógio) que os testes trocam por versões falsas. O lote vai como text/plain e com credentials: "omit": assim o navegador não faz a requisição de preflight do CORS, o sendBeacon funciona quando a aba é escondida, e nenhum cookie de domínio nenhum viaja junto. Toda versão é publicada pelo GitHub Actions com procedência no npm, então dá para conferir de qual commit saiu o pacote que você instalou.

A API é NestJS sobre Fastify, num AWS Lambda atrás do CloudFront, na região de São Paulo, com Postgres no Neon e a infraestrutura em Terraform. Três decisões dela:

  • Cada evento é validado sozinho. Um evento inválido não derruba o lote: a resposta é 202 com quantos foram aceitos, quantos eram repetidos e quantos foram recusados. Um evento reenviado é reconhecido e nunca gravado duas vezes.
  • Limite de taxa sem guardar IP. São 120 lotes por minuto por endereço e 3.000 por projeto, contados em memória em cada ambiente de execução; nenhum contador vai para o banco. O endereço vem de um cabeçalho do CloudFront, que só é confiável porque o CloudFront manda junto um segredo que a função exige antes de fazer qualquer outra coisa. X-Forwarded-For é ignorado.
  • O painel lê os eventos crus, sem tabelas pré-agregadas. Num Postgres local com um milhão de eventos de um projeto, a visão geral de 30 dias responde em cerca de meio segundo. Para o tamanho dos projetos que o Pyxis mede, um job de agregação seria complexidade sem retorno.

Num teste de carga pelo CloudFront, 10.000 eventos em dez minutos foram aceitos sem nenhum erro, com p50 de 104 ms e p95 de 140 ms medidos no cliente, e p95 de 29 ms dentro do Lambda.

O painel é Next.js 16 na Vercel. O login é por um código de seis dígitos enviado por e-mail, trocado por uma sessão opaca que o servidor pode revogar a qualquer momento, inclusive em todos os aparelhos de uma vez. O navegador só vê um cookie HttpOnly do próprio painel, gravado por uma Server Action; quem conversa com a API é o servidor do Next.js. É o desenho que eu defendi num post de julho sobre JWT e sessões, agora num sistema em produção. As telas são visão geral, visitas, funil, funcionalidades, requisições, aparelhos, aquisição e a linha do tempo de uma pessoa, em inglês e português, e toda segunda chega por e-mail um resumo da semana. Os gráficos são SVG desenhados no servidor: nenhuma biblioteca de gráficos vai para o navegador.

O que dá para abrir agora

  • Demo do painel: as mesmas telas da produção, sem login, com os dados inventados de uma loja de exemplo.
  • Playground do rastreador: o rastreador de verdade em modo seco, mostrando cada lote que mandaria, sem nenhuma requisição sair da página.
  • Storybook do painel, com os componentes nos seus estados, e cada história testada também por acessibilidade.
  • Referência da API, gerada do contrato OpenAPI a cada push.
  • O código, com licença MIT e as decisões de arquitetura registradas em ADRs: pyxis-sdk, pyxis-api e pyxis-web. A integração deste blog também é pública, no vertex-web.

O que o BolsoVerde mede

Integrar um site é pequeno: uma chave pública, um init() no layout e um track() em cada momento que importa.

import { init, track, identify } from 'pyxis-analytics';

init({
  key: 'pyxis_pk_…',
  endpoint: 'https://api.pyxis-analytics.dev',
  pathRules: ['/vendas/:id'],
});

track('calculator_result_shown', { calculator: 'ifood' });
identify('id-interno-da-conta');

O BolsoVerde manda as páginas vistas, pelo modelo do caminho; ações com nome, como "viu um resultado da calculadora", "clicou em Criar conta", "criou a conta" e "registrou uma venda"; o resultado das gravações (sucesso ou erro, o código do erro e quanto tempo levou) e só as leituras que falharam; e, depois do login, o id interno da conta. Nunca o e-mail, nunca um valor digitado.

Quando alguém exclui a conta, o backend do BolsoVerde pede ao Pyxis que apague os eventos daquela pessoa, inclusive os da visita anônima em que ela entrou, e repete o pedido até ele dar certo. A cópia que a pessoa baixa em "Baixar Meus Dados" inclui esses eventos. A Política de Privacidade nomeia o Pyxis e descreve exatamente isso, e a ordem foi de propósito: a política entrou no ar antes de a chave ser configurada em produção. Uma política nunca descreve o que não roda, e o código nunca faz o que a política não diz.

Sem banner? A minha leitura

A medição do Pyxis não pede consentimento. No BolsoVerde, o banner de cookies que existe é o dos anúncios do Google, e o Pyxis não passa por ele; neste blog não há banner nenhum. A base que a Política do BolsoVerde declara é o legítimo interesse da LGPD, e o argumento é o desenho inteiro deste post: nenhum cookie, nenhum identificador que reconheça alguém em outro dia, IP e user agent nunca guardados, nenhuma empresa de análise de terceiros recebendo os dados, e, do outro lado, transparência na política, "não rastrear" respeitado e um jeito de desligar a medição.

Isso é a minha leitura, não um parecer jurídico. Quem for adotar algo parecido deve conferir com quem entende de proteção de dados, e lembrar que a conclusão vale para o conjunto, não para uma peça isolada.

Os limites

  • Duas visitas anônimas nunca se ligam, nem duas abas da mesma pessoa. É o preço do item mais importante da tabela.
  • As contagens são um piso. Um bloqueador pode recusar a requisição, e quem liga o "não rastrear" não entra na conta, como deve ser.
  • Robôs com cara de navegador passam. O Pyxis recusa os que se anunciam como robôs, mas um que se apresenta como um Chrome comum conta como visita.
  • É feito para projetos pequenos. As consultas leem os eventos crus; um projeto com milhões de eventos por mês vai precisar das tabelas agregadas que eu ainda não escrevi, e o arquivo que define cada métrica já diz o que cada uma delas teria que contar.

Algumas decisões do Pyxis vão virar posts próprios: o bug que só existia no Lambda, em que o Postgres devolvia uma lista como texto e o painel mostrava uma letra por linha; como o rastreador cabe em 5 KB; e por que comparar "hoje" com "ontem" às dez da manhã mostrava uma queda de 58% todo dia.

Se você chegou até aqui, esta página acabou de mandar um article_read_100, a menos que o seu navegador tenha pedido para não ser rastreado.

Comentários

Carregando comentários...