DEV Community

Cover image for Let the Link Carry the Tracking: UTM, One Table, and Knowing When You Actually Need Signatures
Armando
Armando

Posted on

Let the Link Carry the Tracking: UTM, One Table, and Knowing When You Actually Need Signatures

The same offer goes into a Facebook group, a WhatsApp status, a Telegram channel and a classifieds site. The platform won't tell you which one works — but the link you publish can.

Before the code, four things you should never confuse:

  1. which tagged link generated the visit;
  2. which platform the visitor actually came from;
  3. which visit turned into an inquiry;
  4. which inquiry turned into a sale.

UTM answers #1. It never answers #2. Conversations are #3, sales are #4 — see "clicks are not sales" below.

Step 0 — a table that measures something

No UTM is useful without a place to land. A minimal events table is enough; no data warehouse, no customer SDK:

marketing_events (
  created_at,
  path,
  event,
  source,
  medium,
  campaign,
  content
)
Enter fullscreen mode Exit fullscreen mode

source/medium/campaign/content start empty and get filled once you tag links. If storage is your constraint, aggregate by day instead of storing every event.

Storage depends on deployment, not on fashion:

  • SQLite — right on a small persistent server (one local file, no external service).
  • PostgreSQL/Supabase — right when that's already your stack (add a table, not a service).
  • Serverless (Vercel, Lambda) — the local filesystem is ephemeral: per-invocation, recycled, sometimes read-only. A SQLite file there is silent data loss. Put the database outside the function.

Step 1 — tag the link, don't beg the platform

The platform sends the full URL when someone clicks. So before publishing, embed what you need:

https://your-site.com/offer?utm_source=facebook
Enter fullscreen mode Exit fullscreen mode

The label travels with the click. The platform is just transport.

The four parameters you'll actually use:

Param Role Example
utm_source origin you want to tell apart facebook
utm_medium channel type social
utm_campaign promotion grouping camisas-2026-09
utm_content specific variant of the piece grupo-venta

Same campaign, three sources:

?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
Enter fullscreen mode Exit fullscreen mode

Start with only utm_source if you want; the rest is added when a question demands it.

Explicit assumption: you control the destination page (to read the query string) and you generate the links before posting. If a shortener or a third party generates them, you don't own the data anymore.

What UTM does and doesn't prove

  • Does: tells you which labeled link was used.
  • Doesn't: tell you the physical platform. Links get copied and forwarded; a "facebook" link clicked from WhatsApp still reads utm_source=facebook. That's inherent to the method, not a bug of your code.

The Referer header is a secondary signal only: browsers default to strict-origin-when-cross-origin (cross-origin sends just the origin), some policies strip it, apps often send none. Don't build primary attribution on it.

Step 2 — one axis per column, then GROUP BY

Giant compound labels are an antipattern:

utm_source=facebook-group-1-shirts-september-blue-post
Enter fullscreen mode Exit fullscreen mode

Keep dimensions separate so each question has its own axis:

SELECT source, COUNT(*)
FROM marketing_events
WHERE campaign = 'camisas-2026-09'
GROUP BY source
ORDER BY 2 DESC;
Enter fullscreen mode Exit fullscreen mode

source → which platform works. campaign → whether the whole campaign worked. Same campaign, many sources, comparable without losing identity.

Step 3 — single source of truth + one adapter per channel

The offer exists once:

id: oferta-2026-09-camisas
precio_cup: 2300
Enter fullscreen mode Exit fullscreen mode

Each channel carries its own tracking + copy:

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
Enter fullscreen mode Exit fullscreen mode

URLs are generated from that config, so a price change is one edit and tagging can't be forgotten. Channels differ in text, image, tone and CTA, hence an adapter interface:

def render(oferta, canal):
    adapter = ADAPTERS[canal.id]
    return adapter.render(
        oferta=oferta,
        tracking=tracking_params(canal),
    )
Enter fullscreen mode Exit fullscreen mode

What you published is a separate concern from analytics:

publicaciones (
  oferta,
  canal,
  pieza_hash,
  publicado_at
)
Enter fullscreen mode Exit fullscreen mode

pieza_hash detects content changes so you don't republish the same piece twice — deduplication/idempotency, not security.

Step 4 — clicks are not sales

Channel Visits Inquiries Sales
Facebook 100 10 1
WhatsApp 40 20 8
Classifieds 200 5 0

Classifieds won traffic; WhatsApp won business. Publication → visit auto-measures. Visit → inquiry → sale doesn't: record it (one column, a sheet), start manual, automate only when volume justifies it.

Step 5 — opaque IDs (why, when)

When you need URLs that don't expose campaign internals (shorter, stable, non-editable):

?c=7f3a
Enter fullscreen mode Exit fullscreen mode

7f3a → a row with campaign/channel/content. It doesn't make attribution truer; it changes representation.

Step 6 — signatures (why almost never, and when yes)

HMAC with a shared secret verifies one thing: a value was generated by whoever holds the secret.

Use it when the parameter has consequences: coupons, discounts, affiliate links, attribution that pays someone.

Verbatim non-goals:

  • it does not prove the platform of origin;
  • it does not stop someone reusing a legitimate link 300 times (replay of a valid link is still a valid link);
  • it does not make tracking trustworthy in general — only the parameter it signs.

Decision checklist

  • Just started → events table + utm_source. Stop there.
  • Several channels per campaign → add medium/campaign/content, group by axis.
  • Growing ≥3 places → single source YAML + adapters + generate URLs.
  • Need to know what to keep publishing → manual conversion column before any dashboard.
  • URL looks too informative / needs to change scheme without breaking → opaque ID.
  • Parameter has money attached to it → HMAC. Otherwise don't.

The channel ranking is a GROUP BY. The data is yours. You don't need an enterprise tag manager to answer "which publication should I keep posting?"


Adapted from my original post on TallerWeb.

Top comments (0)