> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livingsecurity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Power BI

> Conecte o Power BI à API Pública da Living Security com o Power Query, crie um modelo em esquema estrela sobre /v1 e escolha entre conjuntos de dados operacionais e de relatórios para phishing.

Este guia é um exemplo completo de leitura de `/v1` no Power BI. Ele abrange
o padrão de conectividade do Power Query (M), um modelo semântico em esquema estrela construído
sobre `/v1/activities`, a atualização incremental e quando usar os conjuntos de dados de
pré-visualização de **relatórios** para os resultados de phishing por pessoa.

Os exemplos usam o host de produção dos EUA
(`https://api.us-east-1.livingsecurity.ai/v1`). Para uma organização da UE, use
`https://api.eu-west-1.livingsecurity.ai/v1` em `Fn_FetchAll` e em
`Fn_GetJson` e, em seguida, vincule a credencial da fonte de dados a esse host. Consulte
[Regiões](/pt/developers/v1/regions).

Este guia usa credenciais Basic armazenadas com uma chave de API. Não é necessário
um cliente OAuth. Consulte [Autenticação](/pt/developers/v1/authentication) para
conhecer as opções de credenciais e o [Developer Hub](/pt/developers/developer-hub/overview)
para acesso e configuração.

## Para que serve este exemplo

Use-o quando precisar:

* Carregar os endpoints de lista de `/v1` em um conjunto de dados do Power BI com paginação por cursor correta e
  nova tentativa em caso de limite de taxa.
