PII-safe Sentry breadcrumbs: error reports without data leaks
Set up PII-safe Sentry breadcrumbs with scrubbers, release tracking, and rich context so errors are actionable without logging secrets or personal data.

What goes wrong with error reporting and privacy
Error reporting should help you fix bugs faster. But it often collects more than you meant to share, especially once you add breadcrumbs.
Breadcrumbs are small notes about what happened right before a crash: which screen opened, which button was clicked, which API call failed. They’re useful because they turn a vague error into a sequence you can replay.
The trouble is that many apps treat breadcrumbs like normal logs. Private data slips in quietly. One breadcrumb can include a full URL with query params, a header dump, or a form payload. That can expose personal data (emails, phone numbers, addresses) and secrets (API keys, session cookies, password reset tokens). Once that data reaches an error tool, it may be stored, searchable, and visible across the team.
Leak points often look harmless in code review:
- URLs with identifiers or reset tokens in the query string
- Headers like Authorization, Cookie, or X-API-Key
- Form fields from signup and checkout flows
- GraphQL variables or JSON request bodies
- Debug logs that print environment variables or config
A simple mental model helps: collect behavior, not identity. Log that “signup failed after submit” and “POST /api/signup returned 500”, not the user’s email, full request body, or any token.
This gets worse in AI-generated prototypes. It’s common to see helpers that print entire objects “just to debug”, including secrets and user records. Fixing the crash is only half the job. Preventing a quiet data leak is the other half.
What to capture (and what to never capture)
Good error reports focus on the smallest set of facts that explains what broke. If you can answer “what action happened, where did it fail, and which version shipped it?”, you usually have enough to fix the bug without collecting personal data.
It helps to separate:
- Business events: what the user is trying to do (create account, upload file, checkout)
- Technical events: what the app did (route change, API request, validation failed)
For PII-safe Sentry breadcrumbs, favor technical events plus a coarse business label. Avoid the full user story.
A practical capture policy:
- Safe to capture: screen/route name, button ID (not button text), feature flag keys, API endpoint path (no query string), HTTP method and status code, timing (ms), retry count, app state like offline/online.
- Risky: full URLs with query params, request/response bodies, user-entered form fields, email/phone/address, raw error messages that might echo input.
- Must never leave the app: passwords, magic links, session tokens, refresh tokens, API keys, payment card data, government IDs.
A concrete example for broken signup:
- Safe breadcrumb: “POST /api/signup -> 400, validation_failed, release 1.8.3”
- Risky breadcrumb: “POST /api/signup body={email:..., password:...}”
The first tells you where to look. The second creates a data leak.
A simple data classification for breadcrumbs and errors
PII-safe Sentry breadcrumbs work best when the team shares one rule: every value you capture is either safe by default, safe only after redaction, or never allowed. If someone can name the category in seconds, you avoid “we thought it was fine” leaks.
Three buckets most teams can stick to
- Never capture: credentials and secrets (passwords, session tokens, API keys, auth headers, private keys) and highly sensitive data (full card numbers, CVV, health details).
- Capture only in a reduced form: personal data that identifies someone (email, phone, IP address, full name, mailing address). If you truly need it, hash it, truncate it, or replace it with a stable internal reference.
- Safe to capture: technical context that helps debugging (feature flags, route name, button clicked, server region, error code).
Examples that usually hold up:
- Email: reduced form (or don’t capture it)
- Internal user_id: often OK if it’s not guessable
- IP address: often personal data
- Session token: never
- API key: never
A redaction policy people will actually follow
Keep it short and consistent across logs, breadcrumbs, and error events:
- List the exact fields you allow (for example: user_id, org_id, release, route, feature).
- List the fields you always redact (token, password, authorization, cookie, secret, key).
- Note where those fields appear (request body, headers, query string, local storage, UI inputs).
The biggest failure mode is “log the whole object”. If you see that pattern in an inherited codebase, treat it as urgent: remove it first, then add guardrails so it can’t creep back.
Step by step: baseline Sentry setup with safe defaults
Pick one error reporting tool and use it consistently across the app. Sentry is common, but the same approach applies elsewhere. The goal is simple: every error should clearly say where it happened and which build shipped it.
Start by standardizing environments and using them everywhere: dev for local work, staging for pre-release testing, and prod for real users. Make the environment a config value, not something people type by hand.
Initialize the SDK with safe defaults and only the integrations you need. A minimal browser setup might look like this:
Sentry.init({
dsn: "…",
environment: process.env.APP_ENV, // dev | staging | prod
release: process.env.APP_RELEASE, // e.g., git sha or build id
tracesSampleRate: 0.1, // performance sampling (start low)
sampleRate: 1.0, // error events (usually keep at 100%)
maxBreadcrumbs: 50,
maxValueLength: 250,
});
Enable breadcrumbs that help you replay what happened without turning your logs into a data dump. Good default sources are navigation changes, user clicks, API calls (method + route, not full URLs), and console errors.
Keep volume under control so you don’t drown in noise or costs. A few limits usually prevent that:
- Cap breadcrumbs (like 50) so one noisy page doesn’t dominate the timeline.
- Trim long strings (like 250 chars) to reduce accidental leakage.
- Sample performance data (start at 5 to 10%) until you know what you need.
- Add basic client-side rate limiting so one broken loop doesn’t send thousands of events.
Scrubbers and allowlists to keep PII out
Scrubbing works best when you start with an allowlist. Instead of trying to block every possible secret, decide what breadcrumb and error fields you accept and drop the rest. That’s the safest way to keep breadcrumbs useful without collecting private data.
A practical default is to keep:
- Event name and a short message
- A stable error code (if you have one)
- A route template (like
/users/:id) - Non-personal tags (release, environment, feature flag)
Treat everything else as suspicious until you’ve proven it’s useful.
Then add boring, strict scrubbers for predictable foot-guns:
- Scrub keys that often carry secrets:
password,pass,token,access_token,refresh_token,authorization,cookie,session,api_key - Redact request headers by default, then allowlist only the ones you truly need (often none)
- Remove URL query strings and fragments (
?…and#…) unless you have a specific, reviewed reason to keep them - Normalize user identifiers: use an internal user id or a one-way hash, not raw email or phone
- Drop request bodies unless you have a tight allowlist for specific fields
Example: a signup bug.
- Leaky breadcrumb: “POST /signup?email=[email protected]”
- Safer breadcrumb: “POST /signup (validation_failed, field=email)”, plus a user id like
u_18429orhash_9f2c…
You still see what happened, but you’re not storing personal data.
Release tracking that makes errors actionable
If an error report doesn’t tell you which version shipped it, it’s hard to fix quickly. Release tracking ties every event and breadcrumb trail to a build so you can answer one question fast: did this start after the latest deploy?
Attach a release identifier to every event. Many teams do this at app startup so the release is present even if the crash happens on the first screen.
A release naming rule that stays sane
Pick a naming format that matches how you already build and deploy. Consistency matters more than cleverness.
- Use the same release string in frontend and backend when possible
- Prefer a git commit SHA or build number
- Add an environment tag only if your tooling doesn’t already separate environments
- Never include user emails, tenant names, or request URLs in the release name
With that in place, PII-safe breadcrumbs get more useful: you can compare “same flow, different version” without inspecting personal data.
Mark deploys so spikes match changes
Deploy markers turn graphs into a story. When error rates jump, you can line the spike up with a deploy and narrow your search to the code that changed.
Example: signup errors jump after build web@3f2c1a9. The breadcrumbs show “clicked Sign up”, “POST /api/signup”, then a 500. You don’t need the user’s email to act. You need the release, the endpoint, and the failing step.
Actionable context without personal data
Good error reports answer one question fast: what happened right before the crash? You can get that clarity without copying emails, tokens, addresses, or full request bodies into breadcrumbs.
Start by adding a few safe tags that describe the situation, not the person. Useful examples are feature flag state (on/off), tenant tier (free/pro/enterprise), device type (mobile/desktop), and plan level. These tags turn a pile of errors into groups you can act on.
Request context is another high-signal area, but keep it minimal:
- HTTP method
- Route template (for example,
/projects/:id/settings, not the real ID) - Status code
If you add latency, consider rounding (for example, 1200ms) instead of storing overly detailed timing per user session.
For breadcrumbs, think in safe state snapshots: screen name, step number in a flow, retry count. That alone can show patterns like “fails on step 2 after 3 retries on mobile,” which is often enough to find a logic bug.
A compact set of fields that tends to stay useful and PII-safe:
screen,flow_step,retry_countroute_template,method,status_codefeature_flag,tenant_tier,plan_level,device_typerelease,build,environment
To connect frontend and backend without user data, add a correlation ID. Generate a random ID per request (or per session), send it in a header, and store it as a tag or extra on both sides. When breadcrumbs show a failing request, you can match it to a server error using that one ID.
Special cases: auth, payments, and AI-generated prototypes
Some parts of an app are more likely to leak sensitive data than others. If you want PII-safe breadcrumbs, treat auth and payments as “always dangerous” and lock them down first.
On the server side, the safest default is to capture less. Strip request headers you don’t need, redact request bodies by default, and never forward raw cookies into error events. If you temporarily capture a body for debugging, allow only specific keys and cap the size.
For authentication flows, redact anything that can be used to sign in as the user:
- Authorization headers (Bearer tokens)
- Cookies and session IDs
- JWTs, refresh tokens, and CSRF tokens
- OAuth codes, state values, and redirect URLs with params
- Magic links and one-time passwords
On the client side, be careful with “helpful” features that over-collect. Avoid capturing full DOM snapshots, form inputs, or clipboard content. Prefer breadcrumbs that describe intent without copying data, like “Clicked Sign in button” or “Validation failed: password too short”.
Payments need the same approach. Never record full card numbers, CVC, bank details, or billing addresses. If you need context, capture high-level outcomes like “Payment provider returned declined” plus a provider error code.
AI-generated prototypes are a special risk because they often log entire objects “just to see what’s inside,” including headers and environment variables. If you inherited code from tools like Cursor, Replit, Bolt, Lovable, or v0, search for console logs and error handlers that dump full requests.
A reliable rule: log actions and outcomes, not payloads and secrets.
How to test that scrubbers actually work
Don’t assume scrubbers work because you configured them once. Treat them like a safety feature: test them in every environment (local, staging, production) after any logging change.
Send a controlled error that includes canary values you would never use in real life. You should still be able to debug the flow, while every sensitive value is removed or replaced.
A repeatable test routine:
- Trigger a test exception that includes a fake email (
[email protected]), a fake token (tok_test_SHOULD_NOT_LEAK), and a credit card-like string (4242 4242 4242 4242). - Reproduce the error once in each environment and confirm the event is received.
- Open the full event payload and check message, breadcrumbs, request headers, and extra context for redactions.
- Search events for your canary values. You should find zero matches.
- Repeat after changing auth, forms, payments, or analytics code.
Also check what your framework adds automatically. Many leaks come from headers (Authorization, Cookie), URLs (query params), and form bodies.
Write a short runbook for when something slips through:
- stop sending the field (disable the breadcrumb or context)
- tighten scrubbers or add an allowlist
- delete impacted events per your policy
- re-test with canaries
- note the root cause so it doesn’t return
Common mistakes that cause accidental data leaks
Most privacy leaks aren’t caused by one big mistake. They come from small defaults that feel harmless until an incident shows up in your error tool.
Relying on a blacklist is a common trap. You scrub password and token, then someone adds ssn, dob, or inviteCode later and it goes out untouched. Safer setups start with an allowlist: only send the fields you truly need to debug.
Logging full URLs is another easy leak. Query params often carry emails, phone numbers, reset tokens, and internal IDs. A breadcrumb like GET /reset?email=...&token=... can expose exactly what an attacker wants. Prefer route templates and drop query strings by default.
Request and response bodies are where the worst surprises live. If your SDK captures bodies by default, you can end up sending signup forms, auth payloads, payment objects, user prompts, and snippets of uploaded content.
Be careful with the user object too. Using raw email or phone as user.id makes every event directly identifying. Use a stable internal ID or a one-way hash, and keep personal fields behind explicit opt-in and scrubbing.
Quick checklist before you ship
Do a quick pass with a “what could this reveal?” mindset. Error reports should help you fix bugs, not become a shadow database of personal info.
A short set of checks prevents most leaks:
- Keep breadcrumbs structured and boring: fixed fields like
category,action,status, and internal IDs. Avoid free-text user input (search terms, form fields, chat messages), even temporarily. - Scrub secrets everywhere they hide: block or redact Authorization headers, cookies, session IDs, CSRF tokens, password fields, API keys, and values that match your token patterns.
- Make every event actionable: confirm
releaseandenvironmentare attached to every error. - Template routes and tame URLs: record
/users/:id/settingsinstead of/users/48392/settings. Drop query strings by default. - Prove redaction end to end: send a test event with fake secrets (like
Bearer test_token_123) and a fake email, then verify the dashboard shows[REDACTED]or nothing at all.
Before launch, pick one realistic flow (signup or checkout), trigger a controlled error, and confirm the report still has enough context (route template, release, feature flag state, network status) to debug without exposing users.
Example: debugging a broken signup without exposing user data
A common story: you deploy on Friday, and signup failures spike. Users see a vague “Something went wrong” after they click “Create account”. You need enough detail to fix it fast, but you can’t afford to leak emails, tokens, or auth headers into your error tool.
With PII-safe Sentry breadcrumbs, the report can still be actionable. The event shows the path without the private payload:
- Breadcrumbs:
Signup screen opened->Email form submitted->POST /api/signup (400)->Magic link screen shown->POST /api/verify (401) - Context: environment
production, browser and OS, feature flag state (on/off), and API response status codes - Tags:
flow=signup,provider=email_magic_link,region=us-east
At the same time, scrubbers redact sensitive values before anything leaves the app. The event should replace or remove:
- Email input values and “name” fields
- Magic link token, OTP code, session cookies
- Authorization header and any
x-api-key
Now the why becomes clear without personal data: errors started with release [email protected], and most of them happen on POST /api/verify with a 401. That points to a logic or config change, not random user behavior.
Release tracking narrows it further. Compare commits between 1.8.2 and 1.8.3 and you’ll usually find a small change like a renamed endpoint, a missing cookie setting, or a new middleware blocking the verify route.
Next steps: keep it safe as your app grows
Once you have PII-safe breadcrumbs working, treat privacy as an ongoing job. Apps change fast: new endpoints, new third-party SDKs, and debug logs added during a late-night fix.
Assign a single owner for privacy-safe error reporting. They don’t need to do everything, but they should run a quick review on a schedule and make sure changes don’t weaken scrubbers or allowlists.
Add a release gate so obvious mistakes are caught before shipping. Even a simple CI check that scans for common secret patterns can prevent a lot of accidental exposure. Pair that with a code review rule: no logging of request bodies, auth headers, or full user objects.
Good ongoing habits:
- Review recent error events and confirm scrubbing still works
- Re-check the allowlist after adding new breadcrumbs or SDK integrations
- Rotate keys if anything sensitive was ever captured, then tighten filters
- Run a targeted audit after big features (auth changes, billing, file uploads)
- Keep a shared team norm: debug with IDs and counts, not raw data
If you inherited an AI-generated project that’s noisy, broken, or leaking secrets, a focused audit can be faster than trying to guess where the logging is happening. FixMyMess (fixmymess.ai) specializes in diagnosing and repairing AI-generated codebases and hardening them for production, including safer logging and error reporting.