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

> Collegare Power BI alla Living Security Public API con Power Query, creare un modello a schema a stella su /v1 e scegliere tra dataset operativi e di reporting per il phishing.

Questa guida è un esempio completo di lettura di `/v1` in Power BI. Illustra
il pattern di connettività Power Query (M), un modello semantico a schema a stella costruito
su `/v1/activities`, l'aggiornamento incrementale e i casi in cui utilizzare i dataset di anteprima
**reporting** per i risultati di phishing per singola persona.

Gli esempi utilizzano l'host di produzione per gli Stati Uniti
(`https://api.us-east-1.livingsecurity.ai/v1`). Per un'organizzazione europea, utilizzare
`https://api.eu-west-1.livingsecurity.ai/v1` sia in `Fn_FetchAll` sia in
`Fn_GetJson`, quindi associare la credenziale dell'origine dati a tale host. Vedere
[Regioni](/it/developers/v1/regions).

Questa guida utilizza credenziali Basic memorizzate con una chiave API. Non è necessario un
client OAuth. Per le opzioni relative alle credenziali, vedere [Autenticazione](/it/developers/v1/authentication); per l'accesso e la configurazione, vedere [Developer Hub](/it/developers/developer-hub/overview).

## A cosa serve questo esempio

Utilizzarlo quando è necessario:

