What UTM parameters are
UTM parameters are key and value pairs added to the query string of a link. They do not change the page. They tell your analytics where the visit came from.
https://example.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=october_update
That link is a UTM URL. Anyone who clicks it lands on /pricing, and your analytics can credit the visit to the October newsletter.
There are six parameters most tools read:
| Parameter | Records | Example |
|---|---|---|
utm_source |
Who sent the visit | google, meta, newsletter |
utm_medium |
The channel type | cpc, paid, email |
utm_campaign |
The campaign name | spring_sale |
utm_id |
The campaign ID | 120210000000 |
utm_term |
The paid keyword | event_analytics |
utm_content |
The creative or link | hero_button |
Google Analytics asks you to always set source, medium and campaign, describes utm_id as the campaign ID, and treats values as case sensitive, so google and Google are two sources (Google Analytics help on URL builders). Use lowercase everywhere.
How to track UTMs end to end
Adding UTMs to links is the easy part. Most broken attribution comes from what happens after the click. To track UTMs properly you need four steps to work.
1. Tag every inbound link
Every link you control that points to your site from outside gets UTMs: ads, social posts, emails, partner pages, your bio links. Use one naming convention and write it down. The UTM builder lowercases values, replaces spaces and warns you about mixed case, which removes most mistakes before they reach a report.
Do not tag links inside your own site. A UTM on an internal link looks like a new campaign visit and overwrites the real source.
2. Read the parameters on landing
When the visitor lands, a script has to read the query string before anything removes it. Two things often get in the way:
- Redirects. Link shorteners,
httptohttpsredirects and login walls can drop the query string. Test the final URL, not the one you pasted. - Client side routers. Some single page apps rewrite the URL on load, or use hash routes like
/#/pricing?utm_source=..., where the parameters sit after the#and a naive script never sees them.
3. Store them across sessions
A visitor clicks an ad on Monday, leaves, and signs up on Thursday from a bookmark. If UTMs only live in the current URL, Thursday's signup is credited to "direct" and Monday's campaign gets nothing.
So the values need storage. Within a visit, session storage keeps them through page changes and redirects. Across visits, local storage (or a first party cookie) keeps them until the conversion happens. This is also where you decide what to keep when a second tagged visit arrives, which is the first touch versus last touch question.
4. Attach them to the conversion
The point of tracking UTMs is to know which campaign produced a lead or a customer. So the stored values must travel with the conversion event, the signup or the form submission, not just with page views. Then you can count leads, customers and revenue per campaign, and divide spend by them to get cost per lead and customer acquisition cost.
First touch vs last touch attribution
When a person arrives through more than one campaign before converting, you need a rule for who gets the credit.
| Model | Credits | Good for |
|---|---|---|
| First touch attribution | The campaign that first brought the visitor | Judging which channels create new demand |
| Last touch attribution | The most recent tagged visit before converting | Judging which channels close |
Example: someone first finds you through a LinkedIn post, comes back a week later through a Google search ad, and signs up. First touch credits LinkedIn. Last touch credits Google.
Neither is the truth on its own. Top of funnel channels tend to look weak on last touch, and retargeting tends to look strong. Keeping both lets you see the difference instead of guessing. Store the first touch once and never overwrite it. Overwrite the last touch on every new tagged visit.
Ad click IDs and auto tagging
Ad platforms add their own parameter to each click, called a click ID:
| Click ID | Platform |
|---|---|
gclid, gbraid, wbraid |
Google Ads |
fbclid |
Meta (Facebook and Instagram) |
ttclid |
TikTok |
msclkid |
Microsoft Advertising |
li_fat_id |
In Google Ads this is called auto tagging. It adds a gclid to every click and is needed to track conversions and to bring Google Ads cost data into Google Analytics. In GA4, auto tagging takes priority over manual UTMs (Google Ads help on auto tagging). Keep it on.
Click IDs and UTMs do different jobs. A click ID ties a visit to one specific ad click, which the platform can match back to a campaign. UTMs are readable by any tool and work on channels with no click ID at all, like email. Use both. For Google Ads, add UTMs to the tracking template with ValueTrack parameters so the campaign ID is filled in for you (ValueTrack parameters):
utm_source=google&utm_medium=cpc&utm_campaign={campaignid}&utm_id={campaignid}
For Meta Ads, the fbclid proves a visit came from a Meta ad, but Meta has no click level report to match it with a campaign. Put the campaign ID in utm_id with Meta's dynamic URL parameters (Meta help on dynamic URL parameters):
utm_source=meta&utm_medium=paid&utm_campaign={{campaign.name}}&utm_id={{campaign.id}}
utm_id is the safer key in both cases. A campaign name changes when someone renames it, and the link between old visits and new spend breaks. The ID stays the same.
Single page app pitfalls
Single page apps (React, Vue, Svelte, Next.js and similar) break UTM tracking in a few predictable ways:
- The router strips the query string. If the app redirects
/to/homeor cleans the URL on load, read the parameters before the router runs, or store them on the very first page view. - Hash routing. With
/#/landing?utm_source=x, the parameters live inside the hash. Your tracking has to look there too. - Route changes are not page loads. Navigating inside the app does not reload the page, so anything that reads UTMs only on load must keep them in storage for later events.
- Auth redirects. A sign in flow that bounces through another domain and back often loses the original parameters. Store them before the redirect.
- Server rendering. If UTMs are read on the server, make sure the client still receives them, or the conversion fired in the browser will arrive without a source.
Privacy notes
- Never put personal data in UTMs. No emails, names, phone numbers or user IDs. URLs are saved in browser history, server logs, referrer headers and third party analytics, and they get shared in screenshots.
- UTMs describe campaigns, not people.
utm_campaign=october_updateis fine.utm_content=jane_at_example_comis not. - Tell visitors what you store. If you keep attribution in local storage or cookies, say so in your privacy policy and follow the consent rules that apply to you.
How Orwel tracks UTMs
Orwel does steps 2 to 4 for you. The SDK reads UTMs and click IDs from the query string, then from a query inside the hash (for hash routers), then from the referrer. It captures utm_source, utm_medium, utm_campaign, utm_term, utm_content and utm_id, and from SDK 1.7.0 the click IDs gclid, gbraid, wbraid, fbclid, ttclid, msclkid and li_fat_id.
Within a session they live in sessionStorage, so they survive client side navigation and redirects. Across sessions the SDK keeps an attribution object in localStorage with a first touch written once and a last touch overwritten on every new tagged visit. Every conversion carries that object, so you do not read the URL or pass UTMs yourself:
import { orwel } from 'orwel';
orwel.init({ apiKey: 'orwel_pk_...' });
// On form submit: attribution is attached automatically
await orwel.conversion('lead', { email });
The lead source comes from an explicit source on the conversion if you set one, otherwise from the last touch, then the first touch, then the session. With Google Ads connected, the gclid is matched with synced clicks to find the campaign. With Meta Ads connected, the match uses utm_id, with utm_campaign as a fallback. The Campaigns screen then shows cost per lead, cost per qualified lead and spend that produced no qualified leads.
To check a setup, open your site with ?utm_source=test&utm_campaign=check using a development key, submit a conversion and look at the request in Dev mode. A conversion with no UTMs or click IDs gets a missing_attribution warning and the lead counts as direct.
See marketing attribution in Orwel for the product side, the install guides to add the SDK, and the CAC calculator to turn the numbers into acquisition cost.