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

> Koble Power BI til Living Security Public API med Power Query, bygg en stjerneskjemamodell på /v1, og velg mellom operasjonelle og rapporteringsbaserte datasett for phishing.

Denne veiledningen er et helhetlig eksempel på hvordan man leser `/v1` inn i Power BI. Den dekker
tilkoblingsmønsteret i Power Query (M), en semantisk stjerneskjemamodell
bygget på `/v1/activities`, inkrementell oppdatering, og når man bør bruke **reporting**-forhåndsvisningsdatasettene for utfall knyttet til phishing per person.

Eksemplene bruker produksjonsverten i USA
(`https://api.us-east-1.livingsecurity.ai/v1`). For en organisasjon i EU bruker du
`https://api.eu-west-1.livingsecurity.ai/v1` i både `Fn_FetchAll` og
`Fn_GetJson`, og binder deretter datakildens legitimasjon til den verten. Se
[Regioner](/no/developers/v1/regions).

Denne veiledningen bruker lagret Basic-legitimasjon med en API-nøkkel. Du trenger ingen
OAuth-klient. Se [Autentisering](/no/developers/v1/authentication) for
valg av legitimasjon, og [Developer Hub](/no/developers/developer-hub/overview)
for tilgang og oppsett.

## Hva dette eksempelet brukes til

Bruk det når du må:

* Laste listeendepunktene i `/v1` inn i et Power BI-datasett med korrekt
  markørbasert paginering og nytt forsøk ved ratebegrensning.
* Bygge en semantisk modell der målinger («aktive enheter», «aktiviteter per
  aktiv enhet») er dimensjonelle i stedet for å telles fra en flat tabell.
* Redusere en planlagt Power BI-oppdatering fra en full gjennomgang av historikken til et
  lite døgnvindu ved hjelp av inkrementell oppdatering.
* Velge riktig granularitet for phishing: kampanjetotaler (operasjonelt) versus
  resultater per person og typede hendelser (forhåndsvisning av rapportering).

## Forutsetninger

* En API-nøkkel med de tillatelsene spørringene dine trenger:
  * Operasjonelt eksempel: `activity:read`, `entity:read` og
    `simulation:read`.
  * Phishing per person via rapporteringsdatasett: `reporting:read`.
    Se [API-nøkler](/no/developers/developer-hub/api-keys).
* Power BI Desktop, eller et Power BI- / Fabric-arbeidsområde der du kan opprette en
  semantisk modell og planlegge oppdatering.

## Velg riktig granularitet

`/v1` tilbyr to beslektede flater for phishing. De er ikke utskiftbare.

| Behov | Bruk | Tillatelse | Granularitet |
| - | - | - | - |
| Totaler for kampanjetrakt | `GET /v1/simulations/{id}/results` | `simulation:read` | Én rad per simulering (levert, åpnet, klikket, …) |
| Utfall per person (operasjonelt) | `GET /v1/simulation_results` | `simulation:read` | Én rad per enhet × simulering |
| Utfall per person (BI / rapportering) | `GET /v1/reporting/simulation_results` | `reporting:read` | Samme granularitet, BI-feltnavn (`entity_id`, `simulation_id`), `updated_after`-vannmerke |
| Typede interaksjonsfakta | `GET /v1/reporting/simulation_events` | `reporting:read` | Ett faktum per interaksjon (`simulation.link_clicked`, …), `occurred_after` |
| Gjeldende arbeidsstyrkedimensjon | `GET /v1/reporting/entities` | `reporting:read` | Én gjeldende rad per enhet. Koble på `reporting.simulation_results.entity_id = reporting.entities.id` |
| Gjeldende enhetsversjoner | `GET /v1/reporting/entity_versions` | `reporting:read` | Ett åpent, gjeldende intervall per enhet. Det finnes ingen tabell med versjonshistorikk |
| Gjeldende kohortmedlemskap | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Ett gjeldende faktum per `(cohort_id, entity_id)`. Det finnes ingen tabell med medlemskapshistorikk |
| Koblingsrygg for alle aktivitetstyper | `GET /v1/activities` | `activity:read` | Én hendelse per aktivitet, med nøstet `participants[]` |