* Caricare gli endpoint di elenco `/v1` in un dataset di Power BI con paginazione a cursore corretta e un nuovo tentativo in caso di rate limit.
* Creare un modello semantico in cui le misure ("entità attive", "attività per
  entità attiva") siano dimensionali anziché calcolate a partire da una tabella piatta.
* Ridurre l'aggiornamento pianificato di Power BI da una scansione completa dello storico a una
  piccola finestra giornaliera mediante l'aggiornamento incrementale.
* Scegliere la granularità corretta per il phishing: totali delle campagne (operativi) oppure
  risultati per singola persona ed eventi tipizzati (anteprima reporting).

## Prerequisiti

* Una chiave API con le autorizzazioni necessarie per le query:
  * Esempio operativo: `activity:read`, `entity:read` e
    `simulation:read`.
  * Phishing per singola persona tramite dataset di reporting: `reporting:read`.
    Vedere [Chiavi API](/it/developers/developer-hub/api-keys).
* Power BI Desktop, oppure un workspace Power BI / Fabric in cui sia possibile creare un
  modello semantico e pianificare l'aggiornamento.

## Scegliere la granularità corretta

`/v1` espone due superfici correlate per il phishing. Non sono intercambiabili.

| Esigenza | Utilizzo | Autorizzazione | Granularità |
| - | - | - | - |
| Totali del funnel della campagna | `GET /v1/simulations/{id}/results` | `simulation:read` | Una riga per simulazione (consegnate, aperte, cliccate, …) |
| Risultati per singola persona (operativi) | `GET /v1/simulation_results` | `simulation:read` | Una riga per entità × simulazione |
| Risultati per singola persona (BI / reporting) | `GET /v1/reporting/simulation_results` | `reporting:read` | Stessa granularità, nomi di campo BI (`entity_id`, `simulation_id`), watermark `updated_after` |
| Fatti di interazione tipizzati | `GET /v1/reporting/simulation_events` | `reporting:read` | Un fatto per interazione (`simulation.link_clicked`, …), `occurred_after` |
| Dimensione corrente della forza lavoro | `GET /v1/reporting/entities` | `reporting:read` | Una riga corrente per entità. Join su `reporting.simulation_results.entity_id = reporting.entities.id` |
| Versioni correnti delle entità | `GET /v1/reporting/entity_versions` | `reporting:read` | Un intervallo corrente aperto per entità. Non esiste alcuna tabella dello storico delle versioni |
| Appartenenze correnti alle coorti | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Un fatto corrente per `(cohort_id, entity_id)`. Non esiste alcuna tabella dello storico delle appartenenze |
| Spina di join per tutti i tipi di attività | `GET /v1/activities` | `activity:read` | Un evento per attività, con `participants[]` annidato |

Le query di esempio di Power BI riportate di seguito richiamano le route **operative**
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Per il reporting del phishing per singola persona, si consiglia la
famiglia di anteprima **reporting** descritta più avanti. Una chiave con la sola autorizzazione `reporting:read` non può
richiamare `/v1/simulation_results` operativo (l'API restituisce `403`).

### Scala di difficoltà

La difficoltà della simulazione è un numero intero da 1 a 5 nelle proprietà `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), `/v1/activities`
(`difficulty_tier`) e `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` è il livello più facile e `5` il più difficile. Le simulazioni adattive e progressive non hanno
un singolo valore a livello di campagna, quindi `/v1/simulations.difficulty_level` è `null`,
mentre `difficulty_level` per singolo destinatario nei risultati può comunque essere impostato. Per mappare i valori sulle
esportazioni legacy basso/medio/alto, considerare `1–2` come basso, `3` come medio e `4–5` come
alto.

<Warning>
  Non creare nuove integrazioni BI basate su chiamate di rete non documentate della Dashboard
  o sulla panoramica legacy curata della Public API `/api/v1`. Autenticarsi
  sull'host `/v1` regionale dell'organizzazione con una chiave API di Developer Hub. Vedere
  [Regioni](/it/developers/v1/regions).
</Warning>

## Pattern di connettività (Power Query)

Quattro regole sono alla base di ogni query dell'esempio. Ciascuna è imposta dal
servizio Power BI e non è una semplice preferenza di stile.

<AccordionGroup>
  <Accordion title="Mantenere statico l'URL di base di Web.Contents e terminarlo con /v1">
    Inserire tutto ciò che è dinamico in `RelativePath` e `Query`. Il primo argomento di
    `Web.Contents` deve essere una stringa letterale che termina alla radice `/v1` regionale dell'organizzazione:

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

    I chiamanti passano quindi solo la risorsa (`"simulations"`, non
    `"v1/simulations"`). Non inserire un host US per un'organizzazione europea.

    Se il primo argomento viene costruito tramite concatenazione di stringhe, il servizio Power BI
    non riesce a identificare staticamente l'origine dati e l'aggiornamento pianificato non riesce con
    un errore di "origine dati dinamica", anche se la query funziona in Power BI
    Desktop.
  </Accordion>

  <Accordion title="Fornire la chiave API come credenziale Basic memorizzata, non nel testo della query">
    Quando Power BI richiede le credenziali per l'origine dati `/v1`, scegliere
    **Basic** e incollare la chiave API come **nome utente**. Inserire un segnaposto, ad esempio
    `x`, nel campo **password**. Il servizio Power BI rifiuta una
    password vuota anche se l'API ignora quel campo. Funziona anche l'uso della chiave come password con un nome utente segnaposto.

    Non aggiungere un'intestazione `Authorization` in M. Funziona sul desktop, ma
    incorpora la chiave nel file della query e ostacola l'aggiornamento pianificato.
  </Accordion>

  <Accordion title="Lasciare eseguire il test di connessione della finestra delle credenziali">
    `GET /v1` è autenticato: restituisce `200` con una chiave valida e `401`
    senza. Puntare l'origine dati a `…/v1` e **non** saltare il test:
    una chiave corretta viene salvata, una chiave revocata o malformata non riesce immediatamente.

    I vecchi esempi utilizzavano l'host semplice come origine dati e suggerivano di saltare
    il test perché `GET /` restituisce sempre `404`. Riassegnare tali modelli a `…/v1`.
  </Accordion>

  <Accordion title="Impostare esplicitamente limit=500 in ogni query di elenco">
    La dimensione di pagina predefinita di ogni endpoint di elenco `/v1` è 25. Lasciarla al valore predefinito
    moltiplica il numero di richieste a parità di dati. I valori della query devono essere testo:
    `limit = "500"` in M, non `limit = 500`.
  </Accordion>
</AccordionGroup>

### Funzione di paginazione condivisa

Ogni query dell'esempio richiama un'unica `Fn_FetchAll` che scorre
`next_page_token` finché non è `null` (vedere
[paginazione](/it/developers/v1/pagination)). Le sue proprietà principali:

* **URL di base `/v1` statico, `RelativePath` e `Query` dinamici.** Necessario per
  l'aggiornamento pianificato e per il test delle credenziali.
* **`ManualStatusHandling = {429}` con un nuovo tentativo limitato.** La funzione legge
  `Retry-After` quando Power Query lo espone, altrimenti attende 30 secondi e
  si arrende dopo 5 risposte `429` consecutive con un errore esplicito, anziché
  continuare a ripetere fino allo scadere del tempo dell'aggiornamento. `5xx` volutamente non viene
  ritentato: un errore del server deve emergere e non produrre silenziosamente un dataset
  parziale.
* **`Timeout` di 60 secondi.** Le pagine rispondono in genere in molto meno di un secondo, quindi
  un minuto di silenzio significa che la richiesta è bloccata.
* **`Binary.Buffer` prima di `Json.Document`.** Senza il buffering, il motore
  mashup potrebbe richiedere nuovamente il flusso di risposta se lo legge più di una volta.
* **`baseQuery` viene passato invariato a ogni iterazione.** I token di pagina sono
  cursori opachi e firmati, associati all'organizzazione, alla credenziale, alla versione
  del contratto e all'impronta di filtri e ordinamento. La modifica di un filtro
  durante la scansione viene rifiutata come token non valido e non riparte
  dalla prima pagina.

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

### Query tipizzate per endpoint

Ogni consumer di `Fn_FetchAll` segue la stessa struttura: richiama l'endpoint,
proietta un elenco fisso di colonne, elimina il discriminatore `object` e
tipizza i timestamp come `datetimezone` in modo che le misure sulle date non subiscano spostamenti silenziosi dovuti al
fuso orario della capacità di aggiornamento.

<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>
  Un elenco esplicito di colonne su `Table.FromRecords` mantiene stabile lo schema quando
  un endpoint restituisce zero righe. Un semplice `Table.FromRecords({})` produce una
  tabella senza colonne e tutti i passaggi successivi non riescono per i campi mancanti.
</Tip>

## Excel

Lo stesso Power Query (M) funziona in Get & Transform di Excel: è lo stesso
motore di Power BI Desktop. Utilizzarlo quando si desidera una tabella in un foglio di lavoro
anziché un modello semantico.

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

1. **Data → Get Data → From Other Sources → Blank Query.**
2. Nel Power Query Editor, **Home → Editor avanzato**. Incollare
   prima `Fn_FetchAll`, quindi rinominare la query `Fn_FetchAll` (Impostazioni
   query → Nome, oppure clic con il pulsante destro nel riquadro Query → **Rename**).
3. Ripetere l'operazione per `Fn_GetJson`, quindi per ogni query di report.
4. **Home → Close & Load → Close & Load To…** apre la finestra di dialogo **Import Data**.
   Scegliere **Only Create Connection** per `Fn_FetchAll` e
   `Fn_GetJson` (sono funzioni, non tabelle). Scegliere **Table** per
   ogni query di report che si desidera visualizzare in un foglio di lavoro.
5. Al primo aggiornamento, scegliere **Basic**: la chiave API come **User name** e un
   segnaposto, ad esempio `x`, come **Password**. Excel memorizza una credenziale
   per ogni URL di origine dati; le query successive che utilizzano lo stesso host `/v1`
   la riutilizzano.

<Note>
  **Close & Load To…** può risultare disattivato la prima volta che si crea una query in
  una cartella di lavoro. Utilizzare **Close & Load**, quindi in Excel selezionare **Data →
  Queries & Connections**, fare clic con il pulsante destro sulla query e scegliere **Load To**.
  Questo percorso è documentato da Microsoft per questo caso.
</Note>

Non è possibile inviare `Living-Security-Version` da Power Query su
un'origine autenticata: la restrizione è la stessa di Power BI. Associare l'account di
servizio oppure passare `api_version` nel record della query. Vedere
[Versioni del contratto e credenziali memorizzate](#versioni-del-contratto-e-credenziali-memorizzate).

Query di report pronte all'uso (mappatura delle colonne e differenze rispetto all'esportazione
di un provider precedente):

<CardGroup cols={3}>
  <Card title="Conformità per singola persona" href="/it/developers/v1/reports/per-person-compliance">
    Iscrizioni alla formazione con granularità persona × assegnazione.
  </Card>

  <Card title="Log del phishing via email" href="/it/developers/v1/reports/email-phishing-log">
    Interazioni di phishing a livello di evento, ultimi 12 mesi.
  </Card>

  <Card title="Utenti più vulnerabili" href="/it/developers/v1/reports/most-susceptible-users">
    Riepilogo per singola persona dei clic, delle credenziali e delle segnalazioni degli ultimi 12 mesi.
  </Card>
</CardGroup>

## Dataset di anteprima reporting (phishing per singola persona)

La famiglia di anteprima reporting fornisce dataset per il phishing per singola persona.
Richiede `reporting:read` e l'accesso al reporting per la propria organizzazione.
Utilizzare il catalogo dei dataset riportato di seguito per esaminarne gli schemi e le policy sui dati.

Consultare il catalogo in tempo reale:

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

Ogni dataset dichiara granularità, chiave primaria, join, watermark, policy di correzione e
di eliminazione.

### simulation\_results

* **Percorso:** `GET /v1/reporting/simulation_results`
* **Granularità:** entità × simulazione
* **Filtri:** `simulation`, `entity`, `updated_after`, più `limit` /
  `page_token`
* **Oggetto wire:** `reporting.simulation_result`
* **Chiavi di join:** `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
```

Utilizzare `updated_after` come watermark inferiore inclusivo quando si aggiornano le partizioni
recenti. Le dimensioni mutabili dei risultati vengono riformulate sul posto con lo stesso `id`.

### simulation\_events

* **Percorso:** `GET /v1/reporting/simulation_events`
* **Granularità:** un fatto tipizzato per ogni interazione osservata
* **Filtri:** `simulation`, `entity`, `event_type`, `occurred_after`, più
  `limit` / `page_token`
* **Oggetto wire:** `reporting.simulation_event`
* **Tipi di evento:** token pubblici chiusi come `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported`, non i tipi di attività
  `phishing.simulation.*` grezzi di ClickHouse
* **Dispositivo:** `device_type` è la classe derivata (i token osservati includono
  `desktop`, `mobile`, `tablet`, `unknown`). `user_agent` grezzo non è presente in
  `/v1`. L'IP grezzo non è mai presente in `/v1` (non viene mai salvato in modo persistente; solo `ip_hash` con 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
```

Utilizzare `occurred_after` per i caricamenti incrementali degli eventi. I fatti sono di sola aggiunta;
gli arrivi tardivi sono nuove righe.

### entities

* **Percorso:** `GET /v1/reporting/entities`
* **Granularità:** una riga corrente per entità
* **Filtri:** `type`, `updated_after`, più `limit` / `page_token`
* **Oggetto wire:** `reporting.entity`
* **Chiave di join:** `id` / `entity_id` (stesso valore)

Collegare i fatti di phishing alle persone con
`reporting.simulation_results.entity_id = reporting.entities.id`.
`GET /v1/entities` operativo rimane su `entity:read` e richiede un'autorizzazione
diversa.

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

Questi due dataset prevedono un'eccezione denominata di sola versione corrente:

> Non esiste alcuna tabella autorevole dello storico delle versioni o delle appartenenze. Questo dataset proietta la riga corrente come un unico intervallo aperto (`valid_to` è null, `is_current` è true). `valid_from` è `created_at` oppure, in alternativa, `updated_at` per le versioni delle entità, e l'ora di osservazione della richiesta per le appartenenze alle coorti. Gli intervalli storici chiusi non vengono fabbricati.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Stessi filtri di
  entities. `valid_from` è `created_at` oppure, in alternativa, `updated_at`. Un'entità
  con entrambi i timestamp null viene omessa.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtri:
  `cohort`, `entity`, `updated_after`. `updated_at` è l'ultimo aggiornamento della coorte,
  in modo che l'aggiornamento incrementale rimanga attendibile. `valid_from` è l'ora
  di osservazione della richiesta. Collegare `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
```

## Modello semantico a schema a stella su /v1/activities

`/v1/activities` è il flusso di eventi con timestamp dell'organizzazione. Non ha
misure numeriche: ogni quantità in un report è un conteggio di righe, il che rende lo schema a stella una scelta naturale.

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

* **`Activities`** è il fatto evento alla granularità reale. Una riga per attività.
  I partecipanti vengono rimossi qui e ricostruiti come tabella a sé stante.
* **`ActivityParticipants`** esiste perché i partecipanti sono effettivamente
  uno-a-molti, con ruoli come `actor` e `target`. Appiattirli su
  `Activities` comporterebbe la duplicazione delle righe delle attività (gonfiando ogni conteggio)
  oppure lo scarto dei partecipanti aggiuntivi.
  La relazione con `Activities` è l'unica relazione bidirezionale del modello,
  così che uno slicer sulle entità raggiunga la tabella dei fatti.
* **`Entities`** è la dimensione che nomina i partecipanti. Le righe dei partecipanti
  in un'attività contengono `entity_id` e un ruolo, ma non un nome visualizzato.
* **`Dates`** viene generata in M (zero chiamate API) e contrassegnata come tabella
  delle date del modello. Questo rende valida la time intelligence DAX
  (`DATESINPERIOD`, `DATEADD`). Le colonne con i nomi di mesi e giorni della settimana dichiarano
  `sortByColumn` sulle rispettive controparti numeriche, in modo che i visual non ordinino i mesi
  in ordine alfabetico.

Tutte le misure sono esplicite (il modello imposta `discourageImplicitMeasures`): totale
attività, entità attive, attività per entità attiva, giorni con attività
e media giornaliera mobile a 7 giorni.

### Struttura della tabella bridge

`ActivityParticipants` scorre `/v1/activities` ed espande l'array `participants`
in una riga per partecipante:

```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` scorre `/v1/activities` in modo indipendente, anziché
fare riferimento alla query `Activities`. In Power BI, due query caricate come due
tabelle eseguono ciascuna un recupero indipendente al momento dell'aggiornamento. Mantenerle indipendenti
consente al servizio di aggiornare in parallelo le partizioni di entrambe le tabelle.

## Aggiornamento incrementale

Entrambe le tabelle a granularità di fatto (`Activities` e `ActivityParticipants`) dichiarano una
policy di aggiornamento incrementale di Power BI: conservare 5 anni di partizioni annuali e
reimportare gli ultimi 14 giorni a ogni aggiornamento.

Il meccanismo è una coppia di parametri DateTime, `RangeStart` e `RangeEnd`,
che il servizio Power BI riassegna per ogni partizione al momento dell'aggiornamento. Ogni query dei fatti
riporta tali parametri nei filtri `start_date` e `end_date` dell'API, in modo che il volume di richieste
a livello di partizione sia proporzionale alla partizione
e non all'intero storico. La query amplia di un minuto per lato la finestra dell'API e riapplica localmente l'esatto limite semiaperto `[RangeStart, RangeEnd)`,
così che la correttezza delle partizioni non dipenda dal fatto che il server
tratti i propri limiti come inclusivi o esclusivi.

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

Il primo aggiornamento dopo una distribuzione applica la policy: un caricamento storico completo,
lento. Ogni aggiornamento successivo interessa solo la finestra incrementale.

Misurato su un dataset di \~158.000 attività e \~2.300 entità:

| Aggiornamento | Tempo totale |
| - | - |
| Primo (applicazione della policy, caricamento storico completo) | \~31 min |
| Ogni aggiornamento successivo | **\~15 s** |

<Warning>
  L'endpoint REST di aggiornamento classico di Power BI (`POST .../refreshes` con il solo
  `notifyOption`) ignora silenziosamente le policy di aggiornamento ed esegue ogni volta una scansione
  completa. Per applicare la policy tramite l'API REST, utilizzare l'aggiornamento avanzato (enhanced refresh): inviare
  `type`, `commitMode` e `applyRefreshPolicy` nel corpo della richiesta. Gli aggiornamenti
  avviati dall'utilità di pianificazione del servizio Power BI applicano sempre le policy: questa
  particolarità è specifica della chiamata REST.
</Warning>

<Note>
  La finestra di 14 giorni è volutamente ampia. L'acquisizione delle attività non è di sola aggiunta all'istante corrente:
  i backfill dei connettori inseriscono eventi con timestamp passati.
  Una finestra di 1-2 giorni li perderebbe in modo permanente.
</Note>

## Estensione della finestra di aggiornamento nell'API

`GET /v1/activities` accetta i filtri `start_date` e `end_date` in cui vengono riportati i
parametri di Power BI, filtrando `event_ts` tra i limiti.
`Entities` è una dimensione, quindi questo esempio la aggiorna ogni volta per intero
anziché in modo incrementale.

I dataset di reporting utilizzano watermark propri (`updated_after`,
`occurred_after`) anziché la finestra di date delle attività.

## Rate limit durante l'aggiornamento

`/v1` applica budget separati per chiave e per IP (vedere
[rate limit](/it/developers/v1/rate-limits)). Rispettare `429` + `Retry-After`; non
inserire in modo fisso un budget di richieste nel modello. Due caratteristiche degli aggiornamenti di Power BI
sono rilevanti:

* **La scansione di una singola tabella è sequenziale.** Ogni richiesta di pagina richiede il
  token della pagina precedente, quindi una singola tabella è vincolata dalla latenza indipendentemente dalla dimensione
  della pagina.
* **Gli aggiornamenti simultanei delle tabelle si moltiplicano.** L'aggiornamento in parallelo di più tabelle
  di grandi dimensioni è il caso che raggiunge il limite massimo, ed è per questo che il percorso di nuovo tentativo
  per il 429 in `Fn_FetchAll` è importante. Preferire un numero ridotto di tabelle in parallelo anziché
  presumere una concorrenza illimitata.

Il nuovo tentativo è stato verificato con un limiter realmente saturo. Un aggiornamento
eseguito mentre un carico di lavoro parallelo manteneva deliberatamente la chiave al limite massimo
è stato comunque completato e ha prodotto lo stesso numero di righe di una baseline senza contesa:
i nuovi tentativi hanno restituito pagine complete, senza lacune.

## Versioni del contratto e credenziali memorizzate

`/v1` accetta un'intestazione di richiesta `Living-Security-Version` per fissare una versione
pubblicata del contratto (vedere [Guida rapida all'API](/it/developers/v1/quickstart)). Power
Query non può inviare tale intestazione quando l'origine dati utilizza credenziali memorizzate:
solo le origini dati anonime consentono intestazioni personalizzate. Aggiungerla fa fallire l'aggiornamento
con:

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

Conservare la chiave nelle credenziali memorizzate di Power BI e omettere l'intestazione personalizzata.
In assenza di una versione esplicita nella richiesta, l'API utilizza il valore `contract_version` fissato per l'account di servizio
oppure, se non è impostato alcun valore, l'ultima versione pubblicata.
Prima di pubblicare un aggiornamento, verificare la versione del contratto dell'account con l'amministratore dell'API. Per le versioni supportate, vedere il [changelog dell'API](/it/developers/v1/changelog).

Se un aggiornamento inizia a comportarsi in modo inatteso, esaminare l'intestazione di risposta
`Living-Security-Version` dell'API per verificare quale contratto
ha effettivamente servito la richiesta.

## Aggiornare e verificare il modello

Pubblicare il modello semantico nel workspace Power BI, associare la relativa credenziale memorizzata
all'origine `/v1` regionale ed eseguire un aggiornamento prima di abilitare una
pianificazione. Esaminare la cronologia degli aggiornamenti per individuare errori o letture incomplete.

Confrontare il numero di righe con una lettura API indipendente che utilizzi gli stessi filtri.
La [guida alla paginazione](/it/developers/v1/pagination) include esempi in Python e cURL
per la lettura di un set di risultati completo. Un aggiornamento che legge solo la
prima pagina può sembrare riuscito su un dataset di piccole dimensioni; confrontare i conteggi prima di
fare affidamento sul report.

## Scelte di modellazione

* **I totali delle campagne e i risultati per singola persona hanno granularità diverse.**
  `SimulationResults` richiama `GET /v1/simulations/{id}/results`
  (una GET per esecuzione). Per il phishing per singola persona, aggiungere query per
  `GET /v1/reporting/simulation_results` e
  `GET /v1/reporting/simulation_events` (oppure `GET /v1/simulation_results`
  operativo se si dispone già di `simulation:read`).
* **`vectors` su `/v1/simulations` viene appiattito in una stringa separata da virgole.**
  Una colonna di tipo elenco non può essere usata come slicer in una vera dimensione. Se una pagina del report
  deve filtrare per vettore, aggiungere una tabella bridge simulazione-vettore.

## Passaggi successivi

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/it/developers/developer-hub/overview">
    Creare un account di servizio e una chiave API per Power BI.
  </Card>

  <Card title="Guida rapida all'API" href="/it/developers/v1/quickstart">
    Autenticare le richieste `/v1` con l'autenticazione Basic.
  </Card>

  <Card title="Paginazione" href="/it/developers/v1/pagination">
    L'envelope dell'elenco, i token cursore e la stabilità dei filtri durante una scansione.
  </Card>

  <Card title="Rate limit" href="/it/developers/v1/rate-limits">
    Budget per chiave e per IP, il contratto 429 e indicazioni sui nuovi tentativi.
  </Card>
</CardGroup>


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