1) Introducción detallada
El tracking de conversiones “avanzado” deja de ser un problema de pegar píxeles cuando operas como desarrollador full‑stack: empiezas a lidiar con navegadores que recortan cookies, usuarios que alternan dispositivos, bloqueadores de anuncios, SSR/SPA híbridas, funnels con múltiples dominios, pagos externos y requisitos regulatorios (RGPD/LOPDGDD/ePrivacy). El resultado típico de un enfoque naïf (solo client‑side) es un dataset inconsistente: conversiones duplicadas, subatribución por pérdida de cookies, sobreatribución por reintentos de red, eventos disparados antes de que existan IDs persistentes o datos personales capturados sin base legal. Para un equipo de producto, eso se traduce en decisiones erróneas (CAC/ROAS inflados o deprimidos), experimentos A/B contaminados y segmentaciones que no se pueden reproducir.
Implementar tracking de conversiones avanzado implica diseñar un sistema de medición como un subsistema de producto: con contrato de eventos, control de versiones, idempotencia, colas, observabilidad, estrategias de identidad (first‑party), y un “edge” o backend que normaliza y enruta datos a destinos (analytics, ads, CRM, data warehouse). El frontend debe disparar eventos con contexto de UI y estado; el backend debe confirmar la conversión “real” (pago capturado, lead validado, suscripción activa) y producir el evento canónico. La convergencia de ambos (client + server) necesita una estrategia de correlación y deduplicación para que los proveedores (GA4, Meta, Google Ads, TikTok, etc.) y tu warehouse no cuenten dos veces.
Además, el tracking avanzado exige un enfoque explícito sobre privacidad: minimización, hashing, consentimiento, y retención; y un enfoque explícito sobre calidad de datos: validaciones, contratos, pruebas end‑to‑end, y reconciliación con fuentes de verdad (PSP, ERP). Si haces esto bien, ganas robustez frente a ITP/ETP, reduces discrepancias entre plataformas, mantienes el control de atribución (al menos para reporting interno) y posibilitas optimización real de campañas, pricing y producto con datos consistentes.
En este artículo voy a bajar a nivel de arquitectura, payloads, idempotency keys, cookies first‑party, CAPI/Measurement Protocol, server‑side GTM, modelado de funnels y un set de ejemplos de código y patrones que puedas copiar en un stack moderno (Node/TypeScript, Next.js, APIs, colas, y destinos típicos).
2) Contexto histórico o teórico
Durante años el tracking se apoyó en cookies third‑party y píxeles client‑side. El modelo era simple: un script en el navegador leía/escribía cookies, medía pageviews y disparaba conversiones en la página de “gracias”. Este enfoque funcionaba razonablemente en un mundo con navegación clásica (MPA), poca fragmentación de dominios y menor presión regulatoria. La evolución hacia SPAs, checkout externos, apps, y múltiples subdominios introdujo discontinuidades en la sesión.
Luego llegaron tres cambios estructurales: (1) regulación (GDPR 2018, ePrivacy en práctica, consent frameworks), (2) restricciones técnicas de navegadores (Safari ITP, Firefox ETP, Chrome con Privacy Sandbox y limitaciones progresivas), y (3) el ecosistema de bloqueadores. Eso rompió supuestos: la cookie de terceros dejó de ser fiable, las cookies first‑party empezaron a expirar antes o a ser particionadas, y el “pixel” empezó a fallar silenciosamente. Al mismo tiempo, las plataformas publicitarias empujaron modelos de “server‑to‑server” para recuperar señal (Meta CAPI, Google Ads Enhanced Conversions, GA4 Measurement Protocol, TikTok Events API). La consecuencia: el tracking dejó de ser puramente frontend; pasó a ser un problema full‑stack y de datos.
Teóricamente, medir conversiones consiste en mapear un evento de negocio a un evento de analítica con identidad y contexto. La pieza clave es definir la fuente de verdad: por ejemplo, “pago capturado” en el PSP, no “clic en botón pagar”. A partir de ahí, defines eventos canónicos (Purchase, Lead, SignUpQualified, TrialStarted) con un contrato estable; y defines eventos auxiliares de UI (ClickPay, FormSubmit) que sirven para diagnóstico y análisis de comportamiento pero no para reporting financiero. En el plano de atribución, pasas de depender de last‑click de cookies a combinar señales: parámetros UTM, click IDs (gclid/fbclid), first‑party IDs, y, cuando aplica y hay consentimiento, datos hash (email/phone) como “matching” probabilístico o determinista según proveedor.
El tracking avanzado es, en esencia, ingeniería de sistemas distribuidos aplicada a eventos: exactamente‑once es caro, así que diseñas idempotencia, deduplicación, compensación y reconciliación. Esto es lo que diferencia una implementación que “parece funcionar” de una que produce métricas defendibles.
3) Sección técnica 1: Arquitectura full‑stack de tracking (cliente, edge, backend, warehouse)
Una arquitectura avanzada separa responsabilidades y reduce acoplamiento con proveedores. En lugar de disparar directamente a 5 píxeles desde el navegador, defines un Event Gateway (tu endpoint o un server‑side tag manager) que recibe eventos normalizados y los reenvía. Las piezas típicas:
Cliente (web/app): captura contexto de UI (página, producto visto, campos del formulario, experimentos, idioma), genera un event_id y un client_id first‑party, y envía a tu gateway. Cuando hay consentimiento, puede incluir identificadores (email) en crudo hacia tu backend para hashear; evita hashear en cliente salvo que tengas claro el threat model (en general, mejor en servidor).
Edge / Gateway: endpoint HTTP (por ejemplo /events) detrás de CDN/WAF que valida esquema, aplica rate limiting, registra raw events para auditoría, y encola para procesamiento asíncrono. Aquí puedes enriquecer con IP/UA (para geolocalización o matching de proveedores) pero ojo: IP es dato personal; minimiza y controla retención. El gateway también asigna request_id y devuelve 202 para no bloquear UX.
Backend de negocio: produce eventos canónicos en momentos de verdad (pago capturado, lead validado). Idealmente, estos eventos nacen del dominio (domain events) y se publican a un bus (Kafka/PubSub/SQS). El backend también expone endpoints para “confirmaciones” (por ejemplo, /purchase/confirm) y para registrar conversiones offline (ventas por teléfono, contratos firmados).
Procesador / Router: consume de cola, aplica reglas de deduplicación e idempotencia, enriquece con datos de negocio (importe final, moneda, impuestos, SKU), y envía a destinos: GA4 (Measurement Protocol), Meta CAPI, Google Ads, CRM, y tu warehouse (BigQuery/Snowflake/Postgres). Es importante que el router sea capaz de reintentar con backoff y registrar el estado por destino.
Warehouse + modelos: guardas eventos crudos (append‑only), eventos normalizados y tablas de hechos (fact_conversions) con claves estables. Esto permite reconciliación: comparar “compras” en PSP vs compras medidas. El warehouse es donde defines reporting interno (atribución propia, cohortes, LTV) que no dependa de la caja negra de un proveedor.
Patrón recomendado: dual‑track para conversiones críticas. Disparas un evento client‑side (para capturar click IDs y contexto de sesión) y un evento server‑side (para garantizar que la conversión existe). Ambos comparten el mismo event_id o una clave de correlación, de modo que el proveedor deduplica. Para GA4, esto se hace con event_id en el payload del Measurement Protocol; para Meta CAPI con event_id y el mismo pixel_id; para Google Ads, con conversion_id/label y transaction_id o order_id.
Otro patrón: provider abstraction. Define un contrato interno: ConversionEvent (type, event_id, occurred_at, user, session, ecommerce, attribution). Luego implementas “adaptadores” por proveedor. Cuando cambie Meta o GA4, actualizas el adaptador, no todo el producto.
4) Sección técnica 2: Identidad, first‑party cookies, session stitching y entorno multi‑dominio
Sin identidad consistente no hay atribución ni deduplicación. En tracking avanzado, la identidad se compone de capas:
1) Client ID first‑party: un identificador aleatorio almacenado en cookie first‑party (por ejemplo cid) o localStorage (mejor cookie para requests server‑side y subdominios). Debe rotar si el usuario rechaza consentimiento o si defines un TTL estricto. A nivel técnico, usa SameSite=Lax por defecto; Secure siempre en HTTPS; y HttpOnly si solo la necesitas en servidor. Si el frontend necesita leerlo para adjuntarlo a eventos, entonces no será HttpOnly; decide según threat model (XSS). Muchos equipos separan: cookie HttpOnly para correlación server‑side y otra “public” para analítica.
2) Session ID: una sesión lógica (por ejemplo 30 minutos de inactividad). En SPAs, la “sesión” no coincide con pageviews. Puedes implementar un sid con timestamp de última actividad. Si usas GA4, su sesión se gestiona internamente, pero para tu sistema conviene tener la tuya (especialmente para server‑side).
3) User ID (login): cuando existe autenticación, el user_id del dominio es el pegamento entre dispositivos. Importante: no envíes IDs internos directamente a proveedores si pueden considerarse PII o si violan términos. Puedes usar un ID pseudónimo (UUID) distinto al PK real, o un hash irreversible. Mantén un mapping interno.
4) Señales de atribución: UTMs, referrer, click IDs (gclid, wbraid, gbraid, fbclid), y parámetros de campañas. Captúralos en el primer request y persístelos en first‑party storage con un TTL razonable (p.ej. 7–30 días según negocio y consentimiento). Ojo con el “last non‑direct”: define tu regla y aplícala de forma determinista.
Multi‑dominio y subdominios: si marketing va en www, app en app y checkout en checkout, necesitas que la identidad viaje. Para subdominios, configura la cookie con Domain=.example.com. Para dominios distintos (por ejemplo, proveedor de checkout), no podrás compartir cookies; necesitas “link decoration” (pasar un token por querystring) o un callback server‑to‑server que incluya tu correlation_id. La decoración de links debe firmarse (HMAC) para evitar que terceros inyecten IDs.
Stitching: cuando el usuario inicia sesión, “atas” el cid previo a un user_id (en tu backend) para unificar sesiones históricas. Hazlo con cuidado: si el usuario rechaza consentimiento, no debes unir ni persistir señal. Implementa un “consent state” por evento y por usuario, y guarda el mínimo.
Problemas reales: Safari puede purgar cookies “script‑set” antes; una mitigación es establecer cookies desde servidor (Set‑Cookie) en un response de navegación. En SSR (Next.js), aprovecha middleware para setear cid/sid en el primer request HTML. También puedes usar “server‑side tagging” para que los proveedores reciban señal desde tu dominio (menos bloqueable), pero sigue habiendo limitaciones.
5) Sección técnica 3: Modelado de eventos, contratos, idempotencia y deduplicación de conversiones
El núcleo de un tracking defendible es un contrato de eventos que sobreviva a refactors y a cambios de proveedor. Define un esquema versionado y validable (JSON Schema/Avro/Protobuf). Un evento de conversión debería incluir:
Identidad: event_id (UUIDv7 recomendado por orden temporal), client_id, session_id, user_id (si existe), consent (analytics/ads/personalization).
Temporalidad: occurred_at en ISO8601 UTC, received_at en servidor, source (client/server), environment (prod/stage).
Contexto: url, referrer, user_agent, ip (si procede y con controles), device, locale, experiment variants.
Negocio: tipo de conversión, valor, moneda, impuestos, order_id/lead_id, items (sku, qty, price), y estado (por ejemplo, “paid”, “refunded”).
Con eso puedes resolver dos problemas crónicos:
Idempotencia: los reintentos de red, los refreshes de la thank‑you page y los webhooks duplicados del PSP disparan eventos repetidos. Solución: toda conversión debe tener una clave idempotente estable. Para compras: order_id (o payment_intent_id) es la clave natural. Para leads: lead_id. Genera un event_id derivado o asociado a esa clave y almacena un registro de “ya procesado”. En el router, guarda un hash (destination + event_id) para evitar reenviar dos veces al mismo destino.
Deduplicación client/server: si envías Purchase desde cliente y desde servidor, ambos deben compartir event_id o transaction_id. Patrón: el frontend genera event_id al iniciar checkout y lo manda al backend; el backend lo persiste junto al pedido y lo reutiliza al confirmar pago. Si no puedes, entonces deduplicas por order_id (y aceptas que proveedores deduzcan por su lógica).
Semántica de conversión: no confundas “intención” con “resultado”. Define: CheckoutStarted, PaymentSubmitted, PurchaseCompleted, PurchaseRefunded. Solo PurchaseCompleted suma revenue. Cuando hay reembolsos o chargebacks, emite eventos compensatorios; en ads platforms no siempre puedes “restar” correctamente, pero en tu warehouse sí. Esto reduce discrepancias entre BI y Ads Manager.
Versionado: añade schema_version. Cuando cambies nombres o estructura, no rompas consumidores; publica v2 y mantiene v1 un tiempo. En frontends con despliegues progresivos, es crítico: durante horas conviven versiones.
Validación: valida en gateway y en router. Rechaza eventos malformados con 400 si son síncronos; si son asíncronos, marca como “dead letter” para inspección. Un tracking avanzado tiene un “DLQ” real, no solo logs.
6) Sección técnica 4: Server-side tracking (GA4 MP, Meta CAPI, Google Ads) y resiliencia operativa
El paso de client‑side a server‑side no es “para saltarse el consentimiento” (eso sería ilegal); es para mejorar robustez (menos bloqueos), mejorar integridad (fuente de verdad) y controlar reintentos. Implementación típica: backend confirma conversión y llama a APIs de medición.
GA4 Measurement Protocol: te permite enviar eventos directamente con measurement_id y api_secret. Requiere client_id o user_id. Para deduplicar con web, usa event_id y envía el mismo en cliente (gtag) y servidor (MP). Importante: GA4 tiene límites de validación y puede descartar silenciosamente si falta formato. Debes tener tooling de validación (GA DebugView + endpoint de validation de MP).
Meta Conversions API (CAPI): necesita event_name, event_time, event_id y user_data (email/phone hash, ip, ua, fbp/fbc). El “matching” mejora si capturas fbp y fbc desde cookie/query y los persistes en backend. Sin consentimiento de ads, no debes enviar user_data a Meta. Implementa un filtro estricto.
Google Ads Enhanced Conversions / Offline conversions: para compras web, sueles reportar conversiones con gclid o con identifiers hash (email). Para deduplicación usa order_id como transaction_id si aplica. Para leads offline (CRM), necesitas subir conversiones cuando se cierran, con la click id capturada en el lead original. Aquí la ingeniería es de “join”: lead → gclid → closed_won.
Resiliencia: los endpoints de proveedores fallan, rate‑limit, o responden 200 pero descartan. Patrón: encolar envíos por destino, reintentos con backoff, y almacenamiento de estado por (destination, event_id). Implementa circuit breakers y un panel de “event delivery” con métricas: success rate, lag, retries, DLQ size. Esto es SRE aplicado a tracking.
Seguridad: secretos (api_secret, access tokens) en un vault. Firma de requests si procede. Asegura que el endpoint /events no permita inyección: valida tamaño, tipos y valores; protege contra spam y replay (nonce o timestamp + firma si es “internal”).
Server-side GTM: alternativa/híbrido. Ventaja: UI para marketing, menos despliegues. Desventaja: complejidad operacional y riesgo de “shadow changes”. En un equipo técnico, mi recomendación es: server‑side GTM solo si hay gobernanza fuerte (reviews, entornos, y control de versiones) o si el router es propio y GTM se limita a un subconjunto.
7) Sección técnica 5: Consentimiento, privacidad, anti‑adblock, y pruebas de calidad (QA) end‑to‑end
Consentimiento por propósito: no basta con “aceptar cookies”. Implementa estados por finalidad: necessary, analytics, ads, personalization. Cada evento debe llevar el estado de consentimiento vigente al momento del disparo. Si el usuario revoca, debes dejar de enviar a destinos correspondientes y, si tu política lo requiere, borrar/rotar identificadores (cid) o marcar eventos futuros como anónimos.
Minimización: envía lo mínimo a cada destino. A tu warehouse puedes mandar más (si hay base legal) pero aun así evita PII cruda en raw logs. Strategy: PII solo en sistemas que lo necesitan (CRM), y para ads platforms usa hashes (SHA‑256) generados en backend, normalizando (trim, lowercase, E.164 para teléfono). Guarda un indicador de “hash_source” y “normalization_version” para auditoría.
Anti‑adblock (sin trampas): el objetivo legítimo es mejorar entrega dentro de tu dominio, no evadir consentimiento. Medidas técnicas aceptables: servir scripts desde tu propio dominio, usar server‑side tracking, reducir dependencias de terceros, y degradar con gracia. Si el pixel es bloqueado, al menos tu backend registrará la conversión canónica (para BI). Para ads attribution, sin señal no hay milagros; la solución es capturar click IDs en landing y persistirlos first‑party.
Entornos y debug: añade environment y debug flags. Nunca mezcles staging con prod en el mismo property sin segmentación clara. Para QA, construye un “Event Inspector” interno: lista de eventos por sesión con payloads, validaciones y estado por destino. Esto ahorra semanas.
Testing automatizado: (1) unit tests para normalización/hashing, (2) contract tests contra JSON Schema, (3) integration tests con un sandbox de PSP que dispare webhooks duplicados, (4) end‑to‑end en Playwright/Cypress que valide que al comprar se generan: client event + server event, ambos con el mismo event_id y que el router marca “delivered”.
Reconciliación: cada día compara: revenue PSP vs revenue fact_conversions vs revenue GA4 vs revenue Ads. Espera discrepancias, pero mide su magnitud y causa. Implementa alertas si la tasa de discrepancia supera umbral (por ejemplo, >5% día). Muchas roturas de tracking se detectan así (cambio de dominio, cookie rota, release de CMP).
4) Ejemplos de código detallados
A continuación incluyo un ejemplo end‑to‑end simplificado: generación de IDs en Next.js, envío al gateway, persistencia del event_id en un pedido, confirmación por webhook y reenvío a GA4 MP y Meta CAPI con deduplicación.
4.1. Esquema TypeScript (contrato interno)
// types/events.ts
export type ConsentState = {
necessary: true;
analytics: boolean;
ads: boolean;
personalization: boolean;
};
export type Attribution = {
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_content?: string;
utm_term?: string;
gclid?: string;
fbclid?: string;
fbc?: string; // Meta click id cookie format
fbp?: string; // Meta browser id cookie
referrer?: string;
};
export type EcommerceItem = {
sku: string;
name?: string;
quantity: number;
price: number; // unit price
currency: string;
};
export type ConversionEvent = {
schema_version: 1;
event_id: string; // UUIDv7/UUIDv4
event_name: 'PurchaseCompleted' | 'LeadQualified' | 'TrialStarted';
source: 'client' | 'server';
occurred_at: string; // ISO UTC
client_id?: string;
session_id?: string;
user_id?: string;
consent: ConsentState;
attribution?: Attribution;
context?: {
url?: string;
user_agent?: string;
ip?: string;
locale?: string;
timezone?: string;
};
purchase?: {
order_id: string;
value: number;
currency: string;
tax?: number;
shipping?: number;
items?: EcommerceItem[];
};
lead?: {
lead_id: string;
value?: number;
currency?: string;
};
};
4.2. Middleware SSR para setear cookie first‑party (Next.js)
// middleware.ts (Next.js)
import { NextRequest, NextResponse } from 'next/server';
import { randomUUID } from 'crypto';
function getCookie(req: NextRequest, name: string) {
return req.cookies.get(name)?.value;
}
export function middleware(req: NextRequest) {
const res = NextResponse.next();
const cid = getCookie(req, 'cid');
const sid = getCookie(req, 'sid');
if (!cid) {
res.cookies.set('cid', randomUUID(), {
httpOnly: false, // si el cliente lo necesita para adjuntar a eventos
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 365,
});
}
if (!sid) {
res.cookies.set('sid', randomUUID(), {
httpOnly: false,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 60 * 30,
});
}
return res;
}
4.3. Captura de UTMs/click IDs y envío de evento (cliente)
// lib/tracking/client.ts
import { v7 as uuidv7 } from 'uuidv7';
function readCookie(name: string): string | undefined {
const m = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return m?.[2];
}
function persistAttribution(params: URLSearchParams) {
const keys = ['utm_source','utm_medium','utm_campaign','utm_content','utm_term','gclid','fbclid'];
for (const k of keys) {
const v = params.get(k);
if (v) localStorage.setItem('attr_' + k, v);
}
// Meta cookies if present (set by pixel when allowed). If not, still try reading.
const fbp = readCookie('_fbp');
const fbc = readCookie('_fbc');
if (fbp) localStorage.setItem('attr_fbp', fbp);
if (fbc) localStorage.setItem('attr_fbc', fbc);
}
export function initAttributionCapture() {
const url = new URL(window.location.href);
persistAttribution(url.searchParams);
}
export async function trackPurchaseClient(payload: {
orderId: string;
value: number;
currency: string;
consent: { analytics: boolean; ads: boolean; personalization: boolean };
}) {
const event_id = uuidv7();
const cid = readCookie('cid');
const sid = readCookie('sid');
const body = {
schema_version: 1,
event_id,
event_name: 'PurchaseCompleted',
source: 'client',
occurred_at: new Date().toISOString(),
client_id: cid,
session_id: sid,
consent: { necessary: true, ...payload.consent },
attribution: {
utm_source: localStorage.getItem('attr_utm_source') || undefined,
utm_medium: localStorage.getItem('attr_utm_medium') || undefined,
utm_campaign: localStorage.getItem('attr_utm_campaign') || undefined,
gclid: localStorage.getItem('attr_gclid') || undefined,
fbclid: localStorage.getItem('attr_fbclid') || undefined,
fbp: localStorage.getItem('attr_fbp') || undefined,
fbc: localStorage.getItem('attr_fbc') || undefined,
referrer: document.referrer || undefined,
},
context: {
url: window.location.href,
user_agent: navigator.userAgent,
locale: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
purchase: {
order_id: payload.orderId,
value: payload.value,
currency: payload.currency,
},
};
// Fire-and-forget; gateway should return 202.
await fetch('/api/events', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
keepalive: true,
});
return { event_id };
}
4.4. Gateway /api/events: validación, persistencia raw, encolado
// pages/api/events.ts (Next.js API route) - simplificado
import type { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
const ConsentSchema = z.object({
necessary: z.literal(true),
analytics: z.boolean(),
ads: z.boolean(),
personalization: z.boolean(),
});
const EventSchema = z.object({
schema_version: z.literal(1),
event_id: z.string().min(10),
event_name: z.enum(['PurchaseCompleted', 'LeadQualified', 'TrialStarted']),
source: z.enum(['client', 'server']),
occurred_at: z.string(),
client_id: z.string().optional(),
session_id: z.string().optional(),
user_id: z.string().optional(),
consent: ConsentSchema,
attribution: z.any().optional(),
context: z.any().optional(),
purchase: z.any().optional(),
lead: z.any().optional(),
});
async function writeRawEvent(_e: unknown) {
// Escribe en un storage append-only (S3/GCS/DB). Aquí omitido.
}
async function enqueueEvent(_e: unknown) {
// Publica en cola (SQS/PubSub/Kafka). Aquí omitido.
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).end();
const parsed = EventSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'invalid_event', details: parsed.error.flatten() });
}
const event = parsed.data;
// Guarda raw para auditoría
await writeRawEvent(event);
// Encola para router asíncrono
await enqueueEvent(event);
return res.status(202).json({ ok: true });
}
4.5. Persistir event_id en el pedido (correlación) y emitir conversión server-side
// backend/orders.ts
// Al crear el pedido, guarda event_id generado en el cliente si lo recibes.
export async function createOrder(input: {
userId?: string;
cartId: string;
clientEventId?: string; // event_id de cliente
attribution?: { gclid?: string; fbp?: string; fbc?: string; utm_source?: string };
}) {
const orderId = 'ord_' + crypto.randomUUID();
// Persistencia (DB) - ejemplo conceptual
await db.orders.insert({
id: orderId,
user_id: input.userId ?? null,
client_event_id: input.clientEventId ?? null,
attribution: input.attribution ?? {},
status: 'created',
});
return { orderId };
}
// Cuando el PSP confirma pago (webhook), emite el evento canónico.
export async function markOrderPaid(orderId: string, psp: { paymentId: string; amount: number; currency: string }) {
const order = await db.orders.find(orderId);
if (!order) throw new Error('order_not_found');
// Idempotencia: si ya está pagado, no reemitir
if (order.status === 'paid') return;
await db.orders.update(orderId, { status: 'paid', paid_at: new Date().toISOString() });
const eventId = order.client_event_id ?? `server-${orderId}`; // ideal: reutiliza el del cliente
await eventBus.publish('ConversionEvent', {
schema_version: 1,
event_id: eventId,
event_name: 'PurchaseCompleted',
source: 'server',
occurred_at: new Date().toISOString(),
user_id: order.user_id ?? undefined,
consent: { necessary: true, analytics: true, ads: true, personalization: false }, // en real: deriva del CMP/usuario
attribution: order.attribution,
purchase: {
order_id: orderId,
value: psp.amount,
currency: psp.currency,
},
});
}
4.6. Router: deduplicación por destino y envío a GA4 + Meta CAPI
// router/consumer.ts
import crypto from 'crypto';
type Destination = 'ga4' | 'meta';
function deliveryKey(dest: Destination, eventId: string) {
return `${dest}:${eventId}`;
}
async function wasDelivered(key: string): Promise<boolean> {
const row = await db.delivery.find(key);
return !!row;
}
async function markDelivered(key: string) {
await db.delivery.insert({ key, delivered_at: new Date().toISOString() });
}
function sha256(value: string) {
return crypto.createHash('sha256').update(value).digest('hex');
}
async function sendToGA4(event: any) {
// Minimal GA4 MP payload
const payload = {
client_id: event.client_id ?? '555.555',
user_id: event.user_id,
timestamp_micros: Date.now() * 1000,
events: [
{
name: 'purchase',
params: {
currency: event.purchase.currency,
value: event.purchase.value,
transaction_id: event.purchase.order_id,
event_id: event.event_id, // clave para deduplicación
},
},
],
};
const url = `https://www.google-analytics.com/mp/collect?measurement_id=${process.env.GA4_MEASUREMENT_ID}&api_secret=${process.env.GA4_API_SECRET}`;
const r = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
if (!r.ok) throw new Error(`ga4_failed_${r.status}`);
}
async function sendToMetaCAPI(event: any) {
// Solo si hay consentimiento de ads
if (!event.consent?.ads) return;
// user_data: idealmente email/phone hash si existe con base legal; aquí usamos fbp/fbc + ip/ua
const user_data: any = {};
if (event.attribution?.fbp) user_data.fbp = event.attribution.fbp;
if (event.attribution?.fbc) user_data.fbc = event.attribution.fbc;
if (event.context?.ip) user_data.client_ip_address = event.context.ip;
if (event.context?.user_agent) user_data.client_user_agent = event.context.user_agent;
const payload = {
data: [
{
event_name: 'Purchase',
event_time: Math.floor(new Date(event.occurred_at).getTime() / 1000),
event_id: event.event_id,
action_source: 'website',
user_data,
custom_data: {
currency: event.purchase.currency,
value: event.purchase.value,
order_id: event.purchase.order_id,
},
},
],
};
const url = `https://graph.facebook.com/v18.0/${process.env.META_PIXEL_ID}/events?access_token=${process.env.META_ACCESS_TOKEN}`;
const r = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
if (!r.ok) throw new Error(`meta_failed_${r.status}`);
}
export async function handleConversionEvent(event: any) {
// Dedup por destino
for (const dest of ['ga4', 'meta'] as Destination[]) {
const key = deliveryKey(dest, event.event_id);
if (await wasDelivered(key)) continue;
if (dest === 'ga4') await sendToGA4(event);
if (dest === 'meta') await sendToMetaCAPI(event);
await markDelivered(key);
}
// Siempre escribe a warehouse interno
await db.fact_conversions.upsert({
event_id: event.event_id,
order_id: event.purchase?.order_id,
value: event.purchase?.value,
currency: event.purchase?.currency,
occurred_at: event.occurred_at,
source: event.source,
});
}
5) Comparativa de pros y contras
Client-side only (píxeles clásicos)
- Pros: rápido de implementar, baja carga en backend, accesible para equipos no técnicos, fácil verificación visual (extensiones/paneles).
- Contras: pérdida de señal por adblock/ITP, duplicados por refresh/reintentos, difícil reconciliación con fuente de verdad, conversiones infladas o deprimidas, dependencia alta de proveedores y de su JS, pobre control de idempotencia y calidad.
Server-side only (solo backend)
- Pros: alta integridad (evento nace de la verdad de negocio), resiliente a bloqueadores, control total de reintentos y observabilidad, más fácil cumplir contratos y deduplicar por order_id.
- Contras: atribución más débil si no capturas click IDs/UTMs en el cliente, requiere más ingeniería (colas, router, secretos), riesgo de enviar datos sin consentimiento si no se diseña bien, latencia/lag (aunque aceptable) y necesidad de correlación.
Híbrido (client + server con deduplicación)
- Pros: mejor combinación: atribución rica (cliente) + verdad de negocio (servidor), deduplicación controlada, robustez ante fallos del pixel, métricas internas defendibles.
- Contras: mayor complejidad: correlación, idempotencia, gobernanza de eventos, doble superficie de fallo; exige disciplina de schema y tooling de QA.
Server-side GTM vs Router propio
- Server-side GTM Pros: agilidad para marketing, plantillas para proveedores, menos código.
- Server-side GTM Contras: cambios sin review, difícil de testear como software, riesgo de derivar en “spaghetti tagging”, costes y operación (hosting), limitaciones en lógica compleja (deduplicación avanzada, reconciliación).
- Router propio Pros: control total, testabilidad, versionado, idempotencia sólida, integración nativa con dominio/PSP/CRM, seguridad y auditoría.
- Router propio Contras: tiempo de desarrollo, mantenimiento, necesidad de expertise en datos/infra.
6) Conclusión
El tracking de conversiones avanzado es un sistema distribuido orientado a eventos: requiere arquitectura, contratos, idempotencia, deduplicación, y observabilidad. La implementación robusta parte de una idea simple pero no negociable: la conversión “real” nace en el backend (fuente de verdad) y el frontend aporta contexto y señal de atribución. La forma de unir ambas sin inflar métricas es compartir event_id/order_id y aplicar deduplicación por destino, con reintentos controlados.
Si además integras consentimiento por propósito, minimización de datos, hashing en servidor, y reconciliación diaria con el PSP/CRM, obtienes un dataset defendible incluso cuando el navegador y el ecosistema publicitario cambian. La mejora no es solo “más conversiones medidas”; es menos ruido, menos discrepancia entre herramientas, y una base sólida para optimización de adquisición, pricing y producto. A partir de aquí, el siguiente salto natural es formalizar el catálogo de eventos (data governance), instrumentar experimentación con métricas confiables, y automatizar auditorías de tracking como parte de tu CI/CD.