DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

Pixel Helper said the tag fired. Chrome Network never requested facebook.com/tr.

GTM preview said the Meta tag fired. Pixel Helper painted a green PageView. Events Manager is empty, and the ticket says the Facebook pixel is not firing.

Those three tools are not looking at the same object.

The GTM loader is googletagmanager.com/gtm.js?id=GTM-XXXX. That request proves a container script was fetched. It is not a conversion. The Meta collector is www.facebook.com/tr. Google Analytics 4 is google-analytics.com/g/collect with v=2 and a tid that starts with G-. TikTok collection is analytics.tiktok.com and ads.tiktok.com. Pinterest is ct.pinterest.com. LinkedIn Insight is px.ads.linkedin.com/collect. Google Ads image conversions sit on googleadservices.com/pagead/conversion. Floodlight is an activity on doubleclick.net, and a filter for the word pixel will miss it.

A pixel that is not firing means the collector URL is absent, cancelled, blocked by an extension, or never queued. A loader that returns 200 with an empty container is a different bug: a missing container id, an unpublished workspace, or a GTM environment query that selected the wrong container. Pixel not firing is the order of those checks. Chrome Network is where you copy the request. Pixel Helper versus Network is why the extension and the request disagree.

Find the collector, then stop arguing about the loader

Do this on the page where the event should exist. Purchase debugging on the homepage is how a thank-you route that never loaded GTM gets filed as a dead pixel. Hosted checkout on Shopify, Stripe Checkout, or Adyen is often a second origin. Your container does not run there unless someone installed it on that host, or the conversion is a server POST.

  1. Publish the container. Then open a clean profile: no preview cookie, no gtm_debug query, extensions off.
  2. Preserve the Network log across navigations.
  3. Filter by the collector host, not by the word pixel.
  4. If the loader is missing, the snippet never ran. Theme, single-page route, or Content-Security-Policy.
  5. If the loader is present and the collector is missing, the tag did not send: trigger, consent, or a block.
  6. If the collector is present, copy the URL and lint it.
  7. If the collector is present and the vendor UI is empty, wait, then check the event name and the id. The UI lags. The URL does not.

pixellint validate url on the loader, then again on the collector, splits those two bugs. The Google Tag Manager pack flags a missing container id. The Meta pack flags a missing numeric Pixel ID on /tr as vendor.meta.param.id.missing.

$ pixellint validate url 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXX' --rulepack vendor/google-tag-manager
$ pixellint validate url 'https://www.facebook.com/tr?ev=Purchse&em=buyer@example.com'
Enter fullscreen mode Exit fullscreen mode

The second URL is a request that did leave the browser and is still the wrong contract. ev=Purchse is a custom event. It returns 200. Optimization will not treat it as Purchase. vendor.meta.param.ev.invalid warns because a custom name is legal. A raw email in the query is vendor.meta.pii.unhashed_email. A Purchase hit without cd[value] and cd[currency] is an incomplete conversion. These are "pixel not tracking" tickets where Network is full of 200s. Copy the URL, redact em= and cookies, and paste it. Do not rotate the pixel id because the spelling is wrong.

Preview fired is not Network sent

GTM preview attaches a debug cookie to a draft. Users without that cookie get the published container. Shipping on preview alone is how an unpublished tag looks fine in the office and absent in production. GTM preview is that split. Submit. Publish. Then a clean browser.

Preview can also list a tag as fired when the request was blocked, cancelled, or still queued. Pixel Helper showing a PageView does not prove Purchase fired. Pixel Helper showing nothing does not prove the tag is unpublished. An extension may have eaten /tr. Disable the blocker on the QA profile or you will debug an EasyList entry and call it a container bug.

Agencies leave old versions live. A rollback reintroduces Purchse. Confirm which version you published before you rewrite the trigger.

The request can die after the tag runs

EasyList and uBlock Origin match facebook.com/tr, google-analytics.com/g/collect, and ads.tiktok.com by host. The list does not care whether the request came from fbq or from an image inside noscript. If the host is listed, both transports die. A noscript image is not a bypass. Ad blockers is that limit. Recovery is a server event or a first-party collector you operate, not a second image to the same listed host.

Content-Security-Policy splits the same way. script-src has to allow the tag host (connect.facebook.net, www.googletagmanager.com, analytics.tiktok.com) or the JavaScript never runs. img-src has to allow the image host (www.facebook.com, ad.doubleclick.net, px.ads.linkedin.com) or the 1x1 never loads. connect-src matters for fetch and sendBeacon. Forgetting it looks like an ad blocker in the Network tab. A red request whose initiator is chrome-extension is an extension. A request that never appears is often CSP, or a tag that never queued.

Consent is the other empty tab. A banner that does not block still fires the pixel. A banner that never grants ads storage looks like a pixel that is not firing. Check Network with the banner denied, then granted, on a fresh profile. Custom HTML in GTM can ignore Consent Overview: preview shows the tag as fired, and a denied browser should not show /tr if you meant to wait. gdpr_consent=1 is valid base64 and not a TC String. That is a wire bug, not a missing loader.

Wrong page, and two installs

PageView belongs on the landing page, where click ids are stored. Purchase belongs on the thank-you page, once, after the money is real. Firing Purchase on every landing inflates ROAS until the platform discounts you. A Lead on page load of /contact is not a lead.

Single-page apps do not reload the document. If you only fire on first load, checkout is invisible. Hook history. React 18 Strict Mode double-mounts and will double Purchase.

Two PageViews per load come from a base code in the theme plus GTM's Meta tag, or from a History Change trigger plus All Pages. Network initiators show both. Pixel Helper shows both. Ads Manager shows both, unless event_id dedups them. Remove one install. Two GTM loaders are two containers. Staging GTM-XXXX on production, or the reverse, is how QA purchases land in Ads Manager or how production looks empty. GTM environments exist to swap those ids.

Other collectors, same order

TikTok: the loader and the collection host need sdkid. lib is expected to be ttq. The Events API is a JSON POST the Network tab will not show. Pinterest: ct.pinterest.com needs tid. Standard events are lowercase (pagevisit, checkout, addtocart). Checkout is a custom event. LinkedIn fallbacks look like px.ads.linkedin.com/collect?pid=...&fmt=gif. Floodlight puts src, type, cat, and ord on the path. Google Ads puts the conversion id in the path and selects the action with label. One unknown host is attribution, not a finding. Lint the URL with the pack that owns that host.

If Network never shows /tr for a large share of users, another image tag will not start it. Send the order from the backend, and lint that JSON the same way you lint the URL. Keep one /tr fixture and one server body in CI. A weekly screenshot of preview is not a contract test.

The playground at pixellint.org runs the same engine as pixellint validate. Paste the collector URL, not the GTM snippet. Artifacts you test there may be stored; the privacy note says so. cargo install pixellint and npm install pixellint do not send the URL anywhere. I maintain that package. It is independent of Meta and Google. It does not replace Pixel Helper. Pixel Helper watches the page. The package checks the request the page was supposed to send.

Top comments (0)