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

> Připojte Power BI k veřejnému API Living Security pomocí Power Query, vytvořte model typu hvězdicové schéma nad /v1 a zvolte provozní nebo reportingové datasety pro phishing.

Tato příručka je kompletní příklad načítání `/v1` do Power BI. Zahrnuje
vzor připojení v jazyce Power Query (M), sémantický model typu hvězdicové schéma vytvořený
nad `/v1/activities`, přírůstkové obnovování a případy, kdy použít **reportingové**
náhledové datasety pro výsledky phishingu na úrovni jednotlivých osob.

Příklady používají produkční hostitele pro USA
(`https://api.us-east-1.livingsecurity.ai/v1`). Pro organizaci v EU použijte
`https://api.eu-west-1.livingsecurity.ai/v1` v `Fn_FetchAll` i
`Fn_GetJson` a poté svažte přihlašovací údaje zdroje dat s tímto hostitelem. Viz
[Regiony](/cs/developers/v1/regions).

Tato příručka používá uložené přihlašovací údaje Basic s klíčem API. Klient
OAuth není potřeba. Možnosti přihlašovacích údajů naleznete v části [Ověřování](/cs/developers/v1/authentication)
a informace o přístupu a nastavení v části [Developer Hub](/cs/developers/developer-hub/overview).

## K čemu tento příklad slouží

Použijte jej, pokud potřebujete:

* Načíst seznamové endpointy `/v1` do datasetu Power BI se správným stránkováním
  pomocí kurzoru a opakováním při překročení limitu požadavků.
* Vytvořit sémantický model, ve kterém jsou míry („aktivní entity“, „aktivity na
  aktivní entitu“) dimenzionální, a nikoli počítané z plochého výpisu tabulky.
* Zkrátit plánované obnovení Power BI z úplného průchodu celou historií na
  malé denní okno pomocí přírůstkového obnovování.
* Zvolit správnou granularitu phishingu: souhrny kampaní (provozní) oproti
  výsledkům na úrovni osob a typovaným událostem (reportingový náhled).

## Požadavky

* Klíč API s oprávněními, která vaše dotazy potřebují:
  * Provozní příklad: `activity:read`, `entity:read` a
    `simulation:read`.
  * Phishing na úrovni osob prostřednictvím reportingových datasetů: `reporting:read`.
    Viz [Klíče API](/cs/developers/developer-hub/api-keys).
* Power BI Desktop nebo pracovní prostor Power BI / Fabric, ve kterém můžete vytvořit
  sémantický model a naplánovat obnovování.

## Zvolte správnou granularitu

`/v1` nabízí pro phishing dva související povrchy. Nejsou zaměnitelné.

| Potřeba | Použití | Oprávnění | Granularita |
| - | - | - | - |
| Souhrny trychtýře kampaní | `GET /v1/simulations/{id}/results` | `simulation:read` | Jeden řádek na simulaci (doručeno, otevřeno, kliknuto, …) |
| Výsledky na úrovni osob (provozní) | `GET /v1/simulation_results` | `simulation:read` | Jeden řádek na entitu × simulaci |
| Výsledky na úrovni osob (BI / reporting) | `GET /v1/reporting/simulation_results` | `reporting:read` | Stejná granularita, názvy polí BI (`entity_id`, `simulation_id`), watermark `updated_after` |
| Typovaná fakta o interakcích | `GET /v1/reporting/simulation_events` | `reporting:read` | Jeden fakt na interakci (`simulation.link_clicked`, …), `occurred_after` |
| Aktuální dimenze Pracovní síla | `GET /v1/reporting/entities` | `reporting:read` | Jeden aktuální řádek na entitu. Propojení přes `reporting.simulation_results.entity_id = reporting.entities.id` |
| Aktuální verze entit | `GET /v1/reporting/entity_versions` | `reporting:read` | Jeden otevřený aktuální interval na entitu. Tabulka historie verzí neexistuje |
| Aktuální členství v kohortách | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Jeden aktuální fakt na `(cohort_id, entity_id)`. Tabulka historie členství neexistuje |
| Spojovací osa pro všechny typy aktivit | `GET /v1/activities` | `activity:read` | Jedna událost na aktivitu, vnořené `participants[]` |

