Saltar al contenido principal

Diseño de la Plataforma de Analítica

Este documento es el diseño consolidado de la plataforma de analítica de Percus. Cubre qué capturamos, dónde vive, cómo se consulta y reporta, la postura de privacidad, la envolvente de costo a escala de startup, y la estrategia de compatibilidad para clientes que migran desde Individeo.

1. Resumen ejecutivo

Percus necesita una plataforma de analítica que:

  • Capture el engagement del viewer en videos personalizados (plays, completitud, CTAs, errores) con granularidad de sesión, sin nunca almacenar PII del recipient en la infraestructura de Percus.
  • Sirva dashboards casi en tiempo real (latencia 1–5 min) para el equipo Percus y dashboards por organización para clientes.
  • Exporte reportes (CSV, PDF, webhook) para que los clientes puedan cruzar datos de engagement con sus propios resultados de negocio.
  • Se mantenga barata: serverless, pay-per-use, sin costo en idle. Objetivo bajo USD 200/mes a escala MVP.
  • Cumpla con GDPR, Ley 19.628 (Chile), y LGPD (Brasil).
  • Preserve compatibilidad para clientes que migran desde Individeo, de modo que la migración sea un cambio de un único <script src>.

La arquitectura recomendada es un pipeline de ingesta basado en Lambda que escribe Parquet a S3, consultado con Athena, con rollups para dashboards en el cluster Aurora Serverless v2 existente. El SDK usa transporte Path B (POSTs desde la página host) con un módulo de tracking cargado de forma lazy, replicando el patrón probado de Individeo.

Decisiones de diseño bloqueadas

#DecisiónResolución
1Identidad del viewerSHA-256(org_id + ":" + recipient_code + ":" + org_salt) computado en el cliente; session_id por iframe (UUID v4); playback_id opcional para replays
2Hot tier para dashboardsSolo Athena en v1; agregar rollups en Aurora en la Fase 1 si los dashboards se sienten lentos
3Formato de tabla en S3Parquet simple (sin Iceberg) en v1
4Transporte del SDKPath B primario (POSTs desde la página host); SDK lazy de dos etapas; fetch con keepalive: true para eventos de unload
5Objetivo de latenciaCasi en tiempo real (1–5 min) para v1; sub-segundo diferido

2. Objetivos y no-objetivos

Dentro del alcance de v1

  • Capturar y almacenar eventos emitidos por el player desde percus-player y percus-embed-sdk.
  • Acceso a datos acotado por organización vía organization_id (única base de datos compartida — sin infraestructura por organización).
  • Dashboards internos (equipo Percus) y dashboards de cliente acotados por organización en el backoffice.
  • Export a CSV y digest semanal en PDF vía SES.
  • Compuerta de consent (GDPR / Ley 19.628 / LGPD).
  • Shim de compatibilidad para clientes embebidos con smartEmbed.js de Individeo.

Fuera del alcance de v1

  • Framework de A/B testing (preocupación separada).
  • Analítica de streaming sub-segundo.
  • Detección de anomalías / alertas sobre métricas de engagement.
  • Correlación con resultados de negocio (Fase 3: requiere integración de webhooks en el cliente).
  • Modelos predictivos de engagement.

3. Modelo de dominio

Se agrega un nuevo bounded context Analytics a la plataforma. Conforme a las convenciones DDD-first del proyecto, la capa de dominio está libre de framework.

Aggregates

  • Session (raíz) — una sesión anónima del viewer. Mantiene metadatos (organización, proyecto, template, canal de distribución, public share, locale, dispositivo, geo) y la secuencia de eventos emitidos dentro de ella. Se crea al inicializar el iframe, se cierra en video.ended o session.timeout (15 min de inactividad).
  • Rollup — métrica pre-agregada para una tupla (organization_id, project_id, template_id, date, dimension). Aggregate independiente; siempre reconstruible a partir de eventos crudos.
  • ConsentDecision — value object sobre Session. Gobierna qué eventos se persisten.
  • ExportJob — para exports asíncronos CSV/PDF y corridas de borrado DSAR.

