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

> Kết nối Power BI với Living Security Public API bằng Power Query, xây dựng mô hình lược đồ hình sao trên /v1 và lựa chọn tập dữ liệu vận hành hay tập dữ liệu báo cáo cho mô phỏng phishing.

Hướng dẫn này là một ví dụ trọn vẹn về việc đọc `/v1` vào Power BI. Hướng dẫn bao gồm
mẫu kết nối Power Query (M), mô hình ngữ nghĩa lược đồ hình sao được xây dựng
trên `/v1/activities`, làm mới gia tăng, và thời điểm nên dùng các tập dữ liệu xem trước
**reporting** cho kết quả phishing theo từng cá nhân.

Các ví dụ sử dụng máy chủ production tại Hoa Kỳ
(`https://api.us-east-1.livingsecurity.ai/v1`). Đối với tổ chức tại EU, hãy dùng
`https://api.eu-west-1.livingsecurity.ai/v1` ở cả `Fn_FetchAll` và
`Fn_GetJson`, sau đó liên kết thông tin xác thực của nguồn dữ liệu với máy chủ đó. Xem
[Khu vực](/vi/developers/v1/regions).

Hướng dẫn này sử dụng thông tin xác thực Basic được lưu trữ cùng với khóa API. Quý vị không cần
máy khách OAuth. Xem [Xác thực](/vi/developers/v1/authentication) để biết các lựa chọn thông tin xác thực, và
[Developer Hub](/vi/developers/developer-hub/overview)
để biết cách truy cập và thiết lập.

## Mục đích của ví dụ này

Hãy sử dụng khi quý vị cần:

* Tải các endpoint danh sách `/v1` vào tập dữ liệu Power BI với phân trang theo con trỏ chính xác và cơ chế thử lại khi gặp giới hạn tốc độ.
* Xây dựng mô hình ngữ nghĩa trong đó các thước đo ("thực thể hoạt động", "hoạt động trên mỗi
  thực thể hoạt động") mang tính chiều thay vì được đếm từ một bảng phẳng.
* Chuyển việc làm mới theo lịch của Power BI từ việc duyệt toàn bộ lịch sử xuống một
  khoảng thời gian nhỏ hằng ngày bằng cách làm mới gia tăng.
* Chọn đúng mức chi tiết cho phishing: tổng số theo chiến dịch (vận hành) so với
  kết quả theo từng cá nhân và các sự kiện có kiểu (xem trước báo cáo).

## Điều kiện tiên quyết

* Một khóa API có các quyền mà truy vấn của quý vị cần:
  * Ví dụ vận hành: `activity:read`, `entity:read` và
    `simulation:read`.
  * Phishing theo từng cá nhân thông qua các tập dữ liệu báo cáo: `reporting:read`.
    Xem [Khóa API](/vi/developers/developer-hub/api-keys).
* Power BI Desktop, hoặc một không gian làm việc Power BI / Fabric nơi quý vị có thể tạo
  mô hình ngữ nghĩa và lên lịch làm mới.

## Chọn đúng mức chi tiết

`/v1` cung cấp hai bề mặt liên quan cho phishing. Hai bề mặt này không thể thay thế cho nhau.

| Nhu cầu | Sử dụng | Quyền | Mức chi tiết |
| - | - | - | - |
| Tổng số theo phễu chiến dịch | `GET /v1/simulations/{id}/results` | `simulation:read` | Một dòng cho mỗi mô phỏng (đã gửi, đã mở, đã nhấp, …) |
| Kết quả theo từng cá nhân (vận hành) | `GET /v1/simulation_results` | `simulation:read` | Một dòng cho mỗi thực thể × mô phỏng |
| Kết quả theo từng cá nhân (BI / báo cáo) | `GET /v1/reporting/simulation_results` | `reporting:read` | Cùng mức chi tiết, tên trường BI (`entity_id`, `simulation_id`), mốc `updated_after` |
| Dữ kiện tương tác có kiểu | `GET /v1/reporting/simulation_events` | `reporting:read` | Một dữ kiện cho mỗi tương tác (`simulation.link_clicked`, …), `occurred_after` |
| Chiều Lực lượng lao động hiện tại | `GET /v1/reporting/entities` | `reporting:read` | Một dòng hiện tại cho mỗi thực thể. Nối `reporting.simulation_results.entity_id = reporting.entities.id` |
| Phiên bản thực thể hiện tại | `GET /v1/reporting/entity_versions` | `reporting:read` | Một khoảng hiện tại không giới hạn cuối cho mỗi thực thể. Không có bảng lịch sử phiên bản |
| Tư cách thành viên Nhóm hiện tại | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Một dữ kiện hiện tại cho mỗi `(cohort_id, entity_id)`. Không có bảng lịch sử tư cách thành viên |
| Trục nối cho mọi loại hoạt động | `GET /v1/activities` | `activity:read` | Một sự kiện cho mỗi hoạt động, lồng `participants[]` |

Các truy vấn ví dụ Power BI bên dưới gọi các route **vận hành**
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Đối với báo cáo phishing theo từng cá nhân, nên ưu tiên dùng nhóm
xem trước **reporting** bên dưới. Khóa chỉ được cấp `reporting:read` không thể
gọi `/v1/simulation_results` vận hành (API trả về `403`).

### Thang độ khó

Độ khó của mô phỏng là số nguyên từ 1–5 trên `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), các thuộc tính `/v1/activities`
(`difficulty_tier`) và `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` là dễ nhất và `5` là khó nhất. Các mô phỏng thích ứng và lũy tiến không có
giá trị đơn nhất ở cấp chiến dịch, do đó `/v1/simulations.difficulty_level` là `null`
trong khi `difficulty_level` theo từng người nhận trên kết quả vẫn có thể được đặt. Để ánh xạ sang
các bản xuất cũ theo mức thấp/trung bình/cao, hãy xem `1–2` là thấp, `3` là trung bình và `4–5` là
cao.

<Warning>
  Không xây dựng các tích hợp BI mới dựa trên các lệnh gọi mạng Bảng điều khiển không được ghi chép
  hoặc phần tổng quan Public API `/api/v1` được tuyển chọn cũ. Hãy xác thực với
  máy chủ `/v1` theo khu vực của tổ chức bằng khóa API của Developer Hub. Xem
  [Khu vực](/vi/developers/v1/regions).
</Warning>

## Mẫu kết nối (Power Query)

Bốn quy tắc làm nền tảng cho mọi truy vấn trong ví dụ. Mỗi quy tắc được áp dụng bởi
Power BI Service chứ không phải do sở thích về phong cách.

<AccordionGroup>
  <Accordion title="Giữ URL cơ sở của Web.Contents ở dạng tĩnh và kết thúc tại /v1">
    Đặt mọi thành phần động vào `RelativePath` và `Query`. Đối số đầu tiên của
    `Web.Contents` phải là một chuỗi literal kết thúc tại gốc `/v1`
    theo khu vực của tổ chức:

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

    Khi đó bên gọi chỉ truyền tài nguyên (`"simulations"`, không phải
    `"v1/simulations"`). Không dán máy chủ US cho tổ chức tại EU.

    Nếu đối số đầu tiên được tạo bằng cách nối chuỗi, Power BI Service
    không thể xác định tĩnh nguồn dữ liệu và việc làm mới theo lịch sẽ thất bại với
    lỗi "dynamic data source", mặc dù truy vấn vẫn hoạt động trong Power BI
    Desktop.
  </Accordion>

  <Accordion title="Cung cấp khóa API dưới dạng thông tin xác thực Basic được lưu trữ, không đặt trong văn bản truy vấn">
    Khi Power BI nhắc nhập thông tin xác thực cho nguồn dữ liệu `/v1`, hãy chọn
    **Basic** và dán khóa API vào mục **username**. Đặt một giá trị giữ chỗ
    như `x` vào trường **Mật khẩu**. Power BI Service từ chối mật khẩu
    để trống mặc dù API bỏ qua trường này. Dùng khóa làm mật khẩu với tên người dùng giữ chỗ cũng hoạt động.

    Không thêm tiêu đề `Authorization` trong M. Cách này chạy được trên máy tính của quý vị, nhưng sau đó
    nhúng khóa vào tệp truy vấn và gây xung đột với việc làm mới theo lịch.
  </Accordion>

  <Accordion title="Để bước kiểm tra kết nối của hộp thoại thông tin xác thực chạy">
    `GET /v1` yêu cầu xác thực: nó trả về `200` với khóa hợp lệ và `401`
    nếu không có khóa. Trỏ nguồn dữ liệu tới `…/v1` và **không** bỏ qua bước kiểm tra —
    khóa hợp lệ sẽ được lưu, còn khóa bị thu hồi hoặc sai định dạng sẽ thất bại ngay lập tức.

    Các ví dụ cũ dùng máy chủ trần làm nguồn dữ liệu và yêu cầu bỏ qua
    bước kiểm tra vì `GET /` luôn là `404`. Hãy trỏ lại các mô hình đó tới `…/v1`.
  </Accordion>

  <Accordion title="Đặt limit=500 một cách tường minh trên mọi truy vấn danh sách">
    Kích thước trang mặc định trên mọi endpoint danh sách `/v1` là 25. Để mặc định
    sẽ làm tăng số lượng yêu cầu cho cùng một lượng dữ liệu. Giá trị truy vấn phải là văn bản —
    `limit = "500"` trong M, không phải `limit = 500`.
  </Accordion>
</AccordionGroup>

### Hàm phân trang dùng chung

Mọi truy vấn trong ví dụ đều gọi một `Fn_FetchAll` duy nhất duyệt
`next_page_token` cho đến khi nó là `null` (xem
[phân trang](/vi/developers/v1/pagination)). Các đặc điểm đáng chú ý:

* **URL cơ sở `/v1` tĩnh, `RelativePath` và `Query` động.** Bắt buộc đối với
  việc làm mới theo lịch và bước kiểm tra thông tin xác thực.
* **`ManualStatusHandling = {429}` với số lần thử lại có giới hạn.** Hàm đọc
  `Retry-After` khi Power Query cung cấp, nếu không thì chờ 30 giây, và
  dừng sau 5 phản hồi `429` liên tiếp với một lỗi tường minh thay vì
  lặp cho đến khi việc làm mới hết thời gian. `5xx` được cố ý không
  thử lại — lỗi máy chủ cần được hiển thị, không âm thầm tạo ra một
  tập dữ liệu không đầy đủ.
* **`Timeout` 60 giây.** Các trang thường phản hồi trong chưa đầy một giây, do đó
  một phút không có phản hồi nghĩa là yêu cầu đã chết.
* **`Binary.Buffer` trước `Json.Document`.** Nếu không có bộ đệm, engine mashup
  có thể yêu cầu lại luồng phản hồi nếu đọc nó nhiều hơn một lần.
* **`baseQuery` được truyền nguyên vẹn ở mỗi lần lặp.** Mã thông báo trang là
  con trỏ có chữ ký, không trong suốt, gắn với tổ chức, thông tin xác thực, phiên bản
  hợp đồng, và dấu vân tay của bộ lọc và sắp xếp. Thay đổi bộ lọc
  giữa chừng sẽ bị từ chối như một mã thông báo không hợp lệ thay vì bắt đầu lại từ trang
  một.

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

### Truy vấn có kiểu cho từng endpoint

Mọi bên sử dụng `Fn_FetchAll` đều theo cùng một cấu trúc: gọi endpoint,
chiếu một danh sách cột cố định, loại bỏ bộ phân biệt `object`, và định kiểu
dấu thời gian là `datetimezone` để các thước đo ngày không bị lệch âm thầm theo
múi giờ của dung lượng làm mới.

<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>
  Danh sách cột tường minh trên `Table.FromRecords` giữ cho lược đồ ổn định khi
  endpoint trả về không dòng nào. Một `Table.FromRecords({})` trần tạo ra một
  bảng không có cột và mọi bước sau đó đều thất bại do thiếu các trường.
</Tip>

## Excel

Cùng Power Query (M) chạy trong Get & Transform của Excel — đây là cùng một
engine với Power BI Desktop. Hãy dùng cách này khi quý vị muốn có bảng trong worksheet
thay vì mô hình ngữ nghĩa.

Trên Excel dành cho Windows (Microsoft 365, Excel 2024, 2021, 2019 hoặc 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. Trong Power Query Editor, chọn **Home → Trình chỉnh sửa nâng cao**. Dán
   `Fn_FetchAll` trước, sau đó đổi tên truy vấn đó thành `Fn_FetchAll` (Query
   Settings → Name, hoặc nhấp chuột phải trong ngăn Queries → **Rename**).
3. Lặp lại cho `Fn_GetJson`, rồi cho từng truy vấn báo cáo.
4. **Home → Close & Load → Close & Load To…** mở hộp thoại **Import Data**.
   Chọn **Only Create Connection** cho `Fn_FetchAll` và
   `Fn_GetJson` (chúng là hàm, không phải bảng). Chọn **Table** cho
   từng truy vấn báo cáo mà quý vị muốn đưa vào worksheet.
5. Ở lần làm mới đầu tiên, chọn **Basic**: khóa API làm **User name**, một
   giá trị giữ chỗ như `x` làm **Mật khẩu**. Excel lưu một thông tin xác thực
   cho mỗi URL nguồn dữ liệu; các truy vấn sau đó truy cập cùng máy chủ `/v1` sẽ dùng lại
   thông tin đó.

<Note>
  **Close & Load To…** có thể bị mờ ở lần đầu quý vị tạo truy vấn trong
  một workbook. Hãy dùng **Close & Load**, sau đó trong Excel chọn **Data →
  Queries & Connections**, nhấp chuột phải vào truy vấn và chọn **Load To**.
  Microsoft có ghi chép đường dẫn này cho trường hợp này.
</Note>

Quý vị không thể gửi `Living-Security-Version` từ Power Query trên một
nguồn đã xác thực — cùng hạn chế như Power BI. Hãy ghim tài khoản dịch vụ
hoặc truyền `api_version` trong bản ghi truy vấn. Xem
[Phiên bản hợp đồng và thông tin xác thực được lưu trữ](#phiên-bản-hợp-đồng-và-thông-tin-xác-thực-được-lưu-trữ).

Các truy vấn báo cáo dựng sẵn (ánh xạ cột và những điểm khác biệt so với bản xuất của
nhà cung cấp trước đây):

<CardGroup cols={3}>
  <Card title="Tuân thủ theo từng cá nhân" href="/vi/developers/v1/reports/per-person-compliance">
    Các lượt ghi danh Đào tạo ở mức chi tiết người × bài giao.
  </Card>

  <Card title="Nhật ký phishing qua email" href="/vi/developers/v1/reports/email-phishing-log">
    Các tương tác phishing ở cấp sự kiện, 12 tháng gần nhất.
  </Card>

  <Card title="Người dùng dễ bị tấn công nhất" href="/vi/developers/v1/reports/most-susceptible-users">
    Tổng hợp 12 tháng theo từng cá nhân về nhấp chuột, thông tin xác thực và báo cáo.
  </Card>
</CardGroup>

## Tập dữ liệu xem trước báo cáo (phishing theo từng cá nhân)

Nhóm tập dữ liệu xem trước báo cáo cung cấp các tập dữ liệu cho phishing theo từng cá nhân.
Nhóm này yêu cầu `reporting:read` và quyền truy cập báo cáo cho tổ chức của quý vị.
Hãy dùng danh mục tập dữ liệu bên dưới để kiểm tra lược đồ và chính sách dữ liệu của chúng.

Khám phá danh mục trực tiếp:

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

Mỗi tập dữ liệu khai báo mức chi tiết, khóa chính, các phép nối, mốc, chính sách hiệu chỉnh và
chính sách xóa.

### simulation\_results

* **Đường dẫn:** `GET /v1/reporting/simulation_results`
* **Mức chi tiết:** thực thể × mô phỏng
* **Bộ lọc:** `simulation`, `entity`, `updated_after`, cộng với `limit` /
  `page_token`
* **Đối tượng wire:** `reporting.simulation_result`
* **Khóa nối:** `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
```

Hãy dùng `updated_after` làm mốc dưới bao gồm khi quý vị làm mới các phân vùng gần đây.
Các chiều kết quả có thể thay đổi sẽ được ghi lại tại chỗ dưới cùng `id`.

### simulation\_events

* **Đường dẫn:** `GET /v1/reporting/simulation_events`
* **Mức chi tiết:** một dữ kiện có kiểu cho mỗi tương tác được quan sát
* **Bộ lọc:** `simulation`, `entity`, `event_type`, `occurred_after`, cộng với
  `limit` / `page_token`
* **Đối tượng wire:** `reporting.simulation_event`
* **Loại sự kiện:** các mã thông báo công khai đã đóng như `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — không phải các loại hoạt động `phishing.simulation.*`
  ClickHouse thô
* **Thiết bị:** `device_type` là lớp được suy ra (các mã thông báo quan sát được gồm
  `desktop`, `mobile`, `tablet`, `unknown`). `user_agent` thô không có trên
  `/v1`. IP thô không bao giờ có trên `/v1` (không bao giờ được lưu trữ; chỉ có `ip_hash` đã thêm salt).

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

Hãy dùng `occurred_after` cho các lần tải sự kiện gia tăng. Các dữ kiện chỉ được nối thêm;
dữ liệu đến muộn là các dòng mới.

### entities

* **Đường dẫn:** `GET /v1/reporting/entities`
* **Mức chi tiết:** một dòng hiện tại cho mỗi thực thể
* **Bộ lọc:** `type`, `updated_after`, cộng với `limit` / `page_token`
* **Đối tượng wire:** `reporting.entity`
* **Khóa nối:** `id` / `entity_id` (cùng giá trị)

Nối các dữ kiện phishing với con người bằng
`reporting.simulation_results.entity_id = reporting.entities.id`.
`GET /v1/entities` vận hành vẫn nằm trên `entity:read` và là một
quyền khác.

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

Hai tập dữ liệu này có một ngoại lệ chỉ-hiện-tại được nêu tên:

> Không tồn tại bảng lịch sử phiên bản hoặc tư cách thành viên có thẩm quyền. Tập dữ liệu này chiếu dòng hiện tại thành một khoảng không giới hạn cuối (`valid_to` là null, `is_current` là true). `valid_from` là `created_at` hoặc nếu không thì `updated_at` đối với phiên bản thực thể, và là thời điểm quan sát yêu cầu đối với tư cách thành viên Nhóm. Các khoảng lịch sử đã đóng không được tạo ra giả.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Cùng bộ lọc với
  entities. `valid_from` là `created_at` hoặc nếu không thì `updated_at`. Một thực thể
  có cả hai dấu thời gian là null sẽ bị bỏ qua.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Bộ lọc:
  `cohort`, `entity`, `updated_after`. `updated_at` là lần cập nhật cuối của Nhóm
  để việc làm mới gia tăng luôn chính xác. `valid_from` là thời điểm
  quan sát yêu cầu. Nối `entity_id` với `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
```

## Mô hình ngữ nghĩa lược đồ hình sao trên /v1/activities

`/v1/activities` là luồng sự kiện có dấu thời gian của tổ chức. Nó không có
thước đo số nào — mọi đại lượng trong báo cáo đều là số dòng được đếm — điều này khiến lược đồ
hình sao trở nên rất phù hợp.

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

* **`Activities`** là bảng dữ kiện sự kiện ở mức chi tiết thực. Một dòng cho mỗi hoạt động.
  Người tham gia được loại bỏ ở đây và được dựng lại thành bảng riêng.
* **`ActivityParticipants`** tồn tại vì người tham gia thực sự
  là quan hệ một-nhiều, với các vai trò như `actor` và `target`. Làm phẳng vào
  `Activities` sẽ hoặc nhân đôi các dòng hoạt động (làm phồng mọi số đếm)
  hoặc loại bỏ các người tham gia bổ sung.
  Quan hệ với `Activities` là quan hệ hai chiều duy nhất của mô hình,
  nhờ đó bộ cắt lát thực thể tác động được tới bảng dữ kiện.
* **`Entities`** là chiều đặt tên cho người tham gia. Các dòng người tham gia
  trên một hoạt động mang `entity_id` và một vai trò nhưng không có tên hiển thị.
* **`Dates`** được tạo trong M (không có lệnh gọi API nào) và được đánh dấu là
  bảng ngày của mô hình. Điều đó giúp cho DAX time intelligence
  (`DATESINPERIOD`, `DATEADD`) hợp lệ. Các cột tên tháng và tên thứ trong tuần khai báo
  `sortByColumn` trên các cột số tương ứng để các hình ảnh trực quan không sắp xếp tháng
  theo thứ tự bảng chữ cái.

Tất cả thước đo đều tường minh (mô hình đặt `discourageImplicitMeasures`): tổng số
hoạt động, thực thể hoạt động, hoạt động trên mỗi thực thể hoạt động, số ngày có hoạt động,
và trung bình hằng ngày trong 7 ngày gần nhất.

### Cấu trúc bảng cầu nối

`ActivityParticipants` duyệt `/v1/activities` và bung mảng `participants`
thành một dòng cho mỗi người tham gia:

```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` duyệt `/v1/activities` một cách độc lập thay vì
tham chiếu truy vấn `Activities`. Trong Power BI, hai truy vấn được tải thành hai
bảng sẽ mỗi bảng tự lấy dữ liệu độc lập tại thời điểm làm mới. Giữ chúng độc lập
cho phép dịch vụ làm mới song song các phân vùng của cả hai bảng.

## Làm mới gia tăng

Cả hai bảng dữ kiện (`Activities` và `ActivityParticipants`) đều khai báo một
chính sách làm mới gia tăng của Power BI: giữ 5 năm phân vùng theo năm,
nhập lại 14 ngày gần nhất ở mỗi lần làm mới.

Cơ chế này là một cặp tham số DateTime, `RangeStart` và `RangeEnd`,
mà dịch vụ Power BI gán lại cho từng phân vùng tại thời điểm làm mới. Mỗi truy vấn
dữ kiện đưa các tham số đó vào các bộ lọc `start_date` và `end_date`
của API, do đó lượng yêu cầu ở cấp phân vùng tỷ lệ với phân vùng,
chứ không phải với tổng lịch sử. Truy vấn mở rộng cửa sổ API thêm một phút ở mỗi
phía và áp dụng lại ranh giới nửa mở `[RangeStart, RangeEnd)` chính xác
tại chỗ, nhờ đó tính đúng đắn của phân vùng không phụ thuộc vào việc máy chủ
coi các cận là bao gồm hay loại trừ.

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

Lần làm mới đầu tiên sau khi triển khai sẽ áp dụng chính sách: một lần tải toàn bộ lịch sử,
chậm. Mọi lần làm mới sau đó chỉ chạm tới cửa sổ gia tăng.

Được đo trên tập dữ liệu khoảng 158.000 hoạt động và khoảng 2.300 thực thể:

| Làm mới | Thời gian thực |
| - | - |
| Lần đầu (áp dụng chính sách, tải toàn bộ lịch sử) | \~31 phút |
| Mọi lần làm mới sau đó | **\~15 giây** |

<Warning>
  Endpoint REST làm mới cổ điển của Power BI (`POST .../refreshes` chỉ với
  `notifyOption`) âm thầm bỏ qua các chính sách làm mới và duyệt toàn bộ mỗi
  lần. Để áp dụng chính sách qua REST API, hãy dùng làm mới nâng cao: gửi
  `type`, `commitMode` và `applyRefreshPolicy` trong nội dung yêu cầu. Các lần làm mới
  được kích hoạt bởi bộ lập lịch của Power BI Service luôn áp dụng chính sách — điểm bất thường này
  chỉ xảy ra với lệnh gọi REST.
</Warning>

<Note>
  Cửa sổ 14 ngày được cố ý đặt rộng rãi. Việc thu nạp hoạt động không phải là
  chỉ-nối-thêm-tại-thời-điểm-hiện-tại: các lần backfill của trình kết nối đưa vào các sự kiện có dấu thời gian trong quá khứ.
  Cửa sổ 1-2 ngày sẽ bỏ sót chúng vĩnh viễn.
</Note>

## Mở rộng cửa sổ làm mới vào API

`GET /v1/activities` chấp nhận các bộ lọc `start_date` và `end_date` mà
các tham số Power BI được đưa vào, lọc `event_ts` giữa các cận.
`Entities` là một chiều, do đó ví dụ này làm mới toàn bộ nó mỗi lần
thay vì làm mới gia tăng.

Các tập dữ liệu báo cáo dùng mốc riêng của chúng (`updated_after`,
`occurred_after`) thay vì cửa sổ ngày của activities.

## Giới hạn tốc độ trong khi làm mới

`/v1` áp dụng các ngân sách riêng theo từng khóa và từng IP (xem
[giới hạn tốc độ](/vi/developers/v1/rate-limits)). Hãy tuân thủ `429` + `Retry-After`; đừng
mã hóa cứng ngân sách yêu cầu vào mô hình của quý vị. Hai đặc tính của việc làm mới Power BI
đáng lưu ý:

* **Việc duyệt một bảng diễn ra tuần tự.** Mỗi yêu cầu trang cần mã thông báo của
  trang trước, do đó một bảng bị giới hạn bởi độ trễ bất kể kích thước
  trang.
* **Việc làm mới đồng thời nhiều bảng làm tăng tải.** Nhiều bảng lớn được làm mới
  song song là trường hợp chạm trần, đó là lý do đường thử lại 429
  trong `Fn_FetchAll` quan trọng. Hãy ưu tiên ít bảng song song hơn thay vì
  giả định khả năng đồng thời không giới hạn.

Cơ chế thử lại đã được kiểm nghiệm với một bộ giới hạn thực sự bão hòa. Một lần làm mới
chạy trong khi một khối lượng công việc song song cố ý giữ khóa ở mức trần
vẫn hoàn tất và tạo ra cùng số dòng như đường cơ sở không tranh chấp —
các lần thử lại trả về các trang đầy đủ, không có khoảng trống.

## Phiên bản hợp đồng và thông tin xác thực được lưu trữ

`/v1` chấp nhận tiêu đề yêu cầu `Living-Security-Version` để ghim một phiên bản
hợp đồng đã xuất bản (xem [Khởi động nhanh API](/vi/developers/v1/quickstart)). Power
Query không thể gửi tiêu đề đó khi nguồn dữ liệu dùng thông tin xác thực được lưu trữ —
chỉ các nguồn dữ liệu ẩn danh mới cho phép tiêu đề tùy chỉnh. Việc thêm tiêu đề này khiến việc làm mới thất bại với
lỗi:

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

Hãy giữ khóa trong thông tin xác thực được lưu trữ của Power BI và bỏ qua tiêu đề tùy chỉnh.
Khi yêu cầu không có phiên bản tường minh, API dùng `contract_version` đã ghim của
tài khoản dịch vụ, hoặc phiên bản đã xuất bản mới nhất nếu không có ghim nào.
Hãy xác nhận phiên bản hợp đồng của tài khoản với quản trị viên API của quý vị trước khi
xuất bản một lần làm mới. Xem [nhật ký thay đổi API](/vi/developers/v1/changelog) để biết các
phiên bản được hỗ trợ.

Nếu một lần làm mới bắt đầu hoạt động không như mong đợi, hãy kiểm tra tiêu đề phản hồi
`Living-Security-Version` của API để xem hợp đồng nào thực sự
đã phục vụ yêu cầu.

## Làm mới và xác minh mô hình của quý vị

Xuất bản mô hình ngữ nghĩa lên không gian làm việc Power BI của quý vị, liên kết thông tin xác thực được lưu trữ
với nguồn `/v1` theo khu vực, và chạy một lần làm mới trước khi bật
lịch. Xem lại lịch sử làm mới để tìm lỗi hoặc các lần đọc không đầy đủ.

Kiểm tra số dòng so với một lần đọc API độc lập dùng cùng các bộ lọc.
[Hướng dẫn phân trang](/vi/developers/v1/pagination) có các ví dụ Python và cURL
để đọc toàn bộ tập kết quả. Một lần làm mới chỉ đọc
trang đầu tiên có thể trông như thành công trên tập dữ liệu nhỏ; hãy so sánh số đếm trước khi
tin cậy vào báo cáo.

## Lựa chọn mô hình

* **Tổng số theo chiến dịch và kết quả theo từng cá nhân có mức chi tiết khác nhau.**
  `SimulationResults` gọi `GET /v1/simulations/{id}/results`
  (một GET cho mỗi lần chạy). Đối với phishing theo từng cá nhân, hãy thêm các truy vấn cho
  `GET /v1/reporting/simulation_results` và
  `GET /v1/reporting/simulation_events` (hoặc `GET /v1/simulation_results`
  vận hành nếu quý vị đã có `simulation:read`).
* **`vectors` trên `/v1/simulations` được làm phẳng thành một chuỗi nối bằng dấu phẩy.**
  Cột kiểu danh sách không thể được dùng để cắt lát như một chiều thực sự. Nếu một trang báo cáo
  cần lọc theo vector, hãy thêm bảng cầu nối mô phỏng-vector.

## Bước tiếp theo

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/vi/developers/developer-hub/overview">
    Tạo tài khoản dịch vụ và khóa API cho Power BI.
  </Card>

  <Card title="Khởi động nhanh API" href="/vi/developers/v1/quickstart">
    Xác thực các yêu cầu `/v1` bằng Basic auth.
  </Card>

  <Card title="Phân trang" href="/vi/developers/v1/pagination">
    Phong bì danh sách, mã thông báo con trỏ và tính ổn định của bộ lọc trong suốt một lần duyệt.
  </Card>

  <Card title="Giới hạn tốc độ" href="/vi/developers/v1/rate-limits">
    Ngân sách theo từng khóa và từng IP, hợp đồng 429 và hướng dẫn thử lại.
  </Card>
</CardGroup>


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