文章 代码库 城市生活记忆 Claude Skill AI分享 问龙虾
返回 Claude Skill

Sentry Next.js SDK

Sentry 错误监控 Next.js SDK 集成,服务端和客户端错误追踪

DevOps 社区公开 by Community

Sentry Next.js SDK

Opinionated wizard that scans your Next.js project and guides you through complete Sentry setup across all three runtimes: browser, Node.js server, and Edge.

Invoke This Skill When

  • User asks to “add Sentry to Next.js” or “set up Sentry” in a Next.js app
  • User wants to install or configure @sentry/nextjs
  • User wants error monitoring, tracing, session replay, logging, or profiling for Next.js
  • User asks about instrumentation.ts, withSentryConfig(), or global-error.tsx
  • User wants to capture server actions, server component errors, or edge runtime errors

Note: SDK versions and APIs below reflect current Sentry docs at time of writing (@sentry/nextjs ≥8.28.0). Always verify against docs.sentry.io/platforms/javascript/guides/nextjs/ before implementing.


Phase 1: Detect

Run these commands to understand the project before making any recommendations:

# Detect Next.js version and existing Sentry
cat package.json | grep -E '"next"|"@sentry/'

# Detect router type (App Router vs Pages Router)
ls src/app app src/pages pages 2>/dev/null

# Check for existing Sentry config files
ls instrumentation.ts instrumentation-client.ts sentry.server.config.ts sentry.edge.config.ts 2>/dev/null
ls src/instrumentation.ts src/instrumentation-client.ts 2>/dev/null

# Check next.config
ls next.config.ts next.config.js next.config.mjs 2>/dev/null

# Check for existing error boundaries
find . -name "global-error.tsx" -o -name "_error.tsx" 2>/dev/null | grep -v node_modules

# Check build tool
cat package.json | grep -E '"turbopack"|"webpack"'

# Check for logging libraries
cat package.json | grep -E '"pino"|"winston"|"bunyan"'

# Check for companion backend
ls ../backend ../server ../api 2>/dev/null
cat ../go.mod ../requirements.txt ../Gemfile 2>/dev/null | head -3

What to determine:

QuestionImpact
Next.js version?13+ required; 15+ needed for Turbopack support
App Router or Pages Router?Determines error boundary files needed (global-error.tsx vs _error.tsx)
@sentry/nextjs already present?Skip install, go to feature config
Existing instrumentation.ts?Merge Sentry into it rather than replace
Turbopack in use?Tree-shaking in withSentryConfig is webpack-only
Logging library detected?Recommend Sentry Logs integration
Backend directory found?Trigger Phase 4 cross-link suggestion

Phase 2: Recommend

Present a concrete recommendation based on what you found. Don’t ask open-ended questions — lead with a proposal:

Recommended (core coverage):

  • Error Monitoring — always; captures server errors, client errors, server actions, and unhandled promise rejections
  • Tracing — server-side request tracing + client-side navigation spans across all runtimes
  • Session Replay — recommended for user-facing apps; records sessions around errors

Optional (enhanced observability):

  • Logging — structured logs via Sentry.logger.*; recommend when pino/winston or log search is needed
  • Profiling — continuous profiling; requires Document-Policy: js-profiling header
  • AI Monitoring — OpenAI, Vercel AI SDK, Anthropic; recommend when AI/LLM calls detected
  • Crons — detect missed/failed scheduled jobs; recommend when cron patterns detected
  • Metrics — custom metrics via Sentry.metrics.*; recommend when custom KPIs or business metrics needed

Recommendation logic:

FeatureRecommend when…
Error MonitoringAlways — non-negotiable baseline
TracingAlways for Next.js — server route tracing + client navigation are high-value
Session ReplayUser-facing app, login flows, or checkout pages
LoggingApp uses structured logging or needs log-to-trace correlation
ProfilingPerformance-critical app; client sets Document-Policy: js-profiling
AI MonitoringApp makes OpenAI, Vercel AI SDK, or Anthropic calls
CronsApp has Vercel Cron jobs, scheduled API routes, or node-cron usage
MetricsApp needs custom counters, gauges, or histograms via Sentry.metrics.*

Propose: “I recommend setting up Error Monitoring + Tracing + Session Replay. Want me to also add Logging or Profiling?”


Phase 3: Guide

npx @sentry/wizard@latest -i nextjs

The wizard walks you through login, org/project selection, and auth token setup interactively — no manual token creation needed. It then installs the SDK, creates all necessary config files (instrumentation-client.ts, sentry.server.config.ts, sentry.edge.config.ts, instrumentation.ts), wraps next.config.ts with withSentryConfig(), configures source map upload, and adds a /sentry-example-page for verification.

Skip to Verification after running the wizard.


Option 2: Manual Setup

Install

npm install @sentry/nextjs --save

Create instrumentation-client.ts — Browser / Client Runtime

Older docs used sentry.client.config.ts — the current pattern is instrumentation-client.ts.

import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN ?? "___PUBLIC_DSN___",

  sendDefaultPii: true,

  // 100% in dev, 10% in production
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,

  // Session Replay: 10% of all sessions, 100% of sessions with errors
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,

  enableLogs: true,

  integrations: [
    Sentry.replayIntegration(),
    // Optional: user feedback widget
    // Sentry.feedbackIntegration({ colorScheme: "system" }),
  ],
});

// Hook into App Router navigation transitions (App Router only)
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;

Create sentry.server.config.ts — Node.js Server Runtime

import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN ?? "___DSN___",

  sendDefaultPii: true,
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,

  // Attach local variable values to stack frames
  includeLocalVariables: true,

  enableLogs: true,
});

Create sentry.edge.config.ts — Edge Runtime

import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN ?? "___DSN___",

  sendDefaultPii: true,
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,

  enableLogs: true,
});

Create instrumentation.ts — Server-Side Registration Hook

Requires experimental.instrumentationHook: true in next.config for Next.js < 14.0.4. It’s stable in 14.0.4+.

import * as Sentry from "@sentry/nextjs";

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./sentry.server.config");
  }

  if (process.env.NEXT_RUNTIME === "edge") {
    await import("./sentry.edge.config");
  }
}

// Automatically captures all unhandled server-side request errors
// Requires @sentry/nextjs >= 8.28.0
export const onRequestError = Sentry.captureRequestError;

Runtime dispatch:

NEXT_RUNTIMEConfig file loaded
"nodejs"sentry.server.config.ts
"edge"sentry.edge.config.ts
(client bundle)instrumentation-client.ts (Next.js handles this directly)

App Router: Create app/global-error.tsx

This catches errors in the root layout and React render errors:

"use client";

import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";

export default function GlobalError({
  error,
}: {
  error: Error & { digest?: string };
}) {
  useEffect(() => {
    Sentry.captureException(error);
  }, [error]);

  return (
    <html>
      <body>
        <NextError statusCode={0} />
      </body>
    </html>
  );
}

Pages Router: Update pages/_error.tsx

import * as Sentry from "@sentry/nextjs";
import type { NextPageContext } from "next";
import NextErrorComponent from "next/error";

type ErrorProps = { statusCode: number };

export default function CustomError({ statusCode }: ErrorProps) {
  return <NextErrorComponent statusCode={statusCode} />;
}

CustomError.getInitialProps = async (ctx: NextPageContext) => {
  await Sentry.captureUnderscoreErrorException(ctx);
  return NextErrorComponent.getInitialProps(ctx);
};

Wrap next.config.ts with withSentryConfig()

import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";

const nextConfig: NextConfig = {
  // your existing Next.js config
};

export default withSentryConfig(nextConfig, {
  org: "___ORG_SLUG___",
  project: "___PROJECT_SLUG___",

  // Source map upload auth token (see Source Maps section below)
  authToken: process.env.SENTRY_AUTH_TOKEN,

  // Upload wider set of client source files for better stack trace resolution
  widenClientFileUpload: true,

  // Create a proxy API route to bypass ad-blockers
  tunnelRoute: "/monitoring",

  // Suppress non-CI output
  silent: !process.env.CI,
});

Exclude Tunnel Route from Middleware

If you have middleware.ts, exclude the tunnel path from auth or redirect logic:

// middleware.ts
export const config = {
  matcher: [
    // Exclude monitoring route, Next.js internals, and static files
    "/((?!monitoring|_next/static|_next/image|favicon.ico).*)",
  ],
};

Source Maps Setup

Source maps make production stack traces readable — without them, you see minified code. This is non-negotiable for production apps.

Step 1: Generate a Sentry auth token

Go to sentry.io/settings/auth-tokens/ and create a token with project:releases and org:read scopes.

Step 2: Set environment variables

# .env.sentry-build-plugin  (gitignore this file)
SENTRY_AUTH_TOKEN=sntrys_eyJ...

Or set in CI secrets:

SENTRY_AUTH_TOKEN=sntrys_eyJ...
SENTRY_ORG=my-org        # optional if set in next.config
SENTRY_PROJECT=my-project # optional if set in next.config

Step 3: Add to .gitignore

.env.sentry-build-plugin

Step 4: Verify authToken is wired in next.config.ts

withSentryConfig(nextConfig, {
  org: "my-org",
  project: "my-project",
  authToken: process.env.SENTRY_AUTH_TOKEN, // reads from .env.sentry-build-plugin or CI env
  widenClientFileUpload: true,
});

Source maps are uploaded automatically on every next build.


For Each Agreed Feature

Load the corresponding reference file and follow its steps:

FeatureReference fileLoad when…
Error Monitoringreferences/error-monitoring.mdAlways (baseline) — App Router error boundaries, Pages Router _error.tsx, server action wrapping
Tracingreferences/tracing.mdServer-side request tracing, client navigation, distributed tracing, tracePropagationTargets
Session Replayreferences/session-replay.mdUser-facing app; privacy masking, canvas recording, network capture
Loggingreferences/logging.mdStructured logs, Sentry.logger.*, log-to-trace correlation
Profilingreferences/profiling.mdContinuous profiling, Document-Policy header, nodeProfilingIntegration
AI Monitoringreferences/ai-monitoring.mdApp uses OpenAI, Vercel AI SDK, or Anthropic
Cronsreferences/crons.mdVercel Cron, scheduled API routes, node-cron

For each feature: read the reference file, follow its steps exactly, and verify before moving on.


Verification

After wizard or manual setup, verify Sentry is working:

// Add temporarily to a server action or API route, then remove
import * as Sentry from "@sentry/nextjs";

throw new Error("Sentry test error — delete me");
// or
Sentry.captureException(new Error("Sentry test error — delete me"));

Then check your Sentry Issues dashboard — the error should appear within ~30 seconds.

Verification checklist:

CheckHow
Client errors capturedThrow in a client component, verify in Sentry
Server errors capturedThrow in a server action or API route
Edge errors capturedThrow in middleware or edge route handler
Source maps workingCheck stack trace shows readable file names
Session Replay workingCheck Replays tab in Sentry dashboard

Config Reference

Sentry.init() Options

OptionTypeDefaultNotes
dsnstringRequired. Use NEXT_PUBLIC_SENTRY_DSN for client, SENTRY_DSN for server
tracesSampleRatenumber0–1; 1.0 in dev, 0.1 in prod recommended
replaysSessionSampleRatenumber0.1Fraction of all sessions recorded
replaysOnErrorSampleRatenumber1.0Fraction of error sessions recorded
sendDefaultPiibooleanfalseInclude IP, request headers in events
includeLocalVariablesbooleanfalseAttach local variable values to stack frames (server only)
enableLogsbooleanfalseEnable Sentry Logs product
environmentstringauto"production", "staging", etc.
releasestringautoSet to commit SHA or version tag
debugbooleanfalseLog SDK activity to console

withSentryConfig() Options

OptionTypeNotes
orgstringSentry organization slug
projectstringSentry project slug
authTokenstringSource map upload token (SENTRY_AUTH_TOKEN)
widenClientFileUploadbooleanUpload more client files for better stack traces
tunnelRoutestringAPI route path for ad-blocker bypass (e.g. "/monitoring")
silentbooleanSuppress build output (!process.env.CI recommended)
webpack.treeshake.*objectTree-shake SDK features (webpack only, not Turbopack)

Environment Variables

VariableRuntimePurpose
NEXT_PUBLIC_SENTRY_DSNClientDSN for browser Sentry init (public)
SENTRY_DSNServer / EdgeDSN for server/edge Sentry init
SENTRY_AUTH_TOKENBuildSource map upload auth token (secret)
SENTRY_ORGBuildOrg slug (alternative to org in config)
SENTRY_PROJECTBuildProject slug (alternative to project in config)
SENTRY_RELEASEServerRelease version string (auto-detected from git)
NEXT_RUNTIMEServer / Edge"nodejs" or "edge" (set by Next.js internally)

After completing Next.js setup, check for companion services:

# Check for backend services in adjacent directories
ls ../backend ../server ../api ../services 2>/dev/null

# Check for backend language indicators
cat ../go.mod 2>/dev/null | head -3
cat ../requirements.txt ../pyproject.toml 2>/dev/null | head -3
cat ../Gemfile 2>/dev/null | head -3
cat ../pom.xml ../build.gradle 2>/dev/null | head -3

If a backend is found, suggest the matching SDK skill:

Backend detectedSuggest skill
Go (go.mod)sentry-go-sdk
Python (requirements.txt, pyproject.toml)sentry-python-sdk
Ruby (Gemfile)sentry-ruby-sdk
Java/Kotlin (pom.xml, build.gradle)See docs.sentry.io/platforms/java/
Node.js (Express, Fastify, Hapi)@sentry/node — see docs.sentry.io/platforms/javascript/guides/express/

Connecting frontend and backend with the same DSN or linked projects enables distributed tracing — stack traces that span your browser, Next.js server, and backend API in a single trace view.


Troubleshooting

IssueCauseSolution
Events not appearingDSN misconfigured or debug: false hiding errorsSet debug: true temporarily; check browser network tab for requests to sentry.io
Stack traces show minified codeSource maps not uploadingCheck SENTRY_AUTH_TOKEN is set; run next build and look for “Source Maps” in build output
onRequestError not firingSDK version < 8.28.0Upgrade: npm install @sentry/nextjs@latest
Edge runtime errors missingsentry.edge.config.ts not loadedVerify instrumentation.ts imports it when NEXT_RUNTIME === "edge"
Tunnel route returns 404tunnelRoute set but Next.js route missingThe plugin creates it automatically; check you ran next build after adding tunnelRoute
withSentryConfig tree-shaking breaks buildTurbopack in useTree-shaking options only work with webpack; remove webpack.treeshake options when using Turbopack
global-error.tsx not catching errorsMissing "use client" directiveAdd "use client" as the very first line of global-error.tsx
Session Replay not recordingreplayIntegration() missing from client initAdd Sentry.replayIntegration() to integrations in instrumentation-client.ts

Reference: Ai Monitoring

AI Monitoring — Sentry Next.js SDK

OpenAI integration: @sentry/nextjs ≥10.28.0+
Vercel AI SDK integration: ≥10.6.0+ (Node/Edge/Bun), ≥10.12.0+ (Deno)
Anthropic integration: see platform docs

⚠️ Tracing must be enabled. AI monitoring piggybacks on tracing infrastructure. tracesSampleRate must be > 0.


Overview

Sentry AI Agents Monitoring automatically tracks:

  • Agent runs and error rates
  • LLM calls (model, token counts, estimated cost)
  • Tool calls and outputs
  • Agent handoffs
  • Full prompt/completion data (opt-in)
  • Performance bottlenecks across the AI pipeline

Supported AI Libraries

LibraryIntegration APIAuto-enabled (Node server)?Min SDK Version
OpenAI (openai)openAIIntegration / instrumentOpenAiClient✅ Yes10.28.0
Vercel AI SDK (ai)vercelAIIntegration✅ Yes (Node), ❌ Edge manual10.6.0
Anthropic (@anthropic-ai/sdk)anthropicAIIntegration / instrumentAnthropicAiClient✅ YesSee platform docs

OpenAI Integration

Which API to Use?

ContextAPI
Next.js server-side (API routes, Server Components, Route Handlers)Sentry.openAIIntegration() in sentry.server.config.ts
Browser / client-sideSentry.instrumentOpenAiClient(openaiInstance) — manual wrapper

Server-Side Setup (sentry.server.config.ts)

openAIIntegration is enabled by default on the server. Pass it explicitly to customize options:

// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,

  // Tracing MUST be enabled for AI monitoring
  tracesSampleRate: 1.0,

  integrations: [
    Sentry.openAIIntegration({
      recordInputs: true,   // capture prompts sent to OpenAI
      recordOutputs: true,  // capture completions from OpenAI
    }),
  ],
});

Client-Side / Manual Wrapping

import OpenAI from "openai";
import * as Sentry from "@sentry/nextjs";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY, // ⚠️ Never expose this in the browser!
});

// Wrap once at module level — reuse this client everywhere
const client = Sentry.instrumentOpenAiClient(openai, {
  recordInputs: true,
  recordOutputs: true,
});

const response = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello!" }],
});

Streaming — Important

For streamed responses, you must pass stream_options: { include_usage: true }. Without this, OpenAI does not include token counts in streamed responses, so Sentry cannot capture usage metrics:

const stream = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Hello!" }],
  stream: true,
  stream_options: { include_usage: true }, // ← REQUIRED for token tracking
});

OpenAI Configuration Options

OptionTypeDefaultDescription
recordInputsbooleantrue if sendDefaultPii: trueCapture prompts/messages sent to OpenAI
recordOutputsbooleantrue if sendDefaultPii: trueCapture generated text/responses

Supported versions: openai ≥4.0.0 <7


Vercel AI SDK Integration

Setup

The integration is auto-enabled in the Node runtime. For the Edge runtime, add it explicitly:

// sentry.server.config.ts — Node runtime (auto-enabled, customize here)
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
  integrations: [
    Sentry.vercelAIIntegration({
      force: true, // ← Required for Vercel production deployments (see note below)
      recordInputs: true,
      recordOutputs: true,
    }),
  ],
});
// sentry.edge.config.ts — Edge runtime requires manual opt-in
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
  integrations: [
    Sentry.vercelAIIntegration(),
  ],
});

Per-Call Telemetry (Required)

You must pass experimental_telemetry: { isEnabled: true } to every AI SDK function call you want traced:

import { generateText, generateObject, streamText } from "ai";
import { openai } from "@ai-sdk/openai";

// generateText
const result = await generateText({
  model: openai("gpt-4o"),
  prompt: "What is the capital of France?",
  experimental_telemetry: {
    isEnabled: true,
    functionId: "my-text-generation",  // helps identify this function in traces
    recordInputs: true,
    recordOutputs: true,
  },
});

// generateObject
const { object } = await generateObject({
  model: openai("gpt-4o"),
  schema: z.object({ answer: z.string() }),
  prompt: "...",
  experimental_telemetry: { isEnabled: true, functionId: "my-object-gen" },
});

// streamText
const { textStream } = await streamText({
  model: openai("gpt-4o"),
  prompt: "...",
  experimental_telemetry: { isEnabled: true, functionId: "my-stream" },
});

Vercel Production: force: true

When deployed to Vercel, the ai package gets bundled in Next.js production builds. This prevents automatic module detection, causing spans to use raw names (ai.toolCall, ai.streamText) instead of semantic names (gen_ai.execute_tool, gen_ai.stream_text).

Fix — always use force: true in sentry.server.config.ts when deploying to Vercel:

Sentry.vercelAIIntegration({ force: true })

Vercel AI SDK Configuration Options

OptionTypeDefaultMin SDKDescription
forcebooleanfalse9.29.0Force-enable regardless of module detection. Use on Vercel.
recordInputsbooleantrue*9.27.0Capture inputs. *Defaults to true when sendDefaultPii: true.
recordOutputsbooleantrue*9.27.0Capture outputs. *Defaults to true when sendDefaultPii: true.

Supported versions: ai ≥3.0.0 ≤6


Anthropic Integration

Server-Side Setup

// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
  integrations: [
    Sentry.anthropicAIIntegration({
      recordInputs: true,
      recordOutputs: true,
    }),
  ],
});

Manual Wrapping

import Anthropic from "@anthropic-ai/sdk";
import * as Sentry from "@sentry/nextjs";

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY, // ⚠️ Never expose in the browser!
});

const client = Sentry.instrumentAnthropicAiClient(anthropic, {
  recordInputs: true,
  recordOutputs: true,
});

const response = await client.messages.create({
  model: "claude-3-5-sonnet-20241022",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello, Claude!" }],
});

Supported Anthropic Operations

OperationMethod
Create messagesclient.messages.create()
Stream messagesclient.messages.stream()
Count tokensclient.messages.countTokens()
Legacy completionsclient.completions.create()
Beta messagesclient.beta.messages.create()

Supported versions: @anthropic-ai/sdk ≥0.19.2 <1.0.0


Token Usage Tracking

Sentry automatically captures token usage following OpenTelemetry GenAI semantic conventions:

Span AttributeDescription
gen_ai.request.modelModel name
gen_ai.usage.input_tokensPrompt/input token count
gen_ai.usage.output_tokensCompletion/output token count
gen_ai.usage.input_tokens.cachedCached input tokens
gen_ai.usage.input_tokens.cache_writeCache write tokens
gen_ai.usage.output_tokens.reasoningReasoning tokens (e.g., o1 models)

Cost estimates are sourced from models.dev and OpenRouter. Limitations: no volume discounts, no non-token charges, unrecognized models show no estimate.


Prompt/Completion Capture & PII

recordInputs captures prompts sent to the AI API.
recordOutputs captures the generated text/completions returned.

Both default to true only when sendDefaultPii: true is set:

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  sendDefaultPii: true, // ← enables input/output recording by default
  tracesSampleRate: 1.0,
});

Or enable explicitly without sendDefaultPii:

integrations: [
  Sentry.openAIIntegration({
    recordInputs: true,   // explicitly opt in
    recordOutputs: true,
  }),
],

⚠️ PII warning: Prompts often contain user-supplied text. If users include personal data in prompts, enabling recordInputs will send that data to Sentry. Review your privacy policy before enabling.


Complete Setup Example

// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
  integrations: [
    Sentry.openAIIntegration({ recordInputs: true, recordOutputs: true }),
    Sentry.vercelAIIntegration({ force: true, recordInputs: true, recordOutputs: true }),
    Sentry.anthropicAIIntegration({ recordInputs: true, recordOutputs: true }),
  ],
});
// app/api/chat/route.ts — OpenAI with streaming
import OpenAI from "openai";
import * as Sentry from "@sentry/nextjs";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const sentryOpenAI = Sentry.instrumentOpenAiClient(openai);

export async function POST(req: Request) {
  const { messages } = await req.json();

  const completion = await sentryOpenAI.chat.completions.create({
    model: "gpt-4o",
    messages,
    stream: true,
    stream_options: { include_usage: true }, // ← Required for token tracking
  });

  // ... stream response to client
}
// app/api/generate/route.ts — Vercel AI SDK
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

export async function POST(req: Request) {
  const { prompt } = await req.json();

  const result = await generateText({
    model: openai("gpt-4o"),
    prompt,
    experimental_telemetry: {
      isEnabled: true,            // ← Required for Sentry to capture spans
      functionId: "chat-handler",
      recordInputs: true,
      recordOutputs: true,
    },
  });

  return Response.json({ text: result.text });
}

AI Agents Dashboard

Access at Sentry → AI → Agents (or Insights → AI).

TabWhat you see
OverviewAgent runs, error rates, duration, LLM calls, tokens used, tool calls
ModelsPer-model cost estimates, token breakdown (input/output/cached), duration
ToolsPer-tool call counts, error rates, input/output for each invocation
TracesFull pipeline from user request to final response with all spans

SDK Version Matrix

FeatureMin SDK Version
Vercel AI SDK integration (Node/CF/Edge/Bun)10.6.0
Vercel AI SDK integration (Deno)10.12.0
Vercel AI recordInputs/recordOutputs9.27.0
Vercel AI force option9.29.0
OpenAI integration (openAIIntegration / instrumentOpenAiClient)10.28.0

Troubleshooting

IssueSolution
No AI spans appearingVerify tracesSampleRate > 0; AI monitoring requires tracing
Token counts missing in streamsAdd stream_options: { include_usage: true } to all OpenAI streaming calls
Vercel AI spans show raw names (ai.toolCall)Add vercelAIIntegration({ force: true }) in server config
recordInputs/recordOutputs not capturingSet sendDefaultPii: true or explicitly pass recordInputs: true to the integration
Anthropic spans missingCheck SDK version supports Anthropic integration; add anthropicAIIntegration() explicitly
Cost estimates not showingModel name must match models.dev/OpenRouter pricing data; custom/fine-tuned models may show no estimate
Edge runtime AI spans missingAdd vercelAIIntegration() to sentry.edge.config.ts explicitly (not auto-enabled for Edge)
OpenAI browser-side spans missingUse instrumentOpenAiClient() wrapper — openAIIntegration() only works server-side
No data in AI Agents dashboardEnsure traces are being sent; check DSN and tracesSampleRate

Reference: Crons

Crons — Sentry Next.js SDK

Minimum SDK: @sentry/nextjs ≥7.51.1+ for captureCheckIn
Sentry.withMonitor(): ≥7.76.0+
Cron library auto-instrumentation: ≥7.92.0+

⚠️ Server and Edge runtimes only. Cron monitoring is not available in the browser runtime.


Overview

Sentry Cron Monitoring detects:

  • Missed check-ins — job didn’t run at the expected time
  • Runtime failures — job ran but encountered an error
  • Timeouts — job exceeded maxRuntime without completing

Option A: Automatic Vercel Cron Integration

For Vercel-hosted Next.js apps using Vercel Cron Jobs:

// next.config.js
const { withSentryConfig } = require("@sentry/nextjs");

module.exports = withSentryConfig(nextConfig, {
  automaticVercelMonitors: true,
});

⚠️ Critical limitation: automaticVercelMonitors only works with the Pages Router. App Router route handlers are NOT yet supported for automatic instrumentation. Use captureCheckIn or withMonitor manually for App Router cron routes.


Option B: Auto-Instrumentation of Cron Libraries (SDK ≥7.92.0)

cron npm package

import { CronJob } from "cron";
import * as Sentry from "@sentry/nextjs";

const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, "my-cron-job");

const job = new CronJobWithCheckIn("* * * * *", () => {
  console.log("Runs every minute");
});

// Or via .from() factory:
const job2 = CronJobWithCheckIn.from({
  cronTime: "* * * * *",
  onTick: () => console.log("Runs every minute"),
});

node-cron npm package

import cron from "node-cron";
import * as Sentry from "@sentry/nextjs";

const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron);

cronWithCheckIn.schedule(
  "* * * * *",
  () => {
    console.log("Running every minute");
  },
  { name: "my-cron-job" }, // ← name is required for Sentry monitoring
);

node-schedule npm package (SDK ≥7.93.0)

import * as schedule from "node-schedule";
import * as Sentry from "@sentry/nextjs";

const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule);

scheduleWithCheckIn.scheduleJob(
  "my-cron-job",   // ← first arg is the monitor slug
  "* * * * *",
  () => {
    console.log("Running every minute");
  },
);

⚠️ node-schedule instrumentation only supports cron string format. Date objects and RecurrenceRule objects are not supported.


Option C: Sentry.withMonitor() Wrapper (SDK ≥7.76.0)

Wraps any callback and automatically sends in_progressok/error check-ins:

import * as Sentry from "@sentry/nextjs";

// Basic usage — monitor must already exist in Sentry
await Sentry.withMonitor("my-monitor-slug", async () => {
  await processQueue();
});

With Full Monitor Configuration (Upsert)

await Sentry.withMonitor(
  "hourly-report-job",
  async () => {
    await generateHourlyReport();
  },
  {
    schedule: {
      type: "crontab",
      value: "0 * * * *",   // runs at top of every hour
    },
    checkinMargin: 2,        // minutes of grace before "missed"
    maxRuntime: 10,          // minutes before marking as failed
    timezone: "America/Los_Angeles",
    failureIssueThreshold: 3,   // consecutive failures before creating issue (SDK ≥8.7.0)
    recoveryThreshold: 2,        // consecutive successes before resolving issue (SDK ≥8.7.0)
  },
);

Interval Schedule

await Sentry.withMonitor(
  "data-sync-job",
  async () => {
    await syncData();
  },
  {
    schedule: {
      type: "interval",
      value: 30,         // numeric value
      unit: "minute",    // "minute" | "hour" | "day" | "week" | "month" | "year"
    },
  },
);

Option D: Manual Sentry.captureCheckIn() (SDK ≥7.51.1)

For full control — send check-ins manually at job start and end:

import * as Sentry from "@sentry/nextjs";

// 1. Signal job started — returns a checkInId for correlation
const checkInId = Sentry.captureCheckIn({
  monitorSlug: "my-monitor-slug",
  status: "in_progress",
});

try {
  await doWork();

  // 2a. Signal success
  Sentry.captureCheckIn({
    checkInId,
    monitorSlug: "my-monitor-slug",
    status: "ok",
  });
} catch (err) {
  // 2b. Signal failure
  Sentry.captureCheckIn({
    checkInId,
    monitorSlug: "my-monitor-slug",
    status: "error",
  });
  throw err;
}

With Upsert Config

const checkInId = Sentry.captureCheckIn(
  {
    monitorSlug: "my-monitor-slug",
    status: "in_progress",
  },
  {
    schedule: {
      type: "crontab",
      value: "*/5 * * * *",  // every 5 minutes
    },
    checkinMargin: 1,
    maxRuntime: 5,
    timezone: "UTC",
  },
);

Heartbeat Check-In (Detects Missed Jobs Only)

If you only need to know whether the job ran (not runtime failures), send a single check-in at completion:

try {
  await doWork();
  Sentry.captureCheckIn({ monitorSlug: "my-monitor-slug", status: "ok" });
} catch (err) {
  Sentry.captureCheckIn({ monitorSlug: "my-monitor-slug", status: "error" });
}

Using Crons with Next.js Route Handlers

For App Router cron endpoints called by Vercel Cron or an external scheduler:

// app/api/cron/route.ts
import * as Sentry from "@sentry/nextjs";
import { NextResponse } from "next/server";

export async function GET() {
  const checkInId = Sentry.captureCheckIn({
    monitorSlug: "my-api-cron",
    status: "in_progress",
  });

  try {
    await runMyScheduledTask();

    Sentry.captureCheckIn({
      checkInId,
      monitorSlug: "my-api-cron",
      status: "ok",
    });

    return NextResponse.json({ ok: true });
  } catch (err) {
    Sentry.captureCheckIn({
      checkInId,
      monitorSlug: "my-api-cron",
      status: "error",
    });
    throw err;
  }
}

Using withMonitor in a Route Handler

// app/api/cron/route.ts
import * as Sentry from "@sentry/nextjs";
import { NextResponse } from "next/server";

export async function GET() {
  await Sentry.withMonitor(
    "my-api-cron",
    async () => {
      await runMyScheduledTask();
    },
    {
      schedule: { type: "crontab", value: "0 * * * *" },
      checkinMargin: 2,
      maxRuntime: 5,
      timezone: "UTC",
    },
  );

  return NextResponse.json({ ok: true });
}

Edge Runtime Route Handler

// app/api/cron/route.ts
export const runtime = "edge";

import * as Sentry from "@sentry/nextjs";
import { NextResponse } from "next/server";

export async function GET() {
  await Sentry.withMonitor("my-edge-cron", async () => {
    await runEdgeTask();
  });

  return NextResponse.json({ ok: true });
}

Monitor Configuration Reference

Full upsert config object shape:

interface MonitorConfig {
  schedule: {
    type: "crontab";
    value: string;       // Standard cron expression, e.g. "0 9 * * 1-5"
  } | {
    type: "interval";
    value: number;       // Numeric quantity
    unit: "minute" | "hour" | "day" | "week" | "month" | "year";
  };
  checkinMargin?: number;          // Minutes of grace period before "missed" alert
  maxRuntime?: number;             // Minutes before in-progress job is marked failed
  timezone?: string;               // IANA tz string, e.g. "America/New_York"
  failureIssueThreshold?: number;  // Consecutive failures → create issue (SDK ≥8.7.0)
  recoveryThreshold?: number;      // Consecutive successes → resolve issue (SDK ≥8.7.0)
}

Cron Status Values

StatusWhen to use
in_progressJob has started, work is underway
okJob completed successfully
errorJob failed — an error occurred

Rate Limits

Cron check-ins are rate-limited to 6 check-ins per minute per monitor environment. Each environment (production, staging, etc.) is tracked independently.


Alerting

Create issue alerts filtered by the tag monitor.slug equals [your-monitor-slug] in Sentry’s Alerts sidebar.


SDK Version Matrix

FeatureMin SDK Version
Sentry.captureCheckIn()7.51.1
Sentry.withMonitor()7.76.0
cron library auto-instrumentation7.92.0
node-cron auto-instrumentation7.92.0
node-schedule auto-instrumentation7.93.0
failureIssueThreshold / recoveryThreshold8.7.0

Troubleshooting

IssueSolution
Check-ins not appearing in SentryVerify monitorSlug matches the slug configured in Sentry; check DSN is correct
Monitor shows “missed” despite job runningAdjust checkinMargin to allow more grace time; check clock skew
Monitor shows “timeout”Increase maxRuntime; investigate why the job is taking longer than expected
automaticVercelMonitors not workingConfirm you’re using Pages Router — App Router is NOT supported for automatic instrumentation
withMonitor not creating the monitorFirst check-in with upsert config creates the monitor; ensure config is passed
Edge runtime check-ins failingEnsure sentry.edge.config.ts is configured; crons work in Edge runtime
Client-side cron calls failingMove cron monitoring to server/edge code — browser runtime is not supported
Rate limit errors on check-insJob is sending more than 6 check-ins/minute; reduce polling frequency or combine check-ins
node-schedule with date/RecurrenceRuleOnly cron string format is supported for auto-instrumentation; use withMonitor instead

Reference: Error Monitoring

Error Monitoring — Sentry Next.js SDK

Minimum SDK: @sentry/nextjs ≥8.0.0
onRequestError hook requires @sentry/nextjs ≥8.28.0 and Next.js 15+
withServerActionInstrumentation available since @sentry/nextjs ≥8.0.0


Three-Runtime Architecture

Next.js runs code in three separate environments. Sentry provides distinct init files for each:

FileRuntimeCaptures
instrumentation-client.tsBrowserClient-side errors, unhandled rejections
sentry.server.config.tsNode.jsAPI routes, Server Components, Server Actions
sentry.edge.config.tsEdgeMiddleware, edge routes

All three use the same DSN but are configured independently.


Automatic vs Manual Error Capture

What Is Captured Automatically

Error TypeCaptured?Mechanism
Unhandled client JS exceptions✅ Yeswindow.onerror (GlobalHandlers integration)
Unhandled promise rejections (client)✅ Yeswindow.onunhandledrejection
Server Component render errors (Next.js 15+)✅ YesonRequestError hook in instrumentation.ts
Unhandled API route crashes (server)✅ YesNode.js uncaught exception handler
Re-thrown errors from try/catch✅ YesBubbles to global handler
error.tsx boundary errors❌ NoNext.js catches before Sentry
global-error.tsx boundary errors❌ NoNext.js catches before Sentry
Caught + swallowed try/catch errors❌ NoMust call captureException manually
Server Action graceful error returns❌ NoMust call captureException or use wrapper
Caught edge middleware errors❌ NoMust call captureException manually

The Core Rule

“If you catch an error and don’t re-throw it, Sentry never sees it.”

// ✅ Automatically captured — unhandled, bubbles up
throw new Error("Unhandled");

// ✅ Automatically captured — re-thrown
try {
  await doSomething();
} catch (err) {
  throw err;
}

// ❌ NOT captured — swallowed by graceful return
try {
  await doSomething();
} catch (err) {
  return { error: "Failed" }; // ← must add captureException here
}

// ✅ Manually captured
try {
  await doSomething();
} catch (err) {
  Sentry.captureException(err);
  return { error: "Failed" };
}

Client-Side Error Capture

Sentry.captureException(error, context?)

Captures an exception and sends it to Sentry. Prefer Error objects — they include stack traces.

// Basic
Sentry.captureException(new Error("Something broke"));

// With inline context (one-off enrichment)
Sentry.captureException(error, {
  level: "fatal",
  tags: { section: "checkout" },
  extra: { orderId, userId: user.id },
  user: { id: "user-123", email: "[email protected]" },
  fingerprint: ["checkout-failure", String(error.code)],
  contexts: {
    cart: { items: 3, total: 99.99 },
  },
});

Sentry.captureMessage(message, levelOrContext?)

Captures a plain message — useful for notable conditions that aren’t exceptions.

// With severity level
Sentry.captureMessage("Deprecated API used", "warning");
// Levels: "fatal" | "error" | "warning" | "log" | "info" | "debug"

// With full context
Sentry.captureMessage("Payment method expired", {
  level: "warning",
  tags: { payment_provider: "stripe" },
  user: { id: currentUser.id },
});

Unhandled Rejections

Automatically captured by the GlobalHandlers integration. To customize:

// instrumentation-client.ts
Sentry.init({
  dsn: "___PUBLIC_DSN___",
  integrations: [
    Sentry.globalHandlersIntegration({
      onerror: true,
      onunhandledrejection: true, // set false to handle manually
    }),
  ],
});

// Manual rejection handling (if onunhandledrejection: false)
window.addEventListener("unhandledrejection", (event) => {
  Sentry.captureException(event.reason);
});

Error Boundaries

App Router: app/error.tsx (Segment-level)

Each route segment can have its own error.tsx. Next.js catches these before Sentry — you must call captureException manually inside useEffect.

// app/error.tsx  (also: app/dashboard/error.tsx, etc.)
"use client";

import { useEffect } from "react";
import * as Sentry from "@sentry/nextjs";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // REQUIRED: Next.js catches this before Sentry can
    Sentry.captureException(error);
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

digest: Server-side errors include a digest hash. Use it to correlate Sentry events with server logs.

App Router: app/global-error.tsx (Root Layout)

Last-resort catch-all for root layout errors. Must render its own <html> and <body>. Use the NextError component for consistency with Next.js default error pages.

// app/global-error.tsx
"use client";

import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";

export default function GlobalError({
  error,
}: {
  error: Error & { digest?: string };
}) {
  useEffect(() => {
    Sentry.captureException(error);
  }, [error]);

  return (
    <html>
      <body>
        {/*
          App Router doesn't expose HTTP status codes for errors,
          so pass 0 to render a generic error message.
        */}
        <NextError statusCode={0} />
      </body>
    </html>
  );
}

App Router: Error Boundary Directory Structure

app/
├── global-error.tsx        # Root layout errors (last resort)
├── error.tsx               # App-wide segment fallback
├── layout.tsx
├── page.tsx
└── dashboard/
    ├── error.tsx           # Dashboard-specific error boundary
    ├── layout.tsx
    └── page.tsx

React 18 and Earlier: <Sentry.ErrorBoundary>

For client components using React 18 or earlier, wrap with <Sentry.ErrorBoundary> for additional control and fallback UIs:

"use client";

import * as Sentry from "@sentry/nextjs";

function CheckoutPage() {
  return (
    <Sentry.ErrorBoundary
      fallback={({ error, resetError }) => (
        <div>
          <p>Checkout failed: {error.message}</p>
          <button onClick={resetError}>Retry</button>
        </div>
      )}
      beforeCapture={(scope, error) => {
        scope.setTag("section", "checkout");
        scope.setLevel("fatal");
      }}
      onError={(error, componentStack, eventId) => {
        analytics.track("error_boundary", { eventId });
      }}
    >
      <CheckoutFlow />
    </Sentry.ErrorBoundary>
  );
}

React 19+: Sentry.reactErrorHandler() with createRoot

React 19 exposes hooks on createRoot. If you’re using React 19 client components, pass Sentry.reactErrorHandler() to each hook:

// In your client entry point or root layout setup
import { createRoot } from "react-dom/client";
import * as Sentry from "@sentry/nextjs";

createRoot(container, {
  onUncaughtError: Sentry.reactErrorHandler(),    // fatal — tree unmounts
  onCaughtError: Sentry.reactErrorHandler(),      // caught by an ErrorBoundary
  onRecoverableError: Sentry.reactErrorHandler(), // auto-recovery (hydration)
}).render(<App />);

Use both together: reactErrorHandler() is the global net; <Sentry.ErrorBoundary> provides scoped fallback UIs.


Pages Router Error Handling

pages/_error.tsx

Use captureUnderscoreErrorException — a helper that reads Next.js context and captures the error with correct status code.

// pages/_error.tsx
import * as Sentry from "@sentry/nextjs";
import type { NextPage } from "next";
import type { ErrorProps } from "next/error";
import Error from "next/error";

const CustomErrorComponent: NextPage<ErrorProps> = (props) => {
  return <Error statusCode={props.statusCode} />;
};

CustomErrorComponent.getInitialProps = async (contextData) => {
  // CRITICAL: await so Sentry flushes before the serverless function exits
  await Sentry.captureUnderscoreErrorException(contextData);
  return Error.getInitialProps(contextData);
};

export default CustomErrorComponent;

pages/_app.tsx

For global error handling at the app level in Pages Router:

// pages/_app.tsx
import type { AppProps } from "next/app";
import * as Sentry from "@sentry/nextjs";

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Sentry.ErrorBoundary fallback={<p>An error has occurred.</p>}>
      <Component {...pageProps} />
    </Sentry.ErrorBoundary>
  );
}

Server-Side Error Capture

onRequestError Hook (Next.js 15+, SDK ≥8.28.0)

Export onRequestError from instrumentation.ts to automatically capture Server Component errors without adding captureException everywhere:

// instrumentation.ts
import * as Sentry from "@sentry/nextjs";

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./sentry.server.config");
  }
  if (process.env.NEXT_RUNTIME === "edge") {
    await import("./sentry.edge.config");
  }
}

// Automatically captures errors from Server Components, Middleware, and proxies
export const onRequestError = Sentry.captureRequestError;

API Routes (App Router)

Unhandled errors crash the request and are auto-captured. Caught errors must be captured manually:

// app/api/users/route.ts
import { NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const user = await db.users.create(body);
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    Sentry.captureException(error, {
      tags: { route: "/api/users", method: "POST" },
    });
    return NextResponse.json({ error: "Failed to create user" }, { status: 500 });
  }
}

API Routes (Pages Router)

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from "next";
import * as Sentry from "@sentry/nextjs";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const data = await fetchData();
    res.status(200).json(data);
  } catch (error) {
    Sentry.captureException(error);
    res.status(500).json({ error: "Internal server error" });
  }
}

Server Actions

Manual Pattern

// app/actions.ts
"use server";

import * as Sentry from "@sentry/nextjs";

export async function createPost(formData: FormData) {
  try {
    const post = await db.posts.create({
      data: { title: formData.get("title") as string },
    });
    return { success: true, id: post.id };
  } catch (error) {
    // Graceful return swallows — must capture manually
    Sentry.captureException(error);
    return { success: false, error: "Failed to create post" };
  }
}

Automatically instruments server actions with tracing, attaches form data, and connects client/server traces:

// app/actions.ts
"use server";

import * as Sentry from "@sentry/nextjs";
import { headers } from "next/headers";

export async function submitForm(formData: FormData) {
  return Sentry.withServerActionInstrumentation(
    "submitForm",
    {
      headers: await headers(), // connects client and server traces
      formData,                 // attaches form data to Sentry events
      recordResponse: true,     // includes response data
    },
    async () => {
      // Errors thrown here are automatically captured
      const result = await processForm(formData);
      return { success: true, data: result };
    },
  );
}

Edge Runtime Error Capture

Edge runtime runs in Next.js middleware and edge API routes. Initialize Sentry via sentry.edge.config.ts:

// sentry.edge.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: "___PUBLIC_DSN___",
  tracesSampleRate: 1.0,
  // Note: Edge runtime has limited Node.js API access
  // Some Node.js-specific integrations are not available
});

Errors in middleware are auto-captured via onRequestError. Caught errors require manual capture:

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import * as Sentry from "@sentry/nextjs";

export function middleware(request: NextRequest) {
  try {
    // middleware logic
    return NextResponse.next();
  } catch (error) {
    Sentry.captureException(error, {
      tags: { runtime: "edge", path: request.nextUrl.pathname },
    });
    return NextResponse.next();
  }
}

export const config = {
  // Exclude tunnel route from middleware if using tunnelRoute in withSentryConfig
  matcher: ["/((?!monitoring|_next/static|_next/image|favicon.ico).*)"],
};

Scope Management

Sentry merges three scope layers before sending each event. Later scopes override earlier ones:

Global → Isolation → Current → Event Sent

Global Scope

Applied to every event. Use for universal data (app version, build ID):

Sentry.getGlobalScope().setTag("app_version", "2.1.0");
Sentry.getGlobalScope().setContext("build", { sha: process.env.VERCEL_GIT_COMMIT_SHA });

Isolation Scope

  • Server: Forked per request — safe for per-request user data (no cross-contamination)
  • Browser: One per page load

All top-level Sentry.setXxx() methods write to the isolation scope:

// These are identical:
Sentry.setTag("my-tag", "my value");
Sentry.getIsolationScope().setTag("my-tag", "my value");

// Set user on login (persists for the current request/page):
Sentry.setUser({ id: "user-42", email: "[email protected]" });

// Clear user on logout:
Sentry.setUser(null);

withScope — Temporary Per-Capture Context

The primary tool for adding context to a single capture without affecting other events:

Sentry.withScope((scope) => {
  scope.setTag("operation", "bulk-delete");
  scope.setLevel("warning");
  scope.setUser({ id: order.userId });
  scope.setContext("bulk", { count: items.length });
  scope.addBreadcrumb({
    category: "operation",
    message: "Bulk delete started",
    level: "info",
  });
  Sentry.captureException(deleteError);
});
// Tags/context above do NOT appear on subsequent events

Scope Decision Guide

GoalAPI
Data on ALL events (app version, build ID)Sentry.getGlobalScope().setTag(...)
Current request/page-load dataSentry.setTag(...) (isolation scope)
One specific capture onlySentry.withScope((scope) => { ... })
Inline on a single eventSecond arg to captureException(err, { tags: {...} })

Event Enrichment

setTag / setTags (Searchable)

Tags are indexed and searchable — use them for filtering, grouping, and alerting.

  • Key: max 32 chars, a-zA-Z0-9_.:- (no spaces); Value: max 200 chars, no newlines
Sentry.setTag("page_locale", "de-at");
Sentry.setTags({
  payment_method: "stripe",
  subscription_tier: "pro",
  region: "eu-west-1",
});

setContext (Structured, Non-searchable)

Attaches arbitrary structured data visible in the issue detail view. Not indexed or searchable.

Sentry.setContext("checkout", {
  step: "payment",
  cart_items: 3,
  total_usd: 99.99,
  coupon_applied: "SAVE20",
});

// Clear a context:
Sentry.setContext("checkout", null);

Depth: Normalized to 3 levels deep by default. The type key is reserved — don’t use it.

setUser (User Identity)

// On login
Sentry.setUser({
  id: "user-42",
  email: "[email protected]",
  username: "janedoe",
  subscription: "pro",  // arbitrary extra fields accepted
});

// On logout
Sentry.setUser(null);

setExtra / setExtras (Arbitrary Debug Data)

Non-indexed supplementary data. Prefer setContext for structured objects with meaningful names.

Sentry.setExtra("raw_api_response", responseText);
Sentry.setExtras({
  formData: { fieldA: "value1" },
  processingStep: "validation",
  retryCount: 3,
});

Tags vs Context vs Extra

FeatureSearchable?Indexed?Best For
Tags✅ Yes✅ YesFiltering, grouping, alerting
Context❌ No❌ NoStructured debug info (nested objects)
Extra❌ No❌ NoArbitrary debug values
User✅ Partially✅ YesUser attribution and filtering

Automatic Breadcrumbs (Zero Config)

TypeWhat’s Captured
ui.clickDOM element clicks
navigationURL changes, route transitions
httpXHR/fetch requests (URL, method, status)
consoleconsole.log, warn, error

Manual Breadcrumbs

Sentry.addBreadcrumb({
  category: "auth",
  message: "User authenticated",
  level: "info",
  data: { userId: "u_42", method: "oauth2" },
});

Sentry.addBreadcrumb({
  type: "http",
  category: "api.request",
  message: "POST /api/orders",
  level: "info",
  data: {
    url: "/api/orders",
    method: "POST",
    status_code: 422,
    reason: "Validation failed",
  },
});

Sentry.addBreadcrumb({
  type: "navigation",
  category: "navigation",
  message: "User navigated to checkout",
  data: { from: "/cart", to: "/checkout/payment" },
});
KeyTypeValues
typestring"default" | "debug" | "error" | "info" | "navigation" | "http" | "query" | "ui" | "user"
categorystringDot-notation: "auth", "ui.click", "api.request"
messagestringHuman-readable description
levelstring"fatal" | "error" | "warning" | "log" | "info" | "debug"
timestampnumberUnix timestamp (auto-set if omitted)
dataobjectArbitrary key/value data

beforeBreadcrumb — Filter or Mutate

Sentry.init({
  beforeBreadcrumb(breadcrumb, hint) {
    // Drop password field interactions
    if (breadcrumb.category === "ui.click") {
      if (hint?.event?.target?.type === "password") return null;
    }

    // Drop verbose console.debug in production
    if (breadcrumb.category === "console" && breadcrumb.level === "debug") {
      return null;
    }

    // Enrich fetch breadcrumbs
    if (breadcrumb.category === "fetch" && hint?.response) {
      breadcrumb.data = {
        ...breadcrumb.data,
        responseStatus: hint.response.status,
      };
    }

    return breadcrumb;
  },
  maxBreadcrumbs: 50, // default: 100
});

beforeSend and Filtering Hooks

beforeSend — Modify or Drop Error Events

Last chance to modify or discard events. Runs after all event processors. Return null to drop.

Sentry.init({
  beforeSend(event, hint) {
    const error = hint.originalException;

    // Drop non-Error rejections (e.g. cancelled requests)
    if (error && !(error instanceof Error)) return null;

    // Drop browser extension errors
    const frames = event.exception?.values?.[0]?.stacktrace?.frames;
    if (frames?.some(f => f.filename?.includes("extension://"))) return null;

    // Scrub PII
    if (event.user?.email) {
      event.user = { ...event.user, email: "[filtered]" };
    }

    // Override fingerprint for known patterns
    if (error?.name === "ChunkLoadError") {
      event.fingerprint = ["chunk-load-failure"];
    }

    return event;
  },
});

Note: Only one beforeSend is allowed. For multiple processors, use addEventProcessor().

beforeSendTransaction — Modify or Drop Performance Events

Sentry.init({
  beforeSendTransaction(event) {
    if (event.transaction === "/api/health") return null;
    return event;
  },
});

ignoreErrors — Pattern-Based Filtering

Sentry.init({
  ignoreErrors: [
    "ResizeObserver loop limit exceeded",
    "fb_xd_fragment",
    /^Network Error$/i,
    /Loading chunk \d+ failed/,
    /^Script error\.?$/,
  ],
});

allowUrls / denyUrls

Sentry.init({
  // Only capture errors from your own scripts:
  allowUrls: [/https?:\/\/((cdn|www)\.)?yourapp\.com/],

  // Block third-party noise:
  denyUrls: [
    /extensions\//i,
    /^chrome:\/\//i,
    /^moz-extension:\/\//i,
    /gtm\.js/,
  ],
});

Fingerprinting and Custom Grouping

All events have a fingerprint. Events with the same fingerprint group into the same issue.

Per-Event Fingerprinting

Sentry.captureException(error, {
  fingerprint: ["checkout-failure", "stripe", String(error.code)],
});

withScope Fingerprinting

Sentry.withScope((scope) => {
  scope.setFingerprint([method, path, String(err.statusCode)]);
  Sentry.captureException(err);
});

beforeSend Fingerprinting

Sentry.init({
  beforeSend(event, hint) {
    const error = hint.originalException;

    // All DatabaseConnectionErrors → one issue:
    if (error instanceof DatabaseConnectionError) {
      event.fingerprint = ["database-connection-error"];
    }

    // Extend default grouping (keep Sentry's stack-trace hash + add dimension):
    if (error instanceof RPCError) {
      event.fingerprint = [
        "{{ default }}",              // keep Sentry's default
        String(error.functionName),   // + split by RPC function
        String(error.errorCode),
      ];
    }

    return event;
  },
});

Template Variables

VariableDescription
{{ default }}Sentry’s normally computed hash (extend rather than replace)
{{ transaction }}Current transaction name
{{ function }}Top function in stack trace
{{ type }}Exception type

Event Processors

Unlike beforeSend (only one allowed), you can register multiple event processors:

// Global — runs for all events
Sentry.addEventProcessor((event, hint) => {
  event.extra = {
    ...event.extra,
    buildId: process.env.VERCEL_GIT_COMMIT_SHA,
  };
  return event;
});

// Scoped — runs only inside the withScope callback
Sentry.withScope((scope) => {
  scope.addEventProcessor((event) => {
    event.tags = { ...event.tags, processed_by: "checkout_handler" };
    return event;
  });
  Sentry.captureException(checkoutError);
});

Execution order: All addEventProcessor() processors run first, then beforeSend runs last (guaranteed).


Error Capture Quick Reference

Scenario Coverage Table

ScenarioAuto Captured?Solution
Unhandled client JS exception✅ Yes
Unhandled promise rejection✅ Yes
Server Component error (Next.js 15+)✅ YesonRequestError hook
Unhandled API route crash✅ Yes
app/error.tsx boundary❌ NocaptureException in useEffect
app/global-error.tsx❌ NocaptureException in useEffect
try/catch with graceful return❌ NocaptureException before return
try/catch with re-throw✅ Yes
Server Action graceful error❌ NocaptureException or withServerActionInstrumentation
Caught edge middleware error❌ NocaptureException manually

