A technical post-mortem on why we stopped trusting ad platforms and built our own measurement layer
TL;DR: Ad platform pixels lose 30-40% of conversion signals to iOS and ad blockers. The attribution models platforms use are self-serving fiction. We built pre-block capture + server-side fractional attribution to fix it. Here's exactly what we found and how it works.
# We Rebuilt Ad Attribution From Scratch. Here's What We Found.
*A technical post-mortem on why we stopped trusting ad platforms and built our own measurement layer*
---
**TL;DR**: Ad platform pixels lose 30-40% of conversion signals to iOS and ad blockers. The attribution models platforms use are self-serving fiction. We built pre-block capture + server-side fractional attribution to fix it. Here's exactly what we found and how it works.
---
## The Problem We Couldn't Ignore
We kept seeing the same thing across brands: Shopify says 241 orders. GA4 says 202. Meta says 149.
That's not a rounding error. That's your ad platform making bidding decisions based on 62% of actual conversions. Paying for Advantage+ optimization that's training on incomplete data.
The root cause is two things hitting at once:
1. **Ad blockers** kill outbound pixel requests at the network level. uBlock, Brave, Safari's built-in protection — all intercept connect.facebook.net, google-analytics.com, and similar domains.
2. **iOS ITP** (Intelligent Tracking Prevention) deletes client-side cookies after 7 days. A user who clicked your TikTok ad on day 1, browsed on day 6, and bought on day 8 is completely invisible to TikTok's pixel by the time of purchase.
---
## The Pre-Block Capture Architecture
The key insight: ad pixels attach to browser globals. window.fbq, window.gtag, window.ttq. These get called the exact millisecond a conversion event fires.
If you wrap those functions at the JS level, you own the payload before the network stack touches it:
```javascript
function wrapPixel(globalName) {
const original = window[globalName];
if (!original) return;
window[globalName] = function(...args) {
// Capture locally — before any network request
// No ad blocker can intercept a local function call
queue.push({ platform: globalName, payload: args, ts: Date.now() });
// Let original proceed (will likely get blocked — doesn't matter)
return original.apply(this, args);
};
}
// Pixels initialize asynchronously, so we poll
const huntingLoop = setInterval(() => {
['fbq', 'gtag', 'ttq', 'pintrk', 'snaptr'].forEach(name => {
if (window[name] && !wrapped.has(name)) {
wrapPixel(name);
wrapped.add(name);
}
});
}, 500);
setTimeout(() => clearInterval(huntingLoop), 5000);
```
The queue flushes to your own first-party endpoint. Because this is your own domain, ad blockers can't block it without breaking your entire site. From your server, you forward to Meta CAPI, Google Measurement Protocol, TikTok Events API, with deduplication to avoid double-counting.
---
## The ITP Problem: Server-Side Identity Persistence
For iOS, the pixel-wrapping approach gets you the event payload. But you still need to link it to the user's prior browsing session — the TikTok click from day 1.
The fix: set identity cookies via Set-Cookie response header, not document.cookie.
- document.cookie → ITP deletes after 7 days (or sooner)
- Set-Cookie header with HttpOnly; Secure; SameSite=Lax → survives ITP indefinitely (first-party server-side cookie)
When a user first hits your site from any ad click, your edge function intercepts the request, reads click ID params (fbclid, gclid, ttclid, etc.), and sets a durable first-party cookie. On conversion 21 days later, that cookie is still there.
---
## Fractional Attribution: The Model We Use
With complete server-side data, we apply time-decay attribution with behavioral weighting:
```
credit(touch) = recency_weight × engagement_weight / Σ(all_touches)
recency_weight = e^(-λ × days_before_conversion) [λ = 0.1, half-life ~7 days]
engagement_weight = 1 + (signals_fired × 0.15)
where signals = cart_add, size_guide_view, review_dwell, etc.
```
A TikTok click with 4 minutes of review reading + cart add, 3 days before purchase, gets substantially more credit than a Meta view impression 30 days out.
---
## The Behavioral Signal Layer
Standard analytics cares about buyers. But 98% of visitors don't convert on day 1 — many are high-intent and just need time or a nudge.
We auto-detect 26 behavioral signals:
**High-intent signals**: cart view, checkout start, pricing dwell >30s, image zoom, size guide view, 3+ gallery images, review dwell >20s
**Friction signals**: payment error, rage click on checkout, coupon failure, cart abandonment, speed scroll (skimming not reading)
When pushed to Meta CAPI and Google Measurement Protocol as custom events, you're feeding the algorithm data it's never had — optimizing toward people who are about to convert, not just people who already did.
---
## What We Built vs. DIY
We're [Zyro](https://zyro.world) — we packaged this into a single script: auto-intercepts 20+ pixels, server-side ITP-resistant identity, 90+ attribution sources (including AI answer engines like ChatGPT), 26 behavioral signals, dispatches to 11+ ad platforms.
But you can DIY the core:
- Pixel wrapping + first-party forwarding: ~2-3 days
- Server-side identity persistence: ~1 day
- Basic time-decay attribution model: ~1-2 days
- Behavioral event capture: ~2-3 days per category
- CAPI/Measurement Protocol: ~1 day per platform
DIY gets you 70% of the value. The remaining 30% is in the edge cases: deduplication logic, cross-domain tracking, multi-currency reconciliation.
---
## Benchmark Numbers (7-Day Window)
| Platform | Reported | Actual | Signal Loss |
|----------|----------|--------|-------------|
| GA4 | 202 orders | 241 | 16% |
| Meta | 149 orders | 241 | 38% |
| TikTok | varies | 241 | 40-50% typical |
Meta was missing 38% of purchases. You're optimizing toward the 62% Meta can see — which is not a random sample.
---
## Open Questions / Still Figuring Out
- **Cross-device attribution**: Linking mobile ad click to desktop purchase is still hard. We use probabilistic matching (IP + UA + timing) — imperfect.
- **Consent frameworks**: GDPR/CCPA compliance with server-side tracking requires passing consent signals server-side before dispatching.
- **AI engine detection**: ChatGPT, Perplexity, and Claude drive real referral traffic that GA4 buckets as direct. We've built detection for 8 AI engines but it needs ongoing maintenance.
Happy to compare notes on attribution architecture, server-side tracking edge cases, or the behavioral signal detection layer. Drop a comment or find me at [zyro.com](https://zyro.world).
0
6
0