Analytics · PMs · engineers · marketers
Product analytics and ads conversion are different jobs
PostHog can be right when Meta is empty: ad blockers, missing click ids, no hash. Meta can be right when PostHog is empty: server CAPI on a backend event the client never sent.
Different failures
Analytics: identity splits, missing pages in SPAs, epoch timestamps on ISO fields, empty distinct_id that still 200s. Ads: match quality, event_id, consent, click ids, hashed email. A single GTM container can cause both, but the fix is rarely one tag. Do not close a Meta ticket because PostHog shows the funnel. Do not close a PostHog ticket because Ads Manager shows purchases.
Ad blockers punch the browser pixel and often the analytics tag too. CAPI still sees the order. Client-side PostHog does not. That disagreement is architecture, not a bug in either vendor.
Different PII rules
Product tools often keep email in the clear for support and people search. Ads conversion APIs want SHA-256 of a normalized address. Do not run the ads hasher on the analytics destination by accident. Do not send raw email on facebook.com/tr. IP and user-agent stay plaintext on CAPI; hashing them kills match quality.
Engineer hashing is an allowlist: em, ph, and the documented advanced matching fields. client_ip_address, client_user_agent, event_source_url, fbp, fbc do not get SHA-256. A generic hash all strings helper is how Event Match Quality dies and how PostHog people search dies, in opposite directions.
fbq('track', 'Purchase', {value: 19.99, currency: 'USD'}, {eventID: 'T12345'});
// CAPI: same string in event_id, hashed em/ph, plaintext IP and UA
// PostHog: distinct_id user-123, timestamp ISO, email only if you meant to
Different success
A PM's activation metric may be tutorial_complete. UA's conversion may be Purchase. Both can live in one dictionary with two columns: product and ads. GA4 recommended purchase is not Meta Purchase is not PostHog purchase. Value definitions differ (with tax or without, shipping or not). ROAS fights itself when CAPI value disagrees with the pixel.
Dedup is an ads problem (event_id on both pipes). Distinct_id is a product problem (one person). transaction_id is a GA4 purchase dedup problem. Do not reuse one UUID field name for all three without a map.
Two pipes on ads, one stream on product
Meta, TikTok, Pinterest, LinkedIn want browser tag plus server API, same event_id (or the vendor's spelling: fbq eventID, Pinterest event_id required, LinkedIn URN, TikTok ISO clock). Product analytics is usually one collect path. Copying the redundant ads setup into PostHog double-counts. Copying the single PostHog setup into Meta undercounts when the tag is blocked.
Clocks disagree. Meta and Pinterest CAPI: seconds. Amplitude and LinkedIn: milliseconds. GA4 MP: microseconds. PostHog, Segment, TikTok, Braze: ISO 8601. The ads helper that divides Date.now() by 1000 is correct for Meta and wrong for Amplitude.
Validate both jobs
pixellint validate url on the pixel. pixellint validate json on CAPI, MP, capture, and track. The packs are per vendor. A green PostHog capture does not lint facebook.com/tr. See CDP versus pixels for routing. See vendor playbooks for both pipes on one ads platform.
Pixellint is not affiliated with Google, PostHog, or Meta. Product being right and ads being empty (or the reverse) is a common healthy state during a CAPI rollout. It is not a reason to delete the other pipe.
pixellint validate json @capture.json --rulepack vendor/posthog
pixellint validate url @tr.txt --rulepack vendor/meta
pixellint validate json @capi.json --rulepack vendor/meta-conversions-api
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.