In short: Shopping and PMax reports show the query but hide the product. Add one URL parameter, {lpurl}?utm_content={product_id}, and within a week GA4 hands you the retired query × product report by joining sessionManualAdContent with sessionGoogleAdsQuery. On a real account that view exposed 85 % of spend flowing through pairs that never converted. The 0.25 % of pairs that did convert ran at 46.8× ROAS and brought in all of the revenue.
Just want the distilled method?
Download the complete AI instructions — one file you paste into Claude or any capable coding agent, and it pulls the query × product report out of your own account. Leave your email and the file is yours — or keep reading below.
One file, one list. I only write when there's something worth reading.
A client once asked me a question that should have a one-click answer. Someone searched “cordless vacuum under 200”, clicked the Shopping ad, and the account paid for it. Which of the 5,000 products in the feed did that click land on? I opened the search terms report, then the product report, and there was no way to connect them. There still isn’t. Google shows you the query, and it shows you the product, but never together.
Years ago the old AdWords API would hand you exactly this pairing. Then it quietly disappeared, and no setting, no report, no API field brings it back. For an online shop that gap is not cosmetic. The link between query and product is where the real optimization lives: it tells you whether your titles match real demand, which products attract garbage traffic, and why a product gets clicks but never converts.
Twelve years of building on the Google Ads API taught me that when Google retires a report, it doesn’t come back. So I rebuilt it myself, and the rebuild is almost embarrassingly simple. One URL parameter. Within a week, GA4 hands you every paid click paired as query × product, with sessions, conversions and revenue attached. The build takes minutes; the rest of this article is what you do with what comes back.
The fix: one parameter, two GA4 dimensions
Add a tracking template
Set {lpurl}?utm_content={product_id} on your Shopping and PMax campaigns. The ValueTrack variable {product_id} sends the Merchant Center product ID with every click.
GA4 stores the product
The UTM lands in the Session manual ad content dimension (sessionManualAdContent).
GA4 already knows the query
Auto-tagging (gclid) fills sessionGoogleAdsQuery on the same session.
Join the two dimensions
Every paid click becomes a query × product pair, with sessions, conversion rate and revenue attached.
Auto-tagging and the manual UTM don’t fight each other. The gclid keeps handling source, medium and campaign; your UTM only carries the product ID.
Know what you’re getting (and what you’re not)
- Clicked queries only. This dataset starts at the click. Queries where your ad showed but nobody clicked never reach GA4. Impression-level analysis stays in Google’s standard report, without products.
- About 20 % of clicks won’t pair up. The gaps are consent-rejected sessions, PMax surfaces with no query at all (Display, YouTube, Gmail), and clicks that never fired analytics.
- One product per session. The session carries the ID of the product the user clicked, even if they then browse ten others.
Get the complete AI instructions for this method
The whole method, rewritten as a brief you can paste straight into Claude or any capable coding agent. It sets the tracking template, pulls the query × product report out of GA4 and runs the aggregations on your account. The template and its three traps are in there too. Leave your email and the file is yours.
One file, one list. I only write when there's something worth reading.
Pulling the report out of GA4
In the GA4 interface, open Explore and build a free-form table. Dimensions are Session manual ad content and Session Google Ads query. Metrics are sessions, key events (formerly conversions) and purchase revenue. Filter on session source/medium equal to google / cpc.
For anything serious you’ll want the Data API, because this report belongs in a dashboard or a BigQuery join, not in a browser tab:
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
RunReportRequest, DateRange, Dimension, Metric, FilterExpression, Filter
)
request = RunReportRequest(
property=f"properties/{GA4_PROPERTY_ID}",
dimensions=[
Dimension(name="sessionManualAdContent"), # product ID
Dimension(name="sessionGoogleAdsQuery"), # search term
],
metrics=[
Metric(name="sessions"),
Metric(name="keyEvents"),
Metric(name="purchaseRevenue"),
],
date_ranges=[DateRange(start_date="30daysAgo", end_date="today")],
dimension_filter=FilterExpression(filter=Filter(
field_name="sessionSourceMedium",
string_filter=Filter.StringFilter(value="google / cpc"),
)),
)
Join sessionManualAdContent to your product feed (id → title, price, category) and the report is complete. Query, product, title, conversions and revenue in one table.
Verified, not theorized
We deployed this on the live account of a Czech electronics retailer running 22 enabled Shopping campaigns. Seven days later I checked GA4:
GA4, 7 days after deployment
- Rows of google/cpc traffic 9,753
- Carried the product ID (utm_content) 96 %
- Carried the search query (sessionGoogleAdsQuery) 81 %
- Carried both, a working query × product report 78 %
Now the fun part: what the pairs tell you
1 · The title gap
Put the queries next to the title and description they triggered. The mismatch jumps out:
The feed roadmap writes itself. I’ve spent the past two years building an AI feed-enrichment tool, and if I’m honest, the weakness of the whole category is proving impact. This report fixes that. Rewrite the title to match the demand you’ve just proved, then watch CTR and conversion rate move per query. AI feed enrichment stops being something you take on trust and becomes a measurable loop.
2 · Who else is on “your” query, and with what product
One thing you can’t do is add negative keywords per product. Google gives you no such lever in Shopping or PMax. So when a query × product pair underperforms, asking how to exclude it leads nowhere. The productive question is why it underperforms.
Take the query and scrape the live results page for it. I use the serp/google/organic/live/advanced endpoint at DataForSEO. One POST with the query text and the location_code of your market returns the whole results page as structured JSON: paid ads, shopping blocks with merchant names and prices, and organic results below. DataForSEO bills per SERP, and one SERP covers up to 10 results. At the current base price of about $0.002 per SERP, checking 200 default-depth queries costs about $0.40, not counting any paid advanced parameters.
A typical finding. Your mid-range sleeping bag collects clicks on a generic query, and the same query shows three budget brands in the same category at half your price. The product is fine. The price loses that specific auction.
Now you have real options. Reprice. Push the differentiator into the title (“down fill, −15 °C comfort”). Move the product into a campaign with bidding that matches its margin reality. Or accept the query as upper-funnel and judge it on assisted metrics instead of last click.
3 · Structure and bidding decisions
Products attracting high-intent queries deserve their own asset groups and budgets. Products collecting only generic traffic belong in catch-all groups with conservative targets. This report gives shopping segmentation an evidence base instead of instinct.
4 · A health check for PMax
PMax tells you almost nothing about search. This report is the closest thing you’ll get to an audit of what PMax actually buys for you on the search surface, per product.
Watch me walk one real export, step by step
Everything above is the why. What follows is the how: what you download, what you look at, and the real number that comes back.
The data in this section comes from a second account, a mid-size Czech online shop in the bargain segment. The shop is not the electronics retailer from the GA4 box above. I picked it because its catalogue is broad enough that every pattern shows up at full scale. I pulled its raw Shopping search-terms report through the Google Ads API into a local SQLite table and ran the aggregations below. One caveat first. The raw report gives you the ad group or product group the query was served under, not the individual product. That is precisely the gap the UTM trick closes. But even at product-group level, the numbers below are clear enough to show where the spend actually goes.
Source for every number in this section: Google Ads Shopping search-terms report, one mid-size Czech online-shop account, ~22.6M lines, data pulled April 2026. Amounts converted from CZK using a rate of ~25 CZK/EUR.
Step 1 · Pull the raw report and measure the pile
Export the Shopping search-terms report via the Google Ads API (search_term_view) into anything you can run GROUP BY on, such as SQLite or BigQuery. Before you join or filter a single thing, run a plain COUNT(*) and a couple of SUMs. You need to feel how big and how noisy the raw pile is, because that one fact resets every expectation that follows:
The raw pile, one COUNT and three SUMs
- Report lines (query × product group) 22,640,716
- Distinct search terms 5,370,131
- Product groups they were served under 10,393
- Spend / revenue (blended ROAS 6.8×) €103k / €697k
22.6 million lines, 5.4 million unique queries. No human reads that. Those two counts have exactly one job: to kill the idea of per-row manual triage. The only way forward is to collapse the pile by the dimension that pays the bills.
Step 2 · Throw away 96 % of it before you analyse anything
Count the lines with zero clicks, then filter them out. The Shopping search-terms report logs every query your ad showed for, and most of those queries nobody ever clicked. Impression-only lines can’t cost you and can’t convert. They’re noise that makes the table look scary.
WHERE clicks > 0 collapses the table to 738,444 lines, a size you can actually work with.
The impression flood
- Lines with zero clicks (pure impressions) 21,902,272 (96.7 %)
- Lines that ever cost money 738,444 (3.3 %)
96.7 % of the scary number was never anything but noise. Filter to clicked lines before you analyse anything, and every aggregation below runs on the part that actually spends money.
Step 3 · Ask the one question that changes how you see the account
Sort the clicked lines into two buckets: “converted at least once” and “never converted.” Then total the cost of each. That split is what turns “the account is fine, ROAS is 6.8” into “most of the budget does nothing.” Compute it before any optimisation idea:
The zero-conversion drag
- Report lines that converted zero times 99.75 %
- Share of total spend those lines ate 85.5 %
- Spend with no conversion behind it €88k
- Of clicked lines only, share with zero conversions 92.3 %
85 % of the budget flowed through query × product-group combinations that never once converted.
That isn’t a rounding error you optimise later. It is the biggest problem in the account. And you could only see it because you collapsed every query down to the product group it was served under.
Step 4 · Check whether the waste is a few villains or the whole crowd
Sort the clicked lines by cost, take the top 1 % and top 10 %, and measure their share of total spend. This decides your tactic. If a handful of terms burn the budget, you pause them and you’re done. If the waste is spread thin, pausing terms is pointless and you need structural fixes:
Where the wasted spend actually sits
- Top 1 % of clicked lines by cost 10.5 % of spend
- Top 10 % of clicked lines by cost 29.8 % of spend
The waste is spread thin. The top 1 % of costly lines make up barely a tenth of spend, so pausing 20 bad terms changes nothing. Instead of chasing individual queries, fix the structure: decide which products sit in which campaign at which target. And remember that Google won’t even let you add a negative per product anyway.
Step 5 · Find out why the long tail leaks: one query, many products
For each search term, count how many distinct product groups it was served under. This explains the waste mechanically. Shopping matches a query to your whole feed’s signals, not to one product’s relevance. A single query leaks across unrelated corners of your catalogue, and you pay for every miss.
A concrete case from the account. The query “anti-bark device” kept burning budget, so I ran COUNT(DISTINCT ad_group) on it. It had been served under 139 different ad and product groups, for €39 and roughly zero conversions. The query “lego technic” was served under 300 different ad and product groups.
Query spray across the catalogue
- Distinct search terms served under more than one product group 46.7 % of 5,370,131
- Most product groups a single query reached 6,661
- "anti-bark device" → groups / cost / conversions 139 / €39 / ~0
Nearly half the queries are smeared across multiple product groups, and the worst offenders reach thousands. That is where the zero-conversion drag from step 3 comes from. It is also the exact thing you can finally see once every click carries its product ID.
Step 6 · Eyeball the worst mismatches, because they’re absurd
Pull the highest-cost lines that never converted and read the query next to the product group it was served under. The aggregate numbers size the problem. These three rows are the ones you put in front of your boss:
| Search query | Served under product group | Clicks | Cost | Conv. |
|---|---|---|---|---|
| dog training collar | Handbags | 97 | €11 | 0 |
| anti-bark device | Baby products | 76 | €9.60 | 0 |
| lego technic | Lighting | 70 | €6.80 | 0 |
The query “dog training collar” was served under the Handbags group. The query “anti-bark device” was served under Baby products. The product group has nothing to do with the query. Google matched the query to broad feed signals, collected the click, and charged the account. With query × product you see this in one glance; with Google’s standard report you never will. (Categories translated and anonymised.)
Step 7 · Now the payoff: the 0.25 % that pays for the whole account
Invert step 3. Isolate only the lines that did convert and total their cost and revenue. This is the reason the whole exercise matters. Once you can separate the winners from the drag, you protect the winners and starve the rest.
The slice that earns its keep
- Lines that converted (share of all lines) 57,209 (0.25 %)
- What they cost €14.9k
- What they returned €697k
- ROAS on that slice 46.8×
A quarter of one percent of the lines run at 46.8× ROAS and effectively carry the account. The other 99.75 % drag the blended figure down to 6.8×. The entire job is to find that 0.25 %, protect its budget, and restructure everything else so it stops competing for the same money. And none of that works until every line names the product whose ad was clicked. That name comes from one place: the utm_content={product_id} you set up at the start.
Download the complete instructions for your AI
The whole method above, rewritten as a brief you can paste straight into Claude or any capable coding agent. It sets the tracking template, pulls the query × product report out of GA4 and runs the aggregations on your account. The template and its three traps are in there too. Leave your email and the file is yours.
One file, one list. I only write when there's something worth reading.
FAQ
Does this work for Performance Max?
Yes, for the search and shopping surface. Display, YouTube and Gmail clicks carry the product ID but no query, so expect those rows to have an empty query dimension.
Will the UTM break my GA4 attribution?
No. Auto-tagging (gclid) keeps handling source, medium and campaign; you only add ad content. What would break things is a custom parameter inside utm_campaign, so never put one there.
Why only 78 % coverage?
Consent mode, queryless PMax surfaces and analytics blockers eat the rest. 78 % is plenty for every use case above, because you’re reading patterns, not auditing cents.
Can I see queries my ad showed for but nobody clicked?
No. This dataset starts at the click. Impression-level analysis stays in the standard search terms report, without products.
Does the pattern work outside Google, say Bing or Sklik?
Yes. The pattern transfers to any platform with a URL template, a product macro and an analytics dimension to catch it. The specific macros differ per platform.
How long until I have usable data?
Depends on volume. Our account had a workable report after 7 days of data. A smaller account should collect 30 days.
CTA: Which of your products earn their clicks, and which just bill you? Add the parameter today, and in a week you’ll know. Or send me your account and we’ll read the report together.