Eksempelspørringene for Power BI nedenfor kaller de **operasjonelle** rutene
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). For rapportering av phishing per person bør du bruke
**reporting**-forhåndsvisningsfamilien nedenfor. En nøkkel som kun har fått `reporting:read` kan ikke
kalle operasjonell `/v1/simulation_results` (API-et returnerer `403`).

### Vanskelighetsskala

Vanskelighetsgraden for simuleringer er et heltall fra 1 til 5 på `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), `/v1/activities`-egenskaper
(`difficulty_tier`) og `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` er enklest og `5` er vanskeligst. Adaptive og progressive simuleringer har ingen
enkeltverdi på kampanjenivå, så `/v1/simulations.difficulty_level` er `null`,
mens `difficulty_level` per mottaker på resultater fortsatt kan være satt. For å tilordne til
eldre eksporter med lav/middels/høy, behandler du `1–2` som lav, `3` som middels og `4–5` som
høy.

<Warning>
  Ikke bygg nye BI-integrasjoner mot udokumenterte nettverkskall fra Dashbord
  eller den eldre, kuraterte oversikten over Public API for `/api/v1`. Autentiser mot
  organisasjonens regionale `/v1`-vert med en API-nøkkel fra Developer Hub. Se
  [Regioner](/no/developers/v1/regions).
</Warning>

## Tilkoblingsmønster (Power Query)

Fire regler ligger til grunn for hver spørring i eksempelet. Hver av dem håndheves av
Power BI-tjenesten, og er ikke et spørsmål om stil.

<AccordionGroup>
  <Accordion title="Hold basis-URL-en i Web.Contents statisk og la den ende på /v1">
    Legg alt dynamisk i `RelativePath` og `Query`. Det første argumentet til
    `Web.Contents` må være en bokstavelig streng som ender ved organisasjonens
    regionale `/v1`-rot:

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

    Kallere sender deretter kun ressursen (`"simulations"`, ikke
    `"v1/simulations"`). Ikke lim inn en amerikansk vert for en organisasjon i EU.

    Hvis det første argumentet bygges ved strengsammenslåing, kan Power BI-tjenesten
    ikke identifisere datakilden statisk, og planlagt oppdatering mislykkes med
    en feil om «dynamisk datakilde», selv om spørringen fungerer i Power BI
    Desktop.
  </Accordion>

  <Accordion title="Oppgi API-nøkkelen som lagret Basic-legitimasjon, ikke i spørringsteksten">
    Når Power BI ber om legitimasjon for datakilden `/v1`, velger du
    **Basic** og limer inn API-nøkkelen som **brukernavn**. Legg inn en plassholder som
    `x` i feltet **passord**. Power BI-tjenesten avviser et tomt
    passord selv om API-et ignorerer det feltet. Nøkkel som passord med et
    plassholder-brukernavn fungerer også.

    Ikke legg til en `Authorization`-header i M. Det fungerer på din lokale maskin, men
    bakes deretter inn i spørringsfilen med nøkkelen og skaper problemer for planlagt oppdatering.
  </Accordion>

  <Accordion title="La tilkoblingstesten i legitimasjonsdialogen kjøre">
    `GET /v1` er autentisert: den returnerer `200` med en gyldig nøkkel og `401`
    uten. Pek datakilden mot `…/v1` og **ikke** hopp over testen –
    en gyldig nøkkel lagres, mens en tilbakekalt eller ugyldig nøkkel mislykkes umiddelbart.

    Eldre eksempler brukte den rene verten som datakilde og ba deg hoppe over
    testen fordi `GET /` alltid er `404`. Pek disse modellene om til `…/v1`.
  </Accordion>

  <Accordion title="Sett limit=500 eksplisitt på hver listespørring">
    Standard sidestørrelse på hvert listeendepunkt i `/v1` er 25. Hvis du beholder
    standarden, øker antall forespørsler for de samme dataene. Spørringsverdier må være tekst –
    `limit = "500"` i M, ikke `limit = 500`.
  </Accordion>
</AccordionGroup>

### Delt pagineringsfunksjon

Hver spørring i eksempelet kaller én enkelt `Fn_FetchAll` som går gjennom
`next_page_token` til den er `null` (se
[paginering](/no/developers/v1/pagination)). Dens viktigste egenskaper:

* **Statisk `/v1`-basis-URL, dynamisk `RelativePath` og `Query`.** Påkrevd for
  planlagt oppdatering og for legitimasjonstesten.
* **`ManualStatusHandling = {429}` med begrenset antall nye forsøk.** Funksjonen leser
  `Retry-After` når Power Query eksponerer den, ellers venter den 30 sekunder, og
  gir opp etter 5 påfølgende `429`-svar med en eksplisitt feil i stedet for
  å gå i løkke til oppdateringen går ut på tid. `5xx` prøves bevisst ikke på
  nytt – en serverfeil skal komme til syne, ikke i det stille gi et delvis
  datasett.
* **`Timeout` på 60 sekunder.** Sider svarer vanligvis på godt under ett sekund, så
  ett minutts stillhet betyr at forespørselen er død.
* **`Binary.Buffer` før `Json.Document`.** Uten buffering kan mashup-motoren
  be om svarstrømmen på nytt hvis den leser den mer enn én gang.
* **`baseQuery` sendes uendret i hver iterasjon.** Sidetokener er
  ugjennomsiktige, signerte markører bundet til organisasjonen, legitimasjonen, kontraktsversjonen
  og fingeravtrykket av filtre og sortering. Hvis et filter endres
  midt i gjennomgangen, avvises det som et ugyldig token i stedet for å starte på nytt fra side
  én.

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

### Typede spørringer per endepunkt

Hver forbruker av `Fn_FetchAll` følger samme mønster: kall endepunktet,
projiser en fast kolonneliste, fjern diskriminatoren `object`, og typ
tidsstempler som `datetimezone` slik at datomål ikke forskyves i det stille på grunn av
tidssonen til oppdateringskapasiteten.

<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>
  En eksplisitt kolonneliste på `Table.FromRecords` holder skjemaet stabilt når
  et endepunkt returnerer null rader. En bar `Table.FromRecords({})` gir en
  tabell uten kolonner, og alle senere trinn mislykkes på grunn av manglende felt.
</Tip>

## Excel

Den samme Power Query (M) kjører i Excels Get & Transform – det er samme
motor som i Power BI Desktop. Bruk dette når du vil ha en regnearktabell
i stedet for en semantisk modell.

I Excel for Windows (Microsoft 365, Excel 2024, 2021, 2019 eller 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. I Power Query Editor velger du **Hjem → Avansert Redigerer**. Lim inn
   `Fn_FetchAll` først, og gi deretter spørringen navnet `Fn_FetchAll` (Innstillinger for spørring → Name, eller høyreklikk i Queries-ruten → **Rename**).
3. Gjenta for `Fn_GetJson`, og deretter for hver rapportspørring.
4. **Hjem → Close & Load → Close & Load To…** åpner dialogen **Import Data**.
   Velg **Only Create Connection** for `Fn_FetchAll` og
   `Fn_GetJson` (de er funksjoner, ikke tabeller). Velg **Table** for
   hver rapportspørring du vil ha i et regneark.
5. Ved første oppdatering velger du **Basic**: API-nøkkelen som **User name**, en
   plassholder som `x` som **passord**. Excel lagrer én legitimasjon
   per datakilde-URL; senere spørringer som treffer samme `/v1`-vert gjenbruker
   den.

<Note>
  **Close & Load To…** kan være nedtonet første gang du oppretter en spørring i
  en arbeidsbok. Bruk **Close & Load**, velg deretter i Excel **Data →
  Queries & Connections**, høyreklikk spørringen og velg **Load To**.
  Denne fremgangsmåten er dokumentert av Microsoft for dette tilfellet.
</Note>

Du kan ikke sende `Living-Security-Version` fra Power Query på en
autentisert kilde – samme begrensning som i Power BI. Fest versjonen på tjenestekontoen
eller send `api_version` i spørringsposten. Se
[Kontraktsversjoner og lagret legitimasjon](#kontraktsversjoner-og-lagret-legitimasjon).

Ferdige rapportspørringer (kolonnetilordninger og avvik fra en
tidligere leverandørs eksport):

<CardGroup cols={3}>
  <Card title="Samsvar per person" href="/no/developers/v1/reports/per-person-compliance">
    Opplæringspåmeldinger med granularitet person × tildeling.
  </Card>

  <Card title="Phishing-logg for e-post" href="/no/developers/v1/reports/email-phishing-log">
    Phishing-interaksjoner på hendelsesnivå, siste 12 måneder.
  </Card>

  <Card title="Mest utsatte brukere" href="/no/developers/v1/reports/most-susceptible-users">
    Oppsummering per person over 12 måneder for klikk, legitimasjon og rapportering.
  </Card>
</CardGroup>

## Forhåndsvisningsdatasett for rapportering (phishing per person)

Forhåndsvisningsfamilien for rapportering tilbyr datasett for phishing per person.
Den krever `reporting:read` og rapporteringstilgang for organisasjonen din.
Bruk datasettkatalogen nedenfor til å undersøke skjemaene og databehandlingsreglene.

Oppdag den aktive katalogen:

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

Hvert datasett deklarerer granularitet, primærnøkkel, koblinger, vannmerke, korrigerings- og
slettingsregel.

### simulation\_results

* **Sti:** `GET /v1/reporting/simulation_results`
* **Granularitet:** enhet × simulering
* **Filtre:** `simulation`, `entity`, `updated_after`, pluss `limit` /
  `page_token`
* **Wire-objekt:** `reporting.simulation_result`
* **Koblingsnøkler:** `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
```

Bruk `updated_after` som inkluderende nedre vannmerke når du oppdaterer nylige
partisjoner. Mutable resultatdimensjoner omskrives på stedet under samme `id`.

### simulation\_events

* **Sti:** `GET /v1/reporting/simulation_events`
* **Granularitet:** ett typet faktum per observerte interaksjon
* **Filtre:** `simulation`, `entity`, `event_type`, `occurred_after`, pluss
  `limit` / `page_token`
* **Wire-objekt:** `reporting.simulation_event`
* **Hendelsestyper:** lukkede offentlige tokens som `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` – ikke rå
  ClickHouse-aktivitetstyper av typen `phishing.simulation.*`
* **Enhet:** `device_type` er den avledede klassen (observerte tokens inkluderer
  `desktop`, `mobile`, `tablet`, `unknown`). Rå `user_agent` finnes ikke på
  `/v1`. Rå IP finnes aldri på `/v1` (lagres aldri; kun saltet `ip_hash`).

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

Bruk `occurred_after` for inkrementelle hendelsesinnlastinger. Fakta er kun-tillegg;
sene ankomster er nye rader.

### entities

* **Sti:** `GET /v1/reporting/entities`
* **Granularitet:** én gjeldende rad per enhet
* **Filtre:** `type`, `updated_after`, pluss `limit` / `page_token`
* **Wire-objekt:** `reporting.entity`
* **Koblingsnøkkel:** `id` / `entity_id` (samme verdi)

Koble phishing-fakta til personer med
`reporting.simulation_results.entity_id = reporting.entities.id`.
Operasjonell `GET /v1/entities` forblir på `entity:read` og er en annen
tillatelse.

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

Disse to datasettene har et navngitt unntak som kun gjelder gjeldende tilstand:

> Det finnes ingen autoritativ tabell for versjons- eller medlemskapshistorikk. Dette datasettet projiserer den gjeldende raden som ett åpent intervall (`valid_to` er null, `is_current` er true). `valid_from` er `created_at` eller ellers `updated_at` for enhetsversjoner, og forespørselens observasjonstidspunkt for kohortmedlemskap. Lukkede historiske intervaller fabrikkeres ikke.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Samme filtre som
  entities. `valid_from` er `created_at` eller ellers `updated_at`. En enhet
  der begge tidsstemplene er null utelates.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtre:
  `cohort`, `entity`, `updated_after`. `updated_at` er siste oppdatering av kohorten,
  slik at inkrementell oppdatering forblir pålitelig. `valid_from` er forespørselens
  observasjonstidspunkt. Koble `entity_id` til `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
