# Integrar uma app

URL: /desenvolvedores/integrar

Conecte sua aplicação ao Mira: origens, CSP, bridge, widget e verificação, com etapas opcionais para erros, source maps e contexto.

Esta trilha leva uma aplicação React 19 com Vite do zero até enviar reports pelo widget. Ela serve a duas pessoas: o **admin** do Mira, que configura o projeto no dashboard, e o **dev** da app, que mexe no código e no CI. Cada etapa diz quem faz.

O bridge (`@mira/bridge`) é o pacote que fica na app. Ele ativa a captura de console, erros e rede desde o carregamento, mostra o widget de report e expõe versão, usuário logado e estado da app. O widget funciona em desenvolvimento, QA e staging. Em produção, só a captura de erros roda, ativada pelo DSN.

<Callout type="info" title="Navegadores">
O widget foi testado no Chrome e no Edge.
</Callout>

## Visão geral [#visão-geral]

| Etapa | Quem faz | Obrigatória |
| --- | --- | --- |
| [1. Cadastrar o ambiente e as origens](#cadastrar-o-ambiente-e-as-origens) | Admin | Sim |
| [2. Liberar a CSP](#liberar-a-csp) | Dev | Sim |
| [3. Instalar o bridge](#instalar-o-bridge) | Dev | Sim |
| [4. Ativar o widget](#ativar-o-widget) | Dev | Sim |
| [5. Verificar](#verificar) | Dev e admin | Sim |
| [A. Capturar erros com o DSN](#a-capturar-erros-com-o-dsn) | Admin e dev | Não |
| [B. Enviar source maps no CI](#b-enviar-source-maps-no-ci) | Admin e dev | Não |
| [C. Expor usuário e estado da app](#c-expor-usuário-e-estado-da-app) | Dev | Não |

## Os três ambientes [#os-três-ambientes]

Uma app costuma rodar em três lugares, e cada um ativa partes diferentes do bridge:

| Ambiente | Widget | Captura de erros (DSN) | Quem tem o ambiente no Mira |
| --- | --- | --- | --- |
| Desenvolvimento local | Sim | Opcional | Ambiente `develop`, com a origem `http://localhost:5173` |
| Staging e QA | Sim | Sim | Ambiente `staging` ou `qa`, com a origem da app |
| Produção | Não | Sim | Ambiente `production`, só para erros |

O Mira só aceita chamadas e logins vindos das origens cadastradas no ambiente. Por isso, cada ambiente da tabela precisa existir no projeto.

## Núcleo da integração [#núcleo-da-integração]

### Passos obrigatórios [#passos-obrigatórios]

<Steps>
<Step>

### Cadastrar o ambiente e as origens [#cadastrar-o-ambiente-e-as-origens]

**Quem faz:** admin.

1. Abra o menu do projeto, clique em **Configurações do projeto** e entre em **Ambientes**. O ambiente criado com o projeto já tem a origem da URL do app.
2. Clique em **Novo ambiente**.
3. Dê um nome, como `staging`, e liste em **Origens** uma origem por linha. Cadastre a origem exata, com a porta quando houver, ou um padrão com `*`, como `https://*.loja-demo.test`.
4. Salve. Repita para cada ambiente da tabela acima.

<Figure src="/images/integrate-environments.png" alt="Lista de ambientes do projeto com as origens de cada um" caption="Ambientes do projeto, cada um com suas origens." />

<Figure src="/images/integrate-environment-form.png" alt="Formulário de novo ambiente com o campo Origens preenchido" caption="O campo Origens aceita uma origem por linha." />

Anote também o **ID do projeto**, que está em **Configurações**, **Geral**, com um botão de copiar. Você usa esse valor na etapa B.

</Step>
<Step>

### Liberar a CSP [#liberar-a-csp]

**Quem faz:** dev.

Se a app define uma Content-Security-Policy, ela precisa deixar a página falar com o Mira. Adicione a URL da instância em `connect-src`:

```text
connect-src 'self' https://app.miraqa.org;
script-src 'self' https://app.miraqa.org;
```

A interface do widget não vai no pacote: a página a importa da instância, em `https://app.miraqa.org/widget/v1/widget.js`, na primeira vez que o widget abre. Por isso a CSP também precisa de `script-src` para a instância. Se o módulo não carregar, o widget não abre e o console mostra `[Mira] widget indisponível na instância`.

O login do widget abre uma aba do dashboard, que não passa pela CSP da app. Se você ativar a captura de erros (etapa A), libere também o host do DSN em `connect-src`. Ele é o mesmo da instância.

<Callout type="tip" title="Sem CSP, nada a fazer">
Uma app sem cabeçalho de CSP já passa por esta etapa.
</Callout>

</Step>
<Step>

### Instalar o bridge [#instalar-o-bridge]

**Quem faz:** dev.

O Mira serve os pacotes do SDK pela própria URL, sem login. Instale o bridge pela URL da instância:

```bash
npm install https://app.miraqa.org/downloads/sdk/mira-bridge-0.15.0.tgz
```

O pacote pede React 19 e `react-dom` como peers, porque o `ErrorBoundary` e o widget usam o React da app. O lockfile fixa a integridade do arquivo. Uma versão publicada nunca muda, e as antigas continuam disponíveis. Para atualizar, troque a versão na URL.

**Sem bundler**, a instância também serve o widget como script. Cole no `<head>`, antes dos scripts da app, e pule o `bootstrap.ts` da próxima etapa:

```html
<script src="https://app.miraqa.org/widget/v1/mira.js"></script>
<script>
  mira.init({ widget: { instanceUrl: "https://app.miraqa.org", trigger: "button" } });
</script>
```

O Guia de configuração do projeto mostra o mesmo trecho na aba **Script**, com a URL da instância preenchida. Pela tag de script, a CSP também bloqueia o `mira.init` inline: mova a chamada para um arquivo `.js` da app ou autorize-a com nonce.

</Step>
<Step>

### Ativar o widget [#ativar-o-widget]

**Quem faz:** dev.

Crie um bootstrap que inicializa o bridge antes da app e só então carrega o resto. Assim o bridge captura também os erros que acontecem durante o carregamento dos módulos.

```tsx
// src/main.tsx
import "./bootstrap";
```

Escolha o ambiente na aba. Cada uma traz o `.env` e o `bootstrap.ts` correspondentes.

<Tabs items={["Desenvolvimento local", "Staging e QA", "Produção"]}>
<Tab value="Desenvolvimento local">

```bash
# .env.development
VITE_MIRA_URL=https://app.miraqa.org
```

```ts
// src/bootstrap.ts
import { mira } from "@mira/bridge";
import { widgetIntegration } from "@mira/bridge/widget";

mira.init({
  integrations: [widgetIntegration({ instanceUrl: import.meta.env.VITE_MIRA_URL, trigger: "button" })],
});

await import("./app");
```

Rode a app em `http://localhost:5173` (ou na porta que você cadastrou) e use essa origem.

</Tab>
<Tab value="Staging e QA">

```bash
# .env.staging
VITE_MIRA_URL=https://app.miraqa.org
VITE_MIRA_DSN=https://<chave>@<host-do-mira>/<id-do-ambiente>
VITE_VERSION=1.4.0
VITE_BUILD_SHA=abc1234
VITE_BUILD_TIME=2026-11-04T12:00:00Z
```

```ts
// src/bootstrap.ts
import { mira } from "@mira/bridge";
import { widgetIntegration } from "@mira/bridge/widget";

mira.init({
  dsn: import.meta.env.VITE_MIRA_DSN,
  release: `${import.meta.env.VITE_VERSION}+${import.meta.env.VITE_BUILD_SHA}`,
  buildTime: import.meta.env.VITE_BUILD_TIME,
  integrations: [widgetIntegration({ instanceUrl: import.meta.env.VITE_MIRA_URL })],
});

await import("./app");
```

Este bootstrap já inclui a captura de erros. Para ativar só o widget, use o mesmo código do desenvolvimento local e deixe o DSN para a [etapa A](#a-capturar-erros-com-o-dsn).

</Tab>
<Tab value="Produção">

Em produção o widget não existe. Passe só o DSN, e o build não baixa a interface do widget.

```bash
# .env.production
VITE_MIRA_DSN=https://<chave>@<host-do-mira>/<id-do-ambiente>
VITE_VERSION=1.4.0
VITE_BUILD_SHA=abc1234
VITE_BUILD_TIME=2026-11-04T12:00:00Z
```

```ts
// src/bootstrap.ts
import { mira } from "@mira/bridge";

mira.init({
  dsn: import.meta.env.VITE_MIRA_DSN,
  release: `${import.meta.env.VITE_VERSION}+${import.meta.env.VITE_BUILD_SHA}`,
  buildTime: import.meta.env.VITE_BUILD_TIME,
});

await import("./app");
```

Veja a [etapa A](#a-capturar-erros-com-o-dsn) para gerar o DSN.

</Tab>
</Tabs>

Em `src/app.tsx`, fica o conteúdo do antigo `main.tsx`: o `createRoot` e a renderização.

<Callout type="warning" title="Inicialize antes de importar a app">
Imports estáticos rodam antes do corpo do arquivo. Por isso o `await import("./app")` vem depois do `mira.init()`.
</Callout>

O parâmetro `trigger` escolhe como o widget abre:

| `trigger` | Efeito |
| --- | --- |
| `"button"` (padrão) | Botão flutuante com o símbolo do Mira e o atalho `Alt+Shift+M` |
| `"shortcut"` | Só o atalho. Troque a combinação com `shortcut: "Ctrl+Shift+M"` |
| `"manual"` | Nenhuma entrada visível. Chame `mira.open()` a partir de um item de menu da app |

O widget sempre abre num painel junto do botão. Sem botão (`"shortcut"` e `"manual"`), o painel fica no canto inferior direito. A opção `mode` das versões antigas é ignorada.

Para o widget copiar o link do report depois do envio, a página precisa permitir `clipboard-write`. Se a app roda o widget num iframe, declare `allow="clipboard-write"` nele. Sem a permissão, o painel mostra o link para copiar à mão.

</Step>
<Step>

### Verificar [#verificar]

**Quem faz:** dev e admin.

1. Abra a app no ambiente configurado. O botão do Mira aparece no canto da página.

<Figure src="/images/integrate-widget-button.png" alt="Botão flutuante do Mira no canto de uma página" caption="O botão do widget no canto da app." />

2. Clique no botão. Na primeira vez, o widget pede o login. Clique em **Entrar**: o painel mostra um código, e **Aprovar no Mira** abre o dashboard numa aba nova. Entre, confira que o código é o do painel e aprove o projeto e o ambiente que o Mira resolveu para a origem da app.

<Figure src="/images/integrate-widget-login.png" alt="Painel do widget pedindo para entrar" caption="Sem sessão, o widget pede para entrar." />

3. Faça uma captura pelo menu do botão, ou escolha **Novo report**. O report abre num diálogo: o tipo começa em **Bug** (troque para **Melhoria** ou **Sugestão** se for o caso), preencha o título e a descrição (em Markdown) e clique em **Criar**.

<Figure src="/images/integrate-widget-form.png" alt="Formulário de bug no widget com título e descrição preenchidos" caption="O formulário de um bug, com as evidências automáticas embaixo." />

4. O painel troca o formulário por **Report enviado**.

<Figure src="/images/integrate-report-sent.png" alt="Confirmação Report enviado no widget" caption="O Mira confirmou o recebimento." />

5. O admin abre o dashboard e confere que o report chegou à triagem do ambiente.

<Figure src="/images/integrate-report-in-triage.png" alt="Report novo na lista de triagem" caption="O report enviado pelo widget na triagem do projeto." />

Se o botão não aparece ou o login falha, confira nesta ordem: a origem cadastrada bate com a da barra de endereço (incluindo a porta), a CSP libera a instância, o navegador não bloqueou a aba de aprovação e nenhum `<dialog>` modal está aberto por cima da página.

</Step>
</Steps>

## Etapas opcionais [#etapas-opcionais]

### A. Capturar erros com o DSN [#a-capturar-erros-com-o-dsn]

**Quem faz:** admin gera a chave, dev configura a app. Vale para staging, QA e produção.

O DSN ativa o SDK de erros. Ele tem o formato `https://<chave>@<host-do-mira>/<id-do-ambiente>`, e a chave define o ambiente. Ela é pública e só escreve ocorrências daquele ambiente, então pode ir no bundle.

<Steps>
<Step>

### Gerar a chave [#gerar-a-chave]

**Quem faz:** admin.

1. Em **Configurações do projeto**, **Erros**, escolha o ambiente no seletor. A seção mostra o nível de dados e as chaves de ingestão.
2. Dê um nome à chave, como `web`, e clique em **Gerar chave**.
3. Copie o DSN pelo botão da linha.

<Figure src="/images/integrate-errors-panel.png" alt="Painel Erros do ambiente com nível de dados e chaves de ingestão" caption="O painel Erros de um ambiente." />

<Figure src="/images/integrate-ingest-key.png" alt="Chave de ingestão gerada com o DSN e o botão de copiar" caption="A chave gerada e o botão que copia o DSN." />

Cada chave vale só para o ambiente em que foi gerada. Revogar não tem volta e faz as apps com aquele DSN pararem de enviar.

</Step>
<Step>

### Configurar a app [#configurar-a-app]

**Quem faz:** dev.

Coloque o DSN em `VITE_MIRA_DSN` e use o `bootstrap.ts` da aba de staging ou de produção da etapa 4 (Ativar o widget). A `release` identifica a versão (`1.4.0+abc1234`), e o `buildTime` permite ao Mira reabrir sozinho um erro resolvido quando um build novo volta a falhar.

Libere o host do DSN em `connect-src`. A origem da app precisa estar cadastrada no ambiente, porque a ingestão confere a origem dos envios do navegador.

</Step>
<Step>

### Conferir [#conferir]

**Quem faz:** dev e admin.

Provoque um erro na app e abra **Erros** no projeto. A ocorrência aparece agrupada em um erro novo.

<Figure src="/images/integrate-error-in-list.png" alt="Lista de erros com um erro novo da app de teste" caption="O erro enviado pela app, agrupado na lista." />

</Step>
</Steps>

O que o SDK captura, as opções de `init`, a captura manual e a redação estão em [SDK](/desenvolvedores/sdk).

### B. Enviar source maps no CI [#b-enviar-source-maps-no-ci]

**Quem faz:** admin cria a credencial, dev configura o build.

Sem source maps, a stack de uma ocorrência chega minificada. O plugin Vite do `@mira/sourcemaps` gera os maps, injeta um Debug ID em cada script, envia os maps ao Mira e apaga os `.map` do `dist`. Os maps nunca vão para o servidor da app.

<Steps>
<Step>

### Criar a credencial de serviço [#criar-a-credencial-de-serviço]

**Quem faz:** admin.

1. Abra **Admin**, **Serviços de API**, e crie um serviço com os projetos da app.
2. Ative só a permissão **Permitir envio de source maps** e desative as consultas.
3. Em **Chaves**, informe nome e validade e use **Emitir chave**. O segredo (`mira_svc_...`) aparece uma vez; salve no gerenciador de segredos do CI.

Veja os detalhes em [Tokens](/desenvolvedores/tokens).

</Step>
<Step>

### Instalar o plugin [#instalar-o-plugin]

**Quem faz:** dev.

```bash
npm install --save-dev https://app.miraqa.org/downloads/sdk/mira-sourcemaps-0.1.1.tgz
```

```ts
// vite.config.ts
import { miraSourceMaps } from "@mira/sourcemaps/vite";
import react from "@vitejs/plugin-react";
import { defineConfig, loadEnv } from "vite";

export default defineConfig(({ mode }) => {
  // Lê os `.env` do modo (`.env.production`, `.env.staging`) junto com o ambiente do CI.
  const env = loadEnv(mode, process.cwd(), "VITE_");

  return {
    plugins: [
      react(),
      miraSourceMaps({
        release: `${env.VITE_VERSION}+${env.VITE_BUILD_SHA}`,
        buildTime: env.VITE_BUILD_TIME,
      }),
    ],
  };
});
```

Use a mesma `release` e o mesmo `buildTime` do `mira.init`, que lê os mesmos valores de `import.meta.env`.

</Step>
<Step>

### Configurar o CI [#configurar-o-ci]

**Quem faz:** dev.

O plugin lê três variáveis de ambiente:

| Variável | Valor |
| --- | --- |
| `MIRA_URL` | `https://app.miraqa.org`. Precisa de `https`, porque o token vai num header; `http` só vale para `localhost` |
| `MIRA_PROJECT_ID` | O ID do projeto, de **Configurações**, **Geral** |
| `MIRA_SOURCEMAPS_TOKEN` | A credencial `mira_svc_` da etapa anterior |

```yaml
# .github/workflows/deploy.yml
- run: pnpm build
  env:
    MIRA_URL: https://app.miraqa.org
    MIRA_PROJECT_ID: ${{ vars.MIRA_PROJECT_ID }}
    MIRA_SOURCEMAPS_TOKEN: ${{ secrets.MIRA_SOURCEMAPS_TOKEN }}
- run: pnpm exec mira-sourcemaps check dist
```

O `check` falha se sobrar um `.map` no diretório ou se um `.js` ou `.css` tiver map inline (`sourceMappingURL=data:`). Rode antes de publicar.

<Callout type="warning" title="Publique depois do envio">
Sem token, o build remove os maps sem enviar e avisa no log. Se o envio falhar, os `.map` saem do `dist` e o build falha. Publique a app só depois do envio: ocorrências que chegam antes dos maps ficam minificadas e formam outro erro.
</Callout>

Fora do Vite, o mesmo pacote traz o comando `mira-sourcemaps upload <dir> --release <release>` para um `dist` já gerado.

O que o Mira armazena do código depende do **nível de código** do projeto, em **Configurações**, **Geral**: sem código, só mapeamento (padrão) ou completo.

</Step>
</Steps>

### C. Expor usuário e estado da app [#c-expor-usuário-e-estado-da-app]

**Quem faz:** dev.

**Versão e ambiente.** O contexto vai junto de cada report:

```ts
mira.setContext({
  version: import.meta.env.VITE_BUILD_SHA,
  env: import.meta.env.MODE,
});
```

`setContext` substitui o contexto inteiro. Para o usuário, chame as duas funções no login e no logout:

```ts
mira.setContext({ version, env, appUser: { id: user.id, email: user.email } });
mira.setUser({ id: user.id, email: user.email });

// no logout
mira.setUser(null);
```

`setContext` vai para os reports e `setUser` para as ocorrências de erro. Depois do logout, o SDK volta ao id anônimo.

**Estado.** Registre um provider por fonte de estado. Ele só roda quando alguém envia um report e marca o contexto e estado da app entre as evidências.

```ts
import { dehydrate } from "@tanstack/react-query";

mira.register("tanstack-query", () => dehydrate(queryClient));
mira.register("router", () => router.state.location);
```

Um provider que lança erro ou demora mais de 2 segundos é ignorado, e o report segue sem aquele snapshot.

<Callout type="danger" title="O estado fica visível ao projeto">
Todos os membros do projeto veem o estado enviado. Não registre providers que devolvam tokens, credenciais ou dados pessoais reais. A redação automática cobre nomes de chave comuns, como `token`, `password` e `secret`, mas não reconhece dados pessoais.
</Callout>

## Problemas comuns [#problemas-comuns]

| Sintoma | Causa provável |
| --- | --- |
| O botão não aparece | O bridge não foi inicializado nessa build, ou a app roda com a variável `VITE_MIRA_URL` vazia |
| O login do widget diz que não há ambiente para a origem | A origem da barra de endereço não está cadastrada no ambiente |
| A aprovação do login não termina | O código expira em 10 minutos. Clique em **Entrar** de novo e aprove o código novo, na mesma instância |
| O widget não recebe foco | Um `<dialog>` modal da app está aberto, ou uma biblioteca de focus trap bloqueia o foco. Feche o modal ou crie uma exceção para o host `<mira-widget>` |
| Os envios do SDK falham com erro de CSP | O host do Mira não está em `connect-src` |
| A stack das ocorrências chega minificada | Os source maps não foram enviados para essa `release`, ou a `release` do build difere da do `init` |

## Próximos passos [#próximos-passos]

<Cards>
<Card href="/desenvolvedores/sdk" title="SDK" description="Opções de init, captura manual, redação e integrações." />
<Card href="/desenvolvedores/tokens" title="Tokens" description="Credenciais de serviço, tokens pessoais e chaves de ingestão." />
<Card href="/desenvolvedores/mcp" title="MCP" description="Deixe um agente de IA ler e comentar os reports." />
<Card href="/desenvolvedores/cli" title="CLI" description="Use o Mira pelo terminal e em scripts." />
</Cards>
