---
title: "Ejecutar consulta analítica flexible"
method: POST
path: "/analytics/query"
tags: ["Query"]
---

# Ejecutar consulta analítica flexible

`POST /analytics/query`

## Request

- Base URL: `http://localhost:8081`
- URL: `http://localhost:8081/analytics/query`
- Auth: HTTP bearer

Ejecuta consultas analíticas flexibles con agregaciones y filtros dinámicos.

El frontend puede definir:
- **Dimensiones**: Campos para agrupar (GROUP BY)
- **Métricas**: Campos para agregar (SUM, COUNT, AVG, etc.)
- **Filtros**: Condiciones WHERE dinámicas
- **Granularidad temporal**: Agregación por día, semana, mes, etc.
- **Ordenamiento**: Campos y dirección
- **Límites**: Número máximo de resultados

Todas las consultas incluyen automáticamente filtrado por tenant.

### Dry Run (estimación de coste)

Añade `?dryRun=true` para ejecutar un **dry run nativo de BigQuery** que valida
la sintaxis SQL y estima los bytes que se procesarían, **sin ejecutar la query**
(coste = $0). Ideal para que el frontend muestre estimaciones de coste antes de
ejecutar queries pesadas.

- Rate limit independiente: **200 req/min** (no consume cuota de queries normales)
- Precio estimado: **$6.25/TB** (BigQuery on-demand pricing)
- La respuesta incluye el SQL generado para depuración

## Query parameters

- `dryRun` 'true' | '1'

## Request body

