Touchpoints & journeys
The pipeline starts by flattening GA4 into a touchpoint stream - one row per touch - then groups those touches into journeys per user.
Touchpoints
Every touch is one of two kinds:
- Traffic touch - where a visit came from (source / medium / campaign / channel). Built from your GA4 data per the selected mode.
- Conversion touch - an event matching one of your configured conversions (e.g.
purchase). Carries the conversion name and revenue.
Touchpoint modes
The touchpoint.mode setting controls how traffic touches are counted. Conversion touches are identical in both modes.
| Mode | One traffic touch per... | Built from | Use when |
|---|---|---|---|
session (default) | ga4_sessions row | ga4_sessions | You think in sessions; 1:1 with GA4 session reporting. |
source_change | intra-session change of traffic source | ga4_events | A single session can span multiple sources and you want each source run credited. |
session mode emits exactly one traffic touch per session, using that session's resolved source. It mirrors GA4's session grain.
source_change mode reads events and starts a new traffic touch whenever the traffic source changes within a session. The fields that define "a change" are configurable via touchpoint.source_change_unique_fields (default: fixed_traffic_source.source / .medium / .campaign). A session where a user arrives organically and then clicks a paid ad mid-session produces two traffic touches instead of one.
session is the safe default and matches how most teams already report. Reach for source_change only when intra-session source switching is common and material to your attribution (e.g. heavy paid + organic overlap).
Both touchpoint tables are intermediate - you normally consume ga4_attribution_journeys and the reports, not the raw touchpoints. See Output tables.
Journeys
ga4_attribution_journeys groups a user's touches, in time order, into one or more journeys. Each row is one journey, with its traffic touches and conversion touches stored as nested arrays plus summary columns (length, revenue, closing conversion, and so on).
Identity
Journeys are grouped by journey.user_identifier - user_pseudo_id by default (always present). If you set it to user_id (or another field), it falls back to user_pseudo_id via COALESCE when the chosen key is null, so cookie-level touches are never dropped.
Journey boundaries
A journey ends (and the next one begins) at the first of:
- A closing conversion - any conversion whose name is in
journey.closing_conversions. This is the primary boundary; the journey is markedends_in_sale = TRUEand gets aclosing_conversion_nameandclosing_conversion_date. - An inactivity gap (
journey.inactivity_gap_days, optional) - if the next touch is more than N days after the previous one, the journey is split. - A lookback cutoff (
journey.pre_conversion_lookback_days, optional) - a closing conversion only attributes touches within M days before it; older touches fall into a separate journey.
With both optional boundaries unset, journeys are conversion-anchored only: they run until a closing conversion, otherwise stay open.
A journey's state is one of:
- Closed - reached a closing conversion (
ends_in_sale = TRUE). - Ongoing (
is_ongoing = TRUE) - no boundary reached yet; still accumulating. - Lapsed - closed by an inactivity/lookback boundary without a sale.
Closing vs micro conversions
Any event can be configured as a conversion, but only those in journey.closing_conversions close a journey. Conversions that are not in that list are micro-conversions: they ride along in the journey's conversions[] array (and in micro_conversion_count / micro_revenue) without ending it. This lets you measure how an intermediate action (newsletter signup, add-to-cart) participates in journeys and lifts the final outcome - see the micro-conversion report.
Zero-touch journeys
A conversion can occur with no preceding traffic touch (e.g. a direct conversion with no captured source). These are zero-touch journeys: journey_length_steps = 0, and the reports label the channel '(no touchpoint)' rather than dropping the conversion. They keep your conversion totals reconciled even when no channel can be credited.