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

> Connectez Power BI à l'API publique de Living Security avec Power Query, créez un modèle en étoile sur /v1 et choisissez entre les jeux de données opérationnels et de reporting pour l'hameçonnage.

Ce guide est un exemple de bout en bout de lecture de `/v1` dans Power BI. Il couvre
le modèle de connectivité Power Query (M), un modèle sémantique en étoile construit
sur `/v1/activities`, l'actualisation incrémentielle, et les cas où utiliser les jeux de données
**reporting** en préversion pour les résultats d'hameçonnage par personne.

Les exemples utilisent l'hôte de production des États-Unis
(`https://api.us-east-1.livingsecurity.ai/v1`). Pour une organisation de l'UE, utilisez
`https://api.eu-west-1.livingsecurity.ai/v1` dans `Fn_FetchAll` et
`Fn_GetJson`, puis associez l'identifiant de la source de données à cet hôte. Consultez
[Régions](/fr-CA/developers/v1/regions).

Ce guide utilise des identifiants Basic stockés avec une clé API. Vous n'avez pas besoin
d'un client OAuth. Consultez [Authentification](/fr-CA/developers/v1/authentication) pour les choix
d'identifiants, et [Developer Hub](/fr-CA/developers/developer-hub/overview)
pour l'accès et la configuration.

## À quoi sert cet exemple

Utilisez-le lorsque vous devez :

* Charger les points de terminaison de liste `/v1` dans un jeu de données Power BI avec une pagination par curseur correcte et une nouvelle tentative en cas de limite de débit.
* Créer un modèle sémantique dans lequel les mesures (« entités actives », « activités par
  entité active ») sont dimensionnelles plutôt que comptées à partir d'une table plate.
* Faire passer une actualisation planifiée de Power BI d'un parcours complet de l'historique à
  une petite fenêtre quotidienne grâce à l'actualisation incrémentielle.
* Choisir la bonne granularité pour l'hameçonnage : totaux de campagne (opérationnel) ou
  résultats par personne et événements typés (préversion reporting).

## Prérequis

* Une clé API disposant des autorisations nécessaires à vos requêtes :
  * Exemple opérationnel : `activity:read`, `entity:read` et
    `simulation:read`.
  * Hameçonnage par personne via les jeux de données de reporting : `reporting:read`.
    Consultez [Clés API](/fr-CA/developers/developer-hub/api-keys).
* Power BI Desktop, ou un espace de travail Power BI / Fabric dans lequel vous pouvez créer un
  modèle sémantique et planifier l'actualisation.

## Choisir la bonne granularité

`/v1` expose deux surfaces connexes pour l'hameçonnage. Elles ne sont pas interchangeables.

| Besoin | À utiliser | Autorisation | Granularité |
| - | - | - | - |
| Totaux de l'entonnoir de campagne | `GET /v1/simulations/{id}/results` | `simulation:read` | Une ligne par simulation (livrés, ouverts, cliqués, …) |
| Résultats par personne (opérationnel) | `GET /v1/simulation_results` | `simulation:read` | Une ligne par entité × simulation |
| Résultats par personne (BI / reporting) | `GET /v1/reporting/simulation_results` | `reporting:read` | Même granularité, noms de champs BI (`entity_id`, `simulation_id`), filigrane `updated_after` |
| Faits d'interaction typés | `GET /v1/reporting/simulation_events` | `reporting:read` | Un fait par interaction (`simulation.link_clicked`, …), `occurred_after` |
| Dimension actuelle de l'effectif | `GET /v1/reporting/entities` | `reporting:read` | Une ligne actuelle par entité. Jointure sur `reporting.simulation_results.entity_id = reporting.entities.id` |
| Versions actuelles des entités | `GET /v1/reporting/entity_versions` | `reporting:read` | Un intervalle actuel sans date de fin par entité. Il n'existe aucune table d'historique des versions |
| Appartenances actuelles aux cohortes | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Un fait actuel par `(cohort_id, entity_id)`. Il n'existe aucune table d'historique des appartenances |
| Colonne vertébrale de jointure pour tous les types d'activité | `GET /v1/activities` | `activity:read` | Un événement par activité, `participants[]` imbriqué |

