Ext Ops Panel
Abrir o painel

Início/Docs/Feedback

Contrato de feedback

Versão do contrato 1.0.0 · grátis em todos os planos

Um pequeno diálogo que as pessoas podem abrir em qualquer página do seu site ou produto para dizer o que não funciona ou o que falta. Cole uma tag de script ou chame o endpoint a partir do seu próprio diálogo. O que chega aparece em um único bloco do painel, ao lado da pesquisa de desinstalação e do Rate Us, e o proprietário recebe um aviso na caixa de entrada.

↓ Baixar OpenAPI 3.1 (YAML) /feedback-openapi.yaml · info.version 1.0.0

1. Cole o widget

Uma única tag script adiada, sem código inline, por isso funciona com uma CSP rígida:

<script src="https://extops.dev/widget.js"
        data-extops-widget="wk_XXXXXXXXXXXXXXXXXXXXXXXXXXXX" defer></script>

Permita a nossa origem em script-src e connect-src. O arquivo é servido por nós, não tem dependências e fica abaixo de 7 KB gzip; os estilos passam pelo CSSOM dentro de um shadow root, então você não precisa de 'unsafe-inline' e o CSS da sua página não consegue deformar o diálogo.

Atributos opcionais: data-locale, data-api, data-position="left" e data-button="false" quando você quiser abri-lo por conta própria com window.ExtOpsFeedback.open(). Também: data-button-style="icon" para um botão redondo de ícone de 48 px, data-surface="app" para posicioná-lo acima da navegação inferior de um app no celular, e data-color-preset para definir a cor do botão pela página.

Tema claro e escuro

Cada preset de cor é desenhado em um tema claro e em um escuro. Por padrão (data-theme="auto") o widget segue a sua página: <html data-theme>, uma classe dark ou light em <html>, ou o seu color-scheme. Uma página sem tema próprio recebe o tema do sistema do visitante. Quando o seu seletor de tema muda uma dessas propriedades, o widget é redesenhado na hora, sem recarregar.

Para fixar o tema, defina data-theme="light" ou data-theme="dark". Se a sua página guarda o tema onde o widget não enxerga, chame window.ExtOpsFeedback.setTheme('dark') a partir do seu seletor, e setTheme('auto') para voltar a seguir a página.

2. Ou chame o endpoint você mesmo

Para um site que não permite nenhum script de terceiros, ou um produto que quer o formulário dentro do próprio design system. É o mesmo endpoint para o qual o nosso widget envia, então você nunca precisa escolher entre a sua interface e o nosso pipeline.

await fetch('https://extops.dev/api/v1/feedback/widget', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    widget_key: 'wk_XXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    topic: 'bug',
    comment: userText,
    page: location.href,
    locale: navigator.language,
    event_id: crypto.randomUUID(),
    dwell_ms: Date.now() - openedAt,
  }),
});

Leia os tópicos que o dono configurou em GET /api/v1/feedback/widget/config?key=… para que o seu diálogo ofereça exatamente o que o painel oferece. Reutilize o mesmo event_id nas novas tentativas: o servidor deduplica por ele, então uma rede instável não transforma uma mensagem em duas.

3. A chave do widget é pública de propósito

Ela aparece no código-fonte da sua página, e tudo bem: o único poder dela é criar uma linha de feedback. Não consegue gravar eventos de instalação ou desinstalação, não lê nada além da configuração do diálogo e não pode indicar um projeto (project_id no corpo não é aceito). Por isso um widget recebe a própria chave em vez do webhook_token da sua fonte, que pode gravar eventos de métricas e nunca deve aparecer no HTML. Você pode reemitir a chave a qualquer momento; a antiga deixa de ser aceita na hora.

4. O que guardamos e o que não guardamos

Guardamos o tópico, o texto, a página normalizada, o idioma, um contato opcional, o horário, o tipo de feedback e qual widget o coletou.

A retenção é de 180 dias, a mesma regra de qualquer outro tipo de feedback.

5. Respostas e códigos de erro

Um sucesso é assim: {"status":"ok","recorded":true}. recorded: false é também um sucesso: o envio era duplicado ou uma heurística antibot foi acionada. Mostre o mesmo agradecimento nos dois casos: um script não deve saber qual regra o pegou, e quem tenta de novo após um timeout não deve ver erro por uma mensagem que já temos.

HTTPcodeQuando
400validation_errorsem chave, sem tópico, um tópico que o widget não oferece, um comentário vazio, um corpo malformado
403origin_not_alloweda origem da página não está na lista de permitidas do widget
404not_foundchave desconhecida ou um widget desativado: a mesma resposta, para que uma chave pública não consiga sondar quais projetos existem
429rate_limited60/hora por chave, 10/hora por IP; traz Retry-After
401source_token_invalidapenas no caminho server-side: token de fonte errado
500internal_errorerro nosso

6. Tópicos, tipos e status

Um tópico é um id mais um label. O id é um slug, porque fica gravado em cada linha e vira o valor de filtro no painel; o rótulo é escrito pelo dono. Um widget sem tópicos configurados oferece os padrões: bug, idea, question, other.

A cor do diálogo é uma de seis predefinições compartilhadas com as páginas hospedadas: amber (o padrão), teal, indigo, violet, rose, slate. Uma lista única de propósito: o widget e as páginas hospedadas do mesmo dono devem se parecer, e uma segunda paleta acabaria se afastando da primeira.

As linhas gravadas têm um tipo: uninstall, rate_us ou widget. O tipo é uma TAG, não um pipeline separado: o bloco do painel, as notificações e a retenção são compartilhados. O status de triagem do dono é new, read ou spam; marcar como spam tira a linha da lista de trabalho e das notificações sem apagar o que a pessoa escreveu, e pode ser desfeito.

7. A proteção contra abuso fica no servidor

Como a chave é pública, uma verificação no navegador seria um conselho, não uma regra. Quem aplica é o servidor: a lista de origens permitidas do widget (vazia significa qualquer origem, porque um cliente pode servir um site por uma dúzia de subdomínios), dois limites de taxa independentes (por chave e por IP), um campo honeypot e um tempo mínimo de permanência, e deduplicação por (widget_id, event_id) no banco em vez de uma verificação prévia, porque duas tentativas simultâneas passariam ambas por um SELECT.

Os dois limitadores falham abertos: se o nosso Redis cair, o feedback de uma pessoa ainda chega a você. Para um endpoint que grava um parágrafo, é o mal menor. Não adicionamos captcha externo, que quebraria sua CSP e a privacidade dos seus visitantes.

8. O contrato legível por máquina

A descrição formal dos dois endpoints de recebimento, da leitura de configuração, dos enums e dos códigos de erro é o arquivo OpenAPI 3.1. Ele traz a mesma versão de contrato (1.0.0).

↓ Baixar OpenAPI 3.1 (YAML) /feedback-openapi.yaml