Reten Docs
Audiencias

Previsualizar

Ejecuta una audience criteria contra BigQuery y devuelve una muestra de filas reales más el total, desde el cuerpo (aún sin persistir) o por id de un criterio guardado.

Previsualizar

Ejecuta una audience criteria contra BigQuery (acotada por limit) y devuelve una muestra de las filas reales que matchea más el total del universo. Un mismo motor sirve dos entradas:

  • POST /api/audience-criterias/preview-from-body — ejecuta una definición enviada en el cuerpo (aún no persistida).
  • POST /api/audience-criterias/:id/preview — ejecuta un criterio ya guardado, referenciado por su id.

Ambos devuelven la misma forma de respuesta: IPreviewAudienceCriteriaResult.

A diferencia de Generar SQL, preview no devuelve SQL; y a diferencia de Validar (que solo hace un dry-run), preview sí trae datos: corre la consulta y te muestra filas.

Dentro de un proyecto, toda audiencia que no sea la audiencia raíz se ejecuta intersectada con la audiencia raíz del proyecto — ver Intersección con la audiencia raíz.

Auth: Requerida — permiso audience-criteria:view-sql (ambos endpoints).

Preview consume cómputo en BigQuery en cada llamada (ejecuta la audiencia de verdad, no un dry-run). Para acotar el costo, cada consulta se corre con maximumBytesBilled de 10 GB (10737418240 bytes) y un timeout de 30 s (jobTimeoutMs): una consulta que exceda cualquiera de los dos límites se rechaza con 400 en vez de facturarse. Usá un limit chico mientras iterás.

El SQL que se ejecuta apunta a BigQuery: referencia las tablas como `<gcp-project>.<dataset>.<tabla>`, donde el <gcp-project> (el proyecto de Google Cloud, no un proyecto de Reten) sale de la configuración de ambiente del API y el <dataset> se resuelve a partir del tenant. Ninguno forma parte del contrato de entrada.


POST /api/audience-criterias/preview-from-body

Ejecuta una definición enviada en el cuerpo, sin persistirla.

Auth: Requerida — permiso audience-criteria:view-sql

El endpoint responde 201 Created —no 200—, aunque no cree ningún recurso.

Cuerpo de la solicitud

Nivel raíz (PreviewFromBodyDto): la definición del criterio (CriteriaDefinitionDto) más el campo limit.

CampoTipoRequeridoDescripción
queryBlocksQueryBlockDto[]SíMínimo 1 elemento. Cada bloque ≈ un statement SQL completo.
composedWithCreateCompositionDto[]NoCompone esta definición con otras audiencias vía operaciones de conjunto.
limitnumber (entero)NoTamaño de la muestra de rows. Entero 1..500. Default 100 (ver limit).
criteriaIdstring (UUID) | nullNoLa audiencia guardada que esta definición edita: el borrador toma la identidad de esa audiencia, así que da el mismo resultado que esa audiencia dará al guardarse (ver Intersección con la audiencia raíz). Ausente o null: una audiencia nueva. Debe ser un UUID válido (si no, 400) de una audiencia del proyecto activo (si no, 404).

A diferencia de Crear, la definición no lleva name ni description: es solo el árbol de consulta más limit. La validación es estricta, así que enviar name/description (u otra propiedad desconocida) produce un 400.

Definición del criterio

Es el mismo árbol que recibe Crear, sin los campos name/description. Se documenta aquí de forma autocontenida.

queryBlocks[] — bloque de consulta

Cada elemento de queryBlocks (QueryBlockDto) es un statement SQL completo:

CampoTipoRequeridoDescripción
ordernumber (entero)SíPosición del bloque; entero ≥ 0.
setOperatorSetOperatorEnumNoCombina este bloque con el anterior. Ausente en el primer bloque; obligatorio en el resto (ver reglas).
fromFromDtoSíFuente de datos: tabla principal + joins.
selectSelectDtoSíProyección (columnas y/o agregaciones).
timeWindowTimeWindowDtoNoVentana temporal del bloque (unión discriminada por timeOperator).
whereWhereDtoSíÁrbol de predicados. Obligatorio aunque sea un grupo vacío.
groupByColumnRefDto[]NoColumnas de agrupación.
havingHavingDtoNoFiltro sobre agregados.

El setOperator de un query block combina bloques dentro de la misma definición; no debe confundirse con el setOperator de una composición, que combina criterios enteros. Son dos niveles distintos de operación de conjunto.

ColumnRefDto — referencia a columna

Se usa en todo el árbol (select, filtros, group by, joins, agregados, ventana temporal):

CampoTipoRequeridoDescripción
tablestringSíTabla del catálogo. No vacío.
columnstringSíColumna. No vacío. Puede ser un path de STRUCT (p. ej. "pricing.final_amount").

Toda columna se cualifica siempre con su table para evitar ambigüedad.

from — fuente de datos (FromDto)
CampoTipoRequeridoDescripción
tablestringSíTabla principal del catálogo. No vacío.
joinsJoinDto[]NoCruces con otras tablas. Default [].
unnestsColumnRefDto[]NoColumnas repetidas declaradas como fuente de filas (una fila por elemento). Una audiencia no las admite: omite el campo o envía [] (el default); una lista no vacía da 400.

Cada JoinDto:

CampoTipoRequeridoDescripción
typeJoinTypeEnumSíINNER | LEFT | RIGHT | FULL.
tablestringSíTabla a unir (del catálogo). No vacío.
onJoinPredicateDto[]SíCondiciones de unión.

Cada JoinPredicateDto es un par de columnas: { left: ColumnRefDto, right: ColumnRefDto }.

select — proyección (SelectDto)
CampoTipoRequeridoDescripción
distinctbooleanSíAplica SELECT DISTINCT.
items(SelectColumnItem | SelectAggregateItem)[]SíNo vacío. Unión discriminada por el campo kind.

Los items se discriminan por kind:

  • Columna — kind: "column":

    CampoTipoRequeridoDescripción
    kind"column"SíDiscriminador.
    columnColumnRefDtoSíColumna proyectada.
    aliasstringNoDebe matchear ^[A-Za-z_][A-Za-z0-9_]*$.
  • Agregado — kind: "aggregate":

    CampoTipoRequeridoDescripción
    kind"aggregate"SíDiscriminador.
    aggregateAggregateDtoSíExpresión de agregación.
    aliasstringSíAquí el alias es obligatorio. Mismo regex de identificador.

AggregateDto:

CampoTipoRequeridoDescripción
aggregationOperatorAggregationOperatorEnumSíCOUNT | SUM | AVG | MIN | MAX.
operandobjectSíLo que se agrega. Unión discriminada por kind: column, star o arithmetic (ver abajo).
distinctbooleanNoAplica DISTINCT dentro del agregado.

operand —lo que se agrega— se discrimina por kind:

kindCampos adicionalesSignificado
columncolumn: ColumnRefDtoUna columna del catálogo.
star(ninguno)*: cuenta filas. Solo es válido con COUNT y sin distinct (COUNT(DISTINCT *) no está permitido).
arithmeticarithmeticOperator: ArithmeticOperatorEnum, left, rightExpresión binaria. Cada lado es otro operando: una column o un arithmetic anidado (nunca star).

ArithmeticOperatorEnum: ADD | SUBTRACT | MULTIPLY | SAFE_DIVIDE (una división que da NULL cuando el denominador es cero, en vez de hacer fallar la consulta). Las columnas de una expresión aritmética tienen que ser numéricas (INT64, NUMERIC, BIGNUMERIC, FLOAT64), y las expresiones se anidan hasta 3 niveles de arithmetic, contando el exterior.

where — árbol de predicados (WhereDto)

El where es un árbol recursivo. El nodo raíz es siempre un grupo, y sus hojas son de dos tipos:

kindQué filtra
groupNo es una hoja: agrupa hijos con AND / OR. Se pueden anidar grupos.
predicateUna columna escalar de una tabla del from.
recordPredicateUna columna RECORD REPEATED: existe ≥1 elemento que cumple el sub-filtro.
Grupo — kind: "group"
CampoTipoRequeridoDescripción
kind"group"SíDiscriminador.
logicalOperatorLogicalOperatorEnum (AND | OR)SíConectivo entre los hijos.
children(Grupo | Hoja)[]SíHijos del grupo; pueden ser grupos anidados.

logicalOperator es obligatorio siempre en un grupo, pero solo tiene efecto con 2 o más hijos. Con menos de 2 hijos su valor es indiferente (genera el mismo SQL), pero igual debe enviarse. Un where sin filtros se expresa como un grupo vacío: { "kind": "group", "logicalOperator": "AND", "children": [] }.

Hoja de columna — kind: "predicate"

Compara una columna escalar cualificada por su tabla.

CampoTipoRequeridoDescripción
kind"predicate"SíDiscriminador.
columnColumnRefDtoSíColumna del predicado.
comparisonOperatorComparisonOperatorEnumSíOperador de comparación.
valuePredicateValueDependeSu forma depende del operador.
{
  "kind": "predicate",
  "column": { "table": "user_properties", "column": "country" },
  "comparisonOperator": "IN",
  "value": ["CL", "MX"]
}
Hoja de registro — kind: "recordPredicate"

Filtra por una columna RECORD REPEATED (un array de structs, como attributes). Significa existe al menos un elemento del array que cumple el sub-filtro.

CampoTipoRequeridoDescripción
kind"recordPredicate"SíDiscriminador.
columnColumnRefDtoSíColumna RECORD REPEATED de una tabla del from.
subWhereSub-árbol relativoSíCondiciones sobre los sub-campos del elemento. No puede quedar vacío.
{
  "kind": "recordPredicate",
  "column": { "table": "event_transactions", "column": "attributes" },
  "subWhere": {
    "kind": "group",
    "logicalOperator": "AND",
    "children": [
      { "kind": "predicate", "field": "key", "comparisonOperator": "EQUALS", "value": "canal" },
      { "kind": "predicate", "field": "value", "comparisonOperator": "EQUALS", "value": "web" }
    ]
  }
}

Las condiciones del subWhere se evalúan sobre un mismo elemento del array, no sobre el array entero. El ejemplo de arriba selecciona filas que tienen un atributo cuya key es canal y cuyo value es web — no filas que tengan un atributo con esa key y otro, distinto, con ese value.

Qué columnas admiten un filtro de registro lo dice el catálogo: las que vienen con kind: "record".

Sub-árbol relativo

El subWhere es un grupo AND/OR cuyas hojas se resuelven contra los sub-campos que el registro declara.

La diferencia con el árbol de primer nivel es una sola, y es la fuente de error más común:

Dentro de un sub-árbol relativo los operandos no llevan tabla: se escribe "field": "key", no "column": { "table": "attributes", "column": "key" }. La hoja usa la clave field (un string), no column.

Hojas admitidas dentro de un sub-árbol relativo:

  • Hoja escalar — { "kind": "predicate", "field": string, "comparisonOperator": …, "value"?: … }. El field debe ser un sub-campo escalar del registro.
  • Registro anidado — { "kind": "recordPredicate", "field": string, "subWhere": … }. Un registro dentro de otro registro; se anida sin límite declarado, siempre que el catálogo declare cada nivel.
  • Grupo — la misma forma de grupo, para combinar las hojas anteriores con AND/OR.
Forma de value según comparisonOperator
OperadoresForma de valueEjemplo
IS_NULL, IS_NOT_NULLSin value (se omite)(se omite)
IN, NOT_INLista no vacía ScalarValue[]["CL", "MX"]
BETWEENPar [min, max][10, 20]
El resto (EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, CONTAINS, NOT_CONTAINS, STARTS_WITH, ENDS_WITH)Escalar único"activo" · 42

ScalarValue = string | number | boolean. Además, cada operador debe ser compatible con el tipo de la columna (ver reglas de validación).

groupBy — columnas de agrupación

