utm_source=chatgpt.com: how to see which pages ChatGPT visitors land on (GA4 API and server logs)
If you've spotted utm_source=chatgpt.com in your analytics or your access logs, you've found the people ChatGPT sends you. The useful next question is where they land, and on most small business sites we measured, the answer isn't mainly the homepage. We build LovedByAI , a generative engine optimi

If you've spotted utm_source=chatgpt.com in your analytics or your access logs, you've found the people ChatGPT sends you. The useful next question is where they land, and on most small business sites we measured, the answer isn't mainly the homepage.
We build LovedByAI, a generative engine optimization platform for small businesses built around WordPress, and its plugin logs every ChatGPT visit with the page it lands on. Across 23,740 ChatGPT visits to 323 small business websites between 14 September and 13 October 2026 (sites with at least 10 ChatGPT visits, each site counted once), the homepage took 42% of ChatGPT's visitors on the average service business site and 23% on the average online store, where product pages took 37%. The full study is ChatGPT referral traffic: where visitors land.
This post covers how to get the same list for your own site, or for every client site you look after: from the GA4 interface, from the GA4 Data API, and from raw access logs, with a script we ran before publishing.
utm_source=chatgpt.com is a query parameter ChatGPT appends to the links in its answers. When a person clicks one, the request for your page carries it, and Google Analytics 4 records the visit with the session source chatgpt.com. The browser usually sends https://chatgpt.com/ as the referrer too.
Three things that look similar but aren't the same:
GA4's default channel group now also has an AI Assistant channel ( Google's channel definitions ), and in our own property ChatGPT's tagged sessions show up there with the medium ai-assistant. We still filter on Session source. Google's rule puts a session in that channel when its medium is ai-assistant, which GA4 sets when the referrer matches its list of assistants, and in our property some chatgpt.com sessions were filed under Organic Search instead. The source is what the tag sets directly.
The same report as a runReport request. POST it to https://analyticsdata.googleapis.com/v1beta/properties/<PROPERTY_ID>:runReport (the "Try this method" panel on that page works, as does any Google client library):
{ "dateRanges": [{ "startDate": "90daysAgo", "endDate": "yesterday" }], "dimensions": [{ "name": "landingPage" }], "metrics": [{ "name": "sessions" }], "dimensionFilter": { "filter": { "fieldName": "sessionSource", "stringFilter": { "matchType": "EXACT", "value": "chatgpt.com" } } }, "orderBys": [{ "metric": { "metricName": "sessions" }, "desc": true }], "limit": 1000 } Enter fullscreen mode Exit fullscreen mode We checked this request against the Data API on our own GA4 property before publishing. Two things to expect in the response: landingPage is the path without the query string (use landingPagePlusQueryString if you want it), and some rows come back as (not set), sessions GA4 couldn't attach a page to. Save the response as a.json file; the script reads it as is, in either the REST shape ( dimensionValues ) or the client library's ( dimension_values ).
A ChatGPT visit in a combined-format access log is a request whose query string carries utm_source=chatgpt.com, or whose referrer is chatgpt.com. The second page of the same visit has your own site as its referrer, so it doesn't match, which means each match is a landing. These demo lines (invented, from the documentation IP ranges) show what counts and what doesn't:
203.0.113.10 - - [06/Oct/2026:08:01:12 +0000] "GET /?utm_source=chatgpt.com HTTP/1.1" 200 18233 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1" 192.0.2.80 - - [06/Oct/2026:12:45:00 +0000] "GET /contact/ HTTP/1.1" 200 9001 "https://chatgpt.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 192.0.2.91 - - [06/Oct/2026:13:20:33 +0000] "GET /?utm_source=chatgpt.com&utm_medium=cpc HTTP/1.1" 200 18233 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 203.0.113.10 - - [06/Oct/2026:08:02:40 +0000] "GET /services/burst-pipe-repair/ HTTP/1.1" 200 15002 "https://demo-plumber.example/?utm_source=chatgpt.com" "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1" 192.0.2.150 - - [06/Oct/2026:14:40:02 +0000] "GET /services/burst-pipe-repair/ HTTP/1.1" 200 15002 "-" "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot" Enter fullscreen mode Exit fullscreen mode In order: a tagged landing on the homepage (counted); a landing on /contact/ with only the referrer (counted); a paid click (skipped); the visitor from the first line moving to a second page, with your own site as referrer (skipped); ChatGPT-User fetching a page (skipped, it's a bot).
Two caveats for production logs. If a CDN serves cached HTML, those hits never reach your origin log, so read the CDN's logs instead. And a reload logs twice, where GA4 counts one session.
chatgpt_landings.py takes one or more sources, one per website: an access log, or a saved GA4 runReport response. For each it prints the split by page type and the top pages, then compares the average across sites with the pooled total. Standard library only, Python 3.8 or later.
#!/usr/bin/env python3 """ Where do ChatGPT ' s visitors land? One split per website. python3 chatgpt_landings.py SOURCE [SOURCE...] Each SOURCE is one website, either: - an access log in the combined format (the nginx and Apache default), or - a saved GA4 Data API runReport response (.json) with landingPage as the first dimension and sessions as the first metric. Prints each site ' s split by page type and its top pages, then the average across sites (every site counts once) next to the pooled split.
Standard library only, Python 3.8 or later. """ import json import re import sys from collections import Counter from urllib.parse import parse_qs, unquote, urlsplit LINE = re. compile ( r ' (?P<ip>\S+) \S+ \S+ \[[^\]]+\] " (?P<method>\S+) (?P<target>\S+)[^ " ]* " ' r ' (?P<status>\d{3}) \S+ " (?P<referer>[^ " ]*) " " (?P<ua>[^ " ]*) "' ) CHATGPT_HOSTS = { " chatgpt.com ", " chat.openai.com " } # Clicks on paid placements: the study left these out. PAID = re. compile ( r " [?&]utm_medium=(paid|cpc|ads?)(&|$)|[?&](oppref|olref)= ", re.
I ) NOT_A_PERSON = re. compile ( r " bot|crawler|spider|ChatGPT-User ", re. I ) ASSET = re. compile ( r " \.(css|js|map|png|jpe?g|gif|webp|svg|ico|woff2?)$ ", re. I ) # Rough URL rules, checked in order.
The study typed pages with an AI model; # edit these to match your own site's URL structure. RULES = [ ( " homepage ", r " ^/([a-z]{2}(-[a-z]{2})?/)?$ " ), ( " product ", r " ^/(product|products|item)/ " ), ( " category ", r " ^/(product-category|shop|collections?)(/|$) " ), ( " blog post ", r " ^/(blog|news|articles?|guides?)/.+|^/\d{4}/\d{2}/ " ), ( " service ", r " ^/(services?|treatments?|practice-areas?)/.+ " ), ( " location ", r " ^/(locations?|areas?|service-areas?)/.+ " ), ( " about ", r " ^/(about|about-us|team|our-story)/?$ " ), ( " contact/booking ", r " ^/(contact|contact-us|book|booking|appointments?|quote|get-a-quote)/?$ " ), ( " pricing ", r " ^/(pricing|prices|rates|plans)/?$ " ), ] RULES = [( name, re. compile ( rx, re. I )) for name, rx in RULES ] def page_type ( path ): for name, rx in RULES: if rx. search ( path ): return name return " other " def from_chatgpt ( target, referer ): """ A person who clicked a link in a ChatGPT answer (tag or referrer).
""" if " chatgpt.com " in parse_qs ( urlsplit ( target ). query ). get ( " utm_source ", []): return True return ( urlsplit ( referer ). hostname or "" ) in CHATGPT_HOSTS def read_log ( path ): pages, dead = Counter (), Counter () with open ( path, encoding = " utf-8 ", errors = " replace " ) as log: for raw in log: m = LINE. match ( raw ) if not m or m [ " method " ]!= " GET " or NOT_A_PERSON. search ( m [ " ua " ]): continue page = unquote ( urlsplit ( m [ " target " ]). path ). lower () or " / " if ASSET. search ( page ) or PAID. search ( m [ " target " ]): continue if from_chatgpt ( m [ " target " ], m [ " referer " ]): pages [ page ] += 1 if m [ " status " ] == " 404 ": dead [ page ] += 1 return pages, dead, 0 def read_ga4 ( path ): """ A runReport response, REST (camelCase) or client-library (snake_case). """ report = json. load ( open ( path, encoding = " utf-8 " )) pages, not_set = Counter (), 0 for row in report. get ( " rows ", []): dims = row. get ( " dimensionValues " ) or row. get ( " dimension_values " ) mets = row. get ( " metricValues " ) or row. get ( " metric_values " ) page, sessions = dims [ 0 ][ " value " ], int ( mets [ 0 ][ " value " ]) if page == " (not set) ": not_set += sessions continue pages [ page. lower ()] += sessions return pages, Counter (), not_set def split ( pages ): by_type = Counter () for page, n in pages. items (): by_type [ page_type ( page )] += n total = sum ( by_type. values ()) return { t: n / total for t, n in by_type. items ()}, total def main ( sources ): shares, pooled = [], Counter () for src in sources: pages, dead, not_set = read_ga4 ( src ) if src. endswith ( ".json " ) else read_log ( src ) if not pages: print ( f " \n { src }: no ChatGPT visits found " ) continue site, total = split ( pages ) shares. append ( site ) for page, n in pages. items (): pooled [ page_type ( page )] += n print ( f " \n { src }: { total } ChatGPT landings " ) for t, s in sorted ( site. items (), key = lambda kv: - kv [ 1 ]): print ( f " { 100 * s: 5.1 f } % { t } " ) top = pages. most_common ( 3 ) print ( f " top 3 pages take { 100 * sum ( n for _, n in top ) / total:. 0 f } %: " + ", ". join ( p for p, _ in top )) if not_set: print ( f " ( { not_set } sessions with landing page ' (not set) ' left out) " ) if dead: print ( f " landed on a 404: { sum ( dead. values ()) } ( { ', '. join ( dead ) } ) " ) if len ( shares ) > 1: types = sorted ({ t for s in shares for t in s }, key = lambda t: - sum ( s. get ( t, 0 ) for s in shares )) pooled_total = sum ( pooled. values ()) print ( f " \n Across { len ( shares ) } sites average of sites pooled visits " ) for t in types: avg = sum ( s. get ( t, 0 ) for s in shares ) / len ( shares ) print ( f " { t: 17 s } { 100 * avg: 14.1 f } % { 100 * pooled [ t ] / pooled_total: 13.1 f } % " ) if __name__ == " __main__ ": if len ( sys. argv ) < 2: sys. exit ( __doc__ ) main ( sys. argv [ 1:]) Enter fullscreen mode Exit fullscreen mode The page types come from rough URL rules at the top.
Our study sorted 7,211 landing pages with an AI model instead; for one site, editing RULES to match your URL structure is quicker and good enough.
We ran it on two demo sites: an access log from an invented plumber ( plumber-access.log, 12 lines) and a saved GA4 response from an invented shop ( shop-ga4-runreport.json, 8 rows):
$ python3 chatgpt_landings.py demo/plumber-access.log demo/shop-ga4-runreport.json demo/plumber-access.log: 7 ChatGPT landings 28.6% homepage 28.6% service 14.3% blog post 14.3% contact/booking 14.3% other top 3 pages take 71%: /, /services/burst-pipe-repair/, /blog/how-to-shut-off-your-water-main/ landed on a 404: 1 (/emergency-plumber-24-7/) demo/shop-ga4-runreport.json: 125 ChatGPT landings 50.4% product 24.0% homepage 12.0% category 9.6% blog post 2.4% about 1.6% contact/booking top 3 pages take 74%: /product/linen-apron/, /, /product/canvas-tote/ (4 sessions with landing page '(not set)' left out) Across 2 sites average of sites pooled visits homepage 26.3% 24.2% product 25.2% 47.7% service 14.3% 1.5% blog post 11.9% 9.8% contact/booking 7.9% 2.3% other 7.1% 0.8% category 6.0% 11.4% about 1.2% 2.3% Enter fullscreen mode Exit fullscreen mode The plumber's 12 log lines become 7 landings: the paid click, the Google visit, the stylesheet, the second page of a visit and the ChatGPT-User fetch all drop out. The 404 is a link ChatGPT handed out to a page that doesn't exist, which is a separate problem we covered in why ChatGPT sends visitors to broken links.
If you look after several client sites, the last table is the one to read. Pooled, the shop's 125 sessions swamp the plumber's 7, and product pages look like 47.7% of ChatGPT traffic. Averaged per site, they're 25.2%. Neither site looks like the pooled number.
The study hit the same thing at scale. Across the 333 sites with at least 10 ChatGPT visits between 14 September and 13 October 2026, the five biggest held 26% of all visits. Pooled, listing pages (directory entries) came out at 16.4% of ChatGPT traffic, mostly from one directory; averaged per site, 2.2%. Pooling also pushed the homepage down to 22.7%, against 32.7% when every site counts once.
Medians don't rescue it either. A typical site gets ChatGPT visitors on only three or four kinds of page, so the median share for most page types is zero and can't rank anything. Use the per-site average, plus a count of how many sites had any visit on each type.
Benchmarks from LovedByAI's data, per-site averages, 14 September to 13 October 2026, sites with at least 10 ChatGPT visits. "Service business" means no ChatGPT visitor landed on a product, category or listing page; "online store" means at least one landed on a product or category page.
The spread matters as much as the split. On the median service business site, ChatGPT visitors landed on 7 different pages in the month, and the top 3 took 78% of them. On the median store it was 16 pages, with the top 3 taking 53%. And blog posts are lopsided: on 47% of the service sites no ChatGPT visitor landed on a blog post, while on 14% blog posts took half or more.
If your own split is far from these, that's not a problem in itself. It tells you which pages ChatGPT uses to introduce you, and those are the pages to look at first.
[[VIDEO: planned youtube video | How to track AI traffic in GA4: the landing page report for ChatGPT visitors, the AI Assistant channel, and what 23,740 ChatGPT visits say you'll find.]]
For the owner's side of this, without the code: ChatGPT traffic: why most of it lands past your homepage.
LovedByAI's plugin keeps this log on every WordPress site it runs on, which is where the 23,740 visits above come from, and the platform automates the on-page optimization across those pages and the rest of the site. If you're not on WordPress, the script and the GA4 request get you the same list by hand.
Original study and full dataset: https://www.lovedby.ai/blog/chatgpt-referral-traffic