Skip to main content

Flujo de Conversiones Offline

Envío automático de eventos de conversión a plataformas publicitarias cuando ocurre una acción en el CRM (deal ganado, campo actualizado, etc.).

Flujo completo

Seguridad

Multi-tenancy

Todas las rutas /api/conversions/* requieren:

  1. Header Authorization: Bearer <jwt> (middleware auth)
  2. Header x-workspace-id: <uuid> (middleware workspaceAuth('edit'))

El orchestrator valida adicionalmente que los platformCredentialIds enviados en una operación manual pertenezcan al workspace solicitante antes de crear eventos. Un request sin x-workspace-id devuelve 400; con un workspace ajeno, 403.

Encriptación en reposo

La columna credentials (JSONB) se encripta con AES-256-GCM usando CredentialCryptoService (src/infrastructure/services/CredentialCryptoService.ts).

Formato en DB:

{ "__v": 1, "__d": "base64iv:base64tag:base64ciphertext" }
  • Lazy migration: filas sin __v (legacy plain JSON) pasan transparentes y se encriptan en el próximo write.
  • Clave: CREDENTIAL_ENCRYPTION_KEY env var (32 bytes en base64). Generar: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))".

Sanitización de errores

Los mensajes de error que llegan al frontend pasan por sanitizeErrorMessage(), que elimina cualquier valor de credentials (tokens, keys) antes de serializar.

Plataformas soportadas

PlataformaAdapterAPI
Meta (Facebook/Instagram)MetaConversionsAdapterConversions API
Google AdsGoogleAdsConversionsAdapterGoogle Ads API
TikTokTikTokConversionsAdapterTikTok Events API
LinkedInLinkedInConversionsAdapterConversions API
X (Twitter)XTwitterConversionsAdapterAds API
PinterestPinterestConversionsAdapterConversions API
SnapchatSnapchatConversionsAdapterConversions API
Microsoft AdsMicrosoftAdsConversionsAdapterUET API

Tipos de triggers disponibles

TipoCuándo se dispara
deal_stage_changeDeal se mueve a una etapa específica
purchase_eventEvento de compra registrado
contact_field_changeCampo del contacto cambia a un valor

Retry con exponential backoff

Si el envío a la plataforma falla:

Hashing de datos PII

Antes de enviar a cualquier plataforma, los identificadores se hashean con SHA256:

email    → SHA256(lowercase(trim(email)))
phone → SHA256(E164_format(phone))
name → SHA256(lowercase(trim(first_name)))

Esto es requerido por Meta CAPI, Google Ads Enhanced Conversions y otras plataformas.

CAPI Gateway (server-side, real-time)

Distinto del flujo de "conversiones offline" batch. El CAPI Gateway forwardea eventos en tiempo real desde el sGTM container hacia las Conversion APIs de Meta y TikTok, autenticando vía token Crimoo.

Patrón fan-out en sGTM (1 Client → N tags)

Browser → GA4 hit (/g/collect)

GA4 Client (único Client en el container) ← claim

│ publica al pipeline interno de sGTM

┌──────────┴──────────┬─────────────────┐
▼ ▼ ▼
GA4 forwarding tag Crimoo Facebook TikTok Events
CAPI tag API tag

Una request entrante solo puede ser claimed por un Client, pero N tags se suscriben a cada evento publicado. Por eso el container sGTM funciona con un único gaaw_client (GA4) y todos los tags de plataforma (Meta, TikTok, etc.) cuelgan de él vía triggers de tipo Custom Event.

Pipeline end-to-end

Headers críticos

HeaderOrigenUso
X-Crimoo-TokenCampo crimooApiToken del templateAutentica el container vs node-api
X-Meta-Access-TokenCampo accessToken del templateReenviado a graph.facebook.com server-side
X-Crimoo-Event-IdInyectado por gtm-proxyDeduplicación entre browser pixel y CAPI

Test mode (test_event_code)

El template gtm-crimoo-facebook-capi/template.tpl soporta inyección de test_event_code desde dos fuentes:

if (eventData.test_event_code || data.testId) {
postBody.test_event_code = eventData.test_event_code || data.testId;
}
  • data.testId — campo de UI del template (label "Test Event Code (optional)"). Se setea en la config del tag.
  • eventData.test_event_code — viene del evento mapeado (si el Client/upstream lo propagó).

Para que un evento aparezca en la pestaña "Probar eventos" de Meta Events Manager, el test_event_code debe ir en el body del POST a Meta. Sin ese campo, los eventos van al stream de producción y NO aparecen ahí. El código se obtiene desde Events Manager → pestaña "Probar eventos" (es generado por Meta, no inventado).

Agregar un nuevo adapter

Cada adapter implementa ConversionPlatformAdapter (src/core/ports/ConversionPlatformAdapter.ts). Contrato obligatorio:

MétodoFirmaContrato
buildPayload(event, credential) => PayloadUsa credential.platformAccountId para el account/pixel/dataset ID
send(payload, credential) => Promise<PlatformSendResult>Nunca lanza — devuelve {success, statusCode, errorMessage}
validateCredentials(credential) => Promise<void>Resuelve = válido. Lanza CredentialValidationError = inválido

El método validateCredentials usa el patrón throw-on-failure:

  • Validar campos requeridos localmente antes de hacer HTTP → error instantáneo con durationMs: 0
  • Si la API externa rechaza, parsear el body de error y lanzar CredentialValidationError('Plataforma: mensaje descriptivo')
  • Nunca retornar boolean

Mapeo de campos frontend → backend

Los credentialFields en el modelo frontend (PLATFORMS en offline-conversion.model.ts) usan snake_case exacto que el adapter lee de credential.credentials.*.

El campo de account/pixel/dataset ID se guarda en la columna separada platformAccountId (no dentro del JSONB credentials). Se declara con platformAccountIdField en PlatformInfo.

Ejemplo (Meta):

{
id: 'meta',
platformAccountIdField: 'pixel_id', // → credential.platformAccountId
credentialFields: [
{ key: 'access_token', ... }, // → credential.credentials.access_token
{ key: 'pixel_id', ... }, // → credential.platformAccountId (extraído por platformAccountIdField)
]
}

Estado de entrega en el historial

Observabilidad

El módulo emite logs JSON estructurados a stdout (ts, level, ctx, msg, + campos específicos). Parseable por GCP Logging, Datadog, etc.

Evento (msg)ctxCampos clave
credential.test.startTestCredentialworkspaceId, credentialId, platform
credential.test.resultTestCredentialsuccess, durationMs, reason (si falla)
delivery.attemptConversionDispatcherdeliveryId, eventId, platform, attempt, success, durationMs
delivery.retry_scheduledConversionDispatcherdeliveryId, platform, attempt, nextRetryAt
delivery.permanent_failureConversionDispatcherdeliveryId, platform, totalAttempts
retry_cycle.start / retry_cycle.endConversionRetrySchedulerfound, processed, errors, durationMs

Reglas de seguridad en logs:

  • Nunca loguear valores de credentials (tokens, keys)
  • Nunca loguear PII crudo (emails, teléfonos) — solo hashes si es necesario