All skills
getsentry avatar

/sentry-nextjs-sdk

@77d210f

Full Sentry SDK setup for Next.js. Use when asked to "add Sentry to Next.js", "install @sentry/nextjs", or configure error monitoring, tracing, session replay, logging, profiling, AI monitoring, or crons for Next.js applications. Supports Next.js 13+ with App Router and Pages Router.

Use this Skill: https://skilld.dev/gh/getsentry/sentry-agent-skills/sentry-nextjs-sdk

This session only. Nothing lands on disk.

referencestracing.md

≈4.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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 file Runtime What it traces
instrumentation-client.ts Browser Page loads, navigations, fetch/XHR, Web Vitals, INP
sentry.server.config.ts Node.js API routes, RSC renders, getServerSideProps, background work
sentry.edge.config.ts Edge Next.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)

Operation Op What's captured
Initial page load pageload LCP, CLS, FCP, TTFB Web Vitals; resource load child spans
Client-side navigation navigation Route change duration; child fetch/XHR spans
fetch() requests http.client URL, method, status code, duration, HTTP timings
XMLHttpRequest http.client Same as fetch
User interactions ui.interaction INP (Interaction to Next Paint) — emitted on page hide
Long Tasks (> 50ms) ui.long-task Main-thread blocking events
Long Animation Frames ui.long-animation-frame LoAF rendering work — SDK ≥8.18.0

Server-Side (Node.js)

Operation Op Notes
API route handlers (App Router) http.server app/api/*/route.ts — auto-instrumented
API route handlers (Pages Router) http.server pages/api/*.ts — auto-instrumented
React Server Components http.server RSC render times
getServerSideProps http.server Pages Router SSR data fetching
Edge Middleware http.server Via 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

Sentry.startSpan() — Active, Auto-Ending (Recommended)

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:

op Use for
http.client Outgoing HTTP requests (fetch, XHR)
http.server Incoming HTTP requests (API routes, SSR)
db / db.query Database queries
db.redis Redis operations
function General function calls
ui.render Component render time
ui.action.click Click event handling
cache.get / cache.put Cache reads/writes
queue.publish / queue.process Message queue operations
task Background / 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:

Option Type Description
formData FormData Logged with the span
headers Headers Required for distributed trace continuation — always pass await headers()
recordResponse boolean Whether to capture the return value as span data

Distributed Tracing

How It Works

Sentry injects two HTTP headers into outgoing requests:

Header Format Purpose
sentry-trace {traceId}-{spanId}-{sampled} Carries trace context
baggage W3C Baggage with sentry-* keys Carries 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

Issue Solution
No transactions in Performance dashboard Verify tracesSampleRate or tracesSampler is set; confirm it's set in all three runtime configs
Server Actions not traced Wrap each with withServerActionInstrumentation(); it's not auto-instrumented
Distributed trace not linking frontend → backend Add backend URL to tracePropagationTargets; verify Access-Control-Allow-Headers: sentry-trace, baggage on the backend
SSR page load not linked to server trace This is automatic — verify both client and server use the same DSN
API requests missing sentry-trace header Check 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 working When 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 volume Use tracesSampler to return 0 for health checks; lower default rate with inheritOrSampleWith(0.02)
Server-only spans not appearing Verify instrumentation.ts exports onRequestError = Sentry.captureRequestError and loads the server config

Source: SKILL.md on GitHub

1 alert17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides comprehensive instructions for setting up Sentry SDKs within a Next.js environment. It includes discovery steps to identify project structure and provides configuration guidance for features such as error monitoring, tracing, and session replay. All tools and packages referenced are official resources from Sentry, a well-known service provider.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    8/8 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 77d210f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last week.

Dormantupdated 7 months ago

README badge

README badge for getsentry/sentry-agent-skills/sentry-nextjs-sdk