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:
- Header
Authorization: Bearer <jwt>(middlewareauth) - Header
x-workspace-id: <uuid>(middlewareworkspaceAuth('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_KEYenv 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
| Plataforma | Adapter | API |
|---|---|---|
| Meta (Facebook/Instagram) | MetaConversionsAdapter | Conversions API |
| Google Ads | GoogleAdsConversionsAdapter | Google Ads API |
| TikTok | TikTokConversionsAdapter | TikTok Events API |
LinkedInConversionsAdapter | Conversions API | |
| X (Twitter) | XTwitterConversionsAdapter | Ads API |
PinterestConversionsAdapter | Conversions API | |
| Snapchat | SnapchatConversionsAdapter | Conversions API |
| Microsoft Ads | MicrosoftAdsConversionsAdapter | UET API |
Tipos de triggers disponibles
| Tipo | Cuándo se dispara |
|---|---|
deal_stage_change | Deal se mueve a una etapa específica |
purchase_event | Evento de compra registrado |
contact_field_change | Campo 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
| Header | Origen | Uso |
|---|---|---|
X-Crimoo-Token | Campo crimooApiToken del template | Autentica el container vs node-api |
X-Meta-Access-Token | Campo accessToken del template | Reenviado a graph.facebook.com server-side |
X-Crimoo-Event-Id | Inyectado por gtm-proxy | Deduplicació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étodo | Firma | Contrato |
|---|---|---|
buildPayload | (event, credential) => Payload | Usa 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) | ctx | Campos clave |
|---|---|---|
credential.test.start | TestCredential | workspaceId, credentialId, platform |
credential.test.result | TestCredential | success, durationMs, reason (si falla) |
delivery.attempt | ConversionDispatcher | deliveryId, eventId, platform, attempt, success, durationMs |
delivery.retry_scheduled | ConversionDispatcher | deliveryId, platform, attempt, nextRetryAt |
delivery.permanent_failure | ConversionDispatcher | deliveryId, platform, totalAttempts |
retry_cycle.start / retry_cycle.end | ConversionRetryScheduler | found, 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