ProjetosCRO & Experimentação

CRO 360 — Testes A/B e Personalização Criados por Agentes de IA, Aprovados por Humanos

Plataforma SaaS multitenant onde um agente de IA inspeciona o site, propõe um teste A/B ou uma personalização dentro de um contrato seguro, e um humano revisa e publica. A execução acontece no edge, em milissegundos, sem JavaScript arbitrário.

CRO & Experimentaçãocroteste-abpersonalizacaomcpcloudflare-workersgcpia

Problema

Times de CRO travam em dois gargalos. O primeiro é de capacidade: cada teste A/B exige briefing, fila de desenvolvimento e QA, então a empresa roda 2 ou 3 testes por mês quando precisaria rodar 20. O segundo é de confiança: as ferramentas que aceleram isso costumam injetar JavaScript arbitrário na página, sem revisão, sem rastro de quem publicou o quê — o que é inaceitável em qualquer operação séria. E com IA no meio, o risco só aumenta: ninguém quer um agente com permissão de escrever código solto no site em produção.

Solução

O CRO 360 separa quem propõe de quem publica. Um agente de IA conectado via MCP inspeciona as páginas do domínio autenticado e cria um rascunho dentro de um contrato declarativo — não escreve código, preenche uma especificação validada por JSON Schema. Um humano revisa esse rascunho com preview ao vivo por variante, aprova e publica. Só então um bundle assinado vai para o edge da Cloudflare, onde a decisão acontece em milissegundos.

Resultados

Em produção: ciclo completo de rascunho por IA → revisão humana → publicação no edge, com preview ao vivo por variante, screenshots automáticos via Playwright, isolamento multitenant no PostgreSQL e audit log imutável de toda transição. O MCP não tem escopo de aprovação nem de publicação — por design, não existe caminho para um agente burlar a revisão humana.

CRO 360

O objetivo

Deixar que um agente de IA faça o trabalho pesado de propor testes A/B e personalizações, sem nunca abrir mão da revisão humana antes de qualquer coisa ir ao ar.

A pergunta que originou o projeto foi bem específica: dá pra colocar uma IA para criar experimentos em um site de produção sem que isso vire um risco de segurança? A resposta foi sim — mas só se a IA nunca escrever código, e só se a publicação for sempre um ato humano explícito.

O problema que ele resolve

Gargalo de capacidade

Um teste A/B tradicional passa por briefing, priorização, fila de desenvolvimento, QA e deploy. Na prática, times enxutos rodam 2 ou 3 testes por mês. A limitação não é falta de hipótese — é o custo de transformar hipótese em experimento no ar.

Gargalo de confiança

As ferramentas que aceleram esse ciclo geralmente resolvem por injeção de JavaScript arbitrário: um editor visual que gera um blob de código executado na página do cliente. Isso é rápido e é exatamente o que áreas de segurança bloqueiam — sem revisão de código, sem rastro de autoria, sem garantia do que roda no navegador do usuário final.

Colocar um LLM nesse fluxo, do jeito ingênuo, multiplica o problema: agora você tem uma entidade não determinística com permissão de escrita no seu site em produção.

Como foi feito

A ideia central: contrato, não código

O agente de IA não escreve JavaScript. Ele preenche uma especificação declarativa — o ExperimentSpec v1, um JSON Schema 2020-12 que é a fonte canônica consumida pela API, pelo MCP, pelo OpenAPI e pelos testes.

As mudanças permitidas são de cinco tipos, e só esses cinco:

  • texto — trocar o conteúdo textual de um elemento;
  • estilo — propriedades de um allowlist, com estados opcionais hover/focus/active;
  • atributo — atributos permitidos;
  • classe — adicionar/remover classes (aceita variantes Tailwind);
  • insert — criar elementos e seções inteiras por template sanitizado.

O que é rejeitado na validação, sempre: JavaScript arbitrário, innerHTML, HTML executável, URLs javascript: e data:. Valores de estilo não podem conter {} ou ;, então nunca escapam da regra CSS gerada. Imagens e assets só podem apontar para a própria origem do site — host externo retorna ASSET_HOST_NOT_ALLOWED.

O insert é o mais interessante de implementar com segurança: ele monta a árvore com createElement + textContent (nunca innerHTML), com tags em allowlist, limite de 6 níveis e 60 nós, e é idempotente — reaplicar não duplica.

O MCP: o que o agente pode e não pode fazer

O agente se conecta por MCP Streamable HTTP (JSON-RPC 2.0), com tools/list publicando inputSchema e outputSchema completos — o agente não precisa adivinhar formato. As tools disponíveis:

Tool Scope O que faz
site.get_installation site:read dados do único site da credencial
site.inspect_page site:read lê HTML estático só na origem cadastrada
experiences.list_active experiences:read experiências ativas do site
experience.validate_draft drafts:write valida sem escrever, erros por campo
experience.create_draft drafts:write cria somente rascunho
preview.get preview:read lê links de preview e screenshots

Repare no que não está na lista: não existe tool de aprovar nem de publicar. Isso não é uma configuração que alguém pode ligar — é ausência de código. O agente não tem como burlar a revisão humana porque o caminho não existe.

A credencial fica presa para sempre ao organization_id e site_id do registro. Qualquer parâmetro que o agente mande no corpo tentando trocar de tenant é ignorado: o escopo vem da credencial, nunca do payload.

claude mcp add --transport http cro360-site \
  "https://.../v1/mcp/CLIENT_ID" \
  --header "Authorization: Bearer SEGREDO_EXIBIDO_UMA_VEZ"

O ciclo humano

rascunho (IA) → enviado p/ revisão → revisão concluída → aprovado → publicado

Console do CRO 360: logs de governança, com cada movimentação registrando experiência, responsável e horário, ao lado das regras de segurança de publicação

Cada transição vira uma linha imutável no log de governança — quem fez, o quê e quando.

Cada seta é uma ação humana explícita, com RBAC: analyst envia e conclui revisão; apenas approver e admin aprovam e publicam; viewer é somente leitura. Toda transição grava organização, experiência, ator e horário no audit_log.

No momento em que o rascunho vai para revisão, o Control API publica um bundle de preview assinado numa chave KV separada, com TTL de 7 dias, e gera um link por variante com token HMAC. O revisor abre a página real com aquela variante aplicada, com badge visual, e sem disparar eventos de exposição no dataLayer — preview não contamina o dado.

Em paralelo, um Cloud Run Job com Playwright captura screenshots de control e variantes por device declarado, grava PNGs num bucket privado, e o console exibe as thumbnails via proxy autenticado por tenant.

A arquitetura

GitHub main
   │ GitHub Actions + OIDC/WIF (sem chave estática)
   ├──> Cloud Run: cro360-console
   ├──> Cloud Run: cro360-control-api ──> Cloud SQL PostgreSQL
   │                                  ├──> Secret Manager
   │                                  └──> Cloudflare KV (publicação)
   └──> Cloudflare Worker + Assets
                         ├──> KV PUBLISHED_CONFIGS
Site do tenant ──SmartCode──> /v1/decision/:siteKey
                         └──> Queue operacional ──> /v1/internal/health
Agente LLM ──Bearer/site──> /v1/mcp/:clientId
GA4 <── dataLayer de exposição/conversão no navegador

A regra de separação é rígida: o Control Plane é a única autoridade de escrita. O Worker no edge só lê bundles já publicados e assinados. O navegador nunca escolhe o tenant por query string — a siteKey resolve a configuração publicada, e a origem da requisição precisa constar em allowed_origins.

Tela de registro de domínio do CRO 360, com o passo a passo de conectar domínio HTTPS, instalar o SmartCode no head, publicar a tag no GTM e validar sem DNS

O onboarding do domínio: conectar, instalar o SmartCode, publicar a tag no GTM e validar — sem nenhum registro de DNS.

O monorepo separa por responsabilidade: control-api (HTTP, auth, banco, MCP), control-plane (núcleo de autorização testável), edge-runtime (Worker), preview-runner (job Playwright), mais os pacotes contracts, decision-engine e smartcode.

