1

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.

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.

Carregando publicação patrocinada...