- QueryRequest
  - `collection` string, required: Nombre de la vista/tabla base a consultar. Debe ser una tabla de la lista blanca de la Analytics API (la que publica `GET /analytics/capabilities`). Cualquier otra es un 400 `INVALID_REQUEST` antes de consultar BigQuery, también en `dryRun` y en `/analytics/export`; el mensaje no repite el nombre.
  - `ctes` CTE[]: **Phase 4 - CTEs (Common Table Expressions)**: Nested aggregations sin hardcoded SQL. Permite queries complejas con agregaciones multi-nivel. Máximo 3 CTEs por query. **Casos de uso**: - Group size distribution (count tickets per purchase, then count purchases per group size) - Cohort analysis (first purchase date, then analyze by cohort) - Top performers (rank by metric, then aggregate ranked results)
    - `name` string, required: Nombre del CTE (alphanumeric + underscores only). Debe ser único dentro de la query. Máximo 3 CTEs.
    - `collection` string, required: Tabla del mart (debe estar en la whitelist). Un CTE que lee otro CTE no está soportado: 400 `INVALID_REQUEST`. El `sourceTable` de la query principal sí puede ser un CTE.
    - `dimensions` Dimension[]: Dimensiones para el CTE (GROUP BY)
      - `field` string, required: Nombre del campo para agrupar
      - `alias` string: Alias para el campo en los resultados
    - `metrics` Metric[]: Métricas para el CTE (agregaciones)
      - `field` string, required: Nombre del campo para agregar. Usar "*" para operaciones COUNT.
      - `operation` 'sum' | 'avg' | 'count' | 'min' | 'max' | 'count_distinct' | 'median' | 'percentile_25' | 'percentile_50' | 'percentile_75' | 'percentile_90' | 'percentile_95' | 'sum_if' | 'avg_if' | 'count_if' | 'string_agg' | 'approx_top_count' | 'date_diff' | 'date_add' | 'date_sub', required: Operaciones disponibles para métricas: ### Básicas - **sum**: Suma de valores numéricos - **avg**: Promedio de valores numéricos - **count**: Conteo de registros (usar field="*") - **min**: Valor mínimo - **max**: Valor máximo ### Agregaciones Avanzadas (Fase 3) - **count_distinct**: Conteo de valores únicos. Requiere field específico (no "*") - **median**: Mediana aproximada (percentil 50) usando APPROX_QUANTILES - **percentile_25/50/75/90/95**: Percentiles específicos aproximados - **approx_top_count**: Top N valores más frecuentes. Acepta params.limit (default: 10, max: 1000) - **string_agg**: Concatenar strings con delimitador. Acepta params.delimiter (default: ",") ### Agregaciones Condicionales - **sum_if**: Suma donde se cumple condición. Requiere field `condition` - **avg_if**: Promedio donde se cumple condición. Requiere field `condition` - **count_if**: Conteo donde se cumple condición. Requiere field `condition` ### Phase 4 - Date Arithmetic - **date_diff**: Calcula `compareField − field` en la unidad pedida: `DATE_DIFF(DATE(compareField), DATE(field), unit)`. Requiere params: `compareField`, `unit` (DAY, WEEK, MONTH, QUARTER, YEAR; HOUR y MINUTE responden 400 porque la operación es sobre fechas). Para «días hasta el evento» desde la creación, `field` es `created_at` y `compareField` es `start_date`. Es una agregación: devuelve `AVG` de la diferencia por grupo por defecto; `params.aggregationFunction` admite `AVG`, `SUM`, `MIN` o `MAX`. - **date_add**: Suma un intervalo a la fecha. Requiere params: `interval`, `unit`. Es una agregación: devuelve la fecha resultante `MIN` por grupo por defecto; `params.aggregationFunction` admite `MIN` o `MAX`. Se puede combinar con dimensiones y otras métricas. - **date_sub**: Resta un intervalo a la fecha. Requiere params: `interval`, `unit`. Igual que `date_add`: `MIN` por defecto, `MIN` o `MAX`. - Cualquier otro valor de `aggregationFunction` responde 400 `INVALID_REQUEST` sin ejecutar la consulta. **Ejemplo date_add** (fecha del último evento de cada nombre más 7 días): ```json { "field": "start_date", "operation": "date_add", "alias": "last_start_plus_7_days", "params": { "interval": 7, "unit": "DAY", "aggregationFunction": "MAX" } } ``` **Ejemplo date_diff**: ```json { "field": "created_at", "operation": "date_diff", "alias": "days_until_event", "params": { "compareField": "start_date", "unit": "DAY" } } ``` ### Compatibilidad por tipo de dato **TEXT**: count, count_distinct, string_agg, approx_top_count **NUMBER**: sum, avg, count, min, max, count_distinct, median, percentile_*, sum_if, avg_if, count_if, approx_top_count **DATE**: count, min, max, count_distinct, count_if, approx_top_count, date_diff, date_add, date_sub **BOOLEAN**: count, count_distinct, count_if, approx_top_count ### Ejemplos ```json // COUNT_DISTINCT - Usuarios únicos { "field": "user_id", "operation": "count_distinct" } // MEDIAN - Edad mediana { "field": "age", "operation": "median" } // SUM_IF - Suma de ventas completadas { "field": "amount", "operation": "sum_if", "condition": { "field": "status", "operator": "eq", "value": "completed" } } // STRING_AGG - Concatenar tags { "field": "tags", "operation": "string_agg", "params": { "delimiter": " | " } } // APPROX_TOP_COUNT - Top 20 categorías { "field": "category", "operation": "approx_top_count", "params": { "limit": 20 } } ```
      - `alias` string: Alias para la métrica en los resultados
      - `condition` FilterCondition
        - `field` string, required: Nombre del campo a filtrar
        - `operator` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'regex' | 'regex_i' | 'is_null' | 'is_not_null' | 'before' | 'after' | 'between' | 'array_contains' | 'array_not_contains' | 'array_contains_any' | 'array_length_eq' | 'array_length_gt', required: Operadores disponibles para filtros Analytics API. **Total: 28 operadores** organizados en 7 categorías. **Validación por tipo de dato**: El sistema valida automáticamente que el operador sea compatible con el tipo de dato del campo. ### Operadores por Tipo de Dato **TEXT** (15 operadores): - Comparación: `eq`, `neq`, `in`, `not_in` - Búsqueda: `contains`, `not_contains`, `starts_with`, `ends_with` - Regex: `regex`, `regex_i` (case-insensitive) - Null: `is_null`, `is_not_null` **NUMBER** (11 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Arrays: `in`, `not_in` - Rangos: `between` - Null: `is_null`, `is_not_null` **DATE** (10 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Temporales: `before`, `after`, `between` - Null: `is_null`, `is_not_null` - El valor es `YYYY-MM-DD` o fecha y hora ISO 8601 y se compara con el cast de la columna: - **Columna DATE** (`DATE(valor)`): todos los operadores comparan días. Un valor con hora u offset cuenta como el **día UTC de su instante** (`2024-01-02T23:30:00-05:00` → 2024-01-03). `eq v` selecciona las mismas filas que `gte v` y `lte v` juntos. - **Columna TIMESTAMP** (`TIMESTAMP(valor)`): `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `before` y `after` comparan el **instante**; una fecha sola es su medianoche UTC. Por ejemplo, `lte '2024-01-02'` no incluye el resto de ese día, y `eq '2024-01-02'` solo coincide con `2024-01-02 00:00:00 UTC`. Solo `between` incluye el día UTC completo del extremo final (`< día UTC siguiente`), igual que el fin de `timeRange`. **BOOLEAN** (3 operadores): `eq` / `neq` con `true` o `false`, y `exists`. `is_true`/`is_false` son operadores de segmentación, no de analytics (400). **ARRAY** (5 operadores): - Contenido: `array_contains`, `array_not_contains`, `array_contains_any` - Tamaño: `array_length_eq`, `array_length_gt` ### Ejemplos ```json // Text - contains { "field": "tenant_name", "operator": "contains", "value": "company" } // Number - range { "field": "emails_sent", "operator": "in_range", "value": [100, 1000] } // Date - before { "field": "send_date", "operator": "before", "value": "2024-12-31" } // Regex case-insensitive { "field": "email", "operator": "regex_i", "value": "^[a-z]+@gmail\\.com$" } // Empty check { "field": "description", "operator": "is_empty" } ``` ### Seguridad - **Regex**: Se valida que el patrón sea válido y no contenga construcciones peligrosas - **Regex y BigQuery (RE2)** (`regex`, `regex_i`): BigQuery evalúa el patrón con RE2, que no admite todo lo que admite una expresión regular de JavaScript. Un conjunto enumerado de construcciones incompatibles se rechaza con **400 `INVALID_ANALYTICS_VALUE` antes de lanzar la consulta** y sin repetir el patrón: lookahead/lookbehind (`(?=`, `(?!`, `(?<=`, `(?<!`), referencias hacia atrás (`\1`, `\k<n>`), escapes que RE2 no define (`\u`, `\c`, `\Z`…), `\x` mal formado (sin comprobar el rango del código en `\x{…}`), `\E` suelto, clases POSIX desconocidas y clases sin cerrar (`[]`, `[^]`). La validación **no es un parser RE2 completo**: otros patrones que RE2 no admite (por ejemplo `\p{…}` desconocido, un escape de carácter no ASCII, repeticiones de más de 1000, un rango que termina en una clase como `[a-\d]`, un código `\x{…}` por encima de `10FFFF`) pueden ser rechazados por BigQuery al ejecutar la consulta, con un error de servidor y sin devolver filas. - **Field names**: Solo se permiten caracteres alfanuméricos, underscore y punto - **Array values**: Los operadores de array requieren valores no vacíos
        - `value` union, required: Valor para la comparación. El tipo depende del operador: - **Comparación**: string, number, o boolean - **Arrays** (`in`, `nin`): array de valores - **Fechas**: string ISO 8601, `YYYY-MM-DD` o fecha y hora (`Z`, offset o sin zona = UTC). Cómo se compara según el tipo de la columna: ver `FilterOperator` (DATE) - **Between**: array con [inicio, fin]
          - string
          - number
          - boolean
          - union[]
            - union
              - …
      - `params` object: Parámetros adicionales para operaciones específicas
        - `delimiter` string: Delimitador para STRING_AGG (default: ","). No puede contener: comillas, punto y coma, backslashes
        - `percentile` number: Percentil personalizado (0-100) - RESERVADO para futuro uso
        - `limit` integer: Límite de resultados para APPROX_TOP_COUNT (default: 10). Debe estar entre 1 y 1000
    - `filters` FilterCondition[]: Filtros para el CTE (WHERE)
      - `field` string, required: Nombre del campo a filtrar
      - `operator` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'regex' | 'regex_i' | 'is_null' | 'is_not_null' | 'before' | 'after' | 'between' | 'array_contains' | 'array_not_contains' | 'array_contains_any' | 'array_length_eq' | 'array_length_gt', required: Operadores disponibles para filtros Analytics API. **Total: 28 operadores** organizados en 7 categorías. **Validación por tipo de dato**: El sistema valida automáticamente que el operador sea compatible con el tipo de dato del campo. ### Operadores por Tipo de Dato **TEXT** (15 operadores): - Comparación: `eq`, `neq`, `in`, `not_in` - Búsqueda: `contains`, `not_contains`, `starts_with`, `ends_with` - Regex: `regex`, `regex_i` (case-insensitive) - Null: `is_null`, `is_not_null` **NUMBER** (11 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Arrays: `in`, `not_in` - Rangos: `between` - Null: `is_null`, `is_not_null` **DATE** (10 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Temporales: `before`, `after`, `between` - Null: `is_null`, `is_not_null` - El valor es `YYYY-MM-DD` o fecha y hora ISO 8601 y se compara con el cast de la columna: - **Columna DATE** (`DATE(valor)`): todos los operadores comparan días. Un valor con hora u offset cuenta como el **día UTC de su instante** (`2024-01-02T23:30:00-05:00` → 2024-01-03). `eq v` selecciona las mismas filas que `gte v` y `lte v` juntos. - **Columna TIMESTAMP** (`TIMESTAMP(valor)`): `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `before` y `after` comparan el **instante**; una fecha sola es su medianoche UTC. Por ejemplo, `lte '2024-01-02'` no incluye el resto de ese día, y `eq '2024-01-02'` solo coincide con `2024-01-02 00:00:00 UTC`. Solo `between` incluye el día UTC completo del extremo final (`< día UTC siguiente`), igual que el fin de `timeRange`. **BOOLEAN** (3 operadores): `eq` / `neq` con `true` o `false`, y `exists`. `is_true`/`is_false` son operadores de segmentación, no de analytics (400). **ARRAY** (5 operadores): - Contenido: `array_contains`, `array_not_contains`, `array_contains_any` - Tamaño: `array_length_eq`, `array_length_gt` ### Ejemplos ```json // Text - contains { "field": "tenant_name", "operator": "contains", "value": "company" } // Number - range { "field": "emails_sent", "operator": "in_range", "value": [100, 1000] } // Date - before { "field": "send_date", "operator": "before", "value": "2024-12-31" } // Regex case-insensitive { "field": "email", "operator": "regex_i", "value": "^[a-z]+@gmail\\.com$" } // Empty check { "field": "description", "operator": "is_empty" } ``` ### Seguridad - **Regex**: Se valida que el patrón sea válido y no contenga construcciones peligrosas - **Regex y BigQuery (RE2)** (`regex`, `regex_i`): BigQuery evalúa el patrón con RE2, que no admite todo lo que admite una expresión regular de JavaScript. Un conjunto enumerado de construcciones incompatibles se rechaza con **400 `INVALID_ANALYTICS_VALUE` antes de lanzar la consulta** y sin repetir el patrón: lookahead/lookbehind (`(?=`, `(?!`, `(?<=`, `(?<!`), referencias hacia atrás (`\1`, `\k<n>`), escapes que RE2 no define (`\u`, `\c`, `\Z`…), `\x` mal formado (sin comprobar el rango del código en `\x{…}`), `\E` suelto, clases POSIX desconocidas y clases sin cerrar (`[]`, `[^]`). La validación **no es un parser RE2 completo**: otros patrones que RE2 no admite (por ejemplo `\p{…}` desconocido, un escape de carácter no ASCII, repeticiones de más de 1000, un rango que termina en una clase como `[a-\d]`, un código `\x{…}` por encima de `10FFFF`) pueden ser rechazados por BigQuery al ejecutar la consulta, con un error de servidor y sin devolver filas. - **Field names**: Solo se permiten caracteres alfanuméricos, underscore y punto - **Array values**: Los operadores de array requieren valores no vacíos
      - `value` union, required: Valor para la comparación. El tipo depende del operador: - **Comparación**: string, number, o boolean - **Arrays** (`in`, `nin`): array de valores - **Fechas**: string ISO 8601, `YYYY-MM-DD` o fecha y hora (`Z`, offset o sin zona = UTC). Cómo se compara según el tipo de la columna: ver `FilterOperator` (DATE) - **Between**: array con [inicio, fin]
        - string
        - number
        - boolean
        - union[]
          - union
            - string
            - number
    - `timeRange` TimeRange: **Phase 4**: Rango temporal para filtrar datos. Automáticamente filtra usando el campo temporal principal de la colección (en un CTE, el de la colección del CTE o su `timeField`). El SQL depende del tipo de la columna: - **TIMESTAMP:** `campo >= TIMESTAMP(start) AND campo < TIMESTAMP(<día UTC siguiente al de end>)`. El inicio es el instante dado (una fecha sola es su medianoche UTC; si trae hora, se conserva). El fin incluye el día UTC completo del instante `end`. - **DATE:** `campo >= DATE(start) AND campo <= DATE(end)`. Con hora u offset, cada extremo cuenta como el **día UTC de su instante**: `2024-01-02T23:30:00-05:00` es el 3 de enero; `2024-01-02T00:30:00+02:00`, el 1 de enero; sin zona, UTC. Los días son días UTC: un rango diario no coincide con el día local de una zona con offset.
      - `start` string, required: Inicio del rango: `YYYY-MM-DD`, o fecha y hora ISO 8601 (con `Z`, con offset `±HH:MM` o sin zona, que se toma como UTC).
      - `end` string, required: Fin del rango, en las mismas formas que `start`. Se incluye el día UTC completo de `end`.
    - `timeField` string: Campo temporal del `timeRange` del CTE (solo se lee si hay `timeRange`). Por defecto, el campo temporal de la colección del propio CTE; si la colección no tiene, `timeRange` exige `timeField` (400 si falta). Debe ser una columna de esa tabla y de tipo DATE o TIMESTAMP: un nombre que no existe en el mart o que no es columna de la tabla da 400 `INVALID_FIELD`, y uno de otro tipo, 400 `INVALID_REQUEST`. Se aceptan los alias del mapper (`start_date` → `start_ts`). Una columna sin tipo declarado se acepta solo si su nombre termina en `_at`, `_ts` o `_date`, o contiene `timestamp`. Límite: un nombre con esa forma que no es columna de la tabla (p. ej. `zz_at`) no se detecta y falla en BigQuery.
    - `timeGranularity` 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'fiscalQuarter' | 'fiscalYear': Granularidad temporal para agregaciones. **Total: 9 granularidades** (2 intra-day + 5 básicas + 2 fiscales). ### Básicas (DATE_TRUNC) Se aplica usando `DATE_TRUNC` in the database sobre campos de fecha. - **day**: Agrupa por día (YYYY-MM-DD) - **week**: Agrupa por semana (lunes como inicio) - **month**: Agrupa por mes (YYYY-MM) - **quarter**: Agrupa por trimestre calendario (Q1: Ene-Mar, Q2: Abr-Jun, Q3: Jul-Sep, Q4: Oct-Dic) - **year**: Agrupa por año calendario ### Phase 4 - Intra-day (TIMESTAMP_TRUNC) Se aplica usando `TIMESTAMP_TRUNC` para análisis de hora/minuto. - **hour**: Agrupa por hora (granularidad de 1 hora) - **minute**: Agrupa por minuto (granularidad de 1 minuto) ### Phase 4 - Fiscal Periods (JOIN con dim_dates) Usa JOIN con tabla `dim_dates` para períodos fiscales. - **fiscalQuarter**: Trimestre fiscal (FQ1: Jul-Sep, FQ2: Oct-Dic, FQ3: Ene-Mar, FQ4: Abr-Jun) - **fiscalYear**: Año fiscal (comienza en Julio, formato: FY2024) - Cobertura de `dim_dates`: 2020-01-01 a 2030-12-31. Fechas fuera de ese rango, o nulas, devuelven el período `null`.
    - `sort` union: Configuración de ordenamiento. Acepta un único objeto (retrocompatible) o un array de objetos para ordenación por múltiples campos. **Formato simple (retrocompatible):** ```json { "field": "revenue", "order": "desc" } ``` **Formato multi-sort (nuevo):** ```json [ { "field": "revenue", "order": "desc" }, { "field": "event_name", "order": "asc" } ] ``` El campo `order` es opcional y toma valor `"asc"` por defecto.
      - SortConfig
        - `field` string, required: Campo por el cual ordenar
        - `order` 'asc' | 'desc': Orden de clasificación
      - SortConfig[]
        - `field` string, required: Campo por el cual ordenar
        - `order` 'asc' | 'desc': Orden de clasificación
    - `limit` integer: Límite de resultados del CTE
    - `groupBy` GroupByType[]: No soportado dentro de un CTE: si trae valores, 400 `INVALID_REQUEST`. El GROUP BY del CTE sale de sus `dimensions`.
  - `sourceTable` string: **Phase 4 - CTEs**: Indica desde qué tabla/CTE se debe hacer el SELECT principal. Si se especifica, el SELECT principal se hace desde este CTE en lugar de `collection`. Los filtros del main query NO se aplican cuando sourceTable es un CTE (data ya filtrada). Si no es uno de los CTE, debe ser una tabla de la lista blanca: si no, 400 `INVALID_REQUEST` sin repetir el nombre.
  - `dimensions` Dimension[]: Campos para agrupar (GROUP BY)
    - `field` string, required: Nombre del campo para agrupar
    - `alias` string: Alias para el campo en los resultados
  - `metrics` Metric[]: Campos para agregar (SUM, COUNT, etc.)
    - `field` string, required: Nombre del campo para agregar. Usar "*" para operaciones COUNT.
    - `operation` 'sum' | 'avg' | 'count' | 'min' | 'max' | 'count_distinct' | 'median' | 'percentile_25' | 'percentile_50' | 'percentile_75' | 'percentile_90' | 'percentile_95' | 'sum_if' | 'avg_if' | 'count_if' | 'string_agg' | 'approx_top_count' | 'date_diff' | 'date_add' | 'date_sub', required: Operaciones disponibles para métricas: ### Básicas - **sum**: Suma de valores numéricos - **avg**: Promedio de valores numéricos - **count**: Conteo de registros (usar field="*") - **min**: Valor mínimo - **max**: Valor máximo ### Agregaciones Avanzadas (Fase 3) - **count_distinct**: Conteo de valores únicos. Requiere field específico (no "*") - **median**: Mediana aproximada (percentil 50) usando APPROX_QUANTILES - **percentile_25/50/75/90/95**: Percentiles específicos aproximados - **approx_top_count**: Top N valores más frecuentes. Acepta params.limit (default: 10, max: 1000) - **string_agg**: Concatenar strings con delimitador. Acepta params.delimiter (default: ",") ### Agregaciones Condicionales - **sum_if**: Suma donde se cumple condición. Requiere field `condition` - **avg_if**: Promedio donde se cumple condición. Requiere field `condition` - **count_if**: Conteo donde se cumple condición. Requiere field `condition` ### Phase 4 - Date Arithmetic - **date_diff**: Calcula `compareField − field` en la unidad pedida: `DATE_DIFF(DATE(compareField), DATE(field), unit)`. Requiere params: `compareField`, `unit` (DAY, WEEK, MONTH, QUARTER, YEAR; HOUR y MINUTE responden 400 porque la operación es sobre fechas). Para «días hasta el evento» desde la creación, `field` es `created_at` y `compareField` es `start_date`. Es una agregación: devuelve `AVG` de la diferencia por grupo por defecto; `params.aggregationFunction` admite `AVG`, `SUM`, `MIN` o `MAX`. - **date_add**: Suma un intervalo a la fecha. Requiere params: `interval`, `unit`. Es una agregación: devuelve la fecha resultante `MIN` por grupo por defecto; `params.aggregationFunction` admite `MIN` o `MAX`. Se puede combinar con dimensiones y otras métricas. - **date_sub**: Resta un intervalo a la fecha. Requiere params: `interval`, `unit`. Igual que `date_add`: `MIN` por defecto, `MIN` o `MAX`. - Cualquier otro valor de `aggregationFunction` responde 400 `INVALID_REQUEST` sin ejecutar la consulta. **Ejemplo date_add** (fecha del último evento de cada nombre más 7 días): ```json { "field": "start_date", "operation": "date_add", "alias": "last_start_plus_7_days", "params": { "interval": 7, "unit": "DAY", "aggregationFunction": "MAX" } } ``` **Ejemplo date_diff**: ```json { "field": "created_at", "operation": "date_diff", "alias": "days_until_event", "params": { "compareField": "start_date", "unit": "DAY" } } ``` ### Compatibilidad por tipo de dato **TEXT**: count, count_distinct, string_agg, approx_top_count **NUMBER**: sum, avg, count, min, max, count_distinct, median, percentile_*, sum_if, avg_if, count_if, approx_top_count **DATE**: count, min, max, count_distinct, count_if, approx_top_count, date_diff, date_add, date_sub **BOOLEAN**: count, count_distinct, count_if, approx_top_count ### Ejemplos ```json // COUNT_DISTINCT - Usuarios únicos { "field": "user_id", "operation": "count_distinct" } // MEDIAN - Edad mediana { "field": "age", "operation": "median" } // SUM_IF - Suma de ventas completadas { "field": "amount", "operation": "sum_if", "condition": { "field": "status", "operator": "eq", "value": "completed" } } // STRING_AGG - Concatenar tags { "field": "tags", "operation": "string_agg", "params": { "delimiter": " | " } } // APPROX_TOP_COUNT - Top 20 categorías { "field": "category", "operation": "approx_top_count", "params": { "limit": 20 } } ```
    - `alias` string: Alias para la métrica en los resultados
    - `condition` FilterCondition
      - `field` string, required: Nombre del campo a filtrar
      - `operator` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'regex' | 'regex_i' | 'is_null' | 'is_not_null' | 'before' | 'after' | 'between' | 'array_contains' | 'array_not_contains' | 'array_contains_any' | 'array_length_eq' | 'array_length_gt', required: Operadores disponibles para filtros Analytics API. **Total: 28 operadores** organizados en 7 categorías. **Validación por tipo de dato**: El sistema valida automáticamente que el operador sea compatible con el tipo de dato del campo. ### Operadores por Tipo de Dato **TEXT** (15 operadores): - Comparación: `eq`, `neq`, `in`, `not_in` - Búsqueda: `contains`, `not_contains`, `starts_with`, `ends_with` - Regex: `regex`, `regex_i` (case-insensitive) - Null: `is_null`, `is_not_null` **NUMBER** (11 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Arrays: `in`, `not_in` - Rangos: `between` - Null: `is_null`, `is_not_null` **DATE** (10 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Temporales: `before`, `after`, `between` - Null: `is_null`, `is_not_null` - El valor es `YYYY-MM-DD` o fecha y hora ISO 8601 y se compara con el cast de la columna: - **Columna DATE** (`DATE(valor)`): todos los operadores comparan días. Un valor con hora u offset cuenta como el **día UTC de su instante** (`2024-01-02T23:30:00-05:00` → 2024-01-03). `eq v` selecciona las mismas filas que `gte v` y `lte v` juntos. - **Columna TIMESTAMP** (`TIMESTAMP(valor)`): `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `before` y `after` comparan el **instante**; una fecha sola es su medianoche UTC. Por ejemplo, `lte '2024-01-02'` no incluye el resto de ese día, y `eq '2024-01-02'` solo coincide con `2024-01-02 00:00:00 UTC`. Solo `between` incluye el día UTC completo del extremo final (`< día UTC siguiente`), igual que el fin de `timeRange`. **BOOLEAN** (3 operadores): `eq` / `neq` con `true` o `false`, y `exists`. `is_true`/`is_false` son operadores de segmentación, no de analytics (400). **ARRAY** (5 operadores): - Contenido: `array_contains`, `array_not_contains`, `array_contains_any` - Tamaño: `array_length_eq`, `array_length_gt` ### Ejemplos ```json // Text - contains { "field": "tenant_name", "operator": "contains", "value": "company" } // Number - range { "field": "emails_sent", "operator": "in_range", "value": [100, 1000] } // Date - before { "field": "send_date", "operator": "before", "value": "2024-12-31" } // Regex case-insensitive { "field": "email", "operator": "regex_i", "value": "^[a-z]+@gmail\\.com$" } // Empty check { "field": "description", "operator": "is_empty" } ``` ### Seguridad - **Regex**: Se valida que el patrón sea válido y no contenga construcciones peligrosas - **Regex y BigQuery (RE2)** (`regex`, `regex_i`): BigQuery evalúa el patrón con RE2, que no admite todo lo que admite una expresión regular de JavaScript. Un conjunto enumerado de construcciones incompatibles se rechaza con **400 `INVALID_ANALYTICS_VALUE` antes de lanzar la consulta** y sin repetir el patrón: lookahead/lookbehind (`(?=`, `(?!`, `(?<=`, `(?<!`), referencias hacia atrás (`\1`, `\k<n>`), escapes que RE2 no define (`\u`, `\c`, `\Z`…), `\x` mal formado (sin comprobar el rango del código en `\x{…}`), `\E` suelto, clases POSIX desconocidas y clases sin cerrar (`[]`, `[^]`). La validación **no es un parser RE2 completo**: otros patrones que RE2 no admite (por ejemplo `\p{…}` desconocido, un escape de carácter no ASCII, repeticiones de más de 1000, un rango que termina en una clase como `[a-\d]`, un código `\x{…}` por encima de `10FFFF`) pueden ser rechazados por BigQuery al ejecutar la consulta, con un error de servidor y sin devolver filas. - **Field names**: Solo se permiten caracteres alfanuméricos, underscore y punto - **Array values**: Los operadores de array requieren valores no vacíos
      - `value` union, required: Valor para la comparación. El tipo depende del operador: - **Comparación**: string, number, o boolean - **Arrays** (`in`, `nin`): array de valores - **Fechas**: string ISO 8601, `YYYY-MM-DD` o fecha y hora (`Z`, offset o sin zona = UTC). Cómo se compara según el tipo de la columna: ver `FilterOperator` (DATE) - **Between**: array con [inicio, fin]
        - string
        - number
        - boolean
        - union[]
          - union
            - string
            - number
    - `params` object: Parámetros adicionales para operaciones específicas
      - `delimiter` string: Delimitador para STRING_AGG (default: ","). No puede contener: comillas, punto y coma, backslashes
      - `percentile` number: Percentil personalizado (0-100) - RESERVADO para futuro uso
      - `limit` integer: Límite de resultados para APPROX_TOP_COUNT (default: 10). Debe estar entre 1 y 1000
  - `timeGranularity` 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'fiscalQuarter' | 'fiscalYear': Granularidad temporal para agregaciones. **Total: 9 granularidades** (2 intra-day + 5 básicas + 2 fiscales). ### Básicas (DATE_TRUNC) Se aplica usando `DATE_TRUNC` in the database sobre campos de fecha. - **day**: Agrupa por día (YYYY-MM-DD) - **week**: Agrupa por semana (lunes como inicio) - **month**: Agrupa por mes (YYYY-MM) - **quarter**: Agrupa por trimestre calendario (Q1: Ene-Mar, Q2: Abr-Jun, Q3: Jul-Sep, Q4: Oct-Dic) - **year**: Agrupa por año calendario ### Phase 4 - Intra-day (TIMESTAMP_TRUNC) Se aplica usando `TIMESTAMP_TRUNC` para análisis de hora/minuto. - **hour**: Agrupa por hora (granularidad de 1 hora) - **minute**: Agrupa por minuto (granularidad de 1 minuto) ### Phase 4 - Fiscal Periods (JOIN con dim_dates) Usa JOIN con tabla `dim_dates` para períodos fiscales. - **fiscalQuarter**: Trimestre fiscal (FQ1: Jul-Sep, FQ2: Oct-Dic, FQ3: Ene-Mar, FQ4: Abr-Jun) - **fiscalYear**: Año fiscal (comienza en Julio, formato: FY2024) - Cobertura de `dim_dates`: 2020-01-01 a 2030-12-31. Fechas fuera de ese rango, o nulas, devuelven el período `null`.
  - `groupBy` GroupByType[]: **Phase 4**: Tipos especiales de agrupación temporal (temporal extractors y fiscal periods). **Diferencia con dimensions**: - `groupBy`: Extrae componentes (hourOfDay, dayOfWeek) o aplica formateo fiscal - `dimensions`: Agrupa por campos raw o con timeGranularity aplicado Ejemplos: hourOfDay, dayOfWeek, fiscalQuarter, fiscalYear
  - `timeRange` TimeRange: **Phase 4**: Rango temporal para filtrar datos. Automáticamente filtra usando el campo temporal principal de la colección (en un CTE, el de la colección del CTE o su `timeField`). El SQL depende del tipo de la columna: - **TIMESTAMP:** `campo >= TIMESTAMP(start) AND campo < TIMESTAMP(<día UTC siguiente al de end>)`. El inicio es el instante dado (una fecha sola es su medianoche UTC; si trae hora, se conserva). El fin incluye el día UTC completo del instante `end`. - **DATE:** `campo >= DATE(start) AND campo <= DATE(end)`. Con hora u offset, cada extremo cuenta como el **día UTC de su instante**: `2024-01-02T23:30:00-05:00` es el 3 de enero; `2024-01-02T00:30:00+02:00`, el 1 de enero; sin zona, UTC. Los días son días UTC: un rango diario no coincide con el día local de una zona con offset.
    - `start` string, required: Inicio del rango: `YYYY-MM-DD`, o fecha y hora ISO 8601 (con `Z`, con offset `±HH:MM` o sin zona, que se toma como UTC).
    - `end` string, required: Fin del rango, en las mismas formas que `start`. Se incluye el día UTC completo de `end`.
  - `filters` FilterCondition[]: Condiciones de filtrado (WHERE)
    - `field` string, required: Nombre del campo a filtrar
    - `operator` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'regex' | 'regex_i' | 'is_null' | 'is_not_null' | 'before' | 'after' | 'between' | 'array_contains' | 'array_not_contains' | 'array_contains_any' | 'array_length_eq' | 'array_length_gt', required: Operadores disponibles para filtros Analytics API. **Total: 28 operadores** organizados en 7 categorías. **Validación por tipo de dato**: El sistema valida automáticamente que el operador sea compatible con el tipo de dato del campo. ### Operadores por Tipo de Dato **TEXT** (15 operadores): - Comparación: `eq`, `neq`, `in`, `not_in` - Búsqueda: `contains`, `not_contains`, `starts_with`, `ends_with` - Regex: `regex`, `regex_i` (case-insensitive) - Null: `is_null`, `is_not_null` **NUMBER** (11 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Arrays: `in`, `not_in` - Rangos: `between` - Null: `is_null`, `is_not_null` **DATE** (10 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Temporales: `before`, `after`, `between` - Null: `is_null`, `is_not_null` - El valor es `YYYY-MM-DD` o fecha y hora ISO 8601 y se compara con el cast de la columna: - **Columna DATE** (`DATE(valor)`): todos los operadores comparan días. Un valor con hora u offset cuenta como el **día UTC de su instante** (`2024-01-02T23:30:00-05:00` → 2024-01-03). `eq v` selecciona las mismas filas que `gte v` y `lte v` juntos. - **Columna TIMESTAMP** (`TIMESTAMP(valor)`): `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `before` y `after` comparan el **instante**; una fecha sola es su medianoche UTC. Por ejemplo, `lte '2024-01-02'` no incluye el resto de ese día, y `eq '2024-01-02'` solo coincide con `2024-01-02 00:00:00 UTC`. Solo `between` incluye el día UTC completo del extremo final (`< día UTC siguiente`), igual que el fin de `timeRange`. **BOOLEAN** (3 operadores): `eq` / `neq` con `true` o `false`, y `exists`. `is_true`/`is_false` son operadores de segmentación, no de analytics (400). **ARRAY** (5 operadores): - Contenido: `array_contains`, `array_not_contains`, `array_contains_any` - Tamaño: `array_length_eq`, `array_length_gt` ### Ejemplos ```json // Text - contains { "field": "tenant_name", "operator": "contains", "value": "company" } // Number - range { "field": "emails_sent", "operator": "in_range", "value": [100, 1000] } // Date - before { "field": "send_date", "operator": "before", "value": "2024-12-31" } // Regex case-insensitive { "field": "email", "operator": "regex_i", "value": "^[a-z]+@gmail\\.com$" } // Empty check { "field": "description", "operator": "is_empty" } ``` ### Seguridad - **Regex**: Se valida que el patrón sea válido y no contenga construcciones peligrosas - **Regex y BigQuery (RE2)** (`regex`, `regex_i`): BigQuery evalúa el patrón con RE2, que no admite todo lo que admite una expresión regular de JavaScript. Un conjunto enumerado de construcciones incompatibles se rechaza con **400 `INVALID_ANALYTICS_VALUE` antes de lanzar la consulta** y sin repetir el patrón: lookahead/lookbehind (`(?=`, `(?!`, `(?<=`, `(?<!`), referencias hacia atrás (`\1`, `\k<n>`), escapes que RE2 no define (`\u`, `\c`, `\Z`…), `\x` mal formado (sin comprobar el rango del código en `\x{…}`), `\E` suelto, clases POSIX desconocidas y clases sin cerrar (`[]`, `[^]`). La validación **no es un parser RE2 completo**: otros patrones que RE2 no admite (por ejemplo `\p{…}` desconocido, un escape de carácter no ASCII, repeticiones de más de 1000, un rango que termina en una clase como `[a-\d]`, un código `\x{…}` por encima de `10FFFF`) pueden ser rechazados por BigQuery al ejecutar la consulta, con un error de servidor y sin devolver filas. - **Field names**: Solo se permiten caracteres alfanuméricos, underscore y punto - **Array values**: Los operadores de array requieren valores no vacíos
    - `value` union, required: Valor para la comparación. El tipo depende del operador: - **Comparación**: string, number, o boolean - **Arrays** (`in`, `nin`): array de valores - **Fechas**: string ISO 8601, `YYYY-MM-DD` o fecha y hora (`Z`, offset o sin zona = UTC). Cómo se compara según el tipo de la columna: ver `FilterOperator` (DATE) - **Between**: array con [inicio, fin]
      - string
      - number
      - boolean
      - union[]
        - union
          - string
          - number
  - `having` FilterCondition[]: Filtros sobre resultados agregados (HAVING). Solo se puede usar cuando hay métricas o dimensiones.
    - `field` string, required: Nombre del campo a filtrar
    - `operator` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'regex' | 'regex_i' | 'is_null' | 'is_not_null' | 'before' | 'after' | 'between' | 'array_contains' | 'array_not_contains' | 'array_contains_any' | 'array_length_eq' | 'array_length_gt', required: Operadores disponibles para filtros Analytics API. **Total: 28 operadores** organizados en 7 categorías. **Validación por tipo de dato**: El sistema valida automáticamente que el operador sea compatible con el tipo de dato del campo. ### Operadores por Tipo de Dato **TEXT** (15 operadores): - Comparación: `eq`, `neq`, `in`, `not_in` - Búsqueda: `contains`, `not_contains`, `starts_with`, `ends_with` - Regex: `regex`, `regex_i` (case-insensitive) - Null: `is_null`, `is_not_null` **NUMBER** (11 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Arrays: `in`, `not_in` - Rangos: `between` - Null: `is_null`, `is_not_null` **DATE** (10 operadores): - Comparación: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` - Temporales: `before`, `after`, `between` - Null: `is_null`, `is_not_null` - El valor es `YYYY-MM-DD` o fecha y hora ISO 8601 y se compara con el cast de la columna: - **Columna DATE** (`DATE(valor)`): todos los operadores comparan días. Un valor con hora u offset cuenta como el **día UTC de su instante** (`2024-01-02T23:30:00-05:00` → 2024-01-03). `eq v` selecciona las mismas filas que `gte v` y `lte v` juntos. - **Columna TIMESTAMP** (`TIMESTAMP(valor)`): `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `before` y `after` comparan el **instante**; una fecha sola es su medianoche UTC. Por ejemplo, `lte '2024-01-02'` no incluye el resto de ese día, y `eq '2024-01-02'` solo coincide con `2024-01-02 00:00:00 UTC`. Solo `between` incluye el día UTC completo del extremo final (`< día UTC siguiente`), igual que el fin de `timeRange`. **BOOLEAN** (3 operadores): `eq` / `neq` con `true` o `false`, y `exists`. `is_true`/`is_false` son operadores de segmentación, no de analytics (400). **ARRAY** (5 operadores): - Contenido: `array_contains`, `array_not_contains`, `array_contains_any` - Tamaño: `array_length_eq`, `array_length_gt` ### Ejemplos ```json // Text - contains { "field": "tenant_name", "operator": "contains", "value": "company" } // Number - range { "field": "emails_sent", "operator": "in_range", "value": [100, 1000] } // Date - before { "field": "send_date", "operator": "before", "value": "2024-12-31" } // Regex case-insensitive { "field": "email", "operator": "regex_i", "value": "^[a-z]+@gmail\\.com$" } // Empty check { "field": "description", "operator": "is_empty" } ``` ### Seguridad - **Regex**: Se valida que el patrón sea válido y no contenga construcciones peligrosas - **Regex y BigQuery (RE2)** (`regex`, `regex_i`): BigQuery evalúa el patrón con RE2, que no admite todo lo que admite una expresión regular de JavaScript. Un conjunto enumerado de construcciones incompatibles se rechaza con **400 `INVALID_ANALYTICS_VALUE` antes de lanzar la consulta** y sin repetir el patrón: lookahead/lookbehind (`(?=`, `(?!`, `(?<=`, `(?<!`), referencias hacia atrás (`\1`, `\k<n>`), escapes que RE2 no define (`\u`, `\c`, `\Z`…), `\x` mal formado (sin comprobar el rango del código en `\x{…}`), `\E` suelto, clases POSIX desconocidas y clases sin cerrar (`[]`, `[^]`). La validación **no es un parser RE2 completo**: otros patrones que RE2 no admite (por ejemplo `\p{…}` desconocido, un escape de carácter no ASCII, repeticiones de más de 1000, un rango que termina en una clase como `[a-\d]`, un código `\x{…}` por encima de `10FFFF`) pueden ser rechazados por BigQuery al ejecutar la consulta, con un error de servidor y sin devolver filas. - **Field names**: Solo se permiten caracteres alfanuméricos, underscore y punto - **Array values**: Los operadores de array requieren valores no vacíos
    - `value` union, required: Valor para la comparación. El tipo depende del operador: - **Comparación**: string, number, o boolean - **Arrays** (`in`, `nin`): array de valores - **Fechas**: string ISO 8601, `YYYY-MM-DD` o fecha y hora (`Z`, offset o sin zona = UTC). Cómo se compara según el tipo de la columna: ver `FilterOperator` (DATE) - **Between**: array con [inicio, fin]
      - string
      - number
      - boolean
      - union[]
        - union
          - string
          - number
  - `sort` union: Configuración de ordenamiento. Acepta un único objeto (retrocompatible) o un array de objetos para ordenación por múltiples campos. **Formato simple (retrocompatible):** ```json { "field": "revenue", "order": "desc" } ``` **Formato multi-sort (nuevo):** ```json [ { "field": "revenue", "order": "desc" }, { "field": "event_name", "order": "asc" } ] ``` El campo `order` es opcional y toma valor `"asc"` por defecto.
    - SortConfig
      - `field` string, required: Campo por el cual ordenar
      - `order` 'asc' | 'desc': Orden de clasificación
    - SortConfig[]
      - `field` string, required: Campo por el cual ordenar
      - `order` 'asc' | 'desc': Orden de clasificación
  - `limit` integer: Límite de resultados
  - `offset` integer: Número de filas del resultado final a saltar (paginación, #62 R-2). Se aplica tras `having`, `sort` y las comparativas, y va como parámetro (`LIMIT … OFFSET …`). - Las páginas solo son estables con un `sort` que ordene todas las filas (p. ej. por un id único); sin `sort` el orden no está garantizado. - Con `limit`: esa página. Sin `limit`: salta `offset` filas del resultado completo (sin tope, como sin `offset`). - `0` o ausente: el resultado de siempre. - `offset > 0` con `groupBy` `dayOfWeek`/`hourOfDay` → 400 `INVALID_REQUEST`: esas franjas se rellenan después de la consulta y las páginas se solaparían. - Un valor que no sea un entero ≥ 0 → 400 `INVALID_ANALYTICS_VALUE`. - No aplica a `ctes[]`, `/analytics/export` ni `/analytics/filters`.
  - `distinct` boolean: **Deduplicación a nivel de query (NEV-1597).** Cuando `true`, genera `SELECT DISTINCT` en lugar de `SELECT`, eliminando filas duplicadas del resultado. **Compatibilidad:** - ✅ Funciona con `dimensions`, `filters`, `sort`, `limit` - ⚠️ Ignorado silenciosamente si hay `metrics` con funciones de agregación (COUNT, SUM, etc.) — en ese caso se incluye un `warning` en `metadata.warnings` - ⚠️ No disponible con `comparePeriods` ni `compareDimensions` - 🔄 Valor por defecto: `false` (retrocompatible) **Ejemplo de uso:** ```json { "collection": "user_tenants", "dimensions": [{ "field": "city" }], "distinct": true } ``` Genera: ```sql SELECT DISTINCT city FROM `nevent-daf5f.mart_nevent_dev.user_tenants` WHERE tenant_id = 'xxx' ```
  - `comparePeriods` PeriodComparison: **✅ IMPLEMENTADO v3.10.1**: Comparación período-sobre-período (Period-over-Period). ## ¿Qué hace? Compara métricas entre dos períodos de tiempo diferentes. Ideal para análisis Year-over-Year (YoY), Month-over-Month (MoM), Week-over-Week (WoW), etc. ## Columnas generadas Para cada métrica (ej. `revenue`), se generan automáticamente: - `{metric}_current` - Valor del período actual - `{metric}_previous` - Valor del período anterior - `{metric}_change` - Diferencia absoluta (current - previous) - `{metric}_change_percent` - Cambio porcentual ((current - previous) / previous * 100) ## Ejemplo de uso ```json { "active": true, "current": { "start": "2025-12-01", "end": "2025-12-31", "name": "Diciembre 2025" }, "previous": { "start": "2024-12-01", "end": "2024-12-31", "name": "Diciembre 2024" } } ``` ## Response esperado ```json { "data": [ { "purchased_at": "2025-12-01", "revenue_current": 15000, "revenue_previous": 12000, "revenue_change": 3000, "revenue_change_percent": 25.0 } ] } ``` ## ⚠️ Importante - NO incluir `timeRange` cuando `comparePeriods.active = true` - Los períodos se definen en `current` y `previous` - Soporta todas las métricas y granularidades temporales
    - `active` boolean, required: Si está activa la comparación de períodos
    - `current` object, required
      - `start` string, date, required: Fecha de inicio del período actual (YYYY-MM-DD)
      - `end` string, date, required: Fecha de fin del período actual (YYYY-MM-DD)
      - `name` string: Nombre opcional del período actual para UI
    - `previous` object, required
      - `start` string, date, required: Fecha de inicio del período anterior (YYYY-MM-DD)
      - `end` string, date, required: Fecha de fin del período anterior (YYYY-MM-DD)
      - `name` string: Nombre opcional del período anterior para UI
  - `compareDimensions` DimensionComparison: **✅ IMPLEMENTADO v3.10.1**: Comparación entidad-vs-entidad (Dimension Comparison). ## ¿Qué hace? Compara métricas entre múltiples valores de una dimensión. Ideal para comparar eventos, categorías, productos, etc. ## Columnas generadas Para cada métrica (ej. `revenue`) y cada valor de comparación, se genera: - `{metric}_{value}` - Métrica para ese valor específico - `{metric}_total` - Suma total de todas las entidades ## Response con metadata La API devuelve `metadata.comparisonMetadata` con información para mapear columnas: ```json { "comparisonMetadata": { "comparisonDimensions": [ { "field": "event_id", "value": "6940193688d2ee658320f9e5", "label": "6940193688d2ee658320f9e5", "columnName": "revenue_6940193688d2ee658320f9e5" } ], "totalColumn": "revenue_total" } } ``` ## Ejemplo de uso (FORMATO CORRECTO) ```json { "active": true, "dimensions": [ { "field": "eventId", "value": "6940193688d2ee658320f9e5" }, { "field": "eventId", "value": "693724b1fca82f737fafa77d" }, { "field": "eventId", "value": "690b9cfb0640eb7992afd03e" } ] } ``` ## Response esperado ```json { "data": [ { "purchased_at": "2025-12-01", "revenue_6940193688d2ee658320f9e5": 15000, "revenue_693724b1fca82f737fafa77d": 12000, "revenue_690b9cfb0640eb7992afd03e": 8000, "revenue_total": 35000 } ] } ``` ## ⚠️ Importante - NO incluir el campo de comparación en `filters` cuando `active = true` - La API filtra automáticamente por los valores en `dimensions` array - Mínimo 2, máximo 10 elementos en array `dimensions` - Frontend debe usar `comparisonMetadata` para mapeo dinámico de columnas - Ejemplo validado en: `postman/Event-Dashboard-Complete-v1.2-Fixed.postman_collection.json` (Sección 6)
    - `active` boolean, required: Si está activa la comparación de dimensiones
    - `dimensions` object[], required: Array de objetos con los valores a comparar. Mínimo 2 elementos, máximo 10 elementos.
      - `field` string, required: Campo de la dimensión a comparar
      - `value` string, required: Valor específico a comparar (ID de la entidad)
      - `name` string: Nombre legible para la entidad (opcional)
    - `useRelativeTime` boolean: **✅ NUEVO v3.9.1**: Usar tiempo relativo al inicio de cada entidad. Cuando `true`, la API alinea las fechas de cada entidad al inicio de su evento/período, permitiendo comparar entidades con fechas de inicio diferentes. Ejemplo: Comparar dos eventos con fechas de inicio distintas alineados por "días desde inicio".

## Response `200`

Consulta ejecutada exitosamente

- object
  - `success` boolean
  - `data` QueryResponse
    - `data` object[], required: Array de resultados de la consulta. Cada objeto contiene los campos solicitados en dimensiones y métricas.
    - `metadata` object
      - `totalRows` integer: Número total de filas retornadas
      - `executionTime` integer: Tiempo de ejecución en millisegundos
      - `query` string: SQL generado que se ejecutó in the database
      - `comparisonMetadata` ComparisonMetadata: **NEW v3.9.1**: Metadata para facilitar el mapeo de columnas dinámicas en comparaciones. ## Cuándo se incluye Se incluye automáticamente en `metadata.comparisonMetadata` cuando `compareDimensions.active = true`. ## Beneficios para frontend 1. **Mapeo Directo**: Acceso a series sin manipulación de strings ```javascript const { comparisonDimensions } = response.data.metadata.comparisonMetadata; comparisonDimensions.forEach(({ columnName, label }) => { const seriesData = response.data.data.map(row => ({ date: row.purchased_at, value: row[columnName] // Acceso directo sin parsing })); }); ``` 2. **Charting Dinámico**: Configuración de gráficos sin hardcoding ```javascript const chartConfig = { lines: comparisonDimensions.map(({ columnName, label }) => ({ dataKey: columnName, name: label })) }; ``` 3. **Cálculos de Porcentaje**: Referencia directa a columna total ```javascript const { totalColumn } = response.data.metadata.comparisonMetadata; data.forEach(row => { const percentage = (row[columnName] / row[totalColumn]) * 100; }); ``` ## Ejemplo completo ```json { "comparisonDimensions": [ { "field": "event_id", "value": "6597d9b3c9a57d0f00fcf194", "columnName": "revenue_6597d9b3c9a57d0f00fcf194", "label": "6597d9b3c9a57d0f00fcf194" }, { "field": "event_id", "value": "666dea428de8c802f52a8be1", "columnName": "revenue_666dea428de8c802f52a8be1", "label": "666dea428de8c802f52a8be1" } ], "totalColumn": "revenue_total" } ```
        - `comparisonDimensions` ComparisonDimensionMetadata[], required: Array con metadata para cada valor de comparación
          - `field` string, required: Campo que se está comparando (ej. "event_id")
          - `value` string, required: Valor de la dimensión para esta comparación
          - `label` string: Label legible para mostrar en UI. Actualmente contiene el mismo valor que `value`. En futuras versiones incluirá el nombre de la entidad (ej. nombre del evento).
          - `columnName` string, required: Nombre de la columna generada en el resultado. Usar este valor para extraer la serie de datos del array de resultados.
        - `totalColumn` string, required: Nombre de la columna con el total agregado de todas las comparaciones
      - `warnings` string[]: **NEV-1597**: Warnings sobre parámetros de la consulta ignorados o ajustados. Ejemplos: - `distinct` ignorado porque hay métricas de agregación presentes
  - `timestamp` string, date-time

## Other responses

- `400`: Solicitud inválida
- `401`: No autorizado - Token JWT inválido o faltante. Los rechazos del middleware de autenticación usan el envelope `AuthError` (`{ success, message, code? }`); un `activeTenantId` mal formado devuelve `code: INVALID_ACTIVE_TENANT` (ver `components.responses.InvalidActiveTenant`), un token de tipo no admitido `code: INVALID_TOKEN_TYPE` con `message: Tipo de token no aceptado`, y un token de impersonación cuya sesión terminó `code: IMPERSONATION_SESSION_TERMINATED`.
- `403`: Acceso denegado antes de ejecutar ninguna consulta. Dos casos: - **Rol no admitido** (`error.code: FORBIDDEN`): las rutas de datos solo admiten tokens con rol ADMIN, OWNER o SUPERADMIN (los `campaign_service_token` de nev-api llevan rol ADMIN). Un token de fan (USER), STAFF, EXTERNAL o sin rol recibe 403. - **Selección ajena** (`code: ACTIVE_TENANT_FORBIDDEN`): la organización seleccionada (`activeTenantId`) no pertenece a la jerarquía actual de `tenantId`.
- `500`: Error interno del servidor
- `503`: No se pudo leer la jerarquía de tenants (MongoDB no disponible). Reintentable; nunca se sustituye por un ámbito parcial o global.

---

[API](https://skmtc.dev/nevent/apis/nevent-data-api.md) · [All operations](https://skmtc.dev/nevent/apis/nevent-data-api/llms.txt) · [OpenAPI document](https://skmtc.dev/nevent/apis/nevent-data-api/revisions/81035d32c1c1?raw)
