Diário de desenvolvimento: separar visitas, prévias de links e falhas de analytics
No Plopino, que estou desenvolvendo para compartilhar arquivos HTML por link, a página inicial e o HTML enviado por alguém não são a mesma superfície de medição. Essa diferença acabou definindo a implementação do analytics.
Quero registrar três decisões do código atual e os limites delas. Não são uma receita para contar “pessoas reais” com precisão.
1. Separar pageview de abertura de conteúdo
O script do Umami está nas páginas do produto. Já o conteúdo HTML enviado pelo usuário é servido sem inserir esse tracker. Para essa segunda superfície, o servidor emite um evento chamado board-view.
A regra que mantive é: o servidor não emite também um pageview para essa abertura. E no relatório, não somo pageview + board-view como se fossem visitantes únicos. São sinais de superfícies diferentes, com limitações diferentes.
No servidor de arquivos, o registro depende da resposta final: 2xx e 304 podem contar; HEAD, redirecionamento de diretório e respostas como 404 ficam fora. Além disso, o callback ignora recursos que não sejam páginas. Isso evita tratar cada CSS ou imagem como uma visita ao conteúdo.
2. Um GET pode ser só uma prévia de link
Quando o contador sai do navegador e vai para o servidor, entram requisições de robôs, prévias de mensagens e ferramentas de diagnóstico. O filtro atual reconhece assinaturas de User-Agent e descarta UA vazio. Há um caso explícito para sondas do Globalping, além de ferramentas como curl.
Isso é uma heurística, não uma prova de humanidade. Um robô pode usar UA de navegador; uma regra ampla pode excluir tráfego legítimo. Também não concluo que toda execução do script no navegador seja humana. Por isso separo as contagens e procuro uma sequência de uso, como abrir o produto, publicar e copiar o link.
3. O upload não deve esperar pelo analytics
O envio de eventos fica fora da espera da resposta principal. Mas escrever apenas “void sendEvent()” não trata uma rejeição: a função chamada precisa absorver a falha esperada.
Este trecho reduzido mostra o padrão usado no reporter, omitindo montagem do payload e filtros:
async function sendEvent(endpoint, payload, fetchImpl = fetch) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetchImpl(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
return response.ok;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
Na implementação do produto também registro avisos para resposta não OK e exceções. Os pontos de chamada de upload e board-view não aguardam o reporter. O timeout limita a tentativa; o finally libera o timer mesmo quando a resposta chega normalmente.
A contrapartida é explícita: esse envio é best effort. Não há fila durável nem confirmação de processamento do evento. Se o processo terminar, um evento pode ser perdido. Para contabilização financeira ou auditoria, eu precisaria de outro desenho.
Como verifiquei o comportamento
Reexecutei os 9 testes existentes do módulo com fetch injetado, sem acessar o serviço real. Eles cobrem filtro de UA, configuração ausente, payload e cabeçalhos, dados adicionais, bloqueio de robôs, normalização da URL, HTTP não OK/exceção, abort por timeout e limites de tamanho dos campos. Todos passaram. O teste de timeout usa um limite curto; o padrão do reporter é 5 segundos.
Esses testes verificam o código, não a qualidade da identificação de pessoas nem a entrega em produção. A parte que continua exigindo cuidado é interpretar o relatório: um evento de upload não equivale automaticamente a um usuário novo, e a mesma ação pode produzir sinais no cliente e no servidor.
Esse é o estado atual da implementação no Plopino. A lição que levo para outros projetos é escrever primeiro o que cada contador significa e só então decidir onde coletá-lo.