Skip to main content
8 min read

Attribution Models

Every visitor in Zenovay carries two attribution snapshots: their first-touch channel (how they first found you) and their last-touch channel (the channel of the session that converted, or the most recent session). Both are stored independently — first-touch is locked at the very first observation and never overwritten.

Why two snapshots

Marketing attribution is a question of credit. If a visitor first arrives via an organic Google search, comes back three weeks later via a paid LinkedIn ad, and converts on a direct visit:

  • Last-touch credits direct. (Often understates marketing.)
  • First-touch credits organic search. (Often overstates SEO.)
  • Linear / position-based / time-decay models split credit across every channel in the visitor's journey — selectable on the Revenue tab (see below).

Zenovay starts simple: both first-touch and last-touch are stored, and the dashboard's attribution surfaces let you toggle between them.

What first-touch captures

When a visitor is observed for the very first time, Zenovay populates these columns on their visitor row:

ColumnMeaning
first_touch_channelChannel bucket — organic, direct, referral, social, email, paid, ai
first_touch_utm_sourceUTM source string from the URL, if present
first_touch_utm_mediumUTM medium
first_touch_utm_campaignUTM campaign
first_touch_utm_contentUTM content
first_touch_utm_termUTM term
first_touch_referrerDocument referrer URL
first_touch_landing_pageThe first page URL they landed on
first_touch_atTimestamp of the first observation

These are never overwritten on subsequent sessions. The corresponding non-prefixed columns (channel, utm_*, referrer, landing_page) are updated each session and represent last-touch.

Visitors observed before 2026-04-30 have NULL first-touch values. The history was not back-filled — see your retention policy for what historical data is available.

Channel classification

Channels are classified in this priority order:

  1. Click IDs (gclid, fbclid, ttclid, msclkid) — always paid.
  2. utm_mediumcpc/ppc/paid/display/banner/cpm/native/rich_mediapaid; emailemail; socialsocial; referral/affiliatereferral; organicorganic.
  3. utm_source matched against curated lists of paid-ad / social / email-ESP sources.
  4. Referrer hostname — search engines → organic; known social platforms → social; webmail clients → email; otherwise → referral.
  5. In-app browser detection (Discord, Facebook, Instagram, etc.) — social.
  6. No referrer + no UTMdirect.

A visitor flagged as bot-like by Zenovay's AI heuristic has their channel overridden to ai regardless of UTM.

Email traffic — the empty-referrer fix

Email links typically have no referrer header. Without UTM parameters, this used to misclassify email traffic as direct.

Zenovay catches this in two ways:

  1. utm_medium=email → channel is email, regardless of referrer.
  2. No referrer + utm_source matching a known ESP (Mailchimp, Klaviyo, HubSpot, ActiveCampaign, Brevo, ConvertKit, MailerLite, Beehiiv, Customer.io, ActiveCampaign, GetResponse, Omnisend, Substack, Drip, Intercom, Postscript, Attentive, Iterable, Braze, Resend, Loops, Marketo, Pardot, and others) → channel is email.

The full ESP list lives in api-zenovay/src/constants/domains.ts (EMAIL_UTM_SOURCES). To make sure your email campaigns classify correctly:

<!-- Recommended UTM convention for all email links -->
<a href="https://your-site.com/?utm_source=mailchimp&utm_medium=email&utm_campaign=launch">
  Read more
</a>

Either utm_medium=email or a recognized utm_source is enough — you don't need both.

Reading attribution in the dashboard

The Sources tab and visitor detail panel let you switch between first-touch and last-touch directly in the UI — you no longer need to query the database to compare the two.

Sources tab

The Sources card on the analytics dashboard has a First touch | Last touch toggle in its header. The selection persists in the URL (?attribution=first or ?attribution=last), so refresh and back/forward navigation preserve your choice. Default is last-touch (matches the prior behavior).

The card has four top-level views:

