Skip to main content
track() registra o que alguém fez no seu produto. Todo o resto desta página existe porque a parte interessante não é a chamada — é o que acontece com o evento entre um navegador prestes a ser fechado e um gráfico que alguém lê na semana seguinte.

Uma chamada, e ela nunca é uma requisição

track() não devolve nada e não espera nada. O evento entra numa fila que mora no storage do navegador, e a fila sai em lotes — num intervalo de cinco segundos, no momento em que o navegador volta a ficar online, e no pagehide com keepalive, que é o flush que sobrevive à aba sendo fechada. Dois campos são cunhados na chamada, não no flush:
  • id, um UUIDv7. É o que faz uma reentrega ser contada uma vez só: um lote cuja resposta a página morreu antes de ouvir é enviado de novo, e os eventos que já tinham chegado não escrevem nada.
  • occurred_at, o relógio do cliente. Uma fila que esperou uma queda passar não pode datar tudo no minuto em que a rede voltou.
Um evento sai da fila somente depois do 202 da API. Uma falha de rede, um 500, uma tampa de notebook — o lote fica, e o próximo flush o leva. A fila guarda 500 eventos e descarta os mais antigos quando estoura: comportamento recente vale mais do que o que uma queda perdeu, e uma fila sem teto seria uma falha de quota de storage caindo em cima da sessão que divide esse storage com ela. Num servidor não há intervalo, nem online, nem pagehide. Chame flushEvents() você mesmo — é também o dreno explícito de um teste, ou de um “antes de você ir” seu.

Nomes

^[a-z0-9_.:-]{1,64}$, minúsculas. Um nome fora disso lança invalid_event_name na chamada em vez de ser descartado em algum lugar mais silencioso — um evento perdido em silêncio é descoberto num dashboard uma semana tarde demais. O prefixo $ é reservado, e track("$auth.signed_in") lança reserved_event_name. Essa recusa é o que torna os eventos $auth.* mais abaixo confiáveis: nada num navegador consegue escrever no namespace da plataforma. properties é um objeto JSON de no máximo 8 KiB — dimensões que o explorer agrupa, não um banco de documentos.

Propriedades que todo evento carrega

register() carimba um conjunto de propriedades em todo evento a partir dali — o que outras ferramentas chamam de super properties.
Existe porque a alternativa é lembrar. Um produto multi-tenant quer organization_id em todo evento, e uma propriedade que precisa ser escrita em trinta lugares é a propriedade que vai faltar exatamente no evento em que alguém precisava dela. O carimbo acontece na chamada, não no envio: um evento enfileirado antes de um register() descreve um momento em que aquilo ainda não era verdade. As propriedades do próprio evento vencem numa colisão — quem chamou sabe mais sobre aquele evento do que o registro sabia. unregister("plan") remove pelo nome. getRegistered() devolve uma cópia do que está sendo carimbado.
São limpas junto com a sessão, pela mesma razão que as feature flags e o time ativo são: a próxima sessão pode ser outra pessoa, e um organization_id que sobrevivesse a um logout não é um buraco no dado — é uma resposta errada dentro dele. Registre onde você chama boot(), não uma vez no topo da página.

Page views numa SPA

Um app de página única nunca carrega uma página de novo, então o navegador não tem nada a relatar e cada produto acaba escrevendo isso contra o próprio router — com um nome de evento próprio, que é como um produto fica sem conseguir comparar page views entre dois tenants.
Funciona com qualquer router: pushState e replaceState são embrulhados e popstate é escutado, que é tudo que existe. No <script>, data-pageviews="auto" liga a mesma coisa — e vem desligado nas duas formas, porque isso escreve no seu fluxo de eventos e não é uma decisão nossa. O evento carrega path, mais from numa navegação, e nunca a query string. Um page view é automático, então o que estiver na URL na hora vai para o analytics: um token de redefinição de senha, um ?code= de OAuth (este pacote coloca um lá). A atribuição também não precisa dela — utm_*, o referrer e o ref são capturados uma vez pelo first-touch. Não há title, e a ausência é a metade deliberada: um router troca a URL e renderiza depois, então document.title naquele instante ainda é o da página anterior. Um título errado é pior que um título ausente. O que você quiser além disso vai em register(), que é carimbado nestes também. Só conta quando o path muda: routers chamam replaceState para coisas que não são navegação — sincronizar um filtro, limpar um parâmetro — e contar isso infla todo produto que tem uma busca.

