Article Information

Category: Development

Published: April 27, 2026

Author: Chris de Gruijter

Reading Time: 8 min

Tags

SecurityPrivacyFrontendLoggingBest Practices
Code on a monitor screen representing frontend security and browser debugging

What Your Browser Console Is Quietly Leaking (And How to Fix It)

Published: April 27, 2026

Most developers treat console.log as harmless debugging scaffolding — something that gets cleaned up before launch. In practice, it almost never does. I recently audited the frontend codebase of a client platform and found over 700 console.* calls across nearly 90 browser-runtime files, quietly printing emails, user IDs, authentication tokens, database URLs, payment customer IDs, and raw API responses to anyone with DevTools open. Here is what I found, why it matters more than most people realise, and what a clean fix actually looks like.

Open DevTools. You Might Be Surprised.

The browser console feels like a private debugging channel. It is not. Any user can open it in two keystrokes. So can a curious support agent, a penetration tester, a journalist, or someone who simply wants to poke around. And in many production apps, it reads like an internal system dump.

On this particular platform, opening the console on the login page and navigating to the dashboard produced output that included: the full Supabase project URL (which directly exposes which cloud provider and region the backend runs on), the user's UUID, their email address on every auth state change, OAuth token fragments in URL hashes, Stripe customer IDs embedded in subscription objects, and raw JSON bodies from every API call the page made on load. All in plain text. All readable without any authentication or access to the backend.

What Was Actually Being Logged

After a full audit, I grouped the leaks into categories. Each one represents a different type of risk:

  • User identifiers — UUIDs and internal user IDs logged on every session restore, profile fetch, and auth state change.
  • Email addresses — Logged during signup, login, admin status checks, and in the global auth middleware on every route navigation.
  • Authentication state — Token expiry timestamps, session objects, cookie presence, and the full flow of which routes were attempted and redirected.
  • Database infrastructure URLs — The backend project URL was logged on every client initialisation, giving away the infrastructure provider.
  • Payment customer IDs — Stripe customer IDs and full subscription objects logged during account page loads and cancellation flows.
  • Raw API responses — Entire JSON payloads from account data, invoice, payment method, and onboarding endpoints were dumped verbatim.
  • OAuth tokens in URL hashes — The subscribe page logged window.location.hash on mount, which during email verification flows contains a live access_token.

The Sentry Problem on Top of Everything

On top of the app-level leaks, the Sentry configuration had two settings that made things meaningfully worse. First, debug: isDevelopment was enabled — which in the local environment meant Sentry's internal SDK logger was active, producing over 100 additional lines per page load. Every integration install, every tracing span start and finish, every replay session status update, all printed to the console. This noise is useful when debugging the Sentry SDK itself; it has no value during normal development and actively obscures the signal you actually care about.

Second — and more seriously — sendDefaultPii: true combined with enableLogs: true meant that the same console output described above was being forwarded wholesale to Sentry's servers. The app was not just leaking internally to anyone with DevTools; it was shipping PII to a third-party error tracking platform on every page load.

Why Developers Let This Happen

It is worth being honest about how this happens, because it is not carelessness in the usual sense. console.log statements are added during development to understand what the application is doing. They work. They are fast. They help. And then a deadline arrives, a feature ships, and the log statements stay because removing them is never urgent.

The problem compounds when a codebase scales. What starts as a handful of debug lines across a few files quietly grows to hundreds of calls spread across middleware, composables, plugins, and pages — each individually reasonable, collectively forming a detailed map of the system's internals. By the time an audit surfaces it, the volume makes cleanup feel overwhelming.

The Fix: A Shared Browser-Safe Logger

The solution is not to delete every log — that destroys developer visibility. The goal is to control where detailed output lands. Server logs and terminal output can remain as verbose as needed. Browser output needs to be safe by default.

The approach I used was a single shared logger utility (utils/logger.ts) that wraps all console output with three behaviours:

  1. Server context: Full output, no redaction. Logs go to terminal only, never to the browser.
  2. Browser + production: All debug and info level output is suppressed entirely. Only warn and error pass through, and even those have data fields redacted.
  3. Browser + development: Logs are emitted but any data object passed to the logger is recursively scanned for known sensitive field names — email, id, token, access_token, session, stripe_customer_id, url, anon_key, and about 20 others — and replaced with [redacted] before printing.

The convention adopted alongside it was simple: message strings must never contain interpolated values. So instead of console.log("User authenticated:", user.id) you write logger.debug("Auth: user authenticated"). The detailed context still lives in the data argument, which gets redacted automatically. The message stays safe by construction.