* Criar um modelo semântico em que as medidas ("entidades ativas", "atividades por
  entidade ativa") sejam dimensionais, em vez de contadas a partir de uma tabela plana.
* Reduzir uma atualização agendada do Power BI de uma varredura completa do histórico para
  uma pequena janela diária, usando a atualização incremental.
* Escolher a granularidade correta de phishing: totais de campanha (operacional) ou
  resultados por pessoa e eventos tipados (pré-visualização de relatórios).

## Pré-requisitos

* Uma chave de API com as permissões de que suas consultas precisam:
  * Exemplo operacional: `activity:read`, `entity:read` e
    `simulation:read`.
  * Phishing por pessoa por meio de conjuntos de dados de relatórios: `reporting:read`.
    Consulte [Chaves de API](/pt/developers/developer-hub/api-keys).
* Power BI Desktop ou um workspace do Power BI / Fabric no qual você possa criar um
  modelo semântico e agendar a atualização.

## Escolha a granularidade correta

`/v1` expõe duas superfícies relacionadas para phishing. Elas não são intercambiáveis.

| Necessidade | Use | Permissão | Granularidade |
| - | - | - | - |
| Totais do funil da campanha | `GET /v1/simulations/{id}/results` | `simulation:read` | Uma linha por simulação (entregues, abertas, clicadas, …) |
| Resultados por pessoa (operacional) | `GET /v1/simulation_results` | `simulation:read` | Uma linha por entidade × simulação |
| Resultados por pessoa (BI / relatórios) | `GET /v1/reporting/simulation_results` | `reporting:read` | Mesma granularidade, nomes de campos de BI (`entity_id`, `simulation_id`), marca-d'água `updated_after` |
| Fatos de interação tipados | `GET /v1/reporting/simulation_events` | `reporting:read` | Um fato por interação (`simulation.link_clicked`, …), `occurred_after` |
| Dimensão atual da Força de trabalho | `GET /v1/reporting/entities` | `reporting:read` | Uma linha atual por entidade. Junte por `reporting.simulation_results.entity_id = reporting.entities.id` |
| Versões atuais das entidades | `GET /v1/reporting/entity_versions` | `reporting:read` | Um intervalo atual em aberto por entidade. Não existe tabela de histórico de versões |
| Associações atuais a Coortes | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Um fato atual por `(cohort_id, entity_id)`. Não existe tabela de histórico de associações |
| Espinha de junção para todos os tipos de atividade | `GET /v1/activities` | `activity:read` | Um evento por atividade, com `participants[]` aninhado |

As consultas de exemplo do Power BI abaixo chamam as rotas **operacionais**
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Para relatórios de phishing por pessoa, prefira a família de
pré-visualização de **relatórios** descrita abaixo. Uma chave que recebeu apenas `reporting:read` não pode
chamar `/v1/simulation_results` operacional (a API retorna `403`).

### Escala de dificuldade

A dificuldade da simulação é um inteiro de 1 a 5 nas propriedades `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), `/v1/activities`
(`difficulty_tier`) e em `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` é o mais fácil e `5` é o mais difícil. As simulações adaptativas e progressivas não têm
um valor único no nível da campanha, portanto `/v1/simulations.difficulty_level` é `null`,
enquanto o `difficulty_level` por destinatário nos resultados ainda pode ser definido. Para mapear para
exportações legadas de nível baixo/médio/alto, trate `1–2` como baixo, `3` como médio e `4–5` como
alto.

<Warning>
  Não crie novas integrações de BI com base em chamadas de rede não documentadas do Painel
  nem na visão geral legada e selecionada da API Pública `/api/v1`. Autentique-se no host `/v1` regional
  da organização com uma chave de API do Developer Hub. Consulte
  [Regiões](/pt/developers/v1/regions).
</Warning>

## Padrão de conectividade (Power Query)

Quatro regras sustentam todas as consultas do exemplo. Cada uma delas é imposta pelo
serviço do Power BI, e não por preferências de estilo.

<AccordionGroup>
  <Accordion title="Mantenha a URL base de Web.Contents estática e terminando em /v1">
    Coloque tudo o que for dinâmico em `RelativePath` e `Query`. O primeiro argumento de
    `Web.Contents` deve ser uma string literal que termine na raiz `/v1`
    regional da organização:

    * EUA: `https://api.us-east-1.livingsecurity.ai/v1`
    * UE: `https://api.eu-west-1.livingsecurity.ai/v1`

    Os chamadores passam então apenas o recurso (`"simulations"`, e não
    `"v1/simulations"`). Não cole um host dos EUA para uma organização da UE.

    Se o primeiro argumento for montado por concatenação de strings, o serviço do Power BI
    não consegue identificar estaticamente a fonte de dados e a atualização agendada falha com
    um erro de "fonte de dados dinâmica", mesmo que a consulta funcione no Power BI
    Desktop.
  </Accordion>

  <Accordion title="Forneça a chave de API como uma credencial Basic armazenada, não no texto da consulta">
    Quando o Power BI solicitar credenciais para a fonte de dados `/v1`, escolha
    **Basic** e cole a chave de API como **nome de usuário**. Coloque um valor de substituição, como
    `x`, no campo **Senha**. O serviço do Power BI rejeita uma senha em
    branco, embora a API ignore esse campo. Também funciona usar a chave como senha com um nome de usuário fictício.

    Não adicione um cabeçalho `Authorization` em M. Ele funciona no seu computador, mas depois
    incorpora a chave ao arquivo da consulta e prejudica a atualização agendada.
  </Accordion>

  <Accordion title="Deixe o teste de conexão da caixa de diálogo de credenciais ser executado">
    `GET /v1` é autenticado: retorna `200` com uma chave válida e `401`
    sem ela. Aponte a fonte de dados para `…/v1` e **não** ignore o teste —
    uma chave válida é salva, enquanto uma chave revogada ou malformada falha imediatamente.

    Exemplos mais antigos usavam o host simples como fonte de dados e orientavam a ignorar
    o teste porque `GET /` é sempre `404`. Aponte esses modelos novamente para `…/v1`.
  </Accordion>

  <Accordion title="Defina limit=500 explicitamente em todas as consultas de lista">
    O tamanho de página padrão em todo endpoint de lista de `/v1` é 25. Deixar o valor padrão
    multiplica o número de requisições para os mesmos dados. Os valores de consulta devem ser texto —
    `limit = "500"` em M, e não `limit = 500`.
  </Accordion>
</AccordionGroup>

### Função de paginação compartilhada

Todas as consultas do exemplo chamam uma única `Fn_FetchAll` que percorre
`next_page_token` até que seja `null` (consulte
[paginação](/pt/developers/v1/pagination)). Suas principais propriedades:

* **URL base `/v1` estática, `RelativePath` e `Query` dinâmicos.** Necessário para
  a atualização agendada e para o teste de credencial.
* **`ManualStatusHandling = {429}` com nova tentativa limitada.** A função lê
  `Retry-After` quando o Power Query o expõe; caso contrário, aguarda 30 segundos e
  desiste após 5 respostas `429` consecutivas, com um erro explícito, em vez de
  entrar em loop até que a atualização expire. `5xx` deliberadamente não é
  repetido — um erro do servidor deve ser exibido, e não produzir silenciosamente um conjunto de dados
  parcial.
* **`Timeout` de 60 segundos.** As páginas normalmente respondem em bem menos de um segundo, portanto
  um minuto de silêncio significa que a requisição falhou.
* **`Binary.Buffer` antes de `Json.Document`.** Sem o buffer, o mecanismo de
  mashup pode solicitar novamente o fluxo de resposta se o ler mais de uma vez.
* **`baseQuery` é passado sem alterações a cada iteração.** Os tokens de página são
  cursores opacos e assinados, vinculados à organização, à credencial, à versão do contrato
  e à impressão digital dos filtros e da ordenação. Alterar um filtro
  durante a varredura é rejeitado como token inválido, em vez de reiniciar a partir da primeira
  página.

```m Fn_FetchAll.pq — base and call shape theme={null}
let
    // Literal /v1 root. Callers pass the resource alone ("simulations").
    // US: "https://api.us-east-1.livingsecurity.ai/v1"
    BaseUrl = "https://api.eu-west-1.livingsecurity.ai/v1",

    Fn_FetchAll = (relativePath as text, baseQuery as record) as list =>
        // Walk next_page_token until null. Full pagination + 429 retry lives
        // in examples/powerbi/queries/Fn_FetchAll.pq.
        ...
in
    Fn_FetchAll
```

### Consultas tipadas por endpoint

Todo consumidor de `Fn_FetchAll` segue o mesmo formato: chamar o endpoint,
projetar uma lista fixa de colunas, descartar o discriminador `object` e
tipar os carimbos de data/hora como `datetimezone` para que as medidas de data não mudem silenciosamente
conforme o fuso horário da capacidade de atualização.

<CodeGroup>
  ```m Simulations.pq (GET /v1/simulations) theme={null}
  let
      Source = Fn_FetchAll("simulations", [ limit = "500" ]),

      AsTable = Table.FromRecords(
          Source,
          {
              "id", "object", "name", "description", "status", "vectors",
              "difficulty_level", "difficulty_mode",
              "started_at", "completed_at", "created_at", "updated_at"
          },
          MissingField.UseNull),

      Dropped = Table.RemoveColumns(AsTable, { "object" }),

      Flattened = Table.TransformColumns(
          Dropped,
          {
              {
                  "vectors",
                  each if _ = null then null
                       else Text.Combine(List.Transform(_, Text.From), ", "),
                  type text
              }
          }),

      Typed = Table.TransformColumnTypes(
          Flattened,
          {
              { "id",           type text },
              { "name",         type text },
              { "description",  type text },
              { "status",       type text },
              { "started_at",   type datetimezone },
              { "completed_at", type datetimezone },
              { "created_at",   type datetimezone },
              { "updated_at",   type datetimezone }
          })
  in
      Typed
  ```

  ```m Entities.pq (GET /v1/entities) theme={null}
  let
      Source = Fn_FetchAll("entities", [ limit = "500" ]),

      AsTable = Table.FromRecords(
          Source,
          { "id", "object", "type", "status", "display_name", "email",
            "created_at", "updated_at" },
          MissingField.UseNull),

      Dropped = Table.RemoveColumns(AsTable, { "object" }),

      Typed = Table.TransformColumnTypes(
          Dropped,
          {
              { "id",           type text },
              { "type",         type text },
              { "status",       type text },
              { "display_name", type text },
              { "email",        type text },
              { "created_at",   type datetimezone },
              { "updated_at",   type datetimezone }
          })
  in
      Typed
  ```
</CodeGroup>

<Tip>
  Uma lista explícita de colunas em `Table.FromRecords` mantém o esquema estável quando
  um endpoint retorna zero linhas. Um `Table.FromRecords({})` simples gera uma
  tabela sem colunas, e todas as etapas seguintes falham por falta dos campos.
</Tip>

## Excel

O mesmo Power Query (M) é executado em Obter e Transformar Dados do Excel — é o mesmo
mecanismo do Power BI Desktop. Use-o quando quiser uma tabela em planilha
em vez de um modelo semântico.

No Excel para Windows (Microsoft 365, Excel 2024, 2021, 2019 ou 2016):

1. **Dados → Obter Dados → De Outras Fontes → Consulta em Branco.**
2. No Editor do Power Query, **Página inicial → Editor Avançado**. Cole
   primeiro `Fn_FetchAll` e, em seguida, renomeie essa consulta para `Fn_FetchAll` (Configurações da
   Consulta → Nome, ou clique com o botão direito no painel Consultas → **Renomear**).
3. Repita para `Fn_GetJson` e, depois, para cada consulta de relatório.
4. **Página inicial → Fechar e Carregar → Fechar e Carregar Para…** abre a caixa de diálogo **Importar Dados**.
   Escolha **Apenas Criar Conexão** para `Fn_FetchAll` e
   `Fn_GetJson` (são funções, não tabelas). Escolha **Tabela** para
   cada consulta de relatório que você queira em uma planilha.
5. Na primeira atualização, escolha **Basic**: a chave de API como **Nome de usuário** e um
   valor de substituição, como `x`, como **Senha**. O Excel armazena uma credencial
   por URL de fonte de dados; as consultas seguintes que acessarem o mesmo host `/v1` a reutilizam.

<Note>
  **Fechar e Carregar Para…** pode aparecer esmaecido na primeira vez que você cria uma consulta em
  uma pasta de trabalho. Use **Fechar e Carregar** e, em seguida, no Excel, selecione **Dados →
  Consultas e Conexões**, clique com o botão direito na consulta e escolha **Carregar Para**.
  Esse caminho está documentado pela Microsoft para esse caso.
</Note>

Não é possível enviar `Living-Security-Version` do Power Query em uma
fonte autenticada — a mesma restrição do Power BI. Fixe a conta de serviço
ou passe `api_version` no registro da consulta. Consulte
[Versões de contrato e credenciais armazenadas](#versões-de-contrato-e-credenciais-armazenadas).

Consultas de relatório prontas (mapeamentos de colunas e diferenças em relação à exportação de um
provedor anterior):

<CardGroup cols={3}>
  <Card title="Conformidade por pessoa" href="/pt/developers/v1/reports/per-person-compliance">
    Matrículas em Treinamento na granularidade pessoa × atribuição.
  </Card>

  <Card title="Registro de phishing por e-mail" href="/pt/developers/v1/reports/email-phishing-log">
    Interações de phishing no nível de evento, últimos 12 meses.
  </Card>

  <Card title="Usuários mais suscetíveis" href="/pt/developers/v1/reports/most-susceptible-users">
    Consolidado por pessoa de cliques, credenciais e relatos nos últimos 12 meses.
  </Card>
</CardGroup>

## Conjuntos de dados de pré-visualização de relatórios (phishing por pessoa)

A família de pré-visualização de relatórios fornece conjuntos de dados para phishing por pessoa.
Ela exige `reporting:read` e acesso a relatórios para a sua organização.
Use o catálogo de conjuntos de dados abaixo para inspecionar seus esquemas e políticas de dados.

Descubra o catálogo em tempo real:

```text theme={null}
GET /v1/reporting/datasets
GET /v1/reporting/datasets/simulation_results
GET /v1/reporting/datasets/simulation_events
GET /v1/reporting/datasets/entities
GET /v1/reporting/datasets/entity_versions
GET /v1/reporting/datasets/cohort_memberships
```

Cada conjunto de dados declara granularidade, chave primária, junções, marca-d'água, política de correção e
política de exclusão.

### simulation\_results

* **Caminho:** `GET /v1/reporting/simulation_results`
* **Granularidade:** entidade × simulação
* **Filtros:** `simulation`, `entity`, `updated_after`, além de `limit` /
  `page_token`
* **Objeto de transmissão:** `reporting.simulation_result`
* **Chaves de junção:** `entity_id`, `simulation_id`

```m ReportingSimulationResults.pq (sketch) theme={null}
let
    Source = Fn_FetchAll(
        "reporting/simulation_results",
        [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "simulation_id", "status",
            "targeted_at", "delivered_at", "opened_at", "clicked_at",
            "credential_submitted_at", "reported_at",
            "email_subject", "sender_email", "difficulty_level",
            "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "entity_id",     type text },
            { "simulation_id", type text },
            { "status",        type text },
            { "targeted_at",   type datetimezone },
            { "updated_at",    type datetimezone }
        })
in
    Typed
```

Use `updated_after` como marca-d'água inferior inclusiva ao atualizar partições
recentes. As dimensões mutáveis dos resultados são reapresentadas no local, sob o mesmo `id`.

### simulation\_events

* **Caminho:** `GET /v1/reporting/simulation_events`
* **Granularidade:** um fato tipado por interação observada
* **Filtros:** `simulation`, `entity`, `event_type`, `occurred_after`, além de
  `limit` / `page_token`
* **Objeto de transmissão:** `reporting.simulation_event`
* **Tipos de evento:** tokens públicos fechados, como `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — não os tipos de atividade
  `phishing.simulation.*` brutos do ClickHouse
* **Dispositivo:** `device_type` é a classe derivada (os tokens observados incluem
  `desktop`, `mobile`, `tablet`, `unknown`). O `user_agent` bruto não está em
  `/v1`. O IP bruto nunca está em `/v1` (nunca é persistido; somente `ip_hash` com salt).

```m ReportingSimulationEvents.pq (sketch) theme={null}
let
    Source = Fn_FetchAll(
        "reporting/simulation_events",
        [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "simulation_id",
            "event_type", "occurred_at",
            "email_subject", "sender_email", "sender_domain", "difficulty_tier",
            "device_type",
            "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "entity_id",     type text },
            { "simulation_id", type text },
            { "event_type",    type text },
            { "occurred_at",   type datetimezone }
        })
in
    Typed
```

Use `occurred_after` para cargas incrementais de eventos. Os fatos são somente de inclusão;
as chegadas tardias são novas linhas.

### entities

* **Caminho:** `GET /v1/reporting/entities`
* **Granularidade:** uma linha atual por entidade
* **Filtros:** `type`, `updated_after`, além de `limit` / `page_token`
* **Objeto de transmissão:** `reporting.entity`
* **Chave de junção:** `id` / `entity_id` (mesmo valor)

Junte os fatos de phishing às pessoas com
`reporting.simulation_results.entity_id = reporting.entities.id`.
O `GET /v1/entities` operacional permanece em `entity:read` e é uma permissão
diferente.

```m ReportingEntities.pq theme={null}
let
    Source = Fn_FetchAll("reporting/entities", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "type", "status",
            "display_name", "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",          type text },
            { "entity_id",   type text },
            { "type",        type text },
            { "status",      type text },
            { "display_name", type text },
            { "created_at",  type datetimezone },
            { "updated_at",  type datetimezone }
        })