Níže uvedené příklady dotazů Power BI volají **provozní** trasy
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Pro reporting phishingu na úrovni osob dávejte přednost
níže uvedené **reportingové** náhledové rodině. Klíč, kterému bylo uděleno pouze `reporting:read`, nemůže
volat provozní `/v1/simulation_results` (API vrátí `403`).

### Škála obtížnosti

Obtížnost simulace je celé číslo 1–5 u vlastností `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), `/v1/activities`
(`difficulty_tier`) a `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` je nejsnazší a `5` nejtěžší. Adaptivní a progresivní simulace nemají
jedinou hodnotu na úrovni kampaně, takže `/v1/simulations.difficulty_level` je `null`,
zatímco `difficulty_level` jednotlivých příjemců ve výsledcích může být stále nastavena. Pro převod na
starší exporty nízká/střední/vysoká považujte `1–2` za nízkou, `3` za střední a `4–5` za
vysokou.

<Warning>
  Nevytvářejte nové integrace BI nad nezdokumentovanými síťovými voláními Nástěnky
  ani nad starším kurátorským přehledem veřejného API `/api/v1`. Ověřujte se
  na regionálním hostiteli `/v1` vaší organizace pomocí klíče API z Developer Hub. Viz
  [Regiony](/cs/developers/v1/regions).
</Warning>

## Vzor připojení (Power Query)

Každý dotaz v příkladu je založen na čtyřech pravidlech. Každé z nich vynucuje
služba Power BI, nejde o stylové preference.

<AccordionGroup>
  <Accordion title="Ponechte základní URL ve Web.Contents statickou a ukončete ji na /v1">
    Vše dynamické vložte do `RelativePath` a `Query`. První argument
    `Web.Contents` musí být literální řetězec, který končí kořenem
    regionálního `/v1` vaší organizace:

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

    Volající pak předávají pouze zdroj (`"simulations"`, nikoli
    `"v1/simulations"`). Nevkládejte hostitele pro USA pro organizaci v EU.

    Pokud je první argument sestaven zřetězením řetězců, služba Power BI
    nemůže staticky identifikovat zdroj dat a plánované obnovení selže
    s chybou „dynamic data source“, přestože dotaz funguje v Power BI
    Desktop.
  </Accordion>

  <Accordion title="Zadejte klíč API jako uložené přihlašovací údaje Basic, nikoli v textu dotazu">
    Když vás Power BI vyzve k zadání přihlašovacích údajů pro zdroj dat `/v1`, zvolte
    **Basic** a vložte klíč API jako **uživatelské jméno**. Do pole **Heslo** zadejte zástupnou hodnotu,
    například `x`. Služba Power BI odmítne prázdné
    heslo, přestože API toto pole ignoruje. Funguje také klíč jako heslo se
    zástupným uživatelským jménem.

    Nepřidávejte hlavičku `Authorization` v M. Na vašem počítači funguje, ale
    vloží klíč do souboru dotazu a brání plánovanému obnovení.
  </Accordion>

  <Accordion title="Nechte proběhnout test připojení v dialogu přihlašovacích údajů">
    `GET /v1` vyžaduje ověření: s platným klíčem vrací `200` a bez něj `401`.
    Nasměrujte zdroj dat na `…/v1` a test **nepřeskakujte** —
    platný klíč se uloží, zrušený nebo chybně zadaný selže okamžitě.

    Starší příklady používaly jako zdroj dat holého hostitele a doporučovaly test přeskočit,
    protože `GET /` je vždy `404`. Tyto modely přesměrujte na `…/v1`.
  </Accordion>

  <Accordion title="Nastavte limit=500 explicitně u každého seznamového dotazu">
    Výchozí velikost stránky u každého seznamového endpointu `/v1` je 25. Ponechání výchozí hodnoty
    násobí počet požadavků pro stejná data. Hodnoty dotazu musí být text —
    `limit = "500"` v M, nikoli `limit = 500`.
  </Accordion>
</AccordionGroup>

### Sdílená funkce stránkování

Každý dotaz v příkladu volá jedinou funkci `Fn_FetchAll`, která prochází
`next_page_token`, dokud není `null` (viz
[stránkování](/cs/developers/v1/pagination)). Její pozoruhodné vlastnosti:

* **Statická základní URL `/v1`, dynamické `RelativePath` a `Query`.** Vyžaduje se pro
  plánované obnovení a pro test přihlašovacích údajů.
* **`ManualStatusHandling = {429}` s omezeným počtem opakování.** Funkce čte
  `Retry-After`, pokud jej Power Query zpřístupní, jinak čeká 30 sekund
  a po 5 po sobě jdoucích odpovědích `429` se vzdá s explicitní chybou, místo aby
  cyklila, dokud obnovení nevyprší. `5xx` se záměrně
  neopakuje — chyba serveru se má projevit, nikoli tiše vytvořit částečný
  dataset.
* **`Timeout` 60 sekund.** Stránky obvykle odpovídají za zlomek sekundy, takže
  minuta ticha znamená, že požadavek je mrtvý.
* **`Binary.Buffer` před `Json.Document`.** Bez vyrovnávací paměti
  může mashup engine znovu vyžádat datový proud odpovědi, pokud jej čte vícekrát.
* **`baseQuery` se v každé iteraci předává beze změny.** Tokeny stránek jsou
  neprůhledné, podepsané kurzory vázané na organizaci, přihlašovací údaje, verzi
  kontraktu a otisk filtrů a řazení. Změna filtru
  uprostřed průchodu je odmítnuta jako neplatný token a průchod se nezačne znovu
  od první stránky.

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

### Typované dotazy pro jednotlivé endpointy

Každý konzument `Fn_FetchAll` dodržuje stejný postup: zavolá endpoint,
promítne pevný seznam sloupců, odstraní diskriminátor `object` a
časová razítka otypuje jako `datetimezone`, aby se míry s datem tiše neposouvaly podle
časového pásma kapacity obnovování.

<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>
  Explicitní seznam sloupců u `Table.FromRecords` udržuje schéma stabilní,
  když endpoint vrátí nula řádků. Holé `Table.FromRecords({})` vytvoří
  tabulku bez sloupců a všechny další kroky selžou kvůli chybějícím polím.
</Tip>

## Excel

Stejný Power Query (M) běží v Excelu v části Načíst a transformovat — jde o stejný
engine jako v Power BI Desktop. Použijte jej, pokud chcete tabulku v listu
místo sémantického modelu.

V Excelu pro Windows (Microsoft 365, Excel 2024, 2021, 2019 nebo 2016):

1. **Data → Získat data → Z jiných zdrojů → Prázdný dotaz.**
2. V editoru Power Query zvolte **Domů → Pokročilý editor**. Vložte nejprve
   `Fn_FetchAll`, poté tento dotaz přejmenujte na `Fn_FetchAll` (Nastavení
   dotazu → Název, nebo klikněte pravým tlačítkem v podokně Dotazy a zvolte **Přejmenovat**).
3. Zopakujte pro `Fn_GetJson` a poté pro každý dotaz sestavy.
4. **Domů → Zavřít a načíst → Zavřít a načíst do…** otevře dialog **Importovat data**.
   Pro `Fn_FetchAll` a
   `Fn_GetJson` zvolte **Vytvořit pouze připojení** (jsou to funkce, nikoli tabulky). Pro
   každý dotaz sestavy, který chcete mít v listu, zvolte **Tabulka**.
5. Při prvním obnovení zvolte **Basic**: klíč API jako **Uživatelské jméno**,
   zástupnou hodnotu, například `x`, jako **Heslo**. Excel ukládá jedny přihlašovací údaje
   na URL zdroje dat; další dotazy, které volají stejného hostitele `/v1`, je znovu použijí.

<Note>
  **Zavřít a načíst do…** může být při prvním vytvoření dotazu v sešitu
  zobrazeno šedě. Použijte **Zavřít a načíst**, poté v Excelu vyberte **Data →
  Dotazy a připojení**, klikněte na dotaz pravým tlačítkem a zvolte **Načíst do**.
  Tento postup pro daný případ uvádí dokumentace Microsoftu.
</Note>

Z Power Query nelze na ověřeném zdroji odeslat `Living-Security-Version` — platí
stejné omezení jako v Power BI. Připněte servisní účet nebo předejte `api_version` v záznamu dotazu. Viz
[Verze kontraktu a uložené přihlašovací údaje](#verze-kontraktu-a-uložené-přihlašovací-údaje).

Hotové dotazy sestav (mapování sloupců a odchylky od exportu
předchozího poskytovatele):

<CardGroup cols={3}>
  <Card title="Soulad na úrovni osob" href="/cs/developers/v1/reports/per-person-compliance">
    Zápisy do školení na úrovni osoba × přiřazení.
  </Card>

  <Card title="Protokol e-mailového phishingu" href="/cs/developers/v1/reports/email-phishing-log">
    Phishingové interakce na úrovni událostí za posledních 12 měsíců.
  </Card>

  <Card title="Nejnáchylnější uživatelé" href="/cs/developers/v1/reports/most-susceptible-users">
    Souhrn kliknutí, zadání přihlašovacích údajů a nahlášení za 12 měsíců na úrovni osob.
  </Card>
</CardGroup>

## Reportingové náhledové datasety (phishing na úrovni osob)

Reportingová náhledová rodina poskytuje datasety pro phishing na úrovni osob.
Vyžaduje `reporting:read` a přístup k reportingu pro vaši organizaci.
K prohlížení schémat a zásad nakládání s daty použijte níže uvedený katalog datasetů.

Zjištění aktuálního katalogu:

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

Každý dataset deklaruje granularitu, primární klíč, spojení, watermark, zásady oprav a
zásady mazání.

### simulation\_results

* **Cesta:** `GET /v1/reporting/simulation_results`
* **Granularita:** entita × simulace
* **Filtry:** `simulation`, `entity`, `updated_after` a `limit` /
  `page_token`
* **Objekt na drátě:** `reporting.simulation_result`
* **Spojovací klíče:** `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
```

Použijte `updated_after` jako inkluzivní dolní watermark při obnovování nedávných
oddílů. Proměnlivé dimenze výsledků se přepisují na místě pod stejným `id`.

### simulation\_events

* **Cesta:** `GET /v1/reporting/simulation_events`
* **Granularita:** jeden typovaný fakt na pozorovanou interakci
* **Filtry:** `simulation`, `entity`, `event_type`, `occurred_after` a
  `limit` / `page_token`
* **Objekt na drátě:** `reporting.simulation_event`
* **Typy událostí:** uzavřené veřejné tokeny, například `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — nikoli surové
  typy aktivit ClickHouse `phishing.simulation.*`
* **Zařízení:** `device_type` je odvozená třída (pozorované tokeny zahrnují
  `desktop`, `mobile`, `tablet`, `unknown`). Surové `user_agent` není v
  `/v1`. Surová IP adresa není nikdy v `/v1` (nikdy se neukládá; pouze osolený `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
```

Pro přírůstkové načítání událostí použijte `occurred_after`. Fakta jsou pouze
přidávaná; pozdě příchozí data jsou nové řádky.

### entities

* **Cesta:** `GET /v1/reporting/entities`
* **Granularita:** jeden aktuální řádek na entitu
* **Filtry:** `type`, `updated_after` a `limit` / `page_token`
* **Objekt na drátě:** `reporting.entity`
* **Spojovací klíč:** `id` / `entity_id` (stejná hodnota)

Phishingová fakta propojte s osobami pomocí
`reporting.simulation_results.entity_id = reporting.entities.id`.
Provozní `GET /v1/entities` zůstává na `entity:read` a jde o jiné
oprávnění.

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

Tyto dvě datasety mají pojmenovanou výjimku pouze pro aktuální stav:

> Neexistuje žádná autoritativní tabulka historie verzí ani členství. Tento dataset promítá aktuální řádek jako jeden otevřený interval (`valid_to` je null, `is_current` je true). `valid_from` je `created_at`, jinak `updated_at` u verzí entit, a čas pozorování požadavku u členství v kohortách. Uzavřené historické intervaly nejsou vymýšleny.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Stejné filtry jako
  entities. `valid_from` je `created_at`, jinak `updated_at`. Entita,
  u které jsou obě časová razítka null, je vynechána.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtry:
  `cohort`, `entity`, `updated_after`. `updated_at` je poslední aktualizace kohorty,
  takže přírůstkové obnovování zůstává věrohodné. `valid_from` je čas
  pozorování požadavku. Propojte `entity_id` s `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
```

## Sémantický model typu hvězdicové schéma nad /v1/activities

`/v1/activities` je časově označený tok událostí organizace. Nemá
žádné číselné míry — každá veličina v sestavě je počet řádků — což činí z
hvězdicového schématu přirozenou volbu.

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

* **`Activities`** je faktová tabulka událostí na skutečné granularitě. Jeden řádek na aktivitu.
  Účastníci se zde odstraňují a vytvářejí se jako samostatná tabulka.
* **`ActivityParticipants`** existuje proto, že účastníci jsou skutečně
  vztahem jedna k mnoha s rolemi jako `actor` a `target`. Zploštění do
  `Activities` by buď duplikovalo řádky aktivit (nafukovalo by každý počet),
  nebo zahazovalo další účastníky.
  Vztah k `Activities` je jediným obousměrným
  vztahem modelu, takže slicer entity dosáhne na faktovou tabulku.
* **`Entities`** je dimenze, která pojmenovává účastníky. Řádky účastníků
  u aktivity obsahují `entity_id` a roli, ale ne zobrazované jméno.
* **`Dates`** je generována v M (nula volání API) a označena jako datová tabulka modelu.
  Díky tomu jsou povoleny časové funkce DAX
  (`DATESINPERIOD`, `DATEADD`). Sloupce s názvy měsíců a dnů v týdnu deklarují
  `sortByColumn` na svých číselných protějšcích, aby vizuály neřadily měsíce
  abecedně.

Všechny míry jsou explicitní (model nastavuje `discourageImplicitMeasures`): celkový počet
aktivit, aktivní entity, aktivity na aktivní entitu, dny s aktivitou
a 7denní klouzavý denní průměr.

### Tvar propojovací tabulky

`ActivityParticipants` prochází `/v1/activities` a rozkládá pole `participants`
na jeden řádek na účastníka:

```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` prochází `/v1/activities` nezávisle, místo aby
odkazovala na dotaz `Activities`. V Power BI dvě tabulky načtené jako dva dotazy
se při obnovení načítají každá samostatně. Jejich nezávislost
umožňuje službě obnovovat oddíly obou tabulek paralelně.

## Přírůstkové obnovování

Obě tabulky faktů (`Activities` a `ActivityParticipants`) deklarují
zásadu přírůstkového obnovování Power BI: uchovávat 5 let ročních oddílů
a při každém obnovení znovu importovat posledních 14 dní.

Mechanismem je dvojice parametrů DateTime, `RangeStart` a `RangeEnd`,
které služba Power BI při obnovování znovu váže pro každý oddíl. Každý dotaz faktů
tyto parametry promítá do filtrů `start_date` a `end_date` API,
takže objem požadavků na oddíl odpovídá oddílu,
nikoli celé historii. Dotaz rozšíří okno API o minutu na každé straně
a přesnou polootevřenou hranici `[RangeStart, RangeEnd)` znovu uplatní lokálně,
takže správnost oddílů nezávisí na tom, zda server
považuje své meze za inkluzivní, nebo exkluzivní.

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

První obnovení po nasazení uplatní zásadu: úplné načtení historie,
pomalé. Každé další obnovení se dotkne pouze přírůstkového okna.

Měřeno na datasetu s \~158 000 aktivitami a \~2 300 entitami:

| Obnovení | Doba běhu |
| - | - |
| První (uplatnění zásady, úplné načtení historie) | \~31 min |
| Každé další obnovení | **\~15 s** |

<Warning>
  Klasický REST endpoint obnovení Power BI (`POST .../refreshes` pouze s
  `notifyOption`) tiše ignoruje zásady obnovování a pokaždé provede úplný průchod.
  Chcete-li zásadu uplatnit přes REST API, použijte rozšířené obnovení: v těle odešlete
  `type`, `commitMode` a `applyRefreshPolicy`. Obnovení
  spuštěná plánovačem služby Power BI zásady uplatňují vždy — tato
  zvláštnost se týká pouze volání REST.
</Warning>

<Note>
  14denní okno je záměrně štědré. Příjem aktivit není
  pouze přidávání s aktuálním časem: zpětná doplnění konektorů ukládají události s minulými časovými razítky.
  Okno 1–2 dnů by je trvale minulo.
</Note>

## Rozšíření okna obnovení do API

`GET /v1/activities` přijímá filtry `start_date` a `end_date`, do kterých se
parametry Power BI promítají a které filtrují `event_ts` mezi mezemi.
`Entities` je dimenze, proto ji tento příklad pokaždé obnovuje celou,
nikoli přírůstkově.

Reportingové datasety používají vlastní watermarky (`updated_after`,
`occurred_after`) místo datového okna aktivit.

## Limity požadavků během obnovování

`/v1` uplatňuje samostatné rozpočty na klíč a na IP adresu (viz
[limity požadavků](/cs/developers/v1/rate-limits)). Respektujte `429` + `Retry-After`;
nevkládejte do modelu pevně zadaný rozpočet požadavků. Důležité jsou dvě vlastnosti
obnovování v Power BI:

* **Průchod jednou tabulkou je sekvenční.** Každý požadavek na stránku potřebuje
  token předchozí stránky, takže jedna tabulka je omezena latencí bez ohledu na velikost
  stránky.
* **Souběžná obnovení tabulek se násobí.** K dosažení stropu dochází, když se
  paralelně obnovuje několik velkých tabulek, proto je důležitá cesta opakování
  při 429 ve `Fn_FetchAll`. Dávejte přednost menšímu počtu paralelních tabulek
  před předpokladem neomezené souběžnosti.

Opakování bylo ověřeno proti skutečně nasycenému limiteru. Obnovení,
které běželo, zatímco paralelní zátěž záměrně držela klíč na jeho stropu,
se přesto dokončilo a vytvořilo stejné počty řádků jako základní měření bez zátěže —
opakování vrátila úplné stránky, nikoli mezery.

## Verze kontraktu a uložené přihlašovací údaje

`/v1` přijímá hlavičku požadavku `Living-Security-Version` pro připnutí publikované
verze kontraktu (viz [Rychlý start API](/cs/developers/v1/quickstart)). Power
Query nemůže tuto hlavičku odeslat, pokud zdroj dat používá uložené přihlašovací údaje —
vlastní hlavičky umožňují pouze anonymní zdroje dat. Její přidání způsobí selhání obnovení
s chybou:

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

Ponechte klíč v uložených přihlašovacích údajích Power BI a vlastní hlavičku vynechte.
Pokud požadavek neobsahuje explicitní verzi, API použije připnutou
verzi `contract_version` servisního účtu, nebo nejnovější publikovanou verzi, pokud není připnutí nastaveno.
Před publikováním obnovení ověřte verzi kontraktu účtu u správce API. Podporované
verze viz [Seznam změn API](/cs/developers/v1/changelog).

Pokud se obnovení začne chovat neočekávaně, zkontrolujte hlavičku odpovědi API
`Living-Security-Version`, abyste zjistili, který kontrakt
požadavek skutečně obsloužil.

## Obnovení a ověření modelu

Publikujte sémantický model do pracovního prostoru Power BI, svažte jeho uložené
přihlašovací údaje s regionálním zdrojem `/v1` a před zapnutím
plánu spusťte obnovení. Zkontrolujte historii obnovení, zda neobsahuje selhání nebo neúplná čtení.

Porovnejte počty řádků s nezávislým čtením API se stejnými filtry.
[Příručka ke stránkování](/cs/developers/v1/pagination) obsahuje příklady v Pythonu a cURL
pro čtení úplné sady výsledků. Obnovení, které načte pouze
první stránku, může u malého datasetu vypadat úspěšně; než se na sestavu spolehnete, porovnejte počty.

## Volby modelu

* **Souhrny kampaní a výsledky na úrovni osob mají různou granularitu.**
  `SimulationResults` volá `GET /v1/simulations/{id}/results`
  (jedno GET na běh). Pro phishing na úrovni osob přidejte dotazy pro
  `GET /v1/reporting/simulation_results` a
  `GET /v1/reporting/simulation_events` (nebo provozní
  `GET /v1/simulation_results`, pokud již máte `simulation:read`).
* **`vectors` u `/v1/simulations` je zploštěno na řetězec spojený čárkami.**
  Sloupec typu seznam nelze použít jako skutečnou dimenzi ve slicerech. Pokud stránka sestavy
  potřebuje filtrovat podle vektoru, přidejte propojovací tabulku simulace–vektor.

## Další kroky

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/cs/developers/developer-hub/overview">
    Vytvořte servisní účet a klíč API pro Power BI.
  </Card>

  <Card title="Rychlý start API" href="/cs/developers/v1/quickstart">
    Ověřujte požadavky `/v1` pomocí Basic auth.
  </Card>

  <Card title="Stránkování" href="/cs/developers/v1/pagination">
    Seznamová obálka, kurzorové tokeny a stabilita filtrů během průchodu.
  </Card>

  <Card title="Limity požadavků" href="/cs/developers/v1/rate-limits">
    Rozpočty na klíč a na IP adresu, kontrakt 429 a doporučení pro opakování.
  </Card>
</CardGroup>


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