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

> Anslut Power BI till Living Security Public API med Power Query, bygg en stjärnschemamodell på /v1 och välj mellan operativa och rapporteringsdatauppsättningar för nätfiske.

Den här guiden är ett heltäckande exempel på hur `/v1` läses in i Power BI. Den omfattar
anslutningsmönstret i Power Query (M), en semantisk stjärnschemamodell byggd
på `/v1/activities`, inkrementell uppdatering samt när de **reporting**-
förhandsdatauppsättningarna ska användas för nätfiskeresultat per person.

Exemplen använder produktionsvärden för USA
(`https://api.us-east-1.livingsecurity.ai/v1`). För en organisation i EU använder du
`https://api.eu-west-1.livingsecurity.ai/v1` i både `Fn_FetchAll` och
`Fn_GetJson` och binder sedan datakällans autentiseringsuppgifter till den värden. Se
[Regioner](/sv/developers/v1/regions).

Den här guiden använder lagrade Basic-autentiseringsuppgifter med en API-nyckel. Du behöver ingen
OAuth-klient. Se [Autentisering](/sv/developers/v1/authentication) för
alternativen för autentiseringsuppgifter och [Developer Hub](/sv/developers/developer-hub/overview)
för åtkomst och konfiguration.

## Vad det här exemplet är till för

Använd det när du behöver:

* Läsa in listslutpunkter i `/v1` i en Power BI-datauppsättning med korrekt markörbaserad
  paginering och nytt försök vid hastighetsbegränsning.
* Bygga en semantisk modell där mått (”aktiva entiteter”, ”aktiviteter per
  aktiv entitet”) är dimensionella i stället för att räknas från en platt tabell.
* Gå från en schemalagd Power BI-uppdatering som läser hela historiken till ett
  litet dagligt fönster med hjälp av inkrementell uppdatering.
* Välja rätt nivå för nätfiskedata: kampanjtotaler (operativt) jämfört med
  resultat per person och typade händelser (förhandsversion för rapportering).

## Förutsättningar

* En API-nyckel med de behörigheter som dina frågor kräver:
  * Operativt exempel: `activity:read`, `entity:read` och
    `simulation:read`.
  * Nätfiske per person via rapporteringsdatauppsättningar: `reporting:read`.
    Se [API-nycklar](/sv/developers/developer-hub/api-keys).
* Power BI Desktop, eller en Power BI-/Fabric-arbetsyta där du kan skapa en
  semantisk modell och schemalägga uppdatering.

## Välj rätt detaljnivå

`/v1` erbjuder två relaterade gränssnitt för nätfiske. De är inte utbytbara.

| Behov | Använd | Behörighet | Detaljnivå |
| - | - | - | - |
| Totaler för kampanjtratt | `GET /v1/simulations/{id}/results` | `simulation:read` | En rad per simulering (levererad, öppnad, klickad, …) |
| Resultat per person (operativt) | `GET /v1/simulation_results` | `simulation:read` | En rad per entitet × simulering |
| Resultat per person (BI / rapportering) | `GET /v1/reporting/simulation_results` | `reporting:read` | Samma detaljnivå, BI-fältnamn (`entity_id`, `simulation_id`), `updated_after`-vattenstämpel |
| Typade interaktionsfakta | `GET /v1/reporting/simulation_events` | `reporting:read` | Ett faktum per interaktion (`simulation.link_clicked`, …), `occurred_after` |
| Aktuell dimension för Arbetsstyrka | `GET /v1/reporting/entities` | `reporting:read` | En aktuell rad per entitet. Koppla `reporting.simulation_results.entity_id = reporting.entities.id` |
| Aktuella entitetsversioner | `GET /v1/reporting/entity_versions` | `reporting:read` | Ett öppet aktuellt intervall per entitet. Ingen tabell för versionshistorik finns |
| Aktuella Kohort-medlemskap | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Ett aktuellt faktum per `(cohort_id, entity_id)`. Ingen tabell för medlemskapshistorik finns |
| Kopplingsrygg för alla aktivitetstyper | `GET /v1/activities` | `activity:read` | En händelse per aktivitet, kapslad `participants[]` |

Power BI-exempelfrågorna nedan anropar de **operativa** vägarna
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). För nätfiskerapportering per person bör du i stället använda
förhandsfamiljen för **rapportering** nedan. En nyckel som endast har beviljats `reporting:read` kan inte
anropa operativa `/v1/simulation_results` (API:et returnerar `403`).

