Skip to content

fix(analytics): record link-configured UTMs on click events (SIT-382) - #43

Merged
onamfc merged 1 commit into
mainfrom
brandon/sit-382-record-link-configured-utms
Sep 1, 2026
Merged

fix(analytics): record link-configured UTMs on click events (SIT-382)#43
onamfc merged 1 commit into
mainfrom
brandon/sit-382-record-link-configured-utms

Conversation

@onamfc

@onamfc onamfc commented Sep 1, 2026

Copy link
Copy Markdown
Member

SIT-382

The problem

click_events.utm_* was only ever populated from the inbound query string on the short-link request. UTMs configured in link settings are appended to the destination URL by buildRedirectUrl, so they reached the landing page but never the click row. Every campaign analytics surface read NULL for a link-tagged click.

Verified against production: a pro-tier workspace had 0 of 3,111 clicks carrying a campaign, all-time, while 3,758 of its web events carried one.

The change

resolveClickUtms() resolves source / medium / campaign independently — inbound query-string value wins, link configuration fills anything left blank.

Inbound takes precedence because a value a sharer put on the short URL is more specific than static link configuration. Production has one workspace running Meta ads into a link whose configured campaign is the placeholder test; letting configuration win would replace their real per-ad campaign IDs with test.

Blank and whitespace-only values resolve to undefined rather than '', since campaign analytics filters on IS NOT NULL AND <> '' but groups on the raw value. Values are stored trimmed so " ig" and "ig" don't split into two UTM library entries. A repeated query parameter arriving as an array falls through to configuration instead of being handed to a VARCHAR column.

Blast radius

Strictly additive — no backfill, so no historical number changes. The write is inside setImmediate after the response, so redirect latency is untouched.

redirect.ts changes are two hunks (the import and the resolution block). Nothing within 90 lines of buildRedirectUrl (467, 606) or lf_click (629) — the destination URL and click-id paths are provably untouched.

Verification

  • npx tsc --noEmit clean
  • 213 tests pass across 14 files, including redirect.safety.test.ts
  • npm run build clean
  • 13 new cases in utils.test.ts covering precedence, per-key mixing, blanks, whitespace, non-object utm_parameters, and array query params

Note for whoever deploys

Campaign data will appear to start from zero on deploy day for workspaces that tag links. It's new data, not a spike — worth a changelog line so nobody trends across the discontinuity.

click_events.utm_* was only ever populated from the inbound query string on
the short-link request. UTMs configured in link settings are appended to the
destination URL by buildRedirectUrl, so they reached the landing page but
never the click row — campaign analytics read NULL for every link-tagged
click.

Add resolveClickUtms(), which resolves each of source/medium/campaign
independently: an inbound query-string value wins, and the link's configured
value fills anything left blank. Inbound takes precedence because a value a
sharer put on the short URL is more specific than static link configuration
(an ad platform appending a per-ad campaign id, for example), and letting
configuration overwrite it would destroy the finer signal.

Blank and whitespace-only values on either side resolve to undefined rather
than '', since campaign analytics filters on IS NOT NULL AND <> '' but groups
on the raw value. Values are stored trimmed so " ig" and "ig" do not split
into two UTM library entries. A repeated query parameter arriving as an array
falls through to configuration instead of being handed to a VARCHAR column.

The destination URL and lf_click construction are untouched.
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@onamfc
onamfc merged commit 7386c8e into main Sep 1, 2026
13 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant