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

> Yhdistä Power BI Living Security Public API -rajapintaan Power Queryn avulla, rakenna tähtiskeemamalli /v1-rajapinnan päälle ja valitse operatiiviset tai raportointitietojoukot tietojenkalastelua varten.

Tämä opas on kattava esimerkki siitä, miten `/v1` luetaan Power BI:hin. Se käsittelee
Power Query (M) -yhteysmallin, rakenteen `/v1/activities` päälle rakennetun tähtiskeemapohjaisen semanttisen mallin, inkrementaalisen päivityksen sekä sen, milloin **reporting**-esikatselutietojoukkoja kannattaa käyttää henkilökohtaisiin tietojenkalastelun tuloksiin.

Esimerkeissä käytetään Yhdysvaltojen tuotantopalvelinta
(`https://api.us-east-1.livingsecurity.ai/v1`). EU-organisaatiossa käytä
`https://api.eu-west-1.livingsecurity.ai/v1` sekä kohdassa `Fn_FetchAll` että
kohdassa `Fn_GetJson` ja sido tietolähteen tunnistetiedot kyseiseen palvelimeen. Katso
[Alueet](/fi/developers/v1/regions).

Tässä oppaassa käytetään tallennettuja Basic-tunnistetietoja ja API-avainta. OAuth-asiakasta ei tarvita. Tunnistetietovaihtoehdot on kuvattu kohdassa [Todennus](/fi/developers/v1/authentication), ja käyttöoikeudet sekä käyttöönotto kohdassa [Developer Hub](/fi/developers/developer-hub/overview).

## Mihin tätä esimerkkiä käytetään

Käytä sitä, kun haluat:

* Ladata `/v1`-luettelopäätepisteet Power BI -tietojoukkoon oikealla kursoripohjaisella sivutuksella ja nopeusrajoituksen uudelleenyrityksellä.
* Rakentaa semanttisen mallin, jossa mittarit (”aktiiviset entiteetit”, ”aktiviteetit aktiivista entiteettiä kohti”) ovat dimensionaalisia eivätkä perustu litteän taulukon rivien laskentaan.
* Lyhentää ajastettua Power BI -päivitystä koko historian läpikäynnistä pieneen päivittäiseen ikkunaan inkrementaalisen päivityksen avulla.
* Valita oikean tietojenkalastelutietojen tarkkuustason: kampanjakohtaiset summat (operatiivinen) tai henkilökohtaiset tulokset ja tyypitetyt tapahtumat (reporting-esikatselu).

## Edellytykset

* API-avain, jolla on kyselyidesi edellyttämät käyttöoikeudet:
  * Operatiivinen esimerkki: `activity:read`, `entity:read` ja
    `simulation:read`.
  * Henkilökohtaiset tietojenkalastelutiedot reporting-tietojoukoista: `reporting:read`.
    Katso [API-avaimet](/fi/developers/developer-hub/api-keys).
* Power BI Desktop tai Power BI- tai Fabric-työtila, jossa voit luoda semanttisen mallin ja ajastaa päivityksen.

## Valitse oikea tarkkuustaso

`/v1` tarjoaa tietojenkalasteluun kaksi toisiinsa liittyvää pintaa. Ne eivät ole keskenään vaihdettavissa.

| Tarve | Käytä | Käyttöoikeus | Tarkkuustaso |
| - | - | - | - |
| Kampanjan suppilon summat | `GET /v1/simulations/{id}/results` | `simulation:read` | Yksi rivi simulaatiota kohti (toimitettu, avattu, klikattu, …) |
| Henkilökohtaiset tulokset (operatiivinen) | `GET /v1/simulation_results` | `simulation:read` | Yksi rivi entiteetti × simulaatio |
| Henkilökohtaiset tulokset (BI / raportointi) | `GET /v1/reporting/simulation_results` | `reporting:read` | Sama tarkkuustaso, BI-kenttien nimet (`entity_id`, `simulation_id`), `updated_after`-vesileima |
| Tyypitetyt vuorovaikutustiedot | `GET /v1/reporting/simulation_events` | `reporting:read` | Yksi tieto vuorovaikutusta kohti (`simulation.link_clicked`, …), `occurred_after` |
| Nykyinen työvoimadimensio | `GET /v1/reporting/entities` | `reporting:read` | Yksi nykyinen rivi entiteettiä kohti. Liitä `reporting.simulation_results.entity_id = reporting.entities.id` |
| Entiteettien nykyiset versiot | `GET /v1/reporting/entity_versions` | `reporting:read` | Yksi avoin nykyinen aikaväli entiteettiä kohti. Versiohistoriataulukkoa ei ole |
| Nykyiset kohorttijäsenyydet | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Yksi nykyinen tieto kohdetta `(cohort_id, entity_id)` kohti. Jäsenyyshistoriataulukkoa ei ole |
| Liitosrunko kaikille aktiviteettityypeille | `GET /v1/activities` | `activity:read` | Yksi tapahtuma aktiviteettia kohti, sisäkkäinen `participants[]` |

Alla olevat Power BI -esimerkkikyselyt kutsuvat **operatiivisia** reittejä
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Henkilökohtaiseen tietojenkalastelun raportointiin kannattaa käyttää alla kuvattua **reporting**-esikatseluperhettä. Avain, jolle on myönnetty vain `reporting:read`, ei voi
kutsua operatiivista `/v1/simulation_results`-päätepistettä (API palauttaa `403`).

### Vaikeusasteikko

Simulaation vaikeustaso on kokonaisluku 1–5 ominaisuuksissa `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), `/v1/activities`
(`difficulty_tier`) ja `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` on helpoin ja `5` vaikein. Mukautuvilla ja etenevillä simulaatioilla ei ole yhtä kampanjatason arvoa, joten `/v1/simulations.difficulty_level` on `null`,
vaikka vastaanottajakohtainen `difficulty_level` tuloksissa voi silti olla asetettu. Vanhoihin matala/keskitaso/korkea-vienteihin voit yhdistää arvot seuraavasti: `1–2` on matala, `3` keskitaso ja `4–5` korkea.

<Warning>
  Älä rakenna uusia BI-integraatioita Kojelaudan dokumentoimattomien verkkokutsujen
  tai vanhan kuratoidun `/api/v1` Public API -yleiskatsauksen varaan. Todenna organisaation alueelliseen `/v1`-palvelimeen Developer Hub -API-avaimella. Katso
  [Alueet](/fi/developers/v1/regions).
</Warning>

## Yhteysmalli (Power Query)

Jokaisen esimerkin kyselyn taustalla on neljä sääntöä. Jokaisen niistä valvoo
Power BI Service, eivätkä ne ole tyylipreferenssejä.

<AccordionGroup>
  <Accordion title="Pidä Web.Contents-perus-URL staattisena ja päätä se kohtaan /v1">
    Sijoita kaikki dynaaminen kohtiin `RelativePath` ja `Query`. Kohteen
    `Web.Contents` ensimmäisen argumentin on oltava kirjaimellinen merkkijono, joka päättyy organisaation
    alueellisen `/v1`-juureen:

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

    Kutsujat välittävät sitten pelkän resurssin (`"simulations"`, ei
    `"v1/simulations"`). Älä liitä yhdysvaltalaista palvelinta EU-organisaatiolle.

    Jos ensimmäinen argumentti muodostetaan merkkijonojen yhdistämisellä, Power BI Service
    ei pysty tunnistamaan tietolähdettä staattisesti, ja ajastettu päivitys epäonnistuu
    ”dynamic data source” -virheeseen, vaikka kysely toimii Power BI Desktopissa.
  </Accordion>

  <Accordion title="Anna API-avain tallennettuna Basic-tunnistetietona, älä kyselyn tekstissä">
    Kun Power BI pyytää tunnistetietoja tietolähteelle `/v1`, valitse
    **Basic** ja liitä API-avain **username**-kenttään. Kirjoita **password**-kenttään paikkamerkki, esimerkiksi
    `x`. Power BI Service hylkää tyhjän
    salasanan, vaikka API ei käytä kyseistä kenttää. Myös avain salasanana ja paikkamerkki käyttäjätunnuksena toimii.

    Älä lisää `Authorization`-otsaketta M-koodissa. Se toimii työasemallasi, mutta
    tallentaa avaimen kyselytiedostoon ja häiritsee ajastettua päivitystä.
  </Accordion>

  <Accordion title="Anna tunnistetietoikkunan yhteystestin suorittua">
    `GET /v1` vaatii todennuksen: se palauttaa `200` kelvollisella avaimella ja `401`
    ilman avainta. Osoita tietolähde kohteeseen `…/v1` äläkä **ohita** testiä —
    kelvollinen avain tallentuu, mutta peruutettu tai virheellinen avain epäonnistuu heti.

    Vanhemmissa esimerkeissä tietolähteenä käytettiin pelkkää palvelinta ja neuvottiin ohittamaan
    testi, koska `GET /` on aina `404`. Osoita nämä mallit uudelleen kohteeseen `…/v1`.
  </Accordion>

  <Accordion title="Aseta limit=500 erikseen jokaiseen luettelokyselyyn">
    Oletussivukoko jokaisessa `/v1`-luettelopäätepisteessä on 25. Oletuksen jättäminen
    kasvattaa pyyntöjen määrää samalle datalle. Kyselyarvojen on oltava tekstiä —
    `limit = "500"` M-koodissa, ei `limit = 500`.
  </Accordion>
</AccordionGroup>

### Jaettu sivutusfunktio

Jokainen esimerkin kysely kutsuu yhtä `Fn_FetchAll`-funktiota, joka käy läpi
`next_page_token`, kunnes se on `null` (katso
[sivutus](/fi/developers/v1/pagination)). Sen keskeiset ominaisuudet:

* **Staattinen `/v1`-perus-URL, dynaaminen `RelativePath` ja `Query`.** Vaaditaan
  ajastettua päivitystä ja tunnistetietojen testausta varten.
* **`ManualStatusHandling = {429}` rajatulla uudelleenyrityksellä.** Funktio lukee
  `Retry-After`, kun Power Query tarjoaa sen, muuten odottaa 30 sekuntia ja
  luovuttaa 5 peräkkäisen `429`-vastauksen jälkeen antaen selkeän virheen sen sijaan, että silmukoisi, kunnes päivitys aikakatkaistaan. `5xx`-virhettä ei
  yritetä uudelleen tarkoituksella — palvelinvirheen on tultava näkyviin eikä sen pidä hiljaa tuottaa osittaista
  tietojoukkoa.
* **60 sekunnin `Timeout`.** Sivut vastaavat tyypillisesti selvästi alle sekunnissa, joten
  minuutin hiljaisuus tarkoittaa, että pyyntö on kuollut.
* **`Binary.Buffer` ennen `Json.Document`.** Ilman puskurointia mashup-
  moottori voi pyytää vastausvirran uudelleen, jos se lukee sen useammin kuin kerran.
* **`baseQuery` välitetään muuttumattomana jokaisella iteraatiolla.** Sivutunnisteet ovat
  läpinäkymättömiä, allekirjoitettuja kursoreita, jotka on sidottu organisaatioon, tunnistetietoihin, sopimusversioon sekä suodattimien ja lajittelun sormenjälkeen. Suodattimen muuttaminen
  kesken läpikäynnin hylätään virheellisenä tunnisteena sen sijaan, että läpikäynti aloitettaisiin uudelleen ensimmäiseltä
  sivulta.

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

### Tyypitetyt kyselyt päätepistettä kohti

Jokainen kohteen `Fn_FetchAll` käyttäjä noudattaa samaa mallia: kutsu päätepistettä,
rajaa kiinteä sarakeluettelo, poista `object`-erottelija ja tyypitä
aikaleimat muotoon `datetimezone`, jotta päivämäärämittarit eivät siirry huomaamatta
päivityskapasiteetin aikavyöhykkeen mukaan.

<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>
  Eksplisiittinen sarakeluettelo kohteessa `Table.FromRecords` pitää skeeman vakaana, kun
  päätepiste palauttaa nolla riviä. Pelkkä `Table.FromRecords({})` tuottaa
  taulukon ilman sarakkeita, ja kaikki myöhemmät vaiheet epäonnistuvat puuttuviin kenttiin.
</Tip>

## Excel

Sama Power Query (M) toimii Excelin Get & Transform -toiminnossa — se on sama
moottori kuin Power BI Desktopissa. Käytä tätä, kun haluat työtaulukkoon taulukon
semanttisen mallin sijaan.

Excel for Windowsissa (Microsoft 365, Excel 2024, 2021, 2019 tai 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. Valitse Power Query Editorissa **Home → Advanced Editor**. Liitä ensin
   `Fn_FetchAll` ja nimeä kysely sitten uudelleen nimellä `Fn_FetchAll` (Query
   Settings → Name tai napsauta hiiren kakkospainikkeella Queries-ruudussa → **Rename**).
3. Toista kohdalle `Fn_GetJson` ja sen jälkeen jokaiselle raporttikyselylle.
4. **Home → Close & Load → Close & Load To…** avaa **Import Data**
   -valintaikkunan. Valitse **Only Create Connection** kyselyille `Fn_FetchAll` ja
   `Fn_GetJson` (ne ovat funktioita, eivät taulukoita). Valitse **Table**
   jokaiselle raporttikyselylle, jonka haluat työtaulukkoon.
5. Valitse ensimmäisessä päivityksessä **Basic**: API-avain kohtaan **User name** ja
   paikkamerkki, esimerkiksi `x`, kohtaan **Password**. Excel tallentaa yhden tunnistetiedon
   tietolähteen URL-osoitetta kohti; myöhemmät kyselyt, jotka käyttävät samaa `/v1`-palvelinta, käyttävät sitä uudelleen.

<Note>
  **Close & Load To…** voi olla harmaana, kun luot työkirjaan ensimmäisen kyselyn.
  Käytä **Close & Load** -toimintoa, valitse sitten Excelissä **Data →
  Queries & Connections**, napsauta kyselyä hiiren kakkospainikkeella ja valitse **Load To**.
  Microsoft dokumentoi tämän polun kyseistä tilannetta varten.
</Note>

Et voi lähettää otsaketta `Living-Security-Version` Power Queryssa todennetusta
lähteestä — sama rajoitus koskee Power BI:tä. Kiinnitä palvelutili tiettyyn versioon tai välitä `api_version` kyselyn tietueessa. Katso
[Sopimusversiot ja tallennetut tunnistetiedot](#sopimusversiot-ja-tallennetut-tunnistetiedot).

Valmiit raporttikyselyt (sarakekuvaukset ja poikkeamat aiemman toimittajan viennistä):

<CardGroup cols={3}>
  <Card title="Henkilökohtainen vaatimustenmukaisuus" href="/fi/developers/v1/reports/per-person-compliance">
    Koulutuksen ilmoittautumiset henkilö × tehtävä -tarkkuudella.
  </Card>

  <Card title="Sähköpostitietojenkalastelun loki" href="/fi/developers/v1/reports/email-phishing-log">
    Tapahtumatason tietojenkalasteluvuorovaikutukset, viimeiset 12 kuukautta.
  </Card>

  <Card title="Alttiimmat käyttäjät" href="/fi/developers/v1/reports/most-susceptible-users">
    Henkilökohtainen 12 kuukauden klikkaus-, tunnistetieto- ja ilmoitusyhteenveto.
  </Card>
</CardGroup>

## Reporting-esikatselutietojoukot (henkilökohtainen tietojenkalastelu)

Reporting-esikatseluperhe tarjoaa tietojoukkoja henkilökohtaiseen tietojenkalasteluun.
Se edellyttää `reporting:read` ja organisaatiosi raportointikäyttöoikeutta.
Tarkastele sen skeemoja ja tietokäytäntöjä alla olevan tietojoukkoluettelon avulla.

Hae ajantasainen luettelo:

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

Jokainen tietojoukko määrittää tarkkuustason, pääavaimen, liitokset, vesileiman, korjauskäytännön ja
poistokäytännön.

### simulation\_results

* **Polku:** `GET /v1/reporting/simulation_results`
* **Tarkkuustaso:** entiteetti × simulaatio
* **Suodattimet:** `simulation`, `entity`, `updated_after` sekä `limit` /
  `page_token`
* **Wire-objekti:** `reporting.simulation_result`
* **Liitosavaimet:** `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
```

Käytä `updated_after` sisältävänä alarajavesileimana, kun päivität viimeaikaisia
osioita. Muuttuvat tulosdimensiot päivittyvät paikallaan saman `id` alla.

### simulation\_events

* **Polku:** `GET /v1/reporting/simulation_events`
* **Tarkkuustaso:** yksi tyypitetty tieto havaittua vuorovaikutusta kohti
* **Suodattimet:** `simulation`, `entity`, `event_type`, `occurred_after` sekä
  `limit` / `page_token`
* **Wire-objekti:** `reporting.simulation_event`
* **Tapahtumatyypit:** suljetut julkiset tunnisteet, kuten `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — eivät raakoja
  ClickHouse-`phishing.simulation.*`-aktiviteettityyppejä