Lista opcional de ColumnRefDto[]. Cuando el bloque agrega (declara groupBy o tiene un agregado en el select), el groupBy debe coincidir exactamente con las columnas no agregadas del select (ver reglas de validación).

having — filtro sobre agregados (HavingDto)

Misma estructura recursiva que where (grupos AND/OR anidados y misma regla de value por operador), pero la hoja opera sobre un agregado, no sobre una columna:

  • Predicado (hoja) — { kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }.
  • Grupo — idéntico al de where.
timeWindow — ventana temporal (TimeWindowDto)

Unión discriminada por timeOperator. Acota el bloque a un período de tiempo sobre una columna TIMESTAMP del catálogo (reference). Es un campo hermano del where, no un nodo del árbol de filtros.

timeOperatorCampos adicionales
ALL_TIME(ninguno) — sin acotar por tiempo.
IN_THE_LASTreference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0
IN_THE_FIRSTreference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0
SINCEreference: ColumnRefDto, from: string (YYYY-MM-DD)
BETWEENreference: ColumnRefDto, from: string (YYYY-MM-DD), to: string (YYYY-MM-DD)

TimeUnitEnum: DAYS | WEEKS | MONTHS | YEARS.

Semántica de negocio de cada operador:

  • ALL_TIME — sin ventana; considera todo el histórico.
  • IN_THE_LAST — ventana rodante relativa a "ahora": los últimos value unit (p. ej. últimos 30 DAYS). Se recomputa en cada corrida.
  • IN_THE_FIRST — ventana relativa al origen del ciclo de vida del usuario (su fecha de alta): los primeros value unit de vida de cada usuario. Se ancla por usuario, no a una fecha fija.
  • SINCE — desde la fecha civil from (inclusiva) en adelante.
  • BETWEEN — entre from y to, ambas fechas civiles inclusivas de su día completo.

Las fechas civiles YYYY-MM-DD (SINCE, BETWEEN) se resuelven al inicio del día en la zona horaria del tenant. El detalle de zona horaria es interno; basta con enviar la fecha civil.

composedWith[] — composición con otras audiencias

Cada elemento (CreateCompositionDto) combina esta definición con otra audiencia vía una operación de conjunto:

CampoTipoRequeridoDescripción
operandCriteriaIdstring (UUID)SíId de la otra audiencia (operando). Debe existir.
setOperatorSetOperatorEnumSíUNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT.

Ejemplo

Audiencia de usuarios con 3 o más transacciones COMPLETED en los últimos 90 días. Proyecta solo user_id, igual que la definición por defecto de la audiencia raíz (ver Intersección con la audiencia raíz). Se pide una muestra de 50 filas (limit).

curl -X POST https://api.reten.ai/api/audience-criterias/preview-from-body \
  -H "Authorization: Bearer <token>" \
  -H "x-tenant-id: <tenant-id>" \
  -H "x-project-id: <project-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "queryBlocks": [
      {
        "order": 0,
        "from": { "table": "event_transactions" },
        "select": {
          "distinct": false,
          "items": [
            {
              "kind": "column",
              "column": { "table": "event_transactions", "column": "user_id" }
            }
          ]
        },
        "timeWindow": {
          "timeOperator": "IN_THE_LAST",
          "reference": { "table": "event_transactions", "column": "event_date" },
          "unit": "DAYS",
          "value": 90
        },
        "where": {
          "kind": "group",
          "logicalOperator": "AND",
          "children": [
            {
              "kind": "predicate",
              "column": { "table": "event_transactions", "column": "status" },
              "comparisonOperator": "EQUALS",
              "value": "COMPLETED"
            }
          ]
        },
        "groupBy": [{ "table": "event_transactions", "column": "user_id" }],
        "having": {
          "kind": "group",
          "logicalOperator": "AND",
          "children": [
            {
              "kind": "predicate",
              "aggregate": {
                "aggregationOperator": "COUNT",
                "operand": { "kind": "star" }
              },
              "comparisonOperator": "GREATER_THAN_OR_EQUAL",
              "value": 3
            }
          ]
        }
      }
    ],
    "limit": 50
  }'
