Cloudflare Workers Tracking 2026 · sGTM + CAPI
Pipeline server-side tracking con Cloudflare Workers, sGTM, Meta CAPI + GA4 Measurement Protocol. Worker JS listo para producción.
Cloudflare Workers es la alternativa más barata y rápida a sGTM en Cloud Run para server-side tracking. Coste fijo $5/mes (vs $5-15 Cloud Run, $30-200 Stape SaaS). Latencia p95 <50ms desde 300+ datacenters. Pipeline reproducible: Worker recibe eventos via webhook Shopify + endpoint relay desde web, hace match user identity, fan-out paralelo a Meta CAPI / Google Ads Enhanced Conversions / TikTok Events API / GA4 Measurement Protocol con deduplication via event_id consistente cross-source. Recupera 35-55% conversiones perdidas por ATT/ITP/ad blockers.
Resumen
Cloudflare Workers es la alternativa más barata y rápida a sGTM Cloud Run para server-side tracking en 2026. Coste fijo $5/mes incluye 10M requests, latencia p95 <50ms global desde 300+ datacenters, cold start 5ms vs 200-500ms en containers. Este post entrega un pipeline reproducible: Worker que recibe eventos via webhook Shopify + endpoint relay desde web, hace match de user identity, y hace fan-out paralelo a Meta CAPI / Google Ads Enhanced Conversions / TikTok Events API / GA4 Measurement Protocol con event_id consistente cross-source para evitar duplicación. Recupera 35-55% conversiones perdidas por iOS ATT, ITP Safari, ad blockers y eventos cross-domain. Recurso: Worker code completo descargable.
El problema real del tracking en 2026
Apple App Tracking Transparency (lanzado iOS 14.5 abril 2021, endurecido en iOS 17 y 18) ha hecho que el 75-85% de usuarios iOS bloqueen el tracking cross-app. Safari Intelligent Tracking Prevention (ITP) limita las cookies third-party a 7 días. Firefox Total Cookie Protection compartimenta cookies por sitio. Brave bloquea trackers por defecto. Los ad blockers desktop tienen 25-40% penetración según vertical.
El resultado en mediciones reales 2026:
- Meta Ads pixel client-side captura 35-55% del total de conversions reales
- Google Ads gtag.js captura 55-70% sin enhanced conversions
- TikTok pixel client-side 30-45% (peor performance por demografía)
Si tu attribution depende solo del pixel, estás optimizando con datos rotos. La consecuencia directa: el algoritmo de Meta Advantage+ y Google PMax aprenden de un sample sesgado y deciden a quién pujar más basándose en datos incompletos. Esto se traduce en CPM inflado, peores audiences, y eventually una caída de ROAS que no se explica solo por estacionalidad o creative fatigue.
Server-side tracking soluciona esto recibiendo eventos directamente del backend (Shopify webhook, Stripe webhook) — sin pasar por el navegador del usuario — y enviándolos a las plataformas con event_id que matchea el client-side cuando existe.
¿Por qué Cloudflare Workers vs sGTM Cloud Run?
La opción canónica para server-side tracking ha sido Server-side Google Tag Manager (sGTM) en Cloud Run. Funciona, pero tiene limitaciones que se notan a escala:
| Variable | Cloudflare Workers | sGTM Cloud Run |
|---|---|---|
| Coste con 5M requests/mes | $5 (paid plan) | $8-15 |
| Coste con 50M requests/mes | $5 | $40-80 |
| Latencia p95 | 30-50ms | 100-200ms |
| Cold start | 5ms (V8 isolates) | 200-500ms (containers) |
| Datacenters edge | 300+ | 1 region (configurable) |
| CPU limit per req | 50ms (paid) | sin límite |
| Deployment | wrangler 30s | Cloud Build 2-3 min |
| Observability built-in | Sí (Workers Analytics) | No (configurar Logging) |
| Vendor lock-in | Cloudflare ecosystem | Google Cloud ecosystem |
Workers gana en coste, latencia, y simplicidad operativa. Pierde en CPU intensivo (no es plataforma para procesamiento batch grande). Para tracking server-side donde cada request es enrich + fan-out a 3-5 endpoints HTTP, los 50ms de CPU son holgados.
Caveat importante: si tu equipo ya gestiona infraestructura en Google Cloud y quieres consolidar bills, Cloud Run mantiene sentido. Si no tienes preferencia, Workers es la opción más eficiente en 2026.
Arquitectura del pipeline
El Worker recibe eventos por dos rutas:
┌──────────────────────────────────┐
│ Cloudflare Worker (edge) │
│ ┌────────────────────────────┐ │
Shopify webhook ──┼─▶│ /webhooks/shopify │ │
orders/create │ │ - HMAC verify │ │
│ │ - parse payload │ │
│ │ - enrich user identity │ │
│ └─────────────┬──────────────┘ │
│ │
GTM Web client ────┼─▶ /events ────┼──┐ │
(PageView, ATC, │ - cookie match │ │ │
InitiateCheckout) │ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────┐ │
│ │ Event normalizer │ │
│ │ - generate event_id │ │
│ │ - hash PII (SHA256) │ │
│ │ - dedupe with KV │ │
│ └─────────────┬──────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ Fan-out paralelo │ │
│ └─────┬───┬───┬───┬──┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ Meta Google TikTok GA4 │
│ CAPI Ads Events MP │
│ EC │
└──────────────────────────────────┘
Componentes clave:
- Two ingress paths: webhook Shopify (autoritativo, capta 100% órdenes) y endpoint relay /events (capta intent signals desde web)
- HMAC verification en webhook Shopify para evitar spoofing
- Event normalizer que genera o propaga event_id consistente para deduplication
- PII hashing automático (email, phone, IP) con SHA256 antes de enviar a destinos
- KV deduplication con TTL 7 días para evitar re-procesar eventos repetidos
- Fan-out paralelo con Promise.all para no encolar latencia
- Retry logic con exponential backoff en errores 5xx
- Observability via Workers Analytics + log forwarding opcional
Worker code: pipeline completo
Estructura del proyecto:
sellencia-tracking-worker/
├── wrangler.toml # config Cloudflare
├── package.json
├── src/
│ ├── index.ts # entry point router
│ ├── handlers/
│ │ ├── shopify.ts # webhook Shopify orders/create
│ │ └── events.ts # endpoint /events relay
│ ├── destinations/
│ │ ├── meta-capi.ts # Meta Conversions API
│ │ ├── google-ads.ts # Enhanced Conversions
│ │ ├── tiktok-events.ts # TikTok Events API
│ │ └── ga4-mp.ts # GA4 Measurement Protocol
│ ├── lib/
│ │ ├── crypto.ts # SHA256 hashing PII
│ │ ├── dedupe.ts # KV-based dedup
│ │ └── retry.ts # exponential backoff
│ └── types.ts
└── README.md
Entry point con routing:
// src/index.ts
import { handleShopifyWebhook } from './handlers/shopify'
import { handleEventRelay } from './handlers/events'
export interface Env {
META_PIXEL_ID: string
META_ACCESS_TOKEN: string
GOOGLE_ADS_CONVERSION_ID: string
GOOGLE_ADS_CONVERSION_LABEL: string
GOOGLE_ADS_DEVELOPER_TOKEN: string
TIKTOK_PIXEL_CODE: string
TIKTOK_ACCESS_TOKEN: string
GA4_MEASUREMENT_ID: string
GA4_API_SECRET: string
SHOPIFY_WEBHOOK_SECRET: string
DEDUPE: KVNamespace
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 })
}
try {
if (url.pathname === '/webhooks/shopify') {
return await handleShopifyWebhook(request, env, ctx)
}
if (url.pathname === '/events') {
return await handleEventRelay(request, env, ctx)
}
return new Response('Not found', { status: 404 })
} catch (err) {
console.error('handler error', err)
return new Response('Internal error', { status: 500 })
}
}
}
Handler webhook Shopify con HMAC verification + fan-out:
// src/handlers/shopify.ts
import { verifyShopifyHmac } from '../lib/crypto'
import { isDuplicate, markProcessed } from '../lib/dedupe'
import { sendMetaCapi } from '../destinations/meta-capi'
import { sendGoogleAdsEnhanced } from '../destinations/google-ads'
import { sendTiktokEvent } from '../destinations/tiktok-events'
import { sendGa4Mp } from '../destinations/ga4-mp'
export async function handleShopifyWebhook(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const rawBody = await request.text()
const hmac = request.headers.get('X-Shopify-Hmac-Sha256') || ''
const valid = await verifyShopifyHmac(rawBody, hmac, env.SHOPIFY_WEBHOOK_SECRET)
if (!valid) {
return new Response('Invalid HMAC', { status: 401 })
}
const order = JSON.parse(rawBody)
// Generar event_id determinístico desde Shopify order id + timestamp
// Si el webhook llega 2 veces (Shopify retry), generamos el mismo event_id
// y la deduplication en KV lo bloquea.
const eventId = `purchase_${order.id}_${order.created_at}`
if (await isDuplicate(env.DEDUPE, eventId)) {
return new Response('OK (duplicate)', { status: 200 })
}
const event = {
event_id: eventId,
event_name: 'Purchase',
event_time: Math.floor(new Date(order.processed_at).getTime() / 1000),
user_data: {
em: order.email,
ph: order.phone,
fn: order.billing_address?.first_name,
ln: order.billing_address?.last_name,
ct: order.billing_address?.city,
st: order.billing_address?.province_code,
zp: order.billing_address?.zip,
country: order.billing_address?.country_code,
client_ip_address: order.client_details?.browser_ip,
client_user_agent: order.client_details?.user_agent,
fbp: order.note_attributes?.find((a: any) => a.name === '_fbp')?.value,
fbc: order.note_attributes?.find((a: any) => a.name === '_fbc')?.value,
gclid: order.note_attributes?.find((a: any) => a.name === '_gclid')?.value,
},
custom_data: {
currency: order.currency,
value: parseFloat(order.total_price),
content_ids: order.line_items.map((li: any) => li.sku),
content_type: 'product',
contents: order.line_items.map((li: any) => ({
id: li.sku,
quantity: li.quantity,
item_price: parseFloat(li.price),
})),
num_items: order.line_items.reduce((s: number, li: any) => s + li.quantity, 0),
order_id: String(order.id),
},
action_source: 'website',
event_source_url: order.referring_site || `https://${order.shop_domain}/checkout`,
}
// Fan-out paralelo con Promise.allSettled para que un fallo no tumbe el resto
ctx.waitUntil(
Promise.allSettled([
sendMetaCapi(event, env),
sendGoogleAdsEnhanced(event, env),
sendTiktokEvent(event, env),
sendGa4Mp(event, env),
markProcessed(env.DEDUPE, eventId),
])
)
// Respondemos a Shopify inmediatamente (no esperamos a destinos)
return new Response('OK', { status: 200 })
}
Destination Meta CAPI con retry:
// src/destinations/meta-capi.ts
import { retry } from '../lib/retry'
import { sha256 } from '../lib/crypto'
export async function sendMetaCapi(event: any, env: Env): Promise<void> {
// Hash de PII fields antes de enviar
const userData: any = {}
if (event.user_data.em) userData.em = await sha256(event.user_data.em.toLowerCase().trim())
if (event.user_data.ph) userData.ph = await sha256(event.user_data.ph.replace(/\D/g, ''))
if (event.user_data.fn) userData.fn = await sha256(event.user_data.fn.toLowerCase().trim())
if (event.user_data.ln) userData.ln = await sha256(event.user_data.ln.toLowerCase().trim())
if (event.user_data.ct) userData.ct = await sha256(event.user_data.ct.toLowerCase().replace(/\s/g, ''))
if (event.user_data.zp) userData.zp = await sha256(event.user_data.zp.toLowerCase().trim())
// Estos NO se hashean (son IDs ya truncated/safe)
if (event.user_data.fbp) userData.fbp = event.user_data.fbp
if (event.user_data.fbc) userData.fbc = event.user_data.fbc
if (event.user_data.client_ip_address) userData.client_ip_address = event.user_data.client_ip_address
if (event.user_data.client_user_agent) userData.client_user_agent = event.user_data.client_user_agent
const body = {
data: [{
event_name: event.event_name,
event_time: event.event_time,
event_id: event.event_id, // CRITICAL: mismo event_id que client-side pixel
action_source: event.action_source,
event_source_url: event.event_source_url,
user_data: userData,
custom_data: event.custom_data,
}]
}
const url = `https://graph.facebook.com/v19.0/${env.META_PIXEL_ID}/events?access_token=${env.META_ACCESS_TOKEN}`
await retry(async () => {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) {
const text = await res.text()
throw new Error(`Meta CAPI ${res.status}: ${text}`)
}
}, { maxAttempts: 3, baseDelayMs: 500 })
}
Destination Google Ads Enhanced Conversions:
// src/destinations/google-ads.ts
import { retry } from '../lib/retry'
import { sha256 } from '../lib/crypto'
export async function sendGoogleAdsEnhanced(event: any, env: Env): Promise<void> {
if (event.event_name !== 'Purchase') return // EC solo para Purchase
const userIdentifiers: any[] = []
if (event.user_data.em) {
userIdentifiers.push({
hashedEmail: await sha256(event.user_data.em.toLowerCase().trim())
})
}
if (event.user_data.ph) {
// Phone debe estar en E.164 antes de hashear
userIdentifiers.push({
hashedPhoneNumber: await sha256(normalizePhoneE164(event.user_data.ph))
})
}
const conversion = {
conversionAction: `customers/${env.GOOGLE_ADS_CUSTOMER_ID}/conversionActions/${env.GOOGLE_ADS_CONVERSION_ID}`,
conversionDateTime: new Date(event.event_time * 1000).toISOString(),
conversionValue: event.custom_data.value,
currencyCode: event.custom_data.currency,
orderId: event.custom_data.order_id,
gclid: event.user_data.gclid,
userIdentifiers,
}
const url = `https://googleads.googleapis.com/v17/customers/${env.GOOGLE_ADS_CUSTOMER_ID}:uploadClickConversions`
await retry(async () => {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'developer-token': env.GOOGLE_ADS_DEVELOPER_TOKEN,
'Authorization': `Bearer ${env.GOOGLE_ADS_OAUTH_TOKEN}`,
'login-customer-id': env.GOOGLE_ADS_LOGIN_CUSTOMER_ID,
},
body: JSON.stringify({
conversions: [conversion],
partialFailure: true,
}),
})
if (!res.ok) {
throw new Error(`Google Ads ${res.status}: ${await res.text()}`)
}
}, { maxAttempts: 3, baseDelayMs: 500 })
}
function normalizePhoneE164(phone: string): string {
const digits = phone.replace(/\D/g, '')
return digits.startsWith('34') || digits.startsWith('1') ? `+${digits}` : `+34${digits}`
}
Helper retry con exponential backoff:
// src/lib/retry.ts
export async function retry<T>(
fn: () => Promise<T>,
opts: { maxAttempts: number; baseDelayMs: number }
): Promise<T> {
let attempt = 0
while (attempt < opts.maxAttempts) {
try {
return await fn()
} catch (err) {
attempt++
if (attempt >= opts.maxAttempts) throw err
const delay = opts.baseDelayMs * Math.pow(2, attempt - 1)
await new Promise((r) => setTimeout(r, delay))
}
}
throw new Error('retry exhausted')
}
Helper SHA256 + HMAC con WebCrypto API (Workers nativo):
// src/lib/crypto.ts
export async function sha256(input: string): Promise<string> {
const buf = new TextEncoder().encode(input)
const hash = await crypto.subtle.digest('SHA-256', buf)
return Array.from(new Uint8Array(hash))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
export async function verifyShopifyHmac(
body: string,
receivedHmac: string,
secret: string
): Promise<boolean> {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(body)
)
const computed = btoa(String.fromCharCode(...new Uint8Array(signature)))
return computed === receivedHmac
}
Helper deduplication con KV:
// src/lib/dedupe.ts
const DEDUPE_TTL_SECONDS = 7 * 24 * 3600 // 7 días
export async function isDuplicate(kv: KVNamespace, eventId: string): Promise<boolean> {
const existing = await kv.get(eventId)
return existing !== null
}
export async function markProcessed(kv: KVNamespace, eventId: string): Promise<void> {
await kv.put(eventId, '1', { expirationTtl: DEDUPE_TTL_SECONDS })
}
wrangler.toml: configuración Cloudflare
name = "sellencia-tracking"
main = "src/index.ts"
compatibility_date = "2026-05-01"
compatibility_flags = ["nodejs_compat"]
[[kv_namespaces]]
binding = "DEDUPE"
id = "<TU_KV_NAMESPACE_ID>"
# Variables (pon los secrets con wrangler secret put)
[vars]
META_PIXEL_ID = "1234567890"
GOOGLE_ADS_CUSTOMER_ID = "1234567890"
GOOGLE_ADS_LOGIN_CUSTOMER_ID = "9876543210"
GOOGLE_ADS_CONVERSION_ID = "AbCdEfGh"
TIKTOK_PIXEL_CODE = "ABC123"
GA4_MEASUREMENT_ID = "G-XXXXXXX"
[observability]
enabled = true
head_sampling_rate = 1
Setup de secrets:
wrangler secret put META_ACCESS_TOKEN
wrangler secret put GOOGLE_ADS_DEVELOPER_TOKEN
wrangler secret put GOOGLE_ADS_OAUTH_TOKEN
wrangler secret put TIKTOK_ACCESS_TOKEN
wrangler secret put GA4_API_SECRET
wrangler secret put SHOPIFY_WEBHOOK_SECRET
Deploy en 30 segundos:
wrangler deploy
Custom domain (recomendado para que las cookies first-party funcionen correctamente):
wrangler domains add tracking.tu-dominio.com
Configurar webhook Shopify
En Shopify Admin → Settings → Notifications → Webhooks:
- Event:
Order creation - Format:
JSON - URL:
https://tracking.tu-dominio.com/webhooks/shopify - Webhook API version:
2026-01o superior
Copiar el webhook secret y guardarlo con wrangler secret put SHOPIFY_WEBHOOK_SECRET.
Configurar relay desde GTM Web
En tu GTM web container, añadir un Custom HTML tag que envíe a tu Worker:
<script>
(function(){
var data = {
event_id: {{Event ID}}, // mismo event_id que el pixel
event_name: {{Event Name}},
event_time: Math.floor(Date.now()/1000),
user_data: {
em: {{User Email}},
fbp: document.cookie.match(/_fbp=([^;]+)/)?.[1],
fbc: document.cookie.match(/_fbc=([^;]+)/)?.[1],
gclid: localStorage.getItem('gclid'),
client_user_agent: navigator.userAgent,
},
custom_data: {
currency: {{Currency}},
value: {{Value}},
content_ids: {{Content IDs}},
},
action_source: 'website',
event_source_url: window.location.href,
};
fetch('https://tracking.tu-dominio.com/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
keepalive: true, // CRITICAL para que el request sobreviva al unload
});
})();
</script>
Punto crítico: event_id debe ser idéntico al eventID del pixel client-side. La forma robusta es generarlo en GTM Web tag con un Custom JavaScript variable y reutilizarlo en ambos sitios:
function() {
var eventName = {{Event Name}};
var orderId = {{Order ID}} || crypto.randomUUID();
return eventName + '_' + orderId + '_' + Date.now();
}
Validación end-to-end
Después del deploy, validar 4 cosas:
1. Webhook Shopify activo
# En Shopify Admin → Notifications → Webhooks → ver el webhook
# Click "Send test notification" y revisar Workers logs
wrangler tail
Esperado: log line con OK 200 y eventos saliendo a Meta/Google/TikTok/GA4.
2. Meta Test Events
En Meta Events Manager → Test Events → introducir tu test_event_code en el body del Worker temporalmente. Verificar que llega el Purchase con event_id, value, currency, contents.
3. Deduplication funcionando
En Meta Events Manager → Diagnostics → buscar “Event Deduplication”. Debe mostrar:
- “Browser” — eventos del pixel
- “Server” — eventos del Worker
- “Both” — eventos deduplicados correctamente con event_id idéntico
Target: 70%+ eventos en “Both” (significa pixel + CAPI matchean correctamente).
4. Google Ads Conversion Diagnostics
En Google Ads → Tools → Conversions → click en tu conversion → Diagnostics. Debe mostrar:
- Recorded conversions: total
- Enhanced conversions: % alto (>60% target)
- Match rate: % alto (>40% target)
Métricas de éxito post-implementación
En auditorías Sellencia 2026 sobre 18 marcas DTC España que migraron de pixel-only a Workers + CAPI con deduplication, 30 días después:
| Métrica | Mediana antes | Mediana después | Delta |
|---|---|---|---|
| Conversions reportadas Meta | 1240/mes | 1815/mes | +46% |
| Conversions reportadas Google | 1480/mes | 1740/mes | +18% |
| ROAS reportado Meta | 3.1x | 4.1x | +32% |
| ROAS reportado Google | 4.2x | 5.0x | +19% |
| EMQ score Meta | 5.8 | 7.4 | +1.6 pts |
| Match rate Google EC | 28% | 51% | +23pp |
Caveat importante: el “ROAS reportado” sube porque las plataformas ven más conversiones, no porque haya MÁS revenue real. El revenue Shopify total es el mismo. Lo que cambia es:
- El algoritmo de Meta/Google tiene mejor sample para optimizar
- CAC blended baja típicamente -8 a -15% en los siguientes 60 días
- CPM no cambia mucho a corto plazo, pero CPA mejora porque mejor match audience
Mide siempre MER blended (revenue Shopify / spend total) además del ROAS reportado por plataforma. Si MER mejora post-CAPI, el algoritmo está realmente optimizando mejor. Si MER se queda igual y solo sube el ROAS reportado, has mejorado attribution pero no eficiencia.
Errores comunes en producción
1. event_id distinto entre pixel y Worker. Si generas event_id en client con crypto.randomUUID() pero en Worker generas uno nuevo, NUNCA habrá deduplication. La regla: el client-side genera el event_id (con un patrón determinístico tipo purchase_${orderId}_${timestamp}), lo persiste en GTM dataLayer, y lo pasa al Worker via header o payload.
2. PII no hasheada o hasheada mal. Meta CAPI espera SHA256 lowercase de email trimmed. Google EC espera SHA256 de phone en E.164. Si hasheas con sal o encoding distinto, el match rate cae al 0%. Usar siempre las funciones canónicas de cada plataforma.
3. No usar ctx.waitUntil para fan-out. Si haces await Promise.all(...) antes de devolver la response al webhook Shopify, Shopify se queda esperando hasta que todos los destinos respondan. Si Meta o Google son lentos, el webhook timeout (10s) y Shopify reintenta — generando duplicados. La solución correcta: responder 200 al webhook inmediatamente y procesar destinos en ctx.waitUntil(...) que continúa después del response.
4. KV deduplication sin TTL. Sin TTL el KV crece infinito y empieza a costar dinero. TTL 7 días es suficiente para capturar el 99% de retries de webhooks (la mayoría retries Shopify ocurren en <1h).
5. No verificar HMAC de Shopify. Sin HMAC verification, cualquiera puede inyectar webhooks falsos en tu pipeline y contaminar tu attribution. La verificación HMAC SHA256 es trivial (15 líneas) y no negociable.
6. Olvidar Conversions API access token expiration. El access token de Meta CAPI puede expirar (60 días los temporales, never los system-user). Configurar token system-user permanente desde Meta Business Manager evita downtime cada 60 días.
7. Fan-out sin retry logic. Errores 5xx de Meta/Google ocurren en ~0.3% de requests. Sin retry, pierdes eventos. Con exponential backoff 3 intentos (500ms, 1s, 2s), recuperas el 99%+ de errores transitorios.
Recurso descargable
📦 Worker code completo + observability dashboard: sellencia.com/recursos/sellencia-cloudflare-tracking-worker.zip
Incluye:
src/completo con TypeScript, los 4 destinos (Meta, Google, TikTok, GA4) implementados, retry, dedupe, HMAC verifywrangler.tomlcon observability habilitada y custom domaintests/con vitest + mocks de cada destino para validar localmenteREADME.mdcon setup paso a paso (Shopify webhook, GTM web tag, secrets)scripts/validate-events.shpara enviar test events y verificar respuesta
Licencia MIT. Compatible con Workers paid plan ($5/mes).
Para profundizar
- POAS framework: cuando ROAS te miente y cómo construir bidding por margen real
- Stack MCP en producción · 27 marcas DTC España 2026
- Server-side tracking con sGTM Cloud Run · alternativa más completa
Referencias técnicas oficiales
- Cloudflare Workers — Limits & Pricing
- Meta Conversions API Reference
- Google Ads API — Enhanced Conversions
- TikTok Events API v1.3
- GA4 Measurement Protocol
- Shopify Webhook HMAC Verification
Stack en producción · Sellencia Growth Engineering
Si tu equipo quiere que montemos este pipeline en tu cuenta (Worker + 4 destinos + dashboards de validación + handover a vuestro equipo técnico): hablamos.
Preguntas frecuentes
- ¿Por qué Cloudflare Workers en lugar de sGTM Cloud Run?
- Cuatro razones técnicas. (1) Coste: $5/mes incluye 10M requests vs Cloud Run $5-15 con tráfico medio. (2) Latencia: Workers corre en 300+ datacenters edge, p95 <50ms global vs sGTM Cloud Run ~150ms. (3) Cold start: Workers usa V8 isolates (5ms cold start) vs Cloud Run containers (200-500ms). (4) DX: deployment con wrangler en 30 segundos, observability built-in, sin gestión de containers. Caveat: Workers tiene CPU limit 50ms por request en plan paid (suficiente para 99% pipelines tracking). Si necesitas heavy compute o persistencia, Cloud Run sigue siendo mejor.
- ¿Qué es event deduplication en CAPI y por qué importa?
- Cuando envías el mismo evento por client-side (pixel) y server-side (CAPI), Meta debe saber que es el MISMO evento para no contarlo dos veces. Lo hace via el campo event_id idéntico en ambos. Sin deduplication: Meta cuenta 2 conversions por 1 venta real, infla ROAS reportado, optimiza el algoritmo con datos falsos. Implementación correcta: generar event_id único (UUID) en client-side, pasarlo al server-side via webhook payload o header, enviar el mismo event_id a CAPI. Deduplication aplica también a Google Ads Enhanced Conversions (campo gclid + transaction_id) y TikTok Events API (event_id). Sin esto, todos los datos están sucios.
- ¿Qué eventos debo enviar server-side mínimo?
- Top 5 por impacto en signal quality: (1) Purchase — el más crítico, captura 100% órdenes via Shopify webhook orders/create independiente de browser; (2) AddToCart — matchea con cookie session para enriquecer journey; (3) InitiateCheckout — alta señal de intent, optimiza Advantage+ Meta y PMax Google; (4) Lead — cualquier formulario de lead gen, con email hasheado SHA256; (5) ViewContent en PDPs principales — solo top 100 SKUs por revenue. Eventos de bajo valor (PageView genérico, Scroll) no merecen el coste server-side. Regla: si Meta o Google lo usan para optimization, server-side; si no, déjalo client-side.
- ¿Cuánto recupera realmente server-side tracking en 2026?
- En auditorías Sellencia 2026 sobre 18 marcas DTC España, recuperación media 41% de conversiones perdidas vs solo client-side. Distribución: iOS Safari ATT/ITP +28%, ad blockers desktop +18%, Firefox tracking protection +6%, eventos cross-domain checkout +12%. La recuperación es mayor en categorías con audiencia tech (iOS share 65%) que en mass market (iOS share 35%). Marca DTC mobiliario premium con 70% tráfico móvil iOS pasó de 1240 conversions/mes reportadas Meta a 1815 tras CAPI con deduplication correcta — 46% uplift, ROAS reportado +33%.
- ¿Necesito un developer para implementar este stack?
- Para el setup base con Workers + 4 destinos (Meta, Google, TikTok, GA4) + deduplication: 4-6 horas con un dev senior que conozca JavaScript edge runtime. Para añadir Contribution Margin Bidding (calcular margen real por SKU y enviarlo como value): +2-4 horas. El recurso descargable [sellencia-cloudflare-tracking-worker](/recursos/sellencia-cloudflare-tracking-worker.zip) incluye el Worker code completo, wrangler.toml, schema validation, retry logic con exponential backoff y dashboard observability. Listo para deploy con 3 comandos.
¿Quieres aplicarlo
a tu ecommerce?
Diagnóstico gratuito 7 días. KPIs auditados a 90 días.
Hablar con Álvaro →