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.
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.
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.
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.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
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 noboot, 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_idno corpo é ignorado. Um evento que nomeia alguém é uma afirmação, e só uma sessão pode fazê-la.
@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: oX-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 nenhumtrack() 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ãoanalytics: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.
Funis
Um funil é uma lista ordenada de nomes de evento (de 2 a 10) e uma janela em dias.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 objetorange 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.