Skip to main content

Configuration

The module uses a single YAML configuration file: includes/custom/modules/ga4_attribution/config.yaml.

It ships disabled. At minimum, set enabled: true, define your conversions, and set journey.closing_conversions to start producing journeys, models and reports.

Minimal example​

version: 1
enabled: true
dependencies:
- ga4

touchpoint:
mode: session

conversions:
- name: purchase
eventFilter: purchase

revenue_field: ecommerce.purchase_revenue

journey:
closing_conversions:
- purchase
user_identifier: user_pseudo_id
channel_grouping: default_channel_group

attribution_models:
- { name: first_touch, type: first_touch }
- { name: last_touch, type: last_touch }
- { name: linear, type: linear }
- { name: position_based, type: position_based, first_weight: 0.4, last_weight: 0.4 }
- { name: time_decay, type: time_decay, half_life_days: 7 }

Top-level options​

OptionTypeDefaultDescription
enabledbooleanfalseToggle the whole module.
versionnumber1Config version (set by Superform Labs).
dependencieslist[ga4]Required modules; the ga4 module must be enabled.
rebuild_schedulestring"* * *"3-field cron ("dayOfMonth month dayOfWeek") gating the rebuild layer (journeys, models and all reports) to cut cost. Touchpoint tables always run incrementally; on non-matching days the previous rebuild stays in place. "* * *" rebuilds on every run; "* * 1" = Mondays only; "1 * *" = 1st of each month. It can only skip runs - rebuilding more often than the pipeline runs requires changing the Dataform workflow schedule instead. The gate is evaluated when the Dataform release compiles (in the compiling host's timezone), not when the workflow executes - so the release must recompile at least as often as the schedule needs to take effect ("* * 1" needs a compilation on the Monday itself); on a non-matching compilation day the rebuild simply does not happen.
revenue_fieldstringecommerce.purchase_revenueDefault revenue path for conversions without their own revenue_field.
custom_lineage.touchpoints_customfalse | "view"falseInterception point between the touchpoints table and ga4_attribution_journeys (offline touches, touch filters). See Custom lineage.
custom_lineage.journeys_customfalse | "view"falseInterception point between ga4_attribution_journeys and the models / reports (journey filters, column overrides). See Custom lineage.

Touchpoints​

touchpoint:
mode: session # or: source_change
source_change_unique_fields:
- fixed_traffic_source.source
- fixed_traffic_source.medium
- fixed_traffic_source.campaign
OptionTypeDefaultDescription
modesession | source_changesessionsession = one traffic touch per ga4_sessions row; source_change = one per intra-session traffic-source change. See Touchpoint modes.
source_change_unique_fieldslistsource / medium / campaignsource_change only. Dotted field paths whose change starts a new traffic touch.

Conversions​

Each entry is either structured (eventFilter + optional paramFilter) or open SQL (sql:) - not both. First match wins, so order matters; entries sharing a name are OR'd together.

conversions:
# First-match-wins: a more specific subset must come before the general one.
- name: high_value_purchase
eventFilter: purchase
paramFilter:
conditions:
- { param: 'safe_cast(ecommerce.purchase_revenue as numeric)', value: 500, operator: '>=' }
- { param: 'geo.country', value: ['Belgium', 'Netherlands'], operator: 'in' }
logic: and
revenue_field: ecommerce.purchase_revenue_in_usd # per-entry override

- name: purchase
eventFilter: purchase

- name: newsletter_signup # shared name is OR'd together
eventFilter: [signup, sign_up]

- name: newsletter_signup
sql: 'event_name = "page_view" AND page.path LIKE "/thank-you%"'
FieldDescription
nameConversion name (appears as conversion_name / closing_conversion_name). Reuse a name to OR several definitions.
eventFilterEvent name, or list of event names, to match.
paramFilterOptional structured predicate: conditions (each param / value / operator) combined by logic (and / or). Operators include =, >=, in, like, and so on.
sqlOpen SQL boolean predicate - an escape hatch when eventFilter / paramFilter cannot express the match. Mutually exclusive with eventFilter / paramFilter.
revenue_fieldOptional per-entry revenue path overriding the top-level revenue_field.

Dimensions & metrics​

Standard channel dimensions (source, medium, campaign, default channel group, device) are always emitted. Use dimensions / metrics to carry extra fields into journeys and the models/reports.

dimensions:
- { name: geo.country, renameTo: country }
- { name: session_source.term, renameTo: utm_term, nullLabel: 'Direct' }
- { name: geo.region, renameTo: regions, pick: chain }

metrics:
- { name: ecommerce.tax_value, renameTo: tax_value }
FieldApplies toDescription
name (dimensions)traffic rowsSource field path. On traffic rows the value is IFNULL(raw, nullLabel).
nullLabel (dimensions)traffic rowsPer-dimension null replacement. Default (not set); use Direct for source/channel dimensions.
renameTobothOutput column name (sanitized).
pick (dimensions)reportsWhich value the six reports carry per journey: first (default, the journey's first traffic touch), last, min, max or chain (every distinct value in first-seen order, joined with ' > ' - high cardinality, like channel_chain). ga4_attribution_models always carries each touch's own value.
name (metrics)conversion rowsMetric field, populated on conversion rows only; traffic rows get a typed NULL.
descriptionbothOptional BigQuery column description for the output column (for metrics it also describes the reports' total_<name> aggregate). Without it, the description is taken from the upstream GA4 column docs where available.
Mode-specific dimensions

Some fields only exist in one touchpoint mode - e.g. session_source.* and last_non_direct_traffic_source.* are session-mode fields. Choose dimensions that exist in your configured touchpoint.mode.

Where dimensions land​

TableValue per row
touchpoints, ga4_attribution_journeys (touches[])the touch's own value
ga4_attribution_modelsthe credited touch's own value
the six reportsone value per journey, chosen by pick ((no touchpoint) for zero-touch journeys)

One value per journey keeps every report additive: the dimension columns join the report's grain, and summing a report over them gives back the figures without dimensions. Every ga4 CUSTOM_CHANNEL_GROUPING is added the same way (always first). A dimension alias that collides with a report column (channel, path_length, lag_bucket, ...) or with a SQL alias the reports use fails compilation with a message naming the clash - pick another renameTo.

Journeys​

journey:
closing_conversions:
- purchase
user_identifier: user_pseudo_id
channel_grouping: default_channel_group
inactivity_gap_days: null
pre_conversion_lookback_days: null
OptionTypeDefaultDescription
closing_conversionslist[]Conversion names that close a journey. Must exist in conversions[]. Empty = nothing closes and the models table is empty. Non-listed conversions become micro-conversions.
user_identifierstringuser_pseudo_idIdentity key; non-user_pseudo_id values fall back to user_pseudo_id via COALESCE.
channel_groupingstringdefault_channel_groupChannel grouping the path reports build on: default_channel_group or a configured GA4 CUSTOM_CHANNEL_GROUPING name.
inactivity_gap_daysnumber | nullnullSplit a journey when the next touch is more than N days later.
pre_conversion_lookback_daysnumber | nullnullA closing conversion only attributes touches within M days before it; older touches become a separate journey.

See Journey boundaries for the full boundary semantics.

Attribution models​

attribution_models:
- { name: first_touch, type: first_touch }
- { name: last_touch, type: last_touch }
- { name: linear, type: linear }
- { name: position_based, type: position_based, first_weight: 0.4, last_weight: 0.4 }
- { name: time_decay, type: time_decay, half_life_days: 7 }
# - { name: participation, type: participation } # full credit to every touch - overcounts (influence, not credit)

Defaults to the five standard fractional models. position_based takes first_weight / last_weight (default 0.4 / 0.4); time_decay takes half_life_days (default 7). The opt-in participation model gives full credit to every touch and overcounts by design - see Fractional credit vs influence. The models table only builds once journey.closing_conversions is set.