# 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á.