All documentation

Traffic

Google Analytics sessions grouped by where they came from, with AI counted first, and the pages where attention is not converting.

What you see

Traffic reads every session your Google Analytics 4 property recorded for the project, not only the AI-referred slice: total sessions for the window, with people, page views, conversions, and conversion value stepped down beneath it (api/routers/integrations.py#ga4_traffic).

Beside the hero, a smaller "Of that, AI-referred" figure and its own share of the window's sessions: the same AI referrer classification AI traffic uses, summed with no page or channel cap, so no AI source is left out of it, current and prior window alike (api/services/integrations/google/ga4_traffic_metrics.py#sum_ai_sessions, #ai_share). A window with none says so as a real zero, not a blank.

Conversions and conversion value are sums of the same rows AI traffic reads for the same window: GA4's own count and revenue stand on every row except where one of your named conversion events fired, or GA4's own purchase and sign_up events if you have named none, in which case that event's own count and value replace GA4's figure on that row (api/services/integrations/google/ga4_attribution.py#_load_project_context, #_DEFAULT_CONVERSION_EVENTS). Each shows a dash and a short note instead of a bare zero when the window's total comes to nothing either way.

Three change rules run side by side here. The hero's sessions figure, the AI-referred figure beside it, and the Change column in all three tables lower down share one chip, which renders nothing at all when the prior window's figure for that row is zero, rather than a change measured up from zero (frontend/src/lib/components/DeltaChip.svelte#delta). The four tiles stepped down from the hero are handed a plain subtraction of the two windows instead, and a prior figure of zero is a real zero to that subtraction (frontend/src/lib/components/Stat.svelte#delta). So on a window with nothing before it, each of those tiles that has a figure to show reads as a rise of that whole figure, printed as a bare count where it would usually carry a percentage. The hero showing no chip while the tiles beneath it show a rise is the tell that the prior window is empty. The movers list follows a third rule: its own delta is written out as plain text and always prints, including the zero-prior case the chip suppresses.

These figures do not all rest on one basis. The import asks GA4 for five metrics at a single grain and stores a row per property, page, source, medium, country, and day (api/services/integrations/google/ga4_attribution.py#import_ga4_daily_for_project, #_upsert_metrics). Page views and conversions are counts of events, and conversion value is the revenue those events carried: an event happened on one page, on one day, from one source, in one country, so it lands in exactly one row, and adding the rows up returns the window's own total. Page views are therefore GA4's own figure for the same days wherever they appear on this screen. Conversions and conversion value are exact in the same way, but they are not always GA4's own number: on any row where one of your named events fired, the stored pair is that event's count and value rather than the property's, and the totals on this screen add both kinds together (#_upsert_metrics), as described further up this page.

Sessions are only as exact as the grouping they are read under, because a visit is recorded against every page it touched. Any figure that does not break down by page therefore adds a visit up once per page: the hero's sessions, the AI-referred figure beside it, the Channels tab and the channel mix built from it, the Countries tab, and the daily trend all come out at roughly GA4's own count for the same days multiplied by the number of pages an average visit covers. The Pages tab does break down by page, so its session count is the visits that included that page, which is what GA4 reports for it (api/routers/integrations.py#ga4_traffic).

People inflate on a second axis and no grouping on this screen removes it. The day is part of the stored grain, so a person is counted once for every day they were active as well as once for every page they saw: one visitor returning daily for four weeks reads as twenty-eight people, on a one-page site where the sessions figure is exact. Read People as a ceiling on your audience rather than a count of it.

The two bases meet wherever this screen divides one by the other. A page's own conversion rate in "Pages that underconvert" puts exact conversions over exact page sessions, but the site average it is measured against divides by the inflated total, so that benchmark sits below your real site rate and the list is a conservative one: fewer pages clear the bar, and a smaller shortfall is estimated for each, than the true average would produce. The list itself is the top eight either way (api/routers/integrations.py#ga4_traffic, api/services/integrations/google/ga4_traffic_metrics.py#below_avg_converters). The Channels tab's Rate column divides exact conversions by inflated channel sessions the same way, so read it as a floor rather than as the rate GA4 would give you.

Where sessions come from

Every session falls into exactly one of seven channel buckets, checked in this order: AI first (a source on the AI referrer list AI traffic classifies), then Paid (a medium such as cpc, cpm, or display), Organic (medium exactly organic), Social (a social medium, or a source such as facebook.com, x.com, or reddit.com), Referral (medium exactly referral), Direct (source (direct) or direct, or a medium that is empty, none, or (none)), then Other for anything left over. Other is the unconditional fallback, so nothing falls outside those seven (api/services/integrations/google/ga4_traffic_metrics.py#channel_group).

AI is checked first on purpose: an AI referral whose GA4 medium happens to read referral, organic, or nothing at all would otherwise land in that bucket instead, splitting the AI slice across several rows. Checking it first keeps this bucket, the hero's AI-referred figure, and the Channels tab's AI badge all agreeing about which sessions count as AI (#channel_group).

The bar and list show the seven buckets in a fixed order, AI, Organic, Direct, Referral, Social, Paid, Other, skipping any with no sessions this window (#channel_mix, frontend/src/routes/(app)/performance/traffic/+page.svelte#channelMix). They are built from the same up to 100 source/medium rows that back the Channels tab beneath, ranked by session count, not the window's full total (#ga4_traffic); the hero's AI-referred figure has no such cap, so on a site with more than 100 distinct channels in a window, this chart's shares, AI's included, can diverge from the hero's own figure above it.

Pages that underconvert

Two lists, switched by a toggle, both drawn from the same up to 100 pages that back the Pages tab below: each keeps only pages with at least 10 sessions in the window, and each is capped at 8 rows (api/routers/integrations.py#TRAFFIC_OPP_FLOOR, #TRAFFIC_OPP_TOP_N).

"Below average" compares each page's own conversion rate against the whole window's rate (total conversions over total sessions, unbounded by that 100-page pool), keeps only pages under that benchmark, and ranks by an estimated missed-conversions figure: sessions times how far the rate sits below average (api/services/integrations/google/ga4_traffic_metrics.py#below_avg_converters). Until a conversion has been recorded in the window, that benchmark is zero, so the tab explains that instead of listing anything.

"No conversions" has no benchmark to wait for: it lists pages with at least 10 sessions and exactly zero counted conversions, ranked by sessions (api/services/integrations/google/ga4_traffic_metrics.py#zero_conversion_pages). A page lands here only when no conversion fired on it at all: not one of GA4's own key events, not one of your named conversion events, and not the purchase/sign_up default that applies when you have named none. Every row arrives carrying GA4's own key-event count, and the replacement only lands on the rows where one of the counted events actually fired, so a page where anything converted holds a count either way (api/services/integrations/google/ga4_attribution.py#_upsert_metrics). This list is therefore not simply every well-trafficked page before you set anything up: a property already recording GA4 key events excludes the pages those events fired on, named conversion events or not.

Movers

Movers compares sessions in the window against the window immediately before it (see below for exactly how the two compare), across the union of two capped lists: this window's top 100 channels or pages, and the prior window's own top 100, each capped separately. A row can therefore appear here without appearing in the tabs above, which show only this window's 100. Kept only where the larger of the two sides reaches 5 sessions, then split into gainers and losers, each capped at 8 rows (api/routers/integrations.py#TRAFFIC_MOVER_FLOOR, #TRAFFIC_MOVER_TOP_N, api/services/integrations/google/ga4_traffic_metrics.py#compute_session_movers).

Anything inside one window's 100 and outside the other's is compared against 0 for the side it is missing from, whether or not it truly was zero. The two cutoffs are set independently of each other, so that 0 can stand in for a figure larger than the one it is being compared with, and then the direction is wrong rather than just the size: a page with 15 sessions in a busy prior window, too few for that window's 100, and 5 in a quieter current one, enough for this window's 100, publishes as a gainer of 5 against a real fall of 10. Every row this can have happened to shows a zero on one side, so they are the ones to read twice: the gainers whose gain equals their whole session count, and the losers showing no sessions in this window. A row carrying a real figure on both sides has an exact change. A row whose sessions did not change appears in neither list: only a real increase counts as a gainer and only a real decrease as a loser, so this is a diff, not every row with enough volume.

The trend, and the breakdowns beneath it

The daily trend draws total sessions with AI-referred sessions overlaid on the same scale, zero-filled for every calendar day in the window whether or not that day has actually been imported yet. It renders whenever ?days= is 4 or higher, which every preset (7, 28, or 90) clears easily. Only a hand-set ?days= that lands on 1, 2, or 3 suppresses it: 0, a negative, or anything that is not a number falls back to the default 28 and the trend draws (frontend/src/routes/(app)/performance/traffic/+page.svelte#hasTrend, api/services/integrations/google/ga4_traffic_metrics.py#build_daily_series, frontend/src/routes/(app)/performance/traffic/+page.server.js#windowDays).

Beneath it, three tabs, Channels, Pages, and Countries, each list up to 100 rows ranked by sessions, with a change chip against that row's own prior-window sessions where that prior figure is not zero (a row with nothing to compare against shows no chip, the same rule the hero's own sessions figure uses). Channels and Pages both add users, page views, conversions, and value; Channels also carries a conversion-rate column and the same AI badge the channel mix's first bucket represents. Countries drops page views but keeps the rest (#ga4_traffic). The row count under the tabs is how many rows came back, not a hint that more exist beyond it.

The window, and how the two periods compare

Left alone, this screen opens on the last 28 days. You can switch it to 7, 28, or 90 days, or set ?days= yourself higher still; 180 is the ceiling this screen enforces regardless of what the URL asks for (frontend/src/routes/(app)/performance/traffic/+page.server.js#windowDays).

Every change figure here, from the hero down to each row in Channels, Pages, and Countries, compares the selected window against the window immediately before it, on the same terms Search performance uses: the current window runs from window_days days ago through today, both ends inclusive, which is window_days + 1 days; the prior window covers exactly window_days days, ending the moment the current one starts, with no gap and no overlap. At the default 28-day setting that is 29 days of current data against 28 days of prior data (api/routers/integrations.py#ga4_traffic).

Before you see data

Traffic needs the same Google Analytics 4 connection and property mapping as AI Traffic; both screens check the same mapped properties for this project (a project can have more than one), so mapping one starts the import that feeds both. With nothing connected, the screen prompts you to connect; connected but unmapped, it prompts you to map a property; mapped but this window still reads zero sessions, whether because the first import has not landed yet, the window itself is genuinely empty, or the request to fetch it failed, it says a day's wait is normal rather than showing an empty chart (frontend/src/routes/(app)/performance/+layout.server.js#ga4Connected, frontend/src/routes/(app)/performance/traffic/+page.server.js#ga4Mapped, frontend/src/routes/(app)/performance/traffic/+page.svelte#ga4HasData). See Troubleshooting: Performance is empty for exactly when the recurring import runs.

  • AI traffic: the AI-referred slice this screen's hero figure summarises, broken out by engine
  • Search performance: the equivalent breakdown for Google Search Console, using the same current-versus-prior window arithmetic
  • Troubleshooting: what an empty or stale-looking screen usually means

Last verified 2026-08-10

Start monitoring your AI visibility.

See how AI search engines talk about your brand.

Free to start. No credit card required.