Pixels · engineers · marketers
Say whether it has fired
Pixellint's artifact state is unknown, template, or fired. Template may contain macros. Fired may not. Unknown stays conservative. Pick the state that matches the paste. Validating a GAM creative as fired will fail every [CACHEBUSTER]. Validating a HAR as template will pass a dead token.
Three states
unknown is the default when you did not say. Macro rules stay conservative: they will not assume the token was supposed to expand, and they will not assume it was supposed to remain. You get core URL checks, vendor parameter checks, and less certainty on macros. Do not leave production CI on unknown because it feels safer. It is vaguer.
template means this is what the ad server or MMP stores. [NAME], ${NAME}, and {{NAME}} are expected in legal slots. Empty consent fields are unfilled slots. fired means this is what left the device or the server. Unexpanded macros are errors. Empty consent on a live GDPR hit is a missing signal. Pass --state on the CLI so the engine does not guess.
Where templates live
GAM creatives, Campaign Manager tags, email ESP image URLs, MMP partner templates, GTM custom pixel fields that still have {{dlv-order-id}}. Copying from the ad server UI is a template even if it looks like a URL. A Slack message from trafficking with ord=[timestamp] is a template. A Jira attachment of the Floodlight tag as sold is a template.
Store them next to the app as fixtures. Validate with pixellint validate url @fixtures/floodlight-template.txt --state template. The point is to catch unsafe positions, mixed syntax, missing required fields that are not macros (src, type, cat), and vendor typos, without punishing [timestamp] for existing.
Where fired URLs live
Chrome Network, Charles, HAR, server access logs, MMP raw data, sGTM preview. If you see a real cache buster value (ord=1724284800123), it has fired. If you see [TIMESTAMP], it has not. If you see both in one URL, something expanded one macro family and not the other. That is mixed syntax plus a partial serve, and it is a fired miss.
A HAR from staging is a fired fixture. Redact PII before you commit it. Validate those URLs as fired. The point is to catch the serve path that left the token in, the http upgrade that never happened, the missing cd[currency] on a real Purchase, the noscript=yes typo that only exists in the live GTM container.
CI wants two fixtures
One job, two states, two fixtures. Run pixellint validate url on the template with --state template, and pixellint validate url on the HAR-extracted URL with --state fired. For CAPI bodies, pixellint validate json on a redacted production-shaped payload. Exit 1 on error-severity findings is the gate.
A weekly HAR archaeology session does not catch the merge that shipped Date.now() as event_time in milliseconds. The fired JSON fixture does. A template fixture does not catch it, because that bug is not in the template. If you only store templates, you only test trafficking. If you only store HARs, you only test one serve and you fail every legal macro in GAM. Store both.
Unknown is not a third product
Paste in the playground without a state and you get unknown. That is fine for a first look. It is not fine as the only CI mode. Teams that leave unknown everywhere either ignore macro findings or argue about them every sprint. Set the state on the fixture, in the command, not in a comment in the URL.
Core still applies in all three states: absolute URL, host present, http or https, no userinfo, fragments ignored, http flagged for upgrade. Vendor packs still apply when the host matches. State only changes macro and empty-template behavior. A fired Meta Purchase without id is still vendor.meta.param.id.missing. Saying template will not invent an id.
pixellint validate url 'https://example.com/pixel?cb=[CACHEBUSTER]' --state template
pixellint validate url 'https://example.com/pixel?cb=1724284800123' --state fired
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.