O que o 202 significa

Aceito, nunca durável. Os eventos ficam em memória de processo e são gravados em segundos; uma queda entre esses dois momentos perde o que foi aceito e ainda não gravado. É uma troca explícita, feita para eventos de analytics e para mais nada — um fato que seu produto não pode perder mora no seu backend, atrás de uma chave de API, não aqui. A validação é por evento. Uma linha ruim é reportada em rejected com seu índice e um código, e o resto do lote chega assim mesmo: um buffer offline reenviando cem eventos não pode perder noventa e nove por causa de um timestamp velho. Os códigos são invalid_id, reserved_name, invalid_name, properties_too_large, invalid_properties, invalid_occurred_at e occurred_at_out_of_range (occurred_at pode estar até 7 dias no passado — a janela do próprio buffer offline — e 5 minutos no futuro, para desvio de relógio). Um evento rejeitado é definitivo: o SDK o descarta junto com o lote, porque o servidor já o respondeu e uma nova tentativa só seria recusada de novo. Uma requisição carrega no máximo 100 eventos e 256 KiB, e a porta tem limite de 600 requisições por minuto por IP, atrás de um teto de 6000 por minuto por ambiente.

De quem é o evento

A chave publicável viaja no corpo, exatamente como no boot, e a lista de origens permitidas da chave é o portão. O ambiente segue a chave: uma uk_pk_test_… escreve eventos de teste. O anonymous_id do dispositivo sempre vai junto, então os eventos de um visitante são atribuídos antes de qualquer sign-in e se costuram ao contato que um dia aparece. Uma sessão de contato nomeia, e nunca barra:
  • com sessão, o lote resolve para aquele contato;
  • um token expirado ou revogado em pleno voo rebaixa o lote para anônimo em vez de perdê-lo;
  • um contact_id no corpo é ignorado. Um evento que nomeia alguém é uma afirmação, e só uma sessão pode fazê-la.
Com o @userkit/nextjs a mesma chamada passa pela sua própria origem — a chave fica fora do bundle e o cookie httpOnly assina o lote.

De qual time é o evento

Um produto B2B tem gente em mais de um time, e “o que essa pessoa fez” só é uma pergunta respondível quando se sabe dentro de qual conta. Por isso todo evento carrega também o time. No navegador, o time é o mesmo que o resto da sessão já usa: o X-Customer-Id que o SDK envia depois de você escolher um time, ou — sem cabeçalho — a associação mais antiga do contato. Nada de novo para chamar. Do seu backend, POST /v1/track aceita customer_id (o id da UserKit) ou customer_external_id (o id no seu sistema, o mesmo que espelha o time) por linha. Os dois juntos são ambiguous_customer; um time que não resolve naquele ambiente é unknown_customer e a linha é recusada em vez de gravada sem time — dizer qual time e ser ignorado é pior do que não dizer. Nenhum time é uma resposta, não um erro. Um produto sem times nunca preenche esse campo, e o evento fica sendo da pessoa: no painel ele aparece na leitura de qualquer time dela, marcado como “Sem time”. Eventos gravados antes desta coluna existir são exatamente esse caso — não há adivinhação retroativa, porque atribuir hoje um fato antigo é datá-lo a uma relação que pode não existir na época.

O funil que você não instrumenta

A UserKit publica o que ela mesma viu na mesma pipeline, sob o prefixo reservado. Um funil que começa no cadastro não precisa de nenhum track() seu. Eles são transcritos do barramento interno, não escritos pelos handlers — é por isso que uma nova porta de entrada não consegue esquecer de produzir um. Duas consequências que vale conhecer: eles são duráveis no commit e chegam quando o barramento drena (segundos, não milissegundos), e visitantes anônimos nunca aparecem — o widget cria uma sessão por carregamento de página e nenhuma delas é uma conta. $auth.signed_in carrega method e deliberadamente não carrega IP nem user agent. O explorer responde como, nunca de onde; de onde veio uma sessão é pergunta da trilha de auditoria, e ela responde a uma permissão mais estreita.

O explorer

Analytics → Eventos no painel, atrás da permissão analytics:read (owner e admin por padrão — as mesmas telas mostram receita, então não há um assento de “só as contagens”; conceda numa role customizada se seu analista não for admin). Duas leituras moram ali. A lista crua filtra por nome do evento, contato e período. O gráfico conta eventos por dia UTC e nome, e diz qual fonte respondeu:
  • uma janela de 48 horas ou menos lê os eventos crus, porque é exatamente onde o atraso de até uma hora do rollup seria a coisa na tela;
  • qualquer janela maior lê o rollup horário, porque reagregar meses de linhas cruas a cada refresh é o custo que o rollup existe para pagar uma vez.
As duas aplicam as mesmas regras de agregação, então qual delas respondeu nunca muda o que um número significa — a resposta diz qual foi porque uma conversa de suporte sobre um gráfico começa por aí. Um filtro que vale a pena guardar vira uma query salva: um nome e o próprio formulário de filtros do explorer, guardados por organização e não por ambiente. A pergunta “onde as pessoas desistem” é a mesma em live e em test; o ambiente é escolhido na hora de perguntar de novo.

Funis

Um funil é uma lista ordenada de nomes de evento (de 2 a 10) e uma janela em dias.
Um contato entra quando seu primeiro evento do passo um cai dentro do período perguntado, e alcança o passo n fazendo cada passo em ordem, a sequência inteira no máximo window_days depois de entrar — a janela ancora no passo um, então um passo do meio generoso não estica o funil indefinidamente. O período delimita quem entra, nunca quem termina: quem entrou no último dia ainda tem sua janela para converter. Um funil conta contatos, então enxerga os eventos que resolveram para um — o contato de uma sessão anônima incluído, já que um funil sobre visitantes é uma pergunta legítima. Eventos que carregam apenas um anonymous_id estão no explorer e não num funil. Os passos podem nomear os eventos $auth.* da plataforma e os seus na mesma lista — essa mistura é o ponto, e é o gráfico que nenhuma ferramenta que só enxerga a sua instrumentação consegue desenhar. Os resultados voltam como uma lista densa: todos os passos, inclusive os que ninguém alcançou, porque um zero é algo que o gráfico precisa desenhar e não um buraco que ele disfarça. conversion é sempre contra o primeiro passo — “de quem entrou” é a pergunta que um funil responde, e a razão de um passo para o seguinte está a uma divisão de distância no cliente, enquanto o contrário perde precisão.

De quem é a meia-noite

Os dias são UTC no armazenamento — para as contagens, para os rollups e para as métricas de receita igualmente. De quem é a meia-noite de um dia se decide uma vez, no armazenamento, e o fuso da sua organização entra na exibição. É por isso que dois gráficos vindos de duas dessas leituras podem ser sobrepostos e concordar.

Por quanto tempo os números ficam

Seu plano define uma retenção para o analytics — 30 dias no Free, 365 no Pro — e ela vale em dois lugares, o que convém saber antes de montar um relatório em cima destas leituras. Na leitura. Toda resposta de analytics traz um objeto range dizendo sobre qual janela ela de fato respondeu:
clamped é verdadeiro quando o plano empurrou o from para frente. Pedir mais nunca é erro e nunca é 402 — você recebe 200 sobre a janela que o plano guarda, e o range diz isso. Leia o clamped em vez de comparar timestamps por conta própria: só a API sabe qual horizonte ela aplicou. retention_days é null quando a leitura enxergou tudo o que está armazenado. No armazenamento. Além desse horizonte os eventos são apagados toda noite, junto com as contagens diárias e as coortes semanais derivadas deles. Isto não é igual à janela da trilha de auditoria do painel, que só filtra uma leitura: aqui, mudar para um plano mais longo dá um horizonte maior dali para a frente, e não o histórico que você tinha antes. Se você precisa guardar analytics além da retenção do seu plano, exporte — o explorador de eventos é a leitura de onde puxar. As métricas de receita são a exceção. Elas são congeladas a partir das suas faturas, e não derivadas de eventos, então só são recortadas na leitura, nunca apagadas.

Onde os números viram dinheiro

Métricas de receita — MRR, os quatro baldes de movimento, LTV, NRR — e receita por canal de aquisição são a outra metade deste módulo, e vêm de assinaturas e faturas, não de eventos.