### Svårighetsskala

Simuleringens svårighetsgrad är ett heltal mellan 1 och 5 på `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), egenskaperna för `/v1/activities`
(`difficulty_tier`) och `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` är lättast och `5` är svårast. Adaptiva och progressiva simuleringar saknar
ett enskilt värde på kampanjnivå, så `/v1/simulations.difficulty_level` är `null`
medan `difficulty_level` per mottagare i resultaten ändå kan vara angivet. För att mappa mot
äldre exporter med låg/medel/hög behandlar du `1–2` som låg, `3` som medel och `4–5` som
hög.

<Warning>
  Bygg inte nya BI-integrationer mot odokumenterade nätverksanrop från Instrumentpanel
  eller den äldre, kurerade `/api/v1` Public API-översikten. Autentisera mot
  organisationens regionala `/v1`-värd med en API-nyckel från Developer Hub. Se
  [Regioner](/sv/developers/v1/regions).
</Warning>

## Anslutningsmönster (Power Query)

Fyra regler ligger till grund för varje fråga i exemplet. Var och en upprätthålls av
Power BI-tjänsten och är inte en fråga om stilpreferenser.

<AccordionGroup>
  <Accordion title="Håll bas-URL:en för Web.Contents statisk och avsluta den vid /v1">
    Lägg allt dynamiskt i `RelativePath` och `Query`. Det första argumentet till
    `Web.Contents` måste vara en bokstavlig sträng som slutar vid organisationens
    regionala `/v1`-rot:

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

    Anropare skickar sedan enbart resursen (`"simulations"`, inte
    `"v1/simulations"`). Klistra inte in en USA-värd för en EU-organisation.

    Om det första argumentet byggs med strängsammanfogning kan Power BI-tjänsten
    inte statiskt identifiera datakällan, och den schemalagda uppdateringen misslyckas med
    ett fel om ”dynamic data source”, trots att frågan fungerar i Power BI
    Desktop.
  </Accordion>

  <Accordion title="Ange API-nyckeln som lagrade Basic-autentiseringsuppgifter, inte i frågetexten">
    När Power BI ber om autentiseringsuppgifter för datakällan `/v1` väljer du
    **Basic** och klistrar in API-nyckeln som **användarnamn**. Ange en platshållare som
    `x` i fältet **Lösenord**. Power BI-tjänsten avvisar ett tomt
    lösenord trots att API:et ignorerar det fältet. Nyckeln som lösenord med ett
    platshållaranvändarnamn fungerar också.

    Lägg inte till en `Authorization`-rubrik i M. Det fungerar på din dator, men
    bäddar sedan in nyckeln i frågefilen och krånglar med schemalagd uppdatering.
  </Accordion>

  <Accordion title="Låt anslutningstestet i dialogrutan för autentiseringsuppgifter köras">
    `GET /v1` är autentiserad: den returnerar `200` med en giltig nyckel och `401`
    utan. Peka datakällan mot `…/v1` och **hoppa inte över** testet –
    en giltig nyckel sparas, en återkallad eller felformad nyckel misslyckas omedelbart.

    Äldre exempel använde enbart värden som datakälla och uppmanade dig att hoppa över
    testet eftersom `GET /` alltid är `404`. Peka om dessa modeller mot `…/v1`.
  </Accordion>

  <Accordion title="Ange limit=500 uttryckligen i varje listfråga">
    Standardstorleken för sidor på varje listslutpunkt i `/v1` är 25. Om du lämnar standardvärdet
    ökar antalet begäranden för samma data. Frågevärden måste vara text –
    `limit = "500"` i M, inte `limit = 500`.
  </Accordion>
</AccordionGroup>

### Delad pagineringsfunktion

Varje fråga i exemplet anropar en enda `Fn_FetchAll` som går igenom
`next_page_token` tills den är `null` (se
[paginering](/sv/developers/v1/pagination)). Dess viktigaste egenskaper:

* **Statisk bas-URL för `/v1`, dynamiska `RelativePath` och `Query`.** Krävs för
  schemalagd uppdatering och för autentiseringstestet.
* **`ManualStatusHandling = {429}` med ett begränsat antal nya försök.** Funktionen läser
  `Retry-After` när Power Query exponerar den, väntar annars 30 sekunder och
  ger upp efter 5 på varandra följande `429`-svar med ett uttryckligt fel i stället för
  att loopa tills uppdateringen får timeout. `5xx` görs avsiktligt inte om –
  ett serverfel ska synas och inte i tysthet ge en ofullständig
  datauppsättning.
* **`Timeout` på 60 sekunder.** Sidor svarar vanligtvis på långt under en sekund, så
  en minuts tystnad innebär att begäran är död.
* **`Binary.Buffer` före `Json.Document`.** Utan buffring kan mashup-
  motorn begära svarsströmmen på nytt om den läser den mer än en gång.
* **`baseQuery` skickas oförändrad i varje iteration.** Sidtokens är
  ogenomskinliga, signerade markörer som är bundna till organisationen, autentiseringsuppgifterna, kontraktsversionen
  och fingeravtrycket för filter och sortering. Om ett filter ändras
  under genomgången avvisas det som en ogiltig token i stället för att starta om från sida
  ett.

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

### Typade frågor per slutpunkt

Varje konsument av `Fn_FetchAll` följer samma mönster: anropa slutpunkten,
projicera en fast kolumnlista, ta bort diskriminatorn `object` och typa
tidsstämplar som `datetimezone` så att datummått inte i tysthet förskjuts av
uppdateringskapacitetens tidszon.

<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 uttrycklig kolumnlista i `Table.FromRecords` håller schemat stabilt när
  en slutpunkt returnerar noll rader. Ett ensamt `Table.FromRecords({})` ger en
  tabell utan kolumner, och varje efterföljande steg misslyckas på de saknade fälten.
</Tip>

## Excel

Samma Power Query (M) körs i Excels Hämta och transformera – det är samma
motor som i Power BI Desktop. Använd detta när du vill ha en kalkylbladstabell
i stället för en semantisk modell.

I Excel för Windows (Microsoft 365, Excel 2024, 2021, 2019 eller 2016):

1. **Data → Hämta data → Från andra källor → Tom fråga.**
2. I Power Query-redigeraren väljer du **Hem → Avancerad redigerare**. Klistra in
   `Fn_FetchAll` först och byt sedan namn på den frågan till `Fn_FetchAll` (Query
   Settings → Name, eller högerklicka i fönstret Queries → **Rename**).
3. Upprepa för `Fn_GetJson` och sedan för varje rapportfråga.
4. **Hem → Stäng och läs in → Stäng och läs in till…** öppnar dialogrutan **Importera data**.
   Välj **Skapa endast anslutning** för `Fn_FetchAll` och
   `Fn_GetJson` (de är funktioner, inte tabeller). Välj **Tabell** för
   varje rapportfråga som du vill ha i ett kalkylblad.
5. Vid första uppdateringen väljer du **Basic**: API-nyckeln som **Användarnamn**, en
   platshållare som `x` som **Lösenord**. Excel lagrar en uppsättning autentiseringsuppgifter
   per datakälls-URL; senare frågor som använder samma `/v1`-värd återanvänder
   dem.

<Note>
  **Stäng och läs in till…** kan vara nedtonat första gången du skapar en fråga i
  en arbetsbok. Använd **Stäng och läs in**, välj sedan i Excel **Data →
  Frågor och anslutningar**, högerklicka på frågan och välj **Läs in till**.
  Den vägen är dokumenterad av Microsoft för det här fallet.
</Note>

Du kan inte skicka `Living-Security-Version` från Power Query på en
autentiserad källa – samma begränsning som i Power BI. Fäst tjänstkontot eller skicka
`api_version` i frågeposten. Se
[Kontraktsversioner och lagrade autentiseringsuppgifter](#kontraktsversioner-och-lagrade-autentiseringsuppgifter).

Färdiga rapportfrågor (kolumnmappningar och avvikelser från en
tidigare leverantörs export):

<CardGroup cols={3}>
  <Card title="Efterlevnad per person" href="/sv/developers/v1/reports/per-person-compliance">
    Utbildningsregistreringar på nivån person × tilldelning.
  </Card>

  <Card title="Logg över e-postnätfiske" href="/sv/developers/v1/reports/email-phishing-log">
    Nätfiskeinteraktioner på händelsenivå, senaste 12 månaderna.
  </Card>

  <Card title="Mest mottagliga användare" href="/sv/developers/v1/reports/most-susceptible-users">
    Sammanställning per person av klick, inloggningsuppgifter och rapportering under 12 månader.
  </Card>
</CardGroup>

## Förhandsdatauppsättningar för rapportering (nätfiske per person)

Förhandsfamiljen för rapportering tillhandahåller datauppsättningar för nätfiske per person.
Den kräver `reporting:read` och åtkomst till rapportering för din organisation.
Använd datauppsättningskatalogen nedan för att granska scheman och datapolicyer.

Upptäck den aktuella 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
```

