Executando verificação de segurança...
31

Criei uma API para substituir o Vercel Analytics

No começo do mês eu criei a publicação Economizando com "analytics", com base num artigo onde o autor experimentou criar uma API de analytics própria, alugando um VPS, e ficou mais barato do que os custos que ele tinha com a Vercel. No fim do artigo, o autor mencionou que por baixo dos panos a Vercel utiliza o Tinybird:

Imagine substituir todos os usos da "Vercel" pelo Tinybird.

Eu decidi experimentar.

Nunca mexi com Analytics, então foi uma experiência 100% nova para mim. Decidi não ir pelo caminho da VPS para evitar um trabalho adicional, como ele descreveu ao longo do artigo. Experimentei o Tinybird: a coleta, envio e busca dos dados. Além disso, pedi a ajuda de vocês para visitarem o site, assim eu teria mais dados para avaliar o custo do processamento.

Não fiz isso só para aprender sobre analytics. Hoje, o TabNews tem um gasto de aproximadamente US$ 50 com o Analytics da Vercel, então esse teste poderia resultar numa economia bem vinda ao TabNews, além de ajudar a resolver um outro issue: contar e exibir as visualizações de conteúdos. Documentei o processo para compartilhar com vocês aqui.

Repositório: Rafatcb/testes-next.

Vou resumir os passos de implementação e focar na avaliação do resultado. Qualquer dúvida, podem ver o repositório, ler os artigos que eu citar ou comentar aqui.

Integração com a Vercel

Iniciei um projeto com Next.js para ter um ambiente similar ao do TabNews. A integração com a Vercel é simples. Você pode consultar a documentação de deploy e de analytics.

No código, só precisei adicionar a dependência @vercel/analytics e renderizar o componente <Analytics />. Você pode ver em app/layout.tsx.

Uma solução própria

Aqui é um pouco mais complicado, porque precisamos coletar os dados pelo cliente, chamar uma API para enviar os dados e criar o código dessa API para receber e salvar os dados. Vamos por partes.

Coletando os dados

Para o código do cliente, me baseei no artigo mencionado no início. Em resumo:

  • Criei um hook useAnalytics para centralizar a funcionalidade num único lugar.
  • Criei um componente Analytics que chama o hook, para podermos usar o useAnalytics indiretamente dentro de um componente renderizado pelo servidor.
  • Gerei um ID com nanoid porque é pequeno. Armazenei no Local Storage, assim cada navegador terá um "id de sessão", e podemos identificar visitas únicas através dele. Outra opção é considerar o IP. Cada abordagem tem seus pontos positivos e negativos.
  • Escutei os eventos visibilitychange, pagehide e beforeunload para salvar quando o usuário sai da página (troca de aba, por exemplo) e quando volta. Se isso não é relevante para você, não precisa escutar esses eventos.
  • navigator.sendBeacon é mais adequado para o envio de analytics do que fetch.

O código não está perfeito. Um problema que percebi é que o código não detecta o evento de "saída" ao fechar a aba no navegador Safari em um iPhone. Outros cenários que testei funcionaram bem. Veja a implementação em app/hooks/useAnalytics.tsx.

Criando a API

Precisamos de um endpoint para receber os dados do cliente. Esses dados não são 100% confiáveis, porque alguém pode pegar o endpoint e enviar qualquer coisa, então vamos obter apenas o necessário.

Para esse teste, decidi coletar o seguinte:

  • request_path: página visitada.
  • session_id: identificador do visitante. O objetivo não é rastrear o visitante, mas sim conseguir identificar visitantes únicos.
  • referrer: de qual site o visitante veio, antes de entrar no seu site.
  • type: page-view se é uma visualização inicial, page-leave se o usuário saiu da página (foi para outra aba do navegador, ou outra janela, ou mesmo saiu do site) e page-return se o usuário está voltando para o seu site (voltou da outra janela, ou aba). Se você só quer gravar visitas, não precisa do type.

Outros dados que decidi armazenar e obtive diretamente pelo lado do servidor foram:

  • timestamp_utc: horário da visita. Você pode armazenar também o horário local com base no local do visitante, ou com base na sua região de interesse (por exemplo, GMT-3 para o Brasil), mas lembre-se de considerar o horário de verão e outras peculiaridades.
  • country: país do visitante. A Vercel disponibiliza isso pelo header X-Vercel-IP-Country. Também é possível descobrir pelo IP.
  • browser, browser_version, engine, engine_version, os, os_version, device, device_vendor, device_model e cpu_architecture: é possível obtê-los através dos dados recebidos no header user-agent, e a biblioteca ua-parser-js facilita isso. Você pode testar com seu navegador em uaparser.dev. Acho que apenas o browser, os e device são relevantes, mas talvez os outros valores tenham utilidade para seu caso de uso.
  • ip: armazene apenas se achar útil, eu o coletei para confirmar que é possível.

Estamos em um cenário onde você tem o controle dos dados que deseja salvar, então salve apenas o que for útil, e se perceber que não está usando algum dado ou que precisa de algo novo, pode mudar a tabela depois.

Você pode ver a implementação da rota em app/api/analytics/route.ts.

A implementação ficou bem simples. O publishPageView é responsável por salvar no banco de dados, que nesse caso é o Tinybird, mas poderia ser outra plataforma, ou diretamente no banco de dados em uma VPS. Mesmo que eu opte por mudar, não precisarei alterar o código da rota.

Salvando os dados no Tinybird

Aqui eu segui as orientações deste artigo e a documentação do Tinybird.

O Tinybird tem um conceito de data sources e pipes. Em resumo, um data source é como uma tabela SQL, e um pipe é como um SELECT. Então, criei duas pastas: lib/tinybird/datasources para data sources e lib/tinybird/endpoints para pipes.

Criei arquivos para eles e usei o CLI para subir para o Tinybird porque assim posso manter essas informações na base de código, de forma versionada, e pessoas sem acesso ao Tinybird podem ver como uso os dados. Para o processo de instalação, autenticação e criação do projeto Tinybird pela CLI, veja a documentação. Eu adicionei .tinyb e /.venv no .gitignore.

Você pode ver a implementação do data source em lib/tinybird/datasources/page_views.datasource.

Para subir para o Tinybird, navegue para a pasta lib/tinybird e use:

