Deleting cookies in beforeSend is not enough to keep personal data out of Sentry. The current Sentry Next.js SDK collects user identity, request and response bodies, URL query parameters, and even local variable values from your stack frames by default [1]. Stripping one of those leaves the other four flowing straight into your error dashboard, where they sit indefinitely and travel to every teammate with project access.
Most Next.js error-handling guides show a beforeSend hook that runs delete event.request.cookies and call it done. That closes a single door in a building with five entrances. The reason this keeps happening is that Sentry's default scrubbing works on a key-name denylist, so a field named password gets masked while one named email does not.
This guide covers what the SDK actually captures, the four vectors a cookie filter misses, and the two-layer fix that stops personal data at both the SDK and the Sentry server.
TL;DR:
- Sentry's Next.js SDK sends user identity (
id,email,username,ip_address), request and response bodies, query strings, and stack-frame local variables by default [1][2]. - The built-in scrubber is a key-name denylist (
password,secret,token,api_key, plus a credit-card regex). PII in a field keyedemailor in a positional argument with no key is not caught [4]. delete event.request.cookiescloses one vector. Fix the rest by opting out at thedataCollectionlevel and usingbeforeSendas an allowlist, not a denylist.- Add Sentry's server-side Advanced Data Scrubbing as defense-in-depth so a config drift in code does not reopen the leak [4].
Table of Contents
- What does Sentry collect from your Next.js app by default?
- Why doesn't deleting cookies stop the leak?
- Which four vectors leak PII past a cookie filter?
- How do you scrub PII at the SDK with beforeSend?
- How does server-side scrubbing add a safety net?
- Where does PII enter Sentry in a real Next.js app?
- Common questions about filtering PII from Sentry
- Scrub at the source, not after the incident
What does Sentry collect from your Next.js app by default?
By default the Sentry Next.js SDK collects rich debugging context including user identity, request and response bodies, and stack-frame variables, then scrubs only values whose keys match a built-in sensitive denylist such as auth, token, and password [2]. Everything outside that denylist is sent and stored as-is.
The behavior lives in a dataCollection object, and the defaults are permissive. Here is what each field collects when you leave it untouched [2]:
| Field | Default | What it sends |
|---|---|---|
userInfo | true | user.id, user.email, user.username, user.ip_address |
cookies | true | Request cookies (denylisted keys scrubbed) |
httpHeaders | true | Request and response headers |
httpBodies | all types | Incoming and outgoing request and response bodies |
queryParams | true | URL query strings |
stackFrameVariables | true | Local variable values in each stack frame |
frameContextLines | 5 | Source code lines around each frame |
The setup wizard does not flip these off for you. The generated sentry.server.config.ts ships the dataCollection block with the opt-out lines commented out and a note that reads "To disable sending user data and HTTP bodies, uncomment the lines below" [5]. Out of the box, user identity and request bodies are on.
There is also an older single switch, sendDefaultPii, which defaults to false and, when set to true, enables automatic IP address collection among other things [2]. Newer SDK versions replace that blunt flag with the granular dataCollection object above, so checking only sendDefaultPii gives a false sense of safety.
Why doesn't deleting cookies stop the leak?
Deleting cookies stops one category of data and leaves the rest. Cookies are not even sent by default in the newest SDK behavior, so a delete event.request.cookies call often removes data that was already handled while the real PII (emails, bodies, stack locals) flows untouched. The filter feels protective without protecting much.
The deeper issue is how the default scrubber decides what is sensitive. Sentry's server-side scrubbing masks values whose key names appear on a fixed list: password, secret, passwd, api_key, apikey, auth, credentials, token, bearer, and similar, plus anything that matches a credit-card regular expression [4]. It is a denylist keyed on field names.
That design has two blind spots that matter for a SaaS handling real users:
- PII rarely lives under a denylisted key. A user's email sits under
email, an address undershipping_address, a date of birth underdob. None of those names are on the list, so the values pass through. - Some data has no key at all. A Server Action receives positional arguments. A thrown error carries a stack trace whose local variables are captured by value, not by name. The denylist has nothing to match against, so it cannot help.
This is the same output-boundary problem that shows up when a Server Component serializes too much into the browser. The fix is the same principle: decide what leaves your server explicitly, rather than trusting a filter to recognize everything that should not. The companion guide on server-to-client data leaks in Next.js walks the browser-facing version of this boundary.
Which four vectors leak PII past a cookie filter?
Four categories of data reach Sentry that a cookie filter never touches: user identity, request and response bodies, stack-frame local variables with their surrounding source, and breadcrumbs plus query strings. Each is enabled by the SDK defaults, and each can carry information a privacy policy or GDPR obligation says you must not store.
Vector 1: user identity. With userInfo at its true default, every event is tagged with user.id, user.email, user.username, and user.ip_address [2]. Email addresses and IP addresses are personal data under GDPR, so attaching them to every error in a third-party tool is a processing decision you should make on purpose, not inherit from a default.
Vector 2: request and response bodies. The httpBodies setting collects incoming and outgoing request and response bodies by default [2]. A Server Action that updates a profile sends the new name, phone, and address in its body. A Supabase response row comes back full of columns. Both land in the event payload.
Vector 3: stack-frame variables and source context. stackFrameVariables defaults to true and captures the value of every local variable in each frame of the stack, and frameContextLines grabs five lines of your source around each frame [2]. If a function held a decrypted token or a user's record in a local variable when it threw, that value is serialized into the error.
Vector 4: breadcrumbs and query strings. Sentry keeps up to 100 breadcrumbs per event by default, and the wizard config sets enableLogs: true, so your application logs ride along with each error [2][5]. Query parameters are collected too, so a URL like /reset?email=user@example.com deposits that address into the event.
How do you scrub PII at the SDK with beforeSend?
Scrub at the SDK in two moves: opt out of the data categories you do not need through dataCollection, then use beforeSend as an allowlist that keeps only the fields you explicitly want. An allowlist is safer than a denylist because new PII fields you add later are excluded by default instead of leaking until someone notices.
Start by turning off the categories you have no reason to send. This is the blunt, reliable layer:
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
dataCollection: {
userInfo: false, // no email, username, or ip_address
httpBodies: [], // no request or response bodies
stackFrameVariables: false, // no local values in stack frames
queryParams: false, // no URL query strings
},
beforeSend(event) {
// Allowlist: rebuild request and user from scratch, keep only safe fields.
if (event.request) {
event.request = { url: event.request.url, method: event.request.method }
}
if (event.user) {
event.user = { id: event.user.id }
}
return event
},
beforeBreadcrumb(breadcrumb) {
// Drop log breadcrumbs that might echo a user-typed value.
if (breadcrumb.category === 'console') {
return null
}
return breadcrumb
},
})
The dataCollection opt-outs stop the four vectors at the category level. The beforeSend hook is the surgical backstop: Sentry invokes it before an event is sent and you can return a modified event or null to drop it entirely [3]. Rebuilding event.request and event.user from a short allowlist means a future field you forget about is never included. Keeping user.id (a random UUID, not an email) preserves your ability to group errors by user without storing who that user is.
beforeBreadcrumb does the same job for the breadcrumb trail, dropping anything that might carry a value a user typed. Apply the same Sentry.init shape in sentry.edge.config.ts and the client config, since each runtime has its own init.
How does server-side scrubbing add a safety net?
Server-side scrubbing is a second layer configured in the Sentry UI that strips data after it arrives but before it is stored, and it applies immediately to new events regardless of your SDK code [3]. It exists because SDK config drifts: someone reverts a dataCollection line, a new integration re-adds user context, or a different service forgets the beforeSend allowlist entirely.
Data scrubbing is enabled by default and runs the same denylist (password, secret, token, api_key, credit cards) on the server side [4]. To cover the PII keys the default list misses, add your own under Settings, then Security & Privacy, then Additional Sensitive Fields: list email, phone, address, dob, and any column names your schema uses. Advanced Data Scrubbing rules go further, letting you target specific paths in the event so you can remove a whole branch of data by location rather than by key name [4].
Think of the two layers by what they prevent. The SDK beforeSend allowlist ensures sensitive data never leaves your server. Server-side scrubbing ensures Sentry never stores what does leave. Defense-in-depth means neither layer is your only line, which matters when the failure mode is silent. This belt-and-suspenders posture is the same one the pre-launch security audit applies to logs in its no-sensitive-data-in-logs check, and it is the architectural defense behind OWASP A09 in the OWASP Top 10 for Next.js guide.
Where does PII enter Sentry in a real Next.js app?
PII enters through the same surfaces that make Next.js apps debuggable: Server Action arguments, the error objects you log, and the request context attached to server-side render failures. SecureStartKit does not ship Sentry, so none of this is a default we set for you. It is the surface you create the moment you add monitoring, and it is worth mapping before you do.
The template logs server errors with console.error, which Vercel captures as function logs. The Stripe webhook handler, for example, logs lines like these on failure:
// app/api/webhooks/stripe/route.ts
console.error('Payment failed for invoice:', invoice.id)
console.error('Webhook handler error:', error)
On their own these log Stripe object IDs, which are not personal data. But once Sentry is installed with enableLogs: true, your logs travel with events, and a future log line that includes a customer email or a full error object inherits that reach. The same applies to the ad-hoc { error } objects our Server Actions return: if one ever wraps a raw exception, the message can carry a query, a row, or an input that a user typed.
The three highest-risk entry points to audit in any Next.js app:
- Server Action arguments. They arrive as request bodies and are captured by
httpBodies. A profile-update action carries name, email, and address in its payload. - Server Component render errors. When a render throws, the request context (headers, query string, sometimes the body) is attached. The error message itself is stripped by Next.js in production, but the surrounding context is not.
- Anything you pass to
console.errororSentry.captureException. If you log the whole error object or the whole user, you log whatever it contains.
The general setup for capturing these errors correctly, including the onRequestError hook that makes server-side errors visible, lives in the Next.js error handling and Sentry setup guide. This post is the privacy layer that sits on top of it: once errors are visible, make sure what you can see is free of personal data.
Common questions about filtering PII from Sentry
Does setting sendDefaultPii: false stop all PII from reaching Sentry?
No. sendDefaultPii defaults to false already and mainly governs automatic IP collection [2]. The newer dataCollection object still collects user identity, request bodies, query strings, and stack-frame variables independently, so you have to opt those out separately.
Does Sentry scrub email addresses automatically?
No. Default scrubbing masks values whose key names are on a fixed denylist (password, secret, token, api_key) plus a credit-card regex [4]. A field named email, phone, or address is not on that list, so you must add those under Additional Sensitive Fields or strip them in beforeSend.
Is server-side scrubbing enough on its own?
It is a strong safety net but not a complete fix. Server-side scrubbing stops Sentry from storing data, but the data still left your server to get there [3]. Pair it with an SDK-side beforeSend allowlist so sensitive values never transmit in the first place.
What happens to Server Action inputs in Sentry?
They are captured as request bodies through httpBodies, and any local variable holding them can be captured through stackFrameVariables [2]. Setting httpBodies: [] and stackFrameVariables: false removes both, while the beforeSend allowlist catches anything a custom integration re-adds.
Scrub at the source, not after the incident
Filtering PII from Sentry is a configuration decision you make once, before the first production error, not a cleanup you do after a privacy review finds emails in your dashboard. The defaults are built for maximum debuggability, which means maximum data, and the built-in denylist only recognizes secrets, not people.
The reliable shape is two layers: opt out of the categories you do not need through dataCollection, rebuild request and user data from a short allowlist in beforeSend, and back it with server-side Advanced Data Scrubbing so a code change never silently reopens the leak. None of it is hard. It is just not the default.
If you would rather start from a codebase where backend-only data access and validated inputs already keep most personal data off the surfaces an error tracker reads, that is the foundation the SecureStartKit template is built on.
Built for developers who care about security
SecureStartKit ships with these patterns out of the box.
Backend-only data access, Zod validation on every input, RLS enabled, Stripe webhooks verified. One purchase, lifetime updates.
References
- Default Data Collected | Sentry for Next.js— docs.sentry.io
- Configuration Options | Sentry for JavaScript— docs.sentry.io
- Scrubbing Sensitive Data | Sentry for Next.js— docs.sentry.io
- Server-Side Data Scrubbing | Sentry— docs.sentry.io
- Manual Setup | Sentry for Next.js— docs.sentry.io
Related Posts
Next.js Secrets: 4 Ways to Share Them Safely [2026]
Committing a .env to share secrets leaks them into git history forever. Compare the safe ways to share environment variables across a Next.js team.
Next.js Server-to-Client Data Leaks: 5 Cases [2026]
Backend-only queries don't help if you serialize the whole row. See what passing data from Server to Client Components leaks, and how to stop it.
Secure Image Uploads in Next.js: 5 Edge Cases [2026]
Magic-byte checks pass polyglots and miss decompression bombs. Re-encode every image upload through Sharp to strip EXIF and neutralize payloads.