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

> Połącz Power BI z publicznym interfejsem API Living Security za pomocą Power Query, zbuduj model w schemacie gwiazdy na /v1 i wybierz zbiory danych operacyjne lub raportowe dla phishingu.

Ten przewodnik to kompletny przykład odczytu `/v1` do Power BI. Obejmuje
wzorzec łączności w Power Query (M), model semantyczny w schemacie gwiazdy
zbudowany na `/v1/activities`, odświeżanie przyrostowe oraz informacje o tym, kiedy używać
zbiorów danych w wersji zapoznawczej **reporting** do wyników phishingu na poziomie poszczególnych osób.

W przykładach użyto hosta produkcyjnego w USA
(`https://api.us-east-1.livingsecurity.ai/v1`). W przypadku organizacji z UE użyj
`https://api.eu-west-1.livingsecurity.ai/v1` zarówno w `Fn_FetchAll`, jak i
w `Fn_GetJson`, a następnie powiąż poświadczenia źródła danych z tym hostem. Zobacz
[Regiony](/pl/developers/v1/regions).

W tym przewodniku używane są zapisane poświadczenia Basic z kluczem API. Klient OAuth nie jest potrzebny. Informacje o dostępnych opcjach poświadczeń znajdziesz w sekcji [Uwierzytelnianie](/pl/developers/v1/authentication),
a informacje o dostępie i konfiguracji w sekcji [Developer Hub](/pl/developers/developer-hub/overview).

## Do czego służy ten przykład

Skorzystaj z niego, gdy musisz:

* Załadować punkty końcowe list `/v1` do zbioru danych Power BI z poprawnym
  stronicowaniem kursorowym i ponawianiem przy przekroczeniu limitu żądań.
* Zbudować model semantyczny, w którym miary („aktywne encje”, „aktywności na
  aktywną encję”) są wymiarowe, a nie zliczane z płaskiej tabeli.
* Skrócić zaplanowane odświeżanie Power BI z pełnego przejścia całej historii do
  niewielkiego okna dziennego za pomocą odświeżania przyrostowego.
* Wybrać właściwy poziom szczegółowości dla phishingu: sumy kampanii (operacyjne)
  albo wyniki na poziomie osób i zdarzenia typowane (raportowanie w wersji zapoznawczej).

## Wymagania wstępne

* Klucz API z uprawnieniami wymaganymi przez Twoje zapytania:
  * Przykład operacyjny: `activity:read`, `entity:read` i
    `simulation:read`.
  * Phishing na poziomie osób za pomocą zbiorów danych raportowych: `reporting:read`.
    Zobacz [Klucze API](/pl/developers/developer-hub/api-keys).
* Power BI Desktop lub obszar roboczy Power BI / Fabric, w którym można utworzyć
  model semantyczny i zaplanować odświeżanie.

## Wybierz właściwy poziom szczegółowości

`/v1` udostępnia dwie powiązane powierzchnie dla phishingu. Nie są one wymienne.

| Potrzeba | Użyj | Uprawnienie | Poziom szczegółowości |
| - | - | - | - |
| Sumy lejka kampanii | `GET /v1/simulations/{id}/results` | `simulation:read` | Jeden wiersz na symulację (dostarczone, otwarte, kliknięte, …) |
| Wyniki na poziomie osób (operacyjne) | `GET /v1/simulation_results` | `simulation:read` | Jeden wiersz na encję × symulację |
| Wyniki na poziomie osób (BI / raportowanie) | `GET /v1/reporting/simulation_results` | `reporting:read` | Ten sam poziom szczegółowości, nazwy pól BI (`entity_id`, `simulation_id`), znacznik `updated_after` |
| Typowane fakty interakcji | `GET /v1/reporting/simulation_events` | `reporting:read` | Jeden fakt na interakcję (`simulation.link_clicked`, …), `occurred_after` |
| Bieżący wymiar Pracownicy | `GET /v1/reporting/entities` | `reporting:read` | Jeden bieżący wiersz na encję. Połącz po `reporting.simulation_results.entity_id = reporting.entities.id` |
| Bieżące wersje encji | `GET /v1/reporting/entity_versions` | `reporting:read` | Jeden otwarty bieżący przedział na encję. Tabela historii wersji nie istnieje |
| Bieżące członkostwa w Kohortach | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Jeden bieżący fakt na `(cohort_id, entity_id)`. Tabela historii członkostw nie istnieje |
| Oś łączenia dla wszystkich typów aktywności | `GET /v1/activities` | `activity:read` | Jedno zdarzenie na aktywność, zagnieżdżone `participants[]` |

Poniższe przykładowe zapytania Power BI wywołują trasy **operacyjne**
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). W przypadku raportowania phishingu na poziomie osób zalecamy
skorzystanie z poniższej rodziny **reporting** w wersji zapoznawczej. Klucz z uprawnieniem wyłącznie `reporting:read` nie może
wywoływać operacyjnego `/v1/simulation_results` (API zwraca `403`).

### Skala trudności

Trudność symulacji jest liczbą całkowitą od 1 do 5 we właściwościach `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), `/v1/activities`
(`difficulty_tier`) oraz `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` oznacza poziom najłatwiejszy, a `5` najtrudniejszy. Symulacje adaptacyjne i progresywne nie mają
jednej wartości na poziomie kampanii, dlatego `/v1/simulations.difficulty_level` ma wartość `null`,
natomiast `difficulty_level` dla poszczególnych odbiorców w wynikach nadal może być ustawione. Aby odwzorować
dawne eksporty z poziomami niski/średni/wysoki, potraktuj `1–2` jako niski, `3` jako średni, a `4–5` jako
wysoki.

<Warning>
  Nie buduj nowych integracji BI opartych na nieudokumentowanych wywołaniach sieciowych Pulpitu
  ani na starszym, kuratorowanym przeglądzie publicznego API `/api/v1`. Uwierzytelniaj się na
  regionalnym hoście `/v1` organizacji za pomocą klucza API z Developer Hub. Zobacz
  [Regiony](/pl/developers/v1/regions).
</Warning>

## Wzorzec łączności (Power Query)

Każde zapytanie w przykładzie opiera się na czterech regułach. Każda z nich jest wymuszana
przez usługę Power BI, a nie wynika z preferencji stylistycznych.

<AccordionGroup>
  <Accordion title="Zachowaj statyczny bazowy adres URL Web.Contents i zakończ go na /v1">
    Wszystko, co dynamiczne, umieść w `RelativePath` i `Query`. Pierwszym argumentem
    `Web.Contents` musi być literał tekstowy kończący się na głównym adresie
    regionalnego hosta `/v1` organizacji:

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

    Wywołujący przekazują wtedy samo zasobowe ścieżki (`"simulations"`, a nie
    `"v1/simulations"`). Nie wklejaj hosta amerykańskiego dla organizacji z UE.

    Jeśli pierwszy argument jest budowany przez konkatenację ciągów, usługa Power BI
    nie może statycznie zidentyfikować źródła danych, a zaplanowane odświeżanie kończy się błędem
    „dynamic data source”, mimo że zapytanie działa w Power BI Desktop.
  </Accordion>

  <Accordion title="Podaj klucz API jako zapisane poświadczenie Basic, a nie w treści zapytania">
    Gdy Power BI poprosi o poświadczenia dla źródła danych `/v1`, wybierz
    **Basic** i wklej klucz API jako **nazwę użytkownika**. W polu **Hasło** wpisz symbol zastępczy, na przykład
    `x`. Usługa Power BI odrzuca puste
    hasło, mimo że API ignoruje to pole. Klucz jako hasło z symbolem zastępczym w nazwie użytkownika również działa.

    Nie dodawaj nagłówka `Authorization` w M. Działa on na Twoim komputerze, ale
    osadza klucz w pliku zapytania i koliduje z zaplanowanym odświeżaniem.
  </Accordion>

  <Accordion title="Pozwól na wykonanie testu połączenia w oknie poświadczeń">
    `GET /v1` wymaga uwierzytelnienia: zwraca `200` z prawidłowym kluczem i `401`
    bez niego. Wskaż źródło danych na `…/v1` i **nie pomijaj** testu —
    poprawny klucz zostanie zapisany, a unieważniony lub nieprawidłowy zakończy się natychmiastowym błędem.

    Starsze przykłady używały samego hosta jako źródła danych i zalecały pominięcie
    testu, ponieważ `GET /` zawsze zwraca `404`. Zmień w tych modelach źródło na `…/v1`.
  </Accordion>

  <Accordion title="Jawnie ustaw limit=500 w każdym zapytaniu listy">
    Domyślny rozmiar strony w każdym punkcie końcowym listy `/v1` wynosi 25. Pozostawienie wartości domyślnej
    zwiększa liczbę żądań dla tych samych danych. Wartości zapytania muszą być tekstem —
    `limit = "500"` w M, a nie `limit = 500`.
  </Accordion>
</AccordionGroup>

### Współdzielona funkcja stronicowania

Każde zapytanie w przykładzie wywołuje jedną funkcję `Fn_FetchAll`, która przechodzi po
`next_page_token`, aż będzie `null` (zobacz
[stronicowanie](/pl/developers/v1/pagination)). Jej najważniejsze cechy:

* **Statyczny bazowy adres URL `/v1`, dynamiczne `RelativePath` i `Query`.** Wymagane
  dla zaplanowanego odświeżania i testu poświadczeń.
* **`ManualStatusHandling = {429}` z ograniczonym ponawianiem.** Funkcja odczytuje
  `Retry-After`, gdy Power Query je udostępnia, w przeciwnym razie czeka 30 sekund
  i rezygnuje po 5 kolejnych odpowiedziach `429`, zgłaszając jawny błąd zamiast
  działać w pętli aż do przekroczenia czasu odświeżania. `5xx` celowo nie jest
  ponawiane — błąd serwera powinien być widoczny, a nie po cichu prowadzić do niepełnego
  zbioru danych.
* **60-sekundowy `Timeout`.** Strony zwykle odpowiadają w czasie znacznie krótszym niż sekunda, więc
  minuta ciszy oznacza, że żądanie jest martwe.
* **`Binary.Buffer` przed `Json.Document`.** Bez buforowania silnik
  mashup może ponownie zażądać strumienia odpowiedzi, jeśli odczyta go więcej niż raz.
* **`baseQuery` jest przekazywany bez zmian w każdej iteracji.** Tokeny stron to
  nieprzejrzyste, podpisane kursory powiązane z organizacją, poświadczeniem, wersją
  kontraktu oraz odciskiem filtrów i sortowania. Zmiana filtra
  w trakcie przechodzenia jest odrzucana jako nieprawidłowy token, a nie restartowana od pierwszej
  strony.

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

### Typowane zapytania dla każdego punktu końcowego

Każdy konsument `Fn_FetchAll` stosuje ten sam schemat: wywołuje punkt końcowy,
rzutuje stałą listę kolumn, usuwa dyskryminator `object` i typuje
znaczniki czasu jako `datetimezone`, aby miary dat nie przesuwały się po cichu o strefę czasową
pojemności odświeżania.

<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>
  Jawna lista kolumn w `Table.FromRecords` utrzymuje stabilny schemat, gdy
  punkt końcowy zwraca zero wierszy. Samo `Table.FromRecords({})` daje
  tabelę bez kolumn, a wszystkie kolejne kroki kończą się błędem z powodu brakujących pól.
</Tip>

## Excel

To samo zapytanie Power Query (M) działa w funkcji Pobieranie i przekształcanie w Excelu — to ten sam
silnik co w Power BI Desktop. Użyj tego, gdy zamiast modelu semantycznego potrzebujesz tabeli w arkuszu.

W Excelu dla systemu Windows (Microsoft 365, Excel 2024, 2021, 2019 lub 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. W Power Query Editor wybierz **Strona główna → Zaawansowany edytor**. Najpierw wklej
   `Fn_FetchAll`, a następnie zmień nazwę tego zapytania na `Fn_FetchAll` (Query
   Settings → Name lub kliknij prawym przyciskiem myszy w okienku Queries → **Rename**).
3. Powtórz dla `Fn_GetJson`, a następnie dla każdego zapytania raportu.
4. **Strona główna → Close & Load → Close & Load To…** otwiera okno **Import Data**.
   Wybierz **Only Create Connection** dla `Fn_FetchAll` i
   `Fn_GetJson` (są to funkcje, a nie tabele). Wybierz **Table** dla
   każdego zapytania raportu, które ma trafić do arkusza.
5. Przy pierwszym odświeżeniu wybierz **Basic**: klucz API jako **User name**, a
   symbol zastępczy, na przykład `x`, jako **Hasło**. Excel zapisuje jedno poświadczenie
   na adres URL źródła danych; kolejne zapytania korzystające z tego samego hosta `/v1` ponownie go używają.

<Note>
  Opcja **Close & Load To…** może być wyszarzona przy pierwszym utworzeniu zapytania w
  skoroszycie. Użyj **Close & Load**, a następnie w Excelu wybierz **Data →
  Queries & Connections**, kliknij zapytanie prawym przyciskiem myszy i wybierz **Load To**.
  Ta ścieżka jest opisana przez firmę Microsoft dla tego przypadku.
</Note>

Nie możesz wysłać `Living-Security-Version` z Power Query w
uwierzytelnionym źródle — obowiązuje to samo ograniczenie co w Power BI. Przypnij konto usługi
lub przekaż `api_version` w rekordzie zapytania. Zobacz
[Wersje kontraktu i zapisane poświadczenia](#wersje-kontraktu-i-zapisane-poświadczenia).

Gotowe zapytania raportów (mapowania kolumn i różnice względem eksportu
poprzedniego dostawcy):

<CardGroup cols={3}>
  <Card title="Zgodność na poziomie osoby" href="/pl/developers/v1/reports/per-person-compliance">
    Zapisy na Szkolenie na poziomie osoba × przypisanie.
  </Card>

  <Card title="Dziennik phishingu e-mail" href="/pl/developers/v1/reports/email-phishing-log">
    Interakcje phishingowe na poziomie zdarzeń, ostatnie 12 miesięcy.
  </Card>

  <Card title="Najbardziej podatni użytkownicy" href="/pl/developers/v1/reports/most-susceptible-users">
    Zestawienie 12-miesięczne kliknięć, poświadczeń i zgłoszeń na poziomie osoby.
  </Card>
</CardGroup>

## Zbiory danych raportowych w wersji zapoznawczej (phishing na poziomie osób)

Rodzina raportowa w wersji zapoznawczej udostępnia zbiory danych dla phishingu na poziomie osób.
Wymaga `reporting:read` oraz dostępu do raportowania dla Twojej organizacji.
Skorzystaj z poniższego katalogu zbiorów danych, aby sprawdzić ich schematy i zasady dotyczące danych.

Wyświetl aktualny katalog:

```text theme={null}
GET /v1/reporting/datasets
GET /v1/reporting/datasets/simulation_results
GET /v1/reporting/datasets/simulation_events
GET /v1/reporting/datasets/entities
GET /v1/reporting/datasets/entity_versions
GET /v1/reporting/datasets/cohort_memberships
```

Każdy zbiór danych określa poziom szczegółowości, klucz główny, złączenia, znacznik, korekty i
zasady usuwania.

### simulation\_results

* **Ścieżka:** `GET /v1/reporting/simulation_results`
* **Poziom szczegółowości:** encja × symulacja
* **Filtry:** `simulation`, `entity`, `updated_after` oraz `limit` /
  `page_token`
* **Obiekt w komunikacji:** `reporting.simulation_result`
* **Klucze złączeń:** `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
```

Użyj `updated_after` jako włącznej dolnej granicy znacznika podczas odświeżania ostatnich
partycji. Zmienne wymiary wyników są aktualizowane w miejscu pod tym samym `id`.

### simulation\_events

* **Ścieżka:** `GET /v1/reporting/simulation_events`
* **Poziom szczegółowości:** jeden typowany fakt na zaobserwowaną interakcję
* **Filtry:** `simulation`, `entity`, `event_type`, `occurred_after` oraz
  `limit` / `page_token`
* **Obiekt w komunikacji:** `reporting.simulation_event`
* **Typy zdarzeń:** zamknięty zbiór tokenów publicznych, takich jak `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — nie surowe
  typy aktywności ClickHouse `phishing.simulation.*`
* **Urządzenie:** `device_type` to klasa pochodna (obserwowane tokeny to m.in.
  `desktop`, `mobile`, `tablet`, `unknown`). Surowy `user_agent` nie występuje w
  `/v1`. Surowy adres IP nigdy nie występuje w `/v1` (nie jest przechowywany; tylko solony `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
```

Użyj `occurred_after` do przyrostowego ładowania zdarzeń. Fakty są wyłącznie dopisywane;
spóźnione dane trafiają jako nowe wiersze.

### entities

* **Ścieżka:** `GET /v1/reporting/entities`
* **Poziom szczegółowości:** jeden bieżący wiersz na encję
* **Filtry:** `type`, `updated_after` oraz `limit` / `page_token`
* **Obiekt w komunikacji:** `reporting.entity`
* **Klucz złączenia:** `id` / `entity_id` (ta sama wartość)

Połącz fakty phishingowe z osobami za pomocą
`reporting.simulation_results.entity_id = reporting.entities.id`.
Operacyjny `GET /v1/entities` pozostaje w `entity:read` i wymaga innego
uprawnienia.

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

Te dwa zbiory danych mają nazwany wyjątek dotyczący wyłącznie stanu bieżącego:

> Nie istnieje autorytatywna tabela historii wersji ani członkostw. Ten zbiór danych przedstawia bieżący wiersz jako jeden otwarty przedział (`valid_to` ma wartość null, `is_current` ma wartość true). `valid_from` to `created_at`, a w przeciwnym razie `updated_at` dla wersji encji oraz czas obserwacji żądania dla członkostw w kohortach. Zamknięte przedziały historyczne nie są tworzone sztucznie.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Te same filtry co w
  entities. `valid_from` to `created_at`, a w przeciwnym razie `updated_at`. Encja,
  w której oba znaczniki czasu mają wartość null, jest pomijana.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtry:
  `cohort`, `entity`, `updated_after`. `updated_at` to ostatnia aktualizacja Kohorty,
  dzięki czemu odświeżanie przyrostowe pozostaje wiarygodne. `valid_from` to czas obserwacji żądania.
  Połącz `entity_id` z `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
```

## Model semantyczny w schemacie gwiazdy na /v1/activities

`/v1/activities` to opatrzony znacznikami czasu strumień zdarzeń organizacji. Nie zawiera
miar liczbowych — każda wielkość w raporcie jest liczbą wierszy — co sprawia, że schemat gwiazdy
jest naturalnym wyborem.

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

* **`Activities`** to tabela faktów zdarzeń na rzeczywistym poziomie szczegółowości. Jeden wiersz na aktywność.
  Uczestnicy są tu usuwani i odtwarzani jako osobna tabela.
* **`ActivityParticipants`** istnieje, ponieważ uczestnicy faktycznie występują
  w relacji jeden-do-wielu, z rolami takimi jak `actor` i `target`. Spłaszczenie do
  `Activities` albo zduplikowałoby wiersze aktywności (zawyżając każdą liczbę),
  albo pominęłoby dodatkowych uczestników.
  Relacja z `Activities` jest jedyną relacją dwukierunkową w modelu,
  dzięki czemu fragmentator encji sięga do tabeli faktów.
* **`Entities`** to wymiar nazywający uczestników. Wiersze uczestników
  w aktywności zawierają `entity_id` i rolę, ale nie nazwę wyświetlaną.
* **`Dates`** jest generowana w M (zero wywołań API) i oznaczona jako tabela dat modelu.
  Dzięki temu funkcje DAX time intelligence
  (`DATESINPERIOD`, `DATEADD`) są dozwolone. Kolumny z nazwami miesięcy i dni tygodnia deklarują
  `sortByColumn` na swoich odpowiednikach liczbowych, aby wizualizacje nie sortowały miesięcy
  alfabetycznie.

Wszystkie miary są jawne (model ustawia `discourageImplicitMeasures`): łączna liczba
aktywności, aktywne encje, aktywności na aktywną encję, dni z aktywnością
oraz 7-dniowa krocząca średnia dzienna.

### Kształt tabeli pomostowej

`ActivityParticipants` przechodzi po `/v1/activities` i rozwija tablicę `participants`
do jednego wiersza na uczestnika:

```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` przechodzi po `/v1/activities` niezależnie, zamiast
odwoływać się do zapytania `Activities`. W Power BI dwa zapytania załadowane jako dwie
tabele pobierają dane niezależnie w czasie odświeżania. Zachowanie ich niezależności
pozwala usłudze odświeżać partycje obu tabel równolegle.

## Odświeżanie przyrostowe

Obie tabele na poziomie faktów (`Activities` i `ActivityParticipants`) deklarują
zasadę odświeżania przyrostowego Power BI: przechowuj 5 lat partycji rocznych,
ponownie importuj ostatnie 14 dni przy każdym odświeżeniu.

Mechanizm opiera się na parze parametrów DateTime, `RangeStart` i `RangeEnd`,
które usługa Power BI ponownie wiąże dla każdej partycji w czasie odświeżania. Każde zapytanie faktów
przekłada te parametry na filtry API `start_date` i `end_date`,
więc liczba żądań na partycję jest proporcjonalna do partycji, a nie do całej historii. Zapytanie poszerza okno API o minutę
z każdej strony i lokalnie ponownie stosuje dokładną półotwartą granicę `[RangeStart, RangeEnd)`,
dzięki czemu poprawność partycji nie zależy od tego, czy serwer
traktuje swoje granice jako włączne, czy wyłączne.

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

Pierwsze odświeżenie po wdrożeniu stosuje zasadę: pełne ładowanie historyczne,
powolne. Każde kolejne odświeżenie obejmuje tylko okno przyrostowe.

Zmierzono na zbiorze danych obejmującym ok. 158 000 aktywności i ok. 2300 encji:

| Odświeżenie | Czas rzeczywisty |
| - | - |
| Pierwsze (zastosowanie zasady, pełne ładowanie historyczne) | ok. 31 min |
| Każde kolejne | **ok. 15 s** |

<Warning>
  Klasyczny punkt końcowy REST odświeżania Power BI (`POST .../refreshes` tylko z
  `notifyOption`) po cichu ignoruje zasady odświeżania i za każdym razem wykonuje pełne przejście. Aby zastosować zasadę przez REST API, użyj odświeżania rozszerzonego: wyślij
  `type`, `commitMode` i `applyRefreshPolicy` w treści żądania. Odświeżenia
  uruchamiane przez harmonogram usługi Power BI zawsze stosują zasady — ta
  osobliwość dotyczy wyłącznie wywołania REST.
</Warning>

<Note>
  14-dniowe okno jest celowo szerokie. Pobieranie aktywności nie polega wyłącznie na dopisywaniu bieżących danych:
  uzupełnienia z konektorów wprowadzają zdarzenia ze znacznikami czasu z przeszłości.
  Okno 1–2 dni trwale by je pominęło.
</Note>

## Poszerzanie okna odświeżania w API

`GET /v1/activities` akceptuje filtry `start_date` i `end_date`, na które przekładane są
parametry Power BI, filtrując `event_ts` pomiędzy granicami.
`Entities` jest wymiarem, dlatego w tym przykładzie jest odświeżany w całości za każdym razem,
a nie przyrostowo.

Zbiory danych raportowych używają własnych znaczników (`updated_after`,
`occurred_after`) zamiast okna dat aktywności.

## Limity żądań podczas odświeżania

`/v1` egzekwuje oddzielne budżety dla klucza i adresu IP (zobacz
[limity żądań](/pl/developers/v1/rate-limits)). Przestrzegaj `429` + `Retry-After`; nie
zapisuj na stałe budżetu żądań w modelu. Istotne są dwie właściwości odświeżania
w Power BI:

* **Przejście pojedynczej tabeli jest sekwencyjne.** Każde żądanie strony wymaga
  tokenu poprzedniej strony, więc pojedyncza tabela jest ograniczona opóźnieniem niezależnie od rozmiaru
  strony.
* **Równoległe odświeżanie tabel mnoży obciążenie.** Przypadek, w którym dochodzi do osiągnięcia limitu, to równoczesne odświeżanie kilku dużych tabel,
  dlatego ścieżka ponawiania 429 w `Fn_FetchAll` ma znaczenie. Lepiej ograniczyć liczbę równoległych tabel, niż
  zakładać nieograniczoną współbieżność.

Mechanizm ponawiania został sprawdzony na rzeczywiście nasyconym ogranicznik limitu. Odświeżenie,
które działało, gdy równoległe obciążenie celowo utrzymywało klucz na jego górnej granicy,
nadal się zakończyło i dało te same liczby wierszy co punkt odniesienia bez rywalizacji —
ponowienia zwracały kompletne strony, bez luk.

## Wersje kontraktu i zapisane poświadczenia

`/v1` akceptuje nagłówek żądania `Living-Security-Version` służący do przypięcia opublikowanej
wersji kontraktu (zobacz [Szybki start API](/pl/developers/v1/quickstart)). Power
Query nie może wysłać tego nagłówka, gdy źródło danych używa zapisanych poświadczeń —
tylko anonimowe źródła danych dopuszczają nagłówki niestandardowe. Dodanie go powoduje błąd odświeżania:

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

Przechowuj klucz w zapisanych poświadczeniach Power BI i pomiń nagłówek niestandardowy.
Gdy w żądaniu nie podano jawnej wersji, API używa wersji przypiętej do konta usługi
`contract_version` lub najnowszej opublikowanej wersji, jeśli nie ustawiono przypięcia.
Przed publikacją odświeżania potwierdź wersję kontraktu konta u administratora API. Obsługiwane wersje znajdziesz w [dzienniku zmian API](/pl/developers/v1/changelog).

Jeśli odświeżanie zacznie zachowywać się nieoczekiwanie, sprawdź nagłówek odpowiedzi API
`Living-Security-Version`, aby zobaczyć, który kontrakt faktycznie
obsłużył żądanie.

## Odśwież i zweryfikuj model

Opublikuj model semantyczny w obszarze roboczym Power BI, powiąż jego zapisane
poświadczenie z regionalnym źródłem `/v1` i uruchom odświeżenie przed włączeniem
harmonogramu. Przejrzyj historię odświeżania pod kątem błędów lub niepełnych odczytów.

Porównaj liczby wierszy z niezależnym odczytem API przy użyciu tych samych filtrów.
[Przewodnik po stronicowaniu](/pl/developers/v1/pagination) zawiera przykłady w Pythonie i cURL
dotyczące odczytu kompletnego zbioru wyników. Odświeżenie, które odczytuje tylko
pierwszą stronę, może wyglądać na udane w małym zbiorze danych; porównaj liczby, zanim zaczniesz polegać na raporcie.

## Wybory dotyczące modelu

* **Sumy kampanii i wyniki na poziomie osób mają różny poziom szczegółowości.**
  `SimulationResults` wywołuje `GET /v1/simulations/{id}/results`
  (jedno GET na uruchomienie). W przypadku phishingu na poziomie osób dodaj zapytania do
  `GET /v1/reporting/simulation_results` i
  `GET /v1/reporting/simulation_events` (lub operacyjnego
  `GET /v1/simulation_results`, jeśli masz już `simulation:read`).
* **`vectors` w `/v1/simulations` jest spłaszczone do ciągu połączonego przecinkami.**
  Kolumny typu lista nie można używać do fragmentowania jako prawdziwego wymiaru. Jeśli strona raportu
  wymaga filtrowania według wektora, dodaj tabelę pomostową symulacja–wektor.

## Dalsze kroki

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/pl/developers/developer-hub/overview">
    Utwórz konto usługi i klucz API dla Power BI.
  </Card>

  <Card title="Szybki start API" href="/pl/developers/v1/quickstart">
    Uwierzytelniaj żądania `/v1` za pomocą uwierzytelniania Basic.
  </Card>

  <Card title="Stronicowanie" href="/pl/developers/v1/pagination">
    Koperta listy, tokeny kursora i stabilność filtrów w trakcie przechodzenia.
  </Card>

  <Card title="Limity żądań" href="/pl/developers/v1/rate-limits">
    Budżety dla klucza i adresu IP, kontrakt 429 oraz wskazówki dotyczące ponawiania.
  </Card>
</CardGroup>


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