API Quick Reference

// ── Capture ───────────────────────────────────────────────────────────
Sentry.captureException(error)
Sentry.captureException(error, { level, tags, extra, contexts, fingerprint, user })
Sentry.captureMessage("text", "warning")
Sentry.captureMessage("text", { level, tags, extra })

// ── Next.js Specific ──────────────────────────────────────────────────
export const onRequestError = Sentry.captureRequestError      // instrumentation.ts
await Sentry.captureUnderscoreErrorException(contextData)     // pages/_error.tsx
Sentry.withServerActionInstrumentation("name", opts, fn)      // server actions

// ── User ──────────────────────────────────────────────────────────────
Sentry.setUser({ id, email, username, ...custom })
Sentry.setUser(null)                                          // clear on logout

// ── Tags (searchable) ─────────────────────────────────────────────────
Sentry.setTag("key", "value")
Sentry.setTags({ key1: "v1", key2: "v2" })

// ── Context (structured, non-searchable) ──────────────────────────────
Sentry.setContext("name", { key: value })
Sentry.setContext("name", null)                               // clear

// ── Extra (arbitrary) ─────────────────────────────────────────────────
Sentry.setExtra("key", anyValue)
Sentry.setExtras({ key1: v1 })

// ── Breadcrumbs ───────────────────────────────────────────────────────
Sentry.addBreadcrumb({ type, category, message, level, data })

// ── Scopes ────────────────────────────────────────────────────────────
Sentry.withScope((scope) => { scope.setTag(...); Sentry.captureException(...) })
Sentry.withIsolationScope((scope) => { ... })
Sentry.getGlobalScope().setTag(...)
Sentry.getIsolationScope().setTag(...)                        // same as Sentry.setTag()

// ── Fingerprinting ────────────────────────────────────────────────────
scope.setFingerprint(["group-key"])
event.fingerprint = ["{{ default }}", "extra-dimension"]      // in beforeSend

// ── Hooks ─────────────────────────────────────────────────────────────
Sentry.init({ beforeSend(event, hint) { return event | null } })
Sentry.init({ beforeSendTransaction(event) { return event | null } })
Sentry.init({ beforeBreadcrumb(breadcrumb, hint) { return breadcrumb | null } })
Sentry.init({ ignoreErrors: ["string", /regex/] })
Sentry.init({ allowUrls: [/regex/] })
Sentry.init({ denyUrls: [/regex/] })

Troubleshooting

IssueSolution
Errors not appearing from error.tsxAdd Sentry.captureException(error) in a useEffect — Next.js catches these before Sentry
Server Component errors missingEnsure export const onRequestError = Sentry.captureRequestError is in instrumentation.ts; requires SDK ≥8.28.0 + Next.js 15
Minified stack tracesConfigure authToken in withSentryConfig for source map upload; use digest to correlate server logs with Sentry events
Duplicate errorsCheck that only one handler captures the same error; in dev, React Strict Mode may double-fire — validate in production builds
Server Action errors missingUse withServerActionInstrumentation wrapper or add captureException before any graceful return
Events blocked by ad-blockersSet tunnelRoute: "/monitoring" in withSentryConfig; exclude the route from your middleware matcher
Missing edge errorsVerify sentry.edge.config.ts is imported via instrumentation.ts when NEXT_RUNTIME === "edge"
Turbopack source map issuesTurbopack source map upload support is experimental; fall back to webpack for production builds if maps are missing
Events from wrong DSN in hybrid appAll three runtimes (client, server, edge) use the same DSN; verify each init file has identical DSN value
captureUnderscoreErrorException not awaitedIn Pages Router _error.tsx, always await it — serverless functions may exit before Sentry flushes otherwise

Reference: Logging

Logging — Sentry Next.js SDK

Minimum SDK: @sentry/nextjs ≥9.41.0+ for Sentry.logger API and enableLogs
consoleLoggingIntegration() multi-arg parsing: requires ≥10.13.0+
Scope-based attributes (getGlobalScope, getIsolationScope): requires ≥10.32.0+

⚠️ Not available via CDN/loader snippet — NPM install required.


Enabling Logs

enableLogs must be set in all three Next.js runtime config files:

// instrumentation-client.ts → use NEXT_PUBLIC_SENTRY_DSN
// sentry.server.config.ts   → use SENTRY_DSN
// sentry.edge.config.ts     → use SENTRY_DSN
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN, // use NEXT_PUBLIC_SENTRY_DSN in client config
  enableLogs: true, // Required — logging is disabled by default
});

Without enableLogs: true, all Sentry.logger.* calls are silently no-ops.


Logger API — Six Levels

import * as Sentry from "@sentry/nextjs";

Sentry.logger.trace("Entering processOrder", { fn: "processOrder", orderId: "ord_1" });
Sentry.logger.debug("Cache lookup", { key: "user:123", hit: false });
Sentry.logger.info("Order created", { orderId: "order_456", total: 99.99 });
Sentry.logger.warn("Rate limit approaching", { current: 95, max: 100 });
Sentry.logger.error("Payment failed", { reason: "card_declined", userId: "u_1" });
Sentry.logger.fatal("Database unavailable", { host: "db-primary" });
LevelMethodTypical Use
traceSentry.logger.trace()Ultra-granular function entry/exit; high-volume — filter out in production
debugSentry.logger.debug()Development diagnostics, cache hits/misses
infoSentry.logger.info()Normal business milestones, confirmations
warnSentry.logger.warn()Degraded state, approaching limits, recoverable issues
errorSentry.logger.error()Failures requiring attention
fatalSentry.logger.fatal()Critical failures, system unavailable

Attribute value types: string, number, boolean only — undefined, arrays, and objects are not accepted.


Parameterized Messages — Sentry.logger.fmt

The fmt tagged template literal binds each interpolated variable as a structured, searchable attribute in Sentry:

const userId = "user_123";
const productName = "Widget Pro";
const amount = 49.99;

Sentry.logger.info(
  Sentry.logger.fmt`User ${userId} purchased ${productName} for $${amount}`
);

This produces:

message.template:     "User %s purchased %s for $%s"
message.parameter.0:  "user_123"
message.parameter.1:  "Widget Pro"
message.parameter.2:  49.99

Each parameter is independently searchable in Sentry’s log explorer.

⚠️ logger.fmt must be used as a tagged template literal — not a function call. Sentry.logger.fmt("text") will not produce structured parameters.

When to use fmt vs plain attributes

ApproachUse when
Sentry.logger.info(msg, { key: val })Variables are logically distinct attributes with names
Sentry.logger.info(Sentry.logger.fmt\…`)`Variables are part of a human-readable sentence

Console Capture — consoleLoggingIntegration

Automatically forwards console.* calls to Sentry as structured logs:

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  enableLogs: true,
  integrations: [
    Sentry.consoleLoggingIntegration({
      levels: ["log", "warn", "error"], // which console methods to forward
    }),
  ],
});

Multiple arguments are mapped to positional parameters (requires SDK ≥10.13.0):

console.log("User action recorded", userId, success)
  → message.parameter.0 = <userId value>
  → message.parameter.1 = <success value>
Console methodSentry log level
console.loginfo
console.infoinfo
console.warnwarn
console.errorerror
console.debugdebug
console.assert (failing)error

Log Filtering — beforeSendLog

Use beforeSendLog to drop, modify, or scrub logs before they are sent. Return null to discard:

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  enableLogs: true,
  beforeSendLog: (log) => {
    // Drop debug and trace logs in production
    if (log.level === "debug" || log.level === "trace") {
      return null;
    }

    // Scrub sensitive attribute keys
    if (log.attributes?.password) {
      delete log.attributes.password;
    }
    if (log.attributes?.["credit_card"]) {
      log.attributes["credit_card"] = "[REDACTED]";
    }

    // Drop noisy health-check logs by message content
    if (log.message?.includes("/health")) {
      return null;
    }

    return log; // send the (possibly modified) log
  },
});

The log object shape

FieldTypeDescription
levelstring"trace" | "debug" | "info" | "warn" | "error" | "fatal"
messagestringThe log message text
timestampnumberUnix timestamp
attributesobjectKey/value pairs attached to this log

Scope-Based Automatic Attributes (SDK ≥10.32.0)

Attributes set on scopes are automatically added to all logs emitted within that scope.

// Global scope — shared across the entire app lifetime
Sentry.getGlobalScope().setAttributes({
  service: "checkout",
  version: "2.1.0",
});

// Isolation scope — unique per request
// ⚠️ CRITICAL for Next.js server-side: use isolation scope (not global)
// to prevent attributes from one request leaking into another
Sentry.getIsolationScope().setAttributes({
  org_id: user.orgId,
  user_tier: user.tier,
});

// Current scope — wraps a single operation
Sentry.withScope((scope) => {
  scope.setAttribute("request_id", req.id);
  Sentry.logger.info("Processing order"); // gets request_id attribute
});

⚠️ Next.js server-side isolation: Always use getIsolationScope() for per-request data on the server. The isolation scope is unique per request, preventing attributes from one user’s request from bleeding into another’s concurrent request.


Third-Party Logger Integrations

Pino (SDK ≥10.18.0)

// sentry.server.config.ts — Pino is a Node.js integration (server-side only)
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  enableLogs: true,
  integrations: [Sentry.pinoIntegration()],
});
// No changes needed to your pino logger — it auto-captures logs

Consola (SDK ≥10.12.0)

import { consola } from "consola";

const sentryReporter = Sentry.createConsolaReporter({
  levels: ["error", "warn"], // optional: only forward these levels
});

consola.addReporter(sentryReporter);

Winston (SDK ≥9.13.0)

import winston from "winston";
import Transport from "winston-transport";

const SentryTransport = Sentry.createSentryWinstonTransport(Transport, {
  levels: ["error", "warn"],
});

const logger = winston.createLogger({
  transports: [new SentryTransport()],
});

Auto-Generated Attributes

These are added by the SDK to every log without any developer configuration:

AttributeNotes
sentry.environmentAlways present
sentry.releaseAlways present
sentry.sdk.namee.g., "sentry.javascript.nextjs"
sentry.sdk.versionAlways present
browser.name, browser.versionClient-side only
user.id, user.name, user.emailWhen Sentry.setUser() + sendDefaultPii: true
sentry.trace.parent_span_idWhen inside an active span (enables log ↔ trace correlation)
sentry.replay_idClient-side with Replay enabled
server.addressServer-side only
message.templateWhen using logger.fmt
message.parameter.NWhen using logger.fmt or consoleLoggingIntegration

Next.js-Specific: Three-Runtime Configuration

For consistency across all runtimes, enable logging in all three config files:

// instrumentation-client.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  enableLogs: true,
  integrations: [Sentry.consoleLoggingIntegration({ levels: ["warn", "error"] })],
});

// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  enableLogs: true,
  integrations: [Sentry.pinoIntegration()], // or consoleLoggingIntegration
});

// sentry.edge.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  enableLogs: true,
});

Server Action Logging Example

// app/actions/order.ts
"use server";
import * as Sentry from "@sentry/nextjs";

export async function createOrder(formData: FormData) {
  // Use isolation scope for per-request context on the server
  Sentry.getIsolationScope().setAttributes({
    action: "createOrder",
    userId: formData.get("userId") as string,
  });

  Sentry.logger.info("Order creation started", {
    productId: formData.get("productId") as string,
  });

  try {
    const order = await db.orders.create(/* ... */);
    Sentry.logger.info("Order created successfully", { orderId: order.id });
    return order;
  } catch (err) {
    Sentry.logger.error("Order creation failed", {
      reason: (err as Error).message,
    });
    throw err;
  }
}

Best Practice: Wide Events

Prefer one comprehensive log with all context over many fragmented logs:

// ✅ Preferred — one wide log with full context
Sentry.logger.info("Checkout completed", {
  orderId: order.id,
  userId: user.id,
  cartValue: cart.total,
  itemCount: cart.items.length,
  paymentMethod: "stripe",
  userTier: user.tier,
  durationMs: Date.now() - startTime,
});

// ❌ Avoid — fragmented logs that are hard to correlate
Sentry.logger.info("Cart validated");
Sentry.logger.info("Payment processed");
Sentry.logger.info("Checkout done");

SDK Version Matrix

FeatureMin SDK Version
enableLogs / Sentry.logger.*9.41.0
Winston transport9.13.0
Consola reporter10.12.0
Console integration multi-arg parsing10.13.0
Pino integration10.18.0
Scope attributes (setAttributes)10.32.0

Troubleshooting

IssueSolution
Logs not appearing in SentryVerify enableLogs: true in Sentry.init(); check all three config files (client, server, edge)
logger.fmt not creating message.parameter.*Use as tagged template: Sentry.logger.fmt\text ${var}`— notSentry.logger.fmt(“text”, var)`
Logs not linked to tracesEnsure tracesSampleRate > 0 and the log is emitted inside an active span
consoleLoggingIntegration not availableUpgrade to ≥10.13.0
Scope attributes not appearingUpgrade to ≥10.32.0; use getIsolationScope() (not getGlobalScope()) for server request data
Cross-request attribute leakage on serverReplace getGlobalScope() with getIsolationScope() for per-request data
Too many logs / high volumeUse beforeSendLog to drop trace and debug levels in production
Log attributes contain undefinedOnly string, number, boolean are accepted — filter out undefined values
beforeSendLog not firingConfirm enableLogs: true is set — without it, no logs are processed
Sensitive data appearing in logsAdd filtering in beforeSendLog; better yet, avoid logging sensitive data at the call site
Edge runtime logs missingAdd enableLogs: true to sentry.edge.config.ts

