Query the events_* (and events_intraday_* for same-day data) tables grouped by user_pseudo_id, ordered by event_timestamp, extracting the traffic-source parameters at each session boundary - the journey is the ordered list of distinct session-level channel touches between a user's first recorded event and the event you are analyzing, typically a conversion.
Why this happens
GA4's BigQuery export is intentionally granular - one row per event, not per session or per user - which is exactly what makes journey reconstruction possible but also means you must do the session- and journey-level aggregation yourself. The UI's own reports pre-aggregate this into fixed views, like the default channel groupings, that do not expose the raw ordered sequence a Markov model or journey-visualization tool needs.
The schema itself has changed across GA4's life - traffic-source attribution fields moved and were renamed as Google refined the export - so a query written against an older schema reference can silently return null or empty traffic-source fields against a newer export. This is a common source of "the journey looks incomplete" that is actually a schema-version mismatch, not a real data gap.
Fix it
- Confirm which GA4 BigQuery export schema version you are working with, and use the field names that match - particularly for the traffic-source struct, which changed across GA4's rollout.
- Write a query that unnests event_params per row to pull out the session ID, then groups rows into sessions per user_pseudo_id.
- For each session, extract the traffic-source values GA4 attaches at session start (source, medium, campaign, or the newer collected_traffic_source struct) as that session's channel touch.
- Order each user's sessions by timestamp to produce their journey: an ordered list of channel touches from first recorded activity to the event of interest.
- Decide explicitly how to handle direct/none sessions in the middle of a journey - a common convention treats them as a continuation of the prior known channel, or as their own distinct "Direct" touch - and document that choice, since it materially changes downstream attribution results.
- Watch for user_pseudo_id churn - cleared cookies, cross-device, incognito - which fragments what is really one person into multiple apparent users in the raw export, a known ceiling of cookie-based reconstruction, not a query bug.
How to verify it worked
Pick one real, recent conversion and manually trace that user's session history in GA4's own user-level exploration to compare against what your BigQuery query reconstructs for the same user_pseudo_id - the touch sequence and channel labels should match. A mismatch usually traces back to a schema-field version issue or a session-boundary definition difference between your query and GA4's own session logic.