pixellint

Consent · engineers · PMs

Event logs are PII adjacent

Debug culture loves full payloads. Privacy culture loves deletion. Write a retention number on the collector the same day you write the collector.

What to keep

Event name, time, event_id, vendor response code, maybe a truncated IP. That is enough to debug 200 versus dropped. You do not need the email in CloudWatch to know hashing failed. You need a boolean from a linter in CI. You hashed the email on the way to Meta and still saved the raw request in a retry table.

Segment, warehouses, and CDC streams multiply this. A track call with traits.email and context.ip is personal data in every destination you enabled, including ones nobody on the ads team remembers. Turning the pixel off does not drain those pipes. Infinite BigQuery for g/collect is a second CRM.

{
  "writeKey": "YOUR_WRITEKEY",
  "type": "track",
  "event": "Order Completed",
  "userId": "user-10492",
  "timestamp": "2026-08-21T18:04:00Z",
  "properties": { "revenue": 129.99, "currency": "USD" },
  "context": { "ip": "203.0.113.42" }
}

HAR and Slack

A HAR from a real purchase is a secret. Browser HARs contain the full pixel URL. If the URL had an email, the HAR has an email. Chrome, Charles, and a pasted Pixellint playground session are all copies. Treat the channel as production. Redact, or use staging ids.

Test Events, debug_mode, and playground pastes are production-shaped data. Strip PII before a HAR leaves the laptop. Prefer event_id and a redacted URL in tickets. Pixellint core will flag a raw email on a pixel query. It will not scrub the file you already uploaded to the agency. vendor.meta.pii.unhashed_email is a finding on the artifact, not a DLP product.

https://www.facebook.com/tr?id=1234567890123456&ev=Lead&ud%5Bem%5D=shopper@example.com

sGTM, MMP, and proxies

Access logs on a reverse proxy in front of sGTM are copies. MMP raw data pulls are copies. Adjust, AppsFlyer, and Branch S2S bodies carry device ids and IP. If your logger prints the full query, you stored IDFA next to the event token. Rotate S2S keys like API secrets. Do not paste them into the same Slack thread as the HAR.

A retry queue that keeps the raw CAPI JSON until drain is a store of em, ph, and client_ip_address. Encrypt it. Expire it on a clock shorter than the CRM. A 200 from Graph API does not delete your copy. vendor.meta-conversions-api.body.hashed_plaintext_field will catch a hashed IP on the way out. It will not expire the row.

Vendor side versus yours

You do not control Meta's retention. You control yours. DPAs still ask what you send, not only what they keep. If you must keep payloads for disputes, encrypt them and expire them on a clock that is shorter than your CRM, not longer.

Child-directed, GDPR erasure, and CCPA delete have to hit these logs too. A warehouse that is append-only will fail that. Plan the delete path when you add the CAPI queue, not when the first request arrives. Segment archives and warehouse syncs are the usual place this is forgotten.

What Pixellint will and will not do

pixellint validate url and pixellint validate json inspect one artifact. They do not scan CloudWatch. They do not delete a HAR. They do not set a BigQuery time-to-live. Put the linter in CI so bad payloads never land in the log. Put a retention number on the log so good payloads do not live forever.

core.url.fragment_ignored and core.url.userinfo_deprecated are URL hygiene. They are not a retention policy. Do not file a feature request for log deletion. Write the collector's TTL the same day you write the collector. Then prove a delete with a ticket that is not a screenshot of a dashboard.

Check the artifact

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