This is a submission for DEV's Summer Bug Smash: Smash Stories.
TL;DR.
I wrote a scrubbing policy before writing any instrumentation code.
I enforced it in a hook.
I unit tested it.
I wrote a comment above the one obviously sensitive line saying exactly what it must never do.
Then I intercepted the actual bytes leaving the browser and found a stranger's shoulder injury in them.
Every guarantee I had written was about data my code hands to the SDK.
None of them were about data the SDK collects on its own.
The setup WhyRep is a workout tracker built local-first.
Training data is created and read on the device, the tracker works offline with no account, and that is not a marketing line, it is the architecture.
It is also the thing people decide to trust or not trust in about four seconds on the landing page.
So when I added Sentry, the scrubbing policy came before the code.
Written down, in the repo, as a list of things that may never appear in an event: exercise names, weights, reps, RIR, session notes, chat content.
Never.
On Android I enforced it twice.
A hook that strips the forbidden fields, and a unit test that constructs an event carrying each one and asserts it comes out stripped.
Green.
Good.
Then I wired up the landing site's share-link page.
It decodes , where the payload is somebody's entire workout template, base64 in the URL fragment.
I was careful there too.
On a decode failure it reports a coarse reason tag and never the payload: I wrote that comment because it was the obviously sensitive thing and I wanted the next person to see it before they "helpfully" added the payload for debugging.
Read the comment again.
It is correct.
It is well-reasoned.
It describes exactly the right danger.
And the leak happened six inches away from it.
What I missed The payload is in the URL.
Sentry's browser SDK attaches to every event, every transaction and every replay, by itself, as standard context.
It is a completely reasonable default.
It is the single most useful piece of context a browser error can carry.
And the fragment goes with it.
Every guarantee I had written was about data my code passes to Sentry.
None of them were about data the SDK collects on its own.
Those are two different data paths and I had only ever thought about one of them.
I only found it because I stopped reading my own code and intercepted the actual bytes leaving the browser.
A Puppeteer test that grabs the outbound envelope body and greps it for known-sensitive strings: It failed on the first run.
Decoded, the intercepted request contained , , and a note reading .
Base64 is not encryption.
That is a lifter's shoulder injury, in a third-party service, from a page whose privacy policy said the site had no third-party trackers.
Both panels are the same request.
The top one is the only part my policy had an opinion about.
Then the same grep found a second one Once the interceptor existed I pointed it at the rest of the site, which is how I found the one I would not have gone looking for.
The waitlist dedupes signups by using the email address as the Firestore document id.
That is a defensible design.
It also puts the email in the request path: Sentry's automatic fetch breadcrumb records request URLs verbatim.
So every waitlist signup email would have been attached to any error that happened afterwards in that session.
Not an event I sent.
A breadcrumb the SDK collected, riding along on somebody else's unrelated exception.
And that one is worse than the first, for a reason worth naming.
The template in the fragment is the user's own data, shared by the user, from a page they opened deliberately.
The waitlist email is a third party's data, collected in confidence, leaking through a mechanism that has nothing to do with the feature it was collected for.
Not my data.
Other people's.
The fix It is unglamorous, which is fine.
Three hooks: All three are needed and that is the part I would underline.
Errors, performance data and breadcrumbs take three different pa