Varje datauppsättning deklarerar detaljnivå, primärnyckel, kopplingar, vattenstämpel, korrigerings- och
raderingspolicy.

### simulation\_results

* **Sökväg:** `GET /v1/reporting/simulation_results`
* **Detaljnivå:** entitet × simulering
* **filter:** `simulation`, `entity`, `updated_after`, samt `limit` /
  `page_token`
* **Wire-objekt:** `reporting.simulation_result`
* **Kopplingsnycklar:** `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
```

Använd `updated_after` som inkluderande nedre vattenstämpel när du uppdaterar senaste
partitioner. Föränderliga resultatdimensioner skrivs om på plats under samma `id`.

### simulation\_events

* **Sökväg:** `GET /v1/reporting/simulation_events`
* **Detaljnivå:** ett typat faktum per observerad interaktion
* **filter:** `simulation`, `entity`, `event_type`, `occurred_after`, samt
  `limit` / `page_token`
* **Wire-objekt:** `reporting.simulation_event`
* **Händelsetyper:** slutna publika tokens som `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` – inte råa
  aktivitetstyper för ClickHouse `phishing.simulation.*`
* **Enhet:** `device_type` är den härledda klassen (observerade tokens omfattar
  `desktop`, `mobile`, `tablet`, `unknown`). Rå `user_agent` finns inte i
  `/v1`. Rå IP finns aldrig i `/v1` (lagras aldrig; endast saltad `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
```

Använd `occurred_after` för inkrementella händelseinläsningar. Fakta är endast tillägg;
sent anlända data blir nya rader.

### entities

* **Sökväg:** `GET /v1/reporting/entities`
* **Detaljnivå:** en aktuell rad per entitet
* **filter:** `type`, `updated_after`, samt `limit` / `page_token`
* **Wire-objekt:** `reporting.entity`
* **Kopplingsnyckel:** `id` / `entity_id` (samma värde)

Koppla nätfiskefakta till personer med
`reporting.simulation_results.entity_id = reporting.entities.id`.
Operativa `GET /v1/entities` finns kvar på `entity:read` och är en annan
behörighet.

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

Dessa två datauppsättningar har ett namngivet undantag som endast gäller aktuella data:

> Ingen auktoritativ tabell för versions- eller medlemskapshistorik finns. Den här datauppsättningen projicerar den aktuella raden som ett öppet intervall (`valid_to` är null, `is_current` är true). `valid_from` är `created_at` eller annars `updated_at` för entitetsversioner, och tidpunkten då begäran observerades för Kohort-medlemskap. Stängda historiska intervall fabriceras inte.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Samma filter som
  entities. `valid_from` är `created_at` eller annars `updated_at`. En entitet
  där båda tidsstämplarna är null utelämnas.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. filter:
  `cohort`, `entity`, `updated_after`. `updated_at` är den senaste uppdateringen av Kohort
  så att inkrementell uppdatering förblir korrekt. `valid_from` är tidpunkten då
  begäran observerades. Koppla `entity_id` till `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 stjärnschemamodell på /v1/activities

`/v1/activities` är organisationens tidsstämplade händelseström. Den har inga
numeriska mått – varje kvantitet i en rapport är ett radantal – vilket gör ett
stjärnschema till ett naturligt val.

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

* **`Activities`** är händelsefaktatabellen på verklig detaljnivå. En rad per aktivitet.
  Deltagare tas bort här och byggs upp igen som en egen tabell.
* **`ActivityParticipants`** finns eftersom deltagare verkligen är
  en-till-många, med roller som `actor` och `target`. Att platta ut dem på
  `Activities` skulle antingen duplicera aktivitetsrader (och blåsa upp varje räkning)
  eller förkasta ytterligare deltagare.
  Relationen till `Activities` är modellens enda dubbelriktade
  relation, så att en entitetsutsnitt når faktatabellen.
* **`Entities`** är dimensionen som namnger deltagare. Deltagarrader
  i en aktivitet innehåller `entity_id` och en roll men inget visningsnamn.
* **`Dates`** genereras i M (noll API-anrop) och markeras som modellens
  datumtabell. Det är det som gör DAX-tidsintelligens
  (`DATESINPERIOD`, `DATEADD`) tillåten. Kolumner för månads- och veckodagsnamn deklarerar
  `sortByColumn` på sina nummertvillingar så att visuella objekt inte sorterar månader
  alfabetiskt.

Alla mått är explicita (modellen anger `discourageImplicitMeasures`): totalt antal
aktiviteter, aktiva entiteter, aktiviteter per aktiv entitet, dagar med aktivitet
samt ett dagligt medelvärde över 7 dagar.

### Bryggtabellens utformning

`ActivityParticipants` går igenom `/v1/activities` och expanderar arrayen `participants`
till en rad per deltagare:

```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 igenom `/v1/activities` oberoende i stället för att
referera till frågan `Activities`. I Power BI hämtar två frågor som läses in som två
tabeller data oberoende av varandra vid uppdateringen. Genom att hålla dem oberoende
kan tjänsten uppdatera båda tabellernas partitioner parallellt.

## Inkrementell uppdatering

Båda tabellerna på faktanivå (`Activities` och `ActivityParticipants`) deklarerar en
inkrementell uppdateringspolicy i Power BI: behåll 5 år av årliga partitioner och
importera om de senaste 14 dagarna vid varje uppdatering.

Mekanismen är ett par DateTime-parametrar, `RangeStart` och `RangeEnd`,
som Power BI-tjänsten binder om per partition vid uppdateringen. Varje faktafråga
viker in dessa parametrar i API:ets filter `start_date` och `end_date`, så att
antalet begäranden per partition är proportionellt mot partitionen,
inte mot hela historiken. Frågan vidgar API-fönstret med en minut åt vardera
håll och tillämpar det exakta halvöppna `[RangeStart, RangeEnd)`-gränsvärdet
lokalt igen, så att partitionens korrekthet inte beror på om servern
behandlar sina gränser som inkluderande eller exkluderande.

```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örsta uppdateringen efter en driftsättning tillämpar policyn: en fullständig historisk inläsning,
vilket är långsamt. Varje uppdatering därefter berör bara det inkrementella fönstret.

Uppmätt på en datauppsättning med ca 158 000 aktiviteter och ca 2 300 entiteter:

| Uppdatering | Tid |
| - | - |
| Första (tillämpning av policy, fullständig historisk inläsning) | ca 31 min |
| Varje uppdatering därefter | **ca 15 s** |

<Warning>
  Den klassiska REST-slutpunkten för uppdatering i Power BI (`POST .../refreshes` med enbart
  `notifyOption`) ignorerar i tysthet uppdateringspolicyer och går igenom allt
  varje gång. För att tillämpa policyn via REST-API:et använder du utökad uppdatering: skicka
  `type`, `commitMode` och `applyRefreshPolicy` i brödtexten. Uppdateringar
  som utlöses av schemaläggaren i Power BI-tjänsten tillämpar alltid policyer – den här
  egenheten gäller enbart REST-anropet.
</Warning>

<Note>
  Fönstret på 14 dagar är avsiktligt generöst. Inläsningen av aktiviteter är inte enbart
  tillägg vid aktuell tidpunkt: ifyllnad från kopplingar levererar händelser med tidsstämplar i det förflutna.
  Ett fönster på 1–2 dagar skulle missa dem permanent.
</Note>

## Att vidga uppdateringsfönstret in i API:et

`GET /v1/activities` accepterar filtren `start_date` och `end_date` som
Power BI-parametrarna viks in i och filtrerar `event_ts` mellan gränserna.
`Entities` är en dimension, så i det här exemplet uppdateras den helt varje gång
i stället för inkrementellt.

Rapporteringsdatauppsättningar använder sina egna vattenstämplar (`updated_after`,
`occurred_after`) i stället för datumfönstret för aktiviteter.

## Hastighetsbegränsningar under uppdatering

`/v1` tillämpar separata budgetar per nyckel och per IP (se
[hastighetsbegränsningar](/sv/developers/v1/rate-limits)). Följ `429` + `Retry-After`; hårdkoda
inte en begäranbudget i din modell. Två egenskaper hos Power BI-
uppdateringar är viktiga:

* **En enskild tabellgenomgång är sekventiell.** Varje sidbegäran behöver
  föregående sidas token, så en tabell är latensbunden oavsett
  sidstorlek.
* **Samtidiga tabelluppdateringar multipliceras.** Flera stora tabeller som uppdateras
  parallellt är det fall som når taket, vilket är anledningen till att återförsöket vid 429
  i `Fn_FetchAll` är viktigt. Föredra färre parallella tabeller framför att
  anta obegränsad samtidighet.

Återförsöket har testats mot en verklig mättad begränsare. En uppdatering
som kördes medan en parallell arbetsbelastning avsiktligt höll nyckeln vid sitt tak
slutfördes ändå och gav samma radantal som en ostörd referenskörning –
återförsöken returnerade kompletta sidor, inte luckor.

## Kontraktsversioner och lagrade autentiseringsuppgifter

`/v1` accepterar en begäranrubrik `Living-Security-Version` för att fästa en publicerad
kontraktsversion (se [API-snabbstart](/sv/developers/v1/quickstart)). Power
Query kan inte skicka den rubriken när datakällan använder lagrade autentiseringsuppgifter –
endast anonyma datakällor tillåter anpassade rubriker. Om den läggs till misslyckas en uppdatering
med:

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

Behåll nyckeln i Power BI:s lagrade autentiseringsuppgifter och utelämna den anpassade rubriken.
Utan uttrycklig version i begäran använder API:et tjänstkontots
fästa `contract_version`, eller den senast publicerade versionen om ingen fästning är angiven.
Bekräfta kontots kontraktsversion med din API-administratör innan du
publicerar en uppdatering. Se [API-ändringsloggen](/sv/developers/v1/changelog) för
versioner som stöds.

Om en uppdatering börjar bete sig oväntat kan du granska API:ets
svarsrubrik `Living-Security-Version` för att se vilket kontrakt som faktiskt
hanterade begäran.

## Uppdatera och verifiera din modell

Publicera den semantiska modellen i din Power BI-arbetsyta, binda dess lagrade
autentiseringsuppgifter till den regionala `/v1`-källan och kör en uppdatering innan du aktiverar ett
schema. Granska uppdateringshistoriken efter fel eller ofullständiga läsningar.

Kontrollera radantal mot en oberoende API-läsning med samma filter.
[Pagineringsguiden](/sv/developers/v1/pagination) innehåller Python- och cURL-exempel
för att läsa en fullständig resultatmängd. En uppdatering som bara läser den
första sidan kan se lyckad ut på en liten datauppsättning; jämför antal innan du
förlitar dig på rapporten.

## Modellval

* **Kampanjtotaler och resultat per person har olika detaljnivåer.**
  `SimulationResults` anropar `GET /v1/simulations/{id}/results`
  (en GET per körning). För nätfiske per person lägger du till frågor för
  `GET /v1/reporting/simulation_results` och
  `GET /v1/reporting/simulation_events` (eller operativa
  `GET /v1/simulation_results` om du redan har `simulation:read`).
* **`vectors` på `/v1/simulations` plattas ut till en kommaseparerad sträng.**
  En kolumn av listtyp kan inte användas som riktig dimension i utsnitt. Om en rapportsida
  behöver filtrera på vektor lägger du till en bryggtabell mellan simulering och vektor.

## Nästa steg

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/sv/developers/developer-hub/overview">
    Skapa ett tjänstkonto och en API-nyckel för Power BI.
  </Card>

  <Card title="API-snabbstart" href="/sv/developers/v1/quickstart">
    Autentisera `/v1`-begäranden med Basic-autentisering.
  </Card>

  <Card title="Paginering" href="/sv/developers/v1/pagination">
    Listkuvertet, markörtokens och filterstabilitet under en genomgång.
  </Card>

  <Card title="Hastighetsbegränsningar" href="/sv/developers/v1/rate-limits">
    Budgetar per nyckel och per IP, 429-kontraktet och vägledning om nya försök.
  </Card>
</CardGroup>


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