Carrinhos abandonados sem nenhuma tabela nova: HogQL do PostHog

Como o PideAí detecta carrinhos abandonados por loja com uma query HogQL sobre eventos do PostHog: sem tabela, sem cron e sem sincronizar estado.

4 min de leitura

Compartilhar WhatsAppLinkedInX
Carrinho de compras em neon sobre um ícone de banco de dados, cercado por linhas de circuito e trechos de SQL

Alguns meses atrás precisei resolver algo que parecia simples: saber, para cada loja do PideAí, quantos clientes adicionam produtos ao carrinho e nunca finalizam o pedido.

O primeiro instinto foi o de sempre. Uma tabela abandoned_carts, um cron rodando a cada hora, lógica para marcar o carrinho como abandonado depois de X minutos sem pedido, e outro job para disparar o aviso pelo WhatsApp. Arquitetura conhecida, documentada e entediante.

Antes de escrever a primeira linha, me fiz uma pergunta: já tenho essa informação em algum lugar?

Tinha. Fazia meses que o PideAí mandava eventos para o PostHog. Toda vez que um cliente adicionava um produto ao carrinho de uma loja, saía um evento. Toda vez que finalizava o pedido, outro. Os dados já existiam; faltava um jeito de consultá-los juntos.

O que é o PostHog

O PostHog é uma plataforma open source de analytics de produto. Em resumo: ele mostra o que os usuários fazem dentro da sua aplicação.

Cada ação que importa (ver um produto, adicionar ao carrinho, abandonar o checkout, concluir um pedido) vira um evento com suas propriedades. Com isso você monta funis, assiste gravações de sessão e roda testes A/B sem trabalhar em cima de suposições.

A diferença para o Google Analytics é o foco. O Google Analytics diz quantas páginas alguém visitou; o PostHog diz exatamente o que a pessoa fez dentro do seu app.

O que pouca gente sabe, e que é a chave de tudo isso, é que o PostHog também deixa você fazer queries SQL sobre os seus eventos a partir da sua própria aplicação. Eles chamam isso de HogQL.

O custo da solução clássica

Ilustração de servidores, tabelas e relógios enroscados num emaranhado de cabos

Implementar carrinhos abandonados do jeito clássico não é difícil. O custo está no que quase ninguém menciona: a manutenção.

Você precisa manter o estado do carrinho no banco sincronizado com o que acontece no frontend. Se o usuário fecha o navegador, se a sessão expira, se dá erro de rede, a tabela fica desatualizada. Os cron jobs falham em silêncio. E no dia em que você quer um filtro novo, como “carrinhos abandonados acima de R$50”, mexe em queries, migrations e jobs.

Tudo isso para responder uma pergunta que, no fundo, é esta: houve um evento A que não foi seguido por um evento B?

HogQL: SQL sobre os seus eventos

HogQL é o dialeto SQL do PostHog. Ele não serve só para os gráficos da interface: você consulta via API a partir da sua aplicação e usa o resultado como se viesse do seu próprio banco.

No PideAí, em vez de manter uma tabela de carrinhos por loja, escrevi uma query que cruza dois eventos:

WITH cart_events AS (
  SELECT person_id, properties.cart_value, timestamp
  FROM events
  WHERE event = 'product_added_to_cart'
    AND properties.store_id = '{storeId}'
    AND timestamp >= now() - INTERVAL {days} DAY
),
completed_orders AS (
  SELECT DISTINCT person_id, timestamp
  FROM events
  WHERE event = 'order_placed'
    AND properties.store_id = '{storeId}'
    AND timestamp >= now() - INTERVAL {days} DAY
)
SELECT
  count(DISTINCT c.person_id) as total_abandoned,
  sum(toFloat(c.cart_value)) as total_value,
  avg(toFloat(c.cart_value)) as avg_cart_value
FROM cart_events c
LEFT JOIN completed_orders o
  ON c.person_id = o.person_id
  AND o.timestamp > c.timestamp
  AND o.timestamp <= c.timestamp + INTERVAL 24 HOUR
WHERE o.person_id IS NULL

A lógica é direta. Pega todos os clientes que adicionaram algo ao carrinho de uma loja e cruza com os que fizeram um pedido nas 24 horas seguintes. Quem não aparece nesse cruzamento é carrinho abandonado.

Sem tabela, sem job e sem nada para sincronizar.

Ilustração de um funil: cliques, carrinhos e pacotes entram de um lado e saem convertidos num gráfico em alta

Como fica no código

No React, o hook que alimenta o painel de cada loja ficou assim:

export function usePostHogAbandonedCartStats(storeId: string, days = 30) {
  return useQuery({
    queryKey: ['abandoned-cart-stats', storeId, days],
    queryFn: () => getAbandonedCartStats(storeId, days),
    refetchInterval: 5 * 60 * 1000, // atualiza a cada 5 minutos
    enabled: !!storeId,
  });
}

Agora cada loja vê quantos carrinhos foram abandonados, quanto dinheiro ficou neles, o valor médio e a taxa de recuperação, com dados que se atualizam a cada cinco minutos. E o banco de dados não recebeu nenhuma migration.

Notebook com um painel de métricas de vendas, usuários e receita

O que eu aprendi

Quase nunca falta infraestrutura. O que falta é fazer perguntas melhores para a que você já tem.

Se você já registra eventos com PostHog, Mixpanel ou qualquer ferramenta de analytics, provavelmente estão lá respostas que você procura em outro lugar. A diferença está em a ferramenta deixar você consultá-la a partir do seu app ou só entregar um dashboard fechado.

O PostHog deixa. No PideAí isso abriu uma categoria inteira de funcionalidades que construímos sem tocar no banco principal.

Então fica a pergunta que ficou na minha cabeça: quantas tabelas do seu banco existem só porque você não sabia que dava para consultar esse dado em outra ferramenta?