> ## 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 Query 將 Power BI 連線至 Living Security Public API，在 /v1 上建立星狀結構描述模型，並為網路釣魚選擇作業型與報表型資料集。

本指南是將 `/v1` 讀入 Power BI 的端對端範例，內容涵蓋
Power Query (M) 連線模式、建構於 `/v1/activities` 之上的星狀結構描述語意模型、增量重新整理，以及何時使用 **reporting**
預覽資料集來取得每位人員的網路釣魚成果。

範例使用美國生產主機
(`https://api.us-east-1.livingsecurity.ai/v1`)。若為歐盟組織，請在 `Fn_FetchAll` 與
`Fn_GetJson` 中使用 `https://api.eu-west-1.livingsecurity.ai/v1`，然後將資料來源認證繫結至該主機。請參閱
[區域](/zh-Hant/developers/v1/regions)。

本指南使用搭配 API 金鑰的已儲存 Basic 認證，不需要
OAuth 用戶端。有關認證方式的選擇，請參閱[驗證](/zh-Hant/developers/v1/authentication)；有關存取與設定，請參閱 [Developer Hub](/zh-Hant/developers/developer-hub/overview)。

## 此範例的用途

在下列情況下使用：

* 以正確的游標分頁與速率限制重試機制，將 `/v1` 清單端點載入 Power BI 資料集。
* 建立語意模型，使量值（「活躍實體」、「每個活躍實體的活動數」）採用維度方式計算，而非從單一平面資料表計數。
* 透過增量重新整理，將排程的 Power BI 重新整理從完整走訪歷史資料縮減為每日的小範圍視窗。
* 選擇正確的網路釣魚粒度：活動總計（作業型）或每位人員的結果與具型別事件（報表預覽）。

## 先決條件

* 具備查詢所需權限的 API 金鑰：
  * 作業型範例：`activity:read`、`entity:read` 與
    `simulation:read`。
  * 透過報表資料集取得每位人員的網路釣魚資料：`reporting:read`。
    請參閱 [API 金鑰](/zh-Hant/developers/developer-hub/api-keys)。
* Power BI Desktop，或可建立語意模型並排程重新整理的 Power BI / Fabric 工作區。

## 選擇正確的粒度

`/v1` 為網路釣魚提供兩個相關的介面，兩者不可互換。

| 需求 | 使用 | 權限 | 粒度 |
| - | - | - | - |
| 活動漏斗總計 | `GET /v1/simulations/{id}/results` | `simulation:read` | 每個模擬一列（已遞送、已開啟、已點擊，……） |
| 每位人員的成果（作業型） | `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` |
| 目前的員工維度 | `GET /v1/reporting/entities` | `reporting:read` | 每個實體一列目前資料。聯結 `reporting.simulation_results.entity_id = reporting.entities.id` |
| 目前的實體版本 | `GET /v1/reporting/entity_versions` | `reporting:read` | 每個實體一個無終點的目前區間。不存在版本歷程資料表 |
| 目前的群組成員資格 | `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** 預覽系列。僅被授予 `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` 仍可設定。若要對應至
舊版低/中/高匯出，請將 `1–2` 視為低、`3` 視為中、`4–5` 視為
高。

<Warning>
  請勿針對未公開文件的儀表板網路呼叫，或舊版精選的 `/api/v1` Public API 概觀建立新的 BI 整合。請使用 Developer Hub API 金鑰驗證至
  組織所屬區域的 `/v1` 主機。請參閱
  [區域](/zh-Hant/developers/v1/regions)。
</Warning>

## 連線模式 (Power Query)

範例中的每個查詢都遵循四項規則，每一項皆由
Power BI 服務強制執行，而非風格偏好。

<AccordionGroup>
  <Accordion title="保持 Web.Contents 基底 URL 為靜態，並止於 /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 服務
    就無法靜態識別資料來源，排程重新整理會因
    「動態資料來源」錯誤而失敗，即使該查詢在 Power BI
    Desktop 中可正常運作。
  </Accordion>

  <Accordion title="以已儲存的 Basic 認證提供 API 金鑰，而非寫在查詢文字中">
    當 Power BI 針對 `/v1` 資料來源提示輸入認證時，請選擇
    **Basic**，並將 API 金鑰貼為**使用者名稱**。在**密碼**欄位中填入預留位置，例如
    `x`。即使 API 會忽略該欄位，Power BI 服務仍會拒絕空白
    密碼。以金鑰作為密碼、搭配預留位置使用者名稱的做法同樣可行。

    請勿在 M 中加入 `Authorization` 標頭。這在您的桌面上可行，但會將金鑰
    寫入查詢檔案，並與排程重新整理衝突。
  </Accordion>

  <Accordion title="讓認證對話方塊的連線測試執行">
    `GET /v1` 需要驗證：使用有效金鑰時傳回 `200`，
    未提供金鑰時傳回 `401`。請將資料來源指向 `…/v1`，且**不要**略過測試——
    有效的金鑰可成功儲存，已撤銷或格式錯誤的金鑰則會立即失敗。

    較舊的範例使用單純主機作為資料來源，並指示您略過
    測試，因為 `GET /` 一律為 `404`。請將這些模型重新指向 `…/v1`。
  </Accordion>

  <Accordion title="在每個清單查詢上明確設定 limit=500">
    每個 `/v1` 清單端點的預設頁面大小為 25。保留預設值
    會使相同資料的請求數量倍增。查詢值必須為文字——
    在 M 中使用 `limit = "500"`，而非 `limit = 500`。
  </Accordion>
</AccordionGroup>

### 共用分頁函式

範例中的每個查詢都呼叫單一 `Fn_FetchAll`，它會走訪
`next_page_token` 直到其為 `null`（請參閱
[分頁](/zh-Hant/developers/v1/pagination)）。其重要特性如下：

* **靜態 `/v1` 基底 URL，動態 `RelativePath` 與 `Query`。** 排程重新整理與認證測試皆需如此。
* **`ManualStatusHandling = {429}` 搭配有限次數的重試。** 此函式在 Power Query 提供時會讀取
  `Retry-After`，否則等待 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
```

### 每個端點的具型別查詢

每個 `Fn_FetchAll` 的使用者都遵循相同的形式：呼叫端點、
投影固定的欄位清單、捨棄 `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` 上使用明確的欄位清單，可在
  端點傳回零列時維持結構描述穩定。單純的 `Table.FromRecords({})` 會產生
  零欄位的資料表，之後的每個步驟都會因找不到欄位而失敗。
</Tip>

## Excel

相同的 Power Query (M) 可在 Excel 的取得與轉換中執行——它與 Power BI Desktop 使用相同的
引擎。當您需要工作表資料表而非語意模型時，請使用此方式。

在 Excel for Windows（Microsoft 365、Excel 2024、2021、2019 或 2016）上：

1. **資料 → 取得資料 → 從其他來源 → 空白查詢。**
2. 在 Power Query 編輯器中，選擇 **Home → Advanced Editor**。先貼上
   `Fn_FetchAll`，然後將該查詢重新命名為 `Fn_FetchAll`（查詢
   設定 → 名稱，或在查詢窗格中按一下滑鼠右鍵 → **重新命名**）。
3. 對 `Fn_GetJson` 重複上述步驟，然後對每個報表查詢重複。
4. **Home → Close & Load → Close & Load To…** 會開啟**匯入資料**
   對話方塊。對 `Fn_FetchAll` 與
   `Fn_GetJson` 選擇**僅建立連線**（它們是函式，不是資料表）。對
   您要放在工作表上的每個報表查詢選擇**資料表**。
5. 第一次重新整理時，選擇 **Basic**：API 金鑰作為**使用者名稱**，
   預留位置（例如 `x`）作為**密碼**。Excel 會依資料來源 URL 儲存一組認證；
   之後存取相同 `/v1` 主機的查詢會重複使用該認證。

<Note>
  在活頁簿中第一次建立查詢時，**Close & Load To…** 可能呈現灰色。
  請使用 **Close & Load**，然後在 Excel 中選擇**資料 →
  查詢與連線**，在查詢上按一下滑鼠右鍵，並選擇**載入至**。
  Microsoft 針對此情況已說明該路徑。
</Note>

在已驗證的來源上，您無法從 Power Query 傳送 `Living-Security-Version`——
限制與 Power BI 相同。請固定服務帳戶，或在查詢記錄中傳遞 `api_version`。請參閱
[合約版本與已儲存的認證](#合約版本與已儲存的認證)。

現成的報表查詢（欄位對應，以及與
先前供應商匯出內容的差異）：

<CardGroup cols={3}>
  <Card title="每位人員的合規性" href="/zh-Hant/developers/v1/reports/per-person-compliance">
    以人員 × 指派為粒度的訓練註冊資料。
  </Card>

  <Card title="電子郵件網路釣魚記錄" href="/zh-Hant/developers/v1/reports/email-phishing-log">
    事件層級的網路釣魚互動，涵蓋最近 12 個月。
  </Card>

  <Card title="最易受影響的使用者" href="/zh-Hant/developers/v1/reports/most-susceptible-users">
    每位人員 12 個月的點擊、認證與回報彙總。
  </Card>
</CardGroup>

## 報表預覽資料集（每位人員的網路釣魚）

報表預覽系列提供每位人員網路釣魚的資料集。
它需要 `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`
* **傳輸物件：** `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`
* **傳輸物件：** `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`
* **傳輸物件：** `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`；對於群組成員資格，則為請求觀察時間。不會虛構已關閉的歷史區間。

* **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` 是群組的最後更新時間，
  以確保增量重新整理保持正確。`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 服務會在重新整理時依分割區重新繫結。每個事實
查詢會將這些參數折疊至 API 的 `start_date` 與 `end_date`
過濾器中，因此分割區層級的請求量與分割區成正比，
而非與全部歷史資料成正比。查詢會將 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>
  Power BI 傳統重新整理 REST 端點（僅帶有 `notifyOption` 的 `POST .../refreshes`）會悄悄忽略重新整理政策，
  每次都進行完整走訪。若要透過 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` 是維度，因此此範例每次都完整重新整理它，
而非增量重新整理。

報表資料集使用自己的浮水印（`updated_after`、
`occurred_after`），而非活動的日期視窗。

## 重新整理期間的速率限制

`/v1` 會分別強制執行每個金鑰與每個 IP 的預算（請參閱
[速率限制](/zh-Hant/developers/v1/rate-limits)）。請遵循 `429` + `Retry-After`；
請勿將請求預算寫死在模型中。Power BI
重新整理有兩項特性值得注意：

* **單一資料表的走訪是循序的。** 每個頁面請求都需要
  前一頁的權杖，因此無論頁面大小為何，單一資料表都受延遲限制。
* **並行的資料表重新整理會成倍增加負載。** 多個大型資料表平行重新整理
  正是會觸及上限的情況，這也是 `Fn_FetchAll` 中
  429 重試路徑的重要性所在。與其假設可無限並行，不如減少平行的資料表數量。

重試機制已在真實飽和的限流器上進行過測試。在平行工作負載刻意使金鑰維持在其上限時執行的重新整理，
仍然完成，且產生的列數與無競爭的基準相同——
重試傳回的是完整頁面，沒有缺漏。

## 合約版本與已儲存的認證

`/v1` 接受 `Living-Security-Version` 請求標頭，以固定已發布的
合約版本（請參閱 [API 快速入門](/zh-Hant/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 變更記錄](/zh-Hant/developers/v1/changelog)。

若重新整理開始出現非預期行為，請檢查 API 的
`Living-Security-Version` 回應標頭，以了解實際由哪個合約
處理該請求。

## 重新整理並驗證您的模型

將語意模型發布至您的 Power BI 工作區，將其已儲存的
認證繫結至區域 `/v1` 來源，並在啟用排程之前先執行一次重新整理。請檢閱重新整理歷程記錄中是否有失敗或不完整的讀取。

請使用相同的過濾器，將列數與獨立的 API 讀取結果比對。
[分頁指南](/zh-Hant/developers/v1/pagination)包含讀取完整結果集的 Python 與 cURL
範例。僅讀取第一頁的重新整理在小型資料集上看起來可能成功；
在依賴報表之前請先比對計數。

## 模型選擇

* **活動總計與每位人員的結果具有不同的粒度。**
  `SimulationResults` 呼叫 `GET /v1/simulations/{id}/results`
  （每次執行一個 GET）。對於每位人員的網路釣魚，請為
  `GET /v1/reporting/simulation_results` 與
  `GET /v1/reporting/simulation_events` 新增查詢（若您已具備 `simulation:read`，
  也可使用作業型 `GET /v1/simulation_results`）。
* **`/v1/simulations` 上的 `vectors` 會被扁平化為以逗號連接的字串。**
  清單型別的欄位無法作為真正的維度進行交叉分析。若報表頁面
  需要依向量過濾，請新增模擬對向量的橋接資料表。

## 後續步驟

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/zh-Hant/developers/developer-hub/overview">
    為 Power BI 建立服務帳戶與 API 金鑰。
  </Card>

  <Card title="API 快速入門" href="/zh-Hant/developers/v1/quickstart">
    使用 Basic 驗證來驗證 `/v1` 請求。
  </Card>

  <Card title="分頁" href="/zh-Hant/developers/v1/pagination">
    清單封包、游標權杖，以及走訪期間過濾器的穩定性。
  </Card>

  <Card title="速率限制" href="/zh-Hant/developers/v1/rate-limits">
    每個金鑰與每個 IP 的預算、429 契約，以及重試指引。
  </Card>
</CardGroup>


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