
Sentry React Sdk
- 2.6k installs
- 243 repo stars
- Updated July 27, 2026
- getsentry/sentry-for-ai
sentry-react-sdk guides full @sentry/react setup with detect, recommend, and configure phases for React observability.
About
Sentry React SDK is an opinionated wizard scanning React projects and guiding complete @sentry/react setup for error monitoring, tracing, session replay, profiling, and logging. Phase one detects React version, existing Sentry packages, router type, Redux usage, build tool, and adjacent backend directories via package.json and config file inspection. Phase two recommends error monitoring always plus tracing and session replay for SPAs, with optional logging and experimental profiling when cross-origin isolation allows. Phase three installs @sentry/react, creates instrument.ts initialized before other imports, configures browserTracingIntegration and replayIntegration, and sets DSN env vars per Vite, CRA, or webpack. React 19 uses reactErrorHandler on createRoot while earlier versions wrap Sentry.ErrorBoundary. Router integrations cover React Router v5 through v7 non-framework and TanStack Router browser tracing. Redux apps add createReduxEnhancer and Vite projects use sentryVitePlugin for source maps. Framework mode React Router apps should switch to sentry-react-router-framework-sdk instead.
- Detect React version, router, Redux, and build tool first.
- instrument.ts must import before all other entry code.
- React 19 reactErrorHandler versus ErrorBoundary patterns.
- browserTracingIntegration and replayIntegration defaults.
- Router and Redux integrations plus Vite source map plugin.
Sentry React Sdk by the numbers
- 2,551 all-time installs (skills.sh)
- +52 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #31 of 610 Debugging skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
sentry-react-sdk capabilities & compatibility
- Capabilities
- project detection for react, router, and build t · instrument.ts early initialization pattern · version specific error boundary or handler setup · tracing and replay integration configuration · react router and tanstack tracing integrations · redux enhancer and vite source map plugin guidan
- Works with
- sentry · vercel
- Use cases
- debugging · ci cd
- Pricing
- Freemium
npx skills add https://github.com/getsentry/sentry-for-ai --skill sentry-react-sdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 243 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | getsentry/sentry-for-ai ↗ |
How do I add Sentry error monitoring, tracing, and replay to a React app with the right router integration?
Add Sentry error monitoring, tracing, session replay, and logging to React apps with router and Redux integrations.
Who is it for?
React developers adding production error monitoring, navigation tracing, or session replay.
Skip if: Skip for React Router Framework mode apps that need sentry-react-router-framework-sdk instead.
When should I use this skill?
User asks to add Sentry to React, install @sentry/react, or configure error boundaries and tracing.
What you get
Initialized Sentry with DSN, integrations, version-appropriate error handling, and optional source maps.
- sentry sdk configuration
- automatic error capture setup
- react error handler integration
By the numbers
- Requires @sentry/react 8.0.0 or newer
- captureReactException requires @sentry/react 9.8.0+
- reactErrorHandler requires @sentry/react 8.6.0+
Files
All Skills > SDK Setup > React SDK
Sentry React SDK
Opinionated wizard that scans your React project and guides you through complete Sentry setup.
Invoke This Skill When
- User asks to "add Sentry to React" or "set up Sentry" in a React app
- User wants error monitoring, tracing, session replay, profiling, or logging in React
- User mentions
@sentry/react, React Sentry SDK, or Sentry error boundaries - User wants to monitor React Router v5/v6/v7 non-framework navigation, Redux state, or component performance
If project is React Router Framework mode using @sentry/react-router, use sentry-react-router-framework-sdk instead of this skill.
Note: SDK versions and APIs below reflect current Sentry docs at time of writing (@sentry/react ≥8.0.0).Always verify against docs.sentry.io/platforms/javascript/guides/react/ before implementing.
---
Phase 1: Detect
Run these commands to understand the project before making any recommendations:
# Detect React version
cat package.json | grep -E '"react"|"react-dom"'
# Check for existing Sentry
cat package.json | grep '"@sentry/'
# Detect router and framework mode hints
cat package.json | grep -E '"react-router-dom"|"react-router"|"@react-router/"|"@tanstack/react-router"|"@sentry/react-router"'
# Detect state management
cat package.json | grep -E '"redux"|"@reduxjs/toolkit"'
# Detect build tool
ls vite.config.ts vite.config.js webpack.config.js craco.config.js 2>/dev/null
cat package.json | grep -E '"vite"|"react-scripts"|"webpack"'
# Detect logging libraries
cat package.json | grep -E '"pino"|"winston"|"loglevel"'
# Check for companion backend in adjacent directories
ls ../backend ../server ../api 2>/dev/null
cat ../go.mod ../requirements.txt ../Gemfile ../pom.xml 2>/dev/null | head -3What to determine:
| Question | Impact |
|---|---|
| React 19+? | Use reactErrorHandler() hook pattern |
| React <19? | Use Sentry.ErrorBoundary |
@sentry/react already present? | Skip install, go straight to feature config |
React Router Framework mode indicators (@sentry/react-router, @react-router/*)? | Use sentry-react-router-framework-sdk |
react-router-dom v5 / v6 / v7? | Determines which router integration to use |
@tanstack/react-router? | Use tanstackRouterBrowserTracingIntegration() |
| Redux in use? | Recommend createReduxEnhancer() |
| Vite detected? | Source maps via sentryVitePlugin |
CRA (react-scripts)? | Source maps via @sentry/webpack-plugin in CRACO |
| 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 unhandled errors, React error boundaries, React 19 hooks
- ✅ Tracing — React SPAs benefit from page load, navigation, and API call tracing
- ✅ Session Replay — recommended for user-facing apps; records sessions around errors
Optional (enhanced observability):
- ⚡ Logging — structured logs via
Sentry.logger.*; recommend when structured log search is needed - ⚡ Profiling — JS Self-Profiling API (⚠️ experimental; requires cross-origin isolation headers)
Recommendation logic:
| Feature | Recommend when... |
|---|---|
| Error Monitoring | Always — non-negotiable baseline |
| Tracing | Always for React SPAs — page load + navigation spans are high-value |
| Session Replay | User-facing app, login flows, or checkout pages |
| Logging | App needs structured log search or log-to-trace correlation |
| Profiling | Performance-critical app; server sends Document-Policy: js-profiling header |
React-specific extras:
- React 19 detected → set up
reactErrorHandler()oncreateRoot - React Router v5/v6/v7 non-framework detected → configure matching router integration (see Phase 3)
- React Router Framework mode detected → switch to
sentry-react-router-framework-sdk - Redux detected → add
createReduxEnhancer()to Redux store - Vite detected → configure
sentryVitePluginfor source maps (essential for readable stack traces)
Propose: "I recommend setting up Error Monitoring + Tracing + Session Replay. Want me to also add Logging or Profiling?"
---
Phase 3: Guide
Install
npm install @sentry/react --saveCreate src/instrument.ts
Sentry must initialize before any other code runs. Put Sentry.init() in a dedicated sidecar file:
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN, // Adjust per build tool (see table below)
environment: import.meta.env.MODE,
release: import.meta.env.VITE_APP_VERSION, // inject at build time
dataCollection: {
// To disable sending user data and HTTP bodies, uncomment the lines below. For more info visit:
// https://docs.sentry.io/platforms/javascript/guides/react/configuration/options/#dataCollection
// userInfo: false,
// httpBodies: [],
},
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
// Tracing
tracesSampleRate: 1.0, // lower to 0.1–0.2 in production
tracePropagationTargets: ["localhost", /^https:\/\/yourapi\.io/],
// Session Replay
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
enableLogs: true,
});DSN environment variable by build tool:
| Build Tool | Variable Name | Access in code |
|---|---|---|
| Vite | VITE_SENTRY_DSN | import.meta.env.VITE_SENTRY_DSN |
| Create React App | REACT_APP_SENTRY_DSN | process.env.REACT_APP_SENTRY_DSN |
| Custom webpack | SENTRY_DSN | process.env.SENTRY_DSN |
Entry Point Setup
Import instrument.ts as the very first import in your entry file:
// src/main.tsx (Vite) or src/index.tsx (CRA/webpack)
import "./instrument"; // ← MUST be first
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);React Version-Specific Error Handling
React 19+ — use reactErrorHandler() on createRoot:
import { reactErrorHandler } from "@sentry/react";
createRoot(document.getElementById("root")!, {
onUncaughtError: reactErrorHandler(),
onCaughtError: reactErrorHandler(),
onRecoverableError: reactErrorHandler(),
}).render(<App />);React <19 — wrap your app in Sentry.ErrorBoundary:
import * as Sentry from "@sentry/react";
createRoot(document.getElementById("root")!).render(
<Sentry.ErrorBoundary fallback={<p>Something went wrong</p>} showDialog>
<App />
</Sentry.ErrorBoundary>
);Use <Sentry.ErrorBoundary> for any sub-tree that should catch errors independently (route sections, widgets, etc.).
Router Integration
Configure the matching integration for your router (non-framework mode):
| Router | Integration | Notes |
|---|---|---|
| React Router v7 | reactRouterV7BrowserTracingIntegration | useEffect, useLocation, useNavigationType, createRoutesFromChildren, matchRoutes from react-router |
| React Router v6 | reactRouterV6BrowserTracingIntegration | useEffect, useLocation, useNavigationType, createRoutesFromChildren, matchRoutes from react-router-dom |
| React Router v5 | reactRouterV5BrowserTracingIntegration | Wrap routes in withSentryRouting(Route) |
| TanStack Router | tanstackRouterBrowserTracingIntegration(router) | Pass router instance — no hooks required |
| No router / custom | browserTracingIntegration() | Names transactions by URL path |
React Router v6/v7 setup:
// in instrument.ts integrations array:
import React from "react";
import {
createRoutesFromChildren, matchRoutes,
useLocation, useNavigationType,
} from "react-router-dom"; // or "react-router" for v7
import * as Sentry from "@sentry/react";
import { reactRouterV6BrowserTracingIntegration } from "@sentry/react";
import { createBrowserRouter } from "react-router-dom";
// Option A — createBrowserRouter (recommended for v6.4+):
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
const router = sentryCreateBrowserRouter([...routes]);
// Option B — createBrowserRouter for React Router v7:
// const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter);
// Option C — integration with hooks (v6 without data APIs):
Sentry.init({
integrations: [
reactRouterV6BrowserTracingIntegration({
useEffect: React.useEffect,
useLocation,
useNavigationType,
matchRoutes,
createRoutesFromChildren,
}),
],
});TanStack Router setup:
import { tanstackRouterBrowserTracingIntegration } from "@sentry/react";
// Pass your TanStack router instance:
Sentry.init({
integrations: [tanstackRouterBrowserTracingIntegration(router)],
});Redux Integration (when detected)
import * as Sentry from "@sentry/react";
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: rootReducer,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(Sentry.createReduxEnhancer()),
});Source Maps Setup (strongly recommended)
Without source maps, stack traces show minified code. Set up the build plugin to upload source maps automatically:
Vite (`vite.config.ts`):
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
build: { sourcemap: "hidden" },
plugins: [
react(),
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
});Add to .env (never commit):
SENTRY_AUTH_TOKEN=sntrys_...
SENTRY_ORG=my-org-slug
SENTRY_PROJECT=my-project-slugCreate React App (via CRACO):
npm install @craco/craco @sentry/webpack-plugin --save-dev// craco.config.js
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
module.exports = {
webpack: {
plugins: {
add: [
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
},
},
};For Each Agreed Feature
Walk through features one at a time. Load the reference file, follow its steps, verify before moving on:
| Feature | Reference | Load when... |
|---|---|---|
| Error Monitoring | ${SKILL_ROOT}/references/error-monitoring.md | Always (baseline) |
| Tracing | ${SKILL_ROOT}/references/tracing.md | SPA navigation / API call tracing |
| Session Replay | ${SKILL_ROOT}/references/session-replay.md | User-facing app |
| Logging | ${SKILL_ROOT}/references/logging.md | Structured log search / log-to-trace |
| Profiling | ${SKILL_ROOT}/references/profiling.md | Performance-critical app |
| React Features | ${SKILL_ROOT}/references/react-features.md | Redux, component tracking, source maps, integrations catalog |
For each feature: Read ${SKILL_ROOT}/references/<feature>.md, follow steps exactly, verify it works.
---
Configuration Reference
Key Sentry.init() Options
| Option | Type | Default | Notes |
|---|---|---|---|
dsn | string | — | Required. SDK disabled when empty |
environment | string | "production" | e.g., "staging", "development" |
release | string | — | e.g., "my-app@1.0.0" or git SHA — links errors to releases |
dataCollection | object | — | Fine-grained control over data collection (see table below); recommended over `sendDefaultPii` |
sendDefaultPii | boolean | false | ⚠️ Legacy — use dataCollection instead; includes IP addresses and request headers |
tracesSampleRate | number | — | 0–1; 1.0 in dev, 0.1–0.2 in prod |
tracesSampler | function | — | Per-transaction sampling; overrides rate |
tracePropagationTargets | `(string\ | RegExp)[]` | — |
replaysSessionSampleRate | number | — | Fraction of all sessions recorded |
replaysOnErrorSampleRate | number | — | Fraction of error sessions recorded |
enableLogs | boolean | false | Enable Sentry.logger.* API |
attachStacktrace | boolean | false | Stack traces on captureMessage() calls |
maxBreadcrumbs | number | 100 | Breadcrumbs stored per event |
debug | boolean | false | Verbose SDK output to console |
tunnel | string | — | Proxy URL to bypass ad blockers |
dataCollection Options (SDK ≥10.57.0)
Fine-grained control over what data the SDK collects. Replaces the simple sendDefaultPii boolean with per-feature toggles:
| Field | Type | Default | Notes |
|---|---|---|---|
userInfo | boolean | true | Collect user IP and headers (equivalent to sendDefaultPii: true) |
cookies | `boolean \ | { allow: string[] } \ | { deny: string[] }` |
httpHeaders.request | `boolean \ | { allow: string[] } \ | { deny: string[] }` |
httpHeaders.response | `boolean \ | { allow: string[] } \ | { deny: string[] }` |
queryParams | `boolean \ | { allow: string[] } \ | { deny: string[] }` |
httpBodies | HttpBodyCollectionTarget[] | ["incomingRequest", "outgoingRequest", "incomingResponse", "outgoingResponse"] | Collect request/response bodies; options: 'incomingRequest', 'outgoingRequest', 'incomingResponse', 'outgoingResponse' |
genAI.inputs | boolean | true | Record AI model inputs (for AI monitoring) |
genAI.outputs | boolean | true | Record AI model outputs (for AI monitoring) |
stackFrameVariables | boolean | true | Capture local variable values in stack frames |
frameContextLines | number | 5 | Source code context lines around stack frames |
Example: Allow only specific cookies and headers:
Sentry.init({
dataCollection: {
cookies: { allow: ['session', 'user_id'] },
httpHeaders: {
request: { allow: ['authorization', 'x-request-id'] },
response: { deny: ['set-cookie'] },
},
},
});React Compatibility Matrix
| React Version | Error handling approach | SDK minimum |
|---|---|---|
| React 19+ | reactErrorHandler() on createRoot | @sentry/react ≥8.0.0 |
| React 16–18 | Sentry.ErrorBoundary component | @sentry/react ≥7.0.0 |
| React 16 | componentDidCatch class boundaries | @sentry/react ≥6.0.0 |
---
Verification
Trigger test events to confirm Sentry is receiving data:
// Add a temporary test button anywhere in your app
import * as Sentry from "@sentry/react";
function SentryTest() {
return (
<>
<button onClick={() => { throw new Error("Sentry React test error"); }}>
Test Error
</button>
<button onClick={() => Sentry.captureMessage("Sentry test message", "info")}>
Test Message
</button>
</>
);
}Check the Sentry dashboard:
- Issues → error appears within seconds
- Traces → page load and navigation transactions visible
- Replays → session recording visible after page interaction
- Logs → structured log entries if logging enabled
Set debug: true in Sentry.init() and check the browser console if nothing appears.
---
Phase 4: Cross-Link
After completing React setup, check for a companion backend missing Sentry coverage:
ls ../backend ../server ../api ../go ../python 2>/dev/null
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 2>/dev/null | grep '<artifactId>' | head -3If a backend exists without Sentry configured, suggest the matching skill:
| Backend detected | Suggest skill |
|---|---|
Go (go.mod) | sentry-go-sdk |
Python (requirements.txt, pyproject.toml) | sentry-python-sdk |
Ruby (Gemfile) | sentry-ruby-sdk |
Java (pom.xml, build.gradle) | Use @sentry/java — see docs.sentry.io/platforms/java/ |
| Node.js (Express, Fastify) | Use @sentry/node — see docs.sentry.io/platforms/javascript/guides/express/ |
---
Troubleshooting
| Issue | Solution |
|---|---|
| Events not appearing | Set debug: true, check DSN, open browser console for SDK errors |
| Source maps not working | Build in production mode (npm run build); verify SENTRY_AUTH_TOKEN is set |
| Minified stack traces | Source maps not uploading — check plugin config and auth token |
instrument.ts not running first | Verify it's the first import in entry file before React/app imports |
| React 19 errors not captured | Confirm reactErrorHandler() is passed to all three createRoot options |
| React <19 errors not captured | Ensure <Sentry.ErrorBoundary> wraps the component tree |
Router transactions named <unknown> | Add router integration matching your router version |
tracePropagationTargets not matching | Check regex escaping; default is localhost and your DSN origin only |
| Session replay not recording | Confirm replayIntegration() is in init; check replaysSessionSampleRate |
| Redux actions not in breadcrumbs | Add Sentry.createReduxEnhancer() to store enhancers |
| Ad blockers dropping events | Set tunnel: "/sentry-tunnel" and add server-side relay endpoint |
| High replay storage costs | Lower replaysSessionSampleRate; keep replaysOnErrorSampleRate: 1.0 |
| Profiling not working | Verify Document-Policy: js-profiling header is set on document responses |
Error Monitoring — Sentry React SDK
Minimum SDK: @sentry/react ≥8.0.0+captureReactException()requires@sentry/react≥9.8.0
reactErrorHandler()requires@sentry/react≥8.6.0
---
How Automatic Capture Works
The React SDK hooks into the browser environment and captures errors automatically from multiple layers:
| Layer | Mechanism | Integration |
|---|---|---|
| Uncaught JS exceptions | window.onerror | GlobalHandlers (default on) |
| Unhandled promise rejections | window.onunhandledrejection | GlobalHandlers (default on) |
Errors in setTimeout / setInterval / requestAnimationFrame | Patched browser APIs | BrowserApiErrors (default on) |
| React render errors (React <19) | componentDidCatch via <ErrorBoundary> | Sentry.ErrorBoundary |
| React render errors (React 19+) | createRoot hooks | Sentry.reactErrorHandler() |
| Console errors (optional) | Patched console.error | CaptureConsole (opt-in) |
What Requires Manual Instrumentation
The global handlers only catch errors that escape your code. These are silently swallowed without manual calls:
- Errors caught by your own
try/catchblocks - Errors swallowed by React Router's default error boundary
- Business-logic failures (validation errors, unexpected states)
- Async errors inside
Promise.then()chains where.catch()is attached - User-visible conditions that aren't exceptions (use
captureMessage)
Disabling or Customizing Automatic Capture
Sentry.init({
integrations: [
Sentry.globalHandlersIntegration({
onerror: true,
onunhandledrejection: false, // handle rejections manually
}),
],
});
// Manual rejection handler:
window.addEventListener("unhandledrejection", (event) => {
Sentry.captureException(event.reason);
});---
React Error Boundaries
Strategy: React 19+ vs. React ≤18
| React ≤18 | React 19+ | |
|---|---|---|
| Global error reporting | window.onerror + Sentry.ErrorBoundary | Sentry.reactErrorHandler() on createRoot |
| Scoped fallback UI | <Sentry.ErrorBoundary> | <Sentry.ErrorBoundary> (still required) |
| Complementary? | N/A | ✅ Use both together |
---
React 19+ — Sentry.reactErrorHandler() with createRoot
React 19 exposes three hooks on createRoot and hydrateRoot. Pass Sentry.reactErrorHandler() to each one. Requires @sentry/react ≥8.6.0.
// src/main.tsx
import { createRoot } from "react-dom/client";
import * as Sentry from "@sentry/react";
Sentry.init({ dsn: "___PUBLIC_DSN___" });
const container = document.getElementById("app")!;
createRoot(container, {
// Fires for errors that bubble up WITHOUT any ErrorBoundary catching them.
// These are fatal — the entire React tree unmounts.
onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
// Optional: runs AFTER Sentry has already captured the error
console.warn("Uncaught React error:", error.message);
console.warn("Component stack:", errorInfo.componentStack);
}),
// Fires for errors caught BY an ErrorBoundary (React 19 re-routes caught errors here).
// The boundary still renders its fallback UI — this is just the reporting hook.
onCaughtError: Sentry.reactErrorHandler(),
// Fires when React recovers from an error automatically (e.g. hydration mismatch).
onRecoverableError: Sentry.reactErrorHandler(),
}).render(<App />);SSR / `hydrateRoot`:
import { hydrateRoot } from "react-dom/client";
import * as Sentry from "@sentry/react";
hydrateRoot(document.getElementById("app")!, <App />, {
onUncaughtError: Sentry.reactErrorHandler(),
onCaughtError: Sentry.reactErrorHandler(),
onRecoverableError: Sentry.reactErrorHandler(),
});Key behavior differences between the three hooks:
| Hook | Fires when... | Tree state after |
|---|---|---|
onUncaughtError | Error escapes all boundaries | Tree unmounts (fatal) |
onCaughtError | ErrorBoundary catches the error | Boundary renders fallback |
onRecoverableError | React auto-recovers (e.g. hydration) | Tree continues rendering |
React 19 + ErrorBoundary Together (Recommended Pattern)
reactErrorHandler() is the global net. <Sentry.ErrorBoundary> provides scoped fallback UIs. Use both:
// src/main.tsx — global net via reactErrorHandler
createRoot(document.getElementById("root")!, {
onUncaughtError: Sentry.reactErrorHandler(),
onCaughtError: Sentry.reactErrorHandler(),
onRecoverableError: Sentry.reactErrorHandler(),
}).render(<App />);
// src/App.tsx — scoped fallback UIs via ErrorBoundary
function App() {
return (
<Layout>
<Sentry.ErrorBoundary fallback={<NavError />}>
<Navigation />
</Sentry.ErrorBoundary>
<Sentry.ErrorBoundary fallback={<DashboardError />}>
<Dashboard />
</Sentry.ErrorBoundary>
</Layout>
);
}---
<Sentry.ErrorBoundary> — Full Props Reference
Works with React 16+. Catches errors in its subtree, reports them to Sentry, and renders a fallback UI.
// Full TypeScript signature
interface ErrorBoundaryProps {
// Fallback UI — static element or render function
fallback?: React.ReactNode | FallbackRender;
// FallbackRender receives: { error: Error; componentStack: string; resetError: () => void }
// Called immediately when a child throws
onError?: (error: Error, componentStack: string, eventId: string) => void;
// Called with the Sentry Scope before the error is captured — enrich here
beforeCapture?: (scope: Scope, error: Error, componentStack: string) => void;
// Called when resetError() is invoked from the fallback
onReset?: (error: Error | null, componentStack: string | null, eventId: string | null) => void;
// Lifecycle hooks
onMount?: () => void;
onUnmount?: (error: Error | null) => void;
// User feedback dialog — shown automatically on error
showDialog?: boolean;
dialogOptions?: ReportDialogOptions;
}---
fallback — Render Fallback UI on Error
// 1. Static element
<Sentry.ErrorBoundary fallback={<p>Something went wrong. Please refresh.</p>}>
<Dashboard />
</Sentry.ErrorBoundary>
// 2. Render function — access error details and reset handler
<Sentry.ErrorBoundary
fallback={({ error, componentStack, resetError }) => (
<div className="error-state">
<h2>Something broke</h2>
<p><strong>Error:</strong> {error.message}</p>
<details>
<summary>Component stack</summary>
<pre style={{ fontSize: 12 }}>{componentStack}</pre>
</details>
<button onClick={resetError}>↺ Try Again</button>
</div>
)}
>
<Dashboard />
</Sentry.ErrorBoundary>`resetError()` resets the boundary's internal state and re-attempts rendering children. Use it for retry UIs.
---
onError — React to a Captured Error
Called immediately when a child throws. Receives the error, component stack, and the Sentry event ID (useful for linking user feedback to the event).
<Sentry.ErrorBoundary
onError={(error, componentStack, eventId) => {
// Report to your own analytics
myAnalytics.track("error_boundary_triggered", {
errorMessage: error.message,
sentryEventId: eventId,
});
// Dispatch to Redux or Zustand
store.dispatch(setGlobalError({ error, eventId }));
// Show feedback dialog linked to this event
Sentry.showReportDialog({ eventId });
}}
fallback={<ErrorScreen />}
>
<App />
</Sentry.ErrorBoundary>---
beforeCapture — Enrich the Event Before Sending
Called with the Sentry Scope before the error is captured. Use it to add tags, context, or level enrichment specific to this boundary's location in the tree.
<Sentry.ErrorBoundary
beforeCapture={(scope, error, componentStack) => {
scope.setTag("section", "checkout");
scope.setTag("error_type", error.constructor.name);
scope.setExtra("componentStack", componentStack);
scope.setLevel("fatal");
scope.setContext("payment", { step: "card-entry" });
}}
fallback={<CheckoutError />}
>
<CheckoutFlow />
</Sentry.ErrorBoundary>---
onReset — Cleanup When the Boundary Resets
Called when resetError() is invoked. Clear stale state in stores or invalidate caches here.
<Sentry.ErrorBoundary
onReset={(error, componentStack, eventId) => {
queryClient.clear();
store.dispatch(clearCheckoutState());
}}
fallback={({ resetError }) => (
<div>
<p>Payment failed to load.</p>
<button onClick={resetError}>Retry</button>
</div>
)}
>
<CheckoutFlow />
</Sentry.ErrorBoundary>---
showDialog + dialogOptions — Crash-Report Modal on Error
<Sentry.ErrorBoundary
showDialog
dialogOptions={{
title: "It looks like something went wrong.",
subtitle: "Our engineering team has been notified.",
subtitle2: "Want to help us fix it? Tell us what happened.",
labelName: "Your name",
labelEmail: "Your email",
labelComments: "What happened before this error?",
labelSubmit: "Send Report",
successMessage: "Thanks! Your report helps us improve.",
user: { email: "currentuser@example.com", name: "Jane Smith" },
}}
fallback={<p>We've logged this issue and are working on a fix.</p>}
>
<Dashboard />
</Sentry.ErrorBoundary>---
onMount / onUnmount — Lifecycle Hooks
<Sentry.ErrorBoundary
onMount={() => analytics.track("error_boundary_mounted", { section: "dashboard" })}
onUnmount={(error) => {
if (error) analytics.track("error_boundary_active_on_unmount");
}}
fallback={<DashboardError />}
>
<Dashboard />
</Sentry.ErrorBoundary>---
Sentry.withErrorBoundary(Component, options) — HOC Pattern
Equivalent to wrapping with <Sentry.ErrorBoundary>. Useful when you want to wrap at the import or module level instead of in JSX.
import * as Sentry from "@sentry/react";
// Basic
const SafeDashboard = Sentry.withErrorBoundary(Dashboard, {
fallback: <p>Dashboard failed to load.</p>,
});
// Full options — identical to ErrorBoundary props
const SafeCheckout = Sentry.withErrorBoundary(CheckoutFlow, {
fallback: ({ error, resetError }) => (
<div>
<p>Checkout error: {error.message}</p>
<button onClick={resetError}>Retry</button>
</div>
),
onError: (error, componentStack, eventId) => {
analytics.track("checkout_boundary_triggered", { eventId });
},
beforeCapture: (scope) => {
scope.setTag("section", "checkout");
scope.setLevel("fatal");
},
showDialog: true,
});
// Use exactly like the unwrapped component
function App() {
return <SafeCheckout />;
}---
Nested Error Boundaries — Isolation Pattern
Each boundary only catches errors from its own subtree. Nesting lets one broken feature fail in isolation without crashing the whole page.
function App() {
return (
// Outermost — catches anything that escapes inner boundaries
<Sentry.ErrorBoundary
fallback={<FullPageError />}
beforeCapture={(scope) => scope.setTag("level", "app")}
>
<Layout>
<Sentry.ErrorBoundary
fallback={<NavError />}
beforeCapture={(scope) => scope.setTag("section", "navigation")}
>
<Navigation />
</Sentry.ErrorBoundary>
<main>
<Sentry.ErrorBoundary
fallback={<SidebarError />}
beforeCapture={(scope) => scope.setTag("section", "sidebar")}
>
<Sidebar />
</Sentry.ErrorBoundary>
<Sentry.ErrorBoundary
fallback={<ContentError />}
beforeCapture={(scope) => scope.setTag("section", "content")}
>
<MainContent />
</Sentry.ErrorBoundary>
</main>
</Layout>
</Sentry.ErrorBoundary>
);
}Recommended placement strategy:
| Boundary location | Purpose |
|---|---|
Outermost (around <App>) | Last resort — prevents total blank page |
| Route level | Isolate route failures; different fallback per route |
| Widget / panel level | Let other panels stay functional when one fails |
| Data-fetching components | Catch errors from async rendering |
---
Custom Class-Based Error Boundaries — captureReactException
Requires @sentry/react ≥9.8.0If you need a custom class boundary, use captureReactException instead of captureException. It correctly attaches the React componentStack as a linked cause via the LinkedErrors integration, producing readable component traces in Sentry.
import * as Sentry from "@sentry/react";
class CustomBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// errorInfo = { componentStack: "\n at Dashboard\n at App..." }
// captureReactException wires up the componentStack correctly
Sentry.captureReactException(error, errorInfo);
}
render() {
if (this.state.hasError) return <p>Something went wrong.</p>;
return this.props.children;
}
}Why not plain `captureException`? CallingcaptureExceptioninsidecomponentDidCatchloses the component stack linkage.captureReactExceptioncorrectly wireserror.causeso the component tree appears as a linked error in Sentry's issue detail view.
What linked errors look like in Sentry:
Error: Cannot read properties of undefined (reading 'map')
at Dashboard (Dashboard.tsx:42)
Caused by: React component stack:
at Dashboard
at Sentry.ErrorBoundary
at AppRequires React 17+ and the LinkedErrors integration (enabled by default). Set up source maps for readable component file paths.---
Manual Error Capture
Sentry.captureException(error, captureContext?)
Captures an error and sends it to Sentry. Prefer Error objects (they include stack traces). Non-Error values (strings, plain objects) are accepted but may lack stack traces.
// Basic usage
try {
riskyOperation();
} catch (err) {
Sentry.captureException(err);
}
// With full capture context
try {
await chargeCard(order);
} catch (err) {
Sentry.captureException(err, {
level: "fatal", // "fatal"|"error"|"warning"|"log"|"info"|"debug"
tags: { module: "checkout", retried: "true" },
extra: { cartItems: 3, coupon: "SAVE20" },
user: { id: "u_123", email: "user@example.com" },
fingerprint: ["checkout-payment-fail"], // custom grouping key
contexts: {
payment: { provider: "stripe", amount: 9999, currency: "usd" },
},
});
}React-specific tip: Avoid calling Sentry in the render path. Wrap Sentry calls in useEffect to prevent firing on every render:
function UserProfile({ userId }) {
const { data: profile, error } = useQuery(["user", userId], fetchUser);
useEffect(() => {
if (error) {
Sentry.captureException(error, {
tags: { component: "UserProfile" },
extra: { userId },
});
}
}, [error, userId]);
if (error) return <p>Failed to load profile.</p>;
return profile ? <Profile data={profile} /> : null;
}---
Sentry.captureMessage(message, level?)
Captures a plain-text message as a Sentry issue. Useful for non-exception events: deprecated API calls, suspicious conditions, rate-limit hits.
// With level as second argument
Sentry.captureMessage("Payment gateway timeout — fallback triggered", "warning");
// All valid levels: "fatal" | "error" | "warning" | "log" | "info" | "debug"
// Default when omitted: "info"
// With full capture context as second argument
Sentry.captureMessage("Feature flag evaluation failed", {
level: "error",
tags: { flagName: "new-checkout", service: "feature-flags" },
extra: { userId: "u_42", evaluationContext: { country: "DE" } },
});---
Sentry.captureEvent(event)
Low-level API for sending a fully constructed Sentry event object. Use captureException or captureMessage in application code. captureEvent is for custom integrations or forwarding events from legacy loggers.
Sentry.captureEvent({
message: "Legacy logger forwarded event",
level: "warning",
tags: { source: "legacy-logger", module: "billing" },
extra: { rawLog: "something went wrong at line 42" },
timestamp: Date.now() / 1000, // Unix timestamp in seconds
fingerprint: ["legacy-billing-error"],
});---
Try/Catch Patterns in React
Event handlers — errors here are NOT caught by error boundaries (boundaries only catch render errors):
function PaymentForm() {
const [status, setStatus] = useState("idle");
async function handleSubmit(event) {
event.preventDefault();
setStatus("loading");
try {
await processPayment(getFormValues(event.target));
setStatus("success");
} catch (err) {
setStatus("error");
Sentry.captureException(err, {
tags: { component: "PaymentForm", action: "submit" },
extra: { formFields: Object.fromEntries(new FormData(event.target)) },
});
}
}
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit" disabled={status === "loading"}>
{status === "loading" ? "Processing..." : "Pay"}
</button>
{status === "error" && <p>Payment failed. Please try again.</p>}
</form>
);
}Async operations in effects:
useEffect(() => {
async function loadData() {
try {
const data = await fetchDashboardData();
setData(data);
} catch (err) {
Sentry.captureException(err, {
tags: { hook: "useEffect", data: "dashboard" },
});
setError(err);
}
}
loadData();
}, []);Promise chains:
fetchUserData(userId)
.then(processUser)
.catch((err) => {
Sentry.captureException(err, {
tags: { operation: "fetchUserData" },
extra: { userId },
});
return null; // graceful fallback
});---
Context Enrichment
Sentry.setUser(user) — Identify the Current User
Associates a user identity with all subsequent events. Call after login; call Sentry.setUser(null) on logout.
// Accepted fields (all optional):
interface SentryUser {
id?: string | number; // your internal user ID
email?: string;
username?: string;
ip_address?: string; // "{{ auto }}" to infer from request
segment?: string; // e.g. "paid", "trial", "beta", "enterprise"
// Any additional custom fields are accepted
}// On login:
Sentry.setUser({
id: "usr_abc123",
email: "jane.smith@example.com",
username: "janesmith",
segment: "enterprise",
// Custom fields:
plan: "pro",
team_id: "team_789",
account_age_days: 365,
});
// On logout — clears user from all subsequent events:
Sentry.setUser(null);
// Auto-infer IP address (requires sendDefaultPii: true):
Sentry.setUser({
id: "usr_abc123",
ip_address: "{{ auto }}",
});Privacy:sendDefaultPii: trueinSentry.initenables automatic IP inference. To prevent IP storage entirely, enable "Prevent Storing of IP Addresses" in your project's Security & Privacy settings in Sentry.
---
Sentry.setContext(name, data) — Attach Structured Custom Data
Attaches arbitrary structured data to all subsequent events. Context is not indexed or searchable — use tags for filterable data. Context appears in the issue detail view.
// E-commerce checkout context
Sentry.setContext("checkout", {
step: "payment",
cart_items: 3,
total_usd: 99.99,
coupon_applied: "SAVE20",
payment_provider: "stripe",
});
// Feature flags in effect
Sentry.setContext("feature_flags", {
new_checkout: true,
dark_mode: false,
experiment_group: "variant_b",
});
// Remove a context:
Sentry.setContext("checkout", null);Depth: Sentry normalizes context to 3 levels deep by default. Adjust vianormalizeDepthinSentry.init. The keytypeis reserved — don't use it in context objects.
---
Sentry.setTag(key, value) / Sentry.setTags(tags) — Searchable Key-Value Pairs
Tags are indexed and searchable. They power Sentry's filter sidebar, tag distribution charts, and issue similarity detection. Use tags for any data you want to filter or aggregate on.
Constraints: Key ≤32 chars (a-z A-Z 0-9 _ . : -, no spaces). Value ≤200 chars, no newlines.
// Single tag
Sentry.setTag("page_locale", "de-at");
Sentry.setTag("user_plan", "enterprise");
Sentry.setTag("app_version", "2.4.1");
// Multiple at once
Sentry.setTags({
"release.stage": "canary",
"tenant.id": "tenant_abc",
"browser.engine": "blink",
});
// Per-event inline (does not persist to subsequent events)
Sentry.captureException(err, {
tags: { component: "PaymentForm", retry_attempt: "2" },
});
// Scoped — only applies within the callback
Sentry.withScope((scope) => {
scope.setTag("operation", "bulk-delete");
Sentry.captureException(deleteError);
});
// "operation" tag does NOT appear on subsequent eventsDo not overwrite Sentry's built-in tags (browser,os,url,environment,release). Use your own namespaced keys.
---
Sentry.setExtra(key, value) / Sentry.setExtras(extras) — Arbitrary Data
For loosely-typed supplementary data. Prefer setContext for structured data with a meaningful group name.
Sentry.setExtra("raw_api_response", responseText);
Sentry.setExtra("debug_state_dump", JSON.stringify(stateSnapshot));
Sentry.setExtras({
component_version: "3.2.1",
last_action: "submit_form",
form_fields: { total: 5, valid: 3, invalid: 2 },
});---
Inline Context on Capture Calls
All context can be provided per-event using the second argument to captureException or captureMessage. This is the cleanest approach for one-off enrichment:
Sentry.captureException(err, {
user: { id: "u_42", email: "user@example.com" },
level: "fatal",
tags: { module: "checkout", payment_provider: "stripe" },
extra: { formState: JSON.stringify(formValues) },
contexts: {
payment: { provider: "stripe", last4: "4242", amount_cents: 9999 },
},
fingerprint: ["{{ default }}", "stripe-card-error"],
});---
Breadcrumbs
Breadcrumbs are a structured trail of events leading up to an error. They're buffered locally and attached to the next event sent to Sentry.
Automatic Breadcrumbs (Zero Config)
| Type | What's Captured |
|---|---|
ui.click | DOM element clicks (CSS selector or component name if annotation enabled) |
ui.input | Keyboard/input interactions |
navigation | URL changes: pushState, popstate, hash changes |
http | XHR and fetch requests (URL, method, status code) |
console | console.log, warn, error, info, debug output |
sentry | SDK-internal events |
---
Sentry.addBreadcrumb(breadcrumb) — Manual Breadcrumbs
interface Breadcrumb {
type?: "default" | "debug" | "error" | "info" | "navigation" | "http" | "query" | "ui" | "user";
category?: string; // dot-namespaced: "auth", "ui.click", "api.request"
message?: string; // human-readable description
level?: "fatal" | "error" | "warning" | "log" | "info" | "debug";
timestamp?: number; // Unix timestamp in seconds (auto-set if omitted)
data?: Record<string, unknown>;
}// Auth events
Sentry.addBreadcrumb({
category: "auth",
message: "User logged in",
level: "info",
data: { userId: "u_42", method: "oauth2", provider: "google" },
});
Sentry.addBreadcrumb({
category: "auth",
message: "Token refresh failed",
level: "warning",
type: "error",
data: { reason: "expired", expiredAt: "2024-01-15T10:00:00Z" },
});
// Navigation
Sentry.addBreadcrumb({
type: "navigation",
category: "navigation",
message: "User navigated to checkout",
data: { from: "/cart", to: "/checkout/payment" },
});
// API call outcome
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",
},
});
// User actions
Sentry.addBreadcrumb({
type: "user",
category: "ui.click",
message: "Clicked 'Place Order' button",
data: { orderId: "ord_xyz", itemCount: 3, total: 99.99 },
});
// State machine transitions
Sentry.addBreadcrumb({
category: "state",
type: "debug",
message: "State machine transitioned",
level: "debug",
data: { from: "PENDING", to: "PROCESSING", trigger: "user_submit" },
});---
Filtering Breadcrumbs — beforeBreadcrumb
Configured in Sentry.init. Return null to discard a breadcrumb entirely.
Sentry.init({
beforeBreadcrumb(breadcrumb, hint) {
// Drop clicks on password fields (privacy)
if (breadcrumb.category === "ui.click") {
const target = hint?.event?.target;
if (target?.type === "password") return null;
}
// Enrich XHR breadcrumbs with request body size
if (breadcrumb.type === "http" && hint?.xhr) {
breadcrumb.data = {
...breadcrumb.data,
requestBodySize: hint.xhr.requestBody?.length ?? 0,
};
}
// Drop verbose console.debug breadcrumbs in production
if (breadcrumb.category === "console" && breadcrumb.level === "debug") {
return null;
}
return breadcrumb;
},
});`maxBreadcrumbs` — Controls how many breadcrumbs are stored. Default: 100. Set in Sentry.init:
Sentry.init({ maxBreadcrumbs: 50 });---
Scopes
Scopes are how Sentry attaches context (tags, user, breadcrumbs, extras) to events. Three scope types are merged before each event is sent.
The Three Scope Types
| Scope | API | Lifetime | Written by |
|---|---|---|---|
| Global | Sentry.getGlobalScope() | Entire process | You (set once) |
| Isolation | Sentry.getIsolationScope() | Current page/request | Sentry.setTag() etc. |
| Current | Sentry.getCurrentScope() | Innermost execution | Sentry.withScope() |
Merge priority (later wins):
Global → Isolation → Current → Event Sent
(lowest priority) (highest priority)---
Global Scope — Sentry.getGlobalScope()
Applied to every event from anywhere in the app. Use for universal data: app version, build ID, deployment region.
const globalScope = Sentry.getGlobalScope();
globalScope.setTag("app_version", "2.4.1");
globalScope.setTag("build_id", import.meta.env.VITE_BUILD_ID);
globalScope.setContext("deployment", {
region: "us-east-1",
datacenter: "aws",
env: "production",
});Cannot capture events — only stores data.
---
Isolation Scope — Sentry.getIsolationScope()
In the browser, the isolation scope is effectively global — only one ever exists per page load (unlike Node where it's forked per request). All top-level Sentry.setXxx() methods write here.
// These two are identical in the browser:
Sentry.setTag("user_plan", "pro");
Sentry.getIsolationScope().setTag("user_plan", "pro");
// On login — persists for all subsequent events on this page:
Sentry.setUser({ id: "u_42", email: "user@example.com" });
// On logout — clears user from isolation scope:
Sentry.setUser(null);Cannot capture events — only stores data.
---
Sentry.withScope(callback) — Scoped Modifications
Creates a fork of the current scope, active only within the callback. Modifications do not leak to subsequent events. The most important tool for per-event enrichment without polluting global state.
// Add context to one specific capture only
Sentry.withScope((scope) => {
scope.setTag("operation", "bulk-delete");
scope.setLevel("warning");
scope.setContext("bulk", { count: items.length, userId: currentUser.id });
Sentry.captureException(deleteError);
});
// "operation" tag does NOT appear on any subsequent events
// Rich per-operation isolation
async function processPayment(order) {
try {
await stripe.charge(order);
} catch (err) {
Sentry.withScope((scope) => {
scope.setTag("module", "payments");
scope.setTag("payment_provider", "stripe");
scope.setLevel("fatal");
scope.setUser({ id: order.userId });
scope.setContext("order", {
id: order.id,
amount: order.amount,
currency: order.currency,
items: order.items.length,
});
scope.setExtra("stripe_error_code", err.code);
scope.addBreadcrumb({
category: "payment",
message: "Stripe charge attempt failed",
level: "error",
data: { stripeCode: err.code, message: err.message },
});
Sentry.captureException(err);
});
}
}
// addEventProcessor inside a scope — transform the event before it's sent
Sentry.withScope((scope) => {
scope.addEventProcessor((event) => {
event.tags = { ...event.tags, processed_by: "payment_handler" };
return event;
});
Sentry.captureException(err);
});---
Scope Decision Guide
| Goal | API |
|---|---|
| Data on ALL events (app version, build ID) | Sentry.getGlobalScope().setTag(...) |
| Data on current page view / user session | Sentry.setTag(...) (isolation scope) |
| Data on ONE specific capture | Sentry.withScope((scope) => { ... }) |
| Data inline on a single event | Second arg to captureException(err, { tags: {...} }) |
Do NOT use `Sentry.configureScope()` — deprecated since SDK v8. UsegetIsolationScope()orgetGlobalScope()instead.
---
Event Filtering
beforeSend(event, hint) — Modify or Drop Events
Called before every error event is sent. Return null to drop the event. Mutate event to scrub or enrich it.
Sentry.init({
beforeSend(event, hint) {
const originalError = hint.originalException;
// Drop non-Error rejections (e.g. cancelled requests)
if (originalError && !(originalError instanceof Error)) {
return null;
}
// Drop browser extension errors
if (event.exception?.values?.[0]?.stacktrace?.frames?.some(
frame => frame.filename?.includes("extension://")
)) {
return null;
}
// Drop 404 errors from event handlers
if (originalError?.message?.includes("404")) {
return null;
}
// Scrub PII from user context
if (event.user?.email) {
event.user = { ...event.user, email: "[filtered]" };
}
// Override fingerprint for known error patterns
if (originalError?.message?.includes("ChunkLoadError")) {
event.fingerprint = ["chunk-load-error"];
}
return event;
},
});Accessing the original error from `hint`:
beforeSend(event, hint) {
const error = hint.originalException; // The original Error object
const syntheticEvent = hint.syntheticException; // SDK-generated error for messages
if (error instanceof TypeError && error.message === "Failed to fetch") {
// Enrich with tag instead of dropping
event.tags = { ...event.tags, network_error: "true" };
}
return event;
}---
ignoreErrors — Pattern-Based Filtering
Array of string or RegExp patterns. Events whose error message matches any pattern are silently dropped before beforeSend.
Sentry.init({
ignoreErrors: [
// Exact strings (substring match):
"ResizeObserver loop limit exceeded",
"Non-Error exception captured",
"Object Not Found Matching Id",
// Regular expressions:
/^Network Error$/,
/ChunkLoadError/,
/Loading chunk \d+ failed/,
/^Script error\.?$/, // cross-origin script errors with no details
// Browser extension noise:
"from accessing a cross-origin frame",
/webkit-masked-url/,
],
});---
allowUrls / denyUrls — URL-Based Filtering
Only capture errors (or skip errors) from scripts at specific URLs.
Sentry.init({
// Only capture errors originating from your own scripts:
allowUrls: [
/https:\/\/yourapp\.com/,
/https:\/\/cdn\.yourapp\.com/,
],
// Skip errors from known third-party noise:
denyUrls: [
/extensions\//i,
/^chrome:\/\//i,
/^safari-extension:\/\//i,
/gtm\.js/,
/analytics\.js/,
],
});---
sampleRate — Capture Only a Fraction of Errors
Sentry.init({
sampleRate: 0.25, // Capture 25% of errors (randomly sampled)
});UsebeforeSendfor conditional filtering (based on error type, URL, user). UsesampleRatefor volume reduction when error rates are very high.
---
Fingerprinting
Default Grouping Behavior
Sentry groups errors into issues by default using a combination of: exception type, exception message, and stack trace. This works well for most cases but can produce false groupings for dynamic error messages.
Custom Fingerprinting
Override grouping by providing a fingerprint array on the event.
// All Stripe card errors grouped together regardless of message:
Sentry.captureException(err, {
fingerprint: ["stripe-card-error"],
});
// Use {{ default }} to extend (not replace) Sentry's default grouping:
Sentry.captureException(err, {
fingerprint: ["{{ default }}", "payment-module"],
});
// Dynamic component — group by component name + error type:
Sentry.captureException(err, {
fingerprint: ["DataGrid", err.constructor.name],
});Via `beforeSend` for pattern-based fingerprinting:
Sentry.init({
beforeSend(event, hint) {
const error = hint.originalException;
// Group all network timeouts as one issue:
if (error?.message?.includes("timeout")) {
event.fingerprint = ["network-timeout"];
}
// Group chunk load failures as one issue:
if (error?.name === "ChunkLoadError") {
event.fingerprint = ["chunk-load-failure"];
}
return event;
},
});---
User Feedback
When to Use Which Mechanism
feedbackIntegration() Widget | Sentry.showReportDialog() | |
|---|---|---|
| Trigger | Anytime — user-initiated | On error — automatic |
| UI | Floating button (bottom-right) | Modal overlay |
| Requires error? | No | Yes (eventId required) |
| Screenshots | Yes (SDK ≥8.0.0) | No |
| Best for | General feedback, bug reports | Post-crash reports |
---
feedbackIntegration() — Persistent Feedback Widget
Adds a floating feedback button to the page. Users submit feedback at any time — no error required.
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.feedbackIntegration({
colorScheme: "system", // "system" | "light" | "dark"
}),
],
});Complete Configuration Reference
Sentry.feedbackIntegration({
// ── Behavior ──────────────────────────────────────────────────────────
autoInject: true, // Auto-inject button into DOM. Set false for programmatic control.
colorScheme: "system", // "system" | "light" | "dark"
showBranding: true, // Show "Powered by Sentry" logo
id: "sentry-feedback", // Container div ID
tags: { // Sentry tags on all feedback submissions
product_area: "checkout",
version: "2.4.1",
},
// ── User Fields ───────────────────────────────────────────────────────
showName: true,
showEmail: true,
isNameRequired: false,
isEmailRequired: false,
enableScreenshot: true, // Allow screenshot attachment (SDK ≥8.0.0, hidden on mobile)
useSentryUser: {
email: "email", // Which Sentry user field maps to the email input
name: "username", // Which Sentry user field maps to the name input
},
// ── Labels / Text ─────────────────────────────────────────────────────
triggerLabel: "Report a Bug",
triggerAriaLabel: "Report a Bug", // v8.20.0+
formTitle: "Report a Bug",
submitButtonLabel: "Send Bug Report",
cancelButtonLabel: "Cancel",
confirmButtonLabel: "Confirm",
addScreenshotButtonLabel: "Add a screenshot",
removeScreenshotButtonLabel: "Remove screenshot",
nameLabel: "Name",
namePlaceholder: "Your Name",
emailLabel: "Email",
emailPlaceholder: "your.email@example.org",
isRequiredLabel: "(required)",
messageLabel: "Description",
messagePlaceholder: "What's the bug? What did you expect?",
successMessageText: "Thank you for your report!",
// Screenshot annotation labels (v10.10.0+):
highlightToolText: "Highlight",
hideToolText: "Hide",
removeHighlightText: "Remove",
// ── Theme Overrides ───────────────────────────────────────────────────
themeLight: {
foreground: "#2b2233",
background: "#ffffff",
accentForeground: "#ffffff",
accentBackground: "#6a3fc8",
successColor: "#268d75",
errorColor: "#df3338",
},
themeDark: {
foreground: "#ebe6ef",
background: "#29232f",
accentForeground: "#ffffff",
accentBackground: "#6a3fc8",
successColor: "#2da98c",
errorColor: "#f55459",
},
// ── Callbacks ─────────────────────────────────────────────────────────
onFormOpen: () => analytics.track("feedback_form_opened"),
onFormClose: () => analytics.track("feedback_form_closed_without_submit"),
onSubmitSuccess: (data, eventId) => {
// data: { name, email, message }
toast.success(`Thanks! Reference: ${eventId}`);
},
onSubmitError: (error) => {
toast.error("Failed to submit feedback. Please try again.");
},
})Programmatic control (when `autoInject: false`):
// In Sentry.init
const feedbackIntegration = Sentry.feedbackIntegration({ autoInject: false });
Sentry.init({ integrations: [feedbackIntegration] });
// Elsewhere — open the widget from a button:
document.getElementById("feedback-btn").addEventListener("click", () => {
feedbackIntegration.openDialog();
});
// Or attach to a DOM element (converts it to a trigger):
feedbackIntegration.attachTo(document.getElementById("help-menu-item"));---
Sentry.captureFeedback(feedback, hints?) — Programmatic Feedback API
Submit feedback without any UI. Ideal for custom feedback forms you build yourself.
// Basic
Sentry.captureFeedback({
name: "John Doe",
email: "john@example.com",
message: "The export button does nothing on Firefox.",
});
// With capture context and attachments
Sentry.captureFeedback(
{
name: "Jane Smith",
email: "jane@example.com",
message: "Chart data looks wrong after filtering by date.",
},
{
captureContext: {
tags: { page: "analytics-dashboard", browser: navigator.userAgent },
extra: { chartConfig: JSON.stringify(currentChartConfig) },
},
attachments: [
{
filename: "screenshot.png",
data: new Uint8Array(screenshotBuffer),
contentType: "image/png",
},
],
}
);---
Sentry.showReportDialog(options) — Crash-Report Modal
Shows a user-facing modal after an error. Requires an eventId to link the feedback to a Sentry event.
From `onError` in `ErrorBoundary`:
<Sentry.ErrorBoundary
onError={(error, componentStack, eventId) => {
Sentry.showReportDialog({
eventId,
user: { name: currentUser.name, email: currentUser.email },
});
}}
fallback={<ErrorScreen />}
>
<App />
</Sentry.ErrorBoundary>From `beforeSend`:
Sentry.init({
beforeSend(event, hint) {
if (event.exception && event.event_id) {
Sentry.showReportDialog({ eventId: event.event_id });
}
return event;
},
});From a manual catch:
function handleCriticalError(err) {
const eventId = Sentry.captureException(err);
Sentry.showReportDialog({
eventId,
user: { name: auth.user.displayName, email: auth.user.email },
title: "It looks like we're having issues.",
subtitle: "Our team has been notified.",
subtitle2: "If you'd like to help, tell us what happened below.",
labelComments: "Steps to reproduce:",
labelSubmit: "Send Report",
successMessage: "Your feedback has been sent. Thank you!",
});
}Complete showReportDialog Options
| Option | Type | Notes |
|---|---|---|
eventId | string | Required. Links feedback to the Sentry event |
dsn | string | Override DSN (defaults to Sentry.init DSN) |
user.name | string | Pre-fill the name field |
user.email | string | Pre-fill the email field |
lang | string | ISO language code (e.g. "de", "fr", "ja") |
title | string | Modal header text |
subtitle | string | First subtitle line |
subtitle2 | string | Second subtitle line |
labelName | string | Label for the name field |
labelEmail | string | Label for the email field |
labelComments | string | Label for the description field |
labelSubmit | string | Submit button text |
labelClose | string | Close button text |
successMessage | string | Shown after successful submission |
onLoad | () => void | Called when dialog opens |
onClose | () => void | Called when dialog closes (v7.82.0+) |
---
React Router — Critical Error Boundary Note
React Router's default error boundary silently discards errors in production. Always provide a custom errorElement that captures to Sentry:
import { useRouteError } from "react-router-dom";
import * as Sentry from "@sentry/react";
function RootErrorBoundary() {
const error = useRouteError();
React.useEffect(() => {
if (error instanceof Error) {
Sentry.captureException(error, {
tags: { source: "react-router-error-element" },
});
}
}, [error]);
return (
<div>
<h1>Something went wrong</h1>
<p>{error instanceof Error ? error.message : "An unexpected error occurred."}</p>
<button onClick={() => window.location.reload()}>Reload page</button>
</div>
);
}
const router = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter)([
{
path: "/",
element: <Layout />,
errorElement: <RootErrorBoundary />, // ← required
children: [ /* your routes */ ],
},
]);---
Quick Reference
// ── Capture APIs ──────────────────────────────────────────────────────
Sentry.captureException(error)
Sentry.captureException(error, { level, tags, extra, contexts, fingerprint, user })
Sentry.captureMessage("text", "warning")
Sentry.captureMessage("text", { level, tags, extra })
Sentry.captureEvent({ message, level, tags, extra, timestamp })
Sentry.captureReactException(error, reactErrorInfo) // ≥9.8.0 — custom class boundaries
// ── React 19+ Error Hooks ─────────────────────────────────────────────
createRoot(el, {
onUncaughtError: Sentry.reactErrorHandler(optionalCallback),
onCaughtError: Sentry.reactErrorHandler(),
onRecoverableError: Sentry.reactErrorHandler(),
})
hydrateRoot(el, <App />, { /* same three hooks */ })
// ── Error Boundaries (React 16+) ──────────────────────────────────────
<Sentry.ErrorBoundary
fallback={<UI /> | ({ error, componentStack, resetError }) => <UI />}
onError={(error, stack, eventId) => {}}
beforeCapture={(scope, error, stack) => {}}
onReset={(error, stack, eventId) => {}}
showDialog dialogOptions={{}}
onMount={() => {}} onUnmount={(error) => {}}
>
Sentry.withErrorBoundary(Component, options) // HOC equivalent
// ── Context ───────────────────────────────────────────────────────────
Sentry.setUser({ id, email, username, ip_address, segment, ...custom })
Sentry.setUser(null) // clear on logout
Sentry.setTag("key", "value")
Sentry.setTags({ key1: "v1", key2: "v2" })
Sentry.setContext("name", { key: value }) // structured, not searchable
Sentry.setContext("name", null) // remove context
Sentry.setExtra("key", value)
Sentry.setExtras({ key1: v1 })
// ── Breadcrumbs ───────────────────────────────────────────────────────
Sentry.addBreadcrumb({ type, category, message, level, data, timestamp })
// ── Scopes ────────────────────────────────────────────────────────────
Sentry.withScope((scope) => { scope.setTag(...); Sentry.captureException(...) })
Sentry.getGlobalScope() // all events, process lifetime
Sentry.getIsolationScope() // current page/session (= Sentry.setTag etc.)
// DON'T: Sentry.configureScope() — deprecated since SDK v8
// ── Filtering ─────────────────────────────────────────────────────────
// Sentry.init({ beforeSend, ignoreErrors, allowUrls, denyUrls, sampleRate })
// ── User Feedback ─────────────────────────────────────────────────────
Sentry.feedbackIntegration({ colorScheme, autoInject, showName, isEmailRequired, ... })
Sentry.captureFeedback({ name, email, message }, { captureContext, attachments })
Sentry.showReportDialog({ eventId, user, title, subtitle, ... })---
Troubleshooting
| Issue | Solution |
|---|---|
| Errors appearing twice in development | Expected behavior — React Strict Mode re-throws caught errors to the global handler. Validate in production builds only. |
| Missing component stack in issues | Requires React 17+. Ensure LinkedErrors integration is enabled (it is by default). |
| React Router errors not captured | React Router's default boundary swallows errors. Add a custom errorElement that calls captureException. |
CaptureConsole causing duplicates | React logs caught errors via console.error. Remove CaptureConsole or exclude console.error from its config. |
captureReactException not available | Upgrade to @sentry/react ≥9.8.0. |
reactErrorHandler not available | Upgrade to @sentry/react ≥8.6.0. |
| Errors captured without user context | Call Sentry.setUser() after login, not inside Sentry.init. It must be called after authentication completes. |
configureScope is not a function | Deprecated in SDK v8. Replace with getIsolationScope() or withScope(). |
| Tags not appearing on events | Tags set via Sentry.setTag() go to the isolation scope; verify you're not clearing it unexpectedly. |
showReportDialog shows but has no event | Pass eventId from Sentry.captureException(err) return value or from onError prop. |
feedbackIntegration button not appearing | Confirm feedbackIntegration() is in the integrations array in Sentry.init. Check for z-index conflicts. |
beforeSend returning null but events still sent | Check beforeSendTransaction — a separate hook for performance events. Also verify no other SDK instance is active. |
| High event volume from known errors | Add patterns to ignoreErrors, or use sampleRate to reduce volume. Use beforeSend for type-specific filtering. |
| Errors from browser extensions captured | Add /extensions\//i and /^chrome:\/\//i to denyUrls. |
Logging — Sentry React SDK
Minimum SDK:@sentry/react≥9.41.0+ forSentry.loggerAPI andenableLogs
consoleLoggingIntegration(): 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 is opt-in and must be explicitly set in Sentry.init():
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
enableLogs: true, // Required — logging is disabled by default
});Without enableLogs: true, all Sentry.logger.* calls are silently no-ops and nothing is sent to Sentry.
---
Logger API — Six Levels
import * as Sentry from "@sentry/react";
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" });| Level | Method | Typical Use |
|---|---|---|
trace | Sentry.logger.trace() | Ultra-granular function entry/exit; high-volume — filter aggressively in production |
debug | Sentry.logger.debug() | Development diagnostics, cache hits/misses, local state changes |
info | Sentry.logger.info() | Normal business milestones, confirmations |
warn | Sentry.logger.warn() | Degraded state, approaching limits, recoverable issues |
error | Sentry.logger.error() | Failures requiring attention |
fatal | Sentry.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.99Each parameter is independently searchable in Sentry's log explorer. You can filter by message.parameter.0 = "user_123" without matching the full message string.
⚠️logger.fmtmust be used as a tagged template literal — not as a function call.Sentry.logger.fmt("text")will not produce structured parameters.
When to use fmt vs plain attributes
| Approach | Use 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. Requires SDK ≥10.13.0.
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
enableLogs: true,
integrations: [
Sentry.consoleLoggingIntegration({
levels: ["log", "warn", "error"], // which console methods to forward
}),
],
});
// These calls are now automatically sent to Sentry:
console.log("User action recorded", { userId: 123 });
console.warn("Slow render detected", 240, "ms");
console.error("Fetch failed", new Error("timeout"));Multiple arguments are mapped to positional parameters:
console.log("Text", 123, true)
→ message.parameter.0 = 123
→ message.parameter.1 = trueCapturable console levels
| Console method | Sentry log level |
|---|---|
console.log | info |
console.info | info |
console.warn | warn |
console.error | error |
console.debug | debug |
console.assert (failing) | error |
Configure levels to include only the methods you want forwarded.
---
Log Filtering — beforeSendLog
Use beforeSendLog to drop, modify, or scrub logs before they leave the client. Return null to discard:
Sentry.init({
dsn: import.meta.env.VITE_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
| Field | Type | Description |
|---|---|---|
level | string | "trace" \ |
message | string | The log message text |
timestamp | number | Unix timestamp |
attributes | object | Key/value pairs attached to this log |
---
Structured Attributes
Every Sentry.logger.* call accepts an attributes object as its second argument:
Sentry.logger.info("Checkout completed", {
orderId: "ord_789",
userId: "usr_123",
cartValue: 149.99,
itemCount: 3,
paymentMethod: "stripe",
userTier: "premium",
duration: Date.now() - startTime,
});Attributes become searchable and filterable in Sentry's log explorer. Prefer one comprehensive log with all relevant context over many small scattered logs ("wide events").
---
Scope-Based Automatic Attributes (SDK ≥10.32.0)
Attributes set on scopes are automatically added to all logs emitted within that scope.
Global scope — entire session
// Set once at app startup — persists for the lifetime of the page
Sentry.getGlobalScope().setAttributes({
service: "react-checkout",
version: "2.1.0",
region: "us-east-1",
});Isolation scope — logical user session context
// Set after user authenticates
Sentry.getIsolationScope().setAttributes({
org_id: user.orgId,
user_tier: user.tier,
account_type: user.accountType,
});Current scope — single operation
Sentry.withScope((scope) => {
scope.setAttribute("order_id", "ord_789");
scope.setAttribute("payment_method", "stripe");
Sentry.logger.info("Processing payment", { amount: 49.99 });
// order_id and payment_method are included on this log only
});Constraint: Scope attributes accept only string, number, and boolean values — no arrays or objects.
---
Auto-Generated Attributes
These are added by the SDK to every log without any developer configuration:
| Attribute | Source | Notes |
|---|---|---|
sentry.environment | environment in Sentry.init() | — |
sentry.release | release in Sentry.init() | — |
sentry.sdk.name | SDK internals | e.g., "sentry.javascript.react" |
sentry.sdk.version | SDK internals | — |
browser.name | User-Agent parsing | e.g., "Chrome" |
browser.version | User-Agent parsing | e.g., "121.0.0" |
user.id, user.name, user.email | Sentry.setUser() | Requires sendDefaultPii: true |
sentry.trace.parent_span_id | Active tracing span | Enables log ↔ trace correlation |
sentry.replay_id | Active Session Replay session | Enables log ↔ replay correlation |
message.template | logger.fmt usage | The template string |
message.parameter.N | logger.fmt usage | Each interpolated value |
---
Log-to-Trace Correlation
When tracing is enabled alongside logging, logs are automatically linked to the active span:
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
enableLogs: true,
tracesSampleRate: 1.0,
integrations: [Sentry.browserTracingIntegration()],
});
// Logs emitted inside a span are linked to it automatically
await Sentry.startSpan({ name: "checkout-flow", op: "ui.action" }, async () => {
Sentry.logger.info("Validating cart", { cartId: "cart_abc" });
await validateCart();
Sentry.logger.info("Initiating payment", { gateway: "stripe" });
await initiatePayment();
});
// Both logs above have sentry.trace.parent_span_id set to the checkout-flow span IDIn the Sentry UI:
- From a log → click the trace link to jump to the parent span and full trace
- From a trace span → click "Logs" to see all logs emitted during that span
- From a replay → logs are shown inline with the user session recording
---
React-Specific 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,
activeFeatureFlags: user.flags.join(","),
durationMs: Date.now() - startTime,
});
// ❌ Avoid — fragmented logs with poor context
Sentry.logger.info("Order ID set", { orderId: order.id });
Sentry.logger.info("Cart total calculated", { cartValue: cart.total });
Sentry.logger.info("Checkout done");---
When to Use Each API
| Scenario | Recommended API |
|---|---|
| Business event with structured data | Sentry.logger.info(msg, { ...attrs }) |
| Message with embedded variables | Sentry.logger.info(Sentry.logger.fmt\...\) |
| Capture an unexpected exception | Sentry.captureException(err) |
| Send an informational string event | Sentry.captureMessage(msg, "info") |
Auto-capture existing console.* calls | consoleLoggingIntegration({ levels: [...] }) |
Use Sentry.logger.* for structured, searchable observability data. Use captureException for actual errors that need issue grouping and stack traces.
---
Log Level Guide
| Level | When to use | Production volume |
|---|---|---|
trace | Function entry/exit, loop iterations | Filter out in production |
debug | Variable values, code paths taken | Filter out in production |
info | User actions, business milestones, API calls | Keep — low/medium volume |
warn | Degraded paths, retries, near-limits | Keep — low volume |
error | Failures that need investigation | Keep — should be rare |
fatal | System-down, unrecoverable state | Keep — should be very rare |
---
Troubleshooting
| Issue | Solution |
|---|---|
| Logs not appearing in Sentry | Verify enableLogs: true is in Sentry.init(); requires SDK ≥9.41.0 |
logger.fmt not creating message.parameter.* | Use as tagged template: Sentry.logger.fmt\text ${var}\` — not Sentry.logger.fmt("text", var)` |
| Logs not linked to traces | Ensure browserTracingIntegration() is added and tracesSampleRate > 0; logs must be emitted inside an active span |
consoleLoggingIntegration not available | Upgrade to @sentry/react ≥10.13.0 |
| Scope attributes not appearing on logs | Upgrade to @sentry/react ≥10.32.0 for getGlobalScope/getIsolationScope APIs |
| Too many logs — high volume / costs | Use beforeSendLog to drop trace and debug levels in production |
Log attributes contain undefined | Only string, number, boolean are accepted — filter undefined values before passing |
beforeSendLog not firing | Confirm enableLogs: true is set; without it, no logs are sent and no hook is called |
| Sensitive data appearing in logs | Add filtering in beforeSendLog; better yet, avoid logging sensitive data at the call site |
| Logs appear but have no user context | Call Sentry.setUser({ id, email }) after authentication and set sendDefaultPii: true |
Browser Profiling — Sentry React SDK
Minimum SDK: @sentry/react ≥10.27.0+ (Beta)⚠️ Beta status — breaking changes may occur. Browser support is limited to Chromium-based browsers only.
---
What Browser Profiling Captures
Sentry's browser profiler uses the JS Self-Profiling API to capture:
- JavaScript call stacks — function names and source file locations (deobfuscated via source maps)
- CPU time per function — how much time is spent in each function
- Flame graphs — aggregated across real user sessions, not just local dev
- Linked profiles — every profile is attached to a trace, enabling navigation from span → flame graph in the Sentry UI
Sampling rate: 100Hz (10ms intervals) — contrast with Chrome DevTools at 1000Hz (1ms). Less granular, but runs unobtrusively in production.
---
Browser Compatibility
| Browser | Supported | Notes |
|---|---|---|
| Chrome / Chromium | ✅ Yes | Primary support target |
| Edge (Chromium) | ✅ Yes | Same engine as Chrome |
| Firefox | ❌ No | Does not implement JS Self-Profiling API |
| Safari / iOS Safari | ❌ No | Does not implement JS Self-Profiling API |
⚠️ Sampling bias: Profile data is collected only from Chromium users. Firefox and Safari sessions are silently not profiled. Consider this when drawing performance conclusions.
In unsupported browsers, browserProfilingIntegration() silently no-ops — no errors thrown, no overhead.
---
Required HTTP Header
Every document response serving your React app must include this header or profiling silently fails:
Document-Policy: js-profilingWithout this header, the JS Self-Profiling API is blocked by the browser and no profiles are collected.
Platform-Specific Header Setup
Vercel (`vercel.json`):
{
"headers": [
{
"source": "/(.*)",
"headers": [{ "key": "Document-Policy", "value": "js-profiling" }]
}
]
}Netlify (`netlify.toml`):
[[headers]]
for = "/*"
[headers.values]
Document-Policy = "js-profiling"Netlify (`_headers` file):
/*
Document-Policy: js-profilingExpress / Node.js:
app.use((req, res, next) => {
res.set("Document-Policy", "js-profiling");
next();
});Nginx (`nginx.conf`):
server {
location / {
add_header Document-Policy "js-profiling";
}
}Apache (`.htaccess`):
<IfModule mod_headers.c>
Header set Document-Policy "js-profiling"
</IfModule>AWS CloudFront (Viewer Response function):
function handler(event) {
var response = event.response;
response.headers["document-policy"] = { value: "js-profiling" };
return response;
}ASP.NET Core (`Program.cs`):
app.Use(async (context, next) => {
context.Response.OnStarting(() => {
context.Response.Headers.Append("Document-Policy", "js-profiling");
return Task.CompletedTask;
});
await next();
});⚠️ Static hosting that disallows custom headers (some CDNs, GitHub Pages) will prevent profiling entirely.
---
Setup
Install
npm install @sentry/react --saveSDK Initialization — Trace Mode (recommended)
Trace mode automatically attaches profiles to all sampled spans. Use this for general production coverage:
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration(), // Must come BEFORE browserProfilingIntegration
Sentry.browserProfilingIntegration(),
],
// Tracing — profiles are only collected when a transaction is also sampled
tracesSampleRate: 1.0,
// Profiling — fraction of sessions to profile
profileSessionSampleRate: 1.0,
// "trace" = automatically attach profiles to all active spans
profileLifecycle: "trace",
});SDK Initialization — Manual Mode
Manual mode lets you profile specific user flows or code paths explicitly:
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.browserProfilingIntegration(),
],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0,
// Omit profileLifecycle for manual mode (default)
});
// Later, wrap specific operations:
Sentry.uiProfiler.startProfiler();
// ... user flow or expensive computation ...
Sentry.uiProfiler.stopProfiler();---
Configuration Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
tracesSampleRate | number | — | 0.0–1.0 — fraction of transactions traced; profiles only attach to traced transactions |
profileSessionSampleRate | number | — | 0.0–1.0 — session-level sampling decision for profiling |
profileLifecycle | "trace" | — | Set to "trace" for Trace mode; omit for Manual mode |
tracePropagationTargets | `(string\ | RegExp)[]` | — |
---
How Profiles Attach to Traces
Profiles are not independent from tracing — they attach to transactions:
1. tracesSampleRate determines whether a transaction is traced at all 2. profileSessionSampleRate determines whether the session opts into profiling 3. A profile is only collected when both sampling decisions are yes
Compound sampling example:
tracesSampleRate: 0.5+profileSessionSampleRate: 0.5→ ~25% of sessions produce profilestracesSampleRate: 1.0+profileSessionSampleRate: 1.0→ 100% (development/testing only)
In the Sentry UI, open a trace and click "Profile" to view the flame graph for that transaction.
---
Profiling Modes Comparison
| Mode | How to trigger | Best for |
|---|---|---|
Trace (profileLifecycle: "trace") | Auto-attached to every sampled span | Broad production coverage |
| Manual (default) | uiProfiler.startProfiler() / stopProfiler() | Specific high-value flows (checkout, render) |
---
Sentry Profiling vs Chrome DevTools
| Aspect | Sentry Browser Profiling | Chrome DevTools |
|---|---|---|
| Environment | Production (real users) | Local development only |
| Sampling rate | 100Hz (10ms) | 1000Hz (1ms) |
| Stack traces | Deobfuscated via source maps | Minified names unless local |
| Data scope | Aggregated across all sessions | Single local session |
| Browser coverage | Chromium only | Any browser with DevTools |
| Overhead | Low (production-safe) | Higher — not production-safe |
⚠️ Chrome DevTools conflict: When browserProfilingIntegration is active, Chrome DevTools profiles incorrectly show profiling overhead mixed into rendering work. Disable the integration when doing local DevTools profiling sessions.---
Source Maps — Critical for Useful Profiles
Without source maps, flame graphs show minified function names like e, t, r — effectively unreadable.
With source maps uploaded to Sentry, flame graphs show original function names and file paths from your source code.
Setup source maps with the Vite plugin (handles both source map upload and component annotation):
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
build: {
sourcemap: "hidden", // Generate maps but don't serve them publicly
},
plugins: [
react(),
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
filesToDeleteAfterUpload: ["./**/*.map"],
},
}),
],
});See the main SKILL.md Source Maps Setup section for Webpack/CRA configuration.
---
Limitations
| Limitation | Detail |
|---|---|
| Beta status | API is experimental; breaking changes possible between releases |
| Chromium only | No Firefox, no Safari, no iOS — data is biased |
| Requires header | Document-Policy: js-profiling must be served; some hosts don't allow custom headers |
| Compound sampling | Profiles only captured when transaction is also sampled |
| 10ms granularity | Very short functions (<10ms) may not appear in profiles |
| Chrome DevTools conflict | Must disable integration when doing local DevTools profiling |
| Not on CDN | browserProfilingIntegration is not available via the CDN loader bundle |
---
Troubleshooting
| Issue | Solution |
|---|---|
| No profiles appearing in Sentry | Verify Document-Policy: js-profiling header is present on document responses |
| Profiles exist but show minified names | Source maps not uploaded — configure sentryVitePlugin or sentryWebpackPlugin |
| Profiling data only from some users | Expected — only Chromium users are profiled; Firefox/Safari silently no-op |
| Chrome DevTools shows inflated rendering times | Disable browserProfilingIntegration during local DevTools sessions |
profileSessionSampleRate has no effect | Ensure browserProfilingIntegration() is listed after browserTracingIntegration() in the integrations array |
| Profiling on static host not working | Verify your host supports custom response headers; GitHub Pages and some CDNs do not |
| Profiles not linked to spans in Trace mode | Confirm profileLifecycle: "trace" is set and tracesSampleRate > 0 |
React-Specific Features — Sentry React SDK
Minimum SDK: @sentry/react v8.0.0+This is the catch-all deep dive for React-specific Sentry features: Redux integration, component tracking, source maps, and the full integrations catalog.
---
Table of Contents
1. Redux Integration 2. Component Tracking & Performance 3. Source Maps 4. Default Integrations 5. Optional Integrations 6. Build Tool Detection & Environment Variables
---
1. Redux Integration
Sentry.createReduxEnhancer() hooks into your Redux store to automatically:
- Capture Redux actions as breadcrumbs on every Sentry error event
- Attach the Redux state as a `redux_state.json` file to error events
- Keep Sentry scope tags in sync with your Redux state
Setup — Redux Toolkit (configureStore)
import * as Sentry from "@sentry/react";
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers";
const sentryReduxEnhancer = Sentry.createReduxEnhancer({
// options — see below
});
const store = configureStore({
reducer: rootReducer,
enhancers: (getDefaultEnhancers) => {
return getDefaultEnhancers().concat(sentryReduxEnhancer);
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;⚠️ Critical:sentryReduxEnhanceris a store enhancer, not Redux middleware. Do NOT pass it insideapplyMiddleware().
Setup — Legacy createStore with Middleware
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import * as Sentry from "@sentry/react";
const sentryReduxEnhancer = Sentry.createReduxEnhancer();
const store = createStore(
rootReducer,
compose(applyMiddleware(thunk), sentryReduxEnhancer),
);---
Options: actionTransformer — Filter/Scrub Actions Before Breadcrumbs
Called for every dispatched action. Return the action to include it as a breadcrumb, a modified copy to scrub sensitive fields, or null to drop it entirely.
const sentryReduxEnhancer = Sentry.createReduxEnhancer({
actionTransformer: (action) => {
// Drop high-volume or sensitive actions entirely
if (action.type === "WEBSOCKET_PING") return null;
if (action.type === "FETCH_SECRETS") return null;
// Scrub sensitive fields from certain actions
if (action.type === "USER_LOGIN") {
return {
...action,
password: "[REDACTED]",
token: "[REDACTED]",
};
}
if (action.type === "UPDATE_PAYMENT") {
return {
...action,
payload: {
...action.payload,
cardNumber: null,
cvv: null,
},
};
}
// Include all other actions as-is
return action;
},
});---
Options: stateTransformer — Filter/Scrub State Snapshots
Called on every state update. Return the state to attach it to error events, a modified copy with sensitive fields redacted, or null to exclude state entirely.
const sentryReduxEnhancer = Sentry.createReduxEnhancer({
stateTransformer: (state: RootState) => {
// Return null to send NO state with errors (reduces context but protects PII)
// if (state.topSecret.active) return null;
// Return a scrubbed copy
return {
...state,
auth: {
...state.auth,
token: null, // remove auth token
refreshToken: null,
password: null,
},
user: {
...state.user,
ssn: "[REDACTED]",
creditCard: null,
dateOfBirth: null,
},
// Remove entire subtrees you don't need
cache: null,
rawApiResponses: null,
};
},
});⚠️ Warning: IfstateTransformerreturnsnull, error events will lack Redux state context. Debugging large state-dependent bugs becomes much harder. Prefer returning a filtered copy over returningnull.
---
Options: configureScopeWithState — Derive Sentry Tags from Redux State
Called after every state update. Use it to keep Sentry scope tags/context in sync with your application's Redux state — these tags then appear on every error captured after the update.
const sentryReduxEnhancer = Sentry.createReduxEnhancer({
configureScopeWithState: (scope: Sentry.Scope, state: RootState) => {
// Tag events with current user plan (great for filtering errors by customer tier)
scope.setTag("user.plan", state.user.plan);
scope.setTag("user.id", state.user.id);
// Tag with feature flag state
scope.setTag("feature.newCheckout", String(state.features.newCheckout));
scope.setTag("feature.darkMode", String(state.settings.darkMode));
// Tag with routing/navigation state
scope.setTag("app.currentFlow", state.navigation.currentFlow);
// Conditional tags based on state shape
if (state.settings.useImperialUnits) {
scope.setTag("user.usesImperialUnits", "true");
}
// Set structured context (not searchable but visible in issue detail)
scope.setContext("cart", {
itemCount: state.cart.items.length,
total: state.cart.total,
coupon: state.cart.couponCode ?? null,
});
},
});---
Options: attachReduxState — Control State File Attachment
Controls whether the Redux state (post-stateTransformer) is attached as a redux_state.json file on error events.
- Type:
boolean - Default:
true - Min SDK:
7.69.0
const sentryReduxEnhancer = Sentry.createReduxEnhancer({
attachReduxState: false, // Don't attach state file — reduces payload size
});---
Options: normalizeDepth — Control State Serialization Depth
Set in Sentry.init(), not in createReduxEnhancer(). Increases the depth at which Redux state trees are serialized. The default of 3 is too shallow for most Redux state shapes.
Sentry.init({
dsn: "___PUBLIC_DSN___",
normalizeDepth: 10, // Default is 3 — increase for deeply nested Redux state
});---
All Options Summary
| Option | Type | Default | Location | Description |
|---|---|---|---|---|
actionTransformer | `(action: Action) => Action \ | null` | — | createReduxEnhancer() |
stateTransformer | `(state: State) => State \ | null` | — | createReduxEnhancer() |
configureScopeWithState | (scope: Scope, state: State) => void | — | createReduxEnhancer() | Sync Sentry scope tags/context with Redux state |
attachReduxState | boolean | true | createReduxEnhancer() | Attach state as redux_state.json file on errors |
normalizeDepth | number | 3 | Sentry.init() | Max depth when serializing nested state |
---
Complete Working Example with @reduxjs/toolkit
// store/sentry.ts
import * as Sentry from "@sentry/react";
import type { RootState } from "./types";
export const sentryReduxEnhancer = Sentry.createReduxEnhancer({
actionTransformer: (action) => {
// Drop noisy/sensitive action types
const DROP_TYPES = new Set([
"SET_AUTH_TOKEN",
"REFRESH_TOKEN",
"WEBSOCKET_HEARTBEAT",
"UPDATE_CURSOR_POSITION",
]);
if (DROP_TYPES.has(action.type)) return null;
// Scrub passwords from login actions
if (action.type === "auth/login/pending") {
return { ...action, meta: { ...action.meta, arg: { email: action.meta?.arg?.email, password: "[REDACTED]" } } };
}
return action;
},
stateTransformer: (state: RootState) => ({
...state,
auth: { isAuthenticated: state.auth.isAuthenticated, userId: state.auth.userId },
// Strip large or sensitive subtrees
rawData: null,
}),
configureScopeWithState: (scope, state: RootState) => {
scope.setTag("user.plan", state.user.plan ?? "unknown");
scope.setTag("user.id", state.auth.userId ?? "anonymous");
scope.setTag("org.id", state.org.id ?? "none");
},
});
// store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import { sentryReduxEnhancer } from "./sentry";
import rootReducer from "./reducers";
export const store = configureStore({
reducer: rootReducer,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(sentryReduxEnhancer),
});// instrument.ts — init with normalizeDepth
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
normalizeDepth: 10, // Required for deeply nested Redux state
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1.0,
});Performance Considerations
Redux state can be very large. Keep in mind:
1. State size: Large state trees (thousands of items) will slow serialization and increase payload size. Use stateTransformer to return only the relevant slices. 2. `normalizeDepth`: Keep it as low as practical. 10 is usually sufficient for deeply nested state; avoid setting it to Infinity. 3. `attachReduxState: false`: For high-traffic production apps where payload size is a concern, disabling state attachment reduces each error event's size. 4. `configureScopeWithState` cost: This runs on every Redux dispatch. Keep the function fast — avoid heavy computations or deep object traversals.
---
2. Component Tracking & Performance
A. React Component Name Annotation (Build-Time)
Replaces opaque CSS selectors in breadcrumbs, Session Replay, and performance spans with readable React component names.
Before: button.en302zp1.app-191aavw.e16hd6vm2[role="button"] After: CheckoutButton
Requirements:
- SDK v7.91.0+
- Components must be in
.jsxor.tsxfiles (.jsand.tsare not annotated) - esbuild is not supported
The bundler plugins inject data-sentry-component and data-sentry-source-file attributes at build time:
<!-- Resulting DOM -->
<button
data-sentry-component="CheckoutButton"
data-sentry-source-file="CheckoutButton.tsx"
>
Checkout
</button>Enable via Vite (recommended):
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
plugins: [
react(),
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
reactComponentAnnotation: {
enabled: true,
// Exclude components that cause "Passing unknown props on Fragment" errors
ignoredComponents: ["AnimationWrapper", "LayoutFragment"],
},
}),
],
build: {
sourcemap: "hidden",
},
});Enable via Babel plugin directly (without bundler plugin):
npm install @sentry/babel-plugin-component-annotate --save-dev// babel.config.js
module.exports = {
plugins: ["@sentry/babel-plugin-component-annotate"],
};Bundler support:
| Bundler | Component Annotation |
|---|---|
| Vite | ✅ Supported |
| Webpack | ✅ Supported |
| Rollup | ✅ Supported |
| esbuild | ❌ Not supported |
What you gain:
| Where | Before | After |
|---|---|---|
| Breadcrumbs | div.sc-abc123 | ProductCard |
| Session Replay | Unreadable selector | Search by ProductCard |
| Performance spans | Generic element | CheckoutButton render |
---
B. Sentry.withProfiler() — React Profiler HOC
withProfiler wraps a component with the React Profiler API to capture render timing as Sentry performance spans.
import * as Sentry from "@sentry/react";
// Basic — display name inferred from component.displayName or .name
const ProfiledDashboard = Sentry.withProfiler(Dashboard);
// With explicit name (required for anonymous or arrow-function components)
const ProfiledWidget = Sentry.withProfiler(
({ data }) => <div>{data.title}</div>,
{ name: "DataWidget" }
);
// Class component decorator syntax
@Sentry.withProfiler
class ExpensiveList extends React.Component {
render() {
return <ul>{this.props.items.map(renderItem)}</ul>;
}
}Options:
| Option | Type | Default | Description |
|---|---|---|---|
name | string | Component displayName or name | Display name shown in Sentry traces |
includeRender | boolean | true | Track initial render phase |
includeUpdates | boolean | true | Track re-render / update phases |
What it captures:
- Mount time (initial render duration)
- Update/re-render time per update
- Number of re-renders
- Component name in the Sentry performance trace waterfall
Data appears in: Sentry → Performance → Trace View, as child spans of the current transaction.
Requires tracing:browserTracingIntegrationmust be in yourSentry.initintegrations.
When to use (and when not to):
✅ Use on:
- Root-level route components (Dashboard, CheckoutFlow, UserProfile)
- Components with expensive render logic (large lists, complex calculations)
- Components that re-render frequently and may cause jank
❌ Do not use on:
- Every component in the tree — the overhead compounds
- Simple presentational/leaf components
- Components that render hundreds of times per second (e.g., animation frames)
Performance overhead: Each profiled component adds a small constant overhead per render cycle. Profile the 5–10 most performance-critical components, not the entire tree.
---
3. Source Maps
Source maps translate minified production stack traces back to your original source code. Without them, stack traces show obfuscated variable names and collapsed line numbers.
Recommended: Sentry Wizard
The fastest path — automatically detects your bundler, installs the plugin, and configures auth:
You need to run this yourself — the wizard opens a browser for login and requires interactive input that the agent can't handle. Copy-paste into your terminal:
>
```
npx @sentry/wizard@latest -i sourcemaps
```
>
It detects Vite/webpack/CRA, installs the appropriate Sentry bundler plugin, addsSENTRY_AUTH_TOKENto.env.sentry-build-plugin, and configuressourcemap: "hidden"withfilesToDeleteAfterUpload.
>
Once it finishes, come back and the agent will continue.
If the user skips the wizard, proceed with manual bundler plugin setup below.
---
How Debug IDs Work
Modern Sentry source map matching uses Debug IDs — unique identifiers injected by the bundler plugin into both the compiled .js bundle and the corresponding .js.map file. This makes source map matching reliable without needing to manage file names or release artifacts manually.
Flow: 1. Production build runs → bundler plugin injects a Debug ID into each .js and .js.map file 2. Plugin uploads .js.map files to Sentry with their Debug IDs 3. Error occurs in production → stack frame contains the Debug ID 4. Sentry uses the Debug ID to locate the exact source map → deobfuscates the trace
Debug IDs are more reliable than release-based matching (older approach) because they don't depend on consistent release naming or artifact upload timing.
---
Vite Plugin
Minimum SDK: @sentry/vite-plugin 2.0.0 / @sentry/react 7.47.0
npm install @sentry/vite-plugin --save-dev// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
build: {
// "hidden" generates source maps but strips the `//# sourceMappingURL=` comment
// from bundles, so browsers won't load them — they're only for Sentry.
sourcemap: "hidden",
},
plugins: [
react(),
// sentryVitePlugin MUST come after all other plugins
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
// Enable React component name annotation at the same time
reactComponentAnnotation: {
enabled: true,
},
sourcemaps: {
// Delete .map files from dist/ after uploading to Sentry
// so they're not deployed to your CDN/server
filesToDeleteAfterUpload: [
"./**/*.map",
".*/**/public/**/*.map",
"./dist/**/client/**/*.map",
],
},
}),
],
});⚠️ PlacesentryVitePluginafter all other plugins (especially@vitejs/plugin-react) to ensure correct source map generation.
---
Webpack Plugin
Minimum SDK: @sentry/webpack-plugin 2.0.0 / @sentry/react 7.47.0
npm install @sentry/webpack-plugin --save-dev// webpack.config.js
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
module.exports = {
// "hidden-source-map" generates source maps without the `//# sourceMappingURL=`
// reference comment, so they won't be served publicly.
devtool: "hidden-source-map",
plugins: [
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
filesToDeleteAfterUpload: [
"./**/*.map",
"./build/static/**/*.map",
],
},
}),
],
};---
Create React App
CRA has limited configuration access. Two approaches:
Option A: CRACO (recommended — no ejection)
npm install @craco/craco @sentry/webpack-plugin --save-dev// craco.config.js
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
module.exports = {
webpack: {
configure: (webpackConfig) => {
webpackConfig.devtool = "hidden-source-map";
webpackConfig.plugins.push(
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
filesToDeleteAfterUpload: ["./build/static/**/*.map"],
},
})
);
return webpackConfig;
},
},
};Update package.json scripts:
{
"scripts": {
"start": "craco start",
"build": "craco build"
}
}Option B: Eject (`npm run eject`)
After ejecting, edit config/webpack.config.js directly — same as the regular webpack setup above. Ejection is irreversible; prefer CRACO.
---
Manual Upload with sentry-cli
Use when you can't use a bundler plugin (e.g., legacy toolchain, pre-built artifacts):
npm install @sentry/cli --save-dev# Upload source maps after a production build
npx sentry-cli sourcemaps upload \
--org $SENTRY_ORG \
--project $SENTRY_PROJECT \
--auth-token $SENTRY_AUTH_TOKEN \
./build`.sentryclirc` configuration file (alternative to env vars):
[defaults]
org = my-org-slug
project = my-project-slug
url = https://sentry.io/# With .sentryclirc, no --org/--project flags needed:
npx sentry-cli sourcemaps upload --auth-token $SENTRY_AUTH_TOKEN ./build⚠️ Never commit SENTRY_AUTH_TOKEN to source control. Always read it from environment variables.CI/CD pattern (GitHub Actions):
- name: Build
run: npm run build
- name: Upload source maps to Sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: my-org-slug
SENTRY_PROJECT: my-react-app
run: |
npx sentry-cli sourcemaps upload \
--org $SENTRY_ORG \
--project $SENTRY_PROJECT \
--auth-token $SENTRY_AUTH_TOKEN \
./build
- name: Delete source maps from build artifacts
run: find ./build -name "*.map" -delete
- name: Deploy
run: # your deploy step---
Environment Variables for Source Maps
Store credentials in .env.sentry-build-plugin (auto-loaded by Sentry bundler plugins) or pass as CI/CD secrets:
# .env.sentry-build-plugin ← auto-loaded by @sentry/vite-plugin and @sentry/webpack-plugin
# ADD THIS FILE TO .gitignore — never commit it
SENTRY_AUTH_TOKEN=sntrys_eyJ...
SENTRY_ORG=my-org-slug
SENTRY_PROJECT=my-project-slugRequired token permissions:
The SENTRY_AUTH_TOKEN must have:
- Project: Read & Write
- Release: Admin
---
Source Map Security
| Strategy | How |
|---|---|
| Don't expose maps to browsers | Use sourcemap: "hidden" (Vite) or devtool: "hidden-source-map" (webpack) |
| Delete maps after upload | Use filesToDeleteAfterUpload in the plugin config |
| Block map access at CDN/server | Configure your server to return 403 for .js.map requests |
All three should be used together for maximum security.
---
Troubleshooting Source Maps
| Problem | Likely Cause | Fix |
|---|---|---|
| Stack traces still minified | Source maps not uploaded, or Debug IDs missing | Rebuild with production config, re-run wizard or plugin |
| Maps not applied to old errors | Maps uploaded after errors occurred | Always upload maps before deploying — ideally in the same CI step |
| "SourceMapDevToolPlugin" stripping sources | noSources: true in your webpack SourceMapDevToolPlugin | Remove noSources: true option |
| Plugin only uploads once | Running in --watch or dev mode | Plugin only uploads during production builds (NODE_ENV=production) |
| SENTRY_AUTH_TOKEN not found | Missing env variable | Check .env.sentry-build-plugin exists and is not gitignored incorrectly |
| Component names still not showing | .js/.ts files instead of .jsx/.tsx | Rename files or use the Babel plugin directly on all JSX transform targets |
---
4. Default Integrations
These integrations are automatically enabled for every @sentry/react installation. No configuration required.
| Integration | Name Constant | What It Does |
|---|---|---|
| Breadcrumbs | breadcrumbsIntegration | Captures breadcrumbs from DOM events (clicks, inputs), XHR, fetch, console calls, history navigation |
| Browser API Errors | browserApiErrorsIntegration | Wraps setTimeout, setInterval, requestAnimationFrame, addEventListener, removeEventListener in try/catch so errors inside them are captured |
| Browser Session | browserSessionIntegration | Tracks session health (healthy vs. crashed) for Release Health metrics |
| Dedupe | dedupeIntegration | Prevents duplicate error events from being sent — deduplicates based on error type, message, and stack trace |
| Function to String | functionToStringIntegration | Preserves original function .toString() output after SDK instrumentation, so stack traces show readable names |
| Global Handlers | globalHandlersIntegration | Listens to window.onerror (uncaught exceptions) and window.onunhandledrejection (unhandled promise rejections) |
| HTTP Context | httpContextIntegration | Attaches current URL, referrer, and user-agent to every event |
| Inbound Filters | inboundFiltersIntegration | Filters known-noisy events by error type, message, or URL (e.g., browser extension errors, localhost-only errors) |
| Linked Errors | linkedErrorsIntegration | Follows JavaScript error.cause chains and attaches nested errors as linked issues — also used for React component stack linkage |
Customizing a Default Integration's Options
Pass the integration explicitly in integrations — it overrides the default instance:
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
// Override breadcrumbs to disable console capturing but keep DOM/fetch/XHR
Sentry.breadcrumbsIntegration({
console: false, // don't capture console.log as breadcrumbs
dom: true,
fetch: true,
history: true,
xhr: true,
}),
// Only capture unhandled exceptions; handle rejections manually
Sentry.globalHandlersIntegration({
onerror: true,
onunhandledrejection: false,
}),
],
});Removing a Default Integration
Use the function form of integrations to filter out what you don't want:
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: (integrations) => {
// Remove deduplication (e.g., if you want every occurrence recorded separately)
return integrations.filter(
(integration) => integration.name !== "Dedupe"
);
},
});Common integration names to filter:
"Dedupe"— deduplication"Breadcrumbs"— all automatic breadcrumbs"GlobalHandlers"— window.onerror / unhandledrejection"LinkedErrors"— error cause chain following"HttpContext"— URL/referrer attachment"InboundFilters"— built-in noise filtering
Disabling ALL Default Integrations
Sentry.init({
dsn: "___PUBLIC_DSN___",
defaultIntegrations: false,
// Build your own set from scratch:
integrations: [
Sentry.globalHandlersIntegration(),
Sentry.browserTracingIntegration(),
Sentry.dedupeIntegration(),
],
});---
5. Optional Integrations
These are available but must be explicitly added to your integrations array.
Performance & Tracing
| Integration | Min SDK | Description |
|---|---|---|
browserTracingIntegration() | 8.0.0 | Page load tracing, navigation tracing, automatic span creation for fetch/XHR, Core Web Vitals (LCP, FID, CLS), distributed tracing headers |
browserProfilingIntegration() | 10.27.0 (Beta) | JS Self-Profiling API — captures call stacks at 100Hz in Chromium browsers. Requires Document-Policy: js-profiling HTTP header |
Session Replay
| Integration | Min SDK | Description |
|---|---|---|
replayIntegration() | 7.27.0 | Session Replay — records DOM mutations, network requests, console output. Configured with replaysSessionSampleRate and replaysOnErrorSampleRate |
replayCanvasIntegration() | 7.98.0 | Extends replayIntegration to record <canvas> elements in replays |
Logging
| Integration | Min SDK | Description |
|---|---|---|
consoleLoggingIntegration() | 9.41.0 | Automatically forwards console.log/warn/error/info/debug calls as structured Sentry logs. Requires enableLogs: true |
Sentry.init({
enableLogs: true,
integrations: [
Sentry.consoleLoggingIntegration({
levels: ["warn", "error"], // Only capture warnings and errors
}),
],
});User Feedback
| Integration | Min SDK | Description |
|---|---|---|
feedbackIntegration() | 7.85.0 | Floating feedback button + form (bottom-right). Supports screenshots, custom theming, programmatic control |
feedbackModalIntegration() | 7.85.0 | Modal dialog variant of the feedback form |
feedbackScreenshotIntegration() | 8.0.0 | Adds screenshot capture capability to the feedback widget |
Error Enhancement
| Integration | Min SDK | Description |
|---|---|---|
extraErrorDataIntegration() | 5.16.0 | Attaches non-standard properties on Error objects (e.g., error.code, error.statusCode, custom fields) as extra context |
contextLinesIntegration() | 7.47.0 | Shows source code lines above and below the erroring line in the stack trace (requires source maps) |
httpClientIntegration() | 7.50.0 | Captures failed HTTP requests (4xx/5xx responses) as Sentry error events, with request/response bodies. Opt-in because it may capture PII |
reportingObserverIntegration() | 5.9.0 | Captures Reporting Observer API events (deprecation warnings, browser interventions, CSP violations) |
captureConsoleIntegration() | 3.3.0 | Captures console.error/console.warn calls as Sentry issues (not logs). Legacy alternative to consoleLoggingIntegration |
Stack Frame Rewriting
| Integration | Min SDK | Description |
|---|---|---|
rewriteFramesIntegration() | 5.7.0 | Rewrites stack frame file paths — useful for normalizing paths in monorepos, Docker containers, or when paths differ between build and deploy environments |
---
httpClientIntegration — Full Setup
Captures HTTP requests that fail (4xx/5xx) as error events, with the request URL, method, status code, and optionally request/response bodies.
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.httpClientIntegration({
// Capture errors for these HTTP status code ranges
failedRequestStatusCodes: [[400, 499], [500, 599]],
// Only capture errors for these URL patterns
failedRequestTargets: [
"https://api.myapp.com",
/^https:\/\/internal\.service\//,
],
}),
],
// Required to capture request/response bodies (PII risk — use cautiously)
sendDefaultPii: true,
});Adding Integrations After Init
If you need to add an integration after Sentry.init() has been called (e.g., after user consent):
// After the user accepts analytics cookies:
Sentry.addIntegration(Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}));Lazy Loading Integrations (Code Splitting)
// Dynamic import (works with any bundler)
import("@sentry/browser").then((lazySentry) => {
Sentry.addIntegration(lazySentry.replayIntegration());
});
// CDN lazyLoadIntegration() — for Sentry loader script environments
async function enableFeedback() {
try {
const feedbackIntegration =
await Sentry.lazyLoadIntegration("feedbackIntegration");
Sentry.addIntegration(feedbackIntegration({ colorScheme: "system" }));
} catch (e) {
// Ad-blockers or network failures — fail gracefully
console.warn("Could not load Sentry feedback integration", e);
}
}Lazy-loadable integrations: replayIntegration, replayCanvasIntegration, feedbackIntegration, feedbackModalIntegration, feedbackScreenshotIntegration, captureConsoleIntegration, contextLinesIntegration, linkedErrorsIntegration, dedupeIntegration, extraErrorDataIntegration, httpClientIntegration, reportingObserverIntegration, rewriteFramesIntegration, browserProfilingIntegration
---
6. Build Tool Detection & Environment Variables
Detecting the Build Tool
# Run from project root
ls vite.config.ts vite.config.js webpack.config.js webpack.config.ts \
craco.config.js next.config.js next.config.ts 2>/dev/null
cat package.json | grep -E '"vite"|"react-scripts"|"webpack"|"@craco"'| File/Package Found | Build Tool |
|---|---|
vite.config.* or "vite" in deps | Vite |
"react-scripts" in deps | Create React App |
craco.config.js or "@craco/craco" | CRA + CRACO |
webpack.config.* or "webpack" in deps | Custom Webpack |
next.config.* or "next" in deps | Next.js (use @sentry/nextjs instead) |
---
DSN Environment Variable Patterns
| Build Tool | Variable Name | How to Access in Code |
|---|---|---|
| Vite | VITE_SENTRY_DSN | import.meta.env.VITE_SENTRY_DSN |
| Create React App | REACT_APP_SENTRY_DSN | process.env.REACT_APP_SENTRY_DSN |
| Custom Webpack | SENTRY_DSN | process.env.SENTRY_DSN (requires DefinePlugin) |
| Any | SENTRY_DSN | Build-time injection (not available at runtime in browser) |
Vite — `.env` file:
# .env.production
VITE_SENTRY_DSN=https://<key>@<org>.ingest.sentry.io/<id>
VITE_SENTRY_ENVIRONMENT=production// instrument.ts
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? "development",
});Create React App — `.env.production` file:
# .env.production (committed — DSN is public)
REACT_APP_SENTRY_DSN=https://<key>@<org>.ingest.sentry.io/<id>
REACT_APP_SENTRY_ENVIRONMENT=production// instrument.ts
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
environment: process.env.REACT_APP_SENTRY_ENVIRONMENT ?? "development",
});Custom Webpack — `webpack.config.js` with `DefinePlugin`:
const webpack = require("webpack");
module.exports = {
plugins: [
new webpack.DefinePlugin({
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN),
"process.env.SENTRY_ENVIRONMENT": JSON.stringify(
process.env.NODE_ENV ?? "development"
),
}),
],
};// instrument.ts
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT,
});---
Conditional Initialization (Development vs Production)
Prevent Sentry from running in local development to avoid polluting your issue inbox with dev noise:
// instrument.ts
import * as Sentry from "@sentry/react";
const IS_PRODUCTION =
import.meta.env.PROD || // Vite
process.env.NODE_ENV === "production"; // webpack / CRA
if (IS_PRODUCTION) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN, // or your env var pattern
environment: "production",
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
tracesSampleRate: 0.2, // 20% in production
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
} else {
// In development: optionally init with verbose debug and full sampling
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: "development",
debug: true, // Verbose SDK logging
tracesSampleRate: 1.0, // 100% in dev so nothing is missed
integrations: [Sentry.browserTracingIntegration()],
// No Replay in dev — too noisy
});
}---
Complete instrument.ts Reference — All Features
A kitchen-sink example combining every feature:
// src/instrument.ts
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_APP_ENV ?? "production",
release: import.meta.env.VITE_APP_VERSION,
// ── Integrations ─────────────────────────────────────────────
integrations: [
// Tracing (navigation + API + Core Web Vitals)
Sentry.browserTracingIntegration(),
// Session Replay
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
// Profiling (Beta — Chromium only, needs Document-Policy header)
// Sentry.browserProfilingIntegration(),
// Structured logging (requires enableLogs: true)
Sentry.consoleLoggingIntegration({
levels: ["warn", "error"],
}),
// Capture HTTP 4xx/5xx as Sentry errors
Sentry.httpClientIntegration({
failedRequestStatusCodes: [[400, 499], [500, 599]],
}),
// Show source lines in stack traces (requires source maps)
Sentry.contextLinesIntegration(),
// Capture non-standard Error properties
Sentry.extraErrorDataIntegration(),
// User feedback widget
Sentry.feedbackIntegration({
colorScheme: "system",
showBranding: false,
triggerLabel: "Report a Bug",
}),
],
// ── Tracing ───────────────────────────────────────────────────
tracesSampleRate: 0.2,
tracePropagationTargets: [
"localhost",
/^https:\/\/api\.myapp\.com/,
],
// ── Session Replay ────────────────────────────────────────────
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
// ── Profiling ─────────────────────────────────────────────────
profileSessionSampleRate: 1.0,
// ── Logging ───────────────────────────────────────────────────
enableLogs: true,
beforeSendLog: (log) => {
if (log.level === "debug") return null; // Drop debug logs
if (log.attributes?.password) delete log.attributes.password;
return log;
},
// ── Redux ─────────────────────────────────────────────────────
normalizeDepth: 10,
// ── Filtering ─────────────────────────────────────────────────
ignoreErrors: [
"ResizeObserver loop limit exceeded",
/^Loading chunk \d+ failed/,
],
denyUrls: [
/extensions\//i,
/^chrome:\/\//i,
],
beforeSend(event) {
// Strip credit card numbers from messages just in case
if (event.message) {
event.message = event.message.replace(/\b\d{16}\b/g, "[CARD]");
}
return event;
},
});// store/index.ts — wire in Redux enhancer
import { configureStore } from "@reduxjs/toolkit";
import * as Sentry from "@sentry/react";
export const store = configureStore({
reducer: rootReducer,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(
Sentry.createReduxEnhancer({
stateTransformer: (state) => ({ ...state, auth: null }),
configureScopeWithState: (scope, state) => {
scope.setTag("user.plan", state.user.plan);
},
})
),
});---
Quick Reference
// ── Redux ─────────────────────────────────────────────────────
Sentry.createReduxEnhancer({
actionTransformer: (action) => action | null,
stateTransformer: (state) => state | null,
configureScopeWithState: (scope, state) => void,
attachReduxState: true,
})
// normalizeDepth goes in Sentry.init(), not createReduxEnhancer()
// ── Component Tracking ────────────────────────────────────────
Sentry.withProfiler(Component, { name?, includeRender?, includeUpdates? })
sentryVitePlugin({ reactComponentAnnotation: { enabled: true, ignoredComponents: [] } })
// Babel plugin direct: @sentry/babel-plugin-component-annotate
// ── Source Maps ───────────────────────────────────────────────
npx @sentry/wizard@latest -i sourcemaps // automated setup
// Vite: build.sourcemap = "hidden" + sentryVitePlugin()
// Webpack: devtool: "hidden-source-map" + sentryWebpackPlugin()
// CRA: use CRACO + sentryWebpackPlugin
// Manual: npx sentry-cli sourcemaps upload --auth-token $TOKEN ./dist
// ── Integrations (opt-in) ─────────────────────────────────────
Sentry.browserTracingIntegration() // Tracing + Core Web Vitals
Sentry.replayIntegration() // Session Replay
Sentry.browserProfilingIntegration() // JS Profiler (Beta, Chromium only)
Sentry.consoleLoggingIntegration() // console → Sentry logs
Sentry.feedbackIntegration() // Feedback widget
Sentry.httpClientIntegration() // 4xx/5xx as errors
Sentry.contextLinesIntegration() // Source lines in stack traces
Sentry.extraErrorDataIntegration() // Non-standard Error properties
Sentry.reportingObserverIntegration() // Browser deprecation/CSP reports
Sentry.rewriteFramesIntegration() // Rewrite stack frame paths
Sentry.captureConsoleIntegration() // console → Sentry issues (legacy)
// ── Remove a Default Integration ─────────────────────────────
Sentry.init({
integrations: (integrations) =>
integrations.filter((i) => i.name !== "Dedupe"),
})
// ── Add Integration After Init ────────────────────────────────
Sentry.addIntegration(Sentry.replayIntegration())
// ── Environment Variables ─────────────────────────────────────
// Vite: import.meta.env.VITE_SENTRY_DSN
// CRA: process.env.REACT_APP_SENTRY_DSN
// Webpack: process.env.SENTRY_DSN (via DefinePlugin)Related skills
Forks & variants (1)
Sentry React Sdk has 1 known copy in the catalog totaling 337 installs. They canonicalize to this original listing.
- getsentry - 337 installs
FAQ
React 19 or older error handling?
React 19 uses reactErrorHandler on createRoot; older versions wrap Sentry.ErrorBoundary.
Where put Sentry.init?
In src/instrument.ts imported as the very first line of the entry file.
Vite DSN variable name?
Use VITE_SENTRY_DSN accessed via import.meta.env.VITE_SENTRY_DSN.
Is Sentry React Sdk safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.