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:
| Column | Meaning |
|---|---|
first_touch_channel | Channel bucket — organic, direct, referral, social, email, paid, ai |
first_touch_utm_source | UTM source string from the URL, if present |
first_touch_utm_medium | UTM medium |
first_touch_utm_campaign | UTM campaign |
first_touch_utm_content | UTM content |
first_touch_utm_term | UTM term |
first_touch_referrer | Document referrer URL |
first_touch_landing_page | The first page URL they landed on |
first_touch_at | Timestamp 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:
- Click IDs (
gclid,fbclid,ttclid,msclkid) — always paid. utm_medium—cpc/ppc/paid/display/banner/cpm/native/rich_media→ paid;email→ email;social→ social;referral/affiliate→ referral;organic→ organic.utm_sourcematched against curated lists of paid-ad / social / email-ESP sources.- Referrer hostname — search engines → organic; known social platforms → social; webmail clients → email; otherwise → referral.
- In-app browser detection (Discord, Facebook, Instagram, etc.) — social.
- No referrer + no UTM → direct.
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:
utm_medium=email→ channel is email, regardless of referrer.- No referrer +
utm_sourcematching 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:
| Tab | Default attribution | What it shows |
|---|---|---|
| Channel | Toggle-aware | Visitors split into 9 buckets: Direct, Organic Search, Organic Social, Referral, Paid Search, Paid Social, Email, Affiliate, Display |
| Referrer | Toggle-aware | Top referring domains with favicons; in-app browsers (ChatGPT, Snapchat, etc.) classified via utm_source fallback |
| UTMs | Toggle-aware | Five sub-tabs: Source, Medium, Campaign, Content, Term. A (none) bucket counts visitors lacking that dimension |
| Keyword | Always last-touch | Pulled 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:
| Model | What it credits | When to use |
|---|---|---|
| Last-Touch | 100% to the last channel before converting | You want to know what closes deals |
| First-Touch | 100% to the channel that first brought the visitor in | You want to know what drives discovery |
| Linear | Split evenly across every channel in the journey | You want a balanced, neutral view |
| Position-Based | 40% first, 40% last, 20% split across the middle | You want to reward discovery AND closing |
| Time-Decay | More credit to channels closer to the conversion | You 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).