> ## 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`) を使用しています。EU の組織の場合は、`Fn_FetchAll` と `Fn_GetJson` の両方で `https://api.eu-west-1.livingsecurity.ai/v1` を使用し、データソースの資格情報をそのホストにバインドしてください。[リージョン](/ja/developers/v1/regions)を参照してください。

このガイドでは、API キーを使用して保存された Basic 資格情報を利用します。OAuth クライアントは必要ありません。資格情報の選択肢については[認証](/ja/developers/v1/authentication)を、アクセスとセットアップについては [Developer Hub](/ja/developers/developer-hub/overview) を参照してください。

## この例の用途

次のような場合にご利用ください。

* 正しいカーソルページネーションとレート制限時の再試行を用いて、`/v1` の一覧エンドポイントを Power BI データセットに読み込む。
* 「アクティブなエンティティ」や「アクティブなエンティティあたりのアクティビティ数」などのメジャーを、フラットなテーブルの件数集計ではなく、ディメンションを用いて構築するセマンティックモデルを作成する。
* 増分更新を使用して、スケジュールされた Power BI の更新を、履歴全体の走査から小さな日次ウィンドウへと短縮する。
* 適切なフィッシングの粒度を選択する。キャンペーンの合計 (運用) か、個人単位の結果と型付きイベント (レポートプレビュー) かを選びます。

## 前提条件

* クエリに必要な権限を持つ API キー:
  * 運用の例: `activity:read`、`entity:read`、`simulation:read`。
  * レポートデータセットによる個人単位のフィッシング: `reporting:read`。
    [API キー](/ja/developers/developer-hub/api-keys)を参照してください。
* Power BI Desktop、またはセマンティックモデルを作成して更新をスケジュールできる Power BI / Fabric ワークスペース。

## 適切な粒度の選択

`/v1` は、フィッシングに関して関連する 2 つのサーフェスを公開しています。これらは互換ではありません。

| 必要なもの | 使用するもの | 権限 | 粒度 |
| - | - | - | - |
| キャンペーンファネルの合計 | `GET /v1/simulations/{id}/results` | `simulation:read` | シミュレーションごとに 1 行 (配信、開封、クリックなど) |
| 個人単位の成果 (運用) | `GET /v1/simulation_results` | `simulation:read` | エンティティ × シミュレーションごとに 1 行 |
| 個人単位の成果 (BI / レポート) | `GET /v1/reporting/simulation_results` | `reporting:read` | 同じ粒度、BI フィールド名 (`entity_id`、`simulation_id`)、`updated_after` ウォーターマーク |
| 型付きインタラクションファクト | `GET /v1/reporting/simulation_events` | `reporting:read` | インタラクションごとに 1 ファクト (`simulation.link_clicked` など)、`occurred_after` |
| 現在の従業員ディメンション | `GET /v1/reporting/entities` | `reporting:read` | エンティティごとに現在の 1 行。`reporting.simulation_results.entity_id = reporting.entities.id` を結合します |
| 現在のエンティティバージョン | `GET /v1/reporting/entity_versions` | `reporting:read` | エンティティごとに終了日のない現在の区間が 1 つ。バージョン履歴テーブルは存在しません |
| 現在のコホートメンバーシップ | `GET /v1/reporting/cohort_memberships` | `reporting:read` | `(cohort_id, entity_id)` ごとに現在のファクトが 1 つ。メンバーシップ履歴テーブルは存在しません |
| すべてのアクティビティタイプの結合スパイン | `GET /v1/activities` | `activity:read` | アクティビティごとに 1 イベント、`participants[]` がネストされます |

以下の Power BI のクエリ例では、**運用**ルート (`/v1/simulations`、`/v1/simulations/{id}/results`、`/v1/activities`、`/v1/entities`) を呼び出します。個人単位のフィッシングレポートには、以下の **reporting** プレビューファミリーを使用することを推奨します。`reporting:read` のみが付与されたキーでは、運用の `/v1/simulation_results` を呼び出すことはできません (API は `403` を返します)。

### 難易度スケール

