Attribution without a cookie banner
The visit lives in a JavaScript variable. It is born when the page loads and gone when the tab closes, and in between it is enough to tell where every signup came from.
What a banner is actually asking about
Most of the friction around consent comes from one rule. The EU ePrivacy rules say that storing information on a visitor's device, or reading information already stored there, needs consent unless it is strictly necessary for the service the visitor asked for. A cookie that remembers "this browser came from a Facebook ad on Tuesday" is not strictly necessary for showing a study app's landing page, so it needs consent. So does the same thing in localStorage, sessionStorage or IndexedDB. The rule cares about the device, not about the word cookie. That points at a design question most analytics tools never ask: what can you learn about a visit if you never write to the device at all?
More than you would think
Everything attribution needs is already sitting in the page when it loads. The URL has the utm_* parameters the ad or newsletter put there. document.referrer has the page the visitor clicked from. window.location.pathname has the landing page. None of that requires storing anything; the browser hands it to every script on the page. The browser helper in @vevee/sdk/browser reads those three things once, adds a random visit id and a start time, and keeps the result in a module variable. That is the whole state. It writes no cookie, no localStorage, no sessionStorage and no IndexedDB entry. It reduces the referrer to its host, so a referring URL with a search query in it never travels further. It ignores click ids such as fbclid and gclid entirely.
import { attribution } from '@vevee/sdk/browser';
attribution.start();
attribution.current();
// {
// visit_id: '3f1c9a2e-7b4d-4e0a-9c55-0d2b8e61a7f4',
// started_at: '2026-10-27T09:14:02.511Z',
// utm_source: 'facebook',
// utm_medium: 'paid_social',
// utm_campaign: 'oct-pricing',
// landing_path: '/pricing'
// }
//
// referrer_host appears when the visitor came from another site.
// Nothing above is written anywhere. Reload the tab and it is gone.Getting it off the page
A variable that dies with the tab is useless unless something reads it before then. The helper sends a pageview for each route change, carrying the visit fields, through a send function you give it. A study app that adopted this mounts one client component in its root layout and points send at its own server route, so the browser never holds a Vevee key and never calls Vevee directly. Every other event the app tracks before signup picks up the same visit fields with attribution.properties(). From then on the visit is a set of anonymous events on the server. Vevee stores a salted hash of the visit id, with a salt that rotates daily and is deleted within about 48 hours, so pageviews of one visit can be grouped on one day and nobody can rebuild the link afterwards. The raw visit id is never stored.
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { attribution, type PageviewEvent } from '@vevee/sdk/browser';
function send(e: PageviewEvent) {
return fetch('/api/analytics/capture', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(e),
keepalive: true,
}).then(() => undefined);
}
export function SiteAnalytics() {
const pathname = usePathname();
useEffect(() => { attribution.start({ send }); }, []);
useEffect(() => { attribution.pageview(pathname ?? undefined); }, [pathname]);
return null;
}What you give up
A cookie exists to recognize the same browser tomorrow. Without one, you cannot. Be honest with yourself about what that costs before you switch:
- No unique visitors across visits. A person who reads the pricing page on Monday and comes back on Thursday is two visits, and the second one shows as direct.
- A reload or a new tab starts a new visit. A login flow that forces a full page load needs the visit carried in the URL (toParam on the way out, fromParam on the way back), or the source is lost at the worst possible moment.
- A visit that crosses midnight in the app's time zone counts as two.
- People who signed up before you shipped this have no first touch at all.
Why that trade was fine for a study app
The question the app needed answered was narrow: which channel and campaign started the visit that ended in a signup, and later in an order. Almost all of its signups happen in the first visit, often within minutes, on a phone. The returning-visitor blind spot mostly hits long B2B buying cycles, where someone reads your site for three weeks before booking a demo. For a product people try on the spot, the first visit is most of the story. On the other side of the scale was a banner on every landing page, shown to students who had not yet seen what the app does. The app chose the blind spot.
The app's reading of the rules
Here is how the study app reads its own setup. This is its reading, written for its privacy notice, and it is not legal advice; check your own case with someone qualified. Nothing is stored on or read from the device for analytics, so the ePrivacy device-storage rule does not come into play and no consent is asked for. The visit lives in page memory and is gone when the tab closes. For what happens on the server (anonymous events, and the first touch copied onto a person at signup), the app relies on legitimate interest under GDPR Article 6(1)(f), with data minimisation: landing paths lose their query string, the IP address is never stored, click ids are dropped, and the visit id is removed before the person record is written. Users the app knows to be under 16 are opted out automatically. The line it put in its privacy notice reads: "This is kept only in the page's memory while you browse: no cookie or other storage is used, your IP address is not stored, and ad click identifiers (such as fbclid or gclid) are removed." The full setup, including carrying the visit through an email login, is in the traffic attribution guide at /docs/guides/traffic-attribution.
Keep reading
Reading a split funnel
The funnel chart tells you how many got through. The split table under it tells you who. Here is a row by row reading of one, from a study app's mock month.
AnalyticsHybrid mode for privacy: put your own server in the middle
The browser never talks to Vevee. It talks to a forty-line route on the app's own server, which decides what leaves and what does not.