> ## 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 **reporting** em versão preliminar para 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
`Fn_GetJson` e, em seguida, vincule a credencial da fonte de dados a esse host. Consulte
[Regiões](/pt-BR/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-BR/developers/v1/authentication) para conhecer as
opções de credenciais e o [Developer Hub](/pt-BR/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) versus
  resultados por pessoa e eventos tipados (reporting em versão preliminar).

## 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-BR/developers/developer-hub/api-keys).
* Power BI Desktop ou um workspace do Power BI / Fabric em que seja possível 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 | Usar | Permissão | Granularidade |
| - | - | - | - |
| Totais do funil da campanha | `GET /v1/simulations/{id}/results` | `simulation:read` | Uma linha por simulação (entregues, abertos, clicados, …) |
| Resultados por pessoa (operacional) | `GET /v1/simulation_results` | `simulation:read` | Uma linha por entidade × simulação |
| Resultados por pessoa (BI / reporting) | `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. Junção por `reporting.simulation_results.entity_id = reporting.entities.id` |
| Versões atuais das entidades | `GET /v1/reporting/entity_versions` | `reporting:read` | Um intervalo atual sem data final 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
**reporting** em versão preliminar abaixo. Uma chave que tenha recebido apenas `reporting:read` não pode
chamar `/v1/simulation_results` operacional (a API retorna `403`).

### Escala de dificuldade

A dificuldade da simulação é um número inteiro de 1 a 5 em `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), nas propriedades de `/v1/activities`
(`difficulty_tier`) e em `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` é a mais fácil e `5` é a mais difícil. Simulações adaptativas e progressivas não têm
um único valor em nível de 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 curada da API Pública de `/api/v1`. Autentique-se no host `/v1` regional
  da organização com uma chave de API do Developer Hub. Consulte
  [Regiões](/pt-BR/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 é 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 construído 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**. Insira um espaço reservado, como
    `x`, no campo **Senha**. O Serviço do Power BI rejeita uma senha em branco,
    mesmo que a API ignore esse campo. Usar a chave como senha com um nome de usuário de
    espaço reservado também funciona.

    Não adicione um cabeçalho `Authorization` em M. Ele funciona na sua área de trabalho, mas
    incorpora a chave ao arquivo de consulta e entra em conflito com 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: ele 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, e 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`. Reaponte esses modelos para `…/v1`.
  </Accordion>

  <Accordion title="Defina limit=500 explicitamente em toda consulta de lista">
    O tamanho de página padrão em todo endpoint de lista de `/v1` é 25. Deixá-lo no padrão
    multiplica o número de solicitações para os mesmos dados. Os valores de consulta devem ser texto —
    `limit = "500"` em M, 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-BR/developers/v1/pagination)). Suas propriedades principais:

* **URL base `/v1` estática, `RelativePath` e `Query` dinâmicos.** Necessário para
  a atualização agendada e para o teste de credenciais.
* **`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é a atualização expirar. `5xx` deliberadamente não é
  repetido — um erro de servidor deve ficar visível, 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 solicitação está morta.
* **`Binary.Buffer` antes de `Json.Document`.** Sem o uso de 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 de filtros e 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
timestamps como `datetimezone` para que as medidas de data não se desloquem silenciosamente pelo
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. **Data → Get Data → From Other Sources → Blank Query.**
2. No Editor do Power Query, **Home → Editor Avançado**. Cole
   `Fn_FetchAll` primeiro e, em seguida, renomeie essa consulta como `Fn_FetchAll` (Query
   Settings → Name, ou clique com o botão direito no painel Queries → **Rename**).
3. Repita para `Fn_GetJson` e, depois, para cada consulta de relatório.
4. **Home → Close & Load → Close & Load To…** abre a caixa de diálogo **Import Data**.
   Escolha **Only Create Connection** para `Fn_FetchAll` e
   `Fn_GetJson` (elas são funções, não tabelas). Escolha **Table** para
   cada consulta de relatório que você queira em uma planilha.
5. Na primeira atualização, escolha **Basic**: a chave de API como **User name** e um
   espaço reservado, como `x`, como **Password**. O Excel armazena uma credencial
   por URL de fonte de dados; as consultas seguintes que acessarem o mesmo host `/v1` a
   reutilizam.

<Note>
  **Close & Load To…** pode aparecer esmaecido na primeira vez que você cria uma consulta em
  uma pasta de trabalho. Use **Close & Load** e, em seguida, no Excel, selecione **Data →
  Queries & Connections**, clique com o botão direito na consulta e escolha **Load To**.
  Esse caminho é 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 divergências em relação à exportação
de um provedor anterior):

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

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

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

## Conjuntos de dados de reporting em versão preliminar (phishing por pessoa)

A família de reporting em versão preliminar fornece conjuntos de dados para phishing por pessoa.
Ela exige `reporting:read` e acesso a reporting 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 ativo:

```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 mesmo lugar, 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 brutos de atividade
  `phishing.simulation.*` 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; apenas `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 acréscimo;
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)

Una 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 tabela autoritativa de histórico de versões ou de associações. Este conjunto de dados projeta a linha atual como um intervalo sem data final (`valid_to` é nulo, `is_current` é verdadeiro). `valid_from` é `created_at` ou, se ausente, `updated_at` para versões de entidades, e o horário de observação da solicitação para associações a Coortes. Intervalos históricos fechados não são fabricados.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Mesmos filtros de
  entities. `valid_from` é `created_at` ou, se ausente, `updated_at`. Uma entidade
  com ambos os timestamps nulos é omitida.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtros:
  `cohort`, `entity`, `updated_after`. `updated_at` é a última atualização da Coorte,
  para que a atualização incremental permaneça fiel. `valid_from` é o horário de
  observação da solicitação. Una `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 em /v1/activities

`/v1/activities` é o fluxo de eventos com timestamp 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 de 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 de fato
  um-para-muitos, com funções como `actor` e `target`. Achatá-los em
  `Activities` duplicaria linhas de atividade (inflando toda contagem)
  ou descartaria participantes adicionais.
  O relacionamento com `Activities` é o único relacionamento bidirecional
  do modelo, de modo que um filtro de segmentação por 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 seus equivalentes numéricos 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 de ponte

`ActivityParticipants` percorre `/v1/activities` e expande a matriz `participants`
para 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 buscam dados 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 reassocia a cada 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 solicitaçõ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 da partição 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. Toda atualização seguinte afeta 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` com apenas
  `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 da solicitação. As atualizações
  disparadas 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 acréscimo no instante atual: os preenchimentos retroativos de conectores inserem eventos com timestamps passados.
  Uma janela de 1 a 2 dias os perderia permanentemente.
</Note>

## Ampliação da 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,
em vez de forma incremental.

Os conjuntos de dados de reporting 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-BR/developers/v1/rate-limits)). Respeite `429` + `Retry-After`; não
codifique um orçamento de solicitações no seu modelo. Duas propriedades das atualizações
do Power BI são relevantes:

* **A varredura de uma única tabela é sequencial.** Cada solicitaçã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 em seu teto
foi concluída e produziu as mesmas contagens de linhas de 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 solicitação `Living-Security-Version` para fixar uma versão
de contrato publicada (consulte o [início rápido da API](/pt-BR/developers/v1/quickstart)). O Power
Query não pode 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 solicitação, a API usa o `contract_version` fixado da conta de serviço
ou a versão publicada mais recente, se nenhuma fixação estiver definida.
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-BR/developers/v1/changelog) para conhecer
as versões compatíveis.

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

## Atualize e verifique o seu modelo

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

Compare as contagens de linhas com uma leitura independente da API usando os mesmos filtros.
O [guia de paginação](/pt-BR/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

* **Totais de campanha e 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 `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 de ponte entre simulação e vetor.

## Próximas etapas

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/pt-BR/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-BR/developers/v1/quickstart">
    Autentique as solicitações de `/v1` com autenticação Basic.
  </Card>

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

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


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