Secure Logging Practices for React Applications
Logging in React is essential for debugging, but careless logging exposes secrets, authentication tokens, and personally identifiable information (PII) to your monitoring service, browser console history, and anyone with access to your logs. A single console.log(apiResponse) that includes an authorization header is a data breach waiting to happen. This guide covers how to implement structured logging in React, what data is safe to log, how to configure log levels for production, and how to integrate error tracking services like Sentry without exposing sensitive data.
What You Must Never Log
Never log these categories of data:
| Data Type | Example | Why Dangerous |
|---|---|---|
| API Keys and Tokens | sk_live_abc123, Bearer xyz789 | Can be used to impersonate your app |
| Passwords and Credentials | User password, OAuth token | Direct account compromise |
| Personal Information (PII) | Email, SSN, phone, credit card | GDPR/CCPA violation, identity theft |
| Session Identifiers | Cookies, session tokens | Session hijacking |
| Request/Response Headers | Authorization, X-API-Key | Secrets travel in headers |
| Full User Objects | User profile data with email | Privacy violation |
| Sensitive Form Data | Credit card numbers, bank details | Payment fraud, GDPR violation |
A typical mistake:
// UNSAFE: Do NOT do this
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
console.log("User data:", user); // Logs entire user object with email, phone, etc.
return user;
}
If user includes an email address or phone number, you have just logged PII. If a user's browser history is examined or your logs are accessed, this data is exposed.
Structured Logging: Logging the Right Data
Structured logging means logging specific, safe fields instead of entire objects. Log the intent and context, not the data:
// Safe: Log only what you need
async function fetchUserData(userId) {
try {
console.log("[UserAPI] Fetching user", { userId });
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
console.error("[UserAPI] Failed to fetch user", {
userId,
status: response.status,
statusText: response.statusText
});
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json();
console.log("[UserAPI] User fetched successfully", { userId, userType: user.type });
return user;
} catch (error) {
console.error("[UserAPI] Exception", { userId, error: error.message });
throw error;
}
}
This log includes the action (fetching), the relevant context (userId, status), and safe metadata (userType), but not the email, phone, or other PII.
Log Levels and Environment-Based Filtering
Most logging libraries support levels: DEBUG, INFO, WARN, ERROR. In development, log everything; in production, log only WARN and ERROR to reduce noise and avoid exposing debugging data:
// logger.js
const logLevel = process.env.NODE_ENV === "development" ? "DEBUG" : "ERROR";
const logger = {
debug: (msg, data) => {
if (logLevel === "DEBUG") console.log(`[DEBUG] ${msg}`, data);
},
info: (msg, data) => {
if (["DEBUG", "INFO"].includes(logLevel)) console.log(`[INFO] ${msg}`, data);
},
warn: (msg, data) => {
if (["DEBUG", "INFO", "WARN"].includes(logLevel)) console.warn(`[WARN] ${msg}`, data);
},
error: (msg, data) => {
console.error(`[ERROR] ${msg}`, data);
}
};
export default logger;
In development:
logger.debug("User component mounted", { userId: 123 });
logger.info("API call started", { endpoint: "/users/123" });
logger.warn("Slow response detected", { duration: 2500 });
logger.error("Failed to fetch", { error: "Network error" });
// All are printed
In production:
logger.debug("User component mounted", { userId: 123 }); // Suppressed
logger.info("API call started", { endpoint: "/users/123" }); // Suppressed
logger.warn("Slow response detected", { duration: 2500 }); // Printed
logger.error("Failed to fetch", { error: "Network error" }); // Printed
Error Tracking Services: Sentry, LogRocket, and Rollbar
Error tracking services aggregate errors from your users' browsers and alert you to issues. However, they also capture console logs and request payloads, which can expose secrets. When integrating Sentry (the most popular error tracker), configure it to strip sensitive data:
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN, // Public DSN only, safe to expose
environment: process.env.NODE_ENV,
integrations: [
// Sentry's built-in integrations
new Sentry.Replay()
],
// Strip sensitive data before sending
beforeSend(event, hint) {
// Remove Authorization headers from requests
if (event.request) {
delete event.request.headers?.Authorization;
delete event.request.headers?.["X-API-Key"];
delete event.request.headers?.["X-Auth-Token"];
}
// Redact PII from breadcrumbs and messages
if (event.breadcrumbs) {
event.breadcrumbs = event.breadcrumbs.map(crumb => {
if (crumb.message && crumb.message.includes("@")) {
// Likely contains email, redact it
crumb.message = crumb.message.replace(/[\w.-]+@[\w.-]+\.\w+/g, "[email redacted]");
}
return crumb;
});
}
return event;
},
// Configure which data to capture
tracesSampleRate: 0.1, // Sample 10% of transactions to reduce noise
denyUrls: [
// Don't track errors from these origins (e.g., browser extensions, third-party scripts)
/extensions\//i,
/moz-extension\//i
]
});
The key is the beforeSend hook: it receives every error before Sentry sends it, allowing you to redact secrets, remove PII, and filter noise.
Redacting Sensitive Data from Logs
For any logging library, use a redaction function to automatically strip secrets:
function redactSecrets(data) {
const secretPatterns = [
/Bearer\s+[\w.-]+/gi, // Bearer tokens
/sk_test_\w+|sk_live_\w+/gi, // Stripe keys
/pk_test_\w+|pk_live_\w+/gi,
/AIza[\w-]+/gi, // Google API keys
/ghp_[\w]+/gi, // GitHub tokens
/[\w.-]+@[\w.-]+\.\w+/g // Email addresses
];
let stringified = JSON.stringify(data);
secretPatterns.forEach(pattern => {
stringified = stringified.replace(pattern, "[REDACTED]");
});
try {
return JSON.parse(stringified);
} catch {
return stringified;
}
}
// Usage
const apiResponse = {
status: 200,
authToken: "Bearer sk_live_abc123",
user: { email: "[email protected]" }
};
console.log(redactSecrets(apiResponse));
// Output: { status: 200, authToken: "[REDACTED]", user: { email: "[REDACTED]" } }
Browser Console History and Local Storage
The browser's console is an underappreciated logging target. If a user has a malware extension or visits a malicious website, attackers can access the console history (via console.log function overrides). Never log secrets to the console in production, and regularly clear user's localStorage:
// React component lifecycle cleanup
useEffect(() => {
return () => {
// Clear sensitive data from localStorage when the component unmounts
localStorage.removeItem("authToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("apiKey");
};
}, []);
// Global cleanup on logout
function handleLogout() {
localStorage.clear(); // Or selectively clear sensitive keys
sessionStorage.clear();
window.location.href = "/login";
}
Network Request Logging
HTTP libraries like axios can log request/response data. Configure them to exclude headers:
import axios from "axios";
// Axios interceptor to redact sensitive headers
axios.interceptors.request.use(config => {
const safeConfig = { ...config };
if (safeConfig.headers) {
delete safeConfig.headers.Authorization;
delete safeConfig.headers["X-API-Key"];
}
console.log("[API] Request:", safeConfig);
return config;
});
axios.interceptors.response.use(response => {
// Log only safe parts of the response
console.log("[API] Response:", {
status: response.status,
statusText: response.statusText
});
return response;
});
Key Takeaways
- Never log API keys, tokens, passwords, emails, or any PII.
- Use structured logging: log context and intent, not entire objects.
- Implement environment-based log levels: DEBUG in development, ERROR/WARN in production.
- Configure error tracking services (Sentry, LogRocket) to redact sensitive headers and data.
- Regularly audit your logs for accidental secret exposure.
- Clear sensitive data from localStorage and sessionStorage on logout.
- Redact secrets automatically before passing data to console.log or external services.
Frequently Asked Questions
If I log a URL query parameter that contains a token, is that safe?
No. Query parameters are visible in logs, browser history, and access logs. Never pass tokens in query parameters; use HTTP headers (Authorization) instead. If you must include tokens, log only the token's first few characters (e.g., token_prefix: sk_test_1234...).
Can I use localStorage to store error logs instead of sending them to Sentry?
You can, but it is risky. If localStorage is compromised or the user's device is stolen, logs with PII are exposed. If you must store logs locally, encrypt them and delete them after 24 hours. Generally, send logs only to a secure backend under your control.
What if a third-party library logs something I don't control?
Audit third-party libraries for logging behavior. Some (like react-query or Apollo Client) log GraphQL queries, which might contain sensitive data. Override their loggers or disable them in production. Check the library's documentation for logging configuration options.
How do I find secrets that are already in my logs?
Use a log searching tool or SIEM (Security Information Event Management) system to search for patterns: sk_live_, Bearer, ghp_, etc. Tools like CloudFlare, Datadog, and Splunk can search logs retroactively. If you find secrets, assume they are compromised and rotate them.
Is it okay to log error stack traces in production?
Yes, error stack traces are usually safe (they show code file names and line numbers, not data). However, if your stack trace includes file paths that reveal your infrastructure, sanitize those. Always log the error message and stack trace; they are invaluable for debugging.