The Tracking Stack That Survives iOS, Ad Blockers, and Cookie Death
You're Missing Half Your Data
Run this test right now: compare your Google Analytics session count against your server access logs. If you're like most companies, GA is showing 30-50% fewer sessions than actually occurred.
That gap is growing every year:
- iOS App Tracking Transparency: 75-85% of users opt out
- Safari ITP: Caps first-party cookies at 7 days
- Ad blockers: 30-40% of desktop users block tracking scripts
- Firefox Enhanced Tracking Protection: Blocks third-party cookies by default
- Chrome Privacy Sandbox: Third-party cookies deprecated
Your client-side tracking is dying. The question isn't whether to move server-side — it's how fast.
The Architecture: First-Party, Server-Side, Everything
Old architecture (dying):
Browser → Third-party pixels (blocked)
Browser → Google Analytics JS (blocked by 30%)
Browser → Facebook Pixel (blocked by 40%)
Result: 50-70% data coverage
New architecture (resilient):
Browser → Your first-party domain → Your server
Your server → Facebook CAPI
Your server → Google Measurement Protocol
Your server → Your analytics database
Your server → Any other destination
Result: 95-99% data coverage
The Key Insight
When tracking goes through YOUR domain (first-party), it can't be blocked by ad blockers or privacy tools. The browser sees it as a normal request to your own website.
// Client-side: sends to YOUR domain, not a third party
fetch("/api/track", {
method: "POST",
body: JSON.stringify({
event: "page_view",
url: window.location.href,
referrer: document.referrer,
timestamp: Date.now(),
sessionId: getSessionId(), // First-party cookie
}),
});
// Server-side: fans out to all destinations
app.post("/api/track", async (req, res) => {
const event = validateEvent(req.body);
const userId = resolveIdentity(req);
// Send to all destinations in parallel
await Promise.allSettled([
storeInDatabase(event, userId),
sendToGoogleMP(event, userId),
sendToFacebookCAPI(event, userId),
sendToAmplitude(event, userId),
]);
res.status(200).send();
});Building the First-Party Tracking Stack
Layer 1: Identity Resolution
The foundation of everything. Without consistent identity, your data is noise:
function resolveIdentity(req: Request): UserIdentity {
// Priority order for identity resolution
const identity: UserIdentity = {
// Authenticated user (strongest signal)
userId: req.user?.id || null,
// First-party cookie (persists across sessions)
deviceId: req.cookies["_device_id"] || generateDeviceId(),
// Session ID (current visit)
sessionId: req.cookies["_session_id"] || generateSessionId(),
// Hashed email (for ad platform matching)
hashedEmail: req.user?.email
? sha256(req.user.email.toLowerCase().trim())
: null,
// IP-based fingerprint (fallback, less reliable)
fingerprint: generateFingerprint(req),
};
return identity;
}Cookie strategy:
_device_id: First-party, HttpOnly, 1-year expiry, SameSite=Lax_session_id: First-party, HttpOnly, 30-minute sliding expiry- Set from your server on your domain — immune to ITP and ad blockers
Layer 2: Event Collection
// Standardized event schema
interface TrackingEvent {
// Required
event: string; // "page_view", "add_to_cart", "purchase"
timestamp: number; // Unix ms
sessionId: string;
// Identity (at least one required)
userId?: string;
deviceId?: string;
// Context
url?: string;
referrer?: string;
userAgent?: string;
ip?: string; // For geo lookup, then discard
// Event-specific properties
properties?: Record<string, unknown>;
}
// Server-side validation and enrichment
function processEvent(raw: TrackingEvent): EnrichedEvent {
return {
...raw,
// Enrich with server-side data
geo: geoLookup(raw.ip),
device: parseUserAgent(raw.userAgent),
// Add server timestamp (client clocks can't be trusted)
serverTimestamp: Date.now(),
// Deduplicate
eventId: generateEventId(raw),
};
}Layer 3: Server-Side Destinations
// Facebook Conversions API
async function sendToFacebookCAPI(event: EnrichedEvent) {
if (!["purchase", "add_to_cart", "lead"].includes(event.event)) return;
await fetch(
`https://graph.facebook.com/v19.0/${FB_PIXEL_ID}/events`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: [{
event_name: mapToFBEvent(event.event),
event_time: Math.floor(event.timestamp / 1000),
event_source_url: event.url,
action_source: "website",
user_data: {
em: [event.hashedEmail],
external_id: [sha256(event.userId || event.deviceId)],
client_ip_address: event.ip,
client_user_agent: event.userAgent,
fbc: event.properties?.fbc, // Facebook click ID
fbp: event.properties?.fbp, // Facebook browser ID
},
custom_data: event.properties,
}],
access_token: FB_ACCESS_TOKEN,
}),
}
);
}
// Google Measurement Protocol (GA4)
async function sendToGoogleMP(event: EnrichedEvent) {
await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${GA_ID}&api_secret=${GA_SECRET}`,
{
method: "POST",
body: JSON.stringify({
client_id: event.deviceId,
user_id: event.userId,
events: [{
name: mapToGAEvent(event.event),
params: {
...event.properties,
session_id: event.sessionId,
engagement_time_msec: 100,
},
}],
}),
}
);
}Layer 4: Your Own Analytics Database
Don't rely solely on third-party platforms. Store your own data:
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_name VARCHAR(100) NOT NULL,
user_id VARCHAR(255),
device_id VARCHAR(255) NOT NULL,
session_id VARCHAR(255) NOT NULL,
url TEXT,
referrer TEXT,
properties JSONB,
geo_country VARCHAR(2),
geo_region VARCHAR(100),
device_type VARCHAR(50),
browser VARCHAR(100),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
server_timestamp TIMESTAMP NOT NULL
);
CREATE INDEX idx_events_user ON events (user_id, created_at);
CREATE INDEX idx_events_session ON events (session_id, created_at);
CREATE INDEX idx_events_name ON events (event_name, created_at);Why own your data: Platforms change APIs, raise prices, and sunset products. Your own event database is the one source of truth you control.
The Migration Checklist
Week 1: Set Up First-Party Collection
- Create
/api/trackendpoint on your domain - Implement first-party cookie identity
- Collect page views and key events server-side
- Run in parallel with existing client-side tracking
Week 2: Add Server-Side Destinations
- Facebook CAPI (with event deduplication)
- Google Measurement Protocol
- Store events in your own database
Week 3: Validate and Compare
- Compare server-side vs client-side event counts
- Verify identity resolution across sessions
- Check ad platform match rates (should improve)
Week 4: Optimize and Expand
- Add remaining event types
- Remove redundant client-side pixels
- Build basic analytics queries against your database
- Set up data quality monitoring
The Results
| Metric | Client-Side Only | First-Party Server-Side |
|---|---|---|
| Event capture rate | 55-70% | 95-99% |
| Cross-session identity | 40-60% | 85-95% |
| Ad platform match rate | 30-50% | 70-85% |
| Data latency | Real-time | Real-time |
| Resilience to privacy changes | Low | High |
| Data ownership | Platform-dependent | You own it |
The tracking landscape will keep getting more restrictive. Every privacy regulation, browser update, and OS change will erode client-side tracking further. The companies that invest in first-party, server-side infrastructure now will have a compounding data advantage over those that keep patching client-side pixels.
Build the stack that survives. Your future self will thank you.