Before and After

A few representative examples from the audit, showing the original call and what it became:

  • console.log("Starting signup process for email:", email)logger.debug("Auth: signup initiated", { hasEmail: !!email })
  • console.log("Initializing auth store with user:", session.user.id) + full subscription dump → logger.debug("Auth store init", { hasUser: true, planId, status })
  • console.log("Supabase URL:", supabaseUrl)logger.debug("Supabase: client configured", { env, hasUrl: !!supabaseUrl, hasKey: !!supabaseKey })
  • console.log(window.location.hash) during OAuth token exchange → logger.debug("Subscribe flow", { hasHash: !!window.location.hash, hasSession, hasUser })
  • console.log("Auth state DETAILED:", { userId, directUserId, ... }) inside the error logger itself → deleted entirely

Fixing the Sentry Config

Alongside the logger refactor, four changes were made to the Sentry client config:

  • debug changed from isDevelopment to false. Sentry's internal logger is for Sentry SDK development, not app development. It should never be on by default.
  • sendDefaultPii changed from true to false. There was no documented legal basis for automatically capturing user IPs, cookies, and request headers.
  • enableLogs changed from true to false temporarily, pending confirmation that the console output was fully clean. Once the audit is verified, it can be safely re-enabled.
  • An explicit console.log at the bottom of the config file — which was printing sample rates and environment labels to the production browser console — was removed.

The Result

After the refactor: zero console.* calls in browser-runtime files. Eighty-nine files now import the shared logger. In production, the browser console is silent unless something actually goes wrong. In development, it shows concise status labels — "Auth: initialized", "Profile: loaded", "Subscription: active" — with no values that would mean anything to an attacker.

The Sentry noise is gone. The OAuth token fragments in URL hashes are gone. The database URL is gone. The console is a debugging tool again, not a data breach waiting to be discovered.

What I Would Do Differently From the Start

If I were starting a new project with this in mind, I would introduce the shared logger from day one rather than retrofitting it. The upfront investment is small — one utility file, a short convention document, and a CI lint rule to catch raw console.* calls in browser-runtime directories. The fix is much simpler when there are ten log lines instead of seven hundred.

The other thing I would do from the start is separate the concern explicitly: server code can log anything; browser code logs safe summaries only. That distinction is intuitive once you have named it. Without it, the default assumption is that console output is private — which it never is.

Treat everything in the browser as public. Because it is.

Practical Checklist for a Console Audit

If you want to run this audit on your own codebase, here is a starting point:

  1. Run grep -rn "console\." composables/ plugins/ middleware/ pages/ components/ stores/ utils/ on your browser-runtime directories. Count the results.
  2. Filter for any log statement that includes .email, .id, .token, .session, JSON.stringify, or raw response variables. Those are your critical items.
  3. Check your error monitoring config (Sentry, Datadog, etc.) for sendDefaultPii, enableLogs, and any debug flags. Understand what is being forwarded.
  4. Introduce a shared logger utility and enforce its use with a linting rule. Even a simple no-restricted-syntax ESLint rule on console.log in browser-runtime files goes a long way.
  5. Adopt the convention that message strings never contain interpolated values. Move all variable context to the data argument, which your logger can redact automatically.

It is a fixable problem. It just tends to go unfixed until someone explicitly looks for it.

Frequently Asked Questions

Is logging to the browser console a GDPR risk?

It can be. If the console output contains personal data — email addresses, user IDs, or other identifiers — and that data is also being forwarded to a third-party monitoring tool via settings like sendDefaultPii or enableLogs, you are transmitting PII to a data processor without it being clearly documented in your privacy policy. At minimum, it is worth reviewing what your error monitoring platform receives.

Does this affect production users or only developers?

Any user can open DevTools on any page. It is not limited to developers. Support agents, power users, journalists, and security researchers routinely inspect the console. In regulated industries or for platforms handling financial or health data, an open console is a compliance issue regardless of user type.

Should I delete all console logs from my frontend?

Not necessarily — developer visibility is valuable. The goal is to control where detailed output lands. Server and terminal logs can remain verbose. Browser output should be safe by default: status labels, booleans, and counts only. A shared logger with environment-aware suppression and automatic field redaction achieves both goals without sacrificing developer experience.

What is the fastest way to find console leaks in a large codebase?

Grep for console.* across your browser-runtime directories and filter the results for known sensitive field names: .email, .id, .token, .session, JSON.stringify, and any variable named after a full object (user, profile, subscription, response). That will surface the highest-risk lines quickly, even in a large codebase.