Skip to content

Constructor de reportes ​

English: Report builder.

El constructor de reportes responde preguntas sobre las llamadas que un tenant colocó, sobre un catálogo cerrado de dimensiones y métricas. Lo sirven tres rutas: GET /v1/report-catalog (el vocabulario), POST /v1/reports/run (un agregado, o una página de intentos) y /v1/report-definitions (consultas guardadas). Los nombres de campo, enums, límites y códigos de error de abajo están copiados del documento OpenAPI vendorizado (core v0.105.0, openapi/dialer-v1.yaml); la Referencia de la API generada de él es la autoridad sobre las formas.

Disponibilidad ​

CapacidadOperación de la spec
Leer el catálogogetReportCatalog
Ejecutar un agregado o una página de detalle, JSON o CSVrunReport
Definiciones guardadas: listar, crear, leer, reemplazar, borrarlistReportDefinitions, createReportDefinition, getReportDefinition, replaceReportDefinition, deleteReportDefinition

Ejecutar depende de la bandera del tenant report_builder_enabled: con la bandera apagada, POST /v1/reports/run responde 503 report_builder_disabled y no se lee nada (runReport). El catálogo sigue respondiendo con la bandera apagada (getReportCatalog) y las definiciones guardadas no dependen de ella (listReportDefinitions). No des por hecho que la bandera está encendida en tu tenant: llama a la ruta de ejecución y maneja el 503.

Credenciales: las rutas son de ámbito tenant, así que sirve una llave dd_ o un login del dashboard (runReport). Una sesión de soporte dds_ es solo GET y no puede ejecutar reportes.

bash
curl -s "$API/v1/report-catalog" -H "Authorization: Bearer $TOKEN"

El cuerpo 200 es el schema ReportCatalog: fact (siempre call_attempts), version (sube cuando cambia una definición publicada), dimensions, metrics, detail_columns y limits.

  • La unidad es el intento: el marcado original que la plataforma colocó más las correcciones anexadas. disposition es la de la última corrección que cierra el intento, e in_flight mientras no hay ninguna (getReportCatalog).
  • Dimensiones publicadas en core v0.105.0: campaign_id, agent_id, disposition, source, trunk_kind, amd_verdict, ai_used, hangup_cause, day, week, month.
  • Métricas: attempts, contacts, contact_rate_bp (puntos base), accounts_attempted, talk_seconds, ai_calls. Cada métrica tiene una sola definición, compartida con los contadores fijos de GET /v1/stats/summary.
  • Ninguna dimensión lleva datos personales: pii es null en todas.
  • Límites (el ejemplo de getReportCatalog): max_window_days: 31, max_groups: 1000, max_dimensions: 4, max_filter_values: 100, max_detail_rows: 1000. Lee los valores vivos del catálogo en lugar de fijarlos en tu código.

Las listas de claves salen de lib/dialer/reports/catalog.ex en el tag v0.105.0 de core; toma GET /v1/report-catalog como fuente de verdad, porque una versión posterior puede añadir claves.

Ejecutar un agregado ​

bash
curl -s -X POST "$API/v1/reports/run" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "from": "2026-10-01T00:00:00Z",
    "to":   "2026-10-08T00:00:00Z",
    "dimensions": ["campaign_id", "disposition"],
    "metrics": ["attempts", "contacts", "contact_rate_bp"],
    "filters": { "source": ["platform"] },
    "zone": "UTC"
  }'

Reglas de runReport y del schema ReportRunRequest:

  • from y to son marcas RFC 3339 obligatorias. La ventana es [from, to) sobre started_at y de 31 días como máximo.
  • Hasta 4 dimensions (ninguna da una sola fila de totales) y al menos una métrica.
  • filters asocia una dimensión filtrable con una lista no vacía de hasta 100 valores.
  • zone es UTC por defecto; es un nombre IANA Area/Location y fija el calendario de day, week y month.
  • Cualquier campo fuera de from, to, dimensions, metrics, filters y zone (por ejemplo tenant_id) es 422 unknown_field: el tenant es el de quien llama.
  • contact_rate_bp son contactos entre intentos en puntos base, redondeado a la mitad hacia arriba, y null (nunca 0) cuando no hay intentos.

El cuerpo 200 es el schema ReportRunResult: fact, catalog_version, from, to, zone, columns (cada una con kind dimension o metric), row_count y rows.

CSV ​

Añade ?format=csv. Una cabecera Accept sola no basta (runReport). El CSV lleva una fila de cabecera con las claves de columna, neutraliza las fórmulas de hoja de cálculo y trae la cabecera X-Artifact-SHA256, el SHA-256 del cuerpo del CSV. Compárala con tu propio hash del cuerpo para detectar una descarga truncada o alterada:

bash
curl -s -D headers.txt -o report.csv -X POST "$API/v1/reports/run?format=csv" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"from":"2026-10-01T00:00:00Z","to":"2026-10-08T00:00:00Z","metrics":["attempts"]}'
grep -i '^x-artifact-sha256' headers.txt
sha256sum report.csv

Modo detalle: una fila por intento ​

"mode": "detail" responde una fila por intento colocado en lugar de un grupo (runReport). Acepta columns (de detail_columns del catálogo), filters, unmask, limit (por defecto y máximo 1000) y after.

json
{ "mode": "detail", "from": "2026-10-01T00:00:00Z", "to": "2026-10-02T00:00:00Z",
  "columns": ["attempt_id", "to_number"], "limit": 500 }
  • La paginación es un keyset sobre (started_at, id), del más antiguo al más reciente. La página trae next_cursor; envíalo como after para la siguiente. next_cursor es null en la última página. Un after mal formado es 400 invalid_cursor.
  • dimensions, metrics y zone son solo del agregado; columns, unmask, limit y after son solo del detalle. La clave del modo equivocado es 422 field_not_for_mode.
  • La nota de texto libre note y compliance_snapshot no son columnas.
  • Con ?format=csv, cada fila lleva una columna export_cursor (la última es el after de la página siguiente) y la respuesta trae X-Artifact-SHA256.