* **Laite:** `device_type` on johdettu luokka (havaittuja tunnisteita ovat
  `desktop`, `mobile`, `tablet`, `unknown`). Raaka `user_agent` ei ole kohteessa
  `/v1`. Raaka IP ei koskaan ole kohteessa `/v1` (sitä ei koskaan tallenneta; vain suolattu `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
```

Käytä `occurred_after` inkrementaalisiin tapahtumalatauksiin. Tiedot ovat vain lisättäviä;
myöhästyneet tiedot ovat uusia rivejä.

### entities

* **Polku:** `GET /v1/reporting/entities`
* **Tarkkuustaso:** yksi nykyinen rivi entiteettiä kohti
* **Suodattimet:** `type`, `updated_after` sekä `limit` / `page_token`
* **Wire-objekti:** `reporting.entity`
* **Liitosavain:** `id` / `entity_id` (sama arvo)

Liitä tietojenkalastelutiedot henkilöihin käyttämällä
`reporting.simulation_results.entity_id = reporting.entities.id`.
Operatiivinen `GET /v1/entities` pysyy kohteessa `entity:read` ja vaatii eri
käyttöoikeuden.

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

Näillä kahdella tietojoukolla on nimetty poikkeus, jossa ne sisältävät vain nykyisen tilan:

> Luotettavaa versio- tai jäsenyyshistoriataulukkoa ei ole. Tämä tietojoukko projisoi nykyisen rivin yhtenä avoimena aikavälinä (`valid_to` on null, `is_current` on true). `valid_from` on `created_at` tai muuten `updated_at` entiteettiversioille ja pyynnön havaintoaika kohorttijäsenyyksille. Suljettuja historiallisia aikavälejä ei muodosteta keinotekoisesti.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Samat suodattimet kuin
  entiteeteillä. `valid_from` on `created_at` tai muuten `updated_at`. Entiteetti,
  jonka molemmat aikaleimat ovat null, jätetään pois.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Suodattimet:
  `cohort`, `entity`, `updated_after`. `updated_at` on kohortin viimeisin päivitys,
  jotta inkrementaalinen päivitys pysyy luotettavana. `valid_from` on pyynnön
  havaintoaika. Liitä `entity_id` kohteeseen `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
```

## Tähtiskeemapohjainen semanttinen malli kohteessa /v1/activities

`/v1/activities` on organisaation aikaleimattu tapahtumavirta. Siinä ei ole
numeerisia mittareita — jokainen raportin suure on rivimäärä — mikä tekee
tähtiskeemasta luontevan ratkaisun.

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

* **`Activities`** on tapahtumatieto todellisella tarkkuustasolla. Yksi rivi aktiviteettia kohti.
  Osallistujat jätetään tästä pois ja rakennetaan uudelleen omaksi taulukokseen.
* **`ActivityParticipants`** on olemassa, koska osallistujat ovat todella
  yksi-moneen-suhteessa ja heillä on rooleja, kuten `actor` ja `target`. Litistäminen kohteeseen
  `Activities` joko monistaisi aktiviteettirivejä (paisuttaen jokaista määrää)
  tai hävittäisi muut osallistujat.
  Suhde kohteeseen `Activities` on mallin ainoa kaksisuuntainen
  suhde, joten entiteettiviipaloija ulottuu faktataulukkoon.
* **`Entities`** on dimensio, joka nimeää osallistujat. Aktiviteetin osallistujarivit
  sisältävät `entity_id` ja roolin, mutta eivät näyttönimeä.
* **`Dates`** luodaan M-koodilla (nolla API-kutsua) ja merkitään mallin
  päivämäärätaulukoksi. Tämä tekee DAX-aikatiedustelutiedon
  (`DATESINPERIOD`, `DATEADD`) käytöstä sallittua. Kuukauden ja viikonpäivän nimisarakkeet määrittävät
  `sortByColumn` numeerisille vastinsarakkeilleen, jotta visualisoinnit eivät lajittele kuukausia
  aakkosjärjestykseen.

Kaikki mittarit ovat eksplisiittisiä (malli asettaa `discourageImplicitMeasures`): aktiviteetit yhteensä, aktiiviset entiteetit, aktiviteetit aktiivista entiteettiä kohti, aktiviteettipäivät
ja 7 päivän liukuva päiväkeskiarvo.

### Siltataulukon rakenne

`ActivityParticipants` käy läpi `/v1/activities` ja purkaa `participants`-taulukon
yhdeksi riviksi osallistujaa kohti:

```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` käy läpi `/v1/activities` itsenäisesti sen sijaan, että se
viittaisi kyselyyn `Activities`. Power BI:ssä kaksi kyselyä, jotka on ladattu kahtena
taulukkona, noutavat kumpikin tietonsa itsenäisesti päivityksen aikana. Itsenäisyys
antaa palvelun päivittää molempien taulukoiden osiot rinnakkain.

## Inkrementaalinen päivitys

Molemmat faktatason taulukot (`Activities` ja `ActivityParticipants`) määrittävät
Power BI:n inkrementaalisen päivityskäytännön: säilytä 5 vuotta vuosiosioita ja
tuo viimeiset 14 päivää uudelleen jokaisessa päivityksessä.

Mekanismina on DateTime-parametripari, `RangeStart` ja `RangeEnd`,
jonka Power BI Service sitoo uudelleen osiokohtaisesti päivityksen aikana. Jokainen faktakysely
välittää nämä parametrit API:n suodattimiin `start_date` ja `end_date`,
joten osiotason pyyntömäärä riippuu osiosta eikä koko historiasta. Kysely laajentaa API-ikkunaa minuutilla kummallakin puolella ja soveltaa tarkan puoliavoimen `[RangeStart, RangeEnd)`-rajan
uudelleen paikallisesti, joten osion oikeellisuus ei riipu siitä,
käsitteleekö palvelin rajansa sisältävinä vai poissulkevina.

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

Käyttöönoton jälkeinen ensimmäinen päivitys ottaa käytännön käyttöön: täydellinen historiallinen lataus,
hidas. Kaikki sen jälkeiset päivitykset käsittelevät vain inkrementaalista ikkunaa.

Mitattu tietojoukolla, jossa on noin 158 000 aktiviteettia ja noin 2 300 entiteettiä:

| Päivitys | Kokonaiskesto |
| - | - |
| Ensimmäinen (käytännön käyttöönotto, täydellinen historiallinen lataus) | \~31 min |
| Jokainen sen jälkeinen | **\~15 s** |

<Warning>
  Power BI:n klassinen päivitys-REST-päätepiste (`POST .../refreshes` pelkällä
  `notifyOption`) ohittaa päivityskäytännöt huomaamatta ja käy läpi koko datan joka kerta. Jos haluat soveltaa käytäntöä REST API:n kautta, käytä parannettua päivitystä: lähetä
  `type`, `commitMode` ja `applyRefreshPolicy` pyynnön rungossa. Power BI Servicen ajastimen
  käynnistämät päivitykset soveltavat käytäntöjä aina — tämä erikoisuus koskee vain REST-kutsua.
</Warning>

<Note>
  14 päivän ikkuna on tarkoituksella väljä. Aktiviteettien vastaanotto ei ole
  vain nykyhetkeen lisääviä: konnektorien täydennykset tuovat tapahtumia menneillä aikaleimoilla.
  1–2 päivän ikkuna jättäisi ne pysyvästi huomiotta.
</Note>

## Päivitysikkunan laajentaminen API:in

`GET /v1/activities` hyväksyy suodattimet `start_date` ja `end_date`, joihin
Power BI -parametrit välittyvät, ja suodattaa kohteen `event_ts` rajojen välillä.
`Entities` on dimensio, joten tässä esimerkissä se päivitetään kokonaisuudessaan joka kerta
inkrementaalisen päivityksen sijaan.

Reporting-tietojoukot käyttävät omia vesileimojaan (`updated_after`,
`occurred_after`) aktiviteettien päivämääräikkunan sijaan.

## Nopeusrajoitukset päivityksen aikana

`/v1` valvoo erillisiä avainkohtaisia ja IP-kohtaisia budjetteja (katso
[nopeusrajoitukset](/fi/developers/v1/rate-limits)). Noudata `429` + `Retry-After`; älä
kovakoodaa pyyntöbudjettia malliisi. Power BI -päivityksissä kaksi asiaa on tärkeää:

* **Yksittäisen taulukon läpikäynti on peräkkäistä.** Jokainen sivupyyntö tarvitsee
  edellisen sivun tunnisteen, joten yksittäisen taulukon nopeutta rajoittaa viive sivukoosta riippumatta.
* **Samanaikaiset taulukkopäivitykset kertautuvat.** Useiden suurten taulukoiden päivittäminen
  rinnakkain on tilanne, jossa yläraja saavutetaan, minkä vuoksi
  kohdan `Fn_FetchAll` 429-uudelleenyrityspolku on tärkeä. Suosi vähäisempää määrää rinnakkaisia taulukoita
  sen sijaan, että olettaisit rajoittamattoman samanaikaisuuden.

Uudelleenyritystä on testattu todellista kyllästynyttä rajoitinta vastaan. Päivitys,
joka ajettiin rinnakkaisen kuorman pitäessä avainta tarkoituksella sen ylärajalla,
valmistui silti ja tuotti samat rivimäärät kuin kuormittamaton vertailuajo —
uudelleenyritykset palauttivat täydet sivut ilman aukkoja.

## Sopimusversiot ja tallennetut tunnistetiedot

`/v1` hyväksyy pyyntöotsakkeen `Living-Security-Version`, jolla julkaistu
sopimusversio kiinnitetään (katso [API-pikaopas](/fi/developers/v1/quickstart)). Power
Query ei voi lähettää kyseistä otsaketta, kun tietolähde käyttää tallennettuja tunnistetietoja —
vain anonyymit tietolähteet sallivat mukautetut otsakkeet. Sen lisääminen aiheuttaa päivityksen epäonnistumisen virheeseen:

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

Pidä avain Power BI:n tallennetuissa tunnistetiedoissa äläkä käytä mukautettua otsaketta.
Jos pyynnössä ei ole eksplisiittistä versiota, API käyttää palvelutilin
kiinnitettyä `contract_version` tai uusinta julkaistua versiota, jos kiinnitystä ei ole asetettu.
Varmista tilin sopimusversio API-ylläpitäjältäsi ennen päivityksen
julkaisemista. Tuetut versiot on lueteltu [API-muutoslokissa](/fi/developers/v1/changelog).

Jos päivitys alkaa käyttäytyä odottamattomasti, tarkista API:n
`Living-Security-Version`-vastausotsake nähdäksesi, mikä sopimus
palveli pyyntöä.

## Päivitä ja varmista mallisi

Julkaise semanttinen malli Power BI -työtilaasi, sido sen tallennetut tunnistetiedot
alueelliseen `/v1`-lähteeseen ja suorita päivitys ennen aikataulun käyttöönottoa.
Tarkista päivityshistoriasta epäonnistumiset tai epätäydelliset lukemiset.

Tarkista rivimäärät riippumatonta API-lukua vastaan samoilla suodattimilla.
[Sivutusoppaassa](/fi/developers/v1/pagination) on Python- ja cURL-esimerkkejä
täydellisen tulosjoukon lukemiseen. Päivitys, joka lukee vain
ensimmäisen sivun, voi näyttää onnistuneelta pienellä tietojoukolla; vertaa määriä ennen kuin luotat raporttiin.

## Mallinnusvalinnat

* **Kampanjasummilla ja henkilökohtaisilla tuloksilla on eri tarkkuustasot.**
  `SimulationResults` kutsuu `GET /v1/simulations/{id}/results`
  (yksi GET ajoa kohti). Henkilökohtaiseen tietojenkalasteluun lisää kyselyt kohteisiin
  `GET /v1/reporting/simulation_results` ja
  `GET /v1/reporting/simulation_events` (tai operatiiviseen
  `GET /v1/simulation_results`, jos sinulla on jo `simulation:read`).
* **`vectors` kohteessa `/v1/simulations` on litistetty pilkuilla erotetuksi merkkijonoksi.**
  Luettelotyyppistä saraketta ei voi viipaloida varsinaisena dimensiona. Jos raporttisivun
  täytyy suodattaa vektorin mukaan, lisää simulaatio–vektori-siltataulukko.

## Seuraavat vaiheet

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/fi/developers/developer-hub/overview">
    Luo palvelutili ja API-avain Power BI:tä varten.
  </Card>

  <Card title="API-pikaopas" href="/fi/developers/v1/quickstart">
    Todenna `/v1`-pyynnöt Basic-todennuksella.
  </Card>

  <Card title="Sivutus" href="/fi/developers/v1/pagination">
    Luettelokuori, kursoritunnisteet ja suodattimien vakaus läpikäynnin aikana.
  </Card>

  <Card title="Nopeusrajoitukset" href="/fi/developers/v1/rate-limits">
    Avainkohtaiset ja IP-kohtaiset budjetit, 429-sopimus ja uudelleenyritysohjeet.
  </Card>
</CardGroup>


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