Skip to main content

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.

ModeOne traffic touch per...Built fromUse when
session (default)ga4_sessions rowga4_sessionsYou think in sessions; 1:1 with GA4 session reporting.
source_changeintra-session change of traffic sourcega4_eventsA 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.

Which mode?

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 marked ends_in_sale = TRUE and gets a closing_conversion_name and closing_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.