본문으로 건너뛰기
Back to blog
Digital Marketing Analytics

You Can Write a UTM Convention, but GA4 Never Reads Your Table

Because UTM values are free text: the table you wrote is read only by you, while GA4 reads the same string by its own rules. Google's published default channel group logic checks source against a list of 819 sites and checks medium against fixed regular expressions, so for one identical link your table says referral and GA4 counts it as Organic Search. The convention document is only half the job. The other half is pulling out the violations with a query on a schedule.

30 min readDigital Marketing Analytics
UTM conventionutm_mediumGA4 default channel groupBigQueryGA4 BigQuery Exportchannel groupingValueTrackdynamic URL parameterstracking links
You Can Write a UTM Convention, but GA4 Never Reads Your Table

For: marketers and analysts who maintain channel reports themselves  ·  Format: explanatory guide  ·  2026-08-06

Key Summary · TL;DR

Q. We agreed on a UTM convention, so why do the channel reports keep splitting apart?

Because UTM values are free text: the table you wrote is read only by you, while GA4 reads the same string by its own rules. Google's published default channel group logic checks source against a list of 819 sites and checks medium against fixed regular expressions, so for one identical link your table says referral and GA4 counts it as Organic Search. The convention document is only half the job. The other half is pulling out the violations with a query on a schedule.

Three lines you can use today

  • GA4 never looks at the channel_group column you invented, only at its own list and regex
  • Dynamic parameters filled in by ad platforms do not follow your convention
  • A convention is held together by a recurring audit query, not by a document

Six months later, the channels have split

UTM tagging is easiest the day you start. You hang a few labels on the end of a link and you are done, and the five slots covered in the beginner's guide are all there is to it.

The trouble arrives half a year later. You open the channel report and one channel is spread across several rows, and nobody can say which row is the real one. A value with different capitalisation, a value with the domain attached, a value a new team member invented, all sitting side by side.

So you build a convention table. Source and medium fixed per platform, a naming rule for campaigns, even a reporting-level category as its own column. Most teams get this far. Then you reopen the report six months on and it is still split.

This article answers three things. Why it splits even with a table. How to write that table so it stops fighting the tool. And how to catch violations with a query instead of a person.

Two parties read the same line differently

The root of the splitting is plain. A UTM value carries no meaning of its own. It is a string, and whoever reads it attaches meaning.

There are at least two readers. One is GA4's default channel group. The other is the mapping table you built. Both look at the same string and sort it by different criteria.

ONE STRING, TWO READERS utm_source=naver&utm_medium=blog GA4 default channel group Checks source against Google's site list Checks medium against fixed regex naver = search site Result: Organic Search Our mapping table Checks medium against our own list Adds a reporting category column blog = our channel Result: referral Two reports carry the same channel names with different numbers. That is the hardest kind of mismatch to notice.

Adding one more column to your table does not make GA4 read that column.

The split is dangerous because both sides use overlapping channel names. Your table has social and referral and paid_search, and GA4 has Organic Social and Referral and Paid Search. Same names, different membership.

So two reports land on the meeting table, both labelled "social", with different numbers. Nobody notices that the definitions differ, and the argument turns into which one is wrong.

Run our table through Google's rules and it splits like this

Google publishes the judging rules for default channel groups. In short, they use two kinds of material.

  • A site list: whether the source value appears on a list Google maintains. It is sorted into search, social, video and shopping, and it is downloadable. Counting the entries gives 819.
  • Medium regex: the paid family is ^(.*cp.*|ppc|retargeting|paid.*)$, display is a fixed list like display, banner and cpm, and referral is only referral, app and link.

Here is the decisive part. The condition for Organic Search is "source matches the search site list OR medium exactly matches organic". Either one on its own is enough. And naver sits on that list as a search site.

So a convention that standardises utm_source on naver drops everything into Organic Search the moment the medium fails to match the paid regex. It makes no difference whether you wrote blog or cafe.

The label we assignedsource / mediumOur channel_groupGA4 default channel group
Official blognaver / blogreferralOrganic Search
Agency ranking placementnaver / blog-topreferralOrganic Search
Brand search adnaver / bspaid_searchOrganic Search
Paid Instagram contentinstagram / snspaid_socialOrganic Social
Meta ad (Facebook·Instagram placement)fb / da, ig / dapaid_socialOrganic Social
Meta ad (other placements)an / da, msg / dapaid_socialUnassigned
Messenger channel broadcastkakao / messagesocialOrganic Social
Official videoyoutube / videovideoOrganic Video

