> ## 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

> Connecteu Power BI a la Public API de Living Security amb Power Query, construïu un model d'esquema en estrella sobre /v1 i trieu entre conjunts de dades operatius i d'informes per a phishing.

Aquesta guia és un exemple complet de lectura de `/v1` a Power BI. Cobreix
el patró de connectivitat de Power Query (M), un model semàntic d'esquema en estrella construït
sobre `/v1/activities`, l'actualització incremental i quan cal utilitzar els conjunts de dades
en versió preliminar de **reporting** per als resultats de phishing per persona.

Els exemples utilitzen l'amfitrió de producció dels EUA
(`https://api.us-east-1.livingsecurity.ai/v1`). Per a una organització de la UE, utilitzeu
`https://api.eu-west-1.livingsecurity.ai/v1` tant a `Fn_FetchAll` com a
`Fn_GetJson` i, a continuació, vinculeu la credencial de l'origen de dades a aquest amfitrió. Vegeu
[Regions](/ca/developers/v1/regions).

Aquesta guia utilitza credencials Basic emmagatzemades amb una clau d'API. No cal cap
client OAuth. Vegeu [Authentication](/ca/developers/v1/authentication) per conèixer les
opcions de credencials i [Developer Hub](/ca/developers/developer-hub/overview)
per a l'accés i la configuració.

## Per a què serveix aquest exemple

Utilitzeu-lo quan necessiteu:

* Carregar els endpoints de llista de `/v1` en un conjunt de dades de Power BI amb paginació
  per cursor correcta i un reintent per límit de velocitat.
* Construir un model semàntic en què les mesures ("entitats actives", "activitats per
  entitat activa") siguin dimensionals en lloc de comptar-se a partir d'una taula plana.
* Reduir una actualització programada de Power BI d'un recorregut complet de l'historial a
  una petita finestra diària mitjançant l'actualització incremental.
* Triar el grau de detall correcte per a phishing: totals de campanya (operatiu) enfront de
  resultats per persona i esdeveniments tipats (versió preliminar de reporting).

## Requisits previs

* Una clau d'API amb els permisos que necessitin les vostres consultes:
  * Exemple operatiu: `activity:read`, `entity:read` i
    `simulation:read`.
  * Phishing per persona mitjançant conjunts de dades de reporting: `reporting:read`.
    Vegeu [API keys](/ca/developers/developer-hub/api-keys).
* Power BI Desktop, o un espai de treball de Power BI / Fabric on pugueu crear un
  model semàntic i programar l'actualització.

## Trieu el grau de detall correcte

`/v1` exposa dues superfícies relacionades per a phishing. No són intercanviables.

| Necessitat | Ús | Permís | Grau de detall |
| - | - | - | - |
| Totals de l'embut de la campanya | `GET /v1/simulations/{id}/results` | `simulation:read` | Una fila per simulació (lliurats, oberts, clicats, …) |
| Resultats per persona (operatiu) | `GET /v1/simulation_results` | `simulation:read` | Una fila per entitat × simulació |
| Resultats per persona (BI / reporting) | `GET /v1/reporting/simulation_results` | `reporting:read` | Mateix grau de detall, noms de camp de BI (`entity_id`, `simulation_id`), marca de nivell `updated_after` |
| Fets d'interacció tipats | `GET /v1/reporting/simulation_events` | `reporting:read` | Un fet per interacció (`simulation.link_clicked`, …), `occurred_after` |
| Dimensió actual de Workforce | `GET /v1/reporting/entities` | `reporting:read` | Una fila actual per entitat. Combineu `reporting.simulation_results.entity_id = reporting.entities.id` |
| Versions actuals de l'entitat | `GET /v1/reporting/entity_versions` | `reporting:read` | Un interval actual obert per entitat. No existeix cap taula d'historial de versions |
| Pertinences actuals a Cohort | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Un fet actual per `(cohort_id, entity_id)`. No existeix cap taula d'historial de pertinences |
| Eix d'unió per a tots els tipus d'activitat | `GET /v1/activities` | `activity:read` | Un esdeveniment per activitat, amb `participants[]` imbricat |

Les consultes d'exemple de Power BI següents criden les rutes **operatives**
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Per als informes de phishing per persona, és preferible la família en
versió preliminar de **reporting** descrita a continuació. Una clau amb permís només de `reporting:read` no pot
cridar l'operatiu `/v1/simulation_results` (l'API retorna `403`).

### Escala de dificultat

La dificultat de la simulació és un enter de l'1 al 5 a `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), les propietats `/v1/activities`
(`difficulty_tier`) i `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` és la més fàcil i `5` la més difícil. Les simulacions adaptatives i progressives no tenen
cap valor únic a nivell de campanya, de manera que `/v1/simulations.difficulty_level` és `null`
mentre que la `difficulty_level` per destinatari als resultats encara es pot establir. Per fer la correspondència amb
exportacions antigues de nivell baix/mitjà/alt, tracteu `1–2` com a baix, `3` com a mitjà i `4–5` com a
alt.

<Warning>
  No construïu noves integracions de BI sobre crides de xarxa no documentades del Dashboard
  ni sobre la vista general antiga i curada de la Public API de `/api/v1`. Autentiqueu-vos a l'amfitrió `/v1` regional
  de l'organització amb una clau d'API de Developer Hub. Vegeu
  [Regions](/ca/developers/v1/regions).
</Warning>

## Patró de connectivitat (Power Query)

Quatre regles sustenten totes les consultes de l'exemple. Cadascuna és imposada pel
servei de Power BI i no respon a preferències d'estil.

<AccordionGroup>
  <Accordion title="Mantingueu estàtica l'URL base de Web.Contents i feu que acabi a /v1">
    Poseu tot el que sigui dinàmic a `RelativePath` i `Query`. El primer argument de
    `Web.Contents` ha de ser una cadena literal que acabi a l'arrel `/v1`
    regional de l'organització:

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

    Els qui crideu passeu només el recurs (`"simulations"`, no
    `"v1/simulations"`). No enganxeu un amfitrió dels EUA per a una organització de la UE.

    Si el primer argument es construeix per concatenació de cadenes, el servei de Power BI
    no pot identificar estàticament l'origen de dades i l'actualització programada falla amb
    un error de "dynamic data source", encara que la consulta funcioni a Power BI
    Desktop.
  </Accordion>

  <Accordion title="Proporcioneu la clau d'API com a credencial Basic emmagatzemada, no al text de la consulta">
    Quan Power BI us demani les credencials de l'origen de dades `/v1`, trieu
    **Basic** i enganxeu la clau d'API com a **username**. Poseu un marcador de posició
    com ara `x` al camp **password**. El servei de Power BI rebutja una contrasenya
    en blanc tot i que l'API ignora aquest camp. També funciona posar la clau com a contrasenya amb un
    nom d'usuari de marcador de posició.

    No afegiu una capçalera `Authorization` en M. Funciona a l'escriptori, però
    incrusta la clau al fitxer de la consulta i interfereix amb l'actualització programada.
  </Accordion>

  <Accordion title="Deixeu que s'executi la prova de connexió del diàleg de credencials">
    `GET /v1` és autenticat: retorna `200` amb una clau vàlida i `401`
    sense. Apunteu l'origen de dades a `…/v1` i **no** ometeu la prova:
    una clau correcta es desa, una de revocada o mal formada falla immediatament.

    Els exemples antics utilitzaven l'amfitrió sense ruta com a origen de dades i deien d'ometre
    la prova perquè `GET /` sempre és `404`. Torneu a apuntar aquests models a `…/v1`.
  </Accordion>

  <Accordion title="Establiu limit=500 explícitament a cada consulta de llista">
    La mida de pàgina per defecte a tots els endpoints de llista de `/v1` és 25. Deixar-la per defecte
    multiplica el nombre de sol·licituds per a les mateixes dades. Els valors de consulta han de ser text:
    `limit = "500"` en M, no `limit = 500`.
  </Accordion>
</AccordionGroup>

### Funció de paginació compartida

Totes les consultes de l'exemple criden una única `Fn_FetchAll` que recorre
`next_page_token` fins que és `null` (vegeu
[pagination](/ca/developers/v1/pagination)). Les seves propietats destacables:

* **URL base `/v1` estàtica, `RelativePath` i `Query` dinàmics.** Necessari per a
  l'actualització programada i per a la prova de credencials.
* **`ManualStatusHandling = {429}` amb un reintent limitat.** La funció llegeix
  `Retry-After` quan Power Query l'exposa; en cas contrari, espera 30 segons i
  s'atura després de 5 respostes `429` consecutives amb un error explícit en lloc de
  entrar en bucle fins que l'actualització caduqui. `5xx` deliberadament no es
  reintenta: un error del servidor ha de aflorar, no produir en silenci un conjunt de dades
  parcial.
* **`Timeout` de 60 segons.** Les pàgines normalment responen en molt menys d'un segon, de
  manera que un minut de silenci vol dir que la sol·licitud és morta.
* **`Binary.Buffer` abans de `Json.Document`.** Sense memòria intermèdia, el motor de
  mashup pot tornar a sol·licitar el flux de resposta si el llegeix més d'una vegada.
* **`baseQuery` es passa sense canvis a cada iteració.** Els testimonis de pàgina són
  cursors opacs i signats, vinculats a l'organització, la credencial, la versió del
  contracte i l'empremta dels filtres i de l'ordenació. Canviar un filtre
  durant el recorregut es rebutja com a testimoni no vàlid en lloc de reiniciar des de la pàgina
  u.

```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
```

### Consultes tipades per endpoint

Tots els consumidors de `Fn_FetchAll` segueixen la mateixa forma: criden l'endpoint,
projecten una llista fixa de columnes, descarten el discriminador `object` i
tipen les marques de temps com a `datetimezone` perquè les mesures de data no es desplacin en silenci segons
la zona horària de la capacitat d'actualització.

<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>
  Una llista de columnes explícita a `Table.FromRecords` manté estable l'esquema quan
  un endpoint retorna zero files. Un `Table.FromRecords({})` simple produeix una
  taula sense columnes i tots els passos posteriors fallen per manca dels camps.
</Tip>

## Excel

El mateix Power Query (M) s'executa a Get & Transform d'Excel: és el mateix
motor que Power BI Desktop. Utilitzeu-lo quan vulgueu una taula de full de càlcul
en lloc d'un model semàntic.

A Excel per a Windows (Microsoft 365, Excel 2024, 2021, 2019 o 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. A l'Editor de Power Query, **Home → Advanced Editor**. Enganxeu-hi
   primer `Fn_FetchAll` i després canvieu el nom d'aquesta consulta a `Fn_FetchAll` (Query
   Settings → Name, o feu clic amb el botó dret al tauler Queries → **Rename**).
3. Repetiu-ho per a `Fn_GetJson` i després per a cada consulta d'informe.
4. **Home → Close & Load → Close & Load To…** obre el diàleg **Import Data**.
   Trieu **Only Create Connection** per a `Fn_FetchAll` i
   `Fn_GetJson` (són funcions, no taules). Trieu **Table** per a
   cada consulta d'informe que vulgueu en un full de càlcul.
5. A la primera actualització, trieu **Basic**: la clau d'API com a **User name** i un
   marcador de posició com ara `x` com a **Password**. Excel desa una credencial
   per URL d'origen de dades; les consultes posteriors que arribin al mateix amfitrió `/v1` la
   reutilitzen.

<Note>
  **Close & Load To…** pot aparèixer atenuat la primera vegada que creeu una consulta en
  un llibre. Utilitzeu **Close & Load**, després a Excel seleccioneu **Data →
  Queries & Connections**, feu clic amb el botó dret a la consulta i trieu **Load To**.
  Microsoft documenta aquest camí per a aquest cas.
</Note>

No es pot enviar `Living-Security-Version` des de Power Query en un origen
autenticat: la mateixa restricció que a Power BI. Fixeu el compte de servei
o passeu `api_version` al registre de la consulta. Vegeu
[Contract versions and stored credentials](#versions-de-contracte-i-credencials-emmagatzemades).

Consultes d'informe preparades (assignacions de columnes i diferències respecte a
l'exportació d'un proveïdor anterior):

<CardGroup cols={3}>
  <Card title="Compliment per persona" href="/ca/developers/v1/reports/per-person-compliance">
    Inscripcions a Training al grau de detall persona × assignació.
  </Card>

  <Card title="Registre de phishing per correu electrònic" href="/ca/developers/v1/reports/email-phishing-log">
    Interaccions de phishing a nivell d'esdeveniment, darrers 12 mesos.
  </Card>

  <Card title="Usuaris més susceptibles" href="/ca/developers/v1/reports/most-susceptible-users">
    Resum per persona de clics, credencials i informes dels darrers 12 mesos.
  </Card>
</CardGroup>

## Conjunts de dades de reporting en versió preliminar (phishing per persona)

La família de reporting en versió preliminar ofereix conjunts de dades per al phishing per persona.
Requereix `reporting:read` i accés a reporting per a la vostra organització.
Utilitzeu el catàleg de conjunts de dades següent per inspeccionar-ne els esquemes i les polítiques de dades.

Descobriu el catàleg en directe:

```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 conjunt de dades declara el grau de detall, la clau primària, les unions, la marca de nivell, la correcció i la
política d'eliminació.

### simulation\_results

* **Ruta:** `GET /v1/reporting/simulation_results`
* **Grau de detall:** entitat × simulació
* **Filtres:** `simulation`, `entity`, `updated_after`, més `limit` /
  `page_token`
* **Objecte de xarxa:** `reporting.simulation_result`
* **Claus d'unió:** `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
```

Utilitzeu `updated_after` com a marca de nivell inferior inclusiva quan actualitzeu particions
recents. Les dimensions mutables dels resultats es tornen a enunciar in situ sota el mateix `id`.

### simulation\_events

* **Ruta:** `GET /v1/reporting/simulation_events`
* **Grau de detall:** un fet tipat per interacció observada
* **Filtres:** `simulation`, `entity`, `event_type`, `occurred_after`, més
  `limit` / `page_token`
* **Objecte de xarxa:** `reporting.simulation_event`
* **Tipus d'esdeveniment:** testimonis públics tancats com ara `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — no els tipus d'activitat
  `phishing.simulation.*` en brut de ClickHouse
* **Dispositiu:** `device_type` és la classe derivada (els testimonis observats inclouen
  `desktop`, `mobile`, `tablet`, `unknown`). El `user_agent` en brut no és a
  `/v1`. La IP en brut mai és a `/v1` (no es conserva mai; només `ip_hash` amb sal).

```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
```

Utilitzeu `occurred_after` per a càrregues incrementals d'esdeveniments. Els fets són només d'afegiment;
les arribades tardanes són files noves.

### entities

* **Ruta:** `GET /v1/reporting/entities`
* **Grau de detall:** una fila actual per entitat
* **Filtres:** `type`, `updated_after`, més `limit` / `page_token`
* **Objecte de xarxa:** `reporting.entity`
* **Clau d'unió:** `id` / `entity_id` (mateix valor)

Combineu els fets de phishing amb les persones mitjançant
`reporting.simulation_results.entity_id = reporting.entities.id`.
El `GET /v1/entities` operatiu es manté a `entity:read` i és un permís
diferent.

```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 i cohort\_memberships

Aquests dos conjunts de dades inclouen una excepció anomenada només d'estat actual:

> No existeix cap taula autoritzada d'historial de versions ni de pertinences. Aquest conjunt de dades projecta la fila actual com un interval obert (`valid_to` és nul, `is_current` és true). `valid_from` és `created_at` o, altrament, `updated_at` per a les versions d'entitat, i l'hora d'observació de la sol·licitud per a les pertinences a Cohort. No es fabriquen intervals històrics tancats.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Mateixos filtres que
  entities. `valid_from` és `created_at` o, altrament, `updated_at`. Una entitat
  amb les dues marques de temps nul·les s'omet.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtres:
  `cohort`, `entity`, `updated_after`. `updated_at` és l'última actualització del Cohort
  perquè l'actualització incremental sigui fidedigna. `valid_from` és l'hora
  d'observació de la sol·licitud. Combineu `entity_id` amb `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