in
    Typed
```

### entity\_versions e cohort\_memberships

Esses dois conjuntos de dados têm uma exceção nomeada, somente para o estado atual:

> Não existe uma tabela autoritativa de histórico de versões ou de associações. Este conjunto de dados projeta a linha atual como um intervalo em aberto (`valid_to` é nulo, `is_current` é verdadeiro). `valid_from` é `created_at` ou, caso contrário, `updated_at` para versões de entidades, e o momento de observação da requisição para associações a Coortes. Intervalos históricos fechados não são fabricados.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Mesmos filtros que
  entities. `valid_from` é `created_at` ou, caso contrário, `updated_at`. Uma entidade
  com ambos os carimbos de data/hora nulos é omitida.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtros:
  `cohort`, `entity`, `updated_after`. `updated_at` é a última atualização da Coorte,
  de modo que a atualização incremental permaneça fiel. `valid_from` é o momento de
  observação da requisição. Junte `entity_id` a `reporting.entities.id`.

```m ReportingEntityVersions.pq theme={null}
let
    Source = Fn_FetchAll("reporting/entity_versions", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "type", "status", "display_name",
            "valid_from", "valid_to", "is_current",
            "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",         type text },
            { "entity_id",  type text },
            { "valid_from", type datetimezone },
            { "is_current", type logical }
        })
