pixellint

Pixels · engineers · marketers

PageView fired twice on one load

Dashboards that show twice the sessions of server logs are usually two tags, not twice the traffic. The Network tab does not lie. Two /tr?ev=PageView or two g/collect page_view hits are two tags. Strict Mode and History Change plus Container Load are the other usual pair.

fbq plus GTM, gtag plus GTM

Hard-coded gtag in the theme plus GTM injecting the same tag. Hard-coded fbq plus a GTM Meta template. Shopify pixel plus a custom pixel plus GTM. Each init stamps a PageView. You cannot dedup what you never gave an event_id, and PageView often has none. Remove one installer. Keep GTM or keep the theme snippet, not both.

GTM preview will show two Meta tags or two GA4 Config tags. Network will show two ids or the same id twice. Same id twice still double-counts pageviews. A second pixel id is a second property, which is a different disaster (split reporting) rather than a duplicate on one property. Search the HTML for gtm.js, gtag/js, fbevents.js, and the Shopify pixel bootstrap. Count to one.

React Strict Mode

React 18 Strict Mode mounts, unmounts, remounts in development. If you put fbq('track', 'PageView') in a useEffect with an empty dependency array and no guard, dev duplicates. Production builds do not remount that way, so prod looks clean and staging looks like a pixel bug. Guard with a module-level flag, or fire from a router subscription that is not an effect on the root layout.

Purchase is worse: finance sees 2x revenue in staging Events Manager. If event_id is a UUID created inside the effect, both fires look unique and CAPI cannot collapse them. If event_id is the order id, they collapse. Still fix the double fire; dedup is a backstop, not a license to mount twice. Next.js App Router plus Strict Mode plus a client layout pixel is the usual reproduction.

History Change plus Container Load

Container Load fires PageView on the first document. History Change fires PageView on pushState. Many SPAs emit a pushState or replaceState during bootstrap for the first route. The first route therefore counts twice. Exclude the first history event (a custom trigger that ignores historyChange when the pathname has not changed from the original), or fire only from the router and not from Container Load.

replaceState for query flags (modal=1) will keep firing History Change if you did not filter. You then get a pageview per toggle. Filter on pathname, or on a dataLayer event the router pushes with a stable page_id. GTM's built-in History Change is a blunt instrument. A dataLayer event named virtual_pageview that you emit once per real route is a sharp one.

How to see it

Chrome Network, filter tr, g/collect, collect?. Two rows with en=page_view or ev=PageView on one document load is the bug. Compare timestamps; if they are milliseconds apart, it is not two users. Look at cid and tid on GA4: same cid, two hits, one load. Look at id on Meta: same numeric pixel id, two PageView. GTM preview's Tags fired tab should match the row count. If preview says one and Network says two, a tag outside the container is the second row.

GA4 DebugView and Meta Test Events will also double. They are not sampling. Do not turn on debug_mode in production to investigate duplicates; you will pollute DebugView and possibly reporting. Use a staging pixel id or a test_event_code, and a clean profile with one tab.

Dedup is the wrong first fix for PageView

event_id dedup is designed for conversion events that you intentionally send twice (pixel plus CAPI). PageView sent twice from two browser tags is not that pattern. Meta may not collapse two PageViews even with the same event_id the way it collapses two Purchases. Remove the extra tag. Fix Strict Mode. Fix History Change. Then use event_id on Purchase.

If you must keep two browsers tags for a migration week, send event_id on both and accept that pageview reports will be messy. Do not also add CAPI PageView without the same id. Three PageViews (theme, GTM, CAPI) is a migration that never ended. Calendar it. Validate a HAR from before and after with pixellint validate url if you care about the URL shape; the duplicate count you still have to read in Network.

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.