Transporte de Eventos
Cómo viajan los eventos de analítica desde el iframe del Percus Player, a través del SDK del host, hasta el endpoint de ingesta. La ruta es tolerante a pérdidas, consciente de bloqueadores de anuncios, y queda gatillada por el consentimiento del viewer.
Arquitectura de dos canales
El iframe del player y la página host se comunican por dos canales postMessage independientes, ambos apuntando a parentWindow con el parentOrigin del player:
| Canal | Dirección | Propósito | Bundle |
|---|---|---|---|
Control (v: 1, tipos READY / PROGRESS / COMPLETE / WARN / CTA / ERROR) | Player → host | Alimenta los callbacks existentes onReady / onProgress / onComplete / onWarn / onCta que el SDK ya expone. | percus-embed.global.js (bootstrap) |
Analítica (PERCUS_ANALYTICS_EVENT_V1) | Player → host | Lleva los momentos del player al pipeline de ingesta. | percus-tracking.global.js (lazy) |
Los dos canales comparten el transporte (postMessage) pero se versionan de forma independiente. El canal de control conserva su contrato v1; el canal de analítica puede evolucionar sin tocarlo.
Una vez que el SDK host recibe un mensaje de analítica, lo enriquece con el contexto a nivel de página (organization_id, project_id, template_id, template_version, session_id, consent, y opcionalmente viewer_hash) y hace POST de un batch al endpoint de ingesta.
┌──────────────┐ PERCUS_ANALYTICS_EVENT_V1 ┌──────────────┐ POST /analytics/v1/events
│ Player │ ───────postMessage────────▶ │ Host SDK │ ──────fetch (keepalive)─────▶ ingesta
│ (iframe) │ │ bridge │
└──────────────┘ └──────────────┘
│
cola ▸ reintentos ▸ batch ▸ Bearer auth
El contrato postMessage
El player emite un mensaje por cada momento de interés analítico:
interface PercusAnalyticsEventMessage {
type: "PERCUS_ANALYTICS_EVENT_V1";
eventType:
| "session.started"
| "video.played"
| "video.paused"
| "video.progressed"
| "video.completed"
| "video.incomplete"
| "video.errored"
| "autoplay.failed"
| "cta.clicked"
| "interaction.engaged"
| "chapter.entered"
| "chapter.exited"
| "consent.accepted"
| "consent.declined";
occurredAt: string; // ISO-8601 con precisión de milisegundos, reloj del player
playbackId: number | null; // entero positivo ≥ 1; null fuera de cualquier reproducción
payload: Record<string, unknown>; // específico por tipo de evento (ver schema)
}
El bridge de eventos del SDK valida cada mensaje entrante:
- Iframe de origen —
event.source === expectedIframe.contentWindow. Mensajes desde cualquier otro iframe en la página se ignoran. - Origen —
event.origin === expectedOrigin(el origen donde se sirve el player). Los intentos de suplantación cross-origin se descartan. - Forma —
eventTypedebe ser uno de los 14 literales del schema;playbackIddebe sernullo un entero positivo;occurredAtypayloaddeben tener forma de string/objeto no vacíos.
Un mensaje que falle cualquiera de estas comprobaciones se descarta silenciosamente — no llega a la cola y nunca consume un round-trip de red.
Tipos de eventos del wire
Cuándo y qué emite hoy el player:
| Tipo de evento | Se dispara cuando | Payload (proporcionado por el player) |
|---|---|---|
session.started | El player completa INIT para una sesión nueva | {} |
video.played | La primera/siguiente reproducción transiciona a playing | { position_ms } |
video.paused | La reproducción transiciona a not playing | { position_ms } |
video.progressed | Cruza 25 / 50 / 75% (sólo el cuartil recién cruzado más alto, por reproducción) | { position_ms, percent } |
video.completed | El media llega al final | { duration_ms, percent: 100 } |
video.errored | El runtime emite ERROR | { error_code } |
autoplay.failed | WARN con código AUTOPLAY_BLOCKED | { reason } |
cta.clicked | Click en un CTA | { cta_id, cta_name, position_ms } |
playbackId lo gestiona el player. Comienza en 1 en el primer video.played y se incrementa en el siguiente video.played después de un video.completed. El estado de cuartiles se reinicia por reproducción.
consent.accepted / consent.declinedEstos dos eventos no los emite el player. Se originan en el bootstrap y se envían vía un beacon dedicado de un solo disparo — ver la página SDK Lazy Load.
Semántica de cola y reintentos
Una vez que el bridge encola un envelope, el SDK es responsable de hacerlo llegar al endpoint de ingesta. El comportamiento es:
- Batch por microtask. Los eventos encolados sincrónicamente se agrupan en un solo POST.
- Single-flight. Mientras hay un batch en vuelo, los nuevos eventos se almacenan en buffer; el worker no inicia un segundo POST en paralelo. Los eventos que llegan a mitad de envío viajan en el siguiente batch.
- Esquema de backoff. Tras un fallo reintentable, la cola espera
200 × nms antes del n-ésimo reintento: 200, 400, 600, 800 ms. - Máximo 5 intentos. En el 5º fallo, el batch se descarta — un endpoint permanentemente roto nunca puede atascar la cola.
fetch(keepalive: true). Un batch lanzado justo antes de cerrar la pestaña aún se completa (reemplazo moderno denavigator.sendBeacon, que no puede setear cabeceras bearer).- Autenticación Bearer. Cada POST lleva
Authorization: Bearer <analytics-ingest-token>del manifiesto. El token está firmado con la misma clave que el autenticador de ingesta valida; el campaign lo mintea con un TTL por defecto de 24 h (configurable entre 15 m y 48 h) de modo que sobrevive al token de embed-session, mucho más corto.
Clasificación de reintentos
Códigos de estado que el SDK trata como reintentables (la cola hace backoff y vuelve a intentar):
| Estado | Por qué |
|---|---|
Cualquier 5xx | El servidor está brevemente fallando |
408 Request Timeout | Transitorio |
429 Too Many Requests | Señal de sobrecarga — backoff |
| Error de red (DNS / TCP / CORS) | Transitorio |
Todo lo demás (400, 401, 403, 404, 413, 422, …) es terminal — un token inválido, una violación de schema o un payload rechazado no se recuperan reintentando, así que el batch se descarta de inmediato para liberar la cola.
Enriquecimiento del lado del servidor
El endpoint de ingesta valida el envelope del cliente contra event-schema-v1.json antes de cualquier enriquecimiento, así que el cliente ya debe pasar la validación de schema. Una vez que la validación tiene éxito el servidor sobreescribe:
organization_idyproject_ida partir de los claims del bearer token (los valores enviados por el cliente son sólo orientativos).devicea partir delUser-Agentde la request (parseado a{ type, os, browser }; el UA crudo se descarta).geoa partir del headerCloudFront-Viewer-Country.received_atdel reloj de ingesta.
Los campos enviados por el cliente template_id, template_version, session_id, playback_id, viewer_hash, referrer_host, occurred_at y consent se preservan tal cual los envió.
Alcance de origen
El parentOrigin del iframe del Player se setea a partir del primer mensaje INIT (que tiene un event.origin verificado). Cada mensaje de analítica posterior apunta a ese origen exacto — así que una página host que navega a otro documento, o un iframe malicioso intentando forjar mensajes, no puede contaminar el flujo. El bridge del SDK aplica el mismo check del lado receptor.
Si la página host restringió el share vía allowed_origins en el token de embed-session, el endpoint de ingesta además rechaza POSTs cuyo header Origin no esté en esa lista (HTTP 403).