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

> Підключіть Power BI до Living Security Public API за допомогою Power Query, побудуйте модель «зірка» на /v1 та оберіть операційні або звітні набори даних для фішингу.

Цей посібник — наскрізний приклад читання `/v1` у Power BI. Він охоплює
шаблон підключення в Power Query (M), семантичну модель за схемою «зірка», побудовану
на `/v1/activities`, інкрементальне оновлення, а також випадки, коли слід використовувати **reporting**
preview-набори даних для результатів фішингу по кожній особі.

У прикладах використовується виробничий хост для США
(`https://api.us-east-1.livingsecurity.ai/v1`). Для організації з ЄС використовуйте
`https://api.eu-west-1.livingsecurity.ai/v1` і в `Fn_FetchAll`, і в
`Fn_GetJson`, після чого прив’яжіть облікові дані джерела даних до цього хоста. Див.
[Regions](/uk/developers/v1/regions).

У цьому посібнику використовуються збережені облікові дані Basic з ключем API. Клієнт
OAuth вам не потрібен. Про варіанти облікових даних див. [Authentication](/uk/developers/v1/authentication),
а про доступ і налаштування — [Developer Hub](/uk/developers/developer-hub/overview).

## Для чого призначений цей приклад

Використовуйте його, якщо вам потрібно:

* Завантажити списочні endpoint-и `/v1` у набір даних Power BI з коректною курсорною
  пагінацією та повторною спробою у разі перевищення ліміту запитів.
* Побудувати семантичну модель, у якій міри («активні сутності», «активності на
  одну активну сутність») є вимірними, а не підраховуються з пласкої таблиці.
* Перевести заплановане оновлення Power BI від повного обходу історії до
  невеликого щоденного вікна за допомогою інкрементального оновлення.
* Обрати правильну деталізацію фішингових даних: підсумки кампаній (операційні) або
  результати по кожній особі та типізовані події (reporting preview).

## Передумови

* Ключ API з дозволами, потрібними для ваших запитів:
  * Операційний приклад: `activity:read`, `entity:read` та
    `simulation:read`.
  * Фішинг по кожній особі через reporting-набори даних: `reporting:read`.
    Див. [API keys](/uk/developers/developer-hub/api-keys).
* Power BI Desktop або робоча область Power BI / Fabric, де можна створити
  семантичну модель і запланувати оновлення.

## Вибір правильної деталізації

`/v1` надає дві пов’язані поверхні для фішингу. Вони не взаємозамінні.

| Потреба | Використання | Дозвіл | Деталізація |
| - | - | - | - |
| Підсумки воронки кампанії | `GET /v1/simulations/{id}/results` | `simulation:read` | Один рядок на симуляцію (delivered, opened, clicked, …) |
| Результати по кожній особі (операційні) | `GET /v1/simulation_results` | `simulation:read` | Один рядок на сутність × симуляцію |
| Результати по кожній особі (BI / звітність) | `GET /v1/reporting/simulation_results` | `reporting:read` | Та сама деталізація, назви полів для BI (`entity_id`, `simulation_id`), водяний знак `updated_after` |
| Типізовані факти взаємодії | `GET /v1/reporting/simulation_events` | `reporting:read` | Один факт на взаємодію (`simulation.link_clicked`, …), `occurred_after` |
| Поточний вимір Workforce | `GET /v1/reporting/entities` | `reporting:read` | Один поточний рядок на сутність. Приєднання за `reporting.simulation_results.entity_id = reporting.entities.id` |
| Поточні версії сутностей | `GET /v1/reporting/entity_versions` | `reporting:read` | Один поточний інтервал без кінцевої дати на сутність. Таблиці історії версій не існує |
| Поточні членства в Cohort | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Один поточний факт на `(cohort_id, entity_id)`. Таблиці історії членств не існує |
| Ключ приєднання для всіх типів активностей | `GET /v1/activities` | `activity:read` | Одна подія на активність, вкладений `participants[]` |

Наведені нижче приклади запитів Power BI викликають **операційні** маршрути
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Для звітності з фішингу по кожній особі надавайте перевагу
**reporting** preview-сімейству, описаному нижче. Ключ, якому надано лише `reporting:read`, не може
викликати операційний `/v1/simulation_results` (API повертає `403`).

