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

> Forbind Power BI til Living Security Public API med Power Query, opbyg en stjerneskemamodel på /v1, og vælg mellem operationelle og rapporteringsdatasæt til phishing.

Denne vejledning er et komplet eksempel på, hvordan `/v1` læses ind i Power BI. Den omfatter
forbindelsesmønstret i Power Query (M), en semantisk stjerneskemamodel opbygget
på `/v1/activities`, trinvis opdatering og hvornår man bør bruge **reporting**-preview-datasættene til phishing-resultater pr. person.

Eksemplerne bruger den amerikanske produktionsvært
(`https://api.us-east-1.livingsecurity.ai/v1`). For en EU-organisation skal du bruge
`https://api.eu-west-1.livingsecurity.ai/v1` i både `Fn_FetchAll` og
`Fn_GetJson` og derefter binde datakildens legitimationsoplysninger til denne vært. Se
[Regioner](/da/developers/v1/regions).

Denne vejledning bruger lagrede Basic-legitimationsoplysninger med en API-nøgle. Du behøver ikke en
OAuth-klient. Se [Godkendelse](/da/developers/v1/authentication) for valgmulighederne for legitimationsoplysninger, og [Developer Hub](/da/developers/developer-hub/overview)
for adgang og opsætning.

## Hvad dette eksempel bruges til

Brug det, når du har brug for at:

* Indlæse listeendepunkter fra `/v1` i et Power BI-datasæt med korrekt cursor-paginering og gentagelsesforsøg ved rate limit.
* Opbygge en semantisk model, hvor målinger ("aktive entiteter", "aktiviteter pr.
  aktiv entitet") er dimensionelle i stedet for at blive talt op fra en flad tabel.
* Bringe en planlagt Power BI-opdatering ned fra en fuld gennemgang af historikken til et
  lille dagligt vindue ved hjælp af trinvis opdatering.
* Vælge den rette phishing-granularitet: kampagnetotaler (operationelle) kontra
  resultater pr. person og typede hændelser (reporting-preview).

## Forudsætninger

* En API-nøgle med de tilladelser, dine forespørgsler kræver:
  * Operationelt eksempel: `activity:read`, `entity:read` og
    `simulation:read`.
  * Phishing pr. person via rapporteringsdatasæt: `reporting:read`.
    Se [API-nøgler](/da/developers/developer-hub/api-keys).
* Power BI Desktop eller et Power BI-/Fabric-arbejdsområde, hvor du kan oprette en
  semantisk model og planlægge opdatering.

## Vælg den rette granularitet

`/v1` udstiller to beslægtede overflader til phishing. De kan ikke bruges i flæng.

| Behov | Brug | Tilladelse | Granularitet |
| - | - | - | - |
| Totaler for kampagnetragten | `GET /v1/simulations/{id}/results` | `simulation:read` | Én række pr. simulering (leveret, åbnet, klikket, …) |
| Resultater pr. person (operationelle) | `GET /v1/simulation_results` | `simulation:read` | Én række pr. entitet × simulering |
| Resultater pr. person (BI / rapportering) | `GET /v1/reporting/simulation_results` | `reporting:read` | Samme granularitet, BI-feltnavne (`entity_id`, `simulation_id`), `updated_after`-vandmærke |
| Typede interaktionsfakta | `GET /v1/reporting/simulation_events` | `reporting:read` | Ét faktum pr. interaktion (`simulation.link_clicked`, …), `occurred_after` |
| Aktuel Arbejdsstyrke-dimension | `GET /v1/reporting/entities` | `reporting:read` | Én aktuel række pr. entitet. Join `reporting.simulation_results.entity_id = reporting.entities.id` |
| Aktuelle entitetsversioner | `GET /v1/reporting/entity_versions` | `reporting:read` | Ét åbent, aktuelt interval pr. entitet. Der findes ingen tabel med versionshistorik |
| Aktuelle Kohorte-medlemskaber | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Ét aktuelt faktum pr. `(cohort_id, entity_id)`. Der findes ingen tabel med medlemskabshistorik |
| Join-rygrad for alle aktivitetstyper | `GET /v1/activities` | `activity:read` | Én hændelse pr. aktivitet, indlejret `participants[]` |

Power BI-eksempelforespørgslerne nedenfor kalder de **operationelle** ruter
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Til phishing-rapportering pr. person anbefales det at bruge
**reporting**-preview-familien nedenfor. En nøgle, der kun har tildelt `reporting:read`, kan ikke
kalde operationelle `/v1/simulation_results` (API'et returnerer `403`).

### Sværhedsgradsskala

Simuleringens sværhedsgrad er et heltal fra 1 til 5 på `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), `/v1/activities`-egenskaber
(`difficulty_tier`) og `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` er nemmest, og `5` er sværest. Adaptive og progressive simuleringer har ingen
enkelt værdi på kampagneniveau, så `/v1/simulations.difficulty_level` er `null`,
mens `difficulty_level` pr. modtager på resultater stadig kan være angivet. For at afbilde til
ældre eksporter med lav/mellem/høj skal `1–2` behandles som lav, `3` som mellem og `4–5` som
høj.

<Warning>
  Opbyg ikke nye BI-integrationer på udokumenterede netværkskald fra Dashboard
  eller den ældre kuraterede `/api/v1` Public API-oversigt. Godkend over for
  organisationens regionale `/v1`-vært med en API-nøgle fra Developer Hub. Se
  [Regioner](/da/developers/v1/regions).
</Warning>

## Forbindelsesmønster (Power Query)

Fire regler ligger til grund for hver forespørgsel i eksemplet. Hver enkelt af dem håndhæves af
Power BI-tjenesten og er ikke blot en stilpræference.

<AccordionGroup>
  <Accordion title="Hold basis-URL'en i Web.Contents statisk, og lad den slutte ved /v1">
    Læg alt dynamisk i `RelativePath` og `Query`. Det første argument til
    `Web.Contents` skal være en bogstavelig streng, der ender ved organisationens
    regionale `/v1`-rod:

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

    Kaldere sender derefter kun ressourcen (`"simulations"`, ikke
    `"v1/simulations"`). Indsæt ikke en amerikansk vært for en EU-organisation.

    Hvis det første argument bygges ved strengsammenkædning, kan Power BI-tjenesten
    ikke statisk identificere datakilden, og planlagt opdatering mislykkes med
    en fejl om "dynamic data source", selvom forespørgslen virker i Power BI
    Desktop.
  </Accordion>

  <Accordion title="Angiv API-nøglen som en lagret Basic-legitimation, ikke i forespørgselsteksten">
    Når Power BI beder om legitimationsoplysninger til `/v1`-datakilden, skal du vælge
    **Basic** og indsætte API-nøglen som **brugernavn**. Angiv en pladsholder som
    `x` i feltet **Adgangskode**. Power BI-tjenesten afviser en tom
    adgangskode, selvom API'et ignorerer feltet. Nøglen som adgangskode med et
    pladsholder-brugernavn virker også.

    Tilføj ikke en `Authorization`-header i M. Den virker på din computer, men
    bager derefter nøglen ind i forespørgselsfilen og giver problemer med planlagt opdatering.
  </Accordion>

  <Accordion title="Lad legitimationsdialogens forbindelsestest køre">
    `GET /v1` er godkendt: det returnerer `200` med en gyldig nøgle og `401`
    uden. Peg datakilden mod `…/v1`, og **spring ikke** testen over —
    en gyldig nøgle gemmes, mens en tilbagekaldt eller fejlformet nøgle fejler med det samme.

    Ældre eksempler brugte den blotte vært som datakilde og fortalte, at du skulle springe
    testen over, fordi `GET /` altid er `404`. Peg disse modeller om til `…/v1`.
  </Accordion>

  <Accordion title="Angiv eksplicit limit=500 på hver listeforespørgsel">
    Standardsidestørrelsen på hvert `/v1`-listeendepunkt er 25. Hvis du lader den stå på standard,
    multipliceres antallet af forespørgsler for de samme data. Forespørgselsværdier skal være tekst —
    `limit = "500"` i M, ikke `limit = 500`.
  </Accordion>
</AccordionGroup>

### Delt pagineringsfunktion

Hver forespørgsel i eksemplet kalder én enkelt `Fn_FetchAll`, der gennemgår
`next_page_token`, indtil den er `null` (se
[paginering](/da/developers/v1/pagination)). Dens væsentligste egenskaber:

* **Statisk `/v1`-basis-URL, dynamisk `RelativePath` og `Query`.** Påkrævet
  for planlagt opdatering og for legitimationstesten.
* **`ManualStatusHandling = {429}` med afgrænset gentagelsesforsøg.** Funktionen læser
  `Retry-After`, når Power Query udstiller den, ellers venter den 30 sekunder og
  opgiver efter 5 på hinanden følgende `429`-svar med en eksplicit fejl i stedet for
  at køre i ring, indtil opdateringen får timeout. `5xx` forsøges bevidst ikke
  igen — en serverfejl skal komme til syne og ikke stilfærdigt give et delvist
  datasæt.
* **`Timeout` på 60 sekunder.** Sider svarer typisk på langt under et sekund, så
  et minuts stilhed betyder, at forespørgslen er død.
* **`Binary.Buffer` før `Json.Document`.** Uden buffering kan mashup-motoren
  anmode om svarstrømmen igen, hvis den læser den mere end én gang.
* **`baseQuery` sendes uændret i hver iteration.** Sidetokens er
  uigennemsigtige, signerede cursorer bundet til organisationen, legitimationen, kontraktversionen
  og fingeraftrykket af filtre og sortering. Hvis et filter ændres
  midt i en gennemgang, afvises det som et ugyldigt token i stedet for at starte forfra fra side
  ét.

```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 forespørgsler pr. endepunkt

Hver forbruger af `Fn_FetchAll` følger samme form: kald endepunktet,
projicer en fast kolonneliste, fjern `object`-diskriminatoren, og typebestem
tidsstempler som `datetimezone`, så datomålinger ikke stilfærdigt forskydes af
opdateringskapacitetens tidszone.

<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 eksplicit kolonneliste på `Table.FromRecords` holder skemaet stabilt, når
  et endepunkt returnerer nul rækker. En blottet `Table.FromRecords({})` giver en
  tabel uden kolonner, og alle senere trin fejler på de manglende felter.
</Tip>

## Excel

Den samme Power Query (M) kører i Excels Hent og transformér — det er den samme
motor som i Power BI Desktop. Brug dette, når du ønsker en regnearkstabel
i stedet for en semantisk model.

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

1. **Data → Hent data → Fra andre kilder → Tom forespørgsel.**
2. I Power Query Editor skal du vælge **Hjem → Avanceret Editor**. Indsæt
   først `Fn_FetchAll`, og omdøb derefter forespørgslen til `Fn_FetchAll` (Forespørgselsindstillinger → Navn, eller højreklik i ruden Forespørgsler → **Omdøb**).
3. Gentag for `Fn_GetJson` og derefter for hver rapportforespørgsel.
4. **Hjem → Luk og indlæs → Luk og indlæs til…** åbner dialogen **Importér data**.
   Vælg **Opret kun forbindelse** for `Fn_FetchAll` og
   `Fn_GetJson` (de er funktioner, ikke tabeller). Vælg **Tabel** for
   hver rapportforespørgsel, du ønsker i et regneark.
5. Ved første opdatering skal du vælge **Basic**: API-nøglen som **Brugernavn**, en
   pladsholder som `x` som **Adgangskode**. Excel gemmer én legitimation
   pr. datakilde-URL; senere forespørgsler, der rammer den samme `/v1`-vært, genbruger
   den.

<Note>
  **Luk og indlæs til…** kan være nedtonet, første gang du opretter en forespørgsel i
  en projektmappe. Brug **Luk og indlæs**, vælg derefter i Excel **Data →
  Forespørgsler og forbindelser**, højreklik på forespørgslen, og vælg **Indlæs til**.
  Den fremgangsmåde er dokumenteret af Microsoft til dette tilfælde.
</Note>

Du kan ikke sende `Living-Security-Version` fra Power Query på en
godkendt kilde — samme begrænsning som i Power BI. Fastlås servicekontoen eller send
`api_version` i forespørgselsposten. Se
[Kontraktversioner og lagrede legitimationsoplysninger](#kontraktversioner-og-lagrede-legitimationsoplysninger).

Færdige rapportforespørgsler (kolonnemappinger og afvigelser fra en
tidligere udbyders eksport):

<CardGroup cols={3}>
  <Card title="Compliance pr. person" href="/da/developers/v1/reports/per-person-compliance">
    Tilmeldinger til Træning på granularitet person × tildeling.
  </Card>

  <Card title="E-mail-phishinglog" href="/da/developers/v1/reports/email-phishing-log">
    Phishing-interaktioner på hændelsesniveau, seneste 12 måneder.
  </Card>

  <Card title="Mest modtagelige brugere" href="/da/developers/v1/reports/most-susceptible-users">
    Opsummering pr. person af klik, legitimationsoplysninger og rapporteringer over 12 måneder.
  </Card>
</CardGroup>

## Reporting-preview-datasæt (phishing pr. person)

Reporting-preview-familien leverer datasæt til phishing pr. person.
Den kræver `reporting:read` og rapporteringsadgang for din organisation.
Brug datasætkataloget nedenfor til at undersøge dens skemaer og datapolitikker.

Find det aktuelle katalog:

```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 datasæt angiver granularitet, primærnøgle, joins, vandmærke, korrektions- og
sletningspolitik.

### simulation\_results

* **Sti:** `GET /v1/reporting/simulation_results`
* **Granularitet:** entitet × simulering
* **filtre:** `simulation`, `entity`, `updated_after` samt `limit` /
  `page_token`
* **Wire-objekt:** `reporting.simulation_result`
* **Join-nøgler:** `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
```

Brug `updated_after` som inklusivt nedre vandmærke, når du opdaterer nylige
partitioner. Foranderlige resultatdimensioner gentages på stedet under samme `id`.

### simulation\_events

* **Sti:** `GET /v1/reporting/simulation_events`
* **Granularitet:** ét typet faktum pr. observeret interaktion
* **filtre:** `simulation`, `entity`, `event_type`, `occurred_after` samt
  `limit` / `page_token`
* **Wire-objekt:** `reporting.simulation_event`
* **Hændelsestyper:** lukkede offentlige tokens som `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — ikke rå
  ClickHouse-`phishing.simulation.*`-aktivitetstyper
* **enhed:** `device_type` er den afledte klasse (observerede tokens omfatter
  `desktop`, `mobile`, `tablet`, `unknown`). Rå `user_agent` findes ikke på
  `/v1`. Rå IP findes aldrig på `/v1` (gemmes aldrig; 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
```

Brug `occurred_after` til trinvise hændelsesindlæsninger. Fakta er kun-tilføj;
sent ankomne data er nye rækker.

### entities

* **Sti:** `GET /v1/reporting/entities`
* **Granularitet:** én aktuel række pr. entitet
* **filtre:** `type`, `updated_after` samt `limit` / `page_token`
* **Wire-objekt:** `reporting.entity`
* **Join-nøgle:** `id` / `entity_id` (samme værdi)

Join phishing-fakta til personer med
`reporting.simulation_results.entity_id = reporting.entities.id`.
Operationelle `GET /v1/entities` forbliver på `entity:read` og er en anden
tilladelse.

```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 datasæt har en navngivet undtagelse, hvor kun det aktuelle vises:

> Der findes ingen autoritativ tabel med versions- eller medlemskabshistorik. Dette datasæt projicerer den aktuelle række som ét åbent interval (`valid_to` er null, `is_current` er true). `valid_from` er `created_at` eller ellers `updated_at` for entitetsversioner og forespørgslens observationstidspunkt for Kohorte-medlemskaber. Lukkede historiske intervaller opdigtes ikke.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Samme filtre som
  entities. `valid_from` er `created_at` eller ellers `updated_at`. En entitet
  med begge tidsstempler null udelades.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. filtre:
  `cohort`, `entity`, `updated_after`. `updated_at` er Kohortens seneste
  opdatering, så trinvis opdatering forbliver retvisende. `valid_from` er forespørgslens
  observationstidspunkt. Join `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 stjerneskemamodel på /v1/activities

`/v1/activities` er organisationens tidsstemplede hændelsesstrøm. Den har ingen
numeriske målinger — enhver størrelse i en rapport er et rækkeantal — hvilket gør et
stjerneskema til et naturligt valg.

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

* **`Activities`** er hændelsesfaktatabellen på den sande granularitet. Én række pr. aktivitet.
  Deltagere fjernes her og genopbygges som deres egen tabel.
* **`ActivityParticipants`** findes, fordi deltagere reelt er
  en-til-mange med roller som `actor` og `target`. Hvis de blev fladet ud på
  `Activities`, ville det enten duplikere aktivitetsrækker (og dermed oppuste hvert antal)
  eller kassere yderligere deltagere.
  Relationen til `Activities` er modellens eneste tovejsrelation, så en entitets-slicer når faktatabellen.
* **`Entities`** er dimensionen, der navngiver deltagere. Deltagerrækker
  på en aktivitet indeholder `entity_id` og en rolle, men ikke et visningsnavn.
* **`Dates`** genereres i M (nul API-kald) og markeres som modellens
  datotabel. Det er det, der gør DAX-tidsintelligens
  (`DATESINPERIOD`, `DATEADD`) lovlig. Kolonner med måneds- og ugedagsnavne angiver
  `sortByColumn` på deres talpartnere, så visuals ikke sorterer måneder
  alfabetisk.

Alle målinger er eksplicitte (modellen angiver `discourageImplicitMeasures`): samlede
aktiviteter, aktive entiteter, aktiviteter pr. aktiv entitet, dage med aktivitet
og et 7-dages glidende dagligt gennemsnit.

### Bridge-tabellens form

`ActivityParticipants` gennemgår `/v1/activities` og udfolder `participants`-arrayet
til én række pr. deltager:

```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` gennemgår `/v1/activities` uafhængigt i stedet for at
referere til `Activities`-forespørgslen. I Power BI henter to forespørgsler, der er indlæst som to
tabeller, hver især data uafhængigt på opdateringstidspunktet. Når de holdes uafhængige,
kan tjenesten opdatere begge tabellers partitioner parallelt.

## Trinvis opdatering

Begge tabeller på faktagranularitet (`Activities` og `ActivityParticipants`) angiver en
Power BI-politik for trinvis opdatering: behold 5 års årlige partitioner,
genimportér de seneste 14 dage ved hver opdatering.

Mekanismen er et par DateTime-parametre, `RangeStart` og `RangeEnd`,
som Power BI-tjenesten gen-binder pr. partition på opdateringstidspunktet. Hver faktaforespørgsel
indfolder disse parametre i API'ets `start_date`- og `end_date`-filtre,
så antallet af forespørgsler pr. partition står i forhold til partitionen og
ikke til den samlede historik. Forespørgslen udvider API-vinduet med et minut på hver
side og genanvender den præcise halvåbne `[RangeStart, RangeEnd)`-grænse
lokalt, så partitionens korrekthed ikke afhænger af, om serveren
behandler sine grænser som inklusive eller eksklusive.

```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 opdatering efter en udrulning anvender politikken: en fuld historisk indlæsning,
langsom. Hver efterfølgende opdatering berører kun det trinvise vindue.

Målt på et datasæt med ca. 158.000 aktiviteter og ca. 2.300 entiteter:

| Opdatering | Tid |
| - | - |
| Første (anvendelse af politik, fuld historisk indlæsning) | \~31 min |
| Hver efterfølgende opdatering | **\~15 s** |

<Warning>
  Power BI's klassiske REST-endepunkt til opdatering (`POST .../refreshes` med kun
  `notifyOption`) ignorerer stilfærdigt opdateringspolitikker og gennemgår alt
  hver gang. For at anvende politikken via REST API skal du bruge forbedret opdatering: send
  `type`, `commitMode` og `applyRefreshPolicy` i brødteksten. Opdateringer
  udløst af Power BI-tjenestens planlægger anvender altid politikker — denne
  særhed gælder kun REST-kaldet.
</Warning>

<Note>
  Vinduet på 14 dage er bevidst generøst. Indlæsning af aktiviteter er ikke
  kun-tilføj-ved-nu: connector-efterfyldninger lander hændelser med tidsstempler i fortiden.
  Et vindue på 1-2 dage ville permanent gå glip af dem.
</Note>

## Udvidelse af opdateringsvinduet ind i API'et

`GET /v1/activities` accepterer de `start_date`- og `end_date`-filtre, som
Power BI-parametrene indfoldes i, og filtrerer `event_ts` mellem grænserne.
`Entities` er en dimension, så dette eksempel opdaterer den fuldt ud hver gang
i stedet for trinvist.

Rapporteringsdatasæt bruger deres egne vandmærker (`updated_after`,
`occurred_after`) i stedet for aktiviteternes datovindue.

## Rate limits under opdatering

`/v1` håndhæver separate budgetter pr. nøgle og pr. IP (se
[rate limits](/da/developers/v1/rate-limits)). Respektér `429` + `Retry-After`; hardkod
ikke et forespørgselsbudget i din model. To egenskaber ved Power BI-opdateringer
er vigtige:

* **En enkelt tabelgennemgang er sekventiel.** Hver sideforespørgsel kræver den
  forrige sides token, så én tabel er latenstidsbundet uanset sidestørrelse.
* **Samtidige tabelopdateringer multiplicerer.** Flere store tabeller, der opdateres
  parallelt, er det tilfælde, der rammer loftet, og derfor er 429-
  genforsøgsstien i `Fn_FetchAll` vigtig. Foretræk færre parallelle tabeller frem for
  at antage ubegrænset samtidighed.

Gentagelsesforsøget er afprøvet mod en reelt mættet begrænser. En opdatering,
der kørte, mens en parallel arbejdsbyrde bevidst holdt nøglen ved dens loft,
blev stadig gennemført og gav de samme rækkeantal som en baseline uden konkurrence —
gentagelsesforsøg returnerede komplette sider, ikke huller.

## Kontraktversioner og lagrede legitimationsoplysninger

`/v1` accepterer en `Living-Security-Version`-forespørgselsheader til at fastlåse en udgivet
kontraktversion (se [API-quickstart](/da/developers/v1/quickstart)). Power
Query kan ikke sende denne header, når datakilden bruger lagrede legitimationsoplysninger —
kun anonyme datakilder tillader brugerdefinerede headers. Hvis den tilføjes, mislykkes en opdatering
med:

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

Behold nøglen i Power BI's lagrede legitimationsoplysninger, og udelad den brugerdefinerede header.
Hvis der ikke er angivet nogen eksplicit version i forespørgslen, bruger API'et servicekontoens
fastlåste `contract_version`, eller den senest udgivne version, hvis der ikke er angivet nogen fastlåsning.
Bekræft kontoens kontraktversion med din API-administrator, før du
udgiver en opdatering. Se [API-ændringsloggen](/da/developers/v1/changelog) for
understøttede versioner.

Hvis en opdatering begynder at opføre sig uventet, skal du undersøge API'ets
`Living-Security-Version`-svarheader for at se, hvilken kontrakt der faktisk
betjente forespørgslen.

## Opdatér og kontrollér din model

Udgiv den semantiske model til dit Power BI-arbejdsområde, bind dens lagrede
legitimation til den regionale `/v1`-kilde, og kør en opdatering, før du aktiverer en
tidsplan. Gennemgå opdateringshistorikken for fejl eller ufuldstændige læsninger.

Kontrollér rækkeantal mod en uafhængig API-læsning med de samme filtre.
[Pagineringsvejledningen](/da/developers/v1/pagination) indeholder Python- og cURL-eksempler
på, hvordan et komplet resultatsæt læses. En opdatering, der kun læser
den første side, kan se vellykket ud på et lille datasæt; sammenlign antal, før
du stoler på rapporten.

## Modelvalg

* **Kampagnetotaler og resultater pr. person har forskellig granularitet.**
  `SimulationResults` kalder `GET /v1/simulations/{id}/results`
  (ét GET pr. kørsel). Til phishing pr. person skal du tilføje forespørgsler for
  `GET /v1/reporting/simulation_results` og
  `GET /v1/reporting/simulation_events` (eller operationelle
  `GET /v1/simulation_results`, hvis du allerede har `simulation:read`).
* **`vectors` på `/v1/simulations` er fladet ud til en kommasepareret streng.**
  En kolonne af listetype kan ikke bruges som en egentlig dimension i en slicer. Hvis en rapportside
  skal filtrere på vektor, skal du tilføje en bridge-tabel fra simulering til vektor.

## Næste trin

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/da/developers/developer-hub/overview">
    Opret en servicekonto og en API-nøgle til Power BI.
  </Card>

  <Card title="API-quickstart" href="/da/developers/v1/quickstart">
    Godkend `/v1`-forespørgsler med Basic-godkendelse.
  </Card>

  <Card title="Paginering" href="/da/developers/v1/pagination">
    Listekonvolutten, cursor-tokens og filterstabilitet gennem en gennemgang.
  </Card>

  <Card title="Rate limits" href="/da/developers/v1/rate-limits">
    Budgetter pr. nøgle og pr. IP, 429-kontrakten og vejledning om gentagelsesforsøg.
  </Card>
</CardGroup>


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