import axios from 'axios';

const body = {
  queryBlocks: [
    {
      order: 0,
      from: { table: 'event_transactions' },
      select: {
        distinct: false,
        items: [
          {
            kind: 'column',
            column: { table: 'event_transactions', column: 'user_id' },
          },
        ],
      },
      timeWindow: {
        timeOperator: 'IN_THE_LAST',
        reference: { table: 'event_transactions', column: 'event_date' },
        unit: 'DAYS',
        value: 90,
      },
      where: {
        kind: 'group',
        logicalOperator: 'AND',
        children: [
          {
            kind: 'predicate',
            column: { table: 'event_transactions', column: 'status' },
            comparisonOperator: 'EQUALS',
            value: 'COMPLETED',
          },
        ],
      },
      groupBy: [{ table: 'event_transactions', column: 'user_id' }],
      having: {
        kind: 'group',
        logicalOperator: 'AND',
        children: [
          {
            kind: 'predicate',
            aggregate: {
              aggregationOperator: 'COUNT',
              operand: { kind: 'star' },
            },
            comparisonOperator: 'GREATER_THAN_OR_EQUAL',
            value: 3,
          },
        ],
      },
    },
  ],
  limit: 50, // opcional; por defecto 100
};

const response = await axios.post(
  'https://api.reten.ai/api/audience-criterias/preview-from-body',
  body,
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const preview = response.data; // IPreviewAudienceCriteriaResult

La respuesta se documenta en IPreviewAudienceCriteriaResult, con un ejemplo de rows no vacío.


POST /api/audience-criterias/:id/preview

Ejecuta un criterio ya guardado, referenciado por su id. La definición no viaja en el cuerpo: se toma del criterio persistido en el proyecto activo. El cuerpo solo lleva limit.

Auth: Requerida — permiso audience-criteria:view-sql

Como el anterior, responde 201 Created.

Parámetros de ruta

ParámetroTipoRequeridoDescripción
idUUIDSíId del audience criteria. Debe ser un UUID válido (si no, 400).

Cuerpo de la solicitud

PreviewQueryDto — un único campo, opcional:

CampoTipoRequeridoDescripción
limitnumber (entero)NoTamaño de la muestra de rows. Entero 1..500. Default 100 (ver limit).

Un cuerpo vacío ({}) es válido: usa el limit por defecto (100).

Ejemplo

curl -X POST https://api.reten.ai/api/audience-criterias/f47ac10b-58cc-4372-a567-0e02b2c3d479/preview \
  -H "Authorization: Bearer <token>" \
  -H "x-tenant-id: <tenant-id>" \
  -H "x-project-id: <project-id>" \
  -H "Content-Type: application/json" \
  -d '{ "limit": 50 }'
import axios from 'axios';

const id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
const response = await axios.post(
  `https://api.reten.ai/api/audience-criterias/${id}/preview`,
  { limit: 50 }, // opcional; por defecto 100
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const preview = response.data; // IPreviewAudienceCriteriaResult

limit — tamaño de la muestra

limit acota solo cuántas filas trae la respuesta en rows (la muestra que ves), no el universo que matchea la audiencia.

  • Rango: entero 1..500 (menor a 1 o mayor a 500 ⇒ 400).
  • Default: 100 (PREVIEW_ROW_LIMIT_DEFAULT) cuando se omite.

limit gobierna el tamaño de la muestra en rows; totalCount es el universo completo que matchea la audiencia y no está acotado por limit. Preview corre dos consultas sobre la misma definición: una SELECT ... LIMIT <limit> para la muestra y un SELECT COUNT(*) para el total. Por eso podés ver rows.length = 50 con totalCount = 4821: rows es un vistazo, totalCount es el tamaño real de la audiencia.

Respuesta — IPreviewAudienceCriteriaResult

CampoTipoDescripción
rowsRecord<string, unknown>[]Muestra de filas reales, acotada por limit. Cada fila trae las columnas que proyecta la audiencia (su select, con sus alias); por eso rows no tiene una forma fija. Esa proyección tiene que ser compatible con la de la audiencia raíz (ver Intersección con la audiencia raíz).
totalCountnumberTotal de filas que matchea la audiencia, sin acotar por limit (el universo completo).
bytesProcessednumberBytes procesados por BigQuery en la corrida. Suma los bytes de ambas consultas (muestra + conteo).

Distinguí siempre rows (la muestra, como mucho limit filas) de totalCount (el universo, sin límite). Si totalCount > rows.length, hay más filas de las que la muestra alcanza a mostrar.

Ejemplo

Respuesta 201 al ejemplo de arriba (limit: 50). La audiencia matchea 4821 usuarios; rows muestra los primeros 50 (aquí se recortan a 3 por brevedad), cada uno con la única columna proyectada (user_id):

{
  "rows": [
    { "user_id": "u_9f3a1c" },
    { "user_id": "u_2b7e04" },
    { "user_id": "u_c081ff" }
  ],
  "totalCount": 4821,
  "bytesProcessed": 5242880
}

Las claves de cada objeto en rows (user_id) salen del select de la audiencia. totalCount (4821) es el universo completo, mayor que las 50 filas de la muestra.

Intersección con la audiencia raíz

Todo proyecto tiene una audiencia raíz, y toda otra audiencia del proyecto se evalúa dentro de ella. Preview ejecuta el mismo SQL que devuelve Generar SQL: para cualquier audiencia que no sea la audiencia raíz, intersectado con la audiencia raíz del proyecto. Por eso rows y totalCount son las filas que matchea la audiencia dentro de la audiencia raíz, y bytesProcessed incluye la parte de la consulta que corresponde a la audiencia raíz.

No hay intersección cuando la audiencia ejecutada es la propia audiencia raíz — :id/preview con el id de la audiencia raíz, o un borrador de preview-from-body cuyo criteriaId es la audiencia raíz.

La intersección es posicional (SELECT * en los dos lados): la audiencia debe proyectar las mismas columnas que la audiencia raíz, en el mismo orden y con tipos compatibles — con la definición por defecto de la audiencia raíz, solo user_id. Si no, BigQuery rechaza la consulta al ejecutarla y preview responde 400 (Query execution failed: <detalle>).

Reglas de validación

Antes de ejecutar, la definición pasa por las mismas dos capas de validación que Crear, y ambas devuelven 400 Bad Request:

  1. Validación del DTO (estructura, tipos, enums, campos requeridos, limit en rango). message es un arreglo de strings y se rechazan propiedades desconocidas (incluidos name/description).
  2. Validación de negocio (la definición debe generar SQL válido y coherente contra el catálogo). message es un string único con la regla incumplida.

La validación de negocio corre en ambos endpoints: preview-from-body valida la definición del cuerpo, y :id/preview valida el criterio guardado antes de ejecutarlo. El catálogo completo de reglas y sus mensajes exactos está en Crear → Reglas de validación.

Si la definición supera las dos capas pero la ejecución en BigQuery falla —por exceder el límite de maximumBytesBilled (10 GB), agotar el timeout de 30 s, o cualquier otro error del motor— la respuesta también es 400, con un message del tipo Query execution failed: <detalle>.

Respuestas de error

EstadoDescripción
400DTO inválido (campo faltante, tipo incorrecto, enum fuera de rango, limit fuera de 1..500, propiedad desconocida como name/description), una regla de negocio incumplida, o un fallo de ejecución en BigQuery (Query execution failed: ..., incluye exceder 10 GB o el timeout de 30 s). En :id/preview, también si el id no es un UUID válido; en preview-from-body, si no lo es el criteriaId.
401Falta o es inválido el Authorization: Bearer.
403El usuario no tiene el permiso audience-criteria:view-sql, o el x-tenant-id no está autorizado.
404En :id/preview, no existe un audience criteria con ese id en el proyecto activo. En preview-from-body, un operandCriteriaId de composedWith o el criteriaId no existe en el proyecto activo.