tb push datasources/*.datasource

O código que utiliza o data source para publicar os dados está em lib/tinybird/publish.ts. É uma chamada de API simples. O token pode ser gerado no site do Tinybird, no seu workspace, de forma bem restrita. Eu o criei com Read habilitado para todos os pipes, e Append para o datasource:

Permissões do token

Você pode visualizar os dados pelo site do Tinybird, então não precisaria das permissões para os pipes, e nem criar funções em JS para consultá-los pela API. O Tinybird também permite criar gráficos no site, mas não testei isso.

Antes de criar o pipe no código e subir para o Tinybird, você pode gerar alguns dados e usar o playground no site para escrever a query do pipe.

Playground

Eu criei vários pipes, mas usei apenas dois, um para consultar visitantes online e outro para visualizar dados similares aos disponibilizados pelo Vercel Analytics, que é mais complexo e está em lib/tinybird/endpoints/visitors_summary.pipe.

Apesar desse pipe retornar visitantes únicos e visualizações, ele ordena por visitantes únicos, então a implementação poderia ser melhorada recebendo um parâmetro para usar a ordenação desejada. Dados entre {{}} são query parameters.

Para subir para o Tinybird:

tb push endpoints/*.pipe

O código para realizar a consulta ao pipe também é bem simples, você pode ver em lib/tinybird/pipes.ts.

Criei uma página para visualizar os dados, com o objetivo de testar a integração com o Tinybird e criar uma página similar à de Analytics da Vercel. A chamada no código com Next.js é bem simples, você pode ver em app/analytics/page.tsx. Esse é um componente rederizado pelo servidor, então o usuário não consegue inspecionar o fetch e nem ler o token da API.

Visualização dos dados

O site da Vercel é simples, mas tem alguns filtros:

Vercel Analytics

Para comparação, veja abaixo como ficou a página que desenvolvi. Como fiz apenas para testes, não implementei os mesmos filtros que a Vercel.

Visualização própria

O Referrer que salvei não está tratando corretamente para salvar apenas o domínio do site. Isso é algo que pode ser melhorado; eu não procurei entender o porquê disso acontecer.

Resultados

Comparei os resultados das visitas que o site recebeu entre os dias 10/08 e 18/08.

Assim como no primeiro artigo citado, a contagem ficou um pouco diferente da Vercel. Às vezes contando mais, às vezes menos. A Vercel contou 87 visitantes para 1.018 visualizações, e meu código contou 96 visitantes para 1.292 visualizações. Se não me engano, o Brave Shield e uBlock Origin bloqueiam por padrão as requisições de analytics da Vercel, então leve isso em consideração.

Outras diferenças:

  • Na Vercel não é possível identificar visitantes sem referrer na tabela de referrer (quem acessou o link diretamente).
  • A Vercel separa Chrome, Chrome Mobile e Chrome Mobile iOS, mas o código que usei deixou todos como Chrome. Apesar disso, ambos analytics separaram Safari e Mobile Safari.
  • Meu código identificou um browser "LinkedIn", mas a Vercel não.

Preço/recursos utilizados

Na Vercel é fácil de precificar, as 1.000 visitas ficaram dentro do plano Hobby, e se fossem mais de 2.500 visitas, eu precisaria do plano Pro. No Tinybird, precisamos avaliar o consumo de processamento e armazenamento, mas que dado o baixo volume de acessos, ficou gratuito.

O GIF abaixo mostra o que consumi no Tinybird. O que foi antes do dia 10 era o período que eu estava desenvolvendo e testando a plataforma sozinho.

12.22MB processado, 30.1KB armazenado

  • Requests/day: são as requisições que eu fiz pela API aos pipes. Requisições diretamente pelo site do Tinybird não contam. Esse limite só existe no plano gratuito.
  • Processed data/month: Custo de US$ 0.07 por GB processado. As consultas pelo site não contam, apenas pela API. Para calcular os dados da página /analytics com o Tinybird, consumi 0.7MB de processamento para cada atualização (Read query API (/sql)), enquanto que Data source operations é para a ingestão de dados.
  • Storage/month: Custo de US$0.34 por GB armazenado no total no final do período (você pode apagar dados, transferir, agrupar num outro data source e depois apagar etc.).

Resolvi realizar mais visitas no site, deixando alguns navegadores indo para a "próxima página" com um código em JavaScript. Veja o antes e depois:

page-viewpage-leavepage-returnArmazenamento (+materialized view)Processamento por ingestãoProcessamento por consulta
1.37327424219.08KB (30.1KB)1.1MB0.7MB
38.100807772410.69KB (530KB)16.6MB15.6MB
  • Linhas na data source: +2.000%
  • Tamanho da data source: +2.050%
  • Tamanho da data source + materialized view: +1.660%
  • Processamento de escrita: +1.409%
  • Processamento por consulta ao /analytics: +2.128%

O aumento da Materialized View (vou falar sobre isso no próximo tópico) acabou sendo alto porque ela faz uma agregação de visitas únicas por página num intervalo de 15 minutos, então como o navegador sempre ia para a próxima página, acabou que agregou pouca coisa.

Observando os dados acima, posso afirmar que um site com 30 mil visitas mensais não teria custos no Tinybird tão cedo (a não ser que você consulte com muita frequência). Já na Vercel, o plano Hobby não atenderia, então custaria US$ 20 pelo plano Pro (por membro) + US$ 14 pelos eventos adicionais, totalizando US$ 34 mensais. Nesse plano os dados ficariam retidos por apenas 12 meses, enquanto que no Tinybird você tem o controle sobre quanto tempo reter os dados.

Vale a pena lembrar que é bom você ter algum controle contra ataques DDoS e similares, porque além disso atrapalhar suas análises com dados inúteis, pode gerar um custo bem alto tanto na Vercel quanto no Tinybird.

Ao final desses testes, a página /analytics ficou assim:

Analytics com mais de 30k visitas

Economizando com Materialized View

Conforme a documentação, uma materialized view é uma forma de melhorar o desempenho de um endpoint (pipe), como uma maneira de pré-agregar e pré-filtrar grandes fontes de dados de forma incremental. Conforme há ingestão de dados no data source, a materialized view pode agregar os dados da forma como você planeja consultar.

O Tinybird não cobra pela população inicial da materialized view, isto é, quando você a cria e os dados existentes do data source são ingeridos por ela. Os custos incrementais de escritas, que ocorrem a cada nova inserção no data source que será agregado na materialized view, é cobrado como "processamento".

Veja como ficou o consumo da materialized view que criei durante as ~38 mil inserções:

5.1MB processado

Apenas 5.1 MB de dados foram processados. Essa materialized view é bem simples, e está disponível no repositório. O código do data source dela:

SCHEMA >
    `request_path` String,
    `session_id` String,
    `visit_time` DateTime,
    `visit_count` AggregateFunction(countDistinct, String)

ENGINE "AggregatingMergeTree"
ENGINE_PARTITION_KEY "toYear(visit_time)"
ENGINE_SORTING_KEY "request_path, visit_time, session_id"

E o pipe de ingestão:

NODE aggregated_visits_mv

SQL >
    SELECT
      request_path,
      session_id,
      date_trunc('minute', toStartOfFifteenMinutes(timestamp_utc)) AS visit_time,
      COUNTDistinctState(session_id) AS visit_count
    FROM page_views__v1
    WHERE type = 'page-view'
    GROUP BY request_path, session_id, visit_time
    ORDER BY visit_time ASC

TYPE materialized
DATASOURCE aggregated_page_views_mv

Ela serve para agregar as visitas únicas (com base no session_id) por página em intervalos de 15 minutos, ou seja, se a mesma pessoa visitar a página duas vezes nesse período, vou considerar uma visita, mas se visitar novamente depois, considero duas visitas. Parecido com o Stack Overflow.

Fiz isso para entender o custo de ingestão da materialized view, seu custo de processamento e o custo de processamento sem materialized view, assim consigo ter uma noção se isso poderia ser usado no TabNews para contar as visitas de uma publicação.

A query normal:

  • Busca: 2.89MB processados, (39.85k linhas x 1 coluna) em 7.74ms

A materialized view:

  • Busca: 173.91KB processados, (8.19k linhas x 1 coluna) em 2.47ms
  • Processamento de ingestão: 5.1MB

Nesse exemplo, com duas consultas já compensa ter uma materialized view. Então, ela pode ser uma ótima forma de diminuir custos com o Tinybird.

Num site muito visitado, ainda faria sentido usar o Tinybird?

Eu mostrei os custos de armazenamento e processamento para cenários bem pequenos, de 1 mil a 40 mil visitas. Para ter noção num cenário maior, de 1 milhão de visualizações, vamos extrapolar os dados.

Na Vercel, 1 milhão de visualizações precisaria do plano Pro com Web Analytics Plus. Isso significa: US$ 20 pelo plano Pro (por membro) + US$ 50 dólares pelo Web Analytics Plus + US$ 20 dólares por 500 mil eventos extras (tem 500 mil inclusos), totalizando US$ 90 dólares por mês.

No Tinybird, fazendo uma regra de três (o que não é 100% certo, mas é uma estimativa), cheguei nos valores abaixo. Considerei apenas eventos page-view para simplificar a comparação.

  • 13.58MB de armazenamento por mês.
  • 435.7MB de processamento de ingestão por mês — considerando os dados anteriores, que 2.000% de aumento de linhas causou 1.400% de aumento de processamento de escrita, talvez o valor mais adequado aqui seja algo mais próximo de 2/3 de 435.7MB, que seria 290.5MB, mas vou considerar o pior cenário.
  • 409.5MB de processamento por requisição ao /analytics no primeiro mês.

Considerando um armazenamento por 24 meses, que é o que o Web Analytics Plus da Vercel disponibiliza, temos:

  • 325.9MB armazenados.
  • 435.7MB processados durante a ingestão — é cobrado apenas o processamento no mês, então esse valor não seria acumulado.
  • 9.8GB de processamento por requisição ao /analytics.

No 24º mês, se colocarmos uma média de 2 consultas por dia ao /analytics (60 no mês), gastaríamos US$ 0 com armazenamento (US$ 0,34/GB), US$ 41,16 com processamento (US$ 0,07/GB), e US$ 20 com o Pro da Vercel por invocar 1 milhão de funções, totalizando US$ 61,16/mês. É mais barato, mas precisa de cuidado nas consultas; lembrando que as consultas pelo próprio site do Tinybird não tem custo. Não sei se eles limitam algo com uma quantidade de dados grande assim, mas nos meus testes não encontrei uma limitação.

O processamento por requisição ao /analytics parece alto, mas isso pode ser melhorado ao:

  • Armazenar apenas os dados relevantes, ao invés de todas as colunas que armazenei.
  • Usar uma materialized view para agregar os dados por dia. Parece fazer sentido, mas não testei.
  • Melhorar a query de consulta. Eu só criei uma que funcionava e não verifiquei se era possível otimizar algo.

A query com a materialized view que mencionei no tópico anterior tinha um custo de processamento de aproximadamente 6% da query normal, então se aqui fosse igual, seriam 588MB por consulta. O custo de ingestão estaria distribuído ao longo dos meses, então acho que não seria relevante.

Conclusão

Se você usa o Vercel Analytics e quer reduzir o custo, faz sentido experimentar o Tinybird. Achei legal fazer esse experimento e compartilhar aqui no TabNews. Lembre-se do cuidado com ataques DoS/DDoS que podem ser uma torneira de gastos, caso você não crie proteções para isso.

Eu não precisei colocar o cartão de crédito em nenhum dos dois serviços para testar, mas também não cheguei a consumir um valor que teria cobrança. As cotas gratuitas do Tinybird são bem generosas.

Carregando publicação patrocinada...
2

Ótimo post, nunca tive experiência com analytics, mas ao ler o seu post deu vontade de testar o tinybird apenas por curiosidade kkkkkkkkk.

Sobre os custos, acredito que a galera do TabNews poderia avaliar se é válido trocar da Vercel para o TB.