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 origen — event.source === expectedIframe.contentWindow. Mensajes desde cualquier otro iframe en la página se ignoran.
  2. Origen — event.origin === expectedOrigin (el origen donde se sirve el player). Los intentos de suplantación cross-origin se descartan.
  3. Forma — eventType 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).