Reten Docs
Audiências

Pré-visualizar

Executa um audience criteria contra o BigQuery e retorna uma amostra de linhas reais mais o total, a partir do corpo (ainda não persistido) ou por id de um critério salvo.

Pré-visualizar

Executa um audience criteria contra o BigQuery (limitado por limit) e retorna uma amostra das linhas reais correspondentes mais o total do universo. Um único motor atende dois pontos de entrada:

  • POST /api/audience-criterias/preview-from-body — executa uma definição enviada no corpo (ainda não persistida).
  • POST /api/audience-criterias/:id/preview — executa um critério já salvo, referenciado por seu id.

Ambos retornam a mesma forma de resposta: IPreviewAudienceCriteriaResult.

Diferente de Gerar SQL, preview não retorna SQL; e diferente de Validar (que só faz um dry-run), preview traz dados de verdade: executa a consulta e mostra linhas.

Dentro de um projeto, toda audiência que não seja a audiência raiz é executada intersectada com a audiência raiz do projeto — veja Interseção com a audiência raiz.

Auth: Obrigatória — permissão audience-criteria:view-sql (ambos os endpoints).

Preview consome cômputo do BigQuery em cada chamada (executa a audiência de verdade, não um dry-run). Para limitar o custo, cada consulta roda com maximumBytesBilled de 10 GB (10737418240 bytes) e um timeout de 30 s (jobTimeoutMs): uma consulta que exceda qualquer um dos dois limites é rejeitada com 400 em vez de ser cobrada. Use um limit pequeno enquanto você itera.

O SQL que é executado aponta para o BigQuery: referencia as tabelas como `<gcp-project>.<dataset>.<table>`, onde o <gcp-project> (o projeto do Google Cloud, não um projeto da Reten) vem da configuração de ambiente da API e o <dataset> é resolvido a partir do tenant. Nenhum dos dois faz parte do contrato de entrada.


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

Executa uma definição enviada no corpo, sem persisti-la.

Auth: Obrigatória — permissão audience-criteria:view-sql

O endpoint responde 201 Created —não 200—, mesmo que não crie nenhum recurso.

Corpo da requisição

Nível raiz (PreviewFromBodyDto): a definição do critério (CriteriaDefinitionDto) mais o campo limit.

CampoTipoObrigatórioDescrição
queryBlocksQueryBlockDto[]SimNo mínimo 1 elemento. Cada bloco ≈ um statement SQL completo.
composedWithCreateCompositionDto[]NãoCompõe esta definição com outras audiências via operações de conjunto.
limitnumber (integer)NãoTamanho da amostra de rows. Inteiro 1..500. Padrão 100 (veja limit).
criteriaIdstring (UUID) | nullNãoA audiência salva que esta definição edita: o rascunho assume a identidade dessa audiência, então gera o mesmo resultado que essa audiência gerará ao ser salva (veja Interseção com a audiência raiz). Ausente ou null: uma audiência nova. Deve ser um UUID válido (caso contrário, 400) de uma audiência do projeto ativo (caso contrário, 404).

Diferente de Criar, a definição não leva name nem description: é apenas a árvore de consulta mais limit. A validação é estrita, então enviar name/description (ou qualquer outra propriedade desconhecida) produz um 400.

Definição do critério

É a mesma árvore que Criar recebe, sem os campos name/description. Está documentada aqui de forma autocontida.

queryBlocks[] — bloco de consulta

Cada elemento de queryBlocks (QueryBlockDto) é um statement SQL completo:

CampoTipoObrigatórioDescrição
ordernumber (integer)SimPosição do bloco; inteiro ≥ 0.
setOperatorSetOperatorEnumNãoCombina este bloco com o anterior. Ausente no primeiro bloco; obrigatório nos demais (veja regras).
fromFromDtoSimFonte de dados: tabela principal + joins.
selectSelectDtoSimProjeção (colunas e/ou agregações).
timeWindowTimeWindowDtoNãoJanela temporal do bloco (união discriminada por timeOperator).
whereWhereDtoSimÁrvore de predicados. Obrigatório mesmo que seja um grupo vazio.
groupByColumnRefDto[]NãoColunas de agrupamento.
havingHavingDtoNãoFiltro sobre agregados.

O setOperator de um query block combina blocos dentro da mesma definição; não deve ser confundido com o setOperator de uma composição, que combina critérios inteiros. São dois níveis distintos de operação de conjunto.

ColumnRefDto — referência de coluna

Usado em toda a árvore (select, filtros, group by, joins, agregados, janela temporal):

CampoTipoObrigatórioDescrição
tablestringSimTabela do catálogo. Não vazio.
columnstringSimColuna. Não vazio. Pode ser um caminho STRUCT (ex.: "pricing.final_amount").

Toda coluna é sempre qualificada com sua table para evitar ambiguidade.

from — fonte de dados (FromDto)
CampoTipoObrigatórioDescrição
tablestringSimTabela principal do catálogo. Não vazio.
joinsJoinDto[]NãoJoins com outras tabelas. Padrão [].
unnestsColumnRefDto[]NãoColunas repetidas declaradas como fonte de linhas (uma linha por elemento). Uma audiência não as aceita: omita o campo ou envie [] (o padrão); uma lista não vazia retorna 400.

Cada JoinDto:

CampoTipoObrigatórioDescrição
typeJoinTypeEnumSimINNER | LEFT | RIGHT | FULL.
tablestringSimTabela a unir (do catálogo). Não vazio.
onJoinPredicateDto[]SimCondições de join.

Cada JoinPredicateDto é um par de colunas: { left: ColumnRefDto, right: ColumnRefDto }.

select — projeção (SelectDto)
CampoTipoObrigatórioDescrição
distinctbooleanSimAplica SELECT DISTINCT.
items(SelectColumnItem | SelectAggregateItem)[]SimNão vazio. União discriminada pelo campo kind.

Os items são discriminados por kind:

  • Coluna — kind: "column":

    CampoTipoObrigatórioDescrição
    kind"column"SimDiscriminador.
    columnColumnRefDtoSimColuna projetada.
    aliasstringNãoDeve corresponder a ^[A-Za-z_][A-Za-z0-9_]*$.
  • Agregado — kind: "aggregate":

    CampoTipoObrigatórioDescrição
    kind"aggregate"SimDiscriminador.
    aggregateAggregateDtoSimExpressão de agregação.
    aliasstringSimAqui o alias é obrigatório. Mesmo regex de identificador.

AggregateDto:

CampoTipoObrigatórioDescrição
aggregationOperatorAggregationOperatorEnumSimCOUNT | SUM | AVG | MIN | MAX.
operandobjectSimO que é agregado. União discriminada por kind: column, star ou arithmetic (veja abaixo).
distinctbooleanNãoAplica DISTINCT dentro do agregado.

operand —o que é agregado— é discriminado por kind:

kindCampos adicionaisSignificado
columncolumn: ColumnRefDtoUma coluna do catálogo.
star(nenhum)*: conta linhas. Só é válido com COUNT e sem distinct (COUNT(DISTINCT *) não é permitido).
arithmeticarithmeticOperator: ArithmeticOperatorEnum, left, rightExpressão binária. Cada lado é outro operando: uma column ou um arithmetic aninhado (nunca star).

ArithmeticOperatorEnum: ADD | SUBTRACT | MULTIPLY | SAFE_DIVIDE (uma divisão que retorna NULL quando o denominador é zero, em vez de fazer a consulta falhar). As colunas de uma expressão aritmética precisam ser numéricas (INT64, NUMERIC, BIGNUMERIC, FLOAT64), e as expressões se aninham até 3 níveis de arithmetic, contando o externo.

where — árvore de predicados (WhereDto)

O where é uma árvore recursiva. O nó raiz é sempre um grupo, e suas folhas são de dois tipos:

kindO que filtra
groupNão é uma folha: agrupa filhos com AND / OR. Os grupos podem ser aninhados.
predicateUma coluna escalar de uma tabela do from.
recordPredicateUma coluna RECORD REPEATED: pelo menos 1 elemento satisfaz o sub-filtro.
Grupo — kind: "group"
CampoTipoObrigatórioDescrição
kind"group"SimDiscriminador.
logicalOperatorLogicalOperatorEnum (AND | OR)SimConectivo entre os filhos.
children(Group | Leaf)[]SimFilhos do grupo; podem ser grupos aninhados.

logicalOperator é sempre obrigatório em um grupo, mas só tem efeito com 2 ou mais filhos. Com menos de 2 filhos seu valor é irrelevante (gera o mesmo SQL), mas ainda assim deve ser enviado. Um where sem filtros é expresso como um grupo vazio: { "kind": "group", "logicalOperator": "AND", "children": [] }.

Folha de coluna — kind: "predicate"

Compara uma coluna escalar qualificada por sua tabela.

CampoTipoObrigatórioDescrição
kind"predicate"SimDiscriminador.
columnColumnRefDtoSimColuna do predicado.
comparisonOperatorComparisonOperatorEnumSimOperador de comparação.
valuePredicateValueDependeSeu formato depende do operador.
{
  "kind": "predicate",
  "column": { "table": "user_properties", "column": "country" },
  "comparisonOperator": "IN",
  "value": ["CL", "MX"]
}
Folha de registro — kind: "recordPredicate"

Filtra por uma coluna RECORD REPEATED (um array de structs, como attributes). Significa que pelo menos um elemento do array satisfaz o sub-filtro.

CampoTipoObrigatórioDescrição
kind"recordPredicate"SimDiscriminador.
columnColumnRefDtoSimColuna RECORD REPEATED de uma tabela do from.
subWhereSub-árvore relativaSimCondições sobre os sub-campos do elemento. Não pode ficar vazio.
{
  "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" }
    ]
  }
}

As condições do subWhere são avaliadas sobre um mesmo elemento do array, e não sobre o array inteiro. O exemplo acima seleciona linhas que têm um atributo cujo key é canal e cujo value é web — não linhas que têm um atributo com esse key e outro, diferente, com esse value.

Quais colunas aceitam um filtro de registro é informado pelo catálogo: as que vêm com kind: "record".

Sub-árvore relativa

O subWhere é um grupo AND/OR cujas folhas são resolvidas contra os sub-campos que o registro declara.

Há uma única diferença em relação à árvore de nível superior, e ela é a origem mais comum de erros:

Dentro de uma sub-árvore relativa os operandos não levam tabela: você escreve "field": "key", e não "column": { "table": "attributes", "column": "key" }. A folha usa a chave field (uma string), não column.

Folhas permitidas dentro de uma sub-árvore relativa:

  • Folha escalar — { "kind": "predicate", "field": string, "comparisonOperator": …, "value"?: … }. O field deve ser um sub-campo escalar do registro.
  • Registro aninhado — { "kind": "recordPredicate", "field": string, "subWhere": … }. Um registro dentro de outro registro; aninha sem limite declarado, desde que o catálogo declare cada nível.
  • Grupo — o mesmo formato de grupo, para combinar as folhas anteriores com AND/OR.
Forma de value segundo comparisonOperator
OperadoresForma de valueExemplo
IS_NULL, IS_NOT_NULLSem value (omitido)(omitido)
IN, NOT_INLista não vazia ScalarValue[]["CL", "MX"]
BETWEENPar [min, max][10, 20]
O 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"ativo" · 42

ScalarValue = string | number | boolean. Além disso, cada operador deve ser compatível com o tipo da coluna (veja regras de validação).

groupBy — colunas de agrupamento

Lista opcional de ColumnRefDto[]. Quando o bloco agrega (declara groupBy ou tem um agregado no select), o groupBy deve corresponder exatamente às colunas não agregadas do select (veja regras de validação).

having — filtro sobre agregados (HavingDto)

Mesma estrutura recursiva que where (grupos AND/OR aninhados e a mesma regra de value por operador), mas a folha opera sobre um agregado, não sobre uma coluna:

  • Predicado (folha) — { kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }.
  • Grupo — idêntico ao de where.
timeWindow — janela temporal (TimeWindowDto)

União discriminada por timeOperator. Limita o bloco a um período de tempo sobre uma coluna TIMESTAMP do catálogo (reference). É um campo irmão de where, não um nó da árvore de filtros.

timeOperatorCampos adicionais
ALL_TIME(nenhum) — não limitado por tempo.
IN_THE_LASTreference: ColumnRefDto, unit: TimeUnitEnum, value: inteiro > 0
IN_THE_FIRSTreference: ColumnRefDto, unit: TimeUnitEnum, value: inteiro > 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 negócio de cada operador:

  • ALL_TIME — sem janela; considera todo o histórico.
  • IN_THE_LAST — janela rolante relativa a "agora": os últimos value unit (ex.: últimos 30 DAYS). Recalculada a cada execução.
  • IN_THE_FIRST — janela relativa à origem do ciclo de vida do usuário (sua data de cadastro): os primeiros value unit de vida de cada usuário. É ancorada por usuário, não a uma data fixa.
  • SINCE — a partir da data civil from (inclusive).
  • BETWEEN — entre from e to, ambas datas civis inclusive do dia completo.

As datas civis YYYY-MM-DD (SINCE, BETWEEN) resolvem para o início do dia no fuso horário do tenant. O detalhe do fuso horário é interno; basta enviar a data civil.

composedWith[] — composição com outras audiências

Cada elemento (CreateCompositionDto) combina esta definição com outra audiência via uma operação de conjunto:

CampoTipoObrigatórioDescrição
operandCriteriaIdstring (UUID)SimId da outra audiência (operando). Deve existir.
setOperatorSetOperatorEnumSimUNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT.

Exemplo

Audiência de usuários com 3 ou mais transações COMPLETED nos últimos 90 dias. Projeta apenas user_id, como a definição padrão da audiência raiz (veja Interseção com a audiência raiz). É solicitada uma amostra de 50 linhas (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; padrão 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

A resposta está documentada em IPreviewAudienceCriteriaResult, com um exemplo de rows não vazio.


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

Executa um critério já salvo, referenciado por seu id. A definição não viaja no corpo: é obtida do critério persistido no projeto ativo. O corpo só carrega limit.

Auth: Obrigatória — permissão audience-criteria:view-sql

Como o anterior, responde 201 Created.

Parâmetros de rota

ParâmetroTipoObrigatórioDescrição
idUUIDSimO id do audience criteria. Deve ser um UUID válido (caso contrário, 400).

Corpo da requisição

PreviewQueryDto — um único campo, opcional:

CampoTipoObrigatórioDescrição
limitnumber (integer)NãoTamanho da amostra de rows. Inteiro 1..500. Padrão 100 (veja limit).

Um corpo vazio ({}) é válido: usa o limit padrão (100).

Exemplo

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; padrão 100
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const preview = response.data; // IPreviewAudienceCriteriaResult

limit — tamanho da amostra

limit limita apenas quantas linhas a resposta traz em rows (a amostra que você vê), não o universo correspondente à audiência.

  • Intervalo: inteiro 1..500 (menor que 1 ou maior que 500 ⇒ 400).
  • Padrão: 100 (PREVIEW_ROW_LIMIT_DEFAULT) quando omitido.

limit governa o tamanho da amostra em rows; totalCount é o universo completo correspondente à audiência e não é limitado por limit. Preview executa duas consultas sobre a mesma definição: um SELECT ... LIMIT <limit> para a amostra e um SELECT COUNT(*) para o total. Por isso você pode ver rows.length = 50 com totalCount = 4821: rows é um vislumbre, totalCount é o tamanho real da audiência.

Resposta — IPreviewAudienceCriteriaResult

CampoTipoDescrição
rowsRecord<string, unknown>[]Amostra de linhas reais, limitada por limit. Cada linha traz as colunas que a audiência projeta (seu select, com seus aliases); por isso rows não tem uma forma fixa. Essa projeção precisa ser compatível com a da audiência raiz (veja Interseção com a audiência raiz).
totalCountnumberTotal de linhas correspondentes à audiência, não limitado por limit (o universo completo).
bytesProcessednumberBytes processados pelo BigQuery na execução. Soma os bytes de ambas as consultas (amostra + contagem).

Sempre distinga rows (a amostra, no máximo limit linhas) de totalCount (o universo, sem limite). Se totalCount > rows.length, há mais linhas do que a amostra consegue mostrar.

Exemplo

Resposta 201 ao exemplo acima (limit: 50). A audiência corresponde a 4821 usuários; rows mostra os primeiros 50 (aqui reduzidos a 3 por brevidade), cada um com a única coluna projetada (user_id):

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

As chaves de cada objeto em rows (user_id) vêm do select da audiência. totalCount (4821) é o universo completo, maior que as 50 linhas da amostra.

Interseção com a audiência raiz

Todo projeto tem uma audiência raiz, e toda outra audiência do projeto é avaliada dentro dela. O preview executa o mesmo SQL que Gerar SQL retorna: para qualquer audiência que não seja a audiência raiz, intersectado com a audiência raiz do projeto. Por isso rows e totalCount são as linhas correspondentes à audiência dentro da audiência raiz, e bytesProcessed inclui a parte da consulta que corresponde à audiência raiz.

Não há interseção quando a audiência executada é a própria audiência raiz — :id/preview com o id da audiência raiz, ou um rascunho de preview-from-body cujo criteriaId é a audiência raiz.

A interseção é posicional (SELECT * nos dois lados): a audiência deve projetar as mesmas colunas que a audiência raiz, na mesma ordem e com tipos compatíveis — com a definição padrão da audiência raiz, apenas user_id. Caso contrário, o BigQuery rejeita a consulta ao executá-la e o preview responde 400 (Query execution failed: <detail>).

Regras de validação

Antes de executar, a definição passa pelas mesmas duas camadas de validação que Criar, e ambas retornam 400 Bad Request:

  1. Validação de DTO (estrutura, tipos, enums, campos obrigatórios, limit dentro do intervalo). message é um array de strings e propriedades desconhecidas são rejeitadas (incluindo name/description).
  2. Validação de negócio (a definição deve gerar SQL válido e coerente com o catálogo). message é uma string única com a regra violada.

A validação de negócio roda em ambos os endpoints: preview-from-body valida a definição no corpo, e :id/preview valida o critério salvo antes de executá-lo. O catálogo completo de regras e suas mensagens exatas está em Criar → Regras de validação.

Se a definição passar pelas duas camadas mas a execução no BigQuery falhar —por exceder o limite de maximumBytesBilled (10 GB), esgotar o timeout de 30 s, ou qualquer outro erro do motor— a resposta também é 400, com um message no formato Query execution failed: <detail>.

Respostas de erro

StatusDescrição
400DTO inválido (campo ausente, tipo incorreto, enum fora do intervalo, limit fora de 1..500, propriedade desconhecida como name/description), uma regra de negócio violada, ou uma falha de execução no BigQuery (Query execution failed: ..., inclui exceder 10 GB ou o timeout de 30 s). Em :id/preview, também se o id não for um UUID válido; em preview-from-body, se o criteriaId não for.
401Ausente ou inválido o Authorization: Bearer.
403O usuário não tem a permissão audience-criteria:view-sql, ou o x-tenant-id não está autorizado.
404Em :id/preview, não existe um audience criteria com esse id no projeto ativo. Em preview-from-body, um operandCriteriaId em composedWith ou o criteriaId não existe no projeto ativo.