in
    Typed
```

```m ReportingCohortMemberships.pq theme={null}
let
    Source = Fn_FetchAll("reporting/cohort_memberships", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "cohort_id", "entity_id",
            "valid_from", "valid_to", "is_current",
            "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",         type text },
            { "cohort_id",  type text },
            { "entity_id",  type text },
            { "valid_from", type datetimezone },
            { "is_current", type logical },
            { "updated_at", type datetimezone }
        })
in
    Typed
```

## Modelo semântico em esquema estrela sobre /v1/activities

`/v1/activities` é o fluxo de eventos com carimbo de data/hora da organização. Ele não tem
medidas numéricas — toda quantidade em um relatório é uma contagem de linhas — o que torna o
esquema estrela uma escolha natural.

```text theme={null}
Dates (dimension)
      │  1
      ▼  ∗
  Activities (fact)   1  ∗  ActivityParticipants (bridge)   ∗  1   Entities (dimension)
```

* **`Activities`** é o fato do evento na granularidade real. Uma linha por atividade.
  Os participantes são removidos aqui e reconstruídos como uma tabela própria.
* **`ActivityParticipants`** existe porque os participantes são genuinamente
  de um-para-muitos, com funções como `actor` e `target`. Achatá-los em
  `Activities` duplicaria as linhas de atividade (inflando todas as contagens)
  ou descartaria os participantes adicionais.
  O relacionamento com `Activities` é o único relacionamento bidirecional do modelo,
  de modo que um segmentador de entidade alcança a tabela de fatos.
* **`Entities`** é a dimensão que nomeia os participantes. As linhas de participantes
  em uma atividade carregam `entity_id` e uma função, mas não um nome de exibição.
* **`Dates`** é gerada em M (zero chamadas de API) e marcada como a tabela de datas do modelo.
  É isso que torna válida a inteligência de tempo do DAX
  (`DATESINPERIOD`, `DATEADD`). As colunas de nome do mês e do dia da semana declaram
  `sortByColumn` em suas equivalentes numéricas, para que os visuais não ordenem os meses
  em ordem alfabética.

Todas as medidas são explícitas (o modelo define `discourageImplicitMeasures`): total de
atividades, entidades ativas, atividades por entidade ativa, dias com atividade
e uma média diária móvel de 7 dias.

### Formato da tabela ponte

`ActivityParticipants` percorre `/v1/activities` e expande a matriz `participants`
em uma linha por participante:

```m ActivityParticipants.pq theme={null}
let
    WindowStart = DateTime.AddZone(RangeStart, 0),
    WindowEnd   = DateTime.AddZone(RangeEnd, 0),
    ToIso = (value as datetimezone) as text =>
        DateTimeZone.ToText(value,
            [ Format = "yyyy-MM-dd'T'HH:mm:ss'Z'", Culture = "en-US" ]),

    Source = Fn_FetchAll(
        "activities",
        [
            limit      = "100",
            start_date = ToIso(WindowStart - #duration(0, 0, 1, 0)),
            end_date   = ToIso(WindowEnd + #duration(0, 0, 1, 0))
        ]),

    AsTable = Table.FromRecords(
        Source,
        { "id", "object", "activity_type", "event_ts",
          "service_id", "participants" },
        MissingField.UseNull),

    Selected = Table.SelectColumns(AsTable, { "id", "event_ts", "participants" }),
    Renamed  = Table.RenameColumns(Selected, { { "id", "activity_id" } }),
    Expanded = Table.ExpandListColumn(Renamed, "participants"),
    NonNull  = Table.SelectRows(Expanded, each [participants] <> null),
    Fields   = Table.ExpandRecordColumn(
        NonNull, "participants",
        { "role", "entity_id", "display_name" }),

    Typed = Table.TransformColumnTypes(
        Fields,
        {
            { "activity_id",  type text },
            { "event_ts",     type datetimezone },
            { "role",         type text },
            { "entity_id",    type text },
            { "display_name", type text }
        }),

    Filtered = Table.SelectRows(
        Typed,
        each [event_ts] >= WindowStart and [event_ts] < WindowEnd)
in
    Filtered
```

`ActivityParticipants` percorre `/v1/activities` de forma independente, em vez de
referenciar a consulta `Activities`. No Power BI, duas consultas carregadas como duas
tabelas são buscadas de forma independente no momento da atualização. Mantê-las independentes
permite que o serviço atualize as partições de ambas as tabelas em paralelo.

## Atualização incremental

As duas tabelas de granularidade de fatos (`Activities` e `ActivityParticipants`) declaram uma
política de atualização incremental do Power BI: manter 5 anos de partições anuais e
reimportar os últimos 14 dias a cada atualização.

O mecanismo é um par de parâmetros DateTime, `RangeStart` e `RangeEnd`,
que o serviço do Power BI revincula por partição no momento da atualização. Cada consulta de fatos
incorpora esses parâmetros aos filtros `start_date` e `end_date` da API,
de modo que o volume de requisições por partição é proporcional à partição,
e não ao histórico total. A consulta amplia a janela da API em um minuto de cada
lado e reaplica localmente o limite exato semiaberto de `[RangeStart, RangeEnd)`,
de modo que a correção das partições não depende de o servidor
tratar seus limites como inclusivos ou exclusivos.

```m Activities.pq — the fold that makes incremental refresh work theme={null}
let
    WindowStart = DateTime.AddZone(RangeStart, 0),
    WindowEnd   = DateTime.AddZone(RangeEnd, 0),
    ToIso = (value as datetimezone) as text =>
        DateTimeZone.ToText(value,
            [ Format = "yyyy-MM-dd'T'HH:mm:ss'Z'", Culture = "en-US" ]),

    Source = Fn_FetchAll(
        "activities",
        [
            limit      = "100",
            start_date = ToIso(WindowStart - #duration(0, 0, 1, 0)),
            end_date   = ToIso(WindowEnd + #duration(0, 0, 1, 0))
        ]),

    AsTable = Table.FromRecords(
        Source,
        { "id", "object", "activity_type", "event_ts",
          "service_id", "participants" },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object", "participants" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "activity_type", type text },
            { "event_ts",      type datetimezone },
            { "service_id",    type text }
        }),

    Filtered = Table.SelectRows(
        Typed,
        each [event_ts] >= WindowStart and [event_ts] < WindowEnd),

    WithDate = Table.AddColumn(
        Filtered, "event_date", each Date.From([event_ts]), type date)
in
    WithDate
```

A primeira atualização após uma implantação aplica a política: uma carga histórica completa,
lenta. Todas as atualizações seguintes afetam apenas a janela incremental.

Medido em um conjunto de dados com \~158.000 atividades e \~2.300 entidades:

| Atualização | Tempo total |
| - | - |
| Primeira (aplicação da política, carga histórica completa) | \~31 min |
| Todas as seguintes | **\~15 s** |

<Warning>
  O endpoint REST clássico de atualização do Power BI (`POST .../refreshes` apenas com
  `notifyOption`) ignora silenciosamente as políticas de atualização e faz uma varredura completa
  todas as vezes. Para aplicar a política por meio da API REST, use a atualização avançada: envie
  `type`, `commitMode` e `applyRefreshPolicy` no corpo. As atualizações
  acionadas pelo agendador do serviço do Power BI sempre aplicam as políticas — essa
  peculiaridade é específica da chamada REST.
</Warning>

<Note>
  A janela de 14 dias é deliberadamente generosa. A ingestão de atividades não é
  somente de inclusão no instante atual: os preenchimentos retroativos dos conectores inserem eventos com carimbos de data/hora passados.
  Uma janela de 1 a 2 dias os perderia permanentemente.
</Note>

## Ampliando a janela de atualização na API

`GET /v1/activities` aceita os filtros `start_date` e `end_date` nos quais os
parâmetros do Power BI são incorporados, filtrando `event_ts` entre os limites.
`Entities` é uma dimensão, portanto este exemplo a atualiza integralmente a cada vez,
e não de forma incremental.

Os conjuntos de dados de relatórios usam suas próprias marcas-d'água (`updated_after`,
`occurred_after`) em vez da janela de datas de activities.

## Limites de taxa durante a atualização

`/v1` impõe orçamentos separados por chave e por IP (consulte
[limites de taxa](/pt/developers/v1/rate-limits)). Respeite `429` + `Retry-After`; não
codifique um orçamento de requisições em seu modelo. Duas propriedades das atualizações do Power BI
são relevantes:

* **A varredura de uma única tabela é sequencial.** Cada requisição de página precisa do
  token da página anterior, portanto uma tabela fica limitada pela latência, independentemente do tamanho
  da página.
* **As atualizações simultâneas de tabelas se multiplicam.** Várias tabelas grandes sendo atualizadas
  em paralelo é o caso que atinge o teto, e é por isso que o caminho de nova tentativa para 429
  em `Fn_FetchAll` é importante. Prefira menos tabelas em paralelo a
  presumir simultaneidade ilimitada.

A nova tentativa foi testada contra um limitador real saturado. Uma atualização
executada enquanto uma carga de trabalho paralela mantinha deliberadamente a chave no limite
foi concluída e produziu as mesmas contagens de linhas que uma linha de base sem contenção —
as novas tentativas retornaram páginas completas, sem lacunas.

## Versões de contrato e credenciais armazenadas

`/v1` aceita um cabeçalho de requisição `Living-Security-Version` para fixar uma versão de contrato
publicada (consulte o [início rápido da API](/pt/developers/v1/quickstart)). O Power
Query não consegue enviar esse cabeçalho quando a fonte de dados usa credenciais armazenadas —
somente fontes de dados anônimas permitem cabeçalhos personalizados. Adicioná-lo faz a atualização falhar com:

```text theme={null}
Expression.Error: The 'Living-Security-Version' header is only supported when
connecting anonymously.
Microsoft.Data.Mashup.ErrorCode = 10348.
```

Mantenha a chave nas credenciais armazenadas do Power BI e omita o cabeçalho personalizado.
Sem uma versão explícita na requisição, a API usa o `contract_version` fixado da conta de serviço
ou a versão publicada mais recente, se não houver versão fixada.
Confirme a versão de contrato da conta com o administrador da API antes de
publicar uma atualização. Consulte o [changelog da API](/pt/developers/v1/changelog) para
conhecer as versões compatíveis.

Se uma atualização começar a se comportar de maneira inesperada, inspecione o cabeçalho de resposta
`Living-Security-Version` da API para ver qual contrato
realmente atendeu à requisição.

## Atualize e verifique seu modelo

Publique o modelo semântico no seu workspace do Power BI, vincule sua credencial armazenada
à fonte `/v1` regional e execute uma atualização antes de ativar um
agendamento. Revise o histórico de atualizações em busca de falhas ou leituras incompletas.

Confira as contagens de linhas com uma leitura independente da API usando os mesmos filtros.
O [guia de paginação](/pt/developers/v1/pagination) inclui exemplos em Python e cURL
para ler um conjunto de resultados completo. Uma atualização que lê apenas a
primeira página pode parecer bem-sucedida em um conjunto de dados pequeno; compare as contagens antes de
confiar no relatório.

## Escolhas de modelagem

* **Os totais de campanha e os resultados por pessoa têm granularidades diferentes.**
  `SimulationResults` chama `GET /v1/simulations/{id}/results`
  (um GET por execução). Para phishing por pessoa, adicione consultas para
  `GET /v1/reporting/simulation_results` e
  `GET /v1/reporting/simulation_events` (ou o `GET /v1/simulation_results`
  operacional, se você já possui `simulation:read`).
* **`vectors` em `/v1/simulations` é achatado em uma string unida por vírgulas.**
  Uma coluna do tipo lista não pode ser segmentada como uma dimensão real. Se uma página de relatório
  precisar filtrar por vetor, adicione uma tabela ponte entre simulação e vetor.

## Próximos passos

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/pt/developers/developer-hub/overview">
    Crie uma conta de serviço e uma chave de API para o Power BI.
  </Card>

  <Card title="Início rápido da API" href="/pt/developers/v1/quickstart">
    Autentique as requisições de `/v1` com autenticação Basic.
  </Card>

  <Card title="Paginação" href="/pt/developers/v1/pagination">
    O envelope de lista, os tokens de cursor e a estabilidade dos filtros ao longo de uma varredura.
  </Card>

  <Card title="Limites de taxa" href="/pt/developers/v1/rate-limits">
    Orçamentos por chave e por IP, o contrato 429 e orientações de nova tentativa.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.