Les exemples de requêtes Power BI ci-dessous appellent les routes **opérationnelles**
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Pour le reporting d'hameçonnage par personne, privilégiez la famille
**reporting** en préversion ci-dessous. Une clé disposant uniquement de `reporting:read` ne peut pas
appeler `/v1/simulation_results` opérationnel (l'API renvoie `403`).

### Échelle de difficulté

La difficulté d'une simulation est un entier de 1 à 5 sur `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), les propriétés `/v1/activities`
(`difficulty_tier`) et `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` est le plus facile et `5` le plus difficile. Les simulations adaptatives et progressives n'ont pas
de valeur unique au niveau de la campagne; `/v1/simulations.difficulty_level` est donc `null`,
alors que `difficulty_level` par destinataire dans les résultats peut tout de même être défini. Pour établir une correspondance avec
les exportations héritées faible/moyenne/élevée, considérez `1–2` comme faible, `3` comme moyenne et `4–5` comme
élevée.

<Warning>
  Ne créez pas de nouvelles intégrations BI à partir d'appels réseau non documentés du Tableau de bord
  ni de l'ancien aperçu de l'API publique `/api/v1` organisé. Authentifiez-vous auprès de l'hôte `/v1`
  régional de l'organisation avec une clé API du Developer Hub. Consultez
  [Régions](/fr-CA/developers/v1/regions).
</Warning>

## Modèle de connectivité (Power Query)

Quatre règles sous-tendent chaque requête de l'exemple. Chacune est imposée par le
service Power BI et non par des préférences de style.

<AccordionGroup>
  <Accordion title="Conserver une URL de base Web.Contents statique se terminant par /v1">
    Placez tout ce qui est dynamique dans `RelativePath` et `Query`. Le premier argument de
    `Web.Contents` doit être une chaîne littérale qui se termine à la racine `/v1`
    régionale de l'organisation :

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

    Les appelants transmettent ensuite la ressource seule (`"simulations"`, et non
    `"v1/simulations"`). Ne collez pas un hôte américain pour une organisation de l'UE.

    Si le premier argument est construit par concaténation de chaînes, le service Power BI
    ne peut pas identifier statiquement la source de données et l'actualisation planifiée échoue avec
    une erreur de « source de données dynamique », même si la requête fonctionne dans Power BI
    Desktop.
  </Accordion>

  <Accordion title="Fournir la clé API sous forme d'identifiant Basic stocké, et non dans le texte de la requête">
    Lorsque Power BI demande les identifiants pour la source de données `/v1`, choisissez
    **Basic** et collez la clé API comme **nom d'utilisateur**. Saisissez un espace réservé tel que
    `x` dans le champ **Mot de passe**. Le service Power BI refuse un mot de passe
    vide, même si l'API ignore ce champ. Utiliser la clé comme mot de passe avec un nom d'utilisateur
    fictif fonctionne également.

    N'ajoutez pas d'en-tête `Authorization` dans M. Cela fonctionne sur votre poste, mais
    intègre la clé dans le fichier de requête et entre en conflit avec l'actualisation planifiée.
  </Accordion>

  <Accordion title="Laisser s'exécuter le test de connexion de la boîte de dialogue des identifiants">
    `GET /v1` est authentifié : il renvoie `200` avec une clé valide et `401`
    sans clé. Pointez la source de données vers `…/v1` et **ne sautez pas** le test —
    une bonne clé est enregistrée, une clé révoquée ou mal formée échoue immédiatement.

    Les anciens exemples utilisaient l'hôte seul comme source de données et indiquaient de sauter
    le test parce que `GET /` est toujours `404`. Redirigez ces modèles vers `…/v1`.
  </Accordion>

  <Accordion title="Définir explicitement limit=500 dans chaque requête de liste">
    La taille de page par défaut de chaque point de terminaison de liste `/v1` est de 25. Conserver la valeur par défaut
    multiplie le nombre de requêtes pour les mêmes données. Les valeurs de requête doivent être du texte —
    `limit = "500"` dans M, et non `limit = 500`.
  </Accordion>
</AccordionGroup>

### Fonction de pagination partagée

Chaque requête de l'exemple appelle une seule `Fn_FetchAll` qui parcourt
`next_page_token` jusqu'à ce qu'il soit `null` (voir
[pagination](/fr-CA/developers/v1/pagination)). Ses propriétés notables :

* **URL de base `/v1` statique, `RelativePath` et `Query` dynamiques.** Requis pour
  l'actualisation planifiée et pour le test des identifiants.
* **`ManualStatusHandling = {429}` avec nouvelle tentative limitée.** La fonction lit
  `Retry-After` lorsque Power Query l'expose, sinon attend 30 secondes, et
  abandonne après 5 réponses `429` consécutives avec une erreur explicite plutôt que de
  boucler jusqu'à l'expiration de l'actualisation. `5xx` n'est volontairement pas
  retenté — une erreur de serveur doit remonter, et non produire silencieusement un jeu de données
  partiel.
* **`Timeout` de 60 secondes.** Les pages répondent généralement en bien moins d'une seconde; une
  minute de silence signifie donc que la requête est morte.
* **`Binary.Buffer` avant `Json.Document`.** Sans mise en mémoire tampon, le moteur
  mashup peut redemander le flux de réponse s'il le lit plus d'une fois.
* **`baseQuery` est transmis sans modification à chaque itération.** Les jetons de page sont
  des curseurs opaques et signés, liés à l'organisation, à l'identifiant, à la version du
  contrat, ainsi qu'à l'empreinte des filtres et du tri. Modifier un filtre
  en cours de parcours entraîne un rejet pour jeton non valide plutôt qu'un redémarrage à la
  première page.

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

### Requêtes typées par point de terminaison

Chaque consommateur de `Fn_FetchAll` suit la même structure : appeler le point de terminaison,
projeter une liste fixe de colonnes, supprimer le discriminant `object` et typer les
horodatages en `datetimezone` afin que les mesures de date ne se décalent pas silencieusement selon le fuseau horaire de
la capacité d'actualisation.

<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>
  Une liste explicite de colonnes sur `Table.FromRecords` maintient le schéma stable lorsqu'un
  point de terminaison renvoie zéro ligne. Un `Table.FromRecords({})` nu produit une
  table sans colonne et toutes les étapes suivantes échouent faute des champs manquants.
</Tip>

## Excel

Le même Power Query (M) s'exécute dans Récupérer et transformer d'Excel — c'est le même
moteur que Power BI Desktop. Utilisez-le lorsque vous voulez un tableau dans une feuille de calcul
plutôt qu'un modèle sémantique.

Dans Excel pour Windows (Microsoft 365, Excel 2024, 2021, 2019 ou 2016) :

1. **Données → Obtenir des données → À partir d'autres sources → Requête vide.**
2. Dans l'Éditeur Power Query, **Accueil → Éditeur avancé**. Collez
   `Fn_FetchAll` en premier, puis renommez cette requête `Fn_FetchAll` (Paramètres de
   la requête → Nom, ou clic droit dans le volet Requêtes → **Renommer**).
3. Répétez l'opération pour `Fn_GetJson`, puis pour chaque requête de rapport.
4. **Accueil → Fermer et charger → Fermer et charger dans…** ouvre la boîte de dialogue **Importer les données**.
   Choisissez **Créer uniquement la connexion** pour `Fn_FetchAll` et
   `Fn_GetJson` (ce sont des fonctions, et non des tableaux). Choisissez **Tableau** pour
   chaque requête de rapport que vous souhaitez placer dans une feuille de calcul.
5. Lors de la première actualisation, choisissez **Basic** : la clé API comme **Nom d'utilisateur**, un
   espace réservé tel que `x` comme **Mot de passe**. Excel stocke un seul identifiant
   par URL de source de données; les requêtes suivantes qui ciblent le même hôte `/v1` le réutilisent.

<Note>
  **Fermer et charger dans…** peut être grisé la première fois que vous créez une requête dans
  un classeur. Utilisez **Fermer et charger**, puis dans Excel, sélectionnez **Données →
  Requêtes et connexions**, effectuez un clic droit sur la requête et choisissez **Charger dans**.
  Microsoft documente cette procédure pour ce cas.
</Note>

Vous ne pouvez pas envoyer `Living-Security-Version` depuis Power Query sur une
source authentifiée — c'est la même restriction que pour Power BI. Épinglez le compte de
service ou transmettez `api_version` dans l'enregistrement de la requête. Consultez
[Versions du contrat et identifiants stockés](#versions-du-contrat-et-identifiants-stockés).

Requêtes de rapport prêtes à l'emploi (correspondances de colonnes et différences par rapport à l'exportation d'un
fournisseur précédent) :

<CardGroup cols={3}>
  <Card title="Conformité par personne" href="/fr-CA/developers/v1/reports/per-person-compliance">
    Inscriptions à la formation à la granularité personne × affectation.
  </Card>

  <Card title="Journal d'hameçonnage par courriel" href="/fr-CA/developers/v1/reports/email-phishing-log">
    Interactions d'hameçonnage au niveau des événements, sur les 12 derniers mois.
  </Card>

  <Card title="Utilisateurs les plus vulnérables" href="/fr-CA/developers/v1/reports/most-susceptible-users">
    Cumul par personne sur 12 mois des clics, des identifiants saisis et des signalements.
  </Card>
</CardGroup>

## Jeux de données de reporting en préversion (hameçonnage par personne)

La famille de reporting en préversion fournit des jeux de données pour l'hameçonnage par personne.
Elle nécessite `reporting:read` et un accès au reporting pour votre organisation.
Utilisez le catalogue de jeux de données ci-dessous pour examiner ses schémas et ses politiques de données.

Découvrez le catalogue en direct :

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

Chaque jeu de données déclare sa granularité, sa clé primaire, ses jointures, son filigrane, sa politique de correction et de
suppression.

### simulation\_results

* **Chemin :** `GET /v1/reporting/simulation_results`
* **Granularité :** entité × simulation
* **Filtres :** `simulation`, `entity`, `updated_after`, ainsi que `limit` /
  `page_token`
* **Objet de transmission :** `reporting.simulation_result`
* **Clés de jointure :** `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
```

Utilisez `updated_after` comme filigrane inférieur inclusif lorsque vous actualisez les partitions
récentes. Les dimensions modifiables des résultats sont réécrites sur place sous le même `id`.

### simulation\_events

* **Chemin :** `GET /v1/reporting/simulation_events`
* **Granularité :** un fait typé par interaction observée
* **Filtres :** `simulation`, `entity`, `event_type`, `occurred_after`, ainsi que
  `limit` / `page_token`
* **Objet de transmission :** `reporting.simulation_event`
* **Types d'événements :** jetons publics fermés tels que `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — et non les types d'activité `phishing.simulation.*`
  ClickHouse bruts
* **Appareil :** `device_type` est la classe dérivée (les jetons observés comprennent
  `desktop`, `mobile`, `tablet`, `unknown`). Le `user_agent` brut n'est pas présent sur
  `/v1`. L'adresse IP brute n'est jamais présente sur `/v1` (jamais conservée; seulement `ip_hash` 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
```

Utilisez `occurred_after` pour les chargements incrémentiels d'événements. Les faits sont en ajout seul;
les arrivées tardives sont de nouvelles lignes.

### entities

* **Chemin :** `GET /v1/reporting/entities`
* **Granularité :** une ligne actuelle par entité
* **Filtres :** `type`, `updated_after`, ainsi que `limit` / `page_token`
* **Objet de transmission :** `reporting.entity`
* **Clé de jointure :** `id` / `entity_id` (même valeur)

Joignez les faits d'hameçonnage aux personnes avec
`reporting.simulation_results.entity_id = reporting.entities.id`.
Le `GET /v1/entities` opérationnel reste sur `entity:read` et correspond à une autorisation
différente.

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

Ces deux jeux de données comportent une exception nommée, limitée à l'état actuel :

> Il n'existe aucune table faisant autorité pour l'historique des versions ou des appartenances. Ce jeu de données projette la ligne actuelle sous la forme d'un intervalle unique sans date de fin (`valid_to` est nul, `is_current` est vrai). `valid_from` est `created_at` ou, à défaut, `updated_at` pour les versions d'entités, et l'heure d'observation de la requête pour les appartenances aux cohortes. Les intervalles historiques fermés ne sont pas fabriqués.

* **entity\_versions :** `GET /v1/reporting/entity_versions`. Mêmes filtres que
  entities. `valid_from` est `created_at` ou, à défaut, `updated_at`. Une entité
  dont les deux horodatages sont nuls est omise.
* **cohort\_memberships :** `GET /v1/reporting/cohort_memberships`. Filtres :
  `cohort`, `entity`, `updated_after`. `updated_at` correspond à la dernière mise à jour de la cohorte,
  afin que l'actualisation incrémentielle reste fiable. `valid_from` est l'heure d'observation
  de la requête. Joignez `entity_id` à `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
```

## Modèle sémantique en étoile sur /v1/activities

`/v1/activities` est le flux d'événements horodatés de l'organisation. Il ne comporte aucune
mesure numérique — chaque quantité d'un rapport est un nombre de lignes — ce qui fait
d'un schéma en étoile un choix naturel.

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

* **`Activities`** est le fait d'événement à sa véritable granularité. Une ligne par activité.
  Les participants y sont retirés et reconstruits dans leur propre table.
* **`ActivityParticipants`** existe parce que les participants sont réellement
  de type un-à-plusieurs, avec des rôles tels que `actor` et `target`. Les aplatir dans
  `Activities` dupliquerait les lignes d'activité (gonflant chaque décompte)
  ou éliminerait les participants supplémentaires.
  La relation avec `Activities` est la seule relation bidirectionnelle du modèle,
  de sorte qu'un segment d'entité atteint la table de faits.
* **`Entities`** est la dimension qui nomme les participants. Les lignes de participants
  d'une activité comportent `entity_id` et un rôle, mais pas de nom d'affichage.
* **`Dates`** est générée en M (aucun appel d'API) et désignée comme table de dates du modèle.
  C'est ce qui rend valide l'intelligence temporelle DAX
  (`DATESINPERIOD`, `DATEADD`). Les colonnes de nom de mois et de jour de la semaine déclarent
  `sortByColumn` sur leurs jumelles numériques afin que les visuels ne trient pas les mois
  par ordre alphabétique.

Toutes les mesures sont explicites (le modèle définit `discourageImplicitMeasures`) : total des
activités, entités actives, activités par entité active, jours avec activité
et moyenne quotidienne glissante sur 7 jours.

### Structure de la table de pont

`ActivityParticipants` parcourt `/v1/activities` et éclate le tableau `participants`
en une ligne par 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` parcourt `/v1/activities` de façon indépendante plutôt que de
référencer la requête `Activities`. Dans Power BI, deux requêtes chargées comme deux
tables effectuent chacune leur récupération indépendamment au moment de l'actualisation. Les garder indépendantes
permet au service d'actualiser les partitions des deux tables en parallèle.

## Actualisation incrémentielle

Les deux tables à la granularité des faits (`Activities` et `ActivityParticipants`) déclarent une
politique d'actualisation incrémentielle Power BI : conserver 5 ans de partitions annuelles,
réimporter les 14 derniers jours à chaque actualisation.

Le mécanisme repose sur une paire de paramètres DateTime, `RangeStart` et `RangeEnd`,
que le service Power BI relie à nouveau pour chaque partition au moment de l'actualisation. Chaque requête de faits
intègre ces paramètres dans les filtres `start_date` et `end_date` de l'API,
de sorte que le volume de requêtes par partition est proportionnel à la partition
et non à l'ensemble de l'historique. La requête élargit d'une minute la fenêtre de l'API de chaque
côté et réapplique localement la borne exacte semi-ouverte `[RangeStart, RangeEnd)`,
de sorte que l'exactitude des partitions ne dépend pas de la façon dont le serveur
traite ses bornes, inclusives ou exclusives.

```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 première actualisation après un déploiement applique la politique : un chargement historique complet,
lent. Chaque actualisation suivante ne touche que la fenêtre incrémentielle.

Mesuré sur un jeu de données d'environ 158 000 activités et 2 300 entités :

| Actualisation | Durée réelle |
| - | - |
| Première (application de la politique, chargement historique complet) | \~31 min |
| Chaque actualisation suivante | **\~15 s** |

<Warning>
  Le point de terminaison REST d'actualisation classique de Power BI (`POST .../refreshes` avec uniquement
  `notifyOption`) ignore silencieusement les politiques d'actualisation et effectue un parcours complet
  à chaque fois. Pour appliquer la politique par l'API REST, utilisez l'actualisation améliorée : envoyez
  `type`, `commitMode` et `applyRefreshPolicy` dans le corps de la requête. Les actualisations
  déclenchées par le planificateur du service Power BI appliquent toujours les politiques — cette
  particularité est propre à l'appel REST.
</Warning>

<Note>
  La fenêtre de 14 jours est volontairement généreuse. L'ingestion des activités n'est pas
  en ajout seul au moment présent : les rattrapages des connecteurs ajoutent des événements portant des horodatages passés.
  Une fenêtre de 1 à 2 jours les manquerait définitivement.
</Note>

## Élargir la fenêtre d'actualisation dans l'API

`GET /v1/activities` accepte les filtres `start_date` et `end_date` dans lesquels les
paramètres Power BI sont intégrés, en filtrant `event_ts` entre les bornes.
`Entities` est une dimension; cet exemple l'actualise donc entièrement à chaque fois
plutôt que de façon incrémentielle.

Les jeux de données de reporting utilisent leurs propres filigranes (`updated_after`,
`occurred_after`) plutôt que la fenêtre de dates des activités.

## Limites de débit pendant l'actualisation

`/v1` applique des budgets distincts par clé et par adresse IP (voir
[limites de débit](/fr-CA/developers/v1/rate-limits)). Respectez `429` + `Retry-After`; ne
codez pas en dur un budget de requêtes dans votre modèle. Deux propriétés des actualisations Power BI
sont importantes :

* **Le parcours d'une seule table est séquentiel.** Chaque requête de page a besoin du
  jeton de la page précédente; une table est donc limitée par la latence, quelle que soit la taille des pages.
* **Les actualisations simultanées de tables se multiplient.** C'est l'actualisation en parallèle de plusieurs grandes tables
  qui atteint le plafond, et c'est pourquoi le chemin de nouvelle tentative 429
  dans `Fn_FetchAll` est important. Privilégiez un plus petit nombre de tables en parallèle plutôt que
  de supposer une simultanéité illimitée.

La nouvelle tentative a été éprouvée face à un limiteur réellement saturé. Une actualisation
exécutée pendant qu'une charge parallèle maintenait volontairement la clé à son plafond
s'est tout de même terminée et a produit les mêmes nombres de lignes qu'une référence sans contention —
les nouvelles tentatives ont renvoyé des pages complètes, sans lacunes.

## Versions du contrat et identifiants stockés

`/v1` accepte un en-tête de requête `Living-Security-Version` pour épingler une version
publiée du contrat (voir [Démarrage rapide de l'API](/fr-CA/developers/v1/quickstart)). Power
Query ne peut pas envoyer cet en-tête lorsque la source de données utilise des identifiants stockés —
seules les sources de données anonymes autorisent les en-têtes personnalisés. Son ajout fait échouer une actualisation avec
l'erreur suivante :

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

Conservez la clé dans les identifiants stockés de Power BI et omettez l'en-tête personnalisé.
Sans version explicite dans la requête, l'API utilise la valeur `contract_version` épinglée
du compte de service, ou la dernière version publiée si aucune version n'est épinglée.
Confirmez la version du contrat du compte auprès de votre administrateur d'API avant de
publier une actualisation. Consultez le [journal des modifications de l'API](/fr-CA/developers/v1/changelog) pour
connaître les versions prises en charge.

Si une actualisation commence à se comporter de façon inattendue, examinez l'en-tête de réponse
`Living-Security-Version` de l'API pour voir quel contrat a réellement
traité la requête.

## Actualiser et vérifier votre modèle

Publiez le modèle sémantique dans votre espace de travail Power BI, associez son identifiant
stocké à la source `/v1` régionale, puis exécutez une actualisation avant d'activer une
planification. Examinez l'historique d'actualisation à la recherche d'échecs ou de lectures incomplètes.

Vérifiez les nombres de lignes par rapport à une lecture indépendante de l'API utilisant les mêmes filtres.
Le [guide de pagination](/fr-CA/developers/v1/pagination) comprend des exemples Python et cURL
pour lire un jeu de résultats complet. Une actualisation qui ne lit que la
première page peut sembler réussie sur un petit jeu de données; comparez les nombres avant de
vous fier au rapport.

## Choix de modélisation

* **Les totaux de campagne et les résultats par personne ont des granularités différentes.**
  `SimulationResults` appelle `GET /v1/simulations/{id}/results`
  (un GET par exécution). Pour l'hameçonnage par personne, ajoutez des requêtes pour
  `GET /v1/reporting/simulation_results` et
  `GET /v1/reporting/simulation_events` (ou `GET /v1/simulation_results`
  opérationnel si vous détenez déjà `simulation:read`).
* **`vectors` sur `/v1/simulations` est aplati en une chaîne séparée par des virgules.**
  Une colonne de type liste ne peut pas servir de véritable dimension de segmentation. Si une page de rapport
  doit filtrer par vecteur, ajoutez une table de pont simulation-vecteur.

## Prochaines étapes

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/fr-CA/developers/developer-hub/overview">
    Créez un compte de service et une clé API pour Power BI.
  </Card>

  <Card title="Démarrage rapide de l'API" href="/fr-CA/developers/v1/quickstart">
    Authentifiez les requêtes `/v1` avec l'authentification Basic.
  </Card>

  <Card title="Pagination" href="/fr-CA/developers/v1/pagination">
    L'enveloppe de liste, les jetons de curseur et la stabilité des filtres pendant un parcours.
  </Card>

  <Card title="Limites de débit" href="/fr-CA/developers/v1/rate-limits">
    Budgets par clé et par adresse IP, contrat 429 et conseils de nouvelle tentative.
  </Card>
</CardGroup>


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