```

## Model semàntic d'esquema en estrella sobre /v1/activities

`/v1/activities` és el flux d'esdeveniments amb marca de temps de l'organització. No té
mesures numèriques: cada quantitat d'un informe és un recompte de files, cosa que fa que un
esquema en estrella sigui una opció natural.

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

* **`Activities`** és el fet d'esdeveniment amb el grau de detall real. Una fila per activitat.
  Els participants s'hi descarten i es reconstrueixen com a taula pròpia.
* **`ActivityParticipants`** existeix perquè els participants són realment
  d'un a molts, amb rols com ara `actor` i `target`. Aplanar-los sobre
  `Activities` duplicaria les files d'activitat (inflant cada recompte)
  o descartaria participants addicionals.
  La relació amb `Activities` és l'única relació bidireccional del model,
  de manera que un segmentador d'entitats arriba a la taula de fets.
* **`Entities`** és la dimensió que nomena els participants. Les files de participants
  d'una activitat porten `entity_id` i un rol, però no un nom per mostrar.
* **`Dates`** es genera en M (zero crides a l'API) i es marca com la taula de dates
  del model. Això és el que fa legal la intel·ligència de temps de DAX
  (`DATESINPERIOD`, `DATEADD`). Les columnes de nom de mes i de dia de la setmana declaren
  `sortByColumn` sobre els seus bessons numèrics perquè els elements visuals no ordenin els mesos
  alfabèticament.

Totes les mesures són explícites (el model estableix `discourageImplicitMeasures`): total
d'activitats, entitats actives, activitats per entitat activa, dies amb activitat
i una mitjana diària de 7 dies mòbils.

### Forma de la taula pont

`ActivityParticipants` recorre `/v1/activities` i expandeix la matriu `participants`
a una fila per participant:

```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` recorre `/v1/activities` de manera independent en lloc de
referenciar la consulta `Activities`. A Power BI, dues consultes carregades com a dues
taules es recuperen cadascuna de manera independent en el moment de l'actualització. Mantenir-les independents
permet que el servei actualitzi en paral·lel les particions de totes dues taules.

## Actualització incremental

Les dues taules de grau de fet (`Activities` i `ActivityParticipants`) declaren una
política d'actualització incremental de Power BI: conservar 5 anys de particions anuals i
tornar a importar els darrers 14 dies a cada actualització.

El mecanisme és un parell de paràmetres DateTime, `RangeStart` i `RangeEnd`,
que el servei de Power BI revincula per partició en el moment de l'actualització. Cada consulta de fets
incorpora aquests paràmetres als filtres `start_date` i `end_date` de l'API,
de manera que el volum de sol·licituds per partició és proporcional a la partició,
no a l'historial total. La consulta amplia la finestra de l'API un minut a cada costat i torna a aplicar localment el límit exacte semiobert `[RangeStart, RangeEnd)`,
de manera que la correcció de la partició no depèn que el servidor
tracti els seus límits com a inclusius o exclusius.

```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
```

La primera actualització després d'un desplegament aplica la política: una càrrega històrica completa,
lenta. Cada actualització posterior només toca la finestra incremental.

Mesurat en un conjunt de dades d'unes 158.000 activitats i unes 2.300 entitats:

| Actualització | Temps real |
| - | - |
| Primera (aplicació de la política, càrrega històrica completa) | \~31 min |
| Cada actualització posterior | **\~15 s** |

<Warning>
  L'endpoint REST d'actualització clàssica de Power BI (`POST .../refreshes` amb només
  `notifyOption`) ignora en silenci les polítiques d'actualització i fa un recorregut complet
  cada vegada. Per aplicar la política mitjançant l'API REST, utilitzeu l'actualització millorada: envieu
  `type`, `commitMode` i `applyRefreshPolicy` al cos. Les actualitzacions
  activades pel planificador del servei de Power BI sempre apliquen les polítiques; aquesta
  particularitat és específica de la crida REST.
</Warning>

<Note>
  La finestra de 14 dies és deliberadament generosa. La ingesta d'activitats no és
  només d'afegiment en el moment actual: els reompliments dels connectors aporten esdeveniments amb marques de temps passades.
  Una finestra d'1-2 dies els perdria de manera permanent.
</Note>

## Ampliació de la finestra d'actualització a l'API

`GET /v1/activities` accepta els filtres `start_date` i `end_date` en què
s'incorporen els paràmetres de Power BI, filtrant `event_ts` entre els límits.
`Entities` és una dimensió, de manera que aquest exemple l'actualitza completament cada vegada
en lloc d'incrementalment.

Els conjunts de dades de reporting utilitzen les seves pròpies marques de nivell (`updated_after`,
`occurred_after`) en lloc de la finestra de dates d'activitats.

## Límits de velocitat durant l'actualització

`/v1` aplica pressupostos separats per clau i per IP (vegeu
[rate limits](/ca/developers/v1/rate-limits)). Respecteu `429` + `Retry-After`; no
codifiqueu un pressupost de sol·licituds al model. Dues propietats de les actualitzacions de Power BI
són rellevants:

* **El recorregut d'una sola taula és seqüencial.** Cada sol·licitud de pàgina necessita el
  testimoni de la pàgina anterior, de manera que una taula està limitada per la latència independentment de la mida de
  pàgina.
* **Les actualitzacions concurrents de taules es multipliquen.** Diverses taules grans actualitzant-se
  en paral·lel és el cas que arriba al límit, i per això la via de reintent del 429
  a `Fn_FetchAll` és important. És preferible tenir menys taules en paral·lel que
  suposar una concurrència il·limitada.

El reintent s'ha posat a prova amb un limitador realment saturat. Una actualització
executada mentre una càrrega paral·lela mantenia deliberadament la clau al seu límit
es va completar igualment i va produir els mateixos recomptes de files que una línia de base sense contenció:
els reintents van retornar pàgines completes, no buits.

## Versions de contracte i credencials emmagatzemades

`/v1` accepta una capçalera de sol·licitud `Living-Security-Version` per fixar una versió de contracte
publicada (vegeu [API quickstart](/ca/developers/v1/quickstart)). Power
Query no pot enviar aquesta capçalera quan l'origen de dades utilitza credencials emmagatzemades:
només els orígens de dades anònims permeten capçaleres personalitzades. Afegir-la fa fallar una actualització
amb:

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

Deixeu la clau a les credencials emmagatzemades de Power BI i ometeu la capçalera personalitzada.
Si la sol·licitud no indica cap versió explícita, l'API utilitza el `contract_version`
fixat del compte de servei o l'última versió publicada si no hi ha cap fixació.
Confirmeu la versió de contracte del compte amb l'administrador de l'API abans de
publicar una actualització. Vegeu l'[API changelog](/ca/developers/v1/changelog) per
conèixer les versions compatibles.

Si una actualització comença a comportar-se de manera inesperada, inspeccioneu la capçalera de resposta
`Living-Security-Version` de l'API per veure quin contracte va
atendre realment la sol·licitud.

## Actualitzeu i verifiqueu el model

Publiqueu el model semàntic a l'espai de treball de Power BI, vinculeu la seva credencial emmagatzemada
a l'origen `/v1` regional i executeu una actualització abans d'activar una
programació. Reviseu l'historial d'actualitzacions per detectar errors o lectures incompletes.

Comproveu els recomptes de files amb una lectura independent de l'API amb els mateixos filtres.
La [guia de paginació](/ca/developers/v1/pagination) inclou exemples en Python i cURL
per llegir un conjunt de resultats complet. Una actualització que només llegeix la
primera pàgina pot semblar correcta en un conjunt de dades petit; compareu els recomptes abans de
confiar en l'informe.

## Decisions de model

* **Els totals de campanya i els resultats per persona tenen graus de detall diferents.**
  `SimulationResults` crida `GET /v1/simulations/{id}/results`
  (un GET per execució). Per al phishing per persona, afegiu consultes per a
  `GET /v1/reporting/simulation_results` i
  `GET /v1/reporting/simulation_events` (o l'operatiu
  `GET /v1/simulation_results` si ja teniu `simulation:read`).
* **`vectors` a `/v1/simulations` s'aplana en una cadena unida per comes.**
  Una columna de tipus llista no es pot segmentar com a dimensió real. Si una pàgina d'informe
  ha de filtrar per vector, afegiu una taula pont simulació-vector.

## Propers passos

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/ca/developers/developer-hub/overview">
    Creeu un compte de servei i una clau d'API per a Power BI.
  </Card>

  <Card title="API quickstart" href="/ca/developers/v1/quickstart">
    Autentiqueu les sol·licituds de `/v1` amb autenticació Basic.
  </Card>

  <Card title="Pagination" href="/ca/developers/v1/pagination">
    L'embolcall de llista, els testimonis de cursor i l'estabilitat dels filtres durant un recorregut.
  </Card>

  <Card title="Rate limits" href="/ca/developers/v1/rate-limits">
    Pressupostos per clau i per IP, el contracte del 429 i orientacions de reintent.
  </Card>
</CardGroup>


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