Set Up Logs for Next.js
Send structured logs from your Next.js application to Sentry for debugging and observability.
Use curl to download, read and follow https://skills.sentry.dev/instrument to set up the Sentry Next.js SDK.Sentry Logs let you send structured log data from your Next.js application. Unlike traditional string-based logging, structured logs include queryable attributes that help you debug issues faster by filtering on specific users, orders, or any business context you include.
Logs are enabled by default across all three runtime files. Initialize the SDK as usual, then use Sentry.logger to send logs.
Logs work across all Next.js runtimes:
- Client — Browser-side logging
- Server — Node.js server-side logging
- Edge — Edge runtime logging
instrumentation-client.tsimport * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "___PUBLIC_DSN___",
});
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "___PUBLIC_DSN___",
});
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "___PUBLIC_DSN___",
});
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "___PUBLIC_DSN___",
});
Use the appropriate level for each message:
| Level | When to Use |
|---|---|
trace | Fine-grained debugging |
debug | Development diagnostics |
info | Normal operations, milestones |
warn | Potential issues, degraded state |
error | Failures that need attention |
fatal | Critical failures, system down |
import * as Sentry from "@sentry/nextjs";
// Different log levels
Sentry.logger.trace("Entering function", { fn: "processOrder" });
Sentry.logger.debug("Cache lookup", { key: "user:123" });
Sentry.logger.info("Order created", { orderId: "order_456" });
Sentry.logger.warn("Rate limit approaching", { current: 95, max: 100 });
Sentry.logger.error("Payment failed", { reason: "card_declined" });
Sentry.logger.fatal("Database unavailable", { host: "primary" });
import * as Sentry from "@sentry/nextjs";
// Different log levels
Sentry.logger.trace("Entering function", { fn: "processOrder" });
Sentry.logger.debug("Cache lookup", { key: "user:123" });
Sentry.logger.info("Order created", { orderId: "order_456" });
Sentry.logger.warn("Rate limit approaching", { current: 95, max: 100 });
Sentry.logger.error("Payment failed", { reason: "card_declined" });
Sentry.logger.fatal("Database unavailable", { host: "primary" });
Pass structured data as the second argument. These attributes become queryable columns in Sentry.
Use the fmt helper for parameterized messages — values are extracted as searchable attributes.
// Pass attributes directly
Sentry.logger.info("User signed up", {
userId: user.id,
plan: "pro",
referrer: "google",
});
// Use fmt for parameterized messages
Sentry.logger.info(
Sentry.logger.fmt`User ${userId} purchased ${productName}`,
);
// Pass attributes directly
Sentry.logger.info("User signed up", {
userId: user.id,
plan: "pro",
referrer: "google",
});
// Use fmt for parameterized messages
Sentry.logger.info(
Sentry.logger.fmt`User ${userId} purchased ${productName}`,
);
Available since: v10.61.0
Use Sentry.setAttribute and Sentry.setAttributes to attach attributes that are automatically included in all logs (as well as your metrics). They work just like Sentry.setTag and Sentry.setTags, but accept string, number, and boolean values. To target a specific scope, use the scope APIs.
See Different Kinds of Scopes to learn more.
// Applied to all logs and metrics
Sentry.setAttributes({
org_id: user.orgId,
user_tier: user.tier,
});
Sentry.setAttribute("service", "checkout");
// Global scope - shared across entire app
Sentry.getGlobalScope().setAttributes({
service: "checkout",
version: "2.1.0",
});
// Current scope - single operation
Sentry.withScope((scope) => {
scope.setAttribute("request_id", req.id);
Sentry.logger.info("Processing order");
});
// Applied to all logs and metrics
Sentry.setAttributes({
org_id: user.orgId,
user_tier: user.tier,
});
Sentry.setAttribute("service", "checkout");
// Global scope - shared across entire app
Sentry.getGlobalScope().setAttributes({
service: "checkout",
version: "2.1.0",
});
// Current scope - single operation
Sentry.withScope((scope) => {
scope.setAttribute("request_id", req.id);
Sentry.logger.info("Processing order");
});
Instead of many thin logs that are hard to correlate, emit one comprehensive log per operation with all relevant context.
This makes debugging dramatically faster — one query returns everything about a specific order, user, or request.
// ❌ Scattered thin logs
Sentry.logger.info("Starting checkout");
Sentry.logger.info("Validating cart");
Sentry.logger.info("Processing payment");
Sentry.logger.info("Checkout complete");
// ✅ One wide event with full context
Sentry.logger.info("Checkout completed", {
order_id: order.id,
user_id: user.id,
user_tier: user.subscription,
cart_value: cart.total,
item_count: cart.items.length,
payment_method: "stripe",
duration_ms: Date.now() - startTime,
});
// ❌ Scattered thin logs
Sentry.logger.info("Starting checkout");
Sentry.logger.info("Validating cart");
Sentry.logger.info("Processing payment");
Sentry.logger.info("Checkout complete");
// ✅ One wide event with full context
Sentry.logger.info("Checkout completed", {
order_id: order.id,
user_id: user.id,
user_tier: user.subscription,
cart_value: cart.total,
item_count: cart.items.length,
payment_method: "stripe",
duration_ms: Date.now() - startTime,
});
Add attributes that help you prioritize and debug:
- User context — tier, account age, lifetime value
- Transaction data — order value, item count
- Feature state — active feature flags
- Request metadata — endpoint, method, duration
This lets you filter logs by high-value customers or specific features.
Sentry.logger.info("API request completed", {
// User context
user_id: user.id,
user_tier: user.plan, // "free" | "pro" | "enterprise"
account_age_days: user.ageDays,
// Request data
endpoint: "/api/orders",
method: "POST",
duration_ms: 234,
// Business context
order_value: 149.99,
feature_flags: ["new-checkout", "discount-v2"],
});
Sentry.logger.info("API request completed", {
// User context
user_id: user.id,
user_tier: user.plan, // "free" | "pro" | "enterprise"
account_age_days: user.ageDays,
// Request data
endpoint: "/api/orders",
method: "POST",
duration_ms: 234,
// Business context
order_value: 149.99,
feature_flags: ["new-checkout", "discount-v2"],
});
Pick a naming convention and stick with it across your codebase. Inconsistent names make queries impossible.
Recommended: Use snake_case for custom attributes to match common conventions.
// ❌ Inconsistent naming
{ user: "123" }
{ userId: "123" }
{ user_id: "123" }
{ UserID: "123" }
// ✅ Consistent snake_case
{
user_id: "123",
order_id: "456",
cart_value: 99.99,
item_count: 3,
}
// ❌ Inconsistent naming
{ user: "123" }
{ userId: "123" }
{ user_id: "123" }
{ UserID: "123" }
// ✅ Consistent snake_case
{
user_id: "123",
order_id: "456",
cart_value: 99.99,
item_count: 3,
}
Capture console.log, console.warn, and console.error calls as structured logs.
The integration parses multiple arguments as searchable attributes.
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.consoleLoggingIntegration({
levels: ["log", "warn", "error"],
}),
],
});
// Arguments become searchable attributes
console.log("User action:", userId, success);
// -> message.parameter.0: userId
// -> message.parameter.1: success
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.consoleLoggingIntegration({
levels: ["log", "warn", "error"],
}),
],
});
// Arguments become searchable attributes
console.log("User action:", userId, success);
// -> message.parameter.0: userId
// -> message.parameter.1: success
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [Sentry.pinoIntegration()],
});
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [Sentry.pinoIntegration()],
});
See Pino integration docs for configuration options.
import { consola } from "consola";
const sentryReporter = Sentry.createConsolaReporter({
levels: ["error", "warn"], // optional filter
});
consola.addReporter(sentryReporter);
import { consola } from "consola";
const sentryReporter = Sentry.createConsolaReporter({
levels: ["error", "warn"], // optional filter
});
consola.addReporter(sentryReporter);
import winston from "winston";
import Transport from "winston-transport";
const SentryTransport = Sentry.createSentryWinstonTransport(Transport, {
levels: ["error", "warn"], // optional filter
});
const logger = winston.createLogger({
transports: [new SentryTransport()],
});
import winston from "winston";
import Transport from "winston-transport";
const SentryTransport = Sentry.createSentryWinstonTransport(Transport, {
levels: ["error", "warn"], // optional filter
});
const logger = winston.createLogger({
transports: [new SentryTransport()],
});
Filter or modify logs before they're sent. Return null to drop a log.
Use this to:
- Remove sensitive data
- Filter noisy logs
- Add computed attributes
instrumentation-client.tsSentry.init({
dsn: "___PUBLIC_DSN___",
beforeSendLog(log) {
// Drop debug logs in production
if (log.level === "debug") {
return null;
}
// Remove sensitive attributes
if (log.attributes?.password) {
delete log.attributes.password;
}
return log;
},
});
Sentry.init({
dsn: "___PUBLIC_DSN___",
beforeSendLog(log) {
// Drop debug logs in production
if (log.level === "debug") {
return null;
}
// Remove sensitive attributes
if (log.attributes?.password) {
delete log.attributes.password;
}
return log;
},
});
Sentry automatically adds these attributes to every log:
{
"message": "Order completed",
"level": "info",
"attributes": {
// Your custom attributes
"order_id": "order_123",
"user_tier": "pro",
// Core (always present)
"sentry.environment": "production",
"sentry.release": "my-app@1.2.3",
"sentry.sdk.name": "sentry.javascript.nextjs",
"sentry.sdk.version": "9.0.0",
// User (if set via Sentry.setUser)
"user.id": "user_456",
"user.email": "jane@example.com",
"user.name": "Jane",
// Browser (client-side only)
"browser.name": "Chrome",
"browser.version": "120.0.0",
// Server (server-side only)
"server.address": "api-server-1",
// Trace (if tracing enabled)
"sentry.trace.parent_span_id": "abc123",
"sentry.replay_id": "def456",
// Message (when using fmt)
"sentry.message.template": "Order {} completed",
"sentry.message.parameter.0": "order_123",
// Payload
"payload_size": 342
}
}
{
"message": "Order completed",
"level": "info",
"attributes": {
// Your custom attributes
"order_id": "order_123",
"user_tier": "pro",
// Core (always present)
"sentry.environment": "production",
"sentry.release": "my-app@1.2.3",
"sentry.sdk.name": "sentry.javascript.nextjs",
"sentry.sdk.version": "9.0.0",
// User (if set via Sentry.setUser)
"user.id": "user_456",
"user.email": "jane@example.com",
"user.name": "Jane",
// Browser (client-side only)
"browser.name": "Chrome",
"browser.version": "120.0.0",
// Server (server-side only)
"server.address": "api-server-1",
// Trace (if tracing enabled)
"sentry.trace.parent_span_id": "abc123",
"sentry.replay_id": "def456",
// Message (when using fmt)
"sentry.message.template": "Order {} completed",
"sentry.message.parameter.0": "order_123",
// Payload
"payload_size": 342
}
}
Any attributes set via Sentry.setAttribute() / Sentry.setAttributes() (or directly on a scope) are automatically included on all logs. Sentry.setAttribute(s) requires SDK version 10.61.0 or above. See Shared Attributes above for details.
Logs are captured as soon as you call a Sentry.logger.* method or add a logging integration. Check that the runtime you're logging from initializes the SDK at all:
instrumentation-client.ts(client)sentry.server.config.ts(server)sentry.edge.config.ts(edge)
Logs larger than 1 MB are dropped. Check your org stats to see if logs are being rate limited or dropped.
If attributes show [Filtered], they're being removed by server-side data scrubbing. Check your project's data scrubbing settings to adjust what gets filtered.
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").