Skip to content

Build1 publisher3 min readPublished

A forwarded link reports utm_source=facebook even when the click came from WhatsApp

A dev.to walkthrough builds channel attribution from tagged URLs and one seven-column events table, and it is specific about where the labels stop being true and why a SQLite file on Lambda loses rows without an error.

The Engineer · Build desk

Illustration accompanying A forwarded link reports utm_source=facebook even when the click came from WhatsApp

What happened

  • A dev.to walkthrough separates four questions, from which tagged link produced a visit to which inquiry produced a sale, and says UTM parameters answer only the first of them.
  • The landing store is one table, marketing_events, with created_at, path, event, source, medium, campaign and content, and the post says no warehouse or customer SDK is needed.
  • Storage follows deployment: SQLite on a small persistent server, PostgreSQL or Supabase when that is already the stack, and a database outside the function on Vercel or Lambda.
  • The Referer header is kept as a secondary signal, since browsers default to strict-origin-when-cross-origin, some policies strip it, and apps often send none at all.
  • In the author's own run, classifieds won the traffic while WhatsApp won the business. The inquiry and sale steps were recorded by hand; nothing measured them.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Any offer whose links get forwarded reports the platform that was printed at publication time, so a source breakdown overstates the tagged channel and no amount of code fixes it.
  • decision A team deploying to a function runtime has to choose the database before writing the insert, because rows written to a local SQLite file disappear without raising an error.
  • exposure Hand link generation to a shortener or an agency and the attribution data sits with them. The seller is left with the platform's own numbers.
  • cost Signing every parameter buys key handling and no better attribution, so the secret is worth carrying only where the value moves money to someone.

A published link arrives with its query string intact. The platform hands the destination page the full URL, so whatever label you typed before posting is what the insert sees [8]. That fixes the boundary of the method. A link tagged utm_source=facebook, copied by a reader into WhatsApp and clicked there, still reads utm_source=facebook. The post treats this as inherent to the approach and says the code is not where the defect lies [9].

The landing table is seven columns: created_at, path, event, source, medium, campaign, content [2]. Four of them, source through content, stay empty until you tag links, so three are filled by the request itself and four by whoever built the URL [3][1]. The column list includes no visitor identifier. A count over this table therefore counts events [2]. Where storage is tight, the post suggests aggregating by day instead of keeping every row [4].

Serverless is where a file-based store breaks. The function filesystem is per-invocation, recycled and sometimes read-only, and the post says: "A SQLite file there is silent data loss." [7]

Compound labels like utm_source=facebook-group-1-shirts-september-blue-post are called an antipattern, because each question needs its own axis [11]. The three worked examples share utm_campaign=camisas-2026-09 and differ on source, medium and content [13]. So SELECT source, COUNT(*) ... WHERE campaign = 'camisas-2026-09' GROUP BY source answers which platform worked, while the campaign filter answers whether the campaign worked [12].

The tagging itself comes out of config. The offer exists once, id oferta-2026-09-camisas at precio_cup 2300, with a channel list underneath that carries source, medium and content. URLs are generated from that, so a price change is one edit and tagging cannot be forgotten [15]. Per-channel copy goes through an adapter interface because text, image, tone and CTA differ by channel [16]. What you published is a second table, publicaciones, keyed with a pieza_hash that detects content changes so the same piece is not republished twice, which the post labels deduplication and idempotency, not security [17].

Two of the four questions do not automate at this size. Publication to visit measures itself; visit to inquiry to sale does not, and the recommendation is one column or a sheet, manual first, automated when volume justifies it [18]. The whole design also rests on a stated assumption: you control the destination page and you generate the links before posting. If a shortener or a third party generates them, the data is not yours [14].

Short codes are presented as representation. A parameter like ?c=7f3a resolves to a row holding campaign, channel and content, and the post says it does not make attribution truer [19]. Signing is scoped just as narrowly. HMAC with a shared secret verifies one thing: that a value was generated by whoever holds the secret. The post reserves it for parameters with consequences: coupons, discounts, affiliate links, attribution that pays someone [20].

What to watch

  • A comparison of source counts against a platform's own click numbers would size how much link forwarding distorts the rows.
  • Whether the inquiry and sale columns ever get automated, and what monthly volume triggered the change.
  • Whether short-code redirects get added, since resolving ?c=7f3a moves link generation into a service that has to stay up.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories