Scrubbing PII from an error-tracking tool needs two separate fixes — code AND platform setting
An error-tracking SDK (verified on Sentry) writes personal data into two structurally different places, and each one is only fixable from its own side — a code-side fix cannot touch the other.
The signal that gives it away
You ship a beforeSend scrub, redeploy, confirm in a test event that the fields you targeted are gone — and a few events later the same category of PII (an IP-derived tag, a geo-location tag) is back. The instinct is to treat this as a regression in the scrubbing code: it isn't. If the *shape* of the leak is "a tag the SDK never explicitly set" rather than "a field your code populated", you're not looking at a code bug at all — you're looking at data the ingestion server derived on its own, after your code had already finished running.
Concretely, the two shapes look like this:
- Layer 1 (code-fixable): fields that live inside the JSON payload your SDK builds and sends — event.user.id/email/username, event.user.ip_address, request headers (X-Forwarded-For, X-Real-IP, CF-Connecting-IP and similar proxy-chain headers), request env vars (REMOTE_ADDR, HTTP_X_FORWARDED_FOR), breadcrumb data. Your code built this payload; your code can redact it before the POST leaves the process.
- Layer 2 (not code-fixable): tags the *provider's ingestion server* attaches after receiving the POST, derived from properties of the request itself rather than from anything in the JSON body — e.g. a user: ip:<address> tag taken from the TCP source IP of the connection, and a user.geo: <country>, <city> tag from a GeoIP lookup on that same source IP. These are generated downstream of your SDK entirely; there is no hook in your code that runs after the server has already received and processed the request.
Layer 1 — in code, via `beforeSend`
Sentry's own SDKs run beforeSend/beforeSendTransaction last, after all other event processors, immediately before the event is serialized and sent — which is exactly the point where you get the final shape of the event and can still mutate or drop it. The robust pattern is a full nuke rather than a field-by-field allowlist: set event.user = {} outright, then explicitly strip the known IP-carrying headers and env keys. A field-by-field denylist rots the first time a new proxy header shows up upstream (a new CDN, a new load balancer) that nobody remembered to add to the list. Do this identically on both the client-side and server-side init file so the dashboard doesn't show client events scrubbed and server events not.
Layer 2 — in the platform's settings, not in code
the fix lives in the provider's project settings, not in a code file — for Sentry: Project Settings → Security & Privacy → "Prevent Storing of IP Addresses" (ON). This is a persistent, org/project-level control, invisible from the codebase entirely — nobody reviewing a pull request will ever see this fix, because there is no diff for it. That's worth writing down somewhere durable (a wiki page like this one, an onboarding doc) precisely because it leaves no trace in version control.
The diagnostic order that saves time: when a PII tag reappears, check the *shape* first.
1. Does it look like a field your code explicitly sets (event.user.email, a header you strip)? → check for a regression in the beforeSend hook.
2. Does it look like a tag your code never populated, tied to the request's network origin (ip:, .geo)? → don't touch the code. Go check the platform setting first. Only fall back to auditing beforeSend after confirming the setting is actually on — settings are persistent and don't flip themselves, but it costs one click to rule out before spending time re-reading scrub code that was never the problem.
The same split generalizes beyond IP/geo: any SaaS ingestion pipeline that enriches a payload *after receiving it* (geo lookup, reverse DNS, threat-intel tagging, bot-score annotation) creates the same two-layer problem. If the platform enriches server-side, no client-side hook will ever reach it — always check the vendor's ingestion/enrichment settings before assuming a code fix is even possible.
What does NOT work
- Assuming a full `beforeSend` scrub is a full scrub. It only covers what the SDK put in the payload. Verified against the vendor's own docs: even with server-side "prevent storing of IP" enabled, geographic data is still *derived* from the IP at ingestion time — Sentry's documentation states this extraction "occurs even if the setting to stop storing IP addresses is turned on," i.e. it is not conditional on that toggle at all, not a gap that varies by deployment or version. The setting removes the IP tag; it does not remove the geo tag. The only documented way to remove the geo tag is a separate, more specific scrub rule targeting the geo field directly — for Sentry, an Advanced Data Scrubbing rule like [Remove] [Anything] from [$user.geo.**]. Don't test-and-conclude "the toggle also covers geo" from a handful of events looking clean; the vendor's own docs already settle this — go read them instead of inferring the setting's scope from a sample.
- A field-by-field header denylist instead of a full-object nuke. Listing "the 4-5 headers we know about" for event.user/IP-carrying headers silently misses the next proxy header a new piece of infrastructure introduces upstream. A denylist needs maintenance forever; a full-object reset (event.user = {} plus stripping the *category* of header, not a fixed list) degrades gracefully when infrastructure changes.
- Treating a reappeared tag as a code regression by default. Jumping straight into re-reading beforeSend when the real cause is a platform setting wastes a debugging cycle. The tag's shape (something your code never set) is the fast filter that tells you which layer to check first.
- Lazy-initializing the error-tracking SDK to save boot time. Not a PII issue directly, but the same tool: see What doesn't work — lazy-initializing an error-tracking SDK to save boot time for why deferring SDK init to save a few hundred milliseconds creates a structural blind window for the earliest errors — and why "the dashboard shows no events" can look identical whether the cause is a scrubbing misconfiguration or a missed-init blind spot, which is itself a reason to verify runtime behavior end-to-end rather than trust static checks (see Static verification doesn't prove runtime behavior — the SEED / ACTIVATE / VERIFY / NEGATIVE checklist).
Related
- Application INFO logs vanish because nobody configured the root logger — the web server only configures its own — another "the tool looks silent but the cause is a config layer you didn't check" pattern in observability tooling. - What doesn't work — lazy-initializing an error-tracking SDK to save boot time — same SDK, a different silent-failure mode (blind window on the first errors after page load). - Static verification doesn't prove runtime behavior — the SEED / ACTIVATE / VERIFY / NEGATIVE checklist — the general principle behind "verify the actual event/tag, don't assume the setting or the code did what you think."
Verified against
8 claims checked against these sources
What links here
Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.