O Navistron possui um sistema de telemetria pública que captura dados de todas as sessões de jogo — registradas e anônimas — e os apresenta em dashboards acessíveis a qualquer visitante. O diferencial: nenhuma informação pessoal é coletada. Sem IP, sem cookies, sem fingerprinting, sem contas. Neste artigo, vamos explorar cada camada do sistema: coleta, armazenamento, processamento e visualização.
Os Três Caminhos dos Dados: Registrado, Unregistered e Unknown
Quando uma partida termina no Navistron, a função endGame() cria um objeto pendingSession com 7 campos de gameplay: score, tier, tierName, boosts, spread, time e difficulty. Esse objeto fica em espera até que o jogador tome uma ação. Dependendo do que acontece a seguir, os dados seguem um de três caminhos:
- Registrado — O jogador digita um nome e clica "Save & Ranking". Os dados vão para a collection
scorescom o nome do piloto. Uma flagsessionResolved = truemarca a sessão como resolvida. Nenhum registro anônimo é criado. - Unregistered — O jogador clica "Skip". Os dados são salvos na collection
anonymous_sessionscomtype: "Unregistered"viafetchpadrão. O jogador deliberadamente optou por não se registrar. - Unknown — O jogador fecha a aba ou inicia um novo jogo sem resolver a sessão pendente. Os dados são salvos com
type: "Unknown". Se o evento ébeforeunload, usanavigator.sendBeacon()para garantir envio mesmo durante o fechamento da página.
Dessa forma, nenhuma partida é perdida: toda sessão gera dados de telemetria, seja no ranking público ou nas estatísticas anônimas.
sendBeacon: Capturando Dados ao Fechar a Aba
O navigator.sendBeacon() é uma API do navegador projetada especificamente para enviar dados de telemetria durante o fechamento de uma página. Diferente do fetch tradicional, o sendBeacon é "fire-and-forget" — o navegador garante o envio mesmo que a aba esteja sendo destruída.
O Navistron usa o sendBeacon no handler de beforeunload: quando o jogador fecha a aba com uma sessão pendente, os dados são serializados em um Blob com tipo application/json e enviados para /api/anonymous-sessions. Isso captura jogadores que simplesmente abandonam o jogo sem clicar em nenhum botão — uma parcela significativa que seria invisível sem essa técnica.
Para as ações de "Skip" e "novo jogo sem resolver", o salvamento usa fetch assíncrono normal, já que a página continua aberta e pode aguardar a resposta.
As 4 Collections do MongoDB
O banco de dados navistron no MongoDB utiliza 4 collections para telemetria:
- scores — Partidas registradas com nome do piloto. 9 campos: name, score, tier, tierName, boosts, spread, time, difficulty, playedAt. Alimenta o ranking e perfis de jogador.
- anonymous_sessions — Partidas não registradas. 9 campos: type (Unregistered/Unknown), score, tier, tierName, boosts, spread, time, difficulty, playedAt. O campo
typeé validado no servidor — valores fora de "Unregistered" e "Unknown" são rejeitados e convertidos para "Unknown". - sponsors — Dados de patrocinadores: name, link, linkText, value, totalClicks, createdAt. O campo
totalClicksé incrementado atomicamente a cada clique. - sponsor_clicks — Log de eventos de clique em links de patrocinadores: sponsorId, clickedAt, userAgent e referer. Esta é a única collection que armazena User-Agent, e apenas para análise de cliques em patrocinadores.
Um Único Fluxo de Partidas: $unionWith Entre as Duas Collections
O coração da telemetria é o módulo src/lib/stats.js. Em vez de consultar scores e anonymous_sessions separadamente e somar os resultados em JavaScript, ele monta um único pipeline que começa em scores, aplica os filtros, e anexa a outra collection com $unionWith. Um $addFields normaliza cada documento: sessões anônimas e registros antigos salvos como "ANONYMOUS" recebem name: 'ANÔNIMO' e registered: false. A partir daí, toda métrica é calculada sobre o mesmo fluxo de partidas.
Sobre esse pipeline base rodam, em paralelo via Promise.all(), as aggregations que alimentam a página:
- Resumo — Total de partidas, registradas e anônimas (
$sumcom$condsobreregistered), tempo total e médio, score médio e máximo, primeira e última partida do recorte - Pilotos únicos —
$grouppor nome apenas entre registrados - Partidas por dia ou por mês —
$dateToStringcomtimezone: 'America/Sao_Paulo', separando registradas e anônimas, com zero-fill. Recortes acima de 92 dias agrupam por mês - Horários de pico —
$hourno fuso de Brasília, 0 a 23 - Distribuição de tiers e faixas de score (
$bucketde 0–49 até 50K+) - Ranking por piloto —
$grouppor nome com melhor score, melhor tier, partidas, tempo total e média; posição via$setWindowFields+$rank; ordenação e paginação com$facet - Lista de partidas — todas as sessões individuais, incluindo anônimas, ordenáveis e paginadas
- Partidas recentes — as 10 últimas dentro do recorte
Cada combinação de filtros é cacheada por 2 minutos com unstable_cache, então a visão padrão continua tão barata quanto uma página estática, e recortes específicos pagam o custo da query só na primeira visita.
Uma Página Só: Filtros na URL
Toda a telemetria mora em /stats. Não existem mais páginas separadas por período nem um dashboard à parte — as rotas antigas (/stats/daily, /stats/monthly, /stats/yearly, /dashboard, /ranking/weekly, /ranking/monthly) redirecionam permanentemente para a página única com o filtro equivalente. Os filtros vivem na query string e são validados no servidor por parseStatsFilters(), que descarta valores inválidos e volta aos padrões:
- periodo —
24h,7d,30d,365doutudo; ou um intervalo livre comdeeate(datas no fuso de Brasília) - tipo —
todos,registradosouanonimos - tier (0–6) e piloto (busca por nome, sem distinção de maiúsculas)
- visao —
pilotos(ranking agrupado) oupartidas(cada sessão em uma linha) - ordem, dir e pagina — ordenação por coluna e paginação de 50 em 50
A barra de filtros é um pequeno Client Component que apenas monta a URL e navega; os gráficos (Chart.js) são outro. Todo o resto — cards, tabelas, cabeçalhos ordenáveis, paginação — é renderizado no servidor com links comuns, então a página funciona mesmo sem JavaScript. Ela inclui JSON-LD com schema schema.org/Dataset apontando para /api/stats como distribuição em JSON.
Gráficos: 4 Visualizações com Chart.js
- Partidas por dia (ou por mês em recortes longos) — Barras empilhadas separando registradas e anônimas
- Horário de pico — Distribuição das partidas pelas 24 horas do dia, no fuso de Brasília
- Distribuição de scores — Barras por faixa de pontuação
- Tiers alcançados — Barras horizontais com a cor de cada tier
Os gráficos respeitam os filtros ativos: filtrar por "anônimas" ou por um piloto redesenha tudo com o recorte escolhido.
Tracking de Patrocinadores: Cliques e Relatórios
O sistema de telemetria também acompanha a interação com patrocinadores. Quando um jogador clica em um link de patrocinador (no leaderboard in-game), duas operações são executadas no servidor:
- O campo
totalClicksdo documento do patrocinador é incrementado atomicamente ($inc: { totalClicks: 1 }) - Um evento de clique é registrado em
sponsor_clickscom: sponsorId, clickedAt, userAgent e referer
O painel admin (protegido por senha) apresenta relatórios com: receita total, total de cliques, cliques dos últimos 7 e 30 dias, tabela de cliques por patrocinador, e gráfico de cliques por dia.
Stats API: Os Mesmos Dados em JSON
A API GET /api/stats usa exatamente o mesmo módulo da página e aceita os mesmos parâmetros de URL. /api/stats?periodo=7d&tipo=anonimos devolve o resumo, os dados dos gráficos, o ranking paginado e as partidas recentes daquele recorte, com cabeçalho Cache-Control de 2 minutos. É o endpoint que o schema Dataset declara como distribuição dos dados abertos.
Privacidade: Zero Dados Pessoais
A telemetria do Navistron foi projetada com privacidade por padrão:
- Sem IP — Nenhuma API armazena endereço IP do jogador
- Sem cookies — Nenhum cookie de rastreamento é emitido pelo jogo
- Sem fingerprinting — Nenhum identificador de dispositivo, canvas fingerprint ou localStorage de tracking
- Sem contas/login — O nome do piloto é voluntário e digitado do zero a cada partida
- User-Agent apenas em cliques de patrocinador — A única informação semi-identificável é o User-Agent, armazenado exclusivamente na collection
sponsor_clickspara análise de dispositivos em cliques de links externos - Timestamps server-side — Todos os
playedAteclickedAtsão gerados no servidor comnew Date(), nunca confiando em dados do cliente - Dados públicos — Todas as estatísticas são acessíveis publicamente nas páginas /stats
FAQ — Perguntas Frequentes sobre a Telemetria do Navistron
Meus dados pessoais são coletados?
Não. A telemetria coleta exclusivamente dados de gameplay: score, tier, boosts, spread, tempo e dificuldade. Nenhuma informação pessoal — IP, email, localização, identificador de dispositivo ou cookie — é armazenada. O único dado "pessoal" é o nome de piloto, que é voluntário, pode ser qualquer texto, e só é registrado se o jogador clicar em "Save & Ranking".
O que aparece como "ANÔNIMO" na telemetria?
Toda partida que não foi registrada com nome: quem clicou "Skip", quem fechou a aba (capturada via sendBeacon) e quem começou um novo jogo sem decidir. O banco ainda guarda internamente se a sessão foi Unregistered ou Unknown, mas a interface não faz distinção — para o público, é tudo "anônimo". Partidas antigas salvas com o nome vazio ("ANONYMOUS") também são tratadas como anônimas.
Os dados de telemetria são públicos?
Sim. Todas as estatísticas são acessíveis em /stats, com filtros por período, tipo, tier e piloto, e em JSON via /api/stats. A página inclui metadata schema.org/Dataset para indexação como dados abertos.
Como os horários de pico são calculados?
O pipeline agrupa todas as partidas do recorte — registradas e anônimas — pela hora do campo playedAt usando $hour com timezone: 'America/Sao_Paulo', produzindo uma distribuição de 0 a 23 no horário de Brasília. Horários sem partidas são preenchidos com zero. O resultado é exibido como gráfico de barras na telemetria, revelando quando a comunidade mais joga.
O sendBeacon garante 100% de captura?
Não com 100% de certeza. O sendBeacon é "best-effort" — funciona na grande maioria dos navegadores modernos, mas pode falhar em navegadores muito antigos ou em situações extremas (crash do navegador, kill forçado do processo). Ainda assim, a taxa de captura é significativamente superior a fetch em beforeunload, que frequentemente é cancelado pelo navegador.