TabDefault attributionWhat it shows
ChannelToggle-awareVisitors split into 9 buckets: Direct, Organic Search, Organic Social, Referral, Paid Search, Paid Social, Email, Affiliate, Display
ReferrerToggle-awareTop referring domains with favicons; in-app browsers (ChatGPT, Snapchat, etc.) classified via utm_source fallback
UTMsToggle-awareFive sub-tabs: Source, Medium, Campaign, Content, Term. A (none) bucket counts visitors lacking that dimension
KeywordAlways last-touchPulled from your Google Search Console connection; first-touch toggle does not apply

When you flip the toggle to First touch, the Channel / Referrer / UTM views all switch to the visitor's first observed values. Imported analytics (Plausible CSV imports) are last-touch only — those rows are skipped on first-touch view to keep the model honest.

Public dashboards always show last-touch attribution. The toggle is owner-only in V1; we'll surface a server-side default per-share in a follow-up.

Visitor detail card

When you click into a visitor's profile, the sidebar shows a compact Attribution panel: first-touch (channel + utm_source + landing page + first-seen date) on the left, last-touch (channel + utm_source + referrer + landing page) on the right. The panel hides itself for visitors observed pre-FND-B (≈2% of historical) so you don't see a row of em-dashes.

Revenue tab

The Revenue tab's Attribution card has a five-model selector — Last-Touch (default), First-Touch, Linear, Position-Based, and Time-Decay. The choice persists in the URL (?model=), so a refresh and back/forward navigation keep the model you picked. The default is last-touch.

The five attribution models

Each model answers the question "which channel gets credit for this conversion?" a different way:

ModelWhat it creditsWhen to use
Last-Touch100% to the last channel before convertingYou want to know what closes deals
First-Touch100% to the channel that first brought the visitor inYou want to know what drives discovery
LinearSplit evenly across every channel in the journeyYou want a balanced, neutral view
Position-Based40% first, 40% last, 20% split across the middleYou want to reward discovery AND closing
Time-DecayMore credit to channels closer to the conversionYou have shorter sales cycles

Time-Decay uses a 7-day half-life: a touch 7 days before the conversion gets half the weight of a touch at conversion time, and the weighting keeps halving for every additional 7 days.

A note on data history

If most conversions in a period came from a single session, the multi-touch models (Linear, Position-Based, Time-Decay) will look very similar to Last-Touch — there is only one channel to spread credit across. This is expected, not a bug, and resolves on its own as more multi-session journeys accumulate.

How each model reads your data

The five models do not all draw from the same source:

  • Last-Touch and First-Touch use a single snapshot. Last-Touch credits the channel recorded on the converting session; First-Touch credits the channel from the visitor's very first recorded session. No journey reconstruction is needed.
  • Linear, Position-Based, and Time-Decay reconstruct the visitor's full multi-session journey, pulling every recorded visit before the conversion and distributing credit across the sessions found.

One consequence of this difference: on identical underlying data, the models can agree completely or diverge noticeably, depending on how many distinct sessions and channels a visitor passed through before converting. A visitor with a single-session journey gives all models the same answer. A visitor with five sessions across three channels will produce measurably different outputs from each model.

Why your models can look almost identical

When you switch between attribution models and the channel breakdown barely changes, the data is behaving correctly — this is not a display issue.

If most of your conversions came from visitors who had only one session (or who consistently used only one channel before converting), every model mathematically awards that channel 100% of the credit. Last-Touch, First-Touch, Linear, Position-Based, and Time-Decay all reach the same answer because there is nothing to distribute differently. As your audience grows and more visitors touch multiple channels across multiple sessions before converting, the models will diverge and become more informative to compare.

A few additional things to keep in mind as you compare models:

  • AI traffic is always credited to the dedicated ai channel under every attribution model. AI detection takes precedence over the touch path, so the AI row stays constant when you switch models.
  • A conversion with no monetary value still counts toward conversion attribution. Revenue-weighted comparisons require a monetary value to be attached to the goal — without it, those conversions appear in counts but contribute nothing to the revenue totals shown by each model.

Empty UTM dimensions

Most visitors don't carry every UTM parameter. In real Zenovay data today, utm_term is ≈100% null and utm_content ≈99.99% null. The Sources tab shows an empty-state with a copyable hint (?utm_term=...) for those views rather than rendering a single 100% bar of (none).

Was this page helpful?