This table applies the published rules to our values. It is not a set of readings taken from a live property.

Three rows are worth pausing on. A brand search ad gets counted as organic. Write bs as the medium and it never matches the paid regex, while the source naver satisfies the search site condition on its own. Money you spent shows up in the report as free traffic.

Paid Instagram content also turns into organic social. The Paid Social condition asks for a social site and a medium matching the paid regex together, and sns does not pass that regex.

The last row is the nasty one. A single Meta campaign splits into Organic Social and Unassigned depending on placement. Why that happens comes up shortly, in the section on dynamic parameters.

Why these are stated flatly rather than hedged. Every row above was chosen because exactly one rule matches the combination. naver / cpc, for instance, satisfies both Paid Search and Organic Search, so the answer depends on the order in which rules are evaluated, and Google's documentation does not state that order. It was left out. If your own values fall into that group, checking a live property is the faster route.

So the convention gets written in two layers

By now it is clear why a convention table needs a separate column like channel_group. The values that go into the five UTM slots and the categories you use in reporting live on completely different layers.

Separate what you parse from what you map

One criterion divides them. Is the set of values finite and decided by a person, or does it keep growing?

Medium is finite. A dozen or so values cover it, and a person approves anything new. So this side gets mapped. A value outside the list becomes an incident signal by itself.

Campaign names, on the other hand, keep growing. New ones every month, different ones per product, impossible to hold in a list. So this side gets parsed. Structure is embedded in the value and split back out later.

DO NOT MIX THE TWO SEPARATORS utm_campaign, parsed official _ content _ 202608 owner purpose period Underscore marks structure. Not three tokens means failure. utm_content, word joiner signup-post-01 Hyphens only join words. An underscore here breaks the parsing. utm_medium, mapped blog-top referral A value outside the list is an incident signal by itself.

Give the underscore and the hyphen different jobs and the query that splits them later stops breaking.

Splitting the separators into two roles looks trivial and pays off. The underscore is a structural separator inside utm_campaign only, and everywhere else hyphens join words. Then SPLIT(campaign, '_') always returns three pieces and the parsing holds.

Keep non-Latin text out of the values

Put Korean or any non-Latin text into a UTM value and the URL turns it into percent encoding. The report shows an unreadable string, and depending on which stage decodes it, the same value can appear in two shapes.

So the values stay lowercase Latin, and the human-readable name lives in a separate mapping table. What product signup refers to is a lookup, and the report joins the table to display it properly.

A finished convention ends up looking roughly like this. Platforms are generalised to types, and the brackets are slots to fill.

Platform typeutm_sourceutm_mediumutm_campaignchannel_group
Official blognaverblogofficial_content_[yyyymm]referral
Official communitynavercafeofficial_community_[yyyymm]referral
Agency placementnaverblog-topagency_top_[yyyymm]referral
Influencer partnershipnaverblog-influencer[creator]_promo_[yyyymm]affiliate
Search adsnavercpc[product]_lead_[yyyymm]paid_search
Messenger broadcastkakaomessageofficial_push_[yyyymm]social
Creator PPL videoyoutubeppl[creator]_ppl_[yyyymm]video
Meta ads{{site_source_name}}da{{campaign.name}}paid_social
Performance affiliate[partner-code]cpa[partner-code]_cpa_[yyyymm]affiliate

utm_content is left out of the table because one line covers the rule. Write an identifier that points to exactly one creative or post, joined with hyphens. For a post, blog-[post-id]; for an ad, the ad ID. That value becomes the key for tracing a single link later.

Values the platform fills in do not know your rules

Everything so far concerned links you write by hand. Ad platforms work differently. The platform fills the value, not you.

Meta offers placeholders in double braces. Put {{campaign.name}} or {{ad.name}} in the link and it is replaced with the real name at click time. Google Ads has ValueTrack parameters like {keyword} and {creative}.

Convenient, and there are three traps.

Trap 1, the name freezes at first delivery

Meta's own documentation spells it out. Name-based parameters are set to the campaign, ad set and ad names used when the ad first delivers, and renaming afterwards leaves the parameter pointing at the original name.

So fixing a campaign name to match the convention later still ships the old name in the UTM. Changing the value means creating and publishing a new campaign. Which makes the moment of naming the only chance to honour the convention on Meta.

The example attached to that same document shows the problem directly. A campaign named Prospecting 2026 arrives in the URL as utm_campaign=Prospecting%202026.

The space becomes %20 and the capitals stay. The three-token underscore rule is already broken here.

Trap 2, placement splits source five ways

{{site_source_name}} returns five values. Audience Network is an, Facebook is fb, Instagram is ig, Messenger is msg, Threads is th.

Google's site list carries only fb and ig as social. an, msg and th are absent. That is why one Meta row in the earlier table came out as two channels.

ONE CAMPAIGN, TWO DESTINATIONS {{site_source_name}} Placement decides the value fb · ig On Google's social list Organic Social an · msg · th Not on the list Unassigned Switch medium to a value that matches the paid regex and both rows land in paid channels.

One campaign on one budget scatters across two channels in the report.

The fix is not to touch source but to change medium to a value that matches the paid regex. Use cpc or paid-social instead of da, anything hitting .*cp.* or paid.*, and even a source missing from the list gathers into the paid family.

Trap 3, auto-tagging beats your UTM

Google Ads adds one more trap. The Analytics documentation puts it this way. If you use manual tagging and auto tagging together, then the source, medium, and other traffic-classification dimensions use the auto-tagged values.

So however carefully you tag the link, when a gclid rides along the channel classification follows the click ID rather than the UTM. In the reverse case, where the click ID cannot be used as intended, the same document states that a single UTM parameter on the URL makes all values come from UTM.

The sentence after that is the one that matters in practice. Google recommends setting every relevant parameter once you set any of them, because missing parameters end up as (not set) in reporting. A half-tagged link is the worst kind.

ValueTrack has a comparable hole. {keyword} returns the matched keyword on the Search Network, but returns a blank value on campaigns that match without keywords, such as Dynamic Search Ads and Performance Max.

The convention table says utm_term={keyword}, and that slot ships empty. The value is not wrong so much as absent, which means the report simply loses it.

Off-page links cannot be told apart by UTM alone

Flat vector illustration of one orange link token copied identically across many separate navy floating platforms, with the dotted lines that once tied each copy to its platform broken and fading in mid air

Image: The moment a link spreads, the label stops remembering where it started.

This is where the tracking links from the previous article come back. Links planted outside your own site cannot be managed with UTM alone.

Two reasons. The first is that UTM is only read at the destination. A click on an off-page link happens before arrival, and counting it means routing through a redirect domain first. That was the reason for a subdomain like go. in the previous article.

The second is that the same link gets copied and spreads. A link posted to a community is screenshotted, quoted, moved to another room. The UTM follows along unchanged the whole way, so the report still credits the spot where it was first planted.

Redirect links therefore need a naming rule of their own, separate from UTM. One principle is enough.

Three rules for naming redirect links
  1. Issue link IDs per placement, not per asset: the same content planted in two places is two links. One link per placement, not one link per piece of content.
  2. Put only a platform type and a serial in the path: something like go.example.com/c/blog-014. A partner name in the URL is exposed publicly, so keep a code and resolve it in a table.
  3. Copy the same value into utm_content: the link ID in the redirect server log and utm_content in GA4 have to be the same value before clicks and arrivals can sit on one row.

The third is the crux. Two systems keyed differently cannot be joined, and then the gap between "the link was clicked" and "no session arrived" stays unexplained forever.

Catching violations with BigQuery

Convention documents do not hold. People move on, an urgent campaign cuts in, and a new value arrives without fail. Rather than growing the document, keep a query that surfaces the violations on a schedule.

If GA4 is exporting to BigQuery the material is already there. One trap has to be cleared first, though.

Three fields look like the UTM

The GA4 export schema carries traffic source in three separate places, and all three have similar names. Pick the wrong one and the query runs fine while the answer is wrong.

THREE FIELDS WITH SIMILAR NAMES traffic_source The value that first acquired the user. It never changes later, so counting this month goes wrong. First acquisition collected_traffic_source.manual_* The raw UTM actually collected with the event. Audit the convention here. Audit this one session_traffic_source_last_click Session level last click, with per-platform campaign detail. Use it for performance. Performance

Auditing the convention means the middle field. The top one counts new users only.

The four queries below are written to show the shape, not lifted from a run. Dataset names and date ranges have to be swapped for your own.

Query 1, find which event carries the values

Which event the UTM attaches to varies by property. Counting beats guessing.

-- Illustrative query. It was never run and carries no result figures.
-- Check first which events carry a collected UTM.
SELECT
  event_name,
  COUNT(*) AS events,
  COUNTIF(collected_traffic_source.manual_source IS NOT NULL) AS with_utm
FROM `project.analytics_XXXXXXXXX.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260701' AND '20260731'
GROUP BY event_name
ORDER BY with_utm DESC
LIMIT 20;

They usually cluster on session_start and the first page_view. To count by session, narrow event_name in the queries below to that value; count across all events instead and one session gets picked up repeatedly, which distorts the ratios.

Query 2, gather the notation variants

The most common form of splitting is the same meaning written differently. Flatten to lowercase and it surfaces immediately.

-- Illustrative query. No result figures.
-- More than one raw value under a lowercase key means a notation variant.
WITH s AS (
  SELECT
    LOWER(TRIM(collected_traffic_source.manual_source)) AS norm_source,
    collected_traffic_source.manual_source AS raw_source
  FROM `project.analytics_XXXXXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260701' AND '20260731'
    AND event_name = 'session_start'
    AND collected_traffic_source.manual_source IS NOT NULL
)
SELECT
  norm_source,
  COUNT(DISTINCT raw_source) AS variants,
  ARRAY_AGG(DISTINCT raw_source ORDER BY raw_source LIMIT 10) AS samples
FROM s
GROUP BY norm_source
HAVING variants > 1
ORDER BY variants DESC;

If this query returns Naver and naver.com bundled under one naver, that is the splitting. What splits there stops at the report rows, though: naver and naver.com both sit on the list as search sites, so the channel group lands in the same place.

The variant that moves the channel group is a different one. The list carries naver as search and blog.naver.com as social, each on its own line. Same blog traffic, and whether the source was written as a domain decides between Organic Search and Organic Social. One notation choice moving the whole channel, not just splitting a row.

Query 3, catch off-list values and broken structure together

Medium, the mapped side, gets compared against the allowed list; campaign, the parsed side, gets checked for token count and forbidden characters. Easier to see them together.

-- Illustrative query. No result figures.
-- Off-list mediums plus broken campaign structure in one pass.
WITH allowed AS (
  SELECT medium FROM UNNEST([
    'blog','cafe','kin','blog-top','kin-top','cafe-affiliate',
    'blog-influencer','message','video','ppl','cpc','bs','da','sns','cpa','display'
  ]) AS medium
),
s AS (
  SELECT
    collected_traffic_source.manual_medium        AS medium,
    collected_traffic_source.manual_campaign_name AS campaign
  FROM `project.analytics_XXXXXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260701' AND '20260731'
    AND event_name = 'session_start'
)
SELECT
  medium,
  campaign,
  COUNT(*) AS sessions,
  medium IS NULL OR medium NOT IN (SELECT medium FROM allowed) AS unknown_medium,
  ARRAY_LENGTH(SPLIT(IFNULL(campaign, ''), '_')) <> 3        AS bad_token_count,
  REGEXP_CONTAINS(IFNULL(campaign, ''), r'[^a-z0-9_\-]')     AS bad_charset
FROM s
GROUP BY medium, campaign
HAVING unknown_medium OR bad_token_count OR bad_charset
ORDER BY sessions DESC;

The bad_charset column catches the Meta trap from earlier exactly as described. Campaign names carrying capitals and %20 land right here. So does any percent-encoded non-Latin text.

Query 4, count where our labels and the tool disagree

The last one turns this article's subject into SQL. Attach the mapping table, then check whether the traffic we call paid is also paid by GA4's standard.

-- Illustrative query. No result figures.
-- Count combinations where our channel_group and GA4's paid rule disagree.
WITH mapping AS (
  SELECT * FROM UNNEST([
    STRUCT('blog' AS medium, 'referral' AS channel_group),
    STRUCT('blog-top', 'referral'),
    STRUCT('bs',       'paid_search'),
    STRUCT('cpc',      'paid_search'),
    STRUCT('da',       'paid_social'),
    STRUCT('sns',      'paid_social'),
    STRUCT('cpa',      'affiliate')
  ])
),
s AS (
  SELECT
    collected_traffic_source.manual_source AS source,
    collected_traffic_source.manual_medium AS medium,
    COUNT(*) AS sessions
  FROM `project.analytics_XXXXXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260701' AND '20260731'
    AND event_name = 'session_start'
  GROUP BY source, medium
)
SELECT
  s.source,
  s.medium,
  m.channel_group                                   AS our_label,
  REGEXP_CONTAINS(IFNULL(s.medium, ''),
    r'^(.*cp.*|ppc|retargeting|paid.*)$')            AS ga4_paid_rule_hit,
  s.sessions
FROM s
LEFT JOIN mapping m USING (medium)
WHERE STARTS_WITH(IFNULL(m.channel_group, ''), 'paid')
  AND NOT REGEXP_CONTAINS(IFNULL(s.medium, ''),
        r'^(.*cp.*|ppc|retargeting|paid.*)$')
ORDER BY s.sessions DESC;

Any rows here mean traffic we call paid that GA4 does not count as paid. bs, da and sns are the ones that show up. Channels you spent money on, sitting in the report as organic.

Two ways to fix it. Change medium to a value that matches the paid regex, or build a custom channel group in GA4 and move our mapping inside the tool.

The first option changes data from today forward and leaves the past alone. Whichever you choose, marking the switchover date in the report has to travel with it.

These four are better scheduled than run by hand once a quarter. Wire an alert for any result that is not zero rows and the convention stops being a document and becomes a mechanism.

Honestly, this is where the limits are

Separating what was verified from what was not.

  • Verified against official documentation: the default channel group conditions and regex, the contents of the site list, the descriptions of the three export schema fields, the five values of Meta's dynamic parameter and its name-freezing behaviour, the blank-value condition in ValueTrack, and the auto-tagging precedence rule. The inline links and the sources below are all primary.
  • Inferred by applying the rules: the GA4 channel outcomes in the comparison table. Those are not readings taken from a live property; they are the published conditions with our values substituted in. Only combinations matching exactly one rule were used, and an inference is still an inference.
  • Not executed: the four BigQuery queries are written to show the shape. They have never been run against any dataset, which is why no result figures appear anywhere in this article. Syntax and field names follow the schema documentation, but each one deserves a run in your own environment.
  • Subject to change: Google updates the site list, and the channel rules get revised. Platform parameter specifications move too. Everything above reflects checks made in August 2026.

One thing a convention cannot fix by design. The person building the link never opens the table. An audit query only reports violations after the fact, so putting the rules into the link-building screen itself works far better. Keep one generator and ban hand-assembled links.

Thirty minutes today tells you where your labels stand

No need to build a new table. Start by looking at the values arriving right now.

Five steps to audit a UTM convention
  1. Count the variants first: flatten last month's source and medium to lowercase and group them. Any group holding more than one raw value shows how far the splitting has gone.
  2. Check the list: see whether the sources you use appear on Google's site list, and under which category. Anything on the list gets pulled into that category regardless of medium.
  3. Compare the paid side: test every medium you call paid against ^(.*cp.*|ppc|retargeting|paid.*)$. The ones that fail are where ad spend leaks into organic.
  4. Open the slots the platform fills: check whether Meta campaign names carry spaces and capitals, and whether {keyword} arrives empty on Performance Max. Names cannot be changed after first delivery.
  5. Put the query on a schedule: run the audit weekly and alert on anything that is not zero rows. Documents get forgotten; alerts do not.

If you take away one thing, take this.

A convention is not about deciding the values. It is about deciding who reads them and how.

This is an advanced entry in the Digital Marketing Analytics series. The structure for measuring links outside your own site is covered in Off-Page Measurement, Tracking Links and MMPs, and if UTM itself is new, start with UTM Parameters, a Beginner's Guide. Naming systems for on-site events rather than links continue in Designing an Event Taxonomy, and the whole map lives in The Five-Layer Measurement Audit.

Sources

The BigQuery queries in this article are illustrative, were never executed, and contain no result figures. The GA4 channel outcomes are inferences drawn by substituting values into published rules, not readings from a live property. Site lists, channel rules and platform parameter specifications are all subject to revision, so the above reflects checks made in August 2026. Platforms and partners in the convention examples have been generalised to types.

Found this useful? Share it

Latest posts

Related projects

Get new posts by email

Insights on marketing, analytics, and dev, delivered to your inbox.