シミュレーションの難易度は、`/v1/simulations` (`difficulty_level`)、`/v1/simulation_results` (`difficulty_level`)、`/v1/activities` プロパティ (`difficulty_tier`)、および `/v1/reporting/simulation_events` (`difficulty_tier`) 上の 1〜5 の整数です。`1` が最も易しく、`5` が最も難しいレベルです。アダプティブおよびプログレッシブのシミュレーションにはキャンペーンレベルの単一の値がないため、`/v1/simulations.difficulty_level` は `null` になりますが、結果上の受信者ごとの `difficulty_level` は設定される場合があります。従来の low/medium/high のエクスポートに対応付けるには、`1–2` を low、`3` を medium、`4–5` を high として扱ってください。

<Warning>
  ドキュメント化されていないダッシュボードのネットワーク呼び出しや、従来の厳選された `/api/v1` Public API の概要に対して、新しい BI 連携を構築しないでください。Developer Hub の API キーを使用して、組織のリージョンの `/v1` ホストに対して認証してください。[リージョン](/ja/developers/v1/regions)を参照してください。
</Warning>

## 接続パターン (Power Query)

この例のすべてのクエリは、4 つのルールに基づいています。それぞれ、スタイル上の好みではなく、Power BI サービスによって強制されるものです。

<AccordionGroup>
  <Accordion title="Web.Contents のベース URL は静的にし、/v1 で終わらせる">
    動的な部分はすべて `RelativePath` と `Query` に含めてください。`Web.Contents` の最初の引数は、組織のリージョンの `/v1` ルートで終わるリテラル文字列である必要があります。

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

    呼び出し側はリソースのみを渡します (`"v1/simulations"` ではなく `"simulations"`)。EU の組織に US のホストを貼り付けないでください。

    最初の引数を文字列連結で組み立てると、Power BI サービスはデータソースを静的に特定できず、クエリが Power BI Desktop で動作していても、スケジュールされた更新が「動的データソース」エラーで失敗します。
  </Accordion>

  <Accordion title="API キーはクエリテキストではなく、保存された Basic 資格情報として指定する">
    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>

### 共通のページネーション関数

この例のすべてのクエリは、`null` になるまで `next_page_token` を走査する単一の `Fn_FetchAll` を呼び出します ([ページネーション](/ja/developers/v1/pagination)を参照)。主な特長は次のとおりです。

* **静的な `/v1` ベース URL と、動的な `RelativePath` および `Query`。** スケジュールされた更新と資格情報のテストに必要です。
* **上限付き再試行を伴う `ManualStatusHandling = {429}`。** この関数は、Power Query が公開している場合は `Retry-After` を読み取り、公開していない場合は 30 秒待機します。`429` レスポンスが 5 回連続した場合は、更新がタイムアウトするまでループするのではなく、明示的なエラーで終了します。`5xx` は意図的に再試行されません。サーバーエラーは、部分的なデータセットを黙って生成するのではなく、表面化させる必要があるためです。
* **60 秒の `Timeout`。** ページは通常 1 秒未満で応答するため、1 分間応答がない場合はリクエストが失敗していることを意味します。
* **`Json.Document` の前に `Binary.Buffer`。** バッファリングしない場合、マッシュアップエンジンがレスポンスストリームを複数回読み取ると、再リクエストすることがあります。
* **`baseQuery` は反復ごとに変更せずに渡されます。** ページトークンは、組織、資格情報、コントラクトバージョン、およびフィルターと並べ替えのフィンガープリントに紐付けられた、不透明で署名付きのカーソルです。走査の途中でフィルターを変更すると、1 ページ目からやり直されるのではなく、無効なトークンとして拒否されます。

```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` で列リストを明示すると、エンドポイントが 0 行を返した場合でもスキーマが安定します。単なる `Table.FromRecords({})` では列が 0 のテーブルになり、以降のすべてのステップで存在しないフィールドが原因で失敗します。
</Tip>

## Excel

同じ Power Query (M) は、Excel の「データの取得と変換」でも実行できます。Power BI Desktop と同じエンジンです。セマンティックモデルではなく、ワークシートのテーブルが必要な場合に使用してください。

Windows 版 Excel (Microsoft 365、Excel 2024、2021、2019、または 2016) の場合:

1. **データ → データの取得 → その他のデータ ソースから → 空のクエリ**を選択します。
2. Power Query エディターで、**ホーム → アドバンストエディター**を選択します。最初に `Fn_FetchAll` を貼り付け、そのクエリの名前を `Fn_FetchAll` に変更します (クエリ設定 → 名前、または \[クエリ] ペインで右クリックして **名前の変更**)。
3. `Fn_GetJson`、続いて各レポートクエリについても同様に繰り返します。
4. **ホーム → 閉じて読み込む → 閉じて次に読み込む…**を選択すると、**データのインポート**ダイアログが開きます。`Fn_FetchAll` と `Fn_GetJson` (これらはテーブルではなく関数です) には**接続の作成のみ**を選択します。ワークシートに表示したい各レポートクエリには**テーブル**を選択します。
5. 最初の更新時に **Basic** を選択し、API キーを**ユーザー名**に、`x` などのプレースホルダーを**パスワード**に入力します。Excel はデータソース URL ごとに 1 つの資格情報を保存します。同じ `/v1` ホストにアクセスする以降のクエリは、それを再利用します。

<Note>
  ブック内で初めてクエリを作成するとき、**閉じて次に読み込む…**がグレー表示になることがあります。その場合は**閉じて読み込む**を使用し、Excel で**データ → クエリと接続**を選択して、クエリを右クリックし、**読み込み先**を選択してください。このケースについては Microsoft がこの手順を案内しています。
</Note>

認証付きソースでは、Power Query から `Living-Security-Version` を送信できません。これは Power BI と同じ制限です。サービスアカウントを固定するか、クエリレコードで `api_version` を渡してください。[コントラクトバージョンと保存された資格情報](#コントラクトバージョンと保存された資格情報)を参照してください。

すぐに使用できるレポートクエリ (列マッピングと、以前のプロバイダーのエクスポートとの相違点):

<CardGroup cols={3}>
  <Card title="個人別コンプライアンス" href="/ja/developers/v1/reports/per-person-compliance">
    個人 × 割り当ての粒度でのトレーニング受講登録。
  </Card>

  <Card title="メールフィッシングログ" href="/ja/developers/v1/reports/email-phishing-log">
    イベント単位のフィッシングインタラクション (直近 12 か月)。
  </Card>

  <Card title="最も影響を受けやすいユーザー" href="/ja/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`
* **粒度:** 観測されたインタラクションごとに 1 つの型付きファクト
* **フィルター:** `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`
* **粒度:** エンティティごとに現在の 1 行
* **フィルター:** `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

これら 2 つのデータセットには、現在の状態のみを提供するという例外が明示されています。

> 権威あるバージョン履歴テーブルまたはメンバーシップ履歴テーブルは存在しません。このデータセットは、現在の行を終了日のない 1 つの区間として投影します (`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`** は、実際の粒度でのイベントファクトです。アクティビティごとに 1 行です。参加者はここから除外され、独自のテーブルとして再構築されます。
* **`ActivityParticipants`** が存在するのは、参加者が `actor` や `target` などのロールを持つ、本質的に 1 対多の関係にあるためです。`Activities` 上でフラット化すると、アクティビティの行が重複して (すべてのカウントが水増しされて) しまうか、追加の参加者が失われてしまいます。`Activities` へのリレーションシップは、このモデルで唯一の双方向リレーションシップであり、エンティティのスライサーがファクトテーブルに到達できるようにします。
* **`Entities`** は、参加者に名前を付けるディメンションです。アクティビティ上の参加者行には `entity_id` とロールはありますが、表示名はありません。
* **`Dates`** は M で生成され (API 呼び出しはゼロ)、モデルの日付テーブルとして指定されます。これにより、DAX のタイムインテリジェンス (`DATESINPERIOD`、`DATEADD`) が使用可能になります。月名と曜日名の列は、ビジュアルで月がアルファベット順に並ばないよう、対応する数値列に `sortByColumn` を宣言しています。

すべてのメジャーは明示的です (モデルは `discourageImplicitMeasures` を設定しています)。アクティビティ合計、アクティブなエンティティ、アクティブなエンティティあたりのアクティビティ数、アクティビティのあった日数、および 7 日間の末尾日次平均があります。

### ブリッジテーブルの構造

`ActivityParticipants` は `/v1/activities` を走査し、`participants` 配列を参加者ごとに 1 行へ展開します。

```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` は、`Activities` クエリを参照するのではなく、`/v1/activities` を独立して走査します。Power BI では、2 つのテーブルとして読み込まれた 2 つのクエリは、更新時にそれぞれ独立して取得を行います。独立させておくことで、サービスは両方のテーブルのパーティションを並列に更新できます。

## 増分更新

2 つのファクト粒度のテーブル (`Activities` と `ActivityParticipants`) は、どちらも Power BI の増分更新ポリシーを宣言しています。年単位のパーティションを 5 年分保持し、更新のたびに直近 14 日分を再インポートします。

この仕組みは、`RangeStart` と `RangeEnd` という 1 組の DateTime パラメーターを使用します。Power BI サービスは、更新時にパーティションごとにこれらを再バインドします。各ファクトクエリは、これらのパラメーターを API の `start_date` および `end_date` フィルターに反映させるため、パーティション単位のリクエスト量は、履歴全体ではなくパーティションに比例します。このクエリは、API のウィンドウを両側に 1 分ずつ広げ、半開区間 `[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` は、Power BI パラメーターが反映される `start_date` および `end_date` フィルターを受け付け、`event_ts` を境界の間でフィルタリングします。`Entities` はディメンションであるため、この例では増分ではなく毎回全件を更新します。

レポートデータセットでは、アクティビティの日付ウィンドウではなく、独自のウォーターマーク (`updated_after`、`occurred_after`) を使用します。

## 更新中のレート制限

`/v1` は、キーごとと IP ごとに別々の予算を適用します ([レート制限](/ja/developers/v1/rate-limits)を参照)。`429` + `Retry-After` に従い、リクエスト予算をモデルにハードコードしないでください。Power BI の更新に関して重要な点が 2 つあります。

* **単一のテーブルの走査は逐次的です。** 各ページリクエストには前のページのトークンが必要なため、ページサイズに関係なく、1 つのテーブルはレイテンシに制約されます。
* **テーブルの同時更新は負荷を増大させます。** 複数の大きなテーブルを並列に更新すると上限に達するため、`Fn_FetchAll` の 429 再試行パスが重要になります。無制限の同時実行を前提とするのではなく、並列テーブル数を抑えることを推奨します。

この再試行は、実際に飽和したリミッターに対して検証されています。並列のワークロードが意図的にキーを上限に保っている間に実行された更新も、問題なく完了し、競合のないベースラインと同じ行数が得られました。再試行では、欠落のない完全なページが返されました。

## コントラクトバージョンと保存された資格情報

`/v1` は、公開されているコントラクトバージョンを固定するための `Living-Security-Version` リクエストヘッダーを受け付けます ([API クイックスタート](/ja/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 変更履歴](/ja/developers/v1/changelog)を参照してください。

更新の動作が予期しないものになった場合は、API の `Living-Security-Version` レスポンスヘッダーを確認して、実際にどのコントラクトがリクエストに応答したかを確認してください。

## モデルの更新と検証

セマンティックモデルを Power BI ワークスペースに公開し、保存された資格情報をリージョンの `/v1` ソースにバインドして、スケジュールを有効にする前に更新を実行してください。更新履歴を確認し、失敗や不完全な読み取りがないかを確認します。

同じフィルターを使用した独立した API の読み取り結果と、行数を照合してください。[ページネーションガイド](/ja/developers/v1/pagination)には、完全な結果セットを読み取るための Python と cURL の例が記載されています。最初のページのみを読み取る更新は、小さなデータセットでは成功したように見えることがあります。レポートを信頼する前に件数を比較してください。

## モデルの選択

* **キャンペーンの合計と個人単位の結果では、粒度が異なります。** `SimulationResults` は `GET /v1/simulations/{id}/results` を呼び出します (実行ごとに 1 回の 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="/ja/developers/developer-hub/overview">
    Power BI 用のサービスアカウントと API キーを作成します。
  </Card>

  <Card title="API クイックスタート" href="/ja/developers/v1/quickstart">
    Basic 認証で `/v1` リクエストを認証します。
  </Card>

  <Card title="ページネーション" href="/ja/developers/v1/pagination">
    一覧のエンベロープ、カーソルトークン、走査中のフィルターの安定性。
  </Card>

  <Card title="レート制限" href="/ja/developers/v1/rate-limits">
    キーごとおよび IP ごとの予算、429 の契約、再試行のガイダンス。
  </Card>
</CardGroup>


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