
Sentry Browser Sdk
- 2.4k installs
- 243 repo stars
- Updated July 27, 2026
- getsentry/sentry-for-ai
sentry-browser-sdk is a wizard for @sentry/browser setup on vanilla JavaScript, static sites, and CMS pages.
About
The sentry-browser-sdk skill guides complete Sentry setup for browser JavaScript including vanilla JS, jQuery, static sites, and WordPress when no framework-specific SDK applies. Phase one detects React, Next.js, Vue, Angular, Svelte, Remix, Nuxt, Astro, Ember, or Node server frameworks and redirects to dedicated Sentry skills because framework SDKs add router transactions and error boundaries. For plain sites it chooses npm install when package.json and a bundler exist, Loader Script for WordPress or Shopify without npm, or CDN bundle for manual script tags. Recommendations lead with error monitoring, tracing for interactive pages, and session replay for user-facing flows, optionally adding user feedback, logging, or Chromium-only profiling when headers allow. npm path installs @sentry/browser and initializes Sentry.init in a sidecar instrument file before other code. Loader Script suits zero-build CMS deployments while CDN bundles cover static HTML without loader access. Source maps plugins align with Vite, webpack, Rollup, or esbuild when detected. Existing @sentry/browser installs skip straight to feature configuration.
- Redirect to framework SDK skills when React, Next.js, Vue, or Angular is detected.
- Path A npm install when package.json and bundler exist; Path B Loader Script for CMS sites.
- Error monitoring is baseline; tracing and session replay recommended for interactive pages.
- Sentry.init must run in instrument.ts before any other application code loads.
- Logging and profiling have path-specific limits such as Loader Script logging gaps.
Sentry Browser Sdk by the numbers
- 2,426 all-time installs (skills.sh)
- +47 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #36 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-browser-sdk capabilities & compatibility
- Capabilities
- framework detection with redirect to specialized · npm, loader script, and cdn installation path se · error monitoring, tracing, and session replay re · bundler and cms indicator detection for source m · feature matrix for logging, feedback, and profil
- Works with
- sentry · chrome
- Use cases
- debugging · frontend
- Runs
- Local or remote
- Pricing
- Freemium
npx skills add https://github.com/getsentry/sentry-for-ai --skill sentry-browser-sdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.4k |
|---|---|
| repo stars | ★ 243 |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | getsentry/sentry-for-ai ↗ |
How do I add Sentry error monitoring and session replay to a plain JavaScript or WordPress site?
Set up @sentry/browser for vanilla JavaScript sites with framework detection, install path selection, tracing, and session replay.
Who is it for?
Vanilla JS, jQuery, static HTML, or WordPress sites without a framework-specific Sentry package.
Skip if: Skip for Next.js, React SPA, or Node servers; redirect to the matching Sentry framework skill.
When should I use this skill?
User asks to add Sentry to a website, install @sentry/browser, or configure Loader Script monitoring.
What you get
Initialized @sentry/browser or Loader Script with error monitoring, tracing, and optional replay configured.
- Sentry browser init config
- Integration enablement checklist
By the numbers
- Minimum SDK: @sentry/browser ≥7.0.0
- makeBrowserOfflineTransport requires @sentry/browser ≥7.48.0
Files
All Skills > SDK Setup > Browser SDK
Sentry Browser SDK
Opinionated wizard that scans your project and guides you through complete Sentry setup for browser JavaScript — vanilla JS, jQuery, static sites, WordPress, and any JS project without a framework-specific SDK.
Invoke This Skill When
- User asks to "add Sentry to a website" or set up Sentry for plain JavaScript
- User wants to install
@sentry/browseror configure the Loader Script - User has a WordPress, Shopify, Squarespace, or static HTML site
- User wants error monitoring, tracing, session replay, or logging without a framework
- No framework-specific SDK applies
Note: SDK versions and APIs below reflect @sentry/browser ≥10.0.0.Always verify against docs.sentry.io/platforms/javascript/ before implementing.
---
Phase 1: Detect
CRITICAL — Check for frameworks first. Framework-specific SDKs provide significantly better coverage and must be recommended before proceeding with @sentry/browser.
Step 1A: Framework Detection (Redirect If Found)
# Check for React
cat package.json 2>/dev/null | grep -E '"react"'
# Check for Next.js
cat package.json 2>/dev/null | grep '"next"'
# Check for Vue
cat package.json 2>/dev/null | grep '"vue"'
# Check for Angular
cat package.json 2>/dev/null | grep '"@angular/core"'
# Check for Svelte / SvelteKit
cat package.json 2>/dev/null | grep -E '"svelte"|"@sveltejs/kit"'
# Check for Remix
cat package.json 2>/dev/null | grep -E '"@remix-run/react"|"@remix-run/node"'
# Check for Nuxt
cat package.json 2>/dev/null | grep '"nuxt"'
# Check for Astro
cat package.json 2>/dev/null | grep '"astro"'
# Check for Ember
cat package.json 2>/dev/null | grep '"ember-source"'
# Check for Node.js server frameworks (wrong SDK entirely)
cat package.json 2>/dev/null | grep -E '"express"|"fastify"|"@nestjs/core"|"koa"'If a framework is detected, stop and redirect:
| Framework detected | Redirect to |
|---|---|
next | Load sentry-nextjs-sdk skill — do not proceed here |
react (without Next.js) | Load sentry-react-sdk skill — do not proceed here |
vue | Suggest @sentry/vue — see docs.sentry.io/platforms/javascript/guides/vue/ |
@angular/core | Suggest @sentry/angular — see docs.sentry.io/platforms/javascript/guides/angular/ |
@sveltejs/kit | Load sentry-svelte-sdk skill — do not proceed here |
svelte (SPA, no kit) | Suggest @sentry/svelte — see docs.sentry.io/platforms/javascript/guides/svelte/ |
@remix-run | Suggest @sentry/remix — see docs.sentry.io/platforms/javascript/guides/remix/ |
nuxt | Suggest @sentry/nuxt — see docs.sentry.io/platforms/javascript/guides/nuxt/ |
astro | Suggest @sentry/astro — see docs.sentry.io/platforms/javascript/guides/astro/ |
ember-source | Suggest @sentry/ember — see docs.sentry.io/platforms/javascript/guides/ember/ |
express / fastify / @nestjs/core | This is a Node.js server — load sentry-node-sdk or sentry-nestjs-sdk skill |
Why redirect matters: Framework SDKs add router-aware transactions, error boundaries, component tracking, and often SSR coverage. Using @sentry/browser directly in a React or Next.js app loses all of that.Only continue with @sentry/browser if no framework is detected.
Step 1B: Installation Method Detection
# Check if there's a package.json at all (bundler environment)
ls package.json 2>/dev/null
# Check package manager
ls package-lock.json yarn.lock pnpm-lock.yaml bun.lockb 2>/dev/null
# Check build tool
ls vite.config.ts vite.config.js webpack.config.js rollup.config.js esbuild.config.js 2>/dev/null
cat package.json 2>/dev/null | grep -E '"vite"|"webpack"|"rollup"|"esbuild"'
# Check for CMS or static site indicators
ls wp-config.php wp-content/ 2>/dev/null # WordPress
ls _config.yml _config.yaml 2>/dev/null # Jekyll
ls config.toml 2>/dev/null # Hugo
ls .eleventy.js 2>/dev/null # Eleventy
# Check for existing Sentry
cat package.json 2>/dev/null | grep '"@sentry/'
grep -r "sentry-cdn.com\|js.sentry-cdn.com" . --include="*.html" -l 2>/dev/null | head -3What to determine:
| Question | Impact |
|---|---|
package.json exists + bundler? | → Path A: npm install |
| WordPress, Shopify, static HTML, no npm? | → Path B: Loader Script |
| Script tags only, no Loader Script access? | → Path C: CDN bundle |
Already has @sentry/browser? | Skip install, go straight to feature config |
| Build tool is Vite / webpack / Rollup / esbuild? | Source maps plugin to configure |
---
Phase 2: Recommend
Present a recommendation based on what you found. Lead with a concrete proposal, don't ask open-ended questions.
Recommended (core coverage):
- ✅ Error Monitoring — always; captures unhandled errors and promise rejections
- ✅ Tracing — recommended for any interactive site; tracks page load and user interactions
- ✅ Session Replay — recommended for user-facing apps; records sessions around errors
Optional (enhanced observability):
- ⚡ User Feedback — capture reports directly from users after errors
- ⚡ Logging — structured logs via
Sentry.logger.*; requires npm or CDN logs bundle (not available via Loader Script) - ⚡ Profiling — JS Self-Profiling API; beta, Chromium-only, requires
Document-Policy: js-profilingresponse header
Feature recommendation logic:
| Feature | Recommend when... |
|---|---|
| Error Monitoring | Always — non-negotiable baseline |
| Tracing | Always for interactive pages — page load + navigation spans are high-value |
| Session Replay | User-facing app, support flows, or checkout pages |
| User Feedback | Support-focused app; want in-app bug reports with screenshots |
| Logging | Structured log search or log-to-trace correlation needed; npm path only |
| Profiling | Performance-critical, Chromium-only app; Document-Policy: js-profiling header required |
Installation path recommendation:
| Scenario | Recommended path |
|---|---|
Project has package.json + bundler | Path A (npm) — full features, source maps, tree-shaking |
| WordPress, Shopify, Squarespace, static HTML | Path B (Loader Script) — zero build tooling, always up to date |
| Static HTML without Loader Script access | Path C (CDN bundle) — manual <script> tag |
Propose: "I recommend setting up Error Monitoring + Tracing + Session Replay using Path A (npm). Want me to also add Logging or User Feedback?"
---
Phase 3: Guide
Path A: npm / yarn / pnpm (Recommended — Bundler Projects)
Install
npm install @sentry/browser --save
# or
yarn add @sentry/browser
# or
pnpm add @sentry/browserCreate src/instrument.ts
Sentry must initialize before any other code runs. Put Sentry.init() in a dedicated sidecar file:
import * as Sentry from "@sentry/browser";
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/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 |
| Custom webpack | SENTRY_DSN | process.env.SENTRY_DSN |
| esbuild | SENTRY_DSN | process.env.SENTRY_DSN |
| Rollup | SENTRY_DSN | process.env.SENTRY_DSN |
Entry Point Setup
Import instrument.ts as the very first import in your entry file:
// src/main.ts or src/index.ts
import "./instrument"; // ← MUST be first
// ... rest of your appSource Maps Setup (Strongly Recommended)
Without source maps, stack traces show minified code. Set up the build plugin to upload source maps automatically:
No dedicated browser wizard: There is nonpx @sentry/wizard -i browserflag. The closest isnpx @sentry/wizard@latest -i sourcemapswhich configures source map upload only for an already-initialized SDK.
Vite (`vite.config.ts`):
import { defineConfig } from "vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
build: { sourcemap: "hidden" },
plugins: [
// sentryVitePlugin MUST be last
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
});webpack (`webpack.config.js`):
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
module.exports = {
devtool: "hidden-source-map",
plugins: [
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
};Rollup (`rollup.config.js`):
import { sentryRollupPlugin } from "@sentry/rollup-plugin";
export default {
output: { sourcemap: "hidden" },
plugins: [
sentryRollupPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
};esbuild (`build.js`):
const { sentryEsbuildPlugin } = require("@sentry/esbuild-plugin");
require("esbuild").build({
entryPoints: ["src/index.ts"],
bundle: true,
sourcemap: "hidden",
plugins: [
sentryEsbuildPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
});⚠️ esbuild plugin does not fully supportsplitting: true. Usesentry-cliinstead if code splitting is enabled.
Using `sentry-cli` (any toolchain / CI):
# After your build step:
npx @sentry/cli sourcemaps inject ./dist
npx @sentry/cli sourcemaps upload ./distAdd .env for auth (never commit):
SENTRY_AUTH_TOKEN=sntrys_...
SENTRY_ORG=my-org-slug
SENTRY_PROJECT=my-project-slug---
Path B: Loader Script (WordPress, Static Sites, Shopify, Squarespace)
Best for: Sites without a build system. The Loader Script is a single <script> tag that lazily loads the full SDK, always stays up to date via Sentry's CDN, and buffers errors before the SDK loads.
Get the Loader Script: Sentry UI → Settings → Projects → (your project) → SDK Setup → Loader Script
Copy the generated tag and place it as the first script on every page:
<!DOCTYPE html>
<html>
<head>
<!-- Configure BEFORE the loader tag -->
<script>
window.sentryOnLoad = function () {
Sentry.init({
// DSN is already configured in the loader URL
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
};
</script>
<!-- Loader Script FIRST — before all other scripts -->
<script
src="https://js.sentry-cdn.com/YOUR_PUBLIC_KEY.min.js"
crossorigin="anonymous"
></script>
</head>
...
</html>Loader loading modes:
| Mode | How | When SDK loads |
|---|---|---|
| Lazy (default) | Nothing extra | On first error or manual Sentry call |
| Eager | Add data-lazy="no" to <script> | After all page scripts finish |
| Manual | Call Sentry.forceLoad() | Whenever you call it |
Safe to call before SDK loads (buffered):
Sentry.captureException()Sentry.captureMessage()Sentry.captureEvent()Sentry.addBreadcrumb()Sentry.withScope()
For other methods, use `Sentry.onLoad()`:
<script>
window.Sentry && Sentry.onLoad(function () {
Sentry.setUser({ id: "123" });
});
</script>Set release via global (optional):
<script>
window.SENTRY_RELEASE = { id: "my-app@1.0.0" };
</script>Loader Script limitations:
- ❌ No
Sentry.logger.*(logging) — npm path only - ❌ No framework-specific features (React ErrorBoundary, Vue Router tracking, etc.)
- ❌ Tracing headers only added to fetch calls made after SDK loads
- ❌ Version changes take a few minutes to propagate via CDN cache
- ⚠️ Use
defer(notasync) on all other scripts when using the loader
CSP requirements:
script-src: https://browser.sentry-cdn.com https://js.sentry-cdn.com
connect-src: *.sentry.io---
Path C: CDN Bundles (Manual Script Tags)
Best for: Pages that can't use the Loader Script but need synchronous loading.
Pick the bundle that matches your feature needs and place it before all other scripts:
Errors only (minimal footprint):
<script
src="https://browser.sentry-cdn.com/10.42.0/bundle.min.js"
integrity="sha384-L/HYBH2QCeLyXhcZ0hPTxWMnyMJburPJyVoBmRk4OoilqrOWq5kU4PNTLFYrCYPr"
crossorigin="anonymous"
></script>Errors + Tracing:
<script
src="https://browser.sentry-cdn.com/10.42.0/bundle.tracing.min.js"
integrity="sha384-DIqcfVcfIewrWiNWfVZcGWExO5v673hkkC5ixJnmAprAfJajpUDEAL35QgkOB5gw"
crossorigin="anonymous"
></script>Errors + Session Replay:
<script
src="https://browser.sentry-cdn.com/10.42.0/bundle.replay.min.js"
integrity="sha384-sbojwIJFpv9duIzsI9FRm87g7pB15s4QwJS1m1xMSOdV1CF3pwgrPPEu38Em7M9+"
crossorigin="anonymous"
></script>Errors + Tracing + Replay (recommended full setup):
<script
src="https://browser.sentry-cdn.com/10.42.0/bundle.tracing.replay.min.js"
integrity="sha384-oo2U4zsTxaHSPXJEnXtaQPeS4Z/qbTqoBL9xFgGxvjJHKQjIrB+VRlu97/iXBtzw"
crossorigin="anonymous"
></script>Errors + Tracing + Replay + User Feedback:
<script
src="https://browser.sentry-cdn.com/10.42.0/bundle.tracing.replay.feedback.min.js"
integrity="sha384-SmHU39Qs9cua0KLtq3A6gis1/cqM1nZ6fnGzlvWAPiwhBDO5SmwFQV65BBpJnB3n"
crossorigin="anonymous"
></script>Full bundle (all features):
<script
src="https://browser.sentry-cdn.com/10.42.0/bundle.tracing.replay.feedback.logs.metrics.min.js"
integrity="sha384-gOjSzRxwpXpy0FlT6lg/AVhagqrsUrOWUO7jm6TJwuZ9YVHtYK0MBA2hW2FGrIGl"
crossorigin="anonymous"
></script>CDN bundle variants summary:
| Bundle | Features | When to use |
|---|---|---|
bundle.min.js | Errors only | Absolute minimum footprint |
bundle.tracing.min.js | + Tracing | Performance monitoring |
bundle.replay.min.js | + Replay | Session recording |
bundle.tracing.replay.min.js | + Tracing + Replay | Full observability |
bundle.tracing.replay.feedback.min.js | + User Feedback | + in-app feedback widget |
bundle.logs.metrics.min.js | + Logs + Metrics | Structured logs (CDN) |
bundle.tracing.replay.feedback.logs.metrics.min.js | Everything | Max coverage |
Initialize after the script tag:
<script>
Sentry.init({
dsn: "https://YOUR_KEY@o0.ingest.sentry.io/YOUR_PROJECT",
environment: "production",
release: "my-app@1.0.0",
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
tracesSampleRate: 1.0,
tracePropagationTargets: ["localhost", /^https:\/\/yourapi\.io/],
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
</script>CDN CSP requirements:
script-src: https://browser.sentry-cdn.com https://js.sentry-cdn.com
connect-src: *.sentry.io---
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 | Page load / API call tracing |
| Session Replay | ${SKILL_ROOT}/references/session-replay.md | User-facing app |
| Logging | ${SKILL_ROOT}/references/logging.md | Structured log search; npm or CDN logs bundle (not Loader Script) |
| Profiling | ${SKILL_ROOT}/references/profiling.md | Performance-critical, Chromium-only |
| User Feedback | ${SKILL_ROOT}/references/user-feedback.md | Capture user reports after errors |
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 |
sendDefaultPii | boolean | false | Includes IP addresses and request headers. Will be deprecated in v11 — use dataCollection instead |
dataCollection | object | — | Fine-grained control over collected data (v10.54+). See table below |
tracesSampleRate | number | — | 0–1; 1.0 in dev, 0.1–0.2 in prod |
tracesSampler | function | — | Per-transaction sampling; overrides rate |
tracePropagationTargets | `(string\ | RegExp)[]` | same-origin |
replaysSessionSampleRate | number | — | Fraction of all sessions recorded |
replaysOnErrorSampleRate | number | — | Fraction of error sessions recorded |
enableLogs | boolean | false | Enable Sentry.logger.* API (npm or CDN logs bundle; not Loader Script) |
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 |
ignoreErrors | `(string\ | RegExp)[]` | [] |
denyUrls | `(string\ | RegExp)[]` | [] |
allowUrls | `(string\ | RegExp)[]` | [] |
spotlight | `boolean\ | string` | false |
Browser-Specific Options
| Option | Type | Default | Notes |
|---|---|---|---|
cdnBaseUrl | string | — | Base URL for lazy-loading integrations |
skipBrowserExtensionCheck | boolean | false | Skip check for browser extension context |
dataCollection Option (v10.54+)
Fine-grained control over what data the SDK collects. Replaces the simple sendDefaultPii boolean with granular settings. When omitted, falls back to sendDefaultPii for backwards compatibility.
| Field | Type | Default | Notes |
|---|---|---|---|
userInfo | boolean | true | Auto-populate user.* fields from instrumentation |
cookies | `boolean \ | { allow: string[] } \ | { deny: string[] }` |
httpHeaders | { request?, response? } | { request: true, response: true } | HTTP header collection; each can be boolean or allow/deny object |
httpBodies | string[] | ["incomingRequest", "outgoingRequest", "incomingResponse", "outgoingResponse"] | HTTP body types to collect (when dataCollection is explicitly set) |
queryParams | `boolean \ | { allow: string[] } \ | { deny: string[] }` |
genAI | { inputs?, outputs? } | { inputs: true, outputs: true } | Generative AI input/output recording |
stackFrameVariables | boolean | true | Capture local variable values in stack frames |
frameContextLines | number | 5 | Source code context lines around stack frames |
Example:
Sentry.init({
dsn: "...",
dataCollection: {
userInfo: true,
cookies: { deny: ["session_id", "auth_token"] },
httpHeaders: {
request: { deny: ["authorization", "cookie"] },
response: true,
},
queryParams: { allow: ["utm_source", "utm_campaign"] },
},
});window.SENTRY_RELEASE Global (CDN / Loader Path)
Set the release version before the SDK loads:
<script>
window.SENTRY_RELEASE = { id: "my-app@1.0.0" };
</script>---
Verification
Trigger test events to confirm Sentry is receiving data:
npm / CDN path:
<!-- Add temporarily to your page -->
<button onclick="throw new Error('Sentry Browser Test Error')">
Test Error
</button>Performance verification (npm path):
import * as Sentry from "@sentry/browser";
Sentry.startSpan({ name: "Test Span", op: "test" }, () => {
// your operation
});Manual capture:
Sentry.captureException(new Error("Manual test"));
Sentry.captureMessage("Manual test message", "info");Check the Sentry dashboard:
- Issues → error appears within seconds
- Traces → page load transaction visible
- Replays → session recording visible after page interaction
- Logs → structured log entries if logging enabled (npm or CDN logs bundle)
Set debug: true in Sentry.init() and check the browser console if nothing appears.
---
Phase 4: Cross-Link
After completing browser 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 -3
cat ../composer.json 2>/dev/null | 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 |
PHP (composer.json) | sentry-php-sdk |
.NET (*.csproj, *.sln) | sentry-dotnet-sdk |
Java (pom.xml, build.gradle) | See docs.sentry.io/platforms/java/ |
| Node.js (Express, Fastify) | sentry-node-sdk |
NestJS (@nestjs/core) | sentry-nestjs-sdk |
---
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 build plugin config; run npx @sentry/wizard@latest -i sourcemaps |
| CDN bundle not found | Check version number in URL; see browser.sentry-cdn.com for latest |
| SRI integrity error | Hash mismatch — re-copy the full <script> tag including integrity attribute from this skill |
| Loader Script not firing | Verify it's the first <script> on the page; check for CSP errors in console |
| Tracing not working with Loader | Fetch calls before SDK loads won't be traced — wrap early calls in Sentry.onLoad() |
sentryOnLoad not called | Must define window.sentryOnLoad before the loader <script> tag |
| Logging not available | Sentry.logger.* requires npm or a CDN bundle with .logs. in its name — not supported via Loader Script |
| Profiling not working | Verify Document-Policy: js-profiling header on document responses; Chromium-only |
| Ad blockers dropping events | Set tunnel: "/sentry-tunnel" and add a server-side relay endpoint |
| Session replay not recording | Confirm replayIntegration() is in init; check replaysSessionSampleRate > 0 |
| Replay CSP errors | Add worker-src 'self' blob: and child-src 'self' blob: to your CSP |
tracePropagationTargets not matching | Check regex escaping; default is same-origin only |
| Events blocked by browser extension | Add denyUrls: [/chrome-extension:\/\//] to filter extension errors |
| High event volume | Lower sampleRate (errors) and tracesSampleRate from 1.0 in production |
| Source maps uploaded after deploy | Source maps must be uploaded before errors occur — integrate into CI/CD |
| esbuild splitting conflict | sentryEsbuildPlugin doesn't support splitting: true — use sentry-cli instead |
Error Monitoring — Sentry Browser SDK
Minimum SDK: @sentry/browser ≥7.0.0makeBrowserOfflineTransportrequires@sentry/browser≥7.48.0
linkedErrorsIntegrationcausechain requires Error.cause support (Chrome 93+, Firefox 91+)
---
How Automatic Capture Works
The browser SDK hooks into the browser environment and captures errors from multiple layers automatically:
| Layer | Mechanism | Integration |
|---|---|---|
| Uncaught synchronous exceptions | window.onerror | globalHandlersIntegration (default on) |
| Unhandled promise rejections | window.onunhandledrejection | globalHandlersIntegration (default on) |
Errors in setTimeout / setInterval / requestAnimationFrame / addEventListener | Patched browser APIs | browserApiErrorsIntegration (default on) |
| Console errors (optional) | Patched console.error | captureConsoleIntegration (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 - Business-logic failures (validation errors, unexpected states)
- Async errors in
.then()chains where.catch()is attached - User-visible conditions that aren't exceptions (use
captureMessage)
---
Core Capture APIs
Sentry.captureException(error, captureContext?)
Captures an exception and sends it to Sentry. Prefer Error objects — they include stack traces.
import * as Sentry from "@sentry/browser";
// Basic usage
try {
riskyOperation();
} catch (err) {
Sentry.captureException(err);
}
// With inline capture context
try {
await chargeCard(order);
} catch (err) {
Sentry.captureException(err, {
level: "fatal",
tags: { module: "checkout", payment_provider: "stripe" },
extra: { orderId: order.id, amount: order.total },
user: { id: "u_123", email: "user@example.com" },
fingerprint: ["checkout-payment-fail"],
contexts: {
payment: { provider: "stripe", amount: 9999, currency: "usd" },
},
});
}
// Non-Error values are accepted but may lack stack traces
Sentry.captureException("Something went wrong as a string");`CaptureContext` shape:
| Field | Type | Description |
|---|---|---|
level | `"fatal" \ | "error" \ |
tags | Record<string, string> | Indexed, filterable key-value pairs |
extra | Record<string, unknown> | Unindexed supplementary data |
user | { id?, email?, username?, ip_address? } | User identity |
contexts | Record<string, Record<string, unknown>> | Named structured context blocks |
fingerprint | string[] | Custom issue grouping key |
---
Sentry.captureMessage(message, levelOrContext?)
Captures a plain-text message as a Sentry issue.
// With a severity level (shorthand second argument)
Sentry.captureMessage("Something went wrong", "warning");
Sentry.captureMessage("Payment gateway timeout", "fatal");
// With full CaptureContext
Sentry.captureMessage("User performed invalid action", {
level: "warning",
user: { id: "u_456" },
tags: { feature: "cart", action: "remove-item" },
extra: { itemId: "sku_789" },
});SetattachStacktrace: trueinSentry.init()to automatically attach a stack trace to message events.
---
Sentry.captureEvent(event)
Sends a fully constructed Sentry event object. Use captureException or captureMessage in application code; use captureEvent for custom integrations or forwarding 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"],
});---
Utility APIs
// Get the event ID of the last sent error event
const eventId = Sentry.lastEventId();
// Flush all pending events before page unload / shutdown
await Sentry.flush(2000); // wait up to 2 seconds
// Flush and disable the SDK permanently
await Sentry.close(2000);
// Check if SDK is initialized and enabled
if (Sentry.isEnabled()) { /* ... */ }---
Scope Management
Sentry uses three nested scope types. Data from all three is merged before each event is sent.
| Scope | API | Lifetime | Use Case |
|---|---|---|---|
| Global | getGlobalScope() | Entire application | App-wide constants: version, build ID, region |
| Isolation | getIsolationScope() | Per page load (browser) | User info, session data, tags set via top-level setTag() |
| Current | withScope() | Per-event / narrowest | Per-operation data: one API call, one form submit |
Merge priority (later overrides earlier):
Global Scope → Isolation Scope → Current Scope → Event-level CaptureContextBrowser note: In a browser there is no per-request isolation, so the isolation scope effectively behaves like the global scope. The distinction matters in server-side runtimes (Node.js, Deno, Cloudflare Workers).
---
withScope — Per-Event Scoping (Recommended)
Forks the current scope, runs your callback with the fork, and discards it when done. This is the preferred way to attach data to a single event without polluting broader scope.
Sentry.withScope((scope) => {
scope.setTag("transaction_id", "txn_abc123");
scope.setExtra("requestPayload", { amount: 50, currency: "USD" });
scope.setLevel("warning");
scope.setUser({ id: "u_789" });
scope.setFingerprint(["payment-error", "stripe"]);
Sentry.captureException(new Error("Payment failed"));
// scope is discarded after this callback
});
// Events captured here are NOT affected by the above scope
Sentry.captureMessage("This event has no payment tags");---
Scope Methods
Every scope instance exposes the same enrichment API:
// Isolation scope — persists for all subsequent events on this page
Sentry.getIsolationScope().setUser({ id: "u_123", email: "user@example.com" });
Sentry.getIsolationScope().setTag("app_version", "3.4.1");
// Global scope — applied to every event in the app
Sentry.getGlobalScope().setTag("datacenter", "us-east-1");
Sentry.getGlobalScope().setContext("build", {
commit: "abc1234",
buildDate: "2026-03-03",
});
// Scope method reference
scope.setUser({ id, email, username, ip_address }); // setUser(null) to clear
scope.setTag("key", "value");
scope.setTags({ key1: "v1", key2: "v2" });
scope.setExtra("key", value);
scope.setExtras({ key1: v1, key2: v2 });
scope.setContext("name", { key: value }); // setContext("name", null) to remove
scope.setLevel("warning");
scope.setFingerprint(["my-group-key"]);
scope.addBreadcrumb({ category: "auth", message: "User logged in" });
scope.addEventProcessor((event) => { /* modify or drop */ return event; });
scope.clear(); // reset all scope data---
Event Enrichment
Tags — Indexed, Searchable Key-Value Pairs
Tags power filtering, search, and tag distribution maps in the Sentry UI.
Constraints: Key ≤32 chars (a-zA-Z0-9_.:-, no spaces). Value ≤200 chars, no newlines.
// Single tag — applied to all subsequent events (isolation scope)
Sentry.setTag("page_locale", "de-at");
Sentry.setTag("subscription_tier", "pro");
Sentry.setTag("feature_flag", "new_checkout_enabled");
// Multiple tags at once
Sentry.setTags({
environment: "staging",
region: "eu-west-1",
api_version: "v3",
});
// Scoped tag — only on this one event
Sentry.withScope((scope) => {
scope.setTag("retry_attempt", "3");
Sentry.captureException(new Error("Max retries exceeded"));
});---
Context — Rich Unindexed Structured Data
Context is not indexed or searchable but displays in full on the event details page. Use it for rich structured data you need for debugging but don't need to filter on.
Sentry.setContext("shopping_cart", {
itemCount: 3,
totalAmount: 149.99,
currency: "USD",
couponApplied: "SAVE10",
});
Sentry.setContext("device", {
platform: navigator.platform,
language: navigator.language,
screenWidth: screen.width,
screenHeight: screen.height,
});
// Clear a context by passing null
Sentry.setContext("shopping_cart", null);Depth limit: Nested context objects are normalized to 3 levels deep by default. UsenormalizeDepthininit()to change this.
---
User Information
// Set user on login
Sentry.setUser({
id: "user_abc123",
email: "alice@example.com",
username: "alice",
subscription: "premium", // arbitrary extra field
org: "acme-corp",
});
// Clear user on logout
Sentry.setUser(null);
// Auto-infer IP address (requires sendDefaultPii: true in init)
Sentry.setUser({ ip_address: "{{auto}}" });---
initialScope — Set Context at Startup
// Object form
Sentry.init({
dsn: "___PUBLIC_DSN___",
initialScope: {
tags: { "app.version": "1.2.3", region: "us-west" },
user: { id: 42, email: "john.doe@example.com" },
},
});
// Callback form (full Scope API access)
Sentry.init({
dsn: "___PUBLIC_DSN___",
initialScope: (scope) => {
scope.setTags({ a: "b", c: "d" });
scope.setContext("device", { platform: navigator.platform });
return scope;
},
});---
Breadcrumbs
Breadcrumbs create a trail of events leading up to an issue. They're buffered locally and attached to the next event sent to Sentry.
Automatic Breadcrumbs
| Source | What is captured |
|---|---|
console | console.log, warn, error, debug calls |
dom | Click and keypress events on DOM elements |
fetch | All fetch() HTTP requests (URL, method, status) |
xhr | All XMLHttpRequest calls |
history | history.pushState, history.replaceState, navigations |
sentry | Internal events when the SDK sends to Sentry |
Manual Breadcrumbs
// Authentication event
Sentry.addBreadcrumb({
category: "auth",
message: "User authenticated",
level: "info",
data: { userId: user.id, method: "oauth2", provider: "google" },
});
// Navigation event
Sentry.addBreadcrumb({
type: "navigation",
category: "navigation",
data: { from: "/home", to: "/checkout" },
});
// Custom action
Sentry.addBreadcrumb({
category: "cart",
message: "Item added to cart",
level: "info",
data: { itemId: "sku_123", quantity: 2, price: 29.99 },
});
// Feature flag
Sentry.addBreadcrumb({
type: "debug",
category: "feature-flag",
message: "New checkout flow enabled",
level: "debug",
data: { flag: "checkout_v2", value: true },
});Breadcrumb schema:
| Field | Type | Description |
|---|---|---|
message | string | Human-readable description |
type | `"default" \ | "debug" \ |
level | `"fatal" \ | "error" \ |
category | string | Dot-namespaced: "auth", "ui.click", "api.request" |
data | Record<string, unknown> | Arbitrary structured payload |
timestamp | number | Unix timestamp; auto-set if omitted |
---
Breadcrumb Configuration
Sentry.init({
dsn: "___PUBLIC_DSN___",
maxBreadcrumbs: 50, // default: 100
beforeBreadcrumb(breadcrumb, hint) {
// Drop UI click breadcrumbs
if (breadcrumb.category === "ui.click") 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 console.debug noise in production
if (breadcrumb.category === "console" && breadcrumb.level === "debug") {
return null;
}
return breadcrumb;
},
integrations: [
Sentry.breadcrumbsIntegration({
console: true,
dom: { serializeAttribute: ["data-testid", "aria-label"] },
fetch: true,
history: true,
xhr: true,
sentry: true,
}),
],
});---
Hooks — beforeSend, beforeSendTransaction, beforeBreadcrumb
beforeSend — Modify or Drop Error Events
Called last, just before an error event is sent. All scope data has already been applied. Return the event to send it, or null to drop it.
Sentry.init({
dsn: "___PUBLIC_DSN___",
beforeSend(event, hint) {
const err = hint.originalException;
// --- Drop known noisy errors ---
if (event.exception?.values?.[0]?.value?.includes("ResizeObserver")) {
return null;
}
// --- Drop browser extension errors ---
if (event.exception?.values?.[0]?.stacktrace?.frames?.some(
(frame) => frame.filename?.includes("extension://")
)) {
return null;
}
// --- PII scrubbing ---
if (event.user?.email) {
delete event.user.email;
}
// --- Custom fingerprinting based on original exception ---
if (err instanceof NetworkError) {
event.fingerprint = ["network-error", err.statusCode?.toString() ?? "unknown"];
}
// --- Add extra context from the original exception ---
if (err instanceof ApiError) {
event.extra = {
...event.extra,
requestId: err.requestId,
endpoint: err.endpoint,
};
}
return event;
},
});`hint` object properties:
| Property | Type | Description |
|---|---|---|
originalException | unknown | The original exception that triggered the event |
syntheticException | `Error \ | null` |
event_id | string | The generated event ID |
data | Record<string, unknown> | Arbitrary extra data |
---
beforeSendTransaction — Modify or Drop Transaction Events
Same as beforeSend but for performance transaction events.
Sentry.init({
beforeSendTransaction(event) {
// Drop health check transactions
if (event.transaction === "/health" || event.transaction === "/ping") {
return null;
}
// Scrub PII from transaction name
event.transaction = event.transaction?.replace(/\/users\/\d+/, "/users/:id");
return event;
},
});---
Event Processors
Event processors intercept every event before it's sent. Unlike beforeSend, multiple processors can be registered and run in series.
// Global event processor — runs on ALL events
Sentry.addEventProcessor((event, hint) => {
// Add build metadata to every event
event.tags = {
...event.tags,
build_sha: BUILD_SHA,
deploy_env: DEPLOY_ENV,
};
// Drop events with no stack trace in production
if (
IS_PRODUCTION &&
!event.exception?.values?.[0]?.stacktrace?.frames?.length
) {
return null;
}
return event;
});
// Scope-level processor — only applies within withScope
Sentry.withScope((scope) => {
scope.addEventProcessor((event) => {
event.tags = { ...event.tags, source: "checkout-flow" };
return event;
});
Sentry.captureException(new Error("Checkout failed"));
});Key differences vs. `beforeSend`:
| Feature | addEventProcessor | beforeSend |
|---|---|---|
| Execution order | Unspecified among processors | Always last (after all processors) |
| Multiple allowed | ✅ Unlimited | ❌ Only one |
| Scope-level support | ✅ Yes | ❌ Global init only |
| Async support | ✅ (slower) | ✅ |
---
Fingerprinting
Every event has a fingerprint array. Events with the same fingerprint are grouped into the same issue.
Extending Default Grouping
Use {{ default }} to keep Sentry's default grouping and add extra discriminators:
Sentry.init({
beforeSend(event, hint) {
const err = hint.originalException;
if (err instanceof ApiError) {
// Keep default grouping but split further by RPC function + error code
event.fingerprint = ["{{ default }}", err.functionName, String(err.errorCode)];
}
return event;
},
});Overriding Default Grouping
Omit {{ default }} to completely replace the auto-generated fingerprint (collapses all matching errors into one issue):
Sentry.init({
beforeSend(event, hint) {
const err = hint.originalException;
if (err?.message?.includes("timeout")) {
event.fingerprint = ["network-timeout"];
}
if (err?.name === "ChunkLoadError") {
event.fingerprint = ["chunk-load-failure"];
}
return event;
},
});Inline Fingerprint on Capture
Sentry.captureException(err, {
fingerprint: ["payment-gateway", "stripe", err.code],
});
Sentry.captureMessage("Rate limit exceeded", {
fingerprint: ["rate-limit", endpoint],
});
// Group by HTTP method + path + status code
Sentry.withScope((scope) => {
scope.setFingerprint([method, path, String(err.statusCode)]);
Sentry.captureException(err);
});Fingerprint variables:
| Variable | Resolves to |
|---|---|
{{ default }} | The auto-generated Sentry fingerprint |
{{ transaction }} | The transaction name |
{{ function }} | The function name in the stack trace |
{{ type }} | The exception type |
{{ module }} | The module name |
{{ value }} | The exception value/message |
---
Event Filtering
ignoreErrors — Pattern-Based Filtering
Sentry.init({
ignoreErrors: [
// String (partial match):
"ResizeObserver loop limit exceeded",
"fb_xd_fragment",
"Non-Error exception captured",
// Regex (full control):
/^Network Error$/,
/ChunkLoadError/,
/Loading chunk \d+ failed/,
/^Script error\.?$/,
],
});allowUrls / denyUrls — Filter by Script Origin
These filter based on stack frame URLs (where the code lives), not the page URL.
Sentry.init({
// Only capture errors from your own scripts
allowUrls: [
/https?:\/\/((cdn|www)\.)?myapp\.com/,
],
// Never capture errors from these script origins
denyUrls: [
/extensions\//i,
/^chrome:\/\//i,
/^moz-extension:\/\//i,
/^safari-extension:\/\//i,
/ads\.doubleclick\.net/,
],
});sampleRate — Error Volume Reduction
Sentry.init({
sampleRate: 0.25, // Capture 25% of errors (randomly sampled)
});---
Default Integrations
Auto-Enabled Browser Integrations (9 total)
| Integration | Purpose | Key Config |
|---|---|---|
breadcrumbsIntegration | Records breadcrumbs from console, DOM, fetch, XHR, history | console, dom, fetch, history, xhr |
browserApiErrorsIntegration | Wraps setTimeout, setInterval, requestAnimationFrame, addEventListener in try/catch | setTimeout, setInterval, requestAnimationFrame, eventTarget |
browserSessionIntegration | Tracks release health (session per page load / route change) | `lifecycle: "route" \ |
dedupeIntegration | Prevents duplicate events from rapid-succession throws | None |
functionToStringIntegration | Preserves original function names in wrapped stack traces | None |
globalHandlersIntegration | Attaches window.onerror and window.onunhandledrejection | onerror, onunhandledrejection |
httpContextIntegration | Attaches page URL, User-Agent, Referer to every event | None |
inboundFiltersIntegration | Client-side filtering via ignoreErrors, denyUrls, allowUrls | Configured via top-level init options |
linkedErrorsIntegration | Follows error.cause chain and attaches linked errors | key: "cause", limit: 5 |
Modifying Default Integrations
// Disable a single default integration by name
Sentry.init({
integrations: (defaults) =>
defaults.filter((i) => i.name !== "Breadcrumbs"),
});
// Reconfigure a default integration
Sentry.init({
integrations: [
Sentry.breadcrumbsIntegration({ console: false }),
Sentry.linkedErrorsIntegration({ limit: 10 }),
Sentry.globalHandlersIntegration({ onunhandledrejection: false }),
Sentry.browserSessionIntegration({ lifecycle: "page" }),
],
});
// Disable ALL defaults (start from scratch)
Sentry.init({
defaultIntegrations: false,
integrations: [
Sentry.globalHandlersIntegration(),
Sentry.linkedErrorsIntegration(),
],
});Adding Integrations After init()
// Lazy-add after init
Sentry.addIntegration(Sentry.reportingObserverIntegration());
// Dynamic import from npm (recommended with bundlers)
const { captureConsoleIntegration } = await import("@sentry/browser");
Sentry.addIntegration(captureConsoleIntegration({ levels: ["error", "warn"] }));---
Transport
Default Transport
The browser SDK uses a fetch-based transport. Events are sent as POST requests to the Sentry ingestion endpoint.
Offline Transport — IndexedDB Queue
Stores events when offline and replays them when the browser reconnects:
import { makeBrowserOfflineTransport, makeFetchTransport } from "@sentry/browser";
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_DSN___",
transport: makeBrowserOfflineTransport(makeFetchTransport),
});Tunneling — Bypass Ad-Blockers
Route all Sentry traffic through your own server endpoint:
Sentry.init({
dsn: "___PUBLIC_DSN___", // Still required for header generation
tunnel: "https://myapp.com/sentry-tunnel",
});Your server endpoint forwards the payload to Sentry's ingestion URL. See Dealing with Ad-Blockers for a full tunneling server implementation.
Custom Transport
import { createTransport } from "@sentry/core";
import * as Sentry from "@sentry/browser";
function makeCustomFetchTransport(options) {
function makeRequest(request) {
return fetch(options.url, {
body: request.body,
method: "POST",
referrerPolicy: "origin",
headers: {
...options.headers,
"X-Custom-Header": "my-value",
},
}).then((response) => ({
statusCode: response.status,
headers: {
"x-sentry-rate-limits": response.headers.get("X-Sentry-Rate-Limits"),
"retry-after": response.headers.get("Retry-After"),
},
}));
}
return createTransport(options, makeRequest);
}
Sentry.init({
dsn: "___PUBLIC_DSN___",
transport: makeCustomFetchTransport,
});---
Best Practices
- Set user context after authentication — call
Sentry.setUser()after login completes, not inSentry.init. - Clear user on logout — always call
Sentry.setUser(null)when the user signs out. - Use `withScope` for per-event context — avoid mutating the isolation scope for temporary data.
- Use tags for filterable data, context for debugging data — tags are indexed; context is not.
- Filter noise early — use
ignoreErrorsanddenyUrlsto drop known-bad events beforebeforeSend. - Avoid capturing in render paths — wrap Sentry calls in event handlers or
try/catchblocks. - Set `release` and `environment` — required for source map resolution and environment-aware alerting.
- Use `beforeSend` for PII scrubbing — never send emails, credit card numbers, or passwords as tags/extra.
- Use `{{ default }}` in fingerprints to extend, not replace, Sentry's grouping when appropriate.
---
Troubleshooting
| Issue | Solution |
|---|---|
| Errors from browser extensions captured | Add /extensions\//i, /^chrome:\/\//i, /^safari-extension:\/\//i to denyUrls |
ResizeObserver loop flooding issues | Add "ResizeObserver loop limit exceeded" to ignoreErrors |
| Script errors with no details | Cross-origin scripts without CORS headers appear as "Script error." — add CORS headers or use allowUrls |
| Events sent twice | If using multiple Sentry.init() calls, only the first takes effect. Check for duplicate SDK instances. |
beforeSend returning null but events still sent | Check beforeSendTransaction — it's a separate hook for performance events |
| User context missing on events | Call Sentry.setUser() after authentication completes; verify it's not being called before auth |
configureScope is not a function | Deprecated in SDK v8. Replace with getIsolationScope() or withScope() |
| Tags not appearing on events | Verify the tag isn't being overwritten by a built-in Sentry tag (browser, os, url, environment, release) |
| High event volume from known errors | Add patterns to ignoreErrors or use sampleRate to reduce volume |
| Unhandled rejections not captured | Verify globalHandlersIntegration({ onunhandledrejection: true }) is active (it is by default) |
linkedErrorsIntegration not showing cause chain | Requires Error.cause support — Chrome 93+, Firefox 91+. Ensure the SDK version is ≥7.0.0. |
Logging — Sentry Browser SDK
Minimum SDK:@sentry/browser≥9.41.0 forSentry.loggerAPI andenableLogs
consoleLoggingIntegration(): requires ≥10.13.0+Scope-based attributes (getGlobalScope,getIsolationScope): requires ≥10.32.0+
⚠️ NPM or CDN logs bundle required — Sentry logging is not available via the Loader Script. Use npm/yarn/pnpm (@sentry/browser) or a CDN bundle with.logs.in its name (e.g.,bundle.logs.metrics.min.js).
---
Enabling Logs
enableLogs is opt-in and must be explicitly set in Sentry.init():
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_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/browser";
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 belong as separate searchable attributes |
` Sentry.logger.info(Sentry.logger.fmt...${var}) ` | Variable is a meaningful part of the message text itself |
---
Console Integration (consoleLoggingIntegration) — SDK ≥10.13.0
Capture console.log, console.warn, console.error, and other console calls as Sentry logs automatically:
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_DSN___",
enableLogs: true,
integrations: [
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
],
});
// These are now automatically sent to Sentry
console.log("User logged in", { userId: 123 });
console.warn("Slow network detected");
console.error("API request failed");The integration intercepts console.* calls and converts them to structured Sentry logs. Interpolated values are extracted as message.parameter.N attributes.
Available levels: "log", "info", "warn", "error", "debug", "trace".
---
Filtering Logs (beforeSendLog)
Filter or modify logs before they are sent to Sentry:
Sentry.init({
dsn: "___PUBLIC_DSN___",
enableLogs: true,
beforeSendLog: (log) => {
// Drop debug logs in production
if (log.level === "debug" || log.level === "trace") {
return null;
}
// Scrub sensitive attributes
if (log.attributes?.password) {
delete log.attributes.password;
}
if (log.attributes?.credit_card) {
log.attributes.credit_card = "[REDACTED]";
}
return 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 |
Return null to drop the log. Return the (optionally modified) log object to send it.
---
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",
});Attributes become searchable and filterable in Sentry's log explorer. Prefer one comprehensive log with all relevant context over many small scattered logs.
---
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: "checkout",
version: "2.1.0",
region: "us-east-1",
});Isolation scope — logical session context
// Set after user authenticates
Sentry.getIsolationScope().setAttributes({
org_id: user.orgId,
user_tier: user.tier,
});Current scope — single operation
Sentry.withScope((scope) => {
scope.setAttribute("order_id", "ord_789");
Sentry.logger.info("Processing payment", { amount: 49.99 });
// order_id is included on this log only
});Constraint: Scope attributes accept only string, number, and boolean values.
---
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 | "sentry.javascript.browser" |
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, logs are automatically linked to the active span:
Sentry.init({
dsn: "___PUBLIC_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
---
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 in Sentry.init(); requires SDK ≥9.41.0 |
| "Not available via CDN/Loader Script" | Install via npm: npm install @sentry/browser — logging requires the npm package |
logger.fmt not creating message.parameter.* | Use as tagged template: ` Sentry.logger.fmttext ${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/browser ≥10.13.0 |
| Scope attributes not appearing on logs | Upgrade to @sentry/browser ≥10.32.0 for getGlobalScope/getIsolationScope APIs |
| Too many logs / high volume | 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; avoid logging sensitive data at the call site |
| Logs appear but have no user context | Call Sentry.setUser({ id, email }) after authentication; set sendDefaultPii: true |
Browser Profiling — Sentry Browser SDK
Minimum SDK: @sentry/browser ≥10.27.0 (Beta)⚠️ Beta status — breaking changes may occur. Browser support is limited to Chromium-based browsers only (Chrome, Edge). Firefox and Safari are not supported.
---
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 Sentry
Sampling rate: 100Hz (10ms intervals) — runs unobtrusively in production.
---
Browser Compatibility
| Browser | Supported | Notes |
|---|---|---|
| Chrome / Chromium | ✅ Yes | Primary support target |
| Edge (Chromium) | ✅ Yes | Same engine as Chrome |
| Firefox | ❌ No | JS Self-Profiling API not implemented |
| Safari / iOS Safari | ❌ No | JS Self-Profiling API not implemented |
⚠️ Sampling bias: Profile data is collected only from Chromium users. Firefox and Safari sessions are silently excluded — no error is thrown, no overhead is added.
---
Required HTTP Header
Every document response 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:
add_header Document-Policy "js-profiling";---
Basic Setup
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.browserTracingIntegration(),
Sentry.browserProfilingIntegration(),
],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0, // Profile 100% of sessions (lower in production)
});Profiling requires tracing to be active.browserTracingIntegration()and atracesSampleRate> 0 are both required.
---
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
profileSessionSampleRate | number (0–1) | — | Fraction of sessions to profile. Evaluated once per page load. |
profileLifecycle | 'manual' \ | 'trace' | 'manual' |
profilesSampleRate vs profileSessionSampleRate
| Option | SDK Version | Description |
|---|---|---|
profilesSampleRate | Legacy (< 10.27.0) | Transaction-based — tied to individual transaction sampling. Deprecated. |
profileSessionSampleRate | Current (≥ 10.27.0) | Session-based — evaluated once per page load. Use this for all new setups. |
---
Profiling Modes
Trace Mode (Automatic)
Profiler starts and stops automatically in sync with every active root span (trace). Recommended for general-purpose production profiling.
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.browserTracingIntegration(),
Sentry.browserProfilingIntegration(),
],
tracesSampleRate: 1.0,
profileSessionSampleRate: 0.1, // Profile 10% of sessions
profileLifecycle: "trace", // Profile automatically with each trace
});Manual Mode (Default)
Start and stop the profiler explicitly around specific code you want to measure.
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.browserTracingIntegration(),
Sentry.browserProfilingIntegration(),
],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0,
profileLifecycle: "manual", // default
});
// Somewhere in your application
Sentry.uiProfiler.startProfiler();
doExpensiveWork();
renderComplexChart();
Sentry.uiProfiler.stopProfiler();Use manual mode when you know exactly which operations to measure and want to avoid profiling overhead during unrelated work.
---
Production Sampling Strategy
Profiling adds CPU overhead. Use conservative rates in production:
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.browserTracingIntegration(),
Sentry.browserProfilingIntegration(),
],
tracesSampleRate: 0.2, // Sample 20% of traces
profileSessionSampleRate: 0.1, // Profile 10% of sessions
profileLifecycle: "trace",
});profileSessionSampleRate is evaluated once per session (page load), not per trace. A session either profiles all its traces or none of them.
---
Best Practices
- Start with `profileLifecycle: "trace"` — automatic profiling with traces requires no extra instrumentation
- Set `profileSessionSampleRate` to 0.1–0.2 in production to limit overhead
- Upload source maps — profiling data shows minified names without source maps; the flame graph is much more useful with them
- Use manual mode when investigating a specific known bottleneck (e.g., a slow chart render or complex animation)
- Combine with tracing — profiles are always linked to a trace, so you can navigate from a slow span to its flame graph
- Don't profile static hosts without header support — GitHub Pages and some CDNs cannot serve custom HTTP response headers; profiling will silently not work
---
Known Limitations
| Limitation | Details |
|---|---|
| Chromium-only | Firefox and Safari do not implement the JS Self-Profiling API. Profile data represents only Chromium users. |
Document-Policy header required | Every served document must include the header. Static hosts that can't set custom headers cannot enable profiling. |
| Chrome DevTools conflict | With browserProfilingIntegration active, Chrome DevTools may display SDK activity as "profiling overhead" in the Performance panel. This is cosmetic. |
| Beta status | The API may change between minor releases. |
---
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| No profiles in Sentry | Missing Document-Policy: js-profiling header | Add the header to all document responses |
| No profiles in Sentry | browserTracingIntegration() not added | Profiling requires tracing — add it and set tracesSampleRate > 0 |
| No profiles in Sentry | profileSessionSampleRate not set | Set it (e.g., 1.0 for dev, 0.1 for production) |
| Profiles appear with minified names | Source maps not uploaded | Upload source maps to Sentry via the build plugin |
| No profiles for Firefox/Safari users | Expected — those browsers don't support the API | No fix needed; this is by design |
| Chrome DevTools shows extra overhead | False positive from profiling integration | Expected; ignore in DevTools, check Sentry instead |
uiProfiler.startProfiler is undefined | SDK version < 10.27.0 or wrong profileLifecycle | Upgrade SDK; uiProfiler is only available in manual mode |
Session Replay — Sentry Browser SDK
Minimum SDK: @sentry/browser ≥7.27.0replayCanvasIntegration available since SDK ≥7.50.0beforeAddRecordingEvent available since SDK ≥7.53.0beforeErrorSampling available since SDK ≥7.56.0Node 12+ required; browsers newer than IE11
---
Basic Setup
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_DSN___",
replaysSessionSampleRate: 0.1, // 10% of all sessions recorded in full
replaysOnErrorSampleRate: 1.0, // 100% of sessions with errors buffered and sent
integrations: [Sentry.replayIntegration()],
});---
Sampling Rates
replaysSessionSampleRate vs. replaysOnErrorSampleRate
| Option | Default | Behavior |
|---|---|---|
replaysSessionSampleRate | 0 | Percentage of sessions to record in full from start to end. 1.0 = 100%, 0 = none. |
replaysOnErrorSampleRate | 0 | Percentage of sessions to record when an error occurs. Buffers up to 60 seconds before the error, then continues until the session ends. |
How Sampling Works
1. replaysSessionSampleRate is checked first at session start.
- If sampled → full session recording starts immediately, sent to Sentry in real-time chunks (Session mode).
- If not sampled → recording is buffered in memory (last 60 seconds only) (Buffer mode).
2. If an error occurs in a buffered session:
replaysOnErrorSampleRateis checked.- If sampled → 60-second buffer + rest of session is sent to Sentry.
- If not sampled → buffer is discarded.
When data leaves the browser:
| Scenario | Data Sent |
|---|---|
| Selected for session sampling | Immediately (real-time chunks) |
| Not selected, no error | Never (buffer discarded) |
| Not selected, error occurs and sampled | After error (60s buffer + everything after) |
Recommended rates by traffic volume:
| Traffic | replaysSessionSampleRate | replaysOnErrorSampleRate |
|---|---|---|
| High (100k+/day) | 0.01 (1%) | 1.0 |
| Medium (10k–100k/day) | 0.1 (10%) | 1.0 |
| Low (<10k/day) | 0.25 (25%) | 1.0 |
Tip: KeepreplaysOnErrorSampleRateat1.0— error sessions provide the most debugging value.
Dev tip: Set replaysSessionSampleRate: 1.0 during development to capture every session.---
replayIntegration() — Configuration Reference
General Options
| Option | Type | Default | Description |
|---|---|---|---|
stickySession | boolean | true | Track the user across page refreshes. Closing a tab ends the session; multiple tabs = multiple sessions. |
mutationLimit | number | 10000 | Upper bound of DOM mutations before replay stops recording (protects performance). |
mutationBreadcrumbLimit | number | 750 | Threshold at which a breadcrumb warning is emitted for large mutations. |
minReplayDuration | number | 5000 (ms) | Minimum replay length before sending. Max configurable: 15000ms. |
maxReplayDuration | number | 3600000 (ms = 1 hr) | Maximum replay length. Max value: 3600000ms. |
workerUrl | string | undefined | URL for a self-hosted compression worker (avoids CSP issues, reduces bundle size). |
beforeAddRecordingEvent | `(event) => event \ | null` | identity fn |
beforeErrorSampling | (event) => boolean | () => true | In buffer mode only — return false to skip error-based sampling for a specific error event. |
slowClickIgnoreSelectors | string[] | [] | CSS selectors for elements where slow/rage click detection should be disabled. |
Privacy Options
| Option | Type | Default | Description |
|---|---|---|---|
maskAllText | boolean | true | Mask all text content (replaced with * characters). |
maskAllInputs | boolean | true | Mask all <input> element values. |
blockAllMedia | boolean | true | Block all media: img, svg, video, object, picture, embed, map, audio. |
mask | string[] | [".sentry-mask", "[data-sentry-mask]"] | Additional CSS selectors to mask. Appended to defaults. |
unmask | string[] | [] | CSS selectors to unmask (overrides maskAllText). |
block | string[] | [".sentry-block", "[data-sentry-block]"] | Additional CSS selectors to block (replaced with same-size empty placeholder). |
unblock | string[] | [] | CSS selectors to unblock (overrides blockAllMedia). |
ignore | string[] | [".sentry-ignore", "[data-sentry-ignore]"] | Input fields whose events are ignored (no keystroke recording). |
maskFn | (text: string) => string | (s) => "*".repeat(s.length) | Custom text masking function. |
Network Options
| Option | Type | Default | Description |
|---|---|---|---|
networkDetailAllowUrls | `(string \ | RegExp)[]` | [] |
networkDetailDenyUrls | `(string \ | RegExp)[]` | [] |
networkCaptureBodies | boolean | true | Whether to capture request/response bodies for allowed URLs. |
networkRequestHeaders | string[] | [] | Additional request headers to capture. Default captured: Content-Type, Content-Length, Accept. |
networkResponseHeaders | string[] | [] | Additional response headers to capture. |
---
Privacy & Masking
Three Privacy Methods
| Method | Effect | Default Trigger |
|---|---|---|
| Mask | Replaces text with * characters (preserving length) | .sentry-mask, [data-sentry-mask] |
| Block | Replaces entire element with same-size empty placeholder | .sentry-block, [data-sentry-block] |
| Ignore | Stops recording input events on matched fields | .sentry-ignore, [data-sentry-ignore] |
HTML Attribute Usage
<!-- Mask text content -->
<p class="sentry-mask">Sensitive user data</p>
<p data-sentry-mask>Sensitive user data</p>
<!-- Block entire element (credit card form, PII section) -->
<div class="sentry-block">
<input type="text" placeholder="Credit card number" />
</div>
<div data-sentry-block>Blocked content</div>
<!-- Ignore input events (no keystrokes recorded) -->
<input class="sentry-ignore" type="password" />
<input data-sentry-ignore type="text" placeholder="SSN" />
<!-- Unmask when maskAllText=true -->
<p class="sentry-unmask">Safe to show in replay</p>
<p data-sentry-unmask>Safe to show</p>
<!-- Unblock media when blockAllMedia=true -->
<img class="sentry-unblock" src="product-image.png" alt="Product" />
<img data-sentry-unblock src="logo.svg" alt="Logo" />Configuration Examples
Disable all default masking (show everything):
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
});Mask and block specific selectors:
Sentry.replayIntegration({
mask: [".user-pii", "[data-sensitive]", "#account-details"],
unmask: [".replay-safe"],
block: ["#payment-form", ".credit-card-widget"],
unblock: [".product-image"],
ignore: [".password-field", "[type='password']"],
});Custom masking function:
Sentry.replayIntegration({
maskFn: (text) => text.replace(/\S/g, "X"), // Replace non-whitespace with X
});Custom recording event filter:
Sentry.replayIntegration({
beforeAddRecordingEvent: (event) => {
// Drop any event tagged "foo"
if (event.data.tag === "foo") return null;
// Only capture network events for 500 errors
if (
event.data.tag === "performanceSpan" &&
(event.data.payload.op === "resource.fetch" ||
event.data.payload.op === "resource.xhr") &&
event.data.payload.data.statusCode !== 500
) {
return null;
}
return event;
},
});---
Network Capture
By default, Session Replay captures basic information about all outgoing fetch and XHR requests (URL, size, method, status code).
Request/response bodies and additional headers require explicit opt-in (SDK ≥7.50.0):
Sentry.replayIntegration({
networkDetailAllowUrls: [window.location.origin],
});Advanced — multiple patterns with custom headers:
Sentry.replayIntegration({
networkDetailAllowUrls: [
window.location.origin,
"api.example.com",
/^https:\/\/api\.example\.com/,
],
networkCaptureBodies: true, // default: true — capture request/response bodies
networkRequestHeaders: ["Cache-Control", "X-Request-ID"],
networkResponseHeaders: ["Referrer-Policy", "X-Trace-ID"],
});Constraints:
- Bodies are truncated to 150,000 characters maximum
networkDetailDenyUrlstakes precedence overnetworkDetailAllowUrls- Set
networkCaptureBodies: falseto keep header capture while disabling body capture
---
Canvas Recording
Canvas elements are not captured by default. Add replayCanvasIntegration() to enable:
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_DSN___",
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.replayIntegration(),
Sentry.replayCanvasIntegration(),
],
});⚠️ There is currently no PII scrubbing in canvas recordings. Review canvas content carefully before enabling.
3D / WebGL Canvases — Manual Snapshot Mode
For WebGL or 3D canvases, use manual snapshotting to optimize performance:
Sentry.replayCanvasIntegration({
enableManualSnapshot: true,
});
// Call in your render loop to capture the canvas
function paint() {
const canvasRef = document.querySelector("#my-canvas");
Sentry.getClient()
?.getIntegrationByName("ReplayCanvas")
?.snapshot(canvasRef);
}WebGPU Canvases
Sentry.replayCanvasIntegration({
enableManualSnapshot: true,
});
function paint() {
const canvasRef = document.querySelector("#my-canvas");
const canvasIntegration =
Sentry.getClient()?.getIntegrationByName("ReplayCanvas");
canvasIntegration?.snapshot(canvasRef, {
skipRequestAnimationFrame: true,
});
}---
Lazy Loading
Defer loading the Replay bundle to avoid impacting initial page load:
// Initialize Sentry without Replay
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [],
});
// Lazy-load Replay later (e.g., after user interaction or route change)
import("@sentry/browser").then((lazySentry) => {
Sentry.addIntegration(lazySentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}));
});For Loader Script users (CDN), use lazyLoadIntegration:
window.sentryOnLoad = function () {
Sentry.init({ dsn: "___PUBLIC_DSN___" });
Sentry.lazyLoadIntegration("replayIntegration")
.then((replayIntegration) => {
Sentry.addIntegration(replayIntegration());
})
.catch(() => {
// Network error — Replay not enabled
});
};---
Session Modes & Manual Control
Session Initialization Modes
| Configuration | Mode | Behavior |
|---|---|---|
replaysSessionSampleRate > 0 and sampled | Session mode | Records continuously; uploads data in real time |
Not sampled, replaysOnErrorSampleRate > 0 | Buffer mode | Records but keeps only last 60 seconds in memory (~2–5 MB) |
Both rates = 0, or integration added without rates | Inactive | Nothing recorded until manually started |
Session mode: sessions end after 15 minutes of inactivity or 60 minutes maximum duration, then reinitialize.
Buffer mode: stores ~2–5 MB in memory (lightweight DOM event logs: clicks, scrolls, mutations — not video files). On sampled error, the 60-second buffer + subsequent recording are uploaded.
---
Manual Session Control API
Sentry.init({
dsn: "___PUBLIC_DSN___",
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
integrations: [Sentry.replayIntegration()],
});
const replay = Sentry.getReplay();
replay.start(); // Start recording in session mode
replay.startBuffering(); // Start recording in buffer mode
await replay.flush(); // Upload pending data (keeps recording active)
await replay.stop(); // Flush data and end session permanently
const replayId = replay.getReplayId(); // Get current replay ID for external linking---
Deferred Initialization (External Sampling Service)
Use when you want to determine sampling rates via an external feature flag service before starting the SDK:
async function initReplay(sessionSampleRate, errorSampleRate) {
const client = Sentry.getClient();
const options = client.getOptions();
options.replaysSessionSampleRate = sessionSampleRate;
options.replaysOnErrorSampleRate = errorSampleRate;
const replay = Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
});
client.addIntegration(replay);
}
// Call after fetching remote config
fetchFeatureFlags().then((flags) => {
initReplay(flags.replaySessionRate, flags.replayErrorRate);
});---
Custom Sampling Patterns
Employee-only recordings:
Sentry.init({
dsn: "___PUBLIC_DSN___",
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [Sentry.replayIntegration()],
});
// Force-flush replay for internal employees
if (loggedInUser.isEmployee) {
const replay = Sentry.getReplay();
replay.flush();
}URL-specific recording:
Sentry.init({
dsn: "___PUBLIC_DSN___",
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
integrations: [Sentry.replayIntegration()],
});
navigation.addEventListener("navigate", (event) => {
const url = new URL(event.destination.url);
const replay = Sentry.getReplay();
if (url.pathname.startsWith("/checkout/")) {
replay.start();
} else {
replay.stop();
}
});Error filtering in buffer mode:
Sentry.replayIntegration({
beforeErrorSampling: (event) => {
// Skip replay capture for this specific error type
return !event.exception?.values?.[0]?.value?.includes("drop me");
},
});---
Support Widget Integration
Link replay sessions to support tickets:
Sentry.init({
dsn: "___PUBLIC_DSN___",
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 0.5,
integrations: [Sentry.replayIntegration()],
});
MySupportWidget.on("open", async () => {
const replay = Sentry.getReplay();
await replay.flush();
const replayId = replay.getReplayId();
MySupportWidget.setTag("replayId", replayId);
// Replay URL format:
// https://<org-slug>.sentry.io/replays/<replay-id>/
});---
Performance Impact
Buffer Mode is Lightweight
Buffer mode stores ~2–5 MB in memory — these are DOM event logs (clicks, scrolls, mutations), not video files. The real-time encoding is handled by a WebWorker.
Mutation Limits
Protect against performance degradation from excessive DOM mutations:
Sentry.replayIntegration({
mutationBreadcrumbLimit: 1000, // Emit breadcrumb warning at this threshold
mutationLimit: 1500, // Stop recording at this threshold
});Custom Compression Worker
Reduces bundle size and avoids CSP violations by self-hosting the compression worker:
Sentry.replayIntegration({
workerUrl: "/assets/sentry-replay-worker.min.js",
});Bundler plugin optimization (excludes worker from main bundle):
sentryVitePlugin({
bundleSizeOptimizations: {
excludeReplayWorker: true,
},
});Content Security Policy
Session Replay uses a WebWorker for compression. Add to your CSP:
worker-src 'self' blob:;
child-src 'self' blob:;Safari ≤15.4 requireschild-src. Use a self-hostedworkerUrlas an alternative.
---
Best Practices
- Keep `replaysOnErrorSampleRate` at `1.0` — error sessions are the highest value for debugging.
- Use `maskAllText: true` in production — default behavior, protects PII. Only disable for internal tools.
- Opt-in to network details explicitly — set
networkDetailAllowUrlsonly for your own API origins, not third-party services. - Never enable `replayCanvasIntegration` without reviewing canvas content — there is no automatic PII scrubbing for canvas.
- Test masking before deploying — use
replaysSessionSampleRate: 1.0in staging and review replays to verify PII is hidden. - Use `workerUrl` to self-host the compression worker if you have strict CSP or want to reduce bundle size.
- Use `beforeAddRecordingEvent` to filter out high-frequency recording events that don't add debugging value.
- Set `minReplayDuration` to avoid sending trivially short sessions (default: 5s is usually fine).
---
Troubleshooting
| Issue | Solution |
|---|---|
| Replay not recording | Check that replaysSessionSampleRate or replaysOnErrorSampleRate is > 0. Confirm replayIntegration() is in the integrations array. |
| CSP errors blocking worker | Add worker-src 'self' blob:; child-src 'self' blob:; to your CSP, or use workerUrl to self-host the worker. |
| Replay stops after mutation spike | The mutationLimit (default: 10000) was hit. Increase it or reduce DOM mutation frequency in your app. |
| Sensitive data visible in replays | Add .sentry-mask / .sentry-block to elements, or use mask/block selector options. Verify with replaysSessionSampleRate: 1.0 in staging. |
| Canvas not recorded | Add replayCanvasIntegration() alongside replayIntegration(). Requires SDK ≥7.50.0. |
| Network request bodies not captured | Set networkDetailAllowUrls to include your API origin. Bodies are opt-in by default. |
| Replay ID unavailable | Call replay.getReplayId() only after replay.start() or replay.startBuffering() has been called. |
| Error replays missing the 60-second buffer | Ensure replaysOnErrorSampleRate > 0 and the replay integration is initialized before the error occurs. |
| Large bundle size from replay | Use the bundler plugin option excludeReplayWorker: true and self-host the worker via workerUrl. |
beforeErrorSampling not firing | Only runs in buffer mode (when the session was not selected for full session recording). |
| Safari CSP issues | Safari ≤15.4 requires child-src in addition to worker-src. Use workerUrl as an alternative. |
Tracing — Sentry Browser SDK
Minimum SDK: @sentry/browser ≥7.0.0enableInpdefaults totrueas of SDK ≥8.0.0 (wasfalsein 7.x)
enableLongAnimationFrame available since SDK ≥8.18.0inheritOrSampleWithintracesSampleravailable since SDK ≥9.0.0
profileSessionSampleRatereplacesprofilesSampleRateas of SDK ≥10.27.0
---
Minimal Setup
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 0.2, // Capture 20% of all transactions
});Disabling tracing: Omit bothtracesSampleRateandtracesSampler. SettingtracesSampleRate: 0does not disable tracing — it simply never sends any traces.
---
browserTracingIntegration() — Configuration Reference
All options are passed as a single object to browserTracingIntegration().
Page Load & Navigation
| Option | Type | Default | Description |
|---|---|---|---|
instrumentPageLoad | boolean | true | Create a pageload root span on initial page load |
instrumentNavigation | boolean | true | Create a navigation root span on client-side history changes |
markBackgroundSpan | boolean | true | Mark pageload/navigation spans as cancelled when the tab goes to the background |
enableReportPageLoaded | boolean | false | Enable the Sentry.reportPageLoaded() utility function (SDK ≥10.13.0) |
linkPreviousTrace | `"in-memory" \ | "session-storage" \ | false` |
HTTP Request Instrumentation
| Option | Type | Default | Description |
|---|---|---|---|
traceFetch | boolean | true | Automatically create spans for outgoing fetch requests |
traceXHR | boolean | true | Automatically create spans for outgoing XMLHttpRequest calls |
enableHTTPTimings | boolean | true | Attach detailed HTTP timing data via the Performance Resource Timing API |
shouldCreateSpanForRequest | (url: string) => boolean | — | Predicate to exclude specific requests from tracing (e.g., health checks) |
onRequestSpanStart | (span, fetchInput, fetchInit) => void | — | Callback invoked when a span is started for an outgoing fetch/XHR request |
Interaction & Long Task Instrumentation
| Option | Type | Default | Description |
|---|---|---|---|
enableInp | boolean | true (8.x+), false (7.x) | Capture Interaction to Next Paint (INP) events |
interactionsSampleRate | number | 1.0 | Additional sampling rate for INP spans (applied on top of tracesSampleRate) |
enableLongTask | boolean | true | Create spans for main-thread blocking tasks exceeding 50 ms |
enableLongAnimationFrame | boolean | true | Create spans for long animation frames (SDK ≥8.18.0) |
Timing & Timeouts
| Option | Type | Default | Description |
|---|---|---|---|
idleTimeout | number | 1000 | Milliseconds of inactivity before pageload/navigation span auto-finishes |
finalTimeout | number | 30000 | Maximum lifespan (ms) for any root span regardless of activity |
childSpanTimeout | number | 15000 | Maximum time (ms) a child span may remain open before the parent can finish |
Propagation & Filtering
| Option | Type | Default | Description |
|---|---|---|---|
tracePropagationTargets | `Array<string \ | RegExp>` | ["localhost", /^\//] |
beforeStartSpan | (context: SpanContext) => SpanContext | — | Modify or enrich a span's context before it is created |
ignoreResourceSpans | Array<string> | [] | Suppress automatic spans by operation category (e.g., "resource.css") |
ignorePerformanceApiSpans | `Array<string \ | RegExp>` | [] |
Full Configuration Example
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.browserTracingIntegration({
// Page / navigation spans
instrumentPageLoad: true,
instrumentNavigation: true,
markBackgroundSpan: true,
linkPreviousTrace: "in-memory",
// HTTP spans
traceFetch: true,
traceXHR: true,
enableHTTPTimings: true,
shouldCreateSpanForRequest: (url) => !url.match(/\/health\/?$/),
// INP / long-task spans
enableInp: true,
interactionsSampleRate: 0.5,
enableLongTask: true,
enableLongAnimationFrame: true,
// Timeouts
idleTimeout: 1000,
finalTimeout: 30000,
childSpanTimeout: 15000,
// Propagation
tracePropagationTargets: ["localhost", /^https:\/\/api\.yourapp\.com/],
// Normalise dynamic URL segments in transaction names
beforeStartSpan: (context) => ({
...context,
name: location.pathname
.replace(/\/[a-f0-9]{32}/g, "/<hash>")
.replace(/\/\d+/g, "/<id>"),
}),
}),
],
tracesSampleRate: 1.0,
});---
Automatic Instrumentation
When browserTracingIntegration() is active, the following are captured automatically:
Page Loads
A root pageload span covers the full page-load lifecycle. Child spans are attached for:
- Web Vitals: LCP, CLS, TTFB
- Resource loads: CSS, JS, images, fonts (each as a
resource.*child span) - HTTP requests made during load
Navigations (SPA Route Changes)
Each client-side route change (via the History API) produces a new navigation root span, along with any HTTP requests and web vitals captured during that navigation.
Fetch / XHR Requests
Every outgoing fetch or XMLHttpRequest produces an http.client child span containing: request duration, HTTP status code, and URL.
Use shouldCreateSpanForRequest to exclude URLs you don't want traced:
Sentry.browserTracingIntegration({
shouldCreateSpanForRequest: (url) => {
return !url.includes("/health") && !url.includes("/metrics");
},
});Web Vitals
| Metric | Description | Auto-captured |
|---|---|---|
| LCP — Largest Contentful Paint | Perceived load speed | ✅ Always |
| CLS — Cumulative Layout Shift | Visual stability | ✅ Always |
| TTFB — Time to First Byte | Server responsiveness | ✅ Always |
| INP — Interaction to Next Paint | Responsiveness to user inputs | ✅ (SDK ≥8.x) |
Long Tasks
Main-thread tasks blocking the browser for more than 50 ms are recorded as ui.long-task child spans.
Custom Router Integration
To integrate with a router that manages its own history, disable automatic span creation and call the low-level helpers directly:
const client = Sentry.init({
integrations: [
Sentry.browserTracingIntegration({
instrumentNavigation: false,
instrumentPageLoad: false,
}),
],
});
// Initial page load
let pageLoadSpan = Sentry.startBrowserTracingPageLoadSpan(client, {
name: window.location.pathname,
attributes: {
[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "url",
},
});
myRouter.on("routeChange", (route) => {
if (pageLoadSpan) {
// Update the name of the in-flight page-load span
pageLoadSpan.updateName(route.name);
pageLoadSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, "route");
pageLoadSpan = undefined;
} else {
// Start a navigation span for subsequent route changes
Sentry.startBrowserTracingNavigationSpan(client, {
op: "navigation",
name: route.name,
attributes: {
[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: "route",
},
});
}
});---
Custom Spans
Three functions are available for manual instrumentation. All accept the same options object.
startSpan(options, callback) — Auto-ending Span (Recommended)
Creates an active span that ends automatically when the callback returns (sync or async).
// Synchronous
const result = Sentry.startSpan({ name: "process-checkout", op: "function" }, () => {
return processCheckoutData();
});
// Asynchronous
const data = await Sentry.startSpan(
{ name: "fetch-user-profile", op: "http.client" },
async () => {
const response = await fetch("/api/user/profile");
return response.json();
}
);
// With attributes
const result = await Sentry.startSpan(
{
name: "query-products",
op: "db",
attributes: {
"db.system": "postgresql",
"db.table": "products",
"db.query.count": 50,
},
},
() => db.query("SELECT * FROM products LIMIT 50")
);---
startSpanManual(options, callback) — Manually-ended Active Span
Creates an active span that must be ended explicitly by calling span.end(). Use when the span's end is decoupled from the callback's return (e.g., event-driven code).
function attachUploadTracing(input) {
input.addEventListener("change", (event) => {
Sentry.startSpanManual({ name: "file-upload", op: "file.upload" }, (span) => {
const file = event.target.files[0];
span.setAttribute("file.size", file.size);
span.setAttribute("file.type", file.type);
const upload = uploadFile(file);
upload.on("complete", () => {
span.setStatus({ code: 1 }); // ok
span.end();
});
upload.on("error", (err) => {
span.setStatus({ code: 2 }); // error
span.end();
});
});
});
}---
startInactiveSpan(options) — Manually-ended Inactive Span
Creates a span that is not set as the active span. Useful for parallel work sharing a common parent.
const span1 = Sentry.startInactiveSpan({ name: "task-a", op: "function" });
const span2 = Sentry.startInactiveSpan({ name: "task-b", op: "function" });
await Promise.all([workA(), workB()]);
span1.end();
span2.end();---
Span Options
| Option | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Human-readable identifier shown in the Sentry UI |
op | string | — | Operation type for categorization (see Operation Types below) |
startTime | number | — | Custom Unix timestamp (seconds, sub-second precision) for span start |
attributes | `Record<string, string \ | number \ | boolean \ |
parentSpan | Span | — | Explicitly designate a parent span instead of using the active span |
onlyIfParent | boolean | — | If true, the span is a no-op when there is no active parent span |
forceTransaction | boolean | — | Force this span to appear as a root transaction in the Sentry UI |
---
Operation Types
Use well-known op values so the Sentry UI presents appropriate icons and filtering:
op Value | Use Case |
|---|---|
http.client | Outgoing HTTP requests |
db | Database queries |
db.system | Database system operations |
ui.click | User click interactions |
ui.long-task | Long-running main-thread tasks |
navigation | Client-side route transitions |
pageload | Initial full page load |
resource.script | Script resource load |
resource.css | CSS resource load |
resource.img | Image resource load |
function | Generic function calls |
file.upload | File upload operations |
---
Working with Span Attributes and Status
// Set attributes at creation
Sentry.startSpan(
{
name: "process-payment",
attributes: { "payment.provider": "stripe", "payment.amount": 9999 },
},
() => processPayment()
);
// On an existing span
const span = Sentry.getActiveSpan();
if (span) {
span.setAttribute("key", "value");
span.setAttributes({ key1: "val1", key2: 42 });
}
// Update span name (SDK ≥8.47.0)
Sentry.updateSpanName(span, "New Name");
// Set span status
span.setStatus({ code: 1 }); // 0 = unknown, 1 = ok, 2 = error
span.setHttpStatus(404);---
Distributed Tracing
Distributed tracing connects browser activity to backend requests, enabling a single timeline across services.
How It Works
Sentry propagates two HTTP headers on every outgoing request matching tracePropagationTargets:
| Header | Contents |
|---|---|
sentry-trace | Trace ID, parent span ID, and sampling decision flag |
baggage | Dynamic sampling context: trace ID, public key, sample rate, environment |
CORS: Both headers must be added to your server's Access-Control-Allow-Headers — otherwise browsers or gateways will strip them.tracePropagationTargets Configuration
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1.0,
tracePropagationTargets: [
"localhost",
/^https:\/\/api\.yourapp\.com/,
],
});Rules:
- String entries = exact substring match against the full URL
- RegExp entries = tested against the full URL (including scheme and port)
- Port numbers matter — a service on port 8080 requires a separate entry
- Set to
[]to disable header propagation entirely
Common patterns:
// E-commerce with multiple backend services
tracePropagationTargets: [
"https://api.myecommerce.com",
"https://auth.myecommerce.com",
];
// Mixed absolute URLs and relative API paths
tracePropagationTargets: [
"https://api.myapp.com",
/^\/api\//,
];
// Disable all header propagation
tracePropagationTargets: [];---
Continuing a Server-Initiated Trace
When your server renders the HTML, emit the current trace context as <meta> tags. The browserTracingIntegration reads them automatically on page load and continues the same trace:
<meta name="sentry-trace"
content="12345678901234567890123456789012-1234567890123456-1" />
<meta name="baggage"
content="sentry-trace_id=12345678901234567890123456789012,sentry-environment=production,sentry-sample_rate=1" />---
Manual Trace Propagation (Non-HTTP Channels)
For WebSockets, message queues, or any non-HTTP transport:
const traceData = Sentry.getTraceData();
webSocket.send(
JSON.stringify({
payload: myData,
metadata: {
sentryTrace: traceData["sentry-trace"],
baggage: traceData["baggage"],
},
})
);---
Sampling
tracesSampleRate — Uniform Sampling
Sentry.init({
tracesSampleRate: 0.2, // Sample 20% of transactions
});- Range:
0–1(random, uniform percentage) 0= send no traces (tracing is still "active"; omit both options to fully disable)1= send 100% of traces
tracesSampler — Dynamic / Context-Aware Sampling
A function that receives a SamplingContext and returns a sample rate (0–1) or a boolean.
Sentry.init({
tracesSampler: ({ name, attributes, inheritOrSampleWith }) => {
// Never sample health checks
if (name.includes("healthcheck") || name.includes("/health")) return 0;
// Always sample authentication flows
if (name.includes("auth") || name.includes("login")) return 1;
// Sample checkout at high rate (business critical)
if (name.includes("checkout")) return 0.5;
// For everything else, inherit the parent's decision or default to 20%
return inheritOrSampleWith(0.2);
},
});SamplingContext Properties
| Property | Type | Description |
|---|---|---|
name | string | The span's initial name |
attributes | Record<string, unknown> | Initial span attributes |
parentSampled | `boolean \ | undefined` |
parentSampleRate | `number \ | undefined` |
inheritOrSampleWith(rate) | function | (SDK ≥9) Returns parentSampled if defined, otherwise uses rate |
Sampling Precedence
1. `tracesSampler` — highest priority (if defined) 2. Parent sampling decision — used when no sampler is defined but a parent trace exists 3. `tracesSampleRate` — fallback uniform rate
INP-Specific Sampling
interactionsSampleRate applies an additional multiplier on top of tracesSampleRate for INP interaction spans:
Sentry.init({
tracesSampleRate: 0.5,
integrations: [
Sentry.browserTracingIntegration({
enableInp: true,
interactionsSampleRate: 0.1, // Effective rate: 50% × 10% = 5% of interactions
}),
],
});---
Best Practices
- Set `tracePropagationTargets` explicitly — the default (
localhost+/) is rarely correct for production. Define your API origins precisely. - Use `beforeStartSpan` to normalize transaction names — avoid high cardinality from dynamic URLs (
/users/123→/users/<id>). - Use `shouldCreateSpanForRequest` to exclude noise — skip health checks, analytics pixels, and third-party beacons.
- Prefer `startSpan` over `startInactiveSpan` — the auto-ending behavior prevents runaway spans.
- Set `op` on custom spans — the Sentry UI uses
opfor icons, grouping, and performance charts. - Use `inheritOrSampleWith` in `tracesSampler` — ensures sampling decisions are deterministically propagated from parent to child traces.
- Add CORS headers on your servers —
sentry-traceandbaggagemust be inAccess-Control-Allow-HeadersandAccess-Control-Expose-Headers. - Tune `idleTimeout` for your SPA — if your navigation transitions are slow (>1s), increase
idleTimeoutto avoid premature span termination.
---
Troubleshooting
| Issue | Solution |
|---|---|
| Transactions not appearing in Sentry | Ensure tracesSampleRate > 0 (or tracesSampler returns a value > 0). Check that browserTracingIntegration() is in the integrations array. |
Missing sentry-trace / baggage headers on requests | Check tracePropagationTargets — the request URL must match an entry. Also verify CORS headers allow these. |
| Transaction names showing raw URLs with IDs | Use beforeStartSpan to normalize dynamic URL segments. |
| Distributed trace not connecting to backend | Ensure the backend SDK reads sentry-trace and baggage headers. Add them to Access-Control-Allow-Headers. |
pageload span ends too early | Increase idleTimeout (default: 1000ms) or finalTimeout (default: 30000ms). |
| INP spans not appearing | Requires SDK ≥8.0.0 (enabled by default). In SDK 7.x set enableInp: true explicitly. |
| Too many transactions overwhelming quota | Use tracesSampler to sample high-volume routes at a lower rate. Drop health checks entirely (return 0). |
| Parallel spans showing wrong parent | Use startInactiveSpan with explicit parentSpan option to control hierarchy. |
beforeSendTransaction not called | Ensure you're returning from beforeSend correctly — beforeSendTransaction is a separate hook for transactions only. |
| Long tasks not captured | Ensure enableLongTask: true (default). Long animation frames require SDK ≥8.18.0 and enableLongAnimationFrame: true. |
User Feedback — Sentry Browser SDK
Minimum SDK:@sentry/browser≥7.85.0 forfeedbackIntegration()
Screenshot capture: requires ≥8.0.0
Self-hosted Sentry: requires version ≥24.4.2
---
Two Approaches
| Approach | When to Use |
|---|---|
| `feedbackIntegration()` widget | Collect feedback anywhere — no error required; embeds a button in the UI |
| `showReportDialog()` crash modal | Triggered after an error is captured; prompts the user to describe what happened |
Both approaches can be used together. The widget is general-purpose; the crash modal is specifically for error-linked feedback.
---
Approach 1: Feedback Widget (feedbackIntegration)
Basic Setup
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.feedbackIntegration({
colorScheme: "system", // "light" | "dark" | "system"
}),
],
});A "Report a Bug" button appears in the bottom-right corner by default. Clicking it opens a modal form.
Lazy Loading via Loader Script
window.sentryOnLoad = function () {
Sentry.init({ dsn: "___PUBLIC_DSN___" });
Sentry.lazyLoadIntegration("feedbackIntegration")
.then((feedbackIntegration) => {
Sentry.addIntegration(
feedbackIntegration({
colorScheme: "system",
}),
);
})
.catch(() => {
// Network error — User Feedback widget not loaded
});
};CDN Bundle Options
<!-- Feedback only (lightest bundle) -->
<script
src="https://browser.sentry-cdn.com/10.42.0/bundle.feedback.min.js"
crossorigin="anonymous"
></script>
<!-- With tracing and replay -->
<script
src="https://browser.sentry-cdn.com/10.42.0/bundle.tracing.replay.feedback.min.js"
crossorigin="anonymous"
></script>Configuration Options
Appearance
| Option | Type | Default | Description |
|---|---|---|---|
colorScheme | 'light' \ | 'dark' \ | 'system' |
buttonLabel | string | "Report a Bug" | Text on the trigger button. |
submitButtonLabel | string | "Send Bug Report" | Text on the form's submit button. |
cancelButtonLabel | string | "Cancel" | Text on the cancel button. |
formTitle | string | "Report a Bug" | Title displayed in the feedback form. |
showBranding | boolean | true | Show "Powered by Sentry" branding in the widget. |
Form Fields
| Option | Type | Default | Description |
|---|---|---|---|
showName | boolean | true | Show the name input field. |
showEmail | boolean | true | Show the email input field. |
isNameRequired | boolean | false | Make name field required. |
isEmailRequired | boolean | false | Make email field required. |
namePlaceholder | string | "Your Name" | Placeholder for name field. |
emailPlaceholder | string | "your.email@example.org" | Placeholder for email field. |
messagePlaceholder | string | "What's the bug? ..." | Placeholder for message field. |
Positioning
| Option | Type | Default | Description |
|---|---|---|---|
position | 'bottom-right' \ | 'bottom-left' \ | 'top-right' \ |
Behaviour
| Option | Type | Default | Description |
|---|---|---|---|
autoInject | boolean | true | Automatically inject the trigger button into the DOM. Set false to control placement manually. |
enableScreenshot | boolean | true | Allow users to attach a screenshot. Requires SDK ≥8.0.0. |
tags | Record<string, string> | {} | Additional tags to attach to every submitted feedback event. |
Pre-fill User Information
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.feedbackIntegration({
// Pre-fill form with logged-in user's details
}),
],
});
// After authentication
Sentry.setUser({
email: "user@example.com",
name: "Jane Smith",
});When Sentry.setUser() is called, the feedback form auto-populates name and email fields.
Manual Widget Control (Custom Button)
Disable auto-inject and open the widget programmatically from your own button:
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.feedbackIntegration({
autoInject: false, // Don't render Sentry's trigger button
}),
],
});
// Open the widget from your own UI element
document.getElementById("my-feedback-btn").addEventListener("click", () => {
const feedback = Sentry.getFeedback();
if (feedback) {
feedback.openDialog();
}
});---
Approach 2: Programmatic Feedback (captureFeedback)
Use a completely custom form UI and submit feedback via the API:
// Minimal — only message is required
Sentry.captureFeedback({
name: "Jane Smith",
email: "jane@example.com",
message: "The checkout button doesn't work on mobile.",
});With Tags and Attachments
// Attach a screenshot as a file
const screenshotDataUrl = "data:image/jpeg;base64,...";
const res = await fetch(screenshotDataUrl);
const buffer = await res.arrayBuffer();
Sentry.captureFeedback(
{
name: "Jane Smith",
email: "jane@example.com",
message: "The checkout button doesn't work on mobile.",
},
{
captureContext: {
tags: { page: "checkout", device: "mobile" },
},
attachments: [
{
filename: "screenshot.png",
data: new Uint8Array(buffer),
},
],
},
);Linking Feedback to a Specific Error
try {
await submitOrder();
} catch (err) {
const eventId = Sentry.captureException(err);
Sentry.captureFeedback({
message: "Something went wrong during checkout.",
associatedEventId: eventId, // Links this feedback to the captured error
});
}captureFeedback Parameters
| Parameter | Required | Description |
|---|---|---|
message | ✅ | The feedback text from the user |
name | ❌ | User's name |
email | ❌ | User's email |
associatedEventId | ❌ | Links feedback to a specific Sentry event (use Sentry.lastEventId() or the return value of captureException) |
---
Approach 3: Crash-Report Modal (showReportDialog)
Show a modal prompting users to describe what happened when an error occurs. Ideal for "something went wrong" pages or after unhandled errors.
Basic Setup
Sentry.init({
dsn: "___PUBLIC_DSN___",
beforeSend(event) {
if (event.exception && event.event_id) {
Sentry.showReportDialog({ eventId: event.event_id });
}
return event;
},
});On a 500 Error Page
<script>
Sentry.init({ dsn: "___PUBLIC_DSN___" });
</script>
<script>
// eventId is provided by your server-side Sentry SDK after capturing the error
Sentry.showReportDialog({ eventId: "{{ sentry_event_id }}" });
</script>After Manual captureException
try {
await riskyOperation();
} catch (err) {
const eventId = Sentry.captureException(err);
Sentry.showReportDialog({ eventId });
}showReportDialog Options
| Option | Required | Description |
|---|---|---|
eventId | ✅ | The Sentry event ID to associate the feedback with |
user.name | ❌ | Pre-fill user's name |
user.email | ❌ | Pre-fill user's email |
lang | ❌ | Dialog language code (e.g., "de", "fr") |
title | ❌ | Override dialog title |
subtitle | ❌ | Override dialog subtitle |
subtitle2 | ❌ | Override second subtitle line |
labelSubmit | ❌ | Override submit button label |
The modal collects: user name, email, and a description — paired with the original captured error event.
---
Screenshot Capture
- Available on SDK v8.0.0+
- Enabled by default via
enableScreenshot: trueonfeedbackIntegration() - Auto-hidden on mobile devices
- Screenshots count against your attachment quota (1GB standard)
Sentry.feedbackIntegration({
enableScreenshot: true, // default — can set false to disable
});---
Session Replay Integration
When Session Replay is configured alongside User Feedback, submitted feedback links to the user's replay:
Sentry.init({
dsn: "___PUBLIC_DSN___",
replaysOnErrorSampleRate: 1.0, // Buffer replays for error sessions
integrations: [
Sentry.replayIntegration(),
Sentry.feedbackIntegration({ colorScheme: "system" }),
],
});The system buffers up to 30 seconds when the feedback widget opens. This enables viewing the replay alongside the submitted feedback in Sentry.
---
Best Practices
- Use `feedbackIntegration()` for proactive collection — don't wait for errors; a persistent feedback button catches issues that never throw exceptions
- Pre-fill user info — call
Sentry.setUser()after login so users don't have to type their email each time - Combine crash modal with `beforeSend` — automatic prompting after errors maximizes feedback capture
- Link programmatic feedback to events — use
associatedEventIdso feedback appears alongside error context in Sentry - Set `autoInject: false` for branded UI — implement your own trigger button to match your design system
- Keep `showReportDialog` for 500 pages — server-rendered error pages are the primary use case; pass the server-side event ID to the client
---
Troubleshooting
| Issue | Solution |
|---|---|
| Widget doesn't appear | Check that feedbackIntegration() is in the integrations array and SDK ≥7.85.0 |
| Widget appears but form won't submit | Verify DSN is correct; check browser network tab for blocked requests |
| Screenshots not showing | Requires SDK ≥8.0.0; check enableScreenshot is not set to false |
showReportDialog shows but feedback not linked to error | Ensure eventId is passed; use captureException() return value or Sentry.lastEventId() |
| Crash modal not appearing after error | showReportDialog must be called with a valid eventId; check beforeSend hook is executing |
| Feedback not appearing in Sentry | Check attachment quota; ensure self-hosted Sentry is version ≥24.4.2 |
| Form fields are empty (no pre-fill) | Call Sentry.setUser({ name, email }) before the widget is opened |
| Replay not linked to feedback | Set replaysOnErrorSampleRate > 0; replay must be active when feedback is submitted |
| Widget conflicts with page z-index | Widget uses Shadow DOM — if still conflicting, use autoInject: false and position manually |
Related skills
How it compares
Choose sentry-browser-sdk when instrumenting client-side JavaScript; use server SDK skills when errors originate in Node, Lambda, or API handlers instead of the browser runtime.
FAQ
When should I not use @sentry/browser?
When package.json includes React, Next.js, Vue, or Angular; use the framework-specific Sentry SDK instead.
Which install path fits WordPress?
Use the Loader Script path when there is no npm bundler or package.json build tooling.
What features are recommended by default?
Error monitoring plus tracing and session replay for interactive user-facing pages.
Is Sentry Browser Sdk safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.