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

> Conecte Power BI a la API pública de Living Security con Power Query, cree un modelo de esquema en estrella sobre /v1 y elija entre conjuntos de datos operativos y de informes para phishing.

Esta guía es un ejemplo integral de cómo leer `/v1` en Power BI. Abarca
el patrón de conectividad de Power Query (M), un modelo semántico de esquema en estrella creado
sobre `/v1/activities`, la actualización incremental y cuándo usar los conjuntos de datos
de vista previa de **reporting** para los resultados de phishing por persona.

Los ejemplos usan el host de producción de EE. UU.
(`https://api.us-east-1.livingsecurity.ai/v1`). Para una organización de la UE, utilice
`https://api.eu-west-1.livingsecurity.ai/v1` tanto en `Fn_FetchAll` como en
`Fn_GetJson` y, a continuación, vincule la credencial del origen de datos a ese host. Consulte
[Regiones](/es/developers/v1/regions).

Esta guía utiliza credenciales Basic almacenadas con una clave de API. No necesita
un cliente OAuth. Consulte [Autenticación](/es/developers/v1/authentication) para conocer
las opciones de credenciales y [Developer Hub](/es/developers/developer-hub/overview)
para el acceso y la configuración.

## Para qué sirve este ejemplo

Úselo cuando necesite:

* Cargar los endpoints de lista de `/v1` en un conjunto de datos de Power BI con paginación por cursor correcta y reintento ante límites de velocidad.
* Crear un modelo semántico en el que las medidas ("entidades activas", "actividades por
  entidad activa") sean dimensionales en lugar de contarse a partir de una tabla plana.
* Reducir una actualización programada de Power BI, de un recorrido completo del historial a
  una pequeña ventana diaria, mediante la actualización incremental.
* Elegir la granularidad correcta de phishing: totales de campaña (operativos) frente a
  resultados por persona y eventos tipificados (vista previa de informes).

## Requisitos previos

* Una clave de API con los permisos que necesiten sus consultas:
  * Ejemplo operativo: `activity:read`, `entity:read` y
    `simulation:read`.
  * Phishing por persona mediante conjuntos de datos de informes: `reporting:read`.
    Consulte [Claves de API](/es/developers/developer-hub/api-keys).
* Power BI Desktop, o un área de trabajo de Power BI / Fabric donde pueda crear un
  modelo semántico y programar la actualización.

## Elija la granularidad correcta

`/v1` expone dos superficies relacionadas para phishing. No son intercambiables.

| Necesidad | Uso | Permiso | Granularidad |
| - | - | - | - |
| Totales del embudo de la campaña | `GET /v1/simulations/{id}/results` | `simulation:read` | Una fila por simulación (entregados, abiertos, con clic, …) |
| Resultados por persona (operativos) | `GET /v1/simulation_results` | `simulation:read` | Una fila por entidad × simulación |
| Resultados por persona (BI / informes) | `GET /v1/reporting/simulation_results` | `reporting:read` | Misma granularidad, nombres de campo de BI (`entity_id`, `simulation_id`), marca de agua `updated_after` |
| Hechos de interacción tipificados | `GET /v1/reporting/simulation_events` | `reporting:read` | Un hecho por interacción (`simulation.link_clicked`, …), `occurred_after` |
| Dimensión actual de fuerza laboral | `GET /v1/reporting/entities` | `reporting:read` | Una fila actual por entidad. Combine con `reporting.simulation_results.entity_id = reporting.entities.id` |
| Versiones actuales de entidades | `GET /v1/reporting/entity_versions` | `reporting:read` | Un intervalo actual abierto por entidad. No existe tabla de historial de versiones |
| Membresías actuales de cohorte | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Un hecho actual por `(cohort_id, entity_id)`. No existe tabla de historial de membresías |
| Eje de unión para todos los tipos de actividad | `GET /v1/activities` | `activity:read` | Un evento por actividad, con `participants[]` anidado |

Las consultas de ejemplo de Power BI que se muestran más abajo llaman a las rutas **operativas**
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Para los informes de phishing por persona, prefiera la
familia de vista previa de **reporting** que se describe más abajo. Una clave a la que solo se haya concedido `reporting:read` no puede
llamar a `/v1/simulation_results` operativo (la API devuelve `403`).

### Escala de dificultad

La dificultad de la simulación es un entero de 1 a 5 en `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), las propiedades de `/v1/activities`
(`difficulty_tier`) y `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` es la más fácil y `5` la más difícil. Las simulaciones adaptativas y progresivas no tienen
un único valor a nivel de campaña, por lo que `/v1/simulations.difficulty_level` es `null`,
mientras que la `difficulty_level` por destinatario en los resultados aún puede establecerse. Para equiparar con
las exportaciones heredadas de nivel bajo/medio/alto, considere `1–2` como bajo, `3` como medio y `4–5` como
alto.

<Warning>
  No cree nuevas integraciones de BI sobre llamadas de red del Panel de control no documentadas
  ni sobre la descripción general heredada y seleccionada de la API pública `/api/v1`. Autentíquese en el
  host `/v1` regional de la organización con una clave de API de Developer Hub. Consulte
  [Regiones](/es/developers/v1/regions).
</Warning>

## Patrón de conectividad (Power Query)

Cuatro reglas sustentan cada consulta del ejemplo. Cada una la impone
el servicio de Power BI, no son preferencias de estilo.

<AccordionGroup>
  <Accordion title="Mantenga estática la URL base de Web.Contents y haga que termine en /v1">
    Coloque todo lo dinámico en `RelativePath` y `Query`. El primer argumento de
    `Web.Contents` debe ser una cadena literal que termine en la raíz `/v1` regional de la organización:

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

    Los llamadores pasan únicamente el recurso (`"simulations"`, no
    `"v1/simulations"`). No pegue un host de EE. UU. para una organización de la UE.

    Si el primer argumento se construye por concatenación de cadenas, el servicio de Power BI
    no puede identificar estáticamente el origen de datos y la actualización programada falla con
    un error de "origen de datos dinámico", aunque la consulta funcione en Power BI
    Desktop.
  </Accordion>

  <Accordion title="Proporcione la clave de API como una credencial Basic almacenada, no en el texto de la consulta">
    Cuando Power BI solicite credenciales para el origen de datos `/v1`, elija
    **Basic** y pegue la clave de API como **username**. Escriba un marcador de posición, como
    `x`, en el campo **password**. El servicio de Power BI rechaza una contraseña
    en blanco aunque la API ignore ese campo. También funciona usar la clave como contraseña con un nombre de usuario de marcador de posición.

    No agregue un encabezado `Authorization` en M. Funciona en su escritorio, pero
    incrusta la clave en el archivo de la consulta y entra en conflicto con la actualización programada.
  </Accordion>

  <Accordion title="Permita que se ejecute la prueba de conexión del cuadro de diálogo de credenciales">
    `GET /v1` está autenticado: devuelve `200` con una clave válida y `401`
    sin ella. Apunte el origen de datos a `…/v1` y **no** omita la prueba:
    una clave válida se guarda, mientras que una revocada o con formato incorrecto falla de inmediato.

    Los ejemplos anteriores usaban el host sin ruta como origen de datos e indicaban omitir
    la prueba porque `GET /` siempre es `404`. Vuelva a apuntar esos modelos a `…/v1`.
  </Accordion>

  <Accordion title="Establezca limit=500 de forma explícita en cada consulta de lista">
    El tamaño de página predeterminado de cada endpoint de lista de `/v1` es 25. Dejarlo en el valor predeterminado
    multiplica el número de solicitudes para los mismos datos. Los valores de consulta deben ser texto:
    `limit = "500"` en M, no `limit = 500`.
  </Accordion>
</AccordionGroup>

### Función de paginación compartida

Cada consulta del ejemplo llama a una única `Fn_FetchAll` que recorre
`next_page_token` hasta que es `null` (consulte
[paginación](/es/developers/v1/pagination)). Sus propiedades más destacadas:

* **URL base de `/v1` estática, `RelativePath` y `Query` dinámicos.** Necesario para
  la actualización programada y para la prueba de credenciales.
* **`ManualStatusHandling = {429}` con reintento acotado.** La función lee
  `Retry-After` cuando Power Query lo expone; de lo contrario, espera 30 segundos y
  se rinde tras 5 respuestas `429` consecutivas con un error explícito, en lugar de
  repetirse hasta que la actualización agote el tiempo de espera. `5xx` deliberadamente no se
  reintenta: un error del servidor debe hacerse visible, no producir en silencio un conjunto de datos
  parcial.
* **`Timeout` de 60 segundos.** Las páginas suelen responder en bastante menos de un segundo, por lo que
  un minuto de silencio significa que la solicitud está muerta.
* **`Binary.Buffer` antes de `Json.Document`.** Sin el almacenamiento en búfer, el motor
  de mashup puede volver a solicitar el flujo de respuesta si lo lee más de una vez.
* **`baseQuery` se pasa sin cambios en cada iteración.** Los tokens de página son
  cursores opacos y firmados, vinculados a la organización, la credencial, la versión del
  contrato y la huella de los filtros y la ordenación. Cambiar un filtro
  a mitad del recorrido se rechaza como token no válido en lugar de reiniciarse desde la
  primera página.

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

### Consultas tipificadas por endpoint

Todo consumidor de `Fn_FetchAll` sigue la misma forma: llama al endpoint,
proyecta una lista fija de columnas, elimina el discriminador `object` y
tipifica las marcas de tiempo como `datetimezone` para que las medidas de fecha no se desplacen en silencio por la
zona horaria de la capacidad de actualización.

<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>
  Una lista explícita de columnas en `Table.FromRecords` mantiene estable el esquema cuando
  un endpoint devuelve cero filas. Un `Table.FromRecords({})` sin más produce una
  tabla sin columnas y todos los pasos posteriores fallan por los campos ausentes.
</Tip>

## Excel

El mismo Power Query (M) se ejecuta en Get & Transform de Excel, ya que es el mismo
motor que Power BI Desktop. Úselo cuando quiera una tabla de hoja de cálculo
en lugar de un modelo semántico.

En Excel para Windows (Microsoft 365, Excel 2024, 2021, 2019 o 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. En Power Query Editor, **Home → Advanced Editor**. Pegue
   primero `Fn_FetchAll` y luego cambie el nombre de esa consulta a `Fn_FetchAll` (Query
   Settings → Name, o clic derecho en el panel Queries → **Rename**).
3. Repita el proceso para `Fn_GetJson` y, después, para cada consulta de informe.
4. **Home → Close & Load → Close & Load To…** abre el cuadro de diálogo **Import Data**.
   Elija **Only Create Connection** para `Fn_FetchAll` y
   `Fn_GetJson` (son funciones, no tablas). Elija **Table** para
   cada consulta de informe que quiera en una hoja de cálculo.
5. En la primera actualización, elija **Basic**: la clave de API como **User name** y un
   marcador de posición, como `x`, como **Password**. Excel almacena una credencial
   por URL de origen de datos; las consultas posteriores que lleguen al mismo host `/v1` la reutilizan.

<Note>
  **Close & Load To…** puede aparecer atenuado la primera vez que crea una consulta en
  un libro. Use **Close & Load** y, a continuación, en Excel seleccione **Data →
  Queries & Connections**, haga clic con el botón derecho en la consulta y elija **Load To**.
  Microsoft documenta esa vía para este caso.
</Note>

No puede enviar `Living-Security-Version` desde Power Query en un origen
autenticado: es la misma restricción que en Power BI. Fije la cuenta de servicio
o pase `api_version` en el registro de la consulta. Consulte
[Versiones de contrato y credenciales almacenadas](#versiones-de-contrato-y-credenciales-almacenadas).

Consultas de informe listas para usar (asignaciones de columnas y diferencias con la exportación de un
proveedor anterior):

<CardGroup cols={3}>
  <Card title="Cumplimiento por persona" href="/es/developers/v1/reports/per-person-compliance">
    Inscripciones de capacitación con granularidad persona × asignación.
  </Card>

  <Card title="Registro de phishing por correo electrónico" href="/es/developers/v1/reports/email-phishing-log">
    Interacciones de phishing a nivel de evento, últimos 12 meses.
  </Card>

  <Card title="Usuarios más susceptibles" href="/es/developers/v1/reports/most-susceptible-users">
    Resumen por persona de clics, credenciales y reportes de los últimos 12 meses.
  </Card>
</CardGroup>

## Conjuntos de datos de vista previa de informes (phishing por persona)

La familia de vista previa de informes proporciona conjuntos de datos para el phishing por persona.
Requiere `reporting:read` y acceso a informes para su organización.
Utilice el catálogo de conjuntos de datos que se muestra más abajo para inspeccionar sus esquemas y políticas de datos.

Descubra el catálogo activo:

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

Cada conjunto de datos declara su granularidad, clave principal, uniones, marca de agua, y política de
corrección y eliminación.

### simulation\_results

* **Ruta:** `GET /v1/reporting/simulation_results`
* **Granularidad:** entidad × simulación
* **Filtros:** `simulation`, `entity`, `updated_after`, además de `limit` /
  `page_token`
* **Objeto de transmisión:** `reporting.simulation_result`
* **Claves de unión:** `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
```

Utilice `updated_after` como marca de agua inferior inclusiva cuando actualice particiones
recientes. Las dimensiones mutables de los resultados se restablecen en su lugar bajo el mismo `id`.

### simulation\_events

* **Ruta:** `GET /v1/reporting/simulation_events`
* **Granularidad:** un hecho tipificado por cada interacción observada
* **Filtros:** `simulation`, `entity`, `event_type`, `occurred_after`, además de
  `limit` / `page_token`
* **Objeto de transmisión:** `reporting.simulation_event`
* **Tipos de evento:** tokens públicos cerrados, como `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported`; no son los tipos de actividad
  `phishing.simulation.*` sin procesar de ClickHouse
* **Dispositivo:** `device_type` es la clase derivada (los tokens observados incluyen
  `desktop`, `mobile`, `tablet`, `unknown`). `user_agent` sin procesar no está en
  `/v1`. La IP sin procesar nunca está en `/v1` (nunca se conserva; solo `ip_hash` con sal).

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

Utilice `occurred_after` para las cargas incrementales de eventos. Los hechos son de solo adición;
las llegadas tardías son filas nuevas.

### entities

* **Ruta:** `GET /v1/reporting/entities`
* **Granularidad:** una fila actual por entidad
* **Filtros:** `type`, `updated_after`, además de `limit` / `page_token`
* **Objeto de transmisión:** `reporting.entity`
* **Clave de unión:** `id` / `entity_id` (el mismo valor)

Una los hechos de phishing con las personas mediante
`reporting.simulation_results.entity_id = reporting.entities.id`.
`GET /v1/entities` operativo permanece en `entity:read` y es un permiso
distinto.

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

Estos dos conjuntos de datos incluyen una excepción con nombre de solo estado actual:

> No existe una tabla autorizada de historial de versiones o membresías. Este conjunto de datos proyecta la fila actual como un único intervalo abierto (`valid_to` es nulo, `is_current` es verdadero). `valid_from` es `created_at` o, en su defecto, `updated_at` en el caso de las versiones de entidades, y la hora de observación de la solicitud en el caso de las membresías de cohorte. No se fabrican intervalos históricos cerrados.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Los mismos filtros que
  entities. `valid_from` es `created_at` o, en su defecto, `updated_at`. Se omite una entidad
  con ambas marcas de tiempo nulas.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtros:
  `cohort`, `entity`, `updated_after`. `updated_at` es la última actualización de la cohorte,
  de modo que la actualización incremental siga siendo fiable. `valid_from` es la hora de
  observación de la solicitud. Una `entity_id` con `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
```

## Modelo semántico de esquema en estrella sobre /v1/activities

`/v1/activities` es el flujo de eventos con marca de tiempo de la organización. No tiene
medidas numéricas: toda cantidad de un informe es un recuento de filas, lo que hace que un esquema
en estrella sea una opción natural.

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

* **`Activities`** es el hecho de evento con la granularidad real. Una fila por actividad.
  Los participantes se eliminan aquí y se reconstruyen como su propia tabla.
* **`ActivityParticipants`** existe porque los participantes son realmente
  de uno a varios, con roles como `actor` y `target`. Aplanarlos en
  `Activities` duplicaría las filas de actividad (inflando todos los recuentos)
  o descartaría a los participantes adicionales.
  La relación con `Activities` es la única relación bidireccional del modelo,
  de modo que una segmentación por entidad llega a la tabla de hechos.
* **`Entities`** es la dimensión que nombra a los participantes. Las filas de participantes
  de una actividad llevan `entity_id` y un rol, pero no un nombre para mostrar.
* **`Dates`** se genera en M (cero llamadas a la API) y se marca como la tabla de fechas
  del modelo. Eso es lo que hace válida la inteligencia de tiempo de DAX
  (`DATESINPERIOD`, `DATEADD`). Las columnas de nombre de mes y de día de la semana declaran
  `sortByColumn` en sus equivalentes numéricos para que las visualizaciones no ordenen los meses
  alfabéticamente.

Todas las medidas son explícitas (el modelo establece `discourageImplicitMeasures`): total de
actividades, entidades activas, actividades por entidad activa, días con actividad
y un promedio diario móvil de 7 días.

### Forma de la tabla puente

`ActivityParticipants` recorre `/v1/activities` y expande la matriz `participants`
a una fila por participante:

```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` recorre `/v1/activities` de forma independiente en lugar de
hacer referencia a la consulta `Activities`. En Power BI, dos consultas cargadas como dos
tablas se obtienen cada una de forma independiente en el momento de la actualización. Mantenerlas independientes
permite que el servicio actualice las particiones de ambas tablas en paralelo.

## Actualización incremental

Ambas tablas con granularidad de hechos (`Activities` y `ActivityParticipants`) declaran una
directiva de actualización incremental de Power BI: conservar 5 años de particiones anuales y
reimportar los últimos 14 días en cada actualización.

El mecanismo es un par de parámetros DateTime, `RangeStart` y `RangeEnd`,
que el servicio de Power BI revincula por partición en el momento de la actualización. Cada consulta de hechos
incorpora esos parámetros a los filtros `start_date` y `end_date`
de la API, de modo que el volumen de solicitudes por partición es proporcional a la partición,
no al historial total. La consulta amplía la ventana de la API en un minuto a cada
lado y vuelve a aplicar localmente el límite semiabierto exacto de `[RangeStart, RangeEnd)`,
de modo que la corrección de las particiones no depende de si el servidor
trata sus límites como inclusivos o exclusivos.

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

La primera actualización tras una implementación aplica la directiva: una carga histórica completa,
lenta. Todas las actualizaciones posteriores solo tocan la ventana incremental.

Medido en un conjunto de datos de \~158 000 actividades y \~2300 entidades:

| Actualización | Tiempo total |
| - | - |
| Primera (aplicación de la directiva, carga histórica completa) | \~31 min |
| Todas las posteriores | **\~15 s** |

<Warning>
  El endpoint REST de actualización clásica de Power BI (`POST .../refreshes` con solo
  `notifyOption`) ignora en silencio las directivas de actualización y hace un recorrido completo
  cada vez. Para aplicar la directiva a través de la API REST, utilice la actualización mejorada: envíe
  `type`, `commitMode` y `applyRefreshPolicy` en el cuerpo. Las actualizaciones
  activadas por el programador del servicio de Power BI siempre aplican las directivas; esta
  peculiaridad es específica de la llamada REST.
</Warning>

<Note>
  La ventana de 14 días es deliberadamente generosa. La ingesta de actividad no es
  de solo adición en el momento actual: los rellenos retroactivos de los conectores incorporan eventos con marcas de tiempo pasadas.
  Una ventana de 1 a 2 días los perdería de forma permanente.
</Note>

## Ampliación de la ventana de actualización en la API

`GET /v1/activities` acepta los filtros `start_date` y `end_date` en los que se
incorporan los parámetros de Power BI, filtrando `event_ts` entre los límites.
`Entities` es una dimensión, por lo que este ejemplo la actualiza por completo cada vez
en lugar de forma incremental.

Los conjuntos de datos de informes usan sus propias marcas de agua (`updated_after`,
`occurred_after`) en lugar de la ventana de fechas de activities.

## Límites de velocidad durante la actualización

`/v1` aplica presupuestos independientes por clave y por IP (consulte
[límites de velocidad](/es/developers/v1/rate-limits)). Respete `429` + `Retry-After`; no
codifique de forma fija un presupuesto de solicitudes en su modelo. Dos propiedades de las actualizaciones
de Power BI son relevantes:

* **El recorrido de una sola tabla es secuencial.** Cada solicitud de página necesita el
  token de la página anterior, por lo que una tabla queda limitada por la latencia independientemente del tamaño de
  página.
* **Las actualizaciones simultáneas de tablas se multiplican.** Varias tablas grandes que se actualizan
  en paralelo es el caso que alcanza el límite máximo, y por eso importa la ruta de reintento ante 429
  de `Fn_FetchAll`. Prefiera menos tablas en paralelo en lugar de
  asumir una concurrencia ilimitada.

El reintento se ha probado contra un limitador real saturado. Una actualización
que se ejecutó mientras una carga de trabajo paralela mantenía deliberadamente la clave en su límite máximo
se completó igualmente y produjo los mismos recuentos de filas que una línea base sin contención:
los reintentos devolvieron páginas completas, sin huecos.

## Versiones de contrato y credenciales almacenadas

`/v1` acepta un encabezado de solicitud `Living-Security-Version` para fijar una versión
de contrato publicada (consulte la [guía de inicio rápido de la API](/es/developers/v1/quickstart)). Power
Query no puede enviar ese encabezado cuando el origen de datos usa credenciales almacenadas;
solo los orígenes de datos anónimos permiten encabezados personalizados. Agregarlo hace fallar una actualización
con:

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

Conserve la clave en las credenciales almacenadas de Power BI y omita el encabezado personalizado.
Sin una versión explícita en la solicitud, la API usa el `contract_version` fijado de la cuenta de servicio
o la última versión publicada si no hay ninguna fijada.
Confirme la versión de contrato de la cuenta con el administrador de su API antes de
publicar una actualización. Consulte el [registro de cambios de la API](/es/developers/v1/changelog) para
conocer las versiones compatibles.

Si una actualización empieza a comportarse de forma inesperada, inspeccione el encabezado de respuesta
`Living-Security-Version` de la API para ver qué contrato atendió
realmente la solicitud.

## Actualice y verifique su modelo

Publique el modelo semántico en su área de trabajo de Power BI, vincule su credencial
almacenada al origen `/v1` regional y ejecute una actualización antes de habilitar una
programación. Revise el historial de actualizaciones en busca de fallos o lecturas incompletas.

Compare los recuentos de filas con una lectura independiente de la API que use los mismos filtros.
La [guía de paginación](/es/developers/v1/pagination) incluye ejemplos en Python y cURL
para leer un conjunto de resultados completo. Una actualización que lee solo la
primera página puede parecer correcta en un conjunto de datos pequeño; compare los recuentos antes de
confiar en el informe.

## Decisiones de modelado

* **Los totales de campaña y los resultados por persona tienen granularidades distintas.**
  `SimulationResults` llama a `GET /v1/simulations/{id}/results`
  (un GET por ejecución). Para el phishing por persona, agregue consultas para
  `GET /v1/reporting/simulation_results` y
  `GET /v1/reporting/simulation_events` (o `GET /v1/simulation_results`
  operativo si ya dispone de `simulation:read`).
* **`vectors` en `/v1/simulations` se aplana a una cadena unida por comas.**
  Una columna de tipo lista no puede segmentarse como una dimensión real. Si una página de informe
  necesita filtrar por vector, agregue una tabla puente de simulación a vector.

## Pasos siguientes

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/es/developers/developer-hub/overview">
    Cree una cuenta de servicio y una clave de API para Power BI.
  </Card>

  <Card title="Inicio rápido de la API" href="/es/developers/v1/quickstart">
    Autentique las solicitudes de `/v1` con autenticación Basic.
  </Card>

  <Card title="Paginación" href="/es/developers/v1/pagination">
    El envoltorio de lista, los tokens de cursor y la estabilidad de los filtros a lo largo de un recorrido.
  </Card>

  <Card title="Límites de velocidad" href="/es/developers/v1/rate-limits">
    Presupuestos por clave y por IP, el contrato 429 y pautas de reintento.
  </Card>
</CardGroup>


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