```

## Semantisk stjerneskjemamodell på /v1/activities

`/v1/activities` er organisasjonens tidsstemplede hendelsesstrøm. Den har ingen
numeriske mål – hver mengde i en rapport er et radantall – noe som gjør et
stjerneskjema til et naturlig valg.

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

* **`Activities`** er hendelsesfaktaet på reell granularitet. Én rad per aktivitet.
  Deltakere fjernes her og bygges opp igjen som sin egen tabell.
* **`ActivityParticipants`** finnes fordi deltakere faktisk er
  én-til-mange, med roller som `actor` og `target`. Flating ut på
  `Activities` ville enten duplisert aktivitetsrader (og blåst opp alle tellinger)
  eller forkastet ytterligere deltakere.
  Relasjonen til `Activities` er modellens eneste toveisrelasjon,
  slik at en enhetsslicer når faktatabellen.
* **`Entities`** er dimensjonen som navngir deltakere. Deltakerrader
  på en aktivitet har `entity_id` og en rolle, men ikke et visningsnavn.
* **`Dates`** genereres i M (null API-kall) og markeres som modellens
  datotabell. Det er dette som gjør DAX-funksjoner for tidsetterretning
  (`DATESINPERIOD`, `DATEADD`) tillatt. Kolonner for måneds- og ukedagsnavn deklarerer
  `sortByColumn` på de numeriske tvillingkolonnene, slik at visualiseringer ikke sorterer måneder
  alfabetisk.

Alle mål er eksplisitte (modellen setter `discourageImplicitMeasures`): totalt antall
aktiviteter, aktive enheter, aktiviteter per aktiv enhet, dager med aktivitet,
og et 7-dagers glidende døgngjennomsnitt.

### Utforming av koblingstabellen

`ActivityParticipants` går gjennom `/v1/activities` og ekspanderer `participants`-arrayen
til én rad per deltaker:

```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` går gjennom `/v1/activities` uavhengig i stedet for å
referere til spørringen `Activities`. I Power BI henter to spørringer som lastes som to
tabeller hver for seg ved oppdatering. Ved å holde dem uavhengige
kan tjenesten oppdatere partisjonene i begge tabellene parallelt.

## Inkrementell oppdatering

Begge tabellene på faktanivå (`Activities` og `ActivityParticipants`) deklarerer en
inkrementell oppdateringspolicy i Power BI: behold 5 år med årlige partisjoner,
importer de siste 14 dagene på nytt ved hver oppdatering.

Mekanismen er et par DateTime-parametere, `RangeStart` og `RangeEnd`,
som Power BI-tjenesten binder på nytt per partisjon ved oppdatering. Hver faktaspørring
bretter disse parameterne inn i API-ets filtre `start_date` og `end_date`,
slik at antall forespørsler per partisjon står i forhold til partisjonen,
ikke til total historikk. Spørringen utvider API-vinduet med ett minutt
på hver side og anvender den nøyaktige halvåpne grensen `[RangeStart, RangeEnd)`
lokalt på nytt, slik at partisjonens korrekthet ikke avhenger av om serveren
behandler grensene sine som inkluderende eller ekskluderende.

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

Den første oppdateringen etter en distribusjon anvender policyen: en full historisk innlasting,
som er langsom. Hver oppdatering etter det berører bare det inkrementelle vinduet.

Målt på et datasett med ca. 158 000 aktiviteter og ca. 2 300 enheter:

| Oppdatering | Tidsbruk |
| - | - |
| Første (anvendelse av policy, full historisk innlasting) | \~31 min |
| Hver oppdatering etter det | **\~15 s** |

<Warning>
  Det klassiske REST-endepunktet for oppdatering i Power BI (`POST .../refreshes` med kun
  `notifyOption`) ignorerer oppdateringspolicyer i det stille og gjør en full gjennomgang hver
  gang. For å anvende policyen gjennom REST-API-et bruker du utvidet oppdatering: send
  `type`, `commitMode` og `applyRefreshPolicy` i forespørselens body. Oppdateringer
  utløst av planleggeren i Power BI-tjenesten anvender alltid policyer – denne
  egenheten gjelder kun REST-kallet.
</Warning>

<Note>
  Vinduet på 14 dager er bevisst romslig. Aktivitetsinnhenting er ikke
  kun-tillegg-på-nåtidspunktet: etterfylling fra koblinger legger inn hendelser med tidsstempler fra fortiden.
  Et vindu på 1–2 dager ville gått glipp av dem for alltid.
</Note>

## Utvide oppdateringsvinduet inn i API-et

`GET /v1/activities` godtar filtrene `start_date` og `end_date` som
Power BI-parameterne bretter seg inn i, og filtrerer `event_ts` mellom grensene.
`Entities` er en dimensjon, så dette eksempelet oppdaterer den fullstendig hver gang
i stedet for inkrementelt.

Rapporteringsdatasett bruker sine egne vannmerker (`updated_after`,
`occurred_after`) i stedet for datovinduet for aktiviteter.

## Ratebegrensninger under oppdatering

`/v1` håndhever separate budsjetter per nøkkel og per IP (se
[ratebegrensninger](/no/developers/v1/rate-limits)). Respekter `429` + `Retry-After`; ikke
hardkod et forespørselsbudsjett inn i modellen. To egenskaper ved Power BI-oppdateringer
er viktige:

* **En enkelt tabellgjennomgang er sekvensiell.** Hver sideforespørsel trenger
  forrige sides token, så én tabell er latensbundet uavhengig av
  sidestørrelse.
* **Samtidige tabelloppdateringer multipliseres.** Når flere store tabeller oppdateres
  parallelt, nås taket, og det er derfor 429-stien for nye forsøk i
  `Fn_FetchAll` er viktig. Foretrekk færre parallelle tabeller fremfor å
  anta ubegrenset samtidighet.

Logikken for nye forsøk er testet mot en reell, mettet begrenser. En oppdatering
som kjørte mens en parallell arbeidsbelastning bevisst holdt nøkkelen ved taket
fullførte likevel og ga samme radantall som en basislinje uten konkurranse –
nye forsøk ga komplette sider, ikke hull.

## Kontraktsversjoner og lagret legitimasjon

`/v1` godtar en forespørselsheader `Living-Security-Version` for å feste en publisert
kontraktsversjon (se [API-hurtigstart](/no/developers/v1/quickstart)). Power
Query kan ikke sende den headeren når datakilden bruker lagret legitimasjon –
bare anonyme datakilder tillater egendefinerte headere. Hvis du legger den til, mislykkes en oppdatering
med:

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

Behold nøkkelen i Power BIs lagrede legitimasjon og utelat den egendefinerte headeren.
Når forespørselen ikke angir eksplisitt versjon, bruker API-et tjenestekontoens
festede `contract_version`, eller den nyeste publiserte versjonen hvis ingen versjon er festet.
Bekreft kontoens kontraktsversjon med API-administratoren før du
publiserer en oppdatering. Se [API-endringsloggen](/no/developers/v1/changelog) for
støttede versjoner.

Hvis en oppdatering begynner å oppføre seg uventet, kan du inspisere API-ets
svarheader `Living-Security-Version` for å se hvilken kontrakt som faktisk
behandlet forespørselen.

## Oppdater og verifiser modellen din

Publiser den semantiske modellen til Power BI-arbeidsområdet ditt, bind den lagrede
legitimasjonen til den regionale `/v1`-kilden, og kjør en oppdatering før du aktiverer en
tidsplan. Gå gjennom oppdateringshistorikken for feil eller ufullstendige lesinger.

Sjekk radantall mot en uavhengig API-lesing med de samme filtrene.
[Pagineringsveiledningen](/no/developers/v1/pagination) inneholder Python- og cURL-eksempler
for å lese et komplett resultatsett. En oppdatering som bare leser den
første siden kan se vellykket ut på et lite datasett; sammenlign antallet før
du stoler på rapporten.

## Modellvalg

* **Kampanjetotaler og resultater per person har ulik granularitet.**
  `SimulationResults` kaller `GET /v1/simulations/{id}/results`
  (ett GET per kjøring). For phishing per person legger du til spørringer for
  `GET /v1/reporting/simulation_results` og
  `GET /v1/reporting/simulation_events` (eller operasjonell
  `GET /v1/simulation_results` hvis du allerede har `simulation:read`).
* **`vectors` på `/v1/simulations` er flatet ut til en kommaseparert streng.**
  En kolonne av listetype kan ikke brukes som en reell dimensjon i slicere. Hvis en rapportside
  må filtrere på vektor, legger du til en koblingstabell mellom simulering og vektor.

## Neste trinn

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/no/developers/developer-hub/overview">
    Opprett en tjenestekonto og en API-nøkkel for Power BI.
  </Card>

  <Card title="API-hurtigstart" href="/no/developers/v1/quickstart">
    Autentiser forespørsler til `/v1` med Basic-autentisering.
  </Card>

  <Card title="Paginering" href="/no/developers/v1/pagination">
    Listeomslaget, markørtokener og filterstabilitet gjennom en hel gjennomgang.
  </Card>

  <Card title="Ratebegrensninger" href="/no/developers/v1/rate-limits">
    Budsjetter per nøkkel og per IP, 429-kontrakten og veiledning for nye forsøk.
  </Card>
</CardGroup>


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