### Шкала складності

Складність симуляції — ціле число від 1 до 5 у властивостях `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), `/v1/activities`
(`difficulty_tier`) та `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` — найлегший рівень, `5` — найскладніший. Адаптивні та прогресивні симуляції не мають
єдиного значення на рівні кампанії, тому `/v1/simulations.difficulty_level` має значення `null`,
хоча `difficulty_level` для окремого отримувача в результатах усе ще може бути задано. Щоб зіставити з
застарілими експортами low/medium/high, розглядайте `1–2` як low, `3` як medium, а `4–5` як
high.

<Warning>
  Не створюйте нових BI-інтеграцій на основі недокументованих мережевих викликів Dashboard
  або застарілого курованого огляду Public API `/api/v1`. Автентифікуйтеся на
  регіональному хості `/v1` вашої організації за допомогою ключа API з Developer Hub. Див.
  [Regions](/uk/developers/v1/regions).
</Warning>

## Шаблон підключення (Power Query)

В основі кожного запиту в цьому прикладі лежать чотири правила. Кожне з них вимагається
службою Power BI, а не є стильовим уподобанням.

<AccordionGroup>
  <Accordion title="Зберігайте базову URL-адресу Web.Contents статичною та завершуйте її на /v1">
    Усе динамічне розміщуйте в `RelativePath` та `Query`. Перший аргумент
    `Web.Contents` має бути літеральним рядком, що закінчується на кореневій адресі
    регіонального `/v1` вашої організації:

    * США: `https://api.us-east-1.livingsecurity.ai/v1`
    * ЄС: `https://api.eu-west-1.livingsecurity.ai/v1`

    Тоді виклики передають лише ресурс (`"simulations"`, а не
    `"v1/simulations"`). Не вставляйте хост США для організації з ЄС.

    Якщо перший аргумент формується конкатенацією рядків, служба Power BI
    не може статично визначити джерело даних, і заплановане оновлення завершується
    помилкою «dynamic data source», хоча запит працює в Power BI
    Desktop.
  </Accordion>

  <Accordion title="Передавайте ключ API як збережені облікові дані Basic, а не в тексті запиту">
    Коли Power BI запитує облікові дані для джерела даних `/v1`, виберіть
    **Basic** і вставте ключ API як **username**. У поле **password** введіть
    заповнювач, наприклад `x`. Служба Power BI відхиляє порожній
    пароль, хоча API це поле ігнорує. Ключ як пароль із заповнювачем замість імені користувача
    також працює.

    Не додавайте заголовок `Authorization` у M. Він працює на вашому комп’ютері, але
    вбудовує ключ у файл запиту та конфліктує із запланованим оновленням.
  </Accordion>

  <Accordion title="Дозвольте виконати перевірку підключення в діалозі облікових даних">
    `GET /v1` вимагає автентифікації: він повертає `200` з дійсним ключем і `401`
    без нього. Вкажіть `…/v1` як джерело даних і **не** пропускайте перевірку —
    дійсний ключ зберігається, а відкликаний або некоректний одразу викликає помилку.

    У старіших прикладах як джерело даних використовувався голий хост і пропонувалося пропустити
    перевірку, оскільки `GET /` завжди повертає `404`. Переналаштуйте ці моделі на `…/v1`.
  </Accordion>

  <Accordion title="Явно задавайте limit=500 у кожному списочному запиті">
    Розмір сторінки за замовчуванням для кожного списочного endpoint-а `/v1` — 25. Якщо залишити
    значення за замовчуванням, кількість запитів для тих самих даних збільшується. Значення запиту
    мають бути текстовими — `limit = "500"` у M, а не `limit = 500`.
  </Accordion>
</AccordionGroup>

### Спільна функція пагінації

Кожен запит у цьому прикладі викликає єдину `Fn_FetchAll`, яка проходить
`next_page_token`, доки воно не стане `null` (див.
[pagination](/uk/developers/v1/pagination)). Її основні властивості:

* **Статична базова URL-адреса `/v1`, динамічні `RelativePath` і `Query`.** Потрібно для
  запланованого оновлення та для перевірки облікових даних.