Columnas de transferencia ​

Dos columnas del detalle describen una llamada transferida. Están en core main (core#2403, ADR #297 de core), posteriores al tag v0.129.0, y allí la version del catálogo es 3 (@version 3 en lib/dialer/reports/catalog.ex); las definiciones están en ese mismo fichero, claves transfer_count y transfer_chain. Lee detail_columns del catálogo vivo para ver si tu despliegue las tiene.

  • transfer_count (entero): los traspasos de control de la llamada, las filas control_handoff aplicadas de call_control_actions. 0 en una llamada que nadie transfirió.
  • transfer_chain (texto): quién tuvo la llamada, en orden: el agente con quien empezó (unassigned si empezó sin ninguno) y luego el de cada traspaso, unidos por >; external donde el control salió de la población de agentes. null si nadie la transfirió. Los tramos con rol y duración son transfer_chain de GET /v1/calls/{call_id} (ver la guía de la API REST).

Ninguna columna lleva datos personales (pii es null). En CSV son columnas normales como las demás.

Enmascarado de teléfonos y unmask ​

Una columna cuyo pii es phone o identifier sale enmascarada por defecto: *** más los últimos cuatro caracteres, leídos de la máscara almacenada sin descifrar nada (getReportCatalog).

"unmask": true devuelve esas columnas completas. Solo lo permite una llave dd_ o un login ddu_ con el rol account_admin. En los demás casos (runReport):

Quien llamaRespuesta
Rol member403 role_forbidden (nunca una respuesta enmascarada en silencio)
Sesión de soporte dds_403 forbidden
unmask sin ninguna columna PII en columns422 unmask_without_pii_column

Cada página con unmask escribe una fila en una tabla de evidencia de solo anexar, en la misma transacción que la lectura: actor, la definición, las columnas, las columnas sin máscara y el número de filas, nunca los valores. Si esa fila no se puede escribir, no se devuelve nada. En CSV un número sin máscara sale detrás de la guarda de fórmulas (con prefijo '). El campo unmasked de la respuesta lista las columnas PII devueltas completas, y va vacío si salieron enmascaradas (ReportRunResult).

Definiciones guardadas ​

Una definición es una consulta sobre el catálogo (dimensions, metrics, filters), nunca su resultado: la ventana y la zona son parámetros de cada ejecución (listReportDefinitions).

RutaPara qué
GET /v1/report-definitionsLista las definiciones del tenant, por nombre
POST /v1/report-definitionsGuarda una; 201 con { "report_definition": {...} }
GET /v1/report-definitions/{id}Lee una
PUT /v1/report-definitions/{id}Reemplazo total de name, dimensions, metrics, filters
DELETE /v1/report-definitions/{id}Borra; 204
  • El cuerpo se valida contra el catálogo igual que una ejecución en línea. Una clave que el catálogo no publica es 422 y el mensaje la nombra.
  • name va de 1 a 120 caracteres y es único por tenant: un duplicado es 409 name_taken.
  • El tenant y el autor son los de quien llama. tenant_id o created_by en el cuerpo es 422 unknown_field. created_by es null si lo guardó una llave dd_.
  • fact y catalog_version los sella el servidor; un PUT refresca catalog_version.
  • Aislamiento por tenant. La tabla de definiciones tiene seguridad por filas con una política tenant_isolation sobre el id del tenant, así que la definición de otro tenant es un 404, no un 403 (runReport; migración create_report_definitions de core v0.105.0).

Ejecuta una definición guardada con definition_id en lugar de dimensions, metrics y filters:

json
{ "definition_id": "<uuid>", "from": "2026-10-01T00:00:00Z", "to": "2026-10-08T00:00:00Z" }

from, to y zone son siempre los de la ejecución. Una ejecución por definition_id es siempre un agregado.

Errores ​

Los errores usan el sobre estándar { "error": { "code", "message" } }. Tal como los devuelve el contrato (runReport):

EstadocodeCuándo
400window_too_largeLa ventana pasa de 31 días. Un to que no es posterior a from, o un límite ausente o que no es RFC 3339, también es 400
400invalid_cursorafter no es un cursor válido
422too_many_groupsLa respuesta pasaría de 1000 grupos. La petición se rechaza entera, nunca se recorta en silencio
422catalog_changedUna definición guardada nombra una clave que el catálogo ya no publica. El mensaje la nombra y no se responde nada, ni un resultado parcial
422definition_conflictdefinition_id junto con dimensions, metrics o filters
422invalid_definition_iddefinition_id mal formado
422unknown_field, unknown_dimension, unknown_metric, unknown_column, unknown_filterUna clave fuera del vocabulario cerrado
422invalid_dimensions, invalid_metrics, invalid_columns, invalid_filters, invalid_zone, invalid_mode, invalid_limit, invalid_unmaskUn límite de lista o un valor fuera de rango
422field_not_for_modeUna clave solo de detalle en un agregado, o al revés
403role_forbiddenunmask: true desde un member
404(no encontrado)definition_id desconocido, o de otro tenant
429rate_limitedPasaste el freno por principal; respeta Retry-After
503report_builder_disabledLa bandera del tenant está apagada
503report_timeoutLa lectura superó su tiempo límite de sentencia

Ante catalog_changed, abre el catálogo, actualiza la definición guardada con un PUT que use claves vigentes y ejecuta de nuevo.

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.