Eventos de integración entrantes (consumidos desde otros contextos para enriquecer analítica)

  • Desde Campaign Service: ProjectCreated, ProjectArchived, TemplateActivated, DistributionChannelPublished, PublicVideoShareCreated. Almacenados como tablas dim_* ligeras en el warehouse para joins.
  • Desde Identity Service: OrganizationCreated, OrganizationSettingsChanged (por ejemplo, overrides de retención).

Eventos de integración entrantes (desde el SDK / Player)

Los eventos canónicos del wire (ver Sección 5 para el schema completo):

session.started, video.played, video.paused, video.progressed,
video.completed, video.incomplete, video.errored,
cta.clicked, interaction.engaged, chapter.entered, chapter.exited,
consent.accepted, consent.declined, autoplay.failed

4. Modelo de identidad y sesión

Nunca se almacena PII. La identificación usa una jerarquía de tres niveles:

IDAlcancePropósito
viewer_hashEstable a través de todas las sesiones de un recipient"Quién" — mismo recipient = mismo hash para siempre (dentro de una época de salt)
session_idUna carga de iframe"Un intento de reproducción" — la unidad contada como una vista
playback_idUna reproducción dentro de una sesiónDistingue replays dentro de la misma sesión (opcional, solo se setea si el Player soporta replay in-place)

Construcción del viewer_hash

Computado del lado del cliente en el SDK, antes de que se emita cualquier evento:

viewer_hash = SHA-256( org_id || ":" || recipient_code || ":" || org_salt )
  • recipient_code es el identificador de recipient que el host ya pasa al Player para la personalización del video. Nunca se envía a los servidores de Percus — solo su hash.
  • org_salt es un secreto por organización entregado al Player con el manifest. Rotado anualmente (rotación dura: el análisis de cohortes de largo plazo se reinicia en cada rotación, más simple y con privacidad más fuerte que un esquema de salts versionados).
  • Mismo recipient embebido por dos organizaciones distintas hashea a valores distintos (org está en el input + salt es por org). Sin correlación cross-organización.
  • Bajo GDPR / Ley 19.628 / LGPD, recipient_code es PII pseudónima porque el CRM del cliente puede revertirlo. El hash + salt por org significan que una filtración del store de analítica de Percus es inútil sin la tabla de mapeo del cliente.

session_id

  • Generado por el Player una vez por inicialización del iframe (UUID v4).
  • Todos los eventos de esa carga de iframe lo llevan.
  • Una apertura de email nueva, una visita nueva a la landing, o un refresh completo de página produce un nuevo session_id.
  • Un session_id equivale a una "vista" en dashboards. Dos sesiones del mismo viewer_hash indican un viewer repetido (útil para métricas de retención).

playback_id

  • Contador entero, incrementado por replay dentro de una sola sesión.
  • Solo se emite si el Player permite replay sin reinicializar.
  • Omitido en v1 si los replays requieren un full reload.

5. Schema de eventos v1

Envelope canónico

Cada evento del SDK comparte este envelope:

event_id UUID v4, generado por el cliente (idempotency key)
schema_version "1"
event_type enum (ver más abajo)
occurred_at ISO-8601 con ms, reloj del cliente
received_at stamp del lado del servidor
organization_id organización del cliente (vinculada al share token, nunca se confía del body)
project_id
template_id
template_version
distribution_channel_id
public_share_id nullable
viewer_hash string hex SHA-256, computado en el cliente
session_id UUID v4, por carga de iframe
playback_id entero, opcional
locale ej. "es-CL"
device { type, os, browser } — parseado del UA en el servidor, UA crudo descartado
geo { country } — desde el header CloudFront-Viewer-Country, sin almacenar IP
referrer_host solo host (sin path, sin query)
consent { tracking: boolean, granted_at }
payload específico del evento (ver más abajo)

Almacenado como Parquet con los campos de metadatos como columnas top-level para pruning de particiones y economía columnar.

Tipos de eventos del wire

Tipo de eventoCampos del payloadNotas
session.started(ninguno)Primer evento de cada sesión
video.playedposition_msReproducción iniciada o reanudada
video.pausedposition_ms
video.progressedposition_ms, percentMuestreado en hitos 25/50/75%, no en cada tick
video.completedduration_ms, percentFin natural, percent ≥ 95
video.incompleteduration_ms, percentSalió antes del fin natural
video.errorederror_code, error_messageSiempre permitido aún sin consent (telemetría operacional, sin identificadores de viewer)
cta.clickedcta_id, cta_name, cta_value, position_msCampos de nombre truncados a 64 caracteres
interaction.engagedinteraction_id, interaction_name, interaction_value, position_msFormularios, hotspots, encuestas
chapter.enteredchapter_id, chapter_name, position_ms
chapter.exitedchapter_id, position_ms
consent.accepted(ninguno)Evento de auditoría
consent.declined(ninguno)Evento de auditoría; emitido incluso con tracking apagado
autoplay.failedreasonBrowser bloqueó el autoplay

Truncado de campos de nombre

Todos los campos libres de nombre (cta_name, interaction_name, chapter_name, error_message) se truncan a 64 caracteres en el SDK antes de la transmisión. Esto coincide con el comportamiento de Individeo y acota el ancho de columna en Parquet.

Versionado de schema

  • schema_version es requerido en cada evento.
  • v2 solo se publica cuando se necesita un cambio no aditivo (rename, eliminación). Los campos aditivos no incrementan la versión.
  • El Lambda de ingesta rutea por schema_version al parser apropiado; los datos antiguos siguen siendo consultables.

6. Arquitectura AWS

[Player iframe] [Página host]
└─ eventos postMessage ───────────────────────────► [percus-embed-sdk.js]

(carga lazy con consent) │

[percus-tracking.js]

│ fetch(POST, keepalive:true)

[CloudFront]


[API Gateway HTTP API]


[Lambda: ingest]
│ - validar envelope
│ - rechazar si consent.tracking=false
│ (excepto consent.* y video.errored)
│ - parsear UA → device, eliminar IP
│ - geo desde CloudFront-Viewer-Country
│ - vincular organization_id desde el share token

[SQS Standard]


[Lambda: writer]
│ batch hasta 1000 mensajes

[S3 raw] s3://percus-analytics/raw/
org=<id>/year=/month=/day=/hour=
NDJSON.gz

EventBridge cada hora

[Lambda: compactor]
│ NDJSON → Parquet

[S3 curated] s3://percus-analytics/curated/
org=<id>/year=/month=/day=
Parquet, comprimido con snappy

EventBridge nocturno

[Lambda + Athena CTAS]

[S3 rollups] s3://percus-analytics/rollups/
+ [Aurora analytics schema] (Fase 1+)

┌──────────────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
[Dashboards del backoffice] [Exports programados] [Webhooks → BI del cliente]
(Athena + rollups en Aurora) (PDF/CSV vía SES) (Fase 3)

Justificación por componente

API Gateway HTTP API (no REST API). USD 1,00 por millón de requests vs USD 3,50 por millón en REST. No necesitamos features exclusivos de REST API (request validators, SDK gen — la integración con WAF también está disponible en HTTP API vía la consola v2). El mayor ahorro de costo.

Lambda → SQS → Lambda (no Lambda directo → S3). Dos razones. Primero, los lanzamientos de campaña pueden hacer spike de 100× sobre el baseline normal; SQS suaviza el burst, nos da reintentos gratis, y desacopla el throughput del writer de la latencia de cola del path de ingesta. Segundo, batchear en el writer nos permite convertir 1000 eventos en un único PUT a S3 — órdenes de magnitud más barato que escrituras por evento.

Sin Kinesis en MVP. Kinesis Data Streams tiene un piso de idle de ~USD 11/mes por shard; Firehose suma costo de delivery por GB. A volúmenes MVP (≤ 100K eventos/hora en peak) ambos son más caros que Lambda + SQS. Trigger de migración: cuando el Lambda writer cruce ~10M eventos/día o la latencia de batching exceda los 5 minutos, intercambiar a Kinesis Firehose con conversión a Parquet. El layout de S3 queda igual.