Reference: Profiling

Profiling — Sentry Next.js SDK

Browser profiling: @sentry/nextjs ≥10.27.0 (Beta)
Node.js profiling: @sentry/profiling-node — must match @sentry/nextjs version exactly


Overview

The Sentry Next.js SDK supports profiling in two independent runtimes:

RuntimeIntegrationWhat it captures
BrowserbrowserProfilingIntegration()JS call stacks in Chrome/Edge (Chromium only) at 100Hz
Node.js servernodeProfilingIntegration()V8 CPU call stacks for API routes, RSC, server actions

Both are opt-in and independent from each other. Each attaches to spans and requires tracing to be enabled.


How Profiling Relates to Tracing

Profiles attach to spans — they are not independent events:

  1. tracesSampleRate / tracesSampler decides whether a request is traced at all
  2. profileSessionSampleRate decides whether the session opts into profiling
  3. A profile is only collected when both sampling decisions are “yes”
tracesSampleRate: 0.1   + profileSessionSampleRate: 0.5
→ ~5% of requests will have both a trace AND a profile attached

In trace lifecycle mode, you can drill from a slow span in the Performance UI directly into a flame graph:

Trace: "POST /api/checkout" (850ms)
  ├── "validateCart" (45ms) → [Profile attached] → shows db driver hot paths
  ├── "processPayment" (620ms)
  └── "updateInventory" (185ms) → [Profile attached] → shows ORM overhead

Browser Profiling

Browser Compatibility

BrowserSupportedNotes
Chrome / ChromiumPrimary support
Edge (Chromium)Same engine as Chrome
FirefoxDoes not implement JS Self-Profiling API
Safari / iOS SafariDoes not implement JS Self-Profiling API

⚠️ Sampling bias: Profile data comes only from Chromium users. In unsupported browsers, browserProfilingIntegration() silently no-ops with no errors and no overhead.

Required: Document-Policy Header

The JS Self-Profiling API is gated behind a required response header. Without it, profiling silently fails even in Chromium:

Document-Policy: js-profiling

Next.js (next.config.ts):

const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [{ key: "Document-Policy", value: "js-profiling" }],
      },
    ];
  },
};

Vercel (vercel.json):

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [{ "key": "Document-Policy", "value": "js-profiling" }]
    }
  ]
}

Netlify (netlify.toml):

[[headers]]
  for = "/*"
  [headers.values]
    Document-Policy = "js-profiling"

Nginx:

add_header Document-Policy "js-profiling";

⚠️ Static hosting that doesn’t support custom headers (some CDNs, GitHub Pages) will prevent browser profiling entirely.

Profiles auto-attach to all sampled spans with no additional code:

// instrumentation-client.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

  integrations: [
    Sentry.browserTracingIntegration(), // Must come BEFORE browserProfilingIntegration
    Sentry.browserProfilingIntegration(),
  ],

  tracesSampleRate: 1.0,

  // Session-level sampling: decision made once at page load
  profileSessionSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,

  // "trace" = profiles auto-attach to every sampled span
  profileLifecycle: "trace",
});

SDK Configuration — Manual Mode

Profile specific flows or code paths explicitly:

// instrumentation-client.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  integrations: [
    Sentry.browserTracingIntegration(),
    Sentry.browserProfilingIntegration(),
  ],
  tracesSampleRate: 1.0,
  profileSessionSampleRate: 1.0,
  // No profileLifecycle → defaults to manual mode
});

// Explicit start/stop around critical code:
Sentry.uiProfiler.startProfiler();
await heavyComputation();
Sentry.uiProfiler.stopProfiler();

Node.js Profiling

Installation

npm install @sentry/profiling-node --save

⚠️ Version pinning is required. @sentry/profiling-node must exactly match your @sentry/nextjs version. Mismatched versions cause silent failures.

# Both should be the same version
npm install @sentry/nextjs@latest @sentry/profiling-node@latest

SDK Configuration

// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
import { nodeProfilingIntegration } from "@sentry/profiling-node";

Sentry.init({
  dsn: process.env.SENTRY_DSN,

  integrations: [
    nodeProfilingIntegration(), // V8 CpuProfiler native add-on
  ],

  tracesSampleRate: 1.0,

  // Session-level: decision made once at process startup
  profileSessionSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,

  profileLifecycle: "trace", // auto-attach profiles to spans
});

⚠️ Do NOT add nodeProfilingIntegration to sentry.edge.config.ts. The Edge runtime does not support native add-ons.

Manual Mode (Node.js)

// sentry.server.config.ts
Sentry.init({
  integrations: [nodeProfilingIntegration()],
  profileSessionSampleRate: 1.0,
  profileLifecycle: "manual",
});

// Explicit start/stop:
Sentry.profiler.startProfiler();
await processHeavyJob();
Sentry.profiler.stopProfiler();

Supported Platforms

Precompiled native binaries are available for:

OSArchitectureNode.js
macOSx6418–24
Linux (glibc)x6418–24
Linux (musl/Alpine)x64, ARM6418–24
LinuxARM6418–24
Windowsx6418–24

⚠️ Deno and Bun are not supported. The native add-on only works in Node.js.

Environment Variables

# Override binary path (for custom builds)
SENTRY_PROFILER_BINARY_PATH=/custom/path/profiler.node

# Override binary directory
SENTRY_PROFILER_BINARY_DIR=/path/to/dir

# Profiler logging mode:
# "eager" (default) — faster startProfiler calls, slightly more CPU overhead
# "lazy" — lower CPU overhead, slightly slower startProfiler
SENTRY_PROFILER_LOGGING_MODE=lazy node server.js

Configuration Parameters Reference

ParameterApplies toDescription
profileSessionSampleRateBrowser + Node.js0.0–1.0; session-level sampling decision made once (at page load for browser, process start for server)
profileLifecycleBrowser + Node.js"trace" = auto-attach to spans; omit for manual mode
browserProfilingIntegration()Browser onlyEnables JS Self-Profiling API (Chromium only); must come after browserTracingIntegration()
nodeProfilingIntegration()Node.js onlyEnables V8 CpuProfiler; must be in integrations array in sentry.server.config.ts

profileSessionSampleRate Semantics

The profiling sampling decision is made once per session:

  • Browser: at page load (instrumentation-client.ts init)
  • Server: at process startup (sentry.server.config.ts init)

A “profiling session” either opts in or opts out for its entire lifetime. Within a profiling session, every traced span gets a profile attached (in trace mode).

profileLifecycle Modes Comparison

ModeTriggerBest for
"trace"Auto-attached to every sampled spanBroad production coverage; no code changes
"manual" (default)startProfiler() / stopProfiler()Specific high-value flows (checkout, heavy renders)

Production vs Development Recommendations

// Browser (instrumentation-client.ts)
Sentry.init({
  profileSessionSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
  profileLifecycle: "trace",
});

// Server (sentry.server.config.ts)
Sentry.init({
  integrations: [nodeProfilingIntegration()],
  profileSessionSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
  profileLifecycle: "trace",
});

Performance impact notes:

  • Browser (100Hz sampling): Low overhead; runs unobtrusively in production. Chrome DevTools profiles at 1000Hz — use Sentry profiling for production coverage, DevTools for local deep-dives.
  • Node.js (V8 CpuProfiler): The native profiler adds CPU overhead. Test with realistic load before deploying profileSessionSampleRate: 1.0 to high-traffic production.

“For high-throughput environments, we recommend testing prior to deployment to ensure that your service’s performance characteristics maintain expectations.” — Sentry docs

Chrome DevTools Conflict

When browserProfilingIntegration is active, Chrome DevTools profiler shows Sentry’s overhead mixed into rendering work. Disable the integration when doing local DevTools profiling sessions.


Complete Setup Example

// instrumentation-client.ts (Browser)
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  integrations: [
    Sentry.browserTracingIntegration(),
    Sentry.browserProfilingIntegration(),
  ],
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
  profileSessionSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
  profileLifecycle: "trace",
});
// sentry.server.config.ts (Node.js)
import * as Sentry from "@sentry/nextjs";
import { nodeProfilingIntegration } from "@sentry/profiling-node";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [nodeProfilingIntegration()],
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
  profileSessionSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
  profileLifecycle: "trace",
});
// sentry.edge.config.ts (Edge — NO profiling)
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1,
  // nodeProfilingIntegration NOT added — Edge runtime doesn't support native add-ons
});
// next.config.ts — required Document-Policy header for browser profiling
import { withSentryConfig } from "@sentry/nextjs";

const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [{ key: "Document-Policy", value: "js-profiling" }],
      },
    ];
  },
};

export default withSentryConfig(nextConfig, {
  org: process.env.SENTRY_ORG,
  project: process.env.SENTRY_PROJECT,
  authToken: process.env.SENTRY_AUTH_TOKEN,
  tunnelRoute: "/monitoring",
});

Troubleshooting

IssueSolution
No browser profiles appearing in SentryVerify Document-Policy: js-profiling is present on document responses (check Network tab in DevTools)
Browser profiles only from some usersExpected — only Chromium users are profiled; Firefox/Safari silently no-op
Chrome DevTools shows inflated rendering timesDisable browserProfilingIntegration() during local DevTools profiling sessions
profileSessionSampleRate has no effect (browser)Ensure browserProfilingIntegration() is listed after browserTracingIntegration() in integrations
No server profiles appearingVerify @sentry/profiling-node version exactly matches @sentry/nextjs version
nodeProfilingIntegration import errorCheck @sentry/profiling-node is installed and versions match; don’t import it in sentry.edge.config.ts
Profiles not linked to spansConfirm profileLifecycle: "trace" is set and tracesSampleRate > 0; both must be set
High CPU usage on serverLower profileSessionSampleRate to 0.1 or 0.05; use SENTRY_PROFILER_LOGGING_MODE=lazy
Native add-on fails to load (Alpine/musl Linux)Ensure the @sentry/profiling-node version supports your OS/arch — check the supported platforms table
Flame graphs show minified namesUpload source maps via withSentryConfig in next.config.ts with authToken and project credentials
Profiles on static host not workingBrowser profiling requires the Document-Policy header — verify your host supports custom response headers

Reference: Session Replay

Session Replay — Sentry Next.js SDK

Minimum SDK: @sentry/nextjs ≥7.27.0+
replayCanvasIntegration(): requires ≥7.98.0+

⚠️ Browser-only feature. Add replayIntegration() only in instrumentation-client.ts. Never in sentry.server.config.ts or sentry.edge.config.ts.


Setup

Session Replay is bundled in @sentry/nextjs — no separate package needed.

// instrumentation-client.ts  ← client-side only
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

  // Sample rates live at init level, NOT inside replayIntegration()
  replaysSessionSampleRate: 0.1,   // record 10% of all sessions from start
  replaysOnErrorSampleRate: 1.0,   // record 100% of sessions that hit an error

  integrations: [
    Sentry.replayIntegration({
      maskAllText: true,     // default: true
      blockAllMedia: true,   // default: true
    }),
  ],
});

Dev tip: Set replaysSessionSampleRate: 1.0 during development to capture every session.

Where NOT to Add Replay

Config fileWhy
sentry.server.config.tsServer runtime — no DOM
sentry.edge.config.tsEdge runtime — no DOM
instrumentation.ts (server section)Server-side code
Any Route Handler or Server ActionServer-side code

Sample Rates

OptionTypeDefaultBehavior
replaysSessionSampleRatenumber (0–1)0Fraction of all sessions recorded continuously from start
replaysOnErrorSampleRatenumber (0–1)0Fraction of sessions captured when an error occurs — flushes ~60s of buffer, then continues recording

Recommended production sample rates by traffic:

Daily SessionsreplaysSessionSampleRatereplaysOnErrorSampleRate
100,000+0.01 (1%)1.0
10,000–100,0000.10 (10%)1.0
Under 10,0000.25 (25%)1.0

Always keep replaysOnErrorSampleRate: 1.0 — error replays provide the most debugging value.

How Sampling Works

  1. When a session starts, replaysSessionSampleRate is checked.
    • Sampled → Session Mode: Recording is sent to Sentry in real-time chunks.
    • Not sampled → Buffer Mode: Last ~60 seconds are kept in memory only. Nothing is sent unless an error occurs.
  2. If an error occurs in a buffered session, replaysOnErrorSampleRate is checked.
    • Sampled: The 60-second buffer plus all subsequent data is sent to Sentry.
    • Not sampled: Buffer is discarded; nothing is sent.

Session Lifecycle

  • Starts: When the SDK first loads/initializes.
  • Ends: After 5 minutes of inactivity OR after a maximum of 60 minutes total.
  • Tab close: Ends the session immediately.
  • Page refreshes/navigations within the same domain and tab are captured within the same session.

replayIntegration() Options Reference

All options go inside Sentry.replayIntegration({}):

General Options

KeyTypeDefaultDescription
stickySessionbooleantrueTracks a user across page refreshes. One tab = one session.
mutationLimitnumber10000Max DOM mutations before recording stops (performance protection).
mutationBreadcrumbLimitnumber750Threshold for sending a warning breadcrumb about large mutations.
minReplayDurationnumber5000 msMin replay length before sending. Max: 15000. Only applies to session sampling.
maxReplayDurationnumber3600000 msMaximum replay length. Capped at 1 hour.
workerUrlstringundefinedURL to a self-hosted compression worker (avoids inline worker in bundle).
beforeAddRecordingEvent(event) => event | nullidentity fnHook to filter/modify recording events before they leave the browser.
beforeErrorSampling(event) => boolean() => trueCalled in buffer mode only. Return false to prevent this error from triggering upload.
slowClickIgnoreSelectorsstring[][]CSS selectors exempt from slow/rage click detection.

Network Capture Options

KeyTypeDefaultDescription
networkDetailAllowUrls(string | RegExp)[][]URLs for which to capture request/response headers and bodies.
networkDetailDenyUrls(string | RegExp)[][]URLs to never capture details for. Takes precedence over allow list.
networkCaptureBodiesbooleantrueWhether to capture request/response bodies for allowed URLs.
networkRequestHeadersstring[][]Additional request headers to capture (beyond Content-Type, Content-Length, Accept).
networkResponseHeadersstring[][]Additional response headers to capture.

Privacy Masking

All masking/blocking happens on the client before any data is sent to Sentry’s servers.

Default Privacy Behavior

SettingDefaultEffect
maskAllTexttrueEvery text character replaced with *
maskAllInputstrueAll <input> values masked
blockAllMediatrueimg, svg, video, object, picture, embed, map, audio replaced with same-size placeholder

Privacy Options in replayIntegration({})

KeyTypeDefaultDescription
maskstring[]['.sentry-mask', '[data-sentry-mask]']Additional selectors to mask.
maskAllTextbooleantrueMask all text via maskFn.
maskAllInputsbooleantrueMask all input values.
maskFn(text: string) => string(s) => '*'.repeat(s.length)Custom masking function.
blockstring[]['.sentry-block', '[data-sentry-block]']Additional selectors to block (replaced with a blank same-size box).
blockAllMediabooleantrueBlock all media elements.
ignorestring[]['.sentry-ignore', '[data-sentry-ignore]']Input events on matching elements are ignored entirely.
unblockstring[][]Selectors to un-block from blockAllMedia.
unmaskstring[][]Selectors to un-mask from maskAllText.

Three Privacy Mechanisms Compared

MechanismWhat It DoesHTML AttributeCSS Class
MaskReplaces text chars with *data-sentry-masksentry-mask
BlockReplaces entire element with blank boxdata-sentry-blocksentry-block
IgnoreSuppresses input events on the elementdata-sentry-ignoresentry-ignore

Code Examples

Opt-out of all masking (for non-PII sites):

Sentry.replayIntegration({
  // Only use if your site has NO sensitive data
  maskAllText: false,
  blockAllMedia: false,
});

Custom masking selectors:

Sentry.replayIntegration({
  mask: [".sensitive-field", "[data-pii]"],
  unmask: [".safe-to-show"],
  block: [".user-avatar", "#credit-card-form"],
  unblock: [".public-image"],
  ignore: ["#search-input"],
});

HTML-level masking (no JS config needed):

<!-- Block this form entirely -->
<form data-sentry-block>...</form>

<!-- Mask text in this element -->
<div class="sentry-mask">Sensitive content</div>

<!-- Ignore events on this input -->
<input class="sentry-ignore" type="text" />

⚠️ v8 Breaking Change: In SDK v8+, unblock and unmask no longer automatically add sentry-unblock/sentry-unmask class selectors. To restore v7 behavior:

Sentry.replayIntegration({
  unblock: [".sentry-unblock, [data-sentry-unblock]"],
  unmask: [".sentry-unmask, [data-sentry-unmask]"],
});

Network Request/Response Capture

By default, Replay captures only: URL, body size, method, status code. SDK ≥7.50.0 required for headers/bodies.

Sentry.replayIntegration({
  // Capture details for all same-origin requests
  networkDetailAllowUrls: [
    window.location.origin,
    "api.example.com",
    /^https:\/\/api\.example\.com/,
  ],

  // Exclude PII-heavy endpoints
  networkDetailDenyUrls: ["/api/auth", /\/users\/\d+\/private/],

  networkCaptureBodies: true,
  networkRequestHeaders: ["Cache-Control", "X-Request-ID"],
  networkResponseHeaders: ["X-RateLimit-Remaining"],
});

Limits:

  • Bodies truncated to 150k characters max.
  • Only text-based bodies captured: JSON, XML, FormData. Binary/media excluded.
  • Sentry applies server-side PII scrubbing (credit cards, SSNs, private keys) on ingested data.

Tree-Shaking Replay out of Server Bundles

Critical for Next.js: Session Replay is browser-only. Prevent it from being bundled into server-side or edge bundles:

// next.config.js
const { withSentryConfig } = require("@sentry/nextjs");

module.exports = withSentryConfig(nextConfig, {
  webpack: {
    treeshake: {
      removeDebugLogging: true,        // Strip SDK internal debug logs
      excludeReplayIframe: false,      // Remove iframe content capture if unused
      excludeReplayShadowDOM: false,   // Remove shadow DOM capture if unused
      excludeReplayCompressionWorker: false, // Remove if using custom workerUrl
    },
  },
});
OptionTypeDefaultDescription
removeDebugLoggingbooleanfalseStrips SDK internal console.log calls. Safe to enable in production.
removeTracingbooleanfalseRemoves ALL tracing code. Never call Sentry.startSpan() etc. if enabled.
excludeReplayIframebooleanfalseRemoves iframe content capture from Replay bundle.
excludeReplayShadowDOMbooleanfalseRemoves shadow DOM capture from Replay bundle.
excludeReplayCompressionWorkerbooleanfalseRemoves built-in compression worker. Requires providing workerUrl.

⚠️ Tree-shaking only works with webpack builds. Turbopack is not supported.


Canvas Recording

⚠️ There is currently NO PII scrubbing in canvas recordings. Use with caution.

Canvas recording is opt-in and requires SDK ≥7.98.0:

// instrumentation-client.ts
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  integrations: [
    Sentry.replayIntegration(),
    Sentry.replayCanvasIntegration(), // adds canvas support
  ],
});

For WebGL/3D canvases (manual snapshot mode):

Sentry.replayCanvasIntegration({
  enableManualSnapshot: true,
});

function paint() {
  // ... your rendering commands ...

  const canvasEl = document.querySelector<HTMLCanvasElement>("#my-canvas");
  Sentry.getClient()
    ?.getIntegrationByName("ReplayCanvas")
    // @ts-ignore
    ?.snapshot(canvasEl);
}

Lazy Loading Replay

To reduce initial bundle size, add replayIntegration() dynamically after the page loads:

// instrumentation-client.ts

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  integrations: [], // Replay NOT included here
});

// Later — after route change, user interaction, or feature flag check
import("@sentry/nextjs").then((lazySentry) => {
  Sentry.addIntegration(lazySentry.replayIntegration());
});

Programmatic Replay Control

const replay = Sentry.getReplay();

replay.start();           // Start in session mode (sends continuously)
replay.startBuffering();  // Start in buffer mode (only sends on error)
await replay.stop();      // End the current session
await replay.flush();     // Force upload any pending buffered data

Use cases:

  • User-based sampling: Check authentication, then call flush() for premium users.
  • Route-based sampling: Call start() only on high-value pages.
  • Error filtering: Use beforeErrorSampling to prevent certain error types from triggering upload:
Sentry.replayIntegration({
  beforeErrorSampling: (event) => {
    // Prevent console.error from triggering replay upload
    if (event.logger === "console") return false;
    return true;
  },
});

Custom Compression Worker

Host the compression worker yourself to reduce bundle size and comply with strict CSP policies:

// Step 1: Download worker.min.js from:
// https://github.com/getsentry/sentry-javascript/blob/develop/packages/replay-worker/examples/worker.min.js
// Host at /public/worker.min.js → served at /worker.min.js

// Step 2: Configure
Sentry.replayIntegration({
  workerUrl: "/assets/worker.min.js",
});
// next.config.js — remove built-in worker from bundle
module.exports = withSentryConfig(nextConfig, {
  webpack: {
    treeshake: {
      excludeReplayCompressionWorker: true, // since you're hosting your own
    },
  },
});

Content Security Policy (CSP)

Session Replay uses a Web Worker for off-thread compression. Required CSP directives:

worker-src 'self' blob:
child-src 'self' blob:   ← Required for Safari ≤ 15.4

Also add sentry.io to your CORS policy so the Sentry replay iframe can fetch CSS, fonts, and images.

For Next.js, set headers in next.config.js:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Content-Security-Policy",
            value: "default-src 'self'; worker-src 'self' blob:; child-src 'self' blob:;",
          },
        ],
      },
    ];
  },
};

Performance Impact

  • Bundle size: ~50KB gzipped added to browser bundle.
  • Compression: Off-thread in a Web Worker — does not block the main thread.
  • Mutation protection: Recording auto-stops if DOM mutations exceed mutationLimit (default 10,000).
  • Large lists: Virtualize or paginate long lists to avoid mutation limit triggers.

Rage/slow click false positives (Download/Print buttons that don’t mutate DOM):

Sentry.replayIntegration({
  slowClickIgnoreSelectors: [
    ".download-btn",
    'a[label*="download" i]',
    "#print-button",
  ],
});

Troubleshooting

ProblemCauseSolution
Replay data missingCSP blocking blob: workersAdd worker-src 'self' blob:
CSS/fonts missing in replayCORS blocking Sentry iframeAdd sentry.io to CORS policy
Replay not recordingAdded to wrong config fileMove to instrumentation-client.ts only
Click positions misalignedCustom variable-width fontsAdd Access-Control-Allow-Origin headers for fonts
Too many rage clicksNon-mutating buttons (Download, Print)Use slowClickIgnoreSelectors
Replay stops earlyToo many DOM mutationsVirtualize lists; adjust mutationLimit
captureConsoleIntegration triggers replaysconsole.error counted as errorUse beforeErrorSampling to return false for console events
iframe content not maskedsrcdoc attribute bypasses maskingAdd block: ["iframe"] to block iframes entirely
Canvas not recordingNot using replayCanvasIntegration()Add Sentry.replayCanvasIntegration() alongside replayIntegration()
Build error about browser globals in serverReplay leaking into server bundleUse tree-shaking options in withSentryConfig
replayCanvasIntegration not availableSDK version too oldUpgrade to ≥7.98.0

Reference: Tracing

Tracing — Sentry Next.js SDK

Minimum SDK: @sentry/nextjs ≥8.0.0
withServerActionInstrumentation: ≥8.0.0
enableLongAnimationFrame: ≥8.18.0
ignoreSpans: ≥10.2.0


How Tracing Is Activated

Tracing is enabled by setting tracesSampleRate or tracesSampler in all three runtime config files. Without one of these, no spans are created.

Config fileRuntimeWhat it traces
instrumentation-client.tsBrowserPage loads, navigations, fetch/XHR, Web Vitals, INP
sentry.server.config.tsNode.jsAPI routes, RSC renders, getServerSideProps, background work
sentry.edge.config.tsEdgeNext.js middleware

⚠️ All three must have tracing configured. Missing one means that runtime produces no spans.


tracesSampleRate — Uniform Sampling

A number between 0.0 and 1.0. Set the same option in all three configs:

// Recommended: 100% in development, lower in production
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,

To disable tracing entirely: omit both tracesSampleRate and tracesSampler. Setting tracesSampleRate: 0 is not the same — it still activates instrumentation but sends nothing.


tracesSampler — Dynamic Per-Request Sampling

When defined, tracesSampler takes precedence over tracesSampleRate. Receives a SamplingContext and returns a number (0–1) or boolean.

// TypeScript: SamplingContext shape
interface SamplingContext {
  name: string;                                      // e.g. "GET /api/users"
  attributes: SpanAttributes | undefined;
  parentSampled: boolean | undefined;                // parent's sampling decision
  parentSampleRate: number | undefined;
  inheritOrSampleWith: (fallbackRate: number) => number;
}

Route-Based Sampling

Sentry.init({
  tracesSampler: ({ name, inheritOrSampleWith }) => {
    // Always drop health checks
    if (name.includes("/health") || name.includes("/ping")) return 0;

    // Always sample critical flows
    if (name.includes("/checkout") || name.includes("/payment")) return 1.0;

    // Sample admin routes at 50%
    if (name.includes("/admin")) return 0.5;

    // For everything else: honor parent's decision, fall back to 10%
    return inheritOrSampleWith(0.1);
  },
});

With Parent Trace Inheritance

Sentry.init({
  tracesSampler: ({ name, parentSampled, inheritOrSampleWith }) => {
    if (name.includes("healthcheck")) return 0;
    if (name.includes("auth"))        return 1;
    // inheritOrSampleWith: respects parent decision if present, else uses fallback
    return inheritOrSampleWith(0.5);
  },
});

Why use inheritOrSampleWith instead of checking parentSampled directly?
It ensures consistent rates flow through distributed traces, enables accurate metric extrapolation, and sets the correct sentry-sampled value in downstream baggage.

Sampling Precedence

  1. tracesSampler function (if defined) — evaluated first
  2. Parent’s sampling decision (propagated via sentry-trace header)
  3. tracesSampleRate (uniform fallback)

Auto-Instrumented Operations

Client-Side (Browser)

OperationOpWhat’s captured
Initial page loadpageloadLCP, CLS, FCP, TTFB Web Vitals; resource load child spans
Client-side navigationnavigationRoute change duration; child fetch/XHR spans
fetch() requestshttp.clientURL, method, status code, duration, HTTP timings
XMLHttpRequesthttp.clientSame as fetch
User interactionsui.interactionINP (Interaction to Next Paint) — emitted on page hide
Long Tasks (> 50ms)ui.long-taskMain-thread blocking events
Long Animation Framesui.long-animation-frameLoAF rendering work — SDK ≥8.18.0

Server-Side (Node.js)

OperationOpNotes
API route handlers (App Router)http.serverapp/api/*/route.ts — auto-instrumented
API route handlers (Pages Router)http.serverpages/api/*.ts — auto-instrumented
React Server Componentshttp.serverRSC render times
getServerSidePropshttp.serverPages Router SSR data fetching
Edge Middlewarehttp.serverVia sentry.edge.config.ts

⚠️ Server Actions are NOT auto-instrumented. Wrap each with withServerActionInstrumentation() — see below.


browserTracingIntegration Options

// instrumentation-client.ts
Sentry.init({
  integrations: [
    Sentry.browserTracingIntegration({
      // Page Load & Navigation
      instrumentPageLoad: true,        // default: true
      instrumentNavigation: true,      // default: true

      // HTTP spans
      traceFetch: true,                // default: true
      traceXHR: true,                  // default: true
      enableHTTPTimings: true,         // default: true
      shouldCreateSpanForRequest: (url) => !url.includes("/health"),

      // Performance observations
      enableLongTask: true,            // default: true
      enableLongAnimationFrame: true,  // default: true (SDK ≥8.18.0)
      enableInp: true,                 // INP spans

      // Span lifecycle
      idleTimeout: 1000,               // ms: wait after last child before ending
      finalTimeout: 30000,             // ms: hard cap on span duration
      childSpanTimeout: 15000,         // ms: max time for child spans

      // Span naming — parameterize URLs
      beforeStartSpan: (context) => ({
        ...context,
        name: context.name.replace(/\/\d+/g, "/<id>"),
      }),

      // Span filtering
      ignoreResourceSpans: ["resource.css", "resource.script", "resource.img"],
    }),
  ],
});

Custom Spans

Wraps a block of work. The span becomes active (children nest under it) and ends automatically when the callback returns or resolves:

// Async
const data = await Sentry.startSpan(
  {
    name: "fetchUserProfile",
    op: "http.client",
    attributes: { "user.id": userId, "cache.hit": false },
  },
  async () => {
    const res = await fetch(`/api/users/${userId}`);
    return res.json();
  },
);

// Sync
const result = Sentry.startSpan(
  { name: "computeRecommendations", op: "function" },
  () => expensiveComputation(),
);

Nested Spans (Parent–Child Hierarchy)

await Sentry.startSpan({ name: "checkout-flow", op: "function" }, async () => {
  // These are automatically children of "checkout-flow"
  const cart = await Sentry.startSpan(
    { name: "fetchCart", op: "db.query" },
    () => db.cart.findUnique({ where: { userId } }),
  );

  const payment = await Sentry.startSpan(
    { name: "processPayment", op: "http.client" },
    () => stripe.paymentIntents.create({ amount: cart.total }),
  );

  return { cart, payment };
});

Sentry.startSpanManual() — Active, Manual End

Use when the span lifetime cannot be enclosed in a callback:

function authMiddleware(req: Request, res: Response, next: NextFunction) {
  return Sentry.startSpanManual({ name: "auth.verify", op: "middleware" }, (span) => {
    res.once("finish", () => {
      span.setStatus({ code: res.statusCode < 400 ? 1 : 2 });
      span.end(); // ← required
    });
    return next();
  });
}

Sentry.startInactiveSpan() — Not Active, Manual End

Creates a span that is never automatically made active. Use for parallel work or event-based tracking:

// Parallel independent operations
const spanA = Sentry.startInactiveSpan({ name: "operation-a" });
const spanB = Sentry.startInactiveSpan({ name: "operation-b" });

await Promise.all([doA(), doB()]);

spanA.end();
spanB.end();

// Explicit parent assignment
const parent = Sentry.startInactiveSpan({ name: "parent" });
const child = Sentry.startInactiveSpan({ name: "child", parentSpan: parent });
child.end();
parent.end();

Browser: setActiveSpanInBrowser() — Persistent Active Span

When a callback-based API isn’t practical (e.g., UI event handlers), keep a span active across event calls. Available since SDK v10.15.0:

let checkoutSpan: Sentry.Span | undefined;

onCheckoutStart(() => {
  checkoutSpan = Sentry.startInactiveSpan({ name: "checkout-flow" });
  Sentry.setActiveSpanInBrowser(checkoutSpan);
});

onCheckoutComplete(() => {
  checkoutSpan?.end();
});

⚠️ setActiveSpanInBrowser is browser-only.


Span Options Reference

interface StartSpanOptions {
  name: string;              // Required: label shown in the UI
  op?: string;               // Operation category (see table below)
  attributes?: Record<string, string | number | boolean>;
  parentSpan?: Span;         // Override automatic parent
  onlyIfParent?: boolean;    // Skip span if no active parent exists
  forceTransaction?: boolean; // Force display as root transaction in UI
  startTime?: number;        // Unix timestamp in seconds
}

Common op values:

opUse for
http.clientOutgoing HTTP requests (fetch, XHR)
http.serverIncoming HTTP requests (API routes, SSR)
db / db.queryDatabase queries
db.redisRedis operations
functionGeneral function calls
ui.renderComponent render time
ui.action.clickClick event handling
cache.get / cache.putCache reads/writes
queue.publish / queue.processMessage queue operations
taskBackground / scheduled work

Span Enrichment

// Set attributes on the currently active span
const span = Sentry.getActiveSpan();
if (span) {
  span.setAttribute("db.table", "users");
  span.setAttributes({
    "http.method": "POST",
    "order.total": 99.99,
    "user.tier": "premium",
  });

  // Status: 0=unset, 1=ok, 2=error
  span.setStatus({ code: 1 });
  span.setStatus({ code: 2, message: "Payment declined" });
}

// Rename a span at runtime
const span = Sentry.getActiveSpan();
if (span) Sentry.updateSpanName(span, "GET /users/:id");

// Modify all spans globally before sending
Sentry.init({
  beforeSendSpan(span) {
    span.data = {
      ...span.data,
      "deployment.region": process.env.AWS_REGION ?? "unknown",
    };
    return span; // return null to drop (but prefer ignoreSpans for that)
  },
});

Server Actions — withServerActionInstrumentation()

Server Actions are not auto-instrumented. Wrap each with withServerActionInstrumentation():

// app/actions/order.ts
"use server";
import * as Sentry from "@sentry/nextjs";
import { headers } from "next/headers";

export async function createOrder(formData: FormData) {
  return Sentry.withServerActionInstrumentation(
    "createOrder",                       // Action name (becomes span name)
    {
      headers: await headers(),          // Enables distributed trace continuation
      formData,                          // Logged as span data
      recordResponse: true,              // Capture the return value
    },
    async () => {
      const order = await db.orders.create({
        data: { items: formData.get("items"), userId: getCurrentUser() },
      });
      return { success: true, orderId: order.id };
    },
  );
}

Options:

OptionTypeDescription
formDataFormDataLogged with the span
headersHeadersRequired for distributed trace continuation — always pass await headers()
recordResponsebooleanWhether to capture the return value as span data

Distributed Tracing

How It Works

Sentry injects two HTTP headers into outgoing requests:

HeaderFormatPurpose
sentry-trace{traceId}-{spanId}-{sampled}Carries trace context
baggageW3C Baggage with sentry-* keysCarries sampling decision + metadata

Backends must allowlist these headers for CORS:

Access-Control-Allow-Headers: sentry-trace, baggage

tracePropagationTargets

Controls which outgoing requests get trace headers. Accepts strings (substring match) and/or RegExp:

// instrumentation-client.ts
Sentry.init({
  tracePropagationTargets: [
    "localhost",                            // any URL containing "localhost"
    /^https:\/\/api\.yourapp\.com/,         // your API
    /^https:\/\/auth\.yourapp\.com/,        // auth service
    /^\//,                                  // all same-origin relative paths
  ],
});

Default: ['localhost', /^\//] — only localhost and same-origin requests.
Disable entirely: tracePropagationTargets: []

⚠️ If your API is at http://localhost:3001, use "localhost:3001" or a regex matching the port — "localhost" alone won’t match.

Automatic SSR → Client Trace Continuation

When Next.js server-renders a page, Sentry emits trace context as <meta> tags in <head>. The browser SDK reads them automatically to continue the same trace:

<!-- Auto-injected by Next.js SDK — no configuration needed -->
<meta name="sentry-trace" content="12345678...-1234567890123456-1" />
<meta name="baggage" content="sentry-trace_id=12345678...,sentry-sample_rate=0.1,..." />

This means a single distributed trace spans the server render and subsequent client-side activity.

Manual Trace Propagation (Non-HTTP Channels)

For WebSockets, message queues, or other protocols:

// Sender — extract current trace context
const traceData = Sentry.getTraceData();
// Returns: { "sentry-trace": "...", "baggage": "..." }

webSocket.send(JSON.stringify({
  payload: myData,
  _sentryMeta: {
    sentryTrace: traceData["sentry-trace"],
    baggage: traceData["baggage"],
  },
}));

// Receiver — continue the trace
const { sentryTrace, baggage } = message._sentryMeta;

Sentry.continueTrace({ sentryTrace, baggage }, () => {
  return Sentry.startSpan({ name: "handleWebSocketMessage" }, () => {
    processMessage(message);
  });
});

Head-Based Sampling

The originating (head) service makes the sampling decision. That decision propagates to all downstream services via sentry-trace. All services either all sample or all drop the trace — ensuring complete traces, never partial ones.


Advanced Span APIs

continueTrace() — Continue Incoming Trace

// When receiving trace headers from a message queue, cron trigger, etc.
Sentry.continueTrace(
  {
    sentryTrace: incomingHeaders["sentry-trace"],
    baggage: incomingHeaders["baggage"],
  },
  () => {
    return Sentry.startSpan({ name: "processJob", op: "function" }, () =>
      doWork(),
    );
  },
);

startNewTrace() — Force a New Trace

// Break the distributed chain — start a completely independent trace
Sentry.startNewTrace(() => {
  return Sentry.startSpan({ name: "isolated-operation" }, () => doWork());
});

suppressTracing() — Prevent Span Capture

// Prevent spans inside this callback from being sent to Sentry
const result = Sentry.suppressTracing(() => {
  return fetch("/internal/health"); // No span created
});

getActiveSpan(), getRootSpan()

const span = Sentry.getActiveSpan();
if (span) {
  span.setAttribute("custom.key", "value");
  const root = Sentry.getRootSpan(span);
  console.log(Sentry.spanToJSON(root).name);
}

withActiveSpan() — Run Code with a Specific Active Span

const mySpan = Sentry.startInactiveSpan({ name: "background-task" });

await Sentry.withActiveSpan(mySpan, async (scope) => {
  scope.setTag("task.type", "email");
  await sendEmails(); // Errors associate with mySpan
});

mySpan.end();

forceTransaction and onlyIfParent

// Forces span to appear as root transaction in Sentry UI
Sentry.startSpan(
  { name: "background-job", op: "function", forceTransaction: true },
  () => runBackgroundJob(),
);

// Only creates span when an active parent exists (drops orphan spans)
Sentry.startSpan(
  { name: "optional-metric", onlyIfParent: true },
  () => measureSomething(),
);

Browser Flat Span Hierarchy

In browsers, all child spans are attached flat to the root span by default. To opt into true nesting (use with care — can produce incorrect data with concurrent async operations):

Sentry.init({
  parentSpanIsAlwaysRootSpan: false,
});

Complete Config Example (All Three Runtimes)

// instrumentation-client.ts (Browser)
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NODE_ENV,

  integrations: [
    Sentry.browserTracingIntegration({
      shouldCreateSpanForRequest: (url) => !url.match(/\/health$/),
    }),
  ],

  tracesSampler: ({ name, inheritOrSampleWith }) => {
    if (name.includes("health")) return 0;
    if (name.includes("/checkout")) return 1.0;
    return inheritOrSampleWith(0.1);
  },

  tracePropagationTargets: [
    "localhost",
    /^https:\/\/api\.myapp\.com/,
  ],
});
// sentry.server.config.ts (Node.js)
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,

  tracesSampler: ({ name, inheritOrSampleWith }) => {
    if (name.includes("healthcheck")) return 0;
    return inheritOrSampleWith(0.1);
  },
});
// sentry.edge.config.ts (Edge)
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1,
});

Troubleshooting

IssueSolution
No transactions in Performance dashboardVerify tracesSampleRate or tracesSampler is set; confirm it’s set in all three runtime configs
Server Actions not tracedWrap each with withServerActionInstrumentation(); it’s not auto-instrumented
Distributed trace not linking frontend → backendAdd backend URL to tracePropagationTargets; verify Access-Control-Allow-Headers: sentry-trace, baggage on the backend
SSR page load not linked to server traceThis is automatic — verify both client and server use the same DSN
API requests missing sentry-trace headerCheck CORS preflight — backend must allow sentry-trace and baggage
Transaction names show raw URLs (/users/42)Use beforeStartSpan to parameterize: replace /\d+/g with /<id>
tracesSampler not workingWhen both tracesSampler and tracesSampleRate are set, tracesSampler wins — expected behavior
Spans missing after async gap (browser)Browser uses flat hierarchy; use startInactiveSpan with explicit parentSpan across async boundaries
tracePropagationTargets port not matching"localhost" won’t match localhost:3001 — use "localhost:3001" or a regex
High transaction volumeUse tracesSampler to return 0 for health checks; lower default rate with inheritOrSampleWith(0.02)
Server-only spans not appearingVerify instrumentation.ts exports onRequestError = Sentry.captureRequestError and loads the server config
#sentry #nextjs #sdk

数据统计

总访客 -- 总访问 --
ESC
输入关键词开始搜索