First-party attribution: track your visitors without third-party cookies
You launch a campaign, you see 400 visitors in Google Analytics, 12 sign-ups… and you can't tell which ones came from which source. Welcome to attribution after third-party cookies, where 30 to 50% of your traffic has gone invisible. The good news: you don't need GA or third-party pixels. A pixel served from your own domain is enough to rebuild the chain visit → sign-up → revenue, and it survives everything that breaks the usual trackers.
Why the classic solutions collapse
Third-party pixels (Meta Pixel, Google's `gtag.js`, Hotjar…) set a cookie or read an identifier from a domain different from yours. That's exactly what browsers decided to kill. Three forces converge and you're already feeling them:
- Safari / ITP: a cookie set in JavaScript (`document.cookie`) is capped at 7 days, and at 24h if the visitor arrives via a link with tracking parameters. Your 30-day attribution is dead before it even starts.
- Third-party cookies blocked: Safari and Firefox have blocked them by default for years. Chrome backtracked on full removal in 2024, but rolled out an opt-out that still drains the reservoir.
- Strict consent (GDPR): without an accepted banner, GA and marketing pixels must not load. The result: a gaping hole in your data, and a hole that isn't random (refusals correlate with certain profiles).
What these three breakages have in common: they target the third party. If the identifier comes from your domain and serves only you, most of these rules don't apply the same way.
The principle: a first-party pixel + an anonymous visitor_id
The idea comes down to three pieces. You serve a collection endpoint on your own domain (ideally a subdomain like `t.yourapp.com`, not an external tracking domain). On the first visit, you generate a random `visitor_id` and store it in a first-party, HttpOnly, Secure cookie. On every page view, the browser sends this cookie back to your server, which logs the event. The detail that changes everything: the cookie is set by a server-side HTTP `Set-Cookie` response, not by `document.cookie` in JavaScript. This is the nuance that gets around ITP's 7-day cap — the limit targets cookies set in JS, not those set by the server first-party. And `HttpOnly` makes them unreadable by any script, so useless for theft via XSS.
# Response from your /t/collect endpoint served on t.yourapp.com
HTTP/1.1 204 No Content
Set-Cookie: vid=8f3c1a9e-...; Max-Age=63072000; Path=/;
Domain=.yourapp.com; HttpOnly; Secure; SameSite=Lax
// pixel.js client-side (~10 lines):
// fetch("https://t.yourapp.com/t/collect", {
// method: "POST", credentials: "include", keepalive: true,
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ path: location.pathname,
// ref: document.referrer, utm: location.search, ts: Date.now() }) });
The merge: from anonymous visitor_id to proven identity
As long as the visitor is anonymous, you accumulate events tied to an opaque `visitor_id`. The magic happens at sign-up: the user proves their email (confirmation link or OTP), and you do the stitching — you link the cookie's `visitor_id` to the freshly created account. At that moment, the whole anonymous history from before (first visit, source, page views) attaches retroactively to a real identity.
- At sign-up, read the `vid` cookie server-side in the same request.
- Write a row `identity(visitor_id, user_id, email_verified_at)` — the email proven, not just typed in.
- Re-attach all anonymous events carrying that `visitor_id` to the `user_id`. You get the full path: Twitter → /pricing → /signup → email confirmed.
- Bonus: if the same email logs back in from another device, merge two `visitor_id`s under a single `user_id`.
What you gain vs Google Analytics and third-party pixels
- Long-window attribution: your server cookie can target 2 years instead of 24h. For a B2B funnel where the decision takes 3 weeks, that's the difference between attributing a sale and losing it in the noise.
- Complete data: you log server-side, so the blockers (uBlock, Brave) that eat `gtag.js` don't touch you. You see your real traffic, not the version riddled with holes by extensions.
- Direct join with your revenue: the `visitor_id` is in your database, next to your `users` and `subscriptions` tables. You run a SQL `JOIN` between a source and real MRR — impossible with GA4's anonymized aggregate.
- Easier compliance scoping: a strictly first-party identifier, for internal measurement purposes, is easier to justify than handing the data to Google and Meta. To validate with your lawyer, but the scope is radically cleaner.
- No sampling or cardinality threshold: GA4 samples and masks segments < ~50 users. On your own logs, every row exists.
The traps that break the setup in production
- Serve the pixel from a subdomain that's truly yours: a CNAME to a known tracking domain (like `.analytics-vendor.com`) gets reclassified as third-party by ITP, which re-imposes the 7-day cap. The subdomain must resolve to your* infra.
- `SameSite=Lax` minimum: with `None` for no reason, you reintroduce third-party behavior. `Lax` covers the normal case (click toward your site).
- Idempotent stitching: the same `visitor_id` can arrive twice (double-submit, retry). Unique key on `(visitor_id, user_id)`.
- Strip PII from raw logs: don't log the IP in plaintext forever. Truncate or hash it; keep the IP just long enough for geo/anti-fraud.
- The "never signed up" case: 95% of visitors will never merge. That's normal. You keep the anonymous aggregate for the top-of-funnel; the merge only concerns those who converted.
Start small: one endpoint, one HttpOnly `Set-Cookie` served on your subdomain, an `events(visitor_id, path, ref, ts)` table and an `identity(visitor_id, user_id)` table populated when the email is proven. In one afternoon you'll have attribution that survives Safari, blockers and consent — and that plugs directly into your MRR. Once it's running, add multi-device and source counting. The rest is just SQL.
The newsletter
By subscribing you agree to receive the Zylior newsletter. One-click unsubscribe in every email.