Tackling GA4 Reporting: A Guide for Accurate Traffic Attribution and Campaign Data Analysis

There’s a lot of work to be done once you’ve migrated to GA4 if you want accurate and relevant reports.

GA4’s standard reports and explorations are limited. For example, you cannot create an Exploration that shows – accurately – conversion rate, sessions, bounce rate, and pages/ session by campaign if you wish to include only some conversions. For this sort of advanced report (which was a Universal Analytics/ GA3 staple), you most likely want to tap into BigQuery.

Accuracy is a big issue with GA4 – especially for B2B companies – because traffic volumes are generally low enough to be affected by Thresholding. GA4 reports also make extensive use of Estimations over Counts, which can further complicate things. Thus, once again, you need BigQuery.

Underlying technical issues with Google Ads Reports and GA4

GA4 works in mysterious ways – it has 3 Traffic Attribution dimension levels (with just one level referred to as ‘Attribution’), several Attribution Models for Conversions (none of which actually exported to BigQuery), and a massive issue: it fails to mark Google Paid Search traffic as what it is. Instead, your traffic will show up as Organic Traffic but retain its gclid marker.

This has been a long-standing problem, and all solutions I could find online seem to rely strictly on clever SQL sorcery that boils down to “look for the presence of gclid values, change source/medium values to google/cpc.” This is helpful, but not if you run a large number of campaigns with different naming conventions, different objectives, and potentially across different countries/ regions. So you most definitely need your Campaign and Content dimensions in those reports.

However, GA4’s BigQuery export does not contain Campaign & Content values for Auto-Tagged accounts. You need Auto-Tagging enabled if you wish to import Conversion definitions from GA4 into Google Ads. “Ok, I’ll just add UTM parameters to my campaigns then,” you think to yourself – but doing so will break attribution within GA4’s UI if you also keep Auto-Tagging enabled. GA4 does not offer the possibility of ignoring UTM parameters like GA3 did.

How do we fix this?

This is a classic situation of wanting to have your cake and eat it. Thankfully, there is a workaround – don’t use UTMs. And while it would be amazing if it were that simple, the actual implementation is a bit more involved and requires work to be done in 3 places:

  1. Google Tag Manager (or gtag if you hate yourself – I don’t)
  2. Google Ads
  3. BigQuery

GTM fixes

This is probably the most straightforward of the bunch. Assuming you’re only interested in Source, Medium, and Campaign, you have 2 things to do:

A. Define 6 variables

  • utm_campaign, utm_source, utm_medium
  • custom_campaign, custom_source, custom_medium

B. Update your GA4 Configuration tag to pass your custom_* parameters to GA4

You want to make this setup backward compatible, so you can still use UTM tags in other channels if you already have them set up. The configuration for the UTM variables is simple:


Your custom_* variables, though, have to be configured to explicitly inherit UTM values:


You can then add these variables as values for the custom_* event parameters included in the Configuration tag:


By adding them at a Config level, you ensure that all events are marked with your campaign data, and that makes BigQuery reporting slightly easier.

There are pros and cons to sending page_view events with the config tag, as well as setting these parameters on all events, but they’re beyond the scope of this already long article.

Google Ads fixes

With GTM improvements in place and published, it’s time to move on to Google Ads.

Head over to Tools > Scripts


And add & run the following script:


Code here:

function main() {
  let adCampaignIterator = AdsApp.campaigns().withCondition("CampaignStatus = ENABLED").get();

	while (adCampaignIterator.hasNext()) {
	let adCampaign = adCampaignIterator.next();
	let cn=adCampaign.getName().replace(/\s/g,'_');
	adCampaign.urls().setCustomParameters({campaign: cn});

What this will do is go through all your enabled campaigns and, at a campaign level, populate the {_campaign} variable with the campaign name value. This is because Google Ads does not include campaign names in its ValueTrack variables.

Then, head over to each campaign (fun, I know) and update your URL options as shown below:


You can now publish your Google Ads changes as well.

BigQuery fixes

Getting accurate Attribution reports from BigQuery involves a lot of SQL work and smart pipelining (either through scheduled queries or Google Cloud Functions). But with the 2 fixes above, BigQuery data should now be correctly identified. The only thing you need to do is make sure to include your custom event parameters in queries as the default campaign/source/medium field, with the standard one (campaign/source/medium) acting as a fallback.

Here’s a quick example that extracts all events that occurred on the 1st of Jan, 2023, their name, timestamp, and source/medium/campaign values:


Code here:

cast(event_date as date format 'YYYYMMDD') as date,
ifnull((select value.string_value from unnest(event_params) where key = custom_source'),
          (select value.string_value from unnest(event_params) where key = 'source')) as source,
ifnull((select value.string_value from unnest(event_params) where key = 'custom_medium'),
    (select value.string_value from unnest(event_params) where key = 'medium')) as medium,
ifnull((select value.string_value from unnest(event_params) where key = 'custom_campaign'),
    (select value.string_value from unnest(event_params) where key = 'campaign')) as campaign


And that was it! If you have any questions, feel free to reach out to me or Matt Garisch from The Croc.

About The Author


Adrian Grigore
Lead Data Analyst @ The Croc

I can hack my way toward a solution no matter the problem (as long as it has to do with data analysis, marketing, or product development). I caveat almost everything I recommend (unless it’s a dish).
My jokes are like a dog’s breath after it’s had expired ice cream – bad, but sometimes surprisingly tolerable.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top