Saltar al contenido principal

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:

CanalDirecciónPropósitoBundle
Control (v: 1, tipos READY / PROGRESS / COMPLETE / WARN / CTA / ERROR)Player → hostAlimenta 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 → hostLleva 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:

  1. Iframe de origenevent.source === expectedIframe.contentWindow. Mensajes desde cualquier otro iframe en la página se ignoran.
  2. Origenevent.origin === expectedOrigin (el origen donde se sirve el player). Los intentos de suplantación cross-origin se descartan.
  3. FormaeventType debe ser uno de los 14 literales del schema; playbackId debe ser null o un entero positivo; occurredAt y payload deben 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 eventoSe dispara cuandoPayload (proporcionado por el player)
session.startedEl player completa INIT para una sesión nueva{}
video.playedLa primera/siguiente reproducción transiciona a playing{ position_ms }
video.pausedLa reproducción transiciona a not playing{ position_ms }
video.progressedCruza 25 / 50 / 75% (sólo el cuartil recién cruzado más alto, por reproducción){ position_ms, percent }
video.completedEl media llega al final{ duration_ms, percent: 100 }
video.erroredEl runtime emite ERROR{ error_code }
autoplay.failedWARN con código AUTOPLAY_BLOCKED{ reason }
cta.clickedClick 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.declined

Estos 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 × n ms 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 de navigator.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):

EstadoPor qué
Cualquier 5xxEl servidor está brevemente fallando
408 Request TimeoutTransitorio
429 Too Many RequestsSeñ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_id y project_id a partir de los claims del bearer token (los valores enviados por el cliente son sólo orientativos).
  • device a partir del User-Agent de la request (parseado a { type, os, browser }; el UA crudo se descarta).
  • geo a partir del header CloudFront-Viewer-Country.
  • received_at del 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).