Compactor horario. Muchos archivos NDJSON.gz pequeños en la zona raw se vuelven Parquet particionado por hora en la zona curada. El costo por consulta de Athena es proporcional a los bytes escaneados, y Parquet + particionado hace que las consultas típicas de dashboard escaneen MB en vez de GB.

Athena, no Redshift / QuickSight / OpenSearch. Athena tiene cero costo en idle y escala a cualquier volumen de scan que necesitemos a USD 5/TB. Redshift Serverless tiene un mínimo de gasto más alto y es más difícil de justificar a escala de startup. Los authors de QuickSight son USD 24/usuario/mes — no startup-friendly. El backoffice Next.js puede renderizar charts directamente desde resultados de queries de Athena/Aurora.

Schema de analítica en Aurora (Fase 1+). Reutiliza el cluster Aurora Serverless v2 existente — sin nueva infraestructura. Solo almacena rollups pre-agregados (pequeños) y los últimos 30 días de metadatos de sesión denormalizados para queries rápidas de dashboard. Los eventos crudos siguen en S3.

Capas de almacenamiento y retención

ZonaFormatoPropósitoRetención
s3://percus-analytics/raw/NDJSON.gzRecovery, replay30 días, luego lifecycle de S3 a Glacier Instant Retrieval (1 año), luego eliminar
s3://percus-analytics/curated/Parquet snappy, particionadoAthena ad-hoc, rebuilds2 años estándar, luego Glacier IR (5 años)
s3://percus-analytics/rollups/Parquet diario y semanalMétricas pre-agregadas5 años
Aurora analytics.*TablasHot tier para dashboards (Fase 1+)30 días de metadatos crudos de sesión; rollups indefinidos

La retención es configurable por organización para variación contractual. Borrado DSAR: lookup por viewer_hash, reescribir las particiones Parquet afectadas removiendo los eventos coincidentes. Esto es aceptable a volúmenes v1 (una reescritura de partición es minutos de trabajo Lambda).

Acceso a datos acotado por organización

Percus usa una única base de datos compartida con organization_id como columna en cada fila — no hay infraestructura ni schema por organización. La misma convención se sigue en los servicios Identity y Campaign. El scoping se aplica en la capa de queries.

  • organization_id es la primera clave de partición en S3, así que cada query de Athena se poda al prefijo de una sola organización. Esto es principalmente una optimización de eficiencia de query (y de costo, ya que Athena cobra por byte escaneado).
  • Las tablas de analítica en Aurora aplican organization_id en cada query a nivel de repositorio, igual que los servicios existentes.
  • Autenticación de ingesta: el SDK carga bajo un share token firmado (por public_share_id o por canal de distribución publicado). El token lleva organization_id y project_id; el Lambda de ingesta confía solo en los valores vinculados al token, nunca en el body de la request. Esto impide que un atacante escriba eventos en datos de otra organización, aunque la base de datos sea compartida.

7. Diseño de transporte del SDK

Path B: POSTs desde la página host

El Player de Percus corre en un iframe en player.percus.cl; la página host vive en el dominio del cliente (por ejemplo bank-client.com). Los eventos del player viajan:

Player iframe ──postMessage──► percus-embed-sdk.js en el host
└─ percus-tracking.js en el host
└─ fetch POST → api.percus.cl/analytics/v1/events

La request HTTP se origina en el contexto first-party de la página host (Origin: bank-client.com), no del iframe. Esto coincide con el patrón de Individeo y es materialmente menos probable de ser bloqueado por ad-blockers, medidas anti-tracking del navegador y políticas CSP que los clientes ya tienen provisionadas.

SDK lazy de dos etapas

Dos scripts:

  • percus-embed-sdk.js — el bootstrap. Pequeño (bajo 30 KB minificado, objetivo). Siempre cargado. Cablea postMessage, maneja el ciclo de vida del iframe, expone la API pública. No contiene ningún código de tracking.
  • percus-tracking.js — el módulo de tracking. Cargado de forma lazy por el bootstrap solo cuando:
    1. El tracking está habilitado para este embed (data-percus-enable-tracking="true" o su alias Individeo), Y
    2. El consent fue otorgado (data-percus-consent-accepted="true" estaba presente al inicio, O el host invocó Percus.acceptConsent() post-load).

Si el consent es declinado, percus-tracking.js nunca se descarga. Literalmente no hay código de analítica en la página. Esta es una postura de privacidad más fuerte que gatear la emisión de eventos post-load y replica la lógica del loader de Individeo de forma exacta.

Confiabilidad

  • Eventos de unload: fetch con keepalive: true (reemplazo moderno de sendBeacon). El navegador maneja el delivery durante el unload de la página; no se necesita reintento.
  • Eventos normales: encolados en memoria, single-flight (flag _trackingInProgress), reintentados con backoff 200ms × (n + 1) (coincide con los defaults bien probados de Individeo). El contador de fallas permanentes es compartido por la cola; tras 5 fallas consecutivas la cola se detiene para no quemar la red del usuario.
  • Un cierre de pestaña a mitad de reintento descarta eventos encolados. Trade-off aceptable — la alternativa (cola en IndexedDB) suma complejidad para retornos decrecientes a nuestros volúmenes de eventos.

Soporte de proxy first-party

Clientes con privacidad estricta pueden rutear eventos a través de su propio subdominio. El SDK respeta, en orden de prioridad:

  1. Por llamada: payload.trackingURL (raro).
  2. Global: window.percusTrackingURL o window.PERCUS_TRACK_BASE_URL.
  3. Atributo: data-percus-tracking-url en el elemento del embed.
  4. Default: https://api.percus.cl/analytics/v1.

Un banco que prefiere fully first-party puede hacer CNAME analytics.bank-client.com → api.percus.cl y setear window.percusTrackingURL = "https://analytics.bank-client.com/v1". Las requests entonces aparecen fully first-party incluso frente a extensiones y CSPs estrictos.

8. Estrategia de compatibilidad con Individeo

Los clientes Percus existentes (Porvenir, Plan Vital, AFP Habitat, Contemporánea Seguros, Produbanco) tienen integraciones contra el smartEmbed.js de Individeo. Para que la migración sea de bajo esfuerzo enviamos un adaptador delgado, percus-individeo-compat.js, que reconoce el namespace de atributos y la API global de Individeo y rutea todo a través de percus-embed-sdk.js. El cambio del cliente es un único swap de <script src>.

El shim de compatibilidad está informado por análisis directo del smartEmbed.js y smartTracking.js de Individeo (Sección 8.4).

8.1 Mapeo de atributos

Atributo de IndivideoAtributo de PercusComportamiento
data-iv-smart-embed-proxydata-percus-embedMarker que dispara el embed
data-iv-tracking-urldata-percus-tracking-urlOverride del endpoint de analítica
data-iv-tracker-group-keydata-percus-organization-idClave de organización
data-iv-enable-trackingdata-percus-enable-trackingFlag de wire utrk
data-iv-enable-smart-trackingdata-percus-enable-trackingMismo target
data-iv-enable-ga-trackingdata-percus-enable-gaForwarding a GA
data-iv-embed-smart-trackingdata-percus-enable-trackingMismo target
data-iv-ga-event-namedata-percus-ga-event-nameNombre de evento de GA
data-iv-ga-category-namedata-percus-ga-category-nameCategoría de GA
data-iv-consent-requireddata-percus-consent-requiredCompuerta de consent
data-iv-consent-accepteddata-percus-consent-acceptedDecisión de consent
data-iv-project-codedata-percus-project-idID de proyecto
data-iv-media-iddata-percus-template-idID de template
data-iv-media-codedata-percus-template-versionVersión del template
data-iv-envdata-percus-environmentDEV/STAGING/PROD

8.2 Aliases globales

El shim define aliases getter/setter en window así el código de la página host que lee o escribe globals de Individeo sigue funcionando:

window.individeoData ──► window.percusData
window.IndivideoVersion ──► window.percusVersion
window.IVDomains ──► window.percusDomains
window.SmartTracking ──► window.percusTracking
window.ivTrackerKey ──► window.percusSessionKey
window.ivtgk ──► window.percusTrackerGroupKey
window.ivTrackingURL ──► window.percusTrackingURL
window.TRACK_BASE_URL ──► window.PERCUS_TRACK_BASE_URL
window.smartEmbedModal ──► window.percusModal
window.BluePlayer ──► window.percusPlayer

8.3 Fan-out de callbacks

El código del cliente puede haber registrado handlers contra cualquiera de los ~60 nombres de callback on* de Individeo. El shim re-emite eventos nativos de Percus bajo cada nombre legacy, de modo que los suscriptores siguen disparándose. El set mínimo a soportar (mayor uso por parte de clientes):

onCreate, onDestroy, onReady, onError
onAutoplay, onAutoplayFailure
onFirstPlay, onPlayIncomplete, onReplay
onFirstQuartileComplete, onSecondQuartileComplete,
onThirdQuartileComplete, onLastQuartileComplete
onCTA, onServiceCTA, onInteraction
onChapterEnter, onChapterExit
onConsentAccepted, onConsentDeclined
onFullscreenEnter, onFullscreenExit
onIFrameReady, onDataReady

El set completo está enumerado en el bundle de Individeo; el shim provee un stub no-op para cualquiera que no esté implementado aún, de modo que el código del cliente no falle.

8.4 Compatibilidad de wire-event (opcional Fase 2)

Algunos clientes corren dashboards o pipelines de BI que consumen directamente el formato de payload del endpoint de tracking de Individeo. Para una migración sin interrupciones, exponer un endpoint de compatibilidad:

POST /analytics/v1/indiTrack?evt={eventType}
Content-Type: application/json
Body: { event, trackerKey, trackerGroupKey, ctaName, ctaValue, ... }

Esto acepta la forma exacta del payload de Individeo y traduce internamente al schema canónico de eventos de Percus. Los clientes que sigan usando el payload estilo Individeo tampoco necesitan cambiar nada aguas abajo del endpoint de analítica.

Hallazgos que informaron esta sección (a partir del análisis directo de cdn.individeo.com/individeo/prod/edge/js/smartEmbed.js y smartTracking.js):

  • Transporte: Path B (POSTs desde la página host); el iframe nunca hace POST directamente.
  • Endpoints: https://track.individeo.com/api/indiTrack (prod), https://track-ci.individeo.com/api/indiTrack (CI). Override por cliente soportado vía atributo tracking-url, window.TRACK_BASE_URL, window.ivTrackingURL, o trackingURL por llamada en el payload.
  • Formato de wire: POST {base}/api/indiTrack?evt={type}, body es JSON.stringify(payload), Content-Type: application/json. La variante de unload usa fetch con keepalive: true.
  • Whitelist de wire-event (solo estos se envían al servidor, los otros ~60 nombres on* son callbacks locales): pageLoad, pageReload, onCTA, onServiceCTA, onInteraction, onChapterEnter, onEvent, onLog, onConsentAccepted, onConsentDeclined, onGATracking, onTrackerKeyDefined.
  • Forma del payload: event, trackerKey, trackerGroupKey, más pares name/value específicos del evento (ctaName/ctaValue, interactionName/interactionValue, etc.), todos truncados a 64 caracteres.
  • Confiabilidad: cola en memoria, single-flight, reintentos 200ms × (n+1).
  • Postura de consent: smartTracking.js ni siquiera se inyecta en la página a menos que el consent esté aceptado. Postura de privacidad fuerte, copiada al pie de la letra.
  • Integración con GA: push paralelo a window.dataLayer cuando enable-ga-tracking está seteado.

9. Reportes y acceso del cliente

Dashboards internos (equipo Percus)

Viven en el backoffice existente como una nueva sección. Páginas:

  • Campañas en vivo — sesiones iniciadas en los últimos 60 minutos, tasa de completitud, CTR de CTAs, picos de error. Refrescado cada 60 segundos.
  • Drill-down por proyecto — histograma de drop-off (hitos 25 / 50 / 75 / 100%), embudo de CTAs, top errores, top dispositivos y locales.
  • Vista cross-organización — solo interno de Percus; identifica qué clientes están saludables versus cuáles ven engagement bajo.

Lee de rollups en Aurora para las vistas por defecto (sub-segundo); Athena para slices ad-hoc profundos (1–5 segundos, surfaceado como botón "deep query").

Dashboards de cliente (por organización)

Mismo backoffice, filtrado por organización. Los usuarios de la org ven solo los datos de su organization_id. Vistas por defecto por proyecto: views, tasa de completitud, CTR de CTAs, curva de drop-off, top dispositivos y locales. Incluye:

  • Export CSV — genera una URL S3 firmada apuntando a una conversión Parquet → CSV de la vista actual. El job corre en Lambda, expira en 24 horas.
  • Digest semanal en PDF — Lambda + SES, programado por organización, opt-in.
  • Export por webhook (Fase 3) — POST por lote de payloads de eventos a un endpoint configurado por el cliente para ingestión en su BI.
  • Forwarding a Google Analytics 4 (Fase 2) — el SDK espeja un set mínimo de eventos a GA4 con el measurement ID del cliente. Igual para Adobe Analytics.

Por qué no QuickSight

Los authors de QuickSight son USD 24 por usuario por mes. Con un objetivo de 10 clientes y 2–3 authors de dashboard por cliente, eso es USD 480–720 por mes antes de cualquier lector real. El backoffice Next.js ya existe, el equipo construye Next.js diariamente, y Recharts o Tremor cubren el vocabulario de charts que se necesita. Controlamos el UX, el scoping por organización y el estilo. Athena y Aurora respaldan directamente las queries.

10. Privacidad y compliance

Los compromisos a nivel plataforma (PII nunca almacenada, identificadores anonimizados, sin IP cruda, gateado por consent) están detallados en Data Handling. Esta sección enumera las reglas de implementación específicas de analítica.

  • Ninguna IP cruda sale del edge. El Lambda de ingesta lee CloudFront-Viewer-Country y descarta la IP origen antes de cualquier persistencia. CloudFront está configurado para quitar la IP del cliente de los headers reenviados.
  • Ningún User-Agent crudo almacenado. Parseado en memoria en el Lambda de ingesta; solo {type, os, browser} sobrevive.
  • Compuerta de consent aplicada al cargar el script. percus-tracking.js no se descarga hasta que el consent es aceptado, excepto para la telemetría operacional video.errored (que omite todos los identificadores de viewer).
  • Solo identificadores anónimos. viewer_hash se computa en el cliente con un salt específico de la organización. La misma persona en dos organizaciones no puede correlacionarse.
  • Soporte de DSAR. Derecho al olvido: job de borrado por org indexado por viewer_hash. SLA documentado: 30 días.
  • Pista de auditoría. Cada request de export y cada job de retención/borrado se loguea a CloudTrail y a la tabla de audit log de la plataforma.
  • Encriptación. S3 SSE-KMS; encriptación en reposo en Aurora; TLS 1.3 en tránsito; secretos en AWS Secrets Manager. Ya es el estándar de la plataforma.
  • Rotación del salt. Rotación anual, documentada en la política de privacidad. El análisis de cohortes a través de límites de rotación se reinicia — trade-off aceptado a cambio de simplicidad y privacidad más fuerte frente a un esquema de salts versionados.

11. Estimación de costo

Asume Lambda + SQS + S3 + Athena + Aurora hot tier (cluster existente, sin costo incremental):

EscalaEventos diariosCosto mensual (solo capa de analítica)Notas de driver
Temprano100 KUSD 15 – 25Mayoritariamente API Gateway + S3 + CloudWatch
Objetivo Fase 22,7 MUSD 120 – 180API Gateway domina; los scans de Athena son baratos por la partición
Multi-cliente Fase 325 MUSD 600 – 900En este punto migrar la ingesta a Firehose + conversión a Parquet para ~30% de ahorro

Principales drivers de costo por orden de prioridad: requests de API Gateway (lineal), logs de CloudWatch (acotados por disciplina de log level), operaciones PUT de S3 (acotadas por el batch size del writer de 1000), scans de Athena (acotados por la partición de Parquet).

La capacidad mínima de Aurora Serverless v2 ya está pagada por la plataforma; la analítica suma solo un schema pequeño y tablas de rollup — la carga incremental es despreciable.

12. Fases

Fase 0 — Cimientos (2–3 semanas). Schema de eventos v1, event bus y buffering del SDK, compuerta de consent, carga lazy de dos etapas, Lambda de ingesta + SQS + zona raw de S3, compactor horario a Parquet, workgroup de Athena y tabla Glue. Resultado: eventos fluyendo, consultables vía Athena. TDD todo el tiempo.

Fase 1 — Dashboards internos (2–3 semanas). Rollups nocturnos, schema analytics en Aurora, página "Project analytics" del backoffice con tasa de completitud, CTR, histograma de drop-off. Pruebas de smoke contra fixture de escala productiva.

Fase 2 — De cara al cliente (3–4 semanas). Dashboards en el backoffice acotados por organización, export CSV, digest semanal en PDF vía SES, shim de compatibilidad con Individeo v1, job de borrado DSAR, forwarding a GA4.

Fase 3 — Integraciones (continuo). Webhooks para BI del cliente, forwarding a Adobe Analytics, API de correlación con resultados de negocio (el cliente empuja eventos "viewer convirtió"; los hacemos join contra las sesiones para ROI), endpoint de compatibilidad de wire-event con Individeo.

13. Preguntas abiertas para el AWS architect

  1. WAF para el endpoint de ingesta. API Gateway HTTP API soporta AWS WAF. ¿Vale el costo en MVP? El endpoint de analítica es público por diseño (fetcheado por el browser), pero rate limiting por viewer_hash o por IP mitigaría abuso. Alternativa: throttling de API Gateway solo.
  2. CloudFront delante de API Gateway. Suma otra capa (USD 0,0085 por 10K requests + transferencia) pero permite cachear el preflight OPTIONS y centralizar el procesamiento de edge. ¿Vale a nuestros volúmenes de request?
  3. VPC para el Lambda de ingesta. El Lambda de ingesta toca solo SQS — sin acceso a DB. Mantenerlo fuera de una VPC evita las penalizaciones de cold-start. Confirmar que no hay razón de compliance que exija aislamiento por VPC.
  4. ¿Glue Data Catalog o definiciones de tabla auto-gestionadas? Glue es conveniente pero tiene un pequeño costo por tabla. A una tabla Parquet para eventos crudos, la diferencia es despreciable — recomendamos Glue. Sanity check.
  5. Cuotas del workgroup de Athena y alarmas de costo. ¿Cuál es el límite por defecto correcto de datos escaneados por query, para prevenir que una query de dashboard fuera de control escanee un año de datos?
  6. Enforcement de lectura en S3. Dado que la base de datos es compartida y el scoping se aplica en la capa de query (el repositorio chequea organization_id), ¿es el prefijo de partición de S3 suficiente para el scoping de query en Athena, o queremos políticas de bucket + condiciones de IAM como segunda capa?
  7. DR cross-región. Active-active es overkill en MVP. ¿Es Cross-Region Replication de S3 sobre las zonas curated y rollups suficiente para los objetivos de RTO/RPO, o necesitamos Lambdas en standby?
  8. Reserved concurrency. ¿El Lambda de ingesta debería tener reserved concurrency para proteger servicios aguas abajo durante un spike de tráfico, o no reservada está bien dada la suavización de SQS?
  9. Triggers de migración a Kinesis Firehose. ¿En qué umbral concreto de eventos-por-día Firehose se vuelve más barato que Lambda + SQS? Ayudar a validar la regla de pulgar de ~10M/día.
  10. Athena Iceberg en v2. ¿Vale planificarlo, aunque v1 sea Parquet plano? Iceberg simplificaría reescrituras de partición DSAR y la evolución de schema.