Ingestão

Situação no projeto

Implementado em backend/controller/evento/criar.rs e backend/model/evento.rs. É a única rota sem token: fica em router_publico().

Objetivo

Receber muito evento com resposta rápida. A rota só valida e enfileira; a gravação e as agregações acontecem depois, numa tarefa em segundo plano.

Entradas e saídas

PontoDescrição
EntradaPOST /evento/ingestao com a chave, os dados da sessão e a lista de eventos, em JSON ou gzip
Saída202 com a contagem de aceitos e recusados
Próximo passoA fila grava eventos, sessão, erros e uso
{
  "chave": "pk_live_…",
  "sessao": {
    "sessao_externa": "…", "anonimo_id": "…", "usuario_id": null,
    "navegador": "…", "sistema_operacional": "…", "dispositivo": "…",
    "resolucao": "…", "pagina_inicial": "…"
  },
  "eventos": [
    { "evento_id": "…", "tipo": "ERRO_JS", "nome": "…", "mensagem": "…", "ocorrido_em": "…" }
  ]
}
json

Os demais campos do evento são opcionais: pilha, arquivo, linha, coluna, url, metodo, status, duracao_ms, nivel, seletor, texto, metrica, valor, usuario_id e payload.

Etapas

1. Leitura do corpo

Corpo com content-encoding: gzip é descompactado. Corpo ilegível responde 400.

2. Chave

A chave é procurada entre os aplicativos não excluídos. Chave desconhecida responde 401.

3. Origem

A origem vem do Origin ou, na falta, do Referer, sem protocolo, caminho e ponto final. Ela passa se for igual a um domínio do aplicativo, se tiver o mesmo host ignorando a porta ou se casar com um curinga *.raiz. A origem do próprio painel (URL_FRONTEND) sempre passa. Fora isso, 403.

Por que o CORS é aberto

A chave roda no site do cliente e é pública. Quem autoriza a ingestão é essa conferência da origem contra os domínios do aplicativo, não o CORS, que aceita qualquer origem para POST e OPTIONS.

4. Limites

RegraResultado
Lista de eventos vazia202 com zero aceitos, sem criar sessão
Mais de 500 eventos no lote ou tipo inválidoO excedente e os inválidos viram recusados
Mais de 6.000 eventos por minuto na chaveLote inteiro recusado e registrado no uso, 429
Franquia estourada com PARAR_INGESTAOLote inteiro recusado e registrado no uso, 402

O limite por minuto fica na memória do processo do backend.

5. Sessão

A sessão é procurada pelo par aplicativo_id + sessao_externa e criada se não existir; um índice único resolve lotes simultâneos. Os dados de uma sessão já existente não são atualizados. O país vem do cabeçalho cf-ipcountry ou x-vercel-ip-country, senão fica DESCONHECIDO. O IP não é guardado.

6. Fila

O lote entra num canal com um único consumidor e a rota responde 202. O número de aceitos conta todos os válidos, inclusive PULSO.

Processamento em segundo plano

  • PULSO não é gravado: só marca atividade na sessão e não conta no uso.
  • O id do evento é um UUIDv5 de aplicativo_id:evento_id e a gravação usa ON CONFLICT DO NOTHING. Reenvio do mesmo lote não duplica.
  • Campos longos são cortados: mensagem 2.000, pilha 8.000, url e arquivo 2.000, seletor 500, texto 200, nome 120 caracteres.
  • IDENTIFICACAO grava o usuario_id na sessão.
  • ERRO_JS, PROMISE_REJEITADA e CONSOLE com nivel = error entram em Erros .
  • DESEMPENHO com metrica = CARREGAMENTO grava o tempo_carregamento_ms da sessão.
  • No fim do lote a sessão soma quantidade_eventos e quantidade_erros, ultima_atividade_em só avança, o uso do dia é somado, os avisos de franquia são conferidos e primeiro_evento_em é preenchido se estiver vazio.

Retenção

Uma tarefa roda a cada hora (a primeira, uma hora depois do boot) e apaga de verdade o que passou de 30 dias:

TabelaCritério
sessaoultima_atividade_em antiga. Os eventos dela caem junto em cascata
eventocriado_em antigo
erroultima_ocorrencia_em antiga

A tabela uso não passa por retenção. Fora daqui, toda exclusão é lógica (deletado = true).

Atualizado em 2026/10/07 00:30