Multitenancy de verdade

Toda consulta e escrita é filtrada por organization_id. Nenhum ID enviado no corpo troca o tenant resolvido pela credencial. FKs são compostas por (organization_id, id).

E uma regra que exigiu cuidado extra: existe no máximo um MCP ativo por site. Está garantida em três camadas — na aplicação, com advisory lock do PostgreSQL, e com o índice parcial uq_mcp_clients_one_active_per_site. Não adianta corrida de requisições.

Validação sem DNS

Conectar um domínio normalmente pede registro TXT no DNS, o que trava o onboarding por dias. Aqui o backend lê o HTML público da página e cruza três sinais: o SmartCode instalado, o container do GTM, e o marcador operacional da tag CRO 360. Passou nos três, o domínio está verificado — e só então o MCP pode ser criado.

O SmartCode no navegador

Um script único servido pelo Worker, instalado como primeiro item do <head>:

<script
  src="https://.../cro360-smartcode.v1.js"
  data-site-key="SITE_KEY"
  data-edge-base="https://..."
  data-consent="granted"></script>

Ele faz decisão determinística com sticky assignment (o mesmo visitante cai sempre no mesmo grupo, por identificador first-party), respeita prioridade, slots e holdout entre experiências concorrentes, e reaplica as mudanças após navegação SPA e re-render de framework — via MutationObserver e hooks de history. Anti-flicker é opt-in por data-antiflicker="true".

O padrão de tráfego é conservador de propósito: 10% da audiência elegível, com 5% variante A, 5% variante B e 90% fora do teste.

Privacidade como restrição de arquitetura

Essa parte foi decidida cedo e amarrou o resto do desenho. O CRO 360 não armazena pageviews, conversões, receita ou qualquer resultado analítico de visitante. O GA4 do próprio cliente continua sendo a fonte de performance — a plataforma só guarda configuração, auditoria, identidade administrativa e saúde operacional sanitizada.

O CRO360.identify() aceita nome, e-mail, telefone e ID do usuário logado, mas esses campos ficam em sessionStorage no navegador e não são enviados à API, ao Worker, à Queue ou ao PostgreSQL. A personalização usa {{cro360.profile.first_name}} resolvido localmente. Para o edge segue apenas contexto não identificável e booleanos como is_logged_in. Geolocalização só existe como segmento coarse e consentido.

Consentimento negado significa nenhum cookie ou localStorage persistente — apenas estado de sessão.

Stack

GCP: Cloud Run (console, control-api), Cloud SQL PostgreSQL 16, Cloud Run Jobs (Playwright), Cloud Storage, Secret Manager, Workload Identity Federation Cloudflare: Workers, Workers KV, Queues + DLQ, Workers Assets Contratos: JSON Schema 2020-12, OpenAPI 3.1, MCP Streamable HTTP, llms.txt CI/CD: GitHub Actions com OIDC, deploy consolidado por merge em main

O que ainda não está pronto

Prefiro ser direto sobre isso:

  • o screenshot de preview captura o viewport (1366×768 e 390×844); elemento abaixo da dobra ainda exige o link ao vivo;
  • diff visual, detecção de seletor inexistente, JS errors e Web Vitals no preview: não construídos;
  • site.inspect_page lê HTML estático — página renderizada só por JS vai precisar de um executor de browser;
  • o MCP usa Bearer estático por domínio (armazenado só como hash, com rotação no console). OAuth 2.1 é o próximo marco;
  • billing, autosserviço de conta e recuperação de senha: no backlog;
  • não existe ambiente de staging implantado.

Quer testar antes de todo mundo?

Estou abrindo um grupo pequeno de primeiros testers — pessoas que vão usar a plataforma de verdade, com acompanhamento direto meu na configuração e no primeiro caso de uso real. Em troca, quero feedback honesto sobre o que funciona e o que ainda não funciona.

Se isso faz sentido pro seu time:

👉 Falar comigo no WhatsApp — (11) 96552-1585

Manda uma mensagem dizendo qual é o seu cenário e eu respondo com os próximos passos.