# Operations Analytics — Master Implementation Guide (LLM-Ready)
> Analytics de site sem cookie: visualizações, visitas, páginas, origens,
> países, dispositivos, eventos e metas. O cliente cola um script de uma linha
> no `
` e ganha o retrato do tráfego — sem banner de consentimento,
> porque não há identificador persistente para consentir.
>
> Também mede **servidores MCP**: cada chamada de tool, com cliente, sessão,
> latência e erro, por um pacote que envolve o servidor uma vez (seção 10).
>
> Este arquivo é a documentação inteira do produto. Cole a URL no seu agente
> (Claude Code, Cursor, Windsurf, Codex, Copilot) e peça a integração.
> **Leia a seção 7 antes de prometer qualquer coisa**: boa parte do que um
> produto de analytics costuma ter não existe aqui, de propósito.
Worker (API, script e ingestão): https://analytics.worker.myoperations.click
Painel: https://analytics.dashboard.myoperations.click
## 0. Estado — leia antes de tudo
- **O worker está no ar**: serve o script, recebe os eventos e responde a API
de leitura descrita na seção 5.
- **O login é o do RiLiGar Auth**, com código por e-mail. A API de leitura
exige a sessão (`Authorization: Bearer `) e responde `401 UNAUTHORIZED`
sem ela. Cada conta vê só os próprios sites — ver seção 5.
- **Não há servidor MCP.** A seção 8 descreve o contrato decidido, não algo
que responda hoje.
- **Não há chave de API própria.** A credencial da API é a sessão do Auth;
`/tracker.js` e `/collect` são públicos. A única chave do produto é a
**chave de servidor** de um MCP (`ask_…`), que só grava chamadas (seção 10).
## 1. Modelo mental
Quatro substantivos, e é só isso:
- **Site** — um domínio medido. Tem `id` (UUID puro), `domain`, `name` e
`timezone`. O `timezone` decide onde um dia começa, e é o que agrupa a série
diária. Eventos só são aceitos do próprio `domain` e dos subdomínios dele.
- **Visualização** — um carregamento de página. **Não é guardada crua**: cada
uma incrementa, no instante em que chega, contadores diários por dimensão
(página, origem, país, dispositivo…). O banco cresce com o número de coisas
DISTINTAS que o site tem, não com o tráfego.
- **Visita** — um visitante distinto **dentro de um dia**. O visitante é um
hash de `segredo + dia + site + IP + user-agent`, calculado no servidor. O
dia entra no hash, então ele muda todo dia — e isso tem uma consequência que
você precisa saber antes de prometer qualquer coisa: **não existe
"visitante recorrente"** neste produto. Não é feature faltando — é o que
torna a contagem sem cookie.
- **Meta** e **evento** — uma meta é um resultado com taxa de conversão sobre
visitas; um evento é algo que aconteceu, com contagem. São listas separadas
na API: "8,5% clicaram no CTA" ao lado de "2,1% assinaram" convidaria a ler
o primeiro como conversão.
### O que uma "visita" NÃO é
Alguém que voltou na terça conta de novo. Alguém que trocou de rede no meio do
dia conta duas vezes. É a mesma definição que Plausible e Fathom usam, e é por
isso que nenhum deles reporta "usuários únicos no mês".
## 2. O script
Uma linha no `` de cada página. O `data-site` é o `id` do site, visível
em **Configurações** no painel:
```html
```
| Atributo | O que faz |
|---|---|
| `data-site` | **Obrigatório.** O UUID do site. |
| `data-spa` | `auto` ou `history` registram `pushState` e voltar/avançar; `hash` registra `#/rotas`. |
| `data-honor-dnt` | `true` respeita Do Not Track e Global Privacy Control. Reduz a contagem, de propósito. |
**`data-spa` é a armadilha silenciosa.** Sem ele, uma SPA reporta o primeiro
carregamento e mais nada — o que parece pouco tráfego, não configuração
faltando. React Router, Vue Router, SvelteKit e Next com navegação
client-side precisam de `data-spa="auto"`.
O script envia para `/collect` no mesmo host de onde foi servido; não há
endereço de envio para configurar.
### API do script
```js
analytics.trackEvent('cta-click') // evento: só contagem
analytics.trackGoal('signup') // meta: taxa de conversão sobre visitas
analytics.trackGoal('purchase', 4900) // meta com valor, em CENTAVOS inteiros
analytics.trackPageview() // manual, quando data-spa não serve
analytics.blockTrackingForMe() // opt-out do dono, só neste navegador
analytics.enableTrackingForMe()
```
Nomes de evento e códigos de meta: até 64 caracteres, letras, números, ponto,
hífen, dois-pontos ou sublinhado — `cta click` (com espaço) é recusado.
Tudo viaja por `navigator.sendBeacon` com corpo `text/plain`: sobrevive ao
fechamento da página e dispensa preflight de CORS. Ao sair de cada página o
script envia o tempo em que ela esteve **visível** — aba em segundo plano não
conta.
## 3. Janelas e períodos
A lista é fechada: **7, 30 ou 90 dias** (`?days=`). Qualquer outro valor
responde `400 VALIDATION_ERROR`.
**`?back=N`** volta N janelas inteiras (de 0 a 8): `?days=30&back=1` são os 30
dias antes dos últimos 30. A resposta diz o intervalo lido em `range`
(`from`, `to`, `back`).
Toda comparação ("vs. anterior") é contra a **janela anterior da mesma
largura**. Um delta sem base anterior volta `null`, e `null` **não é zero**:
crescimento a partir de zero não é "+100%", é uma comparação impossível.
`viewsDelta` e `visitsDelta` são percentuais; `bounceRateDelta` é em pontos
percentuais e `medianSecondsDelta` em segundos.
## 4. Números e o que eles significam
| Campo | Significado |
|---|---|
| `views` | Visualizações. Soma. |
| `visits` | Visitantes distintos por dia, somados na janela. Ver seção 1. |
| `bounceRate` | Percentual das visualizações que terminaram em **menos de 15s**. |
| `medianSeconds` | **Mediana** do tempo visível por página, estimada de um histograma diário. Mediana, não média: a maioria dura segundos, algumas vinte minutos, e a média descreve uma duração que quase ninguém viveu. |
| `share` | Percentual sobre o total da janela. |
**`null` nunca vira zero.** Sem amostra de tempo, `medianSeconds` e
`bounceRate` voltam `null`. Achatar os dois faria o produto afirmar coisas que
ninguém mediu.
### Filtros
`overview`, `pages`, `sources`, `audience` e `goals` aceitam filtros, que se
combinam com "e": `?country=US&device=mobile`. Os parâmetros são `page`,
`referrer`, `campaign`, `country`, `device`, `browser`, `system` e `action`, e
o valor é o `key` que a própria linha traz nas listas (`direct` para sem
origem, `unknown` para país desconhecido).
`action` é uma meta ou um evento: `?action=signup` recorta as visitas que
chegaram à meta `signup` naquele dia, e responde de onde elas vieram, que
páginas viram etc. No sentido contrário, `goals` filtrado responde as metas
e os eventos só das visitas do recorte (as metas de quem veio dos EUA).
`goals` também traz `series`, metas e eventos dia a dia.
Com filtro, o `overview` também traz `whole` (os números do site inteiro, para
comparação), `filters` (cada filtro com o rótulo de exibição) e `since` (o
primeiro dia com registro por visualização). O cruzamento vem de registros
por visualização e por meta ou evento, mantidos por **90 dias**, a maior
janela: eles só existem desde `since`, e um delta cuja janela anterior começa
antes disso volta `null`.
## 5. Rotas
Base: `https://analytics.worker.myoperations.click`. Leitura com `?days=7|30|90`.
```
GET /tracker.js o script
POST /collect ingestão (corpo JSON em text/plain) → 202
POST /collect/server chamadas de um MCP, com a chave de servidor → 202 (seção 10)
GET /overview conta: totais + um resumo por site
GET /sites → { items, page } (?domain=, ?limit=, ?offset=)
POST /sites { domain, name?, timezone?, kind? } → 201 (kind: web | mcp)
GET /sites/:id o site, na raiz
DELETE /sites/:id → { id, deleted: true } (apaga o histórico junto)
GET /sites/:id/overview totais, deltas, rejeição, mediana, série diária (+ filtros)
GET /sites/:id/pages → { items, page } (?search=, ?limit=, ?offset=, + filtros)
GET /sites/:id/sources origens + campanhas UTM + parcela marcada (+ filtros)
GET /sites/:id/audience países, dispositivos, navegadores, sistemas (+ filtros)
GET /sites/:id/goals metas (com taxa e valor) + eventos
GET /sites/:id/calls MCP: o relatório inteiro (+ filtros, seção 10)
GET /sites/:id/health está chegando? e o que deu errado (seção 11)
```
**Autenticação.** Tudo abaixo de `/overview` e `/sites` exige
`Authorization: Bearer ` — a sessão do RiLiGar Auth na aplicação
**Operations** (chave pública `pu_acf9e1121c9c90cfb22515d2e02c9db02c00baa45e799f2eec191091b3d21f17`,
que é pública por desenho). Para obter a sessão, os dois passos do Auth:
```
POST https://auth.worker.myinfrastructure.click/auth/code/start { email } X-API-Key: pu_…
↓ a pessoa lê um código de 8 caracteres no e-mail
POST https://auth.worker.myinfrastructure.click/auth/code/verify { email, code } X-API-Key: pu_…
→ { token, user, session } o `token` é o Bearer; vale 7 dias
```
O dono de cada site é o `sub` do token, nunca um campo do corpo. Um site de
outra conta responde `404`, igual a um que não existe. O domínio é único
**por conta**: `409 NAME_TAKEN` quer dizer que VOCÊ já o cadastrou.
Coleção é sempre `{items, page:{limit, offset, total, hasMore}}`. O `limit`
reportado é o **aplicado** (default 50, teto 200). Erro é sempre
`{error:{code, message}}` com `code` do enum fechado da stack, e **nunca viaja
com 200**.
`POST /collect` responde `403 FORBIDDEN` para um evento de outro domínio,
`404 NOT_FOUND` para um `site` inexistente e `202 {accepted:false}` para um
robô que se declara.
## 6. Busca e paginação
Só `pages` tem busca, e o motivo é uma regra: é a única lista do produto que
**cresce sem teto** — cada URL nova é uma linha nova. As outras têm teto real
(≈20 origens, ≈200 países, 3 dispositivos, meia dúzia de navegadores).
**A busca é no servidor.** Busca local sobre uma lista paginada responderia
"nenhum resultado" para uma página que existe na página seguinte.
## 7. Escopo — o que este produto NÃO faz
- **Visitante recorrente, coorte, retenção ou funil multi-sessão** — o hash
muda por dia (seção 1). Não prometa "usuários únicos no mês".
- **Cruzar dimensões além de 90 dias** — os filtros (seção 4) leem um
registro por visualização que vive 90 dias. Um cruzamento mais antigo que
isso não existe mais.
- **Identificar pessoa** — não há `identify()`, `userId` ou e-mail.
- **Gravação de sessão, heatmap, replay, scroll-depth, A/B test.**
- **"Ao vivo" em tempo real** — não há rota de visitantes agora.
- **SDK de site em npm** — um site se mede pelo script. O pacote
`@ciromaciel/analytics` é o instalador (`analytics web init`, seção 11) e o
código de MCP (`@ciromaciel/analytics/mcp`, seção 10).
- **Receita, carrinho, e-commerce** — uma meta carrega `value` em centavos, e
isso é tudo.
- **Exportar CSV, relatório por e-mail, alerta por limiar.**
- **Proxy de primeira parte** — o `src` do script é o worker, e só.
- **Filtrar robôs além dos que se declaram** — user-agents de automação
(`bot`, `crawler`, `headless`, `curl`…) são descartados; um robô que finge
ser navegador é contado.
## 8. Servidor MCP
**Ainda não existe** (seção 0). O contrato decidido:
- **Endereço:** `https://analytics.worker.myoperations.click/mcp`, para a conta
inteira — sem id no caminho.
- **Autorização por `scope` do token do RiLiGar Auth**, nunca por annotation;
`aud` e `issuer` conferidos em todo JWT.
- **Prefixo do produto no nome da tool** e **resultado em JSON**
(`content[0].text` é o mesmo objeto de `structuredContent`).
Catálogo previsto — todas de leitura:
| Tool | O que devolve |
|---|---|
| `analytics_list_sites` | Os sites da conta |
| `analytics_get_overview` | Totais e série diária de um site |
| `analytics_list_pages` | Páginas mais vistas, com busca |
| `analytics_get_sources` | Origens e campanhas |
| `analytics_get_audience` | Países, dispositivos, navegadores, sistemas |
| `analytics_get_goals` | Metas e eventos |
**Nenhuma escreve, por decisão de produto.** Analytics é o registro do que
aconteceu; uma tool que apagasse dados permitiria a um agente reescrever o
passado da conta. Cadastrar e excluir site ficam no painel e na API.
## 9. Checklist para agentes de IA
1. **Pegue o `data-site` em Configurações** — ou crie o site com `POST /sites`,
levando a sessão do Auth no `Authorization` (seção 5).
2. **Se o site é SPA, `data-spa="auto"` é obrigatório.** É a falha mais comum,
e ela é silenciosa.
3. **Teste do domínio de verdade.** Desenvolvimento local (`localhost`,
`.local`, `.test`, IP) nunca conta: o script nem envia, e o `/collect`
descarta com 202. Evento de outro domínio é recusado com 403.
4. **Não prometa visitante recorrente nem retenção.** Filtro cruzando dimensões
existe, mas só nos últimos 90 dias e desde `since`.
5. **Trate `null` como "não medido", nunca como zero.**
6. **Meta e evento são coisas diferentes.** Taxa de conversão só em meta.
7. **Valor monetário é inteiro em centavos.** `19.99` é recusado.
8. **Não invente rota nem tool.** A seção 5 é a lista inteira das rotas; a 8,
das tools previstas.
9. **Um MCP não tem script.** Ele se mede pelo pacote e pela chave de
servidor, e o relatório dele é `/calls` (seção 10).
## 10. Instalar num MCP
Um site com `kind: "mcp"` mede um **servidor MCP**: cada chamada de tool,
com o cliente (Claude, Cursor…), a sessão, a latência e o código de erro.
País não existe aqui: um MCP remoto é chamado da plataforma do cliente, e o
IP diria o data center, não a pessoa.
**1. O site.** No painel, Novo site → Servidor MCP, com o endereço público do
servidor (`api.meusite.com/mcp`). Pela API: `POST /sites` com
`{ "kind": "mcp", "domain": "api.meusite.com/mcp" }`. A resposta traz
`serverKey` (`ask_` + 64 hex), que também aparece em Configurações.
**2. O comando.** Na pasta do projeto do MCP:
```sh
npx @ciromaciel/analytics mcp init --key ask_…
```
Ele acha o arquivo com `new McpServer(`, instala `@ciromaciel/analytics` com o gerenciador do
projeto, guarda a chave onde o servidor roda (`wrangler secret put`,
`fly secrets import`, `vercel env add` ou `.env`), mostra a mudança de uma
linha e só salva com confirmação (`--yes` pula a pergunta). Pode rodar de novo.
**3. Ou à mão.** O que o `init` faz:
```js
import { withAnalytics } from '@ciromaciel/analytics/mcp'
const server = withAnalytics(new McpServer({ name: 'meu-mcp', version: '1.0.0' }))
// Cloudflare Workers (sem process.env): withAnalytics(new McpServer(…), { key: env.ANALYTICS_KEY })
```
Envolva **antes** de registrar as tools. A chave vem de `ANALYTICS_KEY`. O
envio é em segundo plano: a resposta da tool não espera, e com o Analytics
fora o MCP não percebe. Uma meta: `trackGoal(ctx, 'deploy-done')` dentro da
tool, com o `ctx` que ela recebe.
**4. Confira.** Publique o servidor e chame uma tool. Configurações do MCP
muda de "Aguardando a primeira chamada" para "Recebendo chamadas".
**Sem o pacote** (outra linguagem), um `POST` por chamada:
```
POST /collect/server
Authorization: Bearer ask_…
{ "type": "call", "tool": "deploy", "client": "claude-code", "protocol": "2026-07-28",
"session": "", "ms": 420, "error": null }
{ "type": "goal", "code": "deploy-done", "session": "" }
```
`error` é um código curto (`NOT_FOUND`) ou `null`. Mande a sessão já em hash:
o servidor faz um segundo hash com um sal diário, e o valor original nunca
deve sair do seu servidor. Sem sessão, cada chamada conta como uma sessão.
`protocol` é a revisão do protocolo da chamada (uma data), quando o cliente
diz; o pacote manda sozinho.
**O relatório** — `GET /sites/:id/calls?days=30` (e `back`, seção 3) devolve
`calls`, `sessions`, `errorRate`, `medianMs` e as variações (`callsDelta` e
`sessionsDelta` em %, `errorRateDelta` em pontos, `medianMsDelta` em ms),
`series` (chamadas, sessões, erros e sessões com meta, por dia), `tools` (com
`medianMs` e `errorRate` de cada uma), `clients`, `latency` (cinco faixas),
`outcomes` (sucesso e erro), `protocols`, `errors` (com `topTool`) e `goals`
(taxa sobre as sessões).
Filtros, que se combinam com "e": `tool`, `client`, `band` (a faixa de
latência, pelo `key` de `latency`), `outcome` (`ok` ou `error`), `protocol`,
`error` e `goal` (as chamadas das sessões que chegaram à meta). Em `client` e
`protocol`, `unknown` é "Não informado". Com filtro vêm também `whole`,
`filters` e `since`, como no `overview` de um site. As chamadas ficam
**90 dias**.
## 11. Instalar num site
**1. O site.** No painel, Novo site → Site, com o domínio publicado. Pela API:
`POST /sites` com `{ "domain": "meusite.com.br" }`. O `id` da resposta é o
`data-site`.
**2. O comando.** Na pasta do projeto do site:
```sh
npx @ciromaciel/analytics web init --site
```
Ele acha onde o `` mora e coloca o script lá:
| Projeto | Arquivo |
|---|---|
| Next.js (app router) | `app/layout.*`: `next/script` logo depois do `` |
| Next.js (pages router) | `pages/_document.*`, antes do `` |
| Vite, CRA, HTML puro | `index.html` (ou `public/`, `src/`), antes do `` |
| Astro | o layout em `src/layouts` que tem `` |
Se o projeto depende de um roteador no navegador (`react-router`,
`vue-router`, `next`, `@sveltejs/kit`…), o script vai com `data-spa="auto"`.
A mudança aparece antes de ser salva (`--yes` pula a pergunta), e rodar de
novo não duplica nada.
**3. Ou à mão.** O script da seção 2, no `` de cada página.
**4. Confira.** Publique e abra o site. Configurações muda de "Aguardando a
primeira visualização" para "Recebendo visualizações". Se não mudar,
`GET /sites/:id/health` diz o motivo:
```
{ "receiving": false, "received": 0,
"diagnostics": [ { "kind": "domain", "detail": "meusite.com", "count": 142, "lastSeen": 1790000000000 } ] }
```
| `kind` | O que aconteceu | Correção |
|---|---|---|
| `domain` | eventos de `detail`, que não bate com o domínio do site | cadastre `detail` como site, ou confira onde o script foi colado |
| `spa` | o script viu uma troca de página sem recarregar, e está sem `data-spa` | acrescente `data-spa="auto"` |
| `name` | evento ou meta com nome recusado (`detail` é o nome) | letras, números, `.`, `-`, `:` ou `_`: `cta click` → `cta-click` |
| `value` | meta com valor recusado (`detail` é `código=valor`) | inteiro em centavos: `19.99` → `1999` |
Um problema some da lista 7 dias depois da última vez que aconteceu.
## 12. Criar uma meta ou um evento
**Meta** é um resultado: ganha taxa sobre as visitas e pode ter valor.
**Evento** é algo que aconteceu: só é contado. Na dúvida, pergunte "isto é o
que eu quero que aconteça?". Se sim, é meta.
1. **O nome**: curto, sem espaço nem acento. `signup`, `purchase`,
`contact-click`, `download`.
2. **O lugar**: onde a coisa **de fato acontece**. Uma meta de cadastro vai
depois da confirmação do servidor, não no clique do botão.
3. **O código**:
```js
analytics.trackGoal('signup') // meta
analytics.trackGoal('purchase', 4900) // meta com valor, em centavos inteiros
analytics.trackEvent('cta-click') // evento
```
4. **Confira**: Configurações do site tem o assistente "Medir uma ação", que
gera o nome, o código e o pedido para o agente a partir de uma frase, e
acende quando o nome chega. Um nome ou valor recusado aparece em
"Recusados", com a correção (tabela da seção 11). Na tela do site, a meta
aparece em "Metas e eventos", e clicar nela recorta a tela pelas visitas
que chegaram lá.