* **`ManualStatusHandling = {429}` з обмеженою кількістю повторних спроб.** Функція зчитує
  `Retry-After`, якщо Power Query його надає, інакше очікує 30 секунд і
  припиняє спроби після 5 послідовних відповідей `429` з явною помилкою, а не
  виконує цикл, доки оновлення не перевищить тайм-аут. `5xx` навмисно не
  повторюється — помилка сервера має бути помітною, а не непомітно створювати частковий
  набір даних.
* **60-секундний `Timeout`.** Сторінки зазвичай відповідають значно швидше ніж за секунду, тому
  хвилина тиші означає, що запит «мертвий».
* **`Binary.Buffer` перед `Json.Document`.** Без буферизації рушій
  mashup може повторно запитувати потік відповіді, якщо зчитує його більше одного разу.
* **`baseQuery` передається без змін на кожній ітерації.** Токени сторінок —
  це непрозорі підписані курсори, прив’язані до організації, облікових даних, версії
  контракту та відбитка фільтрів і сортування. Зміна фільтра
  під час обходу відхиляється як недійсний токен, а не перезапускається з першої
  сторінки.

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

### Типізовані запити для кожного endpoint-а

Кожен споживач `Fn_FetchAll` дотримується однакової структури: викликає endpoint,
проєктує фіксований перелік стовпців, видаляє дискримінатор `object` і типізує
часові мітки як `datetimezone`, щоб міри дат непомітно не зсувалися через часовий пояс
ємності оновлення.

<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>
  Явний перелік стовпців у `Table.FromRecords` забезпечує стабільність схеми, коли
  endpoint повертає нуль рядків. Порожній `Table.FromRecords({})` створює
  таблицю без стовпців, і всі наступні кроки завершуються помилкою через відсутні поля.
</Tip>

## Excel

Той самий Power Query (M) працює в Get & Transform в Excel — це той самий
рушій, що й у Power BI Desktop. Використовуйте це, коли вам потрібна таблиця на аркуші
замість семантичної моделі.

В Excel для Windows (Microsoft 365, Excel 2024, 2021, 2019 або 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. У редакторі Power Query виберіть **Home → Advanced Editor**. Спочатку вставте
   `Fn_FetchAll`, потім перейменуйте цей запит на `Fn_FetchAll` (Query
   Settings → Name або клацніть правою кнопкою миші в панелі Queries → **Rename**).
3. Повторіть для `Fn_GetJson`, а потім для кожного запиту звіту.
4. **Home → Close & Load → Close & Load To…** відкриває діалог **Import Data**.
   Виберіть **Only Create Connection** для `Fn_FetchAll` та
   `Fn_GetJson` (це функції, а не таблиці). Виберіть **Table** для
   кожного запиту звіту, який потрібен на аркуші.
5. Під час першого оновлення виберіть **Basic**: ключ API як **User name**,
   заповнювач, наприклад `x`, як **Password**. Excel зберігає одні облікові дані
   на кожну URL-адресу джерела даних; наступні запити до того самого хоста `/v1` використовують
   їх повторно.

<Note>
  **Close & Load To…** може бути неактивним, коли ви вперше створюєте запит у
  книзі. Скористайтеся **Close & Load**, потім в Excel виберіть **Data →
  Queries & Connections**, клацніть запит правою кнопкою миші та виберіть **Load To**.
  Корпорація Microsoft документує цей шлях саме для такого випадку.
</Note>

Ви не можете надсилати `Living-Security-Version` із Power Query для
автентифікованого джерела — це те саме обмеження, що й у Power BI. Закріпіть
службовий обліковий запис або передайте `api_version` у записі запиту. Див.
[Contract versions and stored credentials](#версії-контракту-та-збережені-облікові-дані).

Готові запити звітів (зіставлення стовпців і відмінності від експорту
попереднього постачальника):

<CardGroup cols={3}>
  <Card title="Per-person compliance" href="/uk/developers/v1/reports/per-person-compliance">
    Реєстрації на навчання з деталізацією особа × призначення.
  </Card>

  <Card title="Email phishing log" href="/uk/developers/v1/reports/email-phishing-log">
    Фішингові взаємодії на рівні подій за останні 12 місяців.
  </Card>

  <Card title="Most susceptible users" href="/uk/developers/v1/reports/most-susceptible-users">
    Зведення кліків, облікових даних і повідомлень по кожній особі за 12 місяців.
  </Card>
</CardGroup>

## Reporting preview-набори даних (фішинг по кожній особі)

Reporting preview-сімейство надає набори даних для фішингу по кожній особі.
Воно потребує `reporting:read` та доступу до звітності для вашої організації.
Скористайтеся наведеним нижче каталогом наборів даних, щоб переглянути їхні схеми та політики даних.

Отримайте актуальний каталог:

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

Кожен набір даних оголошує деталізацію, первинний ключ, приєднання, водяний знак, політику виправлень і
видалення.

### simulation\_results

* **Шлях:** `GET /v1/reporting/simulation_results`
* **Деталізація:** сутність × симуляція
* **Фільтри:** `simulation`, `entity`, `updated_after`, а також `limit` /
  `page_token`
* **Об’єкт wire:** `reporting.simulation_result`
* **Ключі приєднання:** `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
```

Використовуйте `updated_after` як включну нижню межу водяного знака, коли оновлюєте недавні
партиції. Змінні виміри результатів перезаписуються на місці під тим самим `id`.

### simulation\_events

* **Шлях:** `GET /v1/reporting/simulation_events`
* **Деталізація:** один типізований факт на кожну зафіксовану взаємодію
* **Фільтри:** `simulation`, `entity`, `event_type`, `occurred_after`, а також
  `limit` / `page_token`
* **Об’єкт wire:** `reporting.simulation_event`
* **Типи подій:** закриті публічні токени, такі як `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — не сирі типи активностей
  ClickHouse `phishing.simulation.*`
* **Пристрій:** `device_type` — це похідний клас (серед спостережуваних токенів
  `desktop`, `mobile`, `tablet`, `unknown`). Сирий `user_agent` відсутній у
  `/v1`. Сира IP-адреса ніколи не потрапляє до `/v1` (ніколи не зберігається; лише `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
```

Використовуйте `occurred_after` для інкрементального завантаження подій. Факти лише додаються;
пізні надходження — це нові рядки.

### entities

* **Шлях:** `GET /v1/reporting/entities`
* **Деталізація:** один поточний рядок на сутність
* **Фільтри:** `type`, `updated_after`, а також `limit` / `page_token`
* **Об’єкт wire:** `reporting.entity`
* **Ключ приєднання:** `id` / `entity_id` (те саме значення)

Приєднуйте фішингові факти до осіб за допомогою
`reporting.simulation_results.entity_id = reporting.entities.id`.
Операційний `GET /v1/entities` залишається на `entity:read` і потребує іншого
дозволу.

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

Ці два набори даних мають названий виняток «лише поточні дані»:

> Авторитетної таблиці історії версій або членств не існує. Цей набір даних проєктує поточний рядок як один інтервал без кінцевої дати (`valid_to` дорівнює null, `is_current` дорівнює true). `valid_from` — це `created_at` або `updated_at` для версій сутностей і час спостереження запиту для членств у Cohort. Закриті історичні інтервали не вигадуються.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Ті самі фільтри, що й у
  entities. `valid_from` — це `created_at` або `updated_at`. Сутність,
  в якої обидві часові мітки null, пропускається.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Фільтри:
  `cohort`, `entity`, `updated_after`. `updated_at` — це останнє оновлення Cohort,
  тож інкрементальне оновлення залишається коректним. `valid_from` — це час
  спостереження запиту. Приєднуйте `entity_id` до `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
```

## Семантична модель «зірка» на /v1/activities

`/v1/activities` — це потік подій організації з часовими мітками. Він не має
числових мір — кожна величина у звіті є кількістю рядків, — що робить схему
«зірка» природним вибором.

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

* **`Activities`** — це факт події з істинною деталізацією. Один рядок на активність.
  Учасників тут вилучено й перебудовано як окрему таблицю.
* **`ActivityParticipants`** існує тому, що учасники справді
  відносяться як «один до багатьох» і мають ролі, як-от `actor` та `target`. Розгортання в
  `Activities` призвело б або до дублювання рядків активностей (що завищує кожну кількість),
  або до втрати додаткових учасників.
  Зв’язок із `Activities` — єдиний двонапрямний
  зв’язок у моделі, тому зріз за сутністю досягає таблиці фактів.
* **`Entities`** — це вимір, що називає учасників. Рядки учасників
  в активності містять `entity_id` та роль, але не відображуване ім’я.
* **`Dates`** генерується в M (без викликів API) і позначається як таблиця дат
  моделі. Саме це робить допустимою часову аналітику DAX
  (`DATESINPERIOD`, `DATEADD`). Стовпці назв місяців і днів тижня оголошують
  `sortByColumn` на своїх числових близнюках, щоб візуалізації не сортували місяці
  за алфавітом.

Усі міри явні (модель задає `discourageImplicitMeasures`): загальна кількість
активностей, активні сутності, активності на одну активну сутність, дні з активністю
та 7-денне ковзне середнє за день.

### Структура таблиці-моста

`ActivityParticipants` проходить `/v1/activities` і розгортає масив `participants`
до одного рядка на учасника:

```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` проходить `/v1/activities` незалежно, а не
посилається на запит `Activities`. У Power BI два запити, завантажені як дві
таблиці, під час оновлення отримують дані незалежно. Їхня незалежність
дає змогу службі оновлювати партиції обох таблиць паралельно.

## Інкрементальне оновлення

Обидві таблиці на рівні фактів (`Activities` і `ActivityParticipants`) оголошують
політику інкрементального оновлення Power BI: зберігати 5 років річних партицій,
повторно імпортувати останні 14 днів під час кожного оновлення.

Механізм — пара параметрів DateTime, `RangeStart` і `RangeEnd`,
які служба Power BI повторно прив’язує до кожної партиції під час оновлення. Кожен запит фактів
підставляє ці параметри у фільтри `start_date` і `end_date` API, тож кількість запитів на рівні партиції
пропорційна партиції, а не всій історії. Запит розширює вікно API на хвилину
з кожного боку та повторно застосовує точну напіввідкриту межу `[RangeStart, RangeEnd)`
локально, тож коректність партицій не залежить від того, чи сервер
трактує свої межі як включні, чи як виключні.

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

Перше оновлення після розгортання застосовує політику: повне завантаження історії,
повільне. Кожне наступне оновлення зачіпає лише інкрементальне вікно.

Виміряно на наборі даних із \~158 000 активностей і \~2 300 сутностей:

| Оновлення | Час виконання |
| - | - |
| Перше (застосування політики, повне завантаження історії) | \~31 хв |
| Кожне наступне | **\~15 с** |

<Warning>
  Класичний REST endpoint оновлення Power BI (`POST .../refreshes` лише з
  `notifyOption`) непомітно ігнорує політики оновлення й щоразу виконує повний обхід. Щоб застосувати політику
  через REST API, використовуйте розширене оновлення: надішліть
  `type`, `commitMode` і `applyRefreshPolicy` у тілі запиту. Оновлення,
  ініційовані планувальником служби Power BI, завжди застосовують політики — ця
  особливість стосується лише REST-виклику.
</Warning>

<Note>
  14-денне вікно навмисно зроблене із запасом. Завантаження активностей не є
  суто додаванням «на поточний момент»: заповнення конекторами заднім числом додає події з минулими часовими мітками.
  Вікно в 1–2 дні назавжди пропустило б їх.
</Note>

## Розширення вікна оновлення в API

`GET /v1/activities` приймає фільтри `start_date` і `end_date`, у які
підставляються параметри Power BI, фільтруючи `event_ts` між межами.
`Entities` є виміром, тому в цьому прикладі його щоразу оновлюють повністю,
а не інкрементально.

Reporting-набори даних використовують власні водяні знаки (`updated_after`,
`occurred_after`) замість вікна дат активностей.

## Ліміти запитів під час оновлення

`/v1` застосовує окремі бюджети на ключ і на IP (див.
[rate limits](/uk/developers/v1/rate-limits)). Дотримуйтеся `429` + `Retry-After`; не
закладайте жорстко бюджет запитів у вашу модель. Важливі дві властивості
оновлень Power BI:

* **Обхід однієї таблиці є послідовним.** Кожен запит сторінки потребує
  токена попередньої сторінки, тому одна таблиця обмежена затримкою незалежно від розміру
  сторінки.
* **Одночасні оновлення таблиць множаться.** Саме паралельне оновлення кількох великих таблиць
  досягає стелі, тому шлях повторних спроб 429
  у `Fn_FetchAll` має значення. Віддавайте перевагу меншій кількості паралельних таблиць,
  а не припущенню про необмежену паралельність.

Повторні спроби перевірено на реальному насиченому обмежувачі. Оновлення,
яке виконувалося, поки паралельне навантаження навмисно тримало ключ на його стелі,
все одно завершилося й дало ті самі кількості рядків, що й базова лінія без конкуренції —
повторні спроби повертали повні сторінки, без пропусків.

## Версії контракту та збережені облікові дані

`/v1` приймає заголовок запиту `Living-Security-Version`, щоб закріпити опубліковану
версію контракту (див. [API quickstart](/uk/developers/v1/quickstart)). Power
Query не може надіслати цей заголовок, коли джерело даних використовує збережені облікові дані —
власні заголовки дозволяють лише анонімні джерела даних. Додавання заголовка призводить до помилки оновлення:

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

Зберігайте ключ у збережених облікових даних Power BI й не додавайте власний заголовок.
Якщо версію в запиті явно не вказано, API використовує закріплену
`contract_version` службового облікового запису або останню опубліковану версію, якщо закріплення не задано.
Перш ніж публікувати оновлення, уточніть версію контракту облікового запису в адміністратора API.
Підтримувані версії див. у [API changelog](/uk/developers/v1/changelog).

Якщо оновлення починає поводитися неочікувано, перевірте заголовок відповіді API
`Living-Security-Version`, щоб побачити, який контракт
фактично обслужив запит.

## Оновлення та перевірка моделі

Опублікуйте семантичну модель у своїй робочій області Power BI, прив’яжіть її збережені
облікові дані до регіонального джерела `/v1` та виконайте оновлення, перш ніж вмикати
розклад. Перегляньте історію оновлень на наявність збоїв або неповного зчитування.

Звірте кількості рядків із незалежним зчитуванням API з тими самими фільтрами.
[Посібник із пагінації](/uk/developers/v1/pagination) містить приклади на Python і cURL
для зчитування повного набору результатів. Оновлення, яке зчитує лише
першу сторінку, може здаватися успішним на малому наборі даних; порівняйте кількості,
перш ніж покладатися на звіт.

## Вибір моделі

* **Підсумки кампаній і результати по кожній особі мають різну деталізацію.**
  `SimulationResults` викликає `GET /v1/simulations/{id}/results`
  (один GET на запуск). Для фішингу по кожній особі додайте запити до
  `GET /v1/reporting/simulation_results` та
  `GET /v1/reporting/simulation_events` (або операційного
  `GET /v1/simulation_results`, якщо у вас уже є `simulation:read`).
* **`vectors` у `/v1/simulations` розгортається в рядок, об’єднаний комами.**
  Стовпець типу списку не можна використовувати як справжній вимір для зрізів. Якщо сторінці звіту
  потрібна фільтрація за вектором, додайте таблицю-міст між симуляціями та векторами.

## Наступні кроки

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/uk/developers/developer-hub/overview">
    Створіть службовий обліковий запис і ключ API для Power BI.
  </Card>

  <Card title="API quickstart" href="/uk/developers/v1/quickstart">
    Автентифікуйте запити `/v1` за допомогою Basic auth.
  </Card>

  <Card title="Pagination" href="/uk/developers/v1/pagination">
    Конверт списку, токени курсора та стабільність фільтрів під час обходу.
  </Card>

  <Card title="Rate limits" href="/uk/developers/v1/rate-limits">
    Бюджети на ключ і на IP, контракт 429 та рекомендації щодо повторних спроб.
  </Card>
</CardGroup>


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