El problema: publiqué en cinco lugares y no sé cuál funciona
Publico la misma oferta —"camisas de lino, 2300 CUP"— en un grupo de Facebook, en el estado de WhatsApp, en un canal de Telegram y en Revolico. Al final de la semana tengo mensajes, pero no puedo responder la única pregunta que importa: ¿qué publicación me los trajo?
El grupo no me dice cuántos clics salieron de mi publicación. El estado de WhatsApp no tiene métricas de origen. Revolico muestra visitas, no procedencia. Ninguna plataforma ofrece un botón que diga "de aquí salió quien te escribió".
Y aquí conviene separar cuatro preguntas que no son lo mismo:
- qué enlace etiquetado generó la visita;
- de qué plataforma procedió realmente el usuario;
- qué visita terminó en una consulta;
- qué consulta terminó en una venta.
Este artículo es sobre atribución de marketing, no sobre seguridad ni criptografía. Y la tesis es simple:
No necesitas que la plataforma te entregue datos de atribución si tú controlas el enlace que publicas. La información viaja en el enlace; tú la pones antes de publicar.
Voy por niveles, del más simple al más sofisticado. Al final vas a entender por qué casi nadie necesita el último.
Primero mide algo
Antes de pensar en etiquetas de campaña hace falta un lugar donde caigan los datos. Un módulo de analytics diminuto: registra el timestamp, el path, qué evento ocurrió (una vista, un clic) y, cuando existan, los parámetros de fuente.
Esto no es Google Analytics casero. No necesito fingerprinting, ni cookies obligatorias, ni identificar visitas individuales, ni un SDK que pese como un libro de texto. Son unas columnas en una tabla:
marketing_events (
created_at,
path,
event,
source,
medium,
campaign,
content
)
(o una versión agregada por día si lo que quieres es ahorrar almacenamiento).
Las primeras preguntas que responde son casi tontas, y son exactamente las que faltaban:
- ¿Cuántas visitas recibí?
- ¿Cuántas recibió determinada página?
- ¿De dónde parecen proceder?
Dónde vive esa tabla depende de dónde esté tu sistema. No hay respuesta universal:
- SQLite tiene todo el sentido en un servidor pequeño y persistente: es un archivo local, no pide permiso a nadie.
- PostgreSQL/Supabase tiene sentido cuando ya usas esa infraestructura: no añades un servicio, añades una tabla.
- En funciones serverless (Vercel, Lambda) el filesystem local no es persistente: cada invocación puede empezar de cero, y escribir en un archivo SQLite es perder datos silenciosamente. Ahí la base de datos vive fuera de la función.
Lo importante: para responder "cuántas visitas recibió cada campaña" no necesitas un data warehouse ni mil líneas de tag. Una tabla chica da para todo este artículo.
El truco no está en la plataforma: está en el enlace
Aquí está la pieza central. Facebook no tiene que darme un informe de atribución si yo ya puse la información en el enlace antes de publicarlo.
Si publico:
https://mitienda.com/oferta?utm_source=facebook
ese parámetro viaja hasta mi servidor cuando alguien hace clic. Yo controlo lo que publico, así que yo pongo la etiqueta. La plataforma es el cartero: entrega el enlace entero, parámetros incluidos.
UTM: la solución sencilla
UTM es el estándar para hacer exactamente eso. De los cinco parámetros que define, yo uso cuatro:
utm_source— el origen que quiero distinguir (facebook, whatsapp, telegram, revolico).utm_medium— el tipo de canal (social, messaging, classified…).utm_campaign— la campaña (camisas-2026-09).utm_content— la variante concreta de la pieza (grupo-venta, estado, anuncio-1).
La misma campaña, tres lugares:
?utm_source=facebook&utm_medium=social&utm_campaign=camisas-2026-09&utm_content=grupo-venta
?utm_source=whatsapp&utm_medium=messaging&utm_campaign=camisas-2026-09&utm_content=estado
?utm_source=revolico&utm_medium=classified&utm_campaign=camisas-2026-09&utm_content=anuncio-1
No hace falta usar los cuatro desde el día uno. Empezar solo con utm_source ya responde la pregunta más importante, y el resto se añade cuando la necesites.
Pero UTM mide el enlace, no prueba el origen
Precisión importante. UTM no te dice "este usuario vino de Facebook". Te dice algo más modesto y suficiente:
Este visitante usó el enlace etiquetado como Facebook.
Esto pasa todo el tiempo: publico el enlace "de Facebook", alguien lo copia, lo reenvía por WhatsApp, la próxima persona hace clic y mi servidor sigue viendo utm_source=facebook. Medir por enlaces etiquetados tiene esa limitación, y no es un defecto: es la naturaleza del método.
El Referer puede servir como señal secundaria —cuando llega un referer de facebook.com, apoya la intuición—, pero no es una fuente fiable de atribución primaria: puede faltar, venir reducido a solo el origen (los navegadores modernos envían strict-origin-when-cross-origin por defecto) o no existir si el clic vino de otra app. Con el utm_source bien puesto no tengo que pelear con eso.
Cuando tienes diez campañas, necesitas orden
UTM resuelve la medición. El siguiente problema es de higiene: cuando los enlaces se acumulan, aparecen cosas como:
?utm_source=facebook-grupo-1-camisas-septiembre-post-azul
eso no es un valor, es un archivo metido en un parámetro. Por eso se separan dimensiones: source, medium, campaign, content son columnas distintas, y cada una responde una pregunta distinta:
source→ ¿qué plataforma funciona?medium→ ¿qué tipo de canal funciona?campaign→ ¿qué campaña funciona?content→ ¿qué publicación concreta funciona?
Y lo más útil: la misma campaña vive en varias fuentes.
campaign = camisas-2026-09
source = facebook
source = whatsapp
source = telegram
Así comparo canales sin que la campaña se pierda de vista. GROUP BY source me dice dónde mirar; GROUP BY campaign me dice qué campaña entera funcionó.
Una oferta, muchos adaptadores
Con el tiempo, mantener cinco copias de la misma oferta se vuelve insostenible: cambio un precio y edito cinco lugares, y siempre queda una vieja. La oferta existe una sola vez:
id: oferta-2026-09-camisas
precio_cup: 2300
Y cada canal tiene su configuración y su copia:
canales:
- id: grupo-venta
source: facebook
medium: social
content: grupo-venta
- id: estado-wa
source: whatsapp
medium: messaging
content: estado
- id: revolico
source: revolico
medium: classified
content: anuncio-1
La URL de cada canal se genera desde ahí, y publicar una oferta nueva es escribir un bloque YAML.
Pero publicar no es copiar el mismo texto a cinco sitios: cada canal tiene reglas distintas de texto, imagen, tono y CTA. Por eso cada canal es un adaptador:
def render(oferta, canal):
adapter = ADAPTERS[canal.id]
return adapter.render(
oferta=oferta,
tracking=tracking_params(canal),
)
El tracking sale del mismo lugar que la pieza: así no existe la posibilidad de publicar y olvidar etiquetar.
En paralelo llevo cuenta de qué publiqué. Eso es un problema distinto del de analytics:
publicaciones (
oferta,
canal,
pieza_hash,
publicado_at
)
El analytics responde "qué funciona"; esta tabla responde "qué publiqué". El pieza_hash sirve para saber si la pieza cambió y no republicar lo mismo dos veces. Es deduplicación e idempotencia: una herramienta de higiene, no de seguridad.
Medir clics no significa medir ventas
Aquí el sistema se pone honesto. Un ejemplo realista:
| Canal | Visitas | Consultas | Ventas |
|---|---|---|---|
| 100 | 10 | 1 | |
| 40 | 20 | 8 | |
| Revolico | 200 | 5 | 0 |
Revolico ganó el tráfico. WhatsApp ganó el negocio. "Más clics" no significa "mejor marketing", y eso solo se ve cuando unes visitas con conversiones.
El salto de publicación → visita se mide solo. El salto visita → consulta → venta no: al principio se registra a mano, con una columna más en la tabla de eventos o una hoja aparte. Veinte apuntes al mes tiran más decisiones que cien dashboards. Y no prometo automatizar eso: si algún día el volumen lo pide, se automatiza; antes no.
¿Cuándo necesito IDs opacos?
Hasta aquí todo es legible: la URL dice utm_source=facebook&utm_campaign=camisas-2026-09 y cualquiera lo lee. Eso es una ventaja —se depura mirando la URL— salvo el día en que quieras lo contrario.
Si aparece una necesidad real de no exponer la estructura interna, los parámetros se sustituyen por un código opaco:
?utm_source=facebook&utm_campaign=camisas-2026-09
se convierte en:
?c=7f3a
donde 7f3a responde a una fila interna que guarda campaña, canal y contenido. Un ID opaco sirve para URLs más cortas, para ocultar el esquema, para controlar el tracking sin romper enlaces viejos y para que el visitante no manipule los nombres de campaña.
Pero que quede claro: un ID opaco no hace la atribución más verdadera. Solo cambia cómo se ve en la URL.
¿Cuándo tendría sentido firmarlos?
La última capa, y la que necesita más cuidado. HMAC (una firma con secreto compartido) hace exactamente una cosa:
Te permite comprobar que un valor fue generado por alguien que conoce el secreto.
En un sistema de atribución puro, ¿para qué la querría? Casi nunca. Pero si el parámetro tiene consecuencias —un cupón, un descuento, un enlace de afiliado, atribución con impacto económico—, entonces sí me interesa:
- que no se fabrique un cupón válido a mano;
- que
?campaign=facebook&descuento=50no se lo crea nadie.
Firmado se vería así:
?campaign=facebook&sig=abc123
y el receptor verifica la firma con el secreto antes de confiar en el parámetro.
Y lo que una firma NO hace, porque se vende mal:
- No demuestra que la visita "realmente vino de Facebook". Eso lo sigue haciendo, con sus límites, el
utm_source. - No evita que alguien abra 300 veces un enlace legítimo. Reusar un enlace válido es reusar un enlace válido; la firma no distingue abuso de uso.
- No vuelve el tracking "confiable" en general: protege la autenticidad del parámetro que firma, y nada más.
Traducción: para el problema de "qué publicación funciona", HMAC casi seguro no hace falta. Para cupones y afiliados, sí. Distinguir esos dos casos vale más que cualquier librería de firma.
La regla: no construyas el nivel 7 si el nivel 2 resuelve el problema
El camino completo, condensado:
1. Analytics mínimo → "¿cuántas visitas tengo?"
2. UTM → "¿qué enlace etiquetado genera visitas?"
3. Dimensiones s/m/c/c → "¿qué canal, campaña o pieza funciona?"
4. Fuente única + adap. → "¿cómo evito inconsistencias al publicar?"
5. Conversiones → "¿qué canal genera negocio?"
6. ID opaco → "¿necesito ocultar o separar la identidad interna?"
7. HMAC → "¿el parámetro tiene consecuencias que exigen integridad?"
La tesis:
Empieza arriba y baja solo cuando tengas un problema real que justifique la complejidad.
No al revés. Para publicar ofertas en grupos, estados y clasificados, la cadena que mueve los números es la de arriba hasta el nivel 5. El ranking de canales sale con un GROUP BY, los datos te pertenecen y nadie pagó por un SDK.