A small browser helper that remembers where the current visit came from: the utm_* tags, the referrer host and the landing path. It keeps that in a JavaScript variable and nothing else: no cookie, no localStorage, no sessionStorage, no IndexedDB. A reload or a new tab starts a new visit. Added in @vevee/sdk 1.14.0.
import { attribution } from '@vevee/sdk/browser';send callback: usually your own server route, which forwards to Vevee with a secret key (the hybrid proxy pattern), or vevee.analytics.capture with a pk_* key.Types
interface VisitContext {
visit_id: string; // random UUID, generated in memory
started_at: string; // ISO 8601
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_term?: string;
utm_content?: string;
referrer_host?: string; // host only, never the full referrer URL
landing_path: string; // window.location.pathname, no query string
}
type VisitProperties = Omit<VisitContext, 'started_at'>;
interface PageviewEvent {
event: '$pageview';
properties: VisitProperties & { path: string };
}
type AttributionSend = (e: PageviewEvent) => void | Promise<void>;What it reads
- UTM tags from the landing URL's query string:
utm_source,utm_medium,utm_campaign,utm_term,utm_content. Each is trimmed and capped at 200 characters; empty values are dropped. - The referrer host from
document.referrer, reduced to its host. It is left out when the referrer is your own site. - The landing path:
window.location.pathname, without the query string or fragment.
It never reads click identifiers such as fbclid or gclid, and it reads the URL once, when the visit starts.
attribution.start()
attribution.start(opts?: { send?: AttributionSend }): VisitContext | nullStarts the visit on the first call: generates a random visit_id, records started_at and reads the URL and referrer. Later calls return the same visit, so it is safe to call from several components. Passing send sets (or replaces) the callback used by pageview().
Returns null on the server (no window) and in a browser with no crypto at all. Where crypto.randomUUID is missing, the id is built from crypto.getRandomValues.
Once started, the helper publishes the visit_id on a global symbol. An anonymous vevee.analytics.capture() on the same page picks it up and adds it as visit_id.
attribution.pageview()
attribution.pageview(path?: string): PageviewEvent | nullBuilds a $pageview event with path (defaults to the current pathname) plus the visit properties, hands it to send if one is set, and returns it. Errors thrown by send, and rejected promises, are swallowed: a failing transport never breaks the page. Returns null on the server. Call it on every route change.
attribution.current()
attribution.current(): VisitContext | nullThe whole visit, including started_at, or null if start()never ran. Send it with the signup so your server can store it as the person's first touch.
attribution.properties()
attribution.properties(): VisitProperties | Record<string, never>The visit without started_at, ready to spread into the properties of any anonymous event. An empty object before start().
Carrying a visit across a full page load
Memory does not survive a reload. A magic link, an OTP verify page, an OAuth redirect or a link to another subdomain would start a new visit and lose the source. Carry the visit in the URL instead.
attribution.toParam()
attribution.toParam(): string | nullThe current visit encoded as a URL-safe string (a v1. prefix plus base64url JSON). Returns null before start(), or when the encoded value would be longer than 1024 characters.
attribution.fromParam()
attribution.fromParam(s: string | null | undefined): booleanRestores a carried visit. It rewrites the carried started_at to ISO 8601 form and rejects one that is not a valid date. It adopts the carried visit when it started at or before the current one (a tie adopts), and returns true only when it adopted it. It returns false for missing or malformed input, input over 1024 characters, on the server, or when the current visit is earlier. Earlier wins, so the original source beats whatever the reloaded page saw, such as a webmail host as the referrer of a magic-link click.
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { attribution, type PageviewEvent } from '@vevee/sdk/browser';
async function send(e: PageviewEvent): Promise<void> {
await fetch('/api/analytics/capture', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(e),
keepalive: true,
});
}
export function SiteAnalytics() {
const pathname = usePathname();
useEffect(() => {
attribution.start({ send });
const url = new URL(window.location.href);
const vv = url.searchParams.get('vv');
if (vv !== null) {
attribution.fromParam(vv);
// Remove it, so a shared or bookmarked URL never merges two people's visits.
url.searchParams.delete('vv');
window.history.replaceState(window.history.state, '', url);
}
}, []);
useEffect(() => {
attribution.pageview(pathname ?? undefined);
}, [pathname]);
return null;
}
// Before leaving for the verify page or the OAuth provider:
export function withVisit(target: URL): URL {
const vv = attribution.toParam();
if (vv) target.searchParams.set('vv', vv);
return target;
}vv is only a convention; use any query name, as long as both sides agree.
Privacy
- No cookie, no
localStorage, nosessionStorage, no IndexedDB, ever. - The visit dies with the tab. Each reload without a carried value is a new visit.
- The
visit_idis random and short-lived. Vevee never stores it raw: it keeps only a hash with a per-day salt that is deleted within about 48 hours. See Privacy & GDPR.