Sentry

Error Monitoring, Performance Tracing & Session Replay — Deep Study

Error Monitoring Performance Session Replay Distributed Tracing Alerting Open Source

What is Sentry?

Sentry is an open-source application monitoring platform that helps developers identify, triage, and fix bugs in real time. Originally built as an error tracker, it has evolved into a full observability tool covering errors, performance, profiling, session replay, and release tracking — all tied to your actual code with stack traces.

🐛

Error Monitoring

Capture, group, and triage every unhandled exception with full stack traces and context

Performance

Distributed tracing, transaction spans, N+1 detection, and slow query analysis

🎬

Session Replay

Watch exactly what the user did before an error — pixel-perfect DOM replays

📊

Profiling

Continuous and on-demand profiling to find CPU hotspots in production

🚀

Releases

Track which release introduced a regression, link commits to errors

🔔

Alerting

Issue alerts, metric alerts, and uptime monitors with Slack/PagerDuty/email routing

Your App (SDK instrumented) Browser / Server / Mobile │ Exceptions, Transactions, Replays │ (batched + compressed, HTTPS) ▼ Sentry Relay ← envelope normalization, PII scrubbing, rate limiting │ ▼ Sentry Ingest ← Kafka queue, deduplication, grouping algorithms │ ▼ Sentry Processing ├─ Error grouping (fingerprinting, stack trace similarity) ├─ Source map lookup (minified → original source) ├─ Span aggregation (trace assembly, performance metrics) └─ Alerts evaluation (thresholds, anomaly detection) │ ▼ PostgreSQL + ClickHouse + Redis + Kafka │ ▼ Sentry UI ← Issues, Traces, Replays, Dashboards, Alerts

Core Concepts

📁

Project

Unit of isolation

A Sentry project maps to one application or service. Each project gets a unique DSN (Data Source Name) used to route events. Projects belong to an Organization.

🔴

Issue

Grouped errors

An Issue is a group of similar events. Sentry deduplicates events using fingerprinting — new occurrences increment a counter rather than creating duplicate issues. Issues have states: unresolved, resolved, ignored.

📨

Event

Single occurrence

A single captured error, message, or transaction. Contains stack trace, breadcrumbs, tags, user info, extra context, and environment metadata. Events belong to Issues.

🧵

Trace

Distributed request

A Trace is a tree of Spans representing a single request flowing through multiple services. Uses W3C traceparent / Sentry's sentry-trace headers for propagation.

📏

Transaction

Root span

The root span of a trace — typically one HTTP request or page load. Transactions have a name, duration, and status. Sentry samples transactions to control volume.

🔷

Span

Unit of work

A span represents a single operation within a transaction: a DB query, HTTP call, cache lookup, render. Spans are hierarchical and form the trace waterfall.

🍞

Breadcrumbs

Event trail

Structured log entries automatically collected before an error: navigation events, console logs, HTTP requests, UI interactions. Gives "what happened before the crash."

🧬

DSN

Data Source Name

The SDK connection string for your project. Format: https://<key>@<host>/<project-id>. Never commit DSNs — use environment variables.

Data Model Hierarchy

Organization └── Project (one per service / app) ├── Issue ← grouped by fingerprint │ └── Event ← single occurrence │ ├── stack trace │ ├── breadcrumbs │ ├── tags (indexed, filterable) │ ├── context (extra data, not indexed) │ └── user (id, email, username) │ ├── Transaction ← root span (sampled) │ └── Span ← db.query, http.request, etc. │ ├── Replay ← DOM recording session │ ├── Release ← version + commit range │ └── Deploy ← environment + timestamp │ └── Alert ← issue rule or metric rule

Setup & Installation

# Install SDK npm install @sentry/nextjs # Next.js (recommended wizard) npm install @sentry/node # Plain Node.js # Next.js wizard (auto-creates config files) npx @sentry/wizard@latest -i nextjs

Next.js — Manual Setup

sentry.client.config.ts

import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NEXT_PUBLIC_ENV, // % of errors sent (1.0 = 100%) sampleRate: 1.0, // % of transactions traced tracesSampleRate: 0.1, // Session replay integrations: [ Sentry.replayIntegration(), ], replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, });

sentry.server.config.ts

import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 0.1, // Capture unhandled promise rejections integrations: [ Sentry.nodeContextIntegration(), Sentry.localVariablesIntegration(), ], });

next.config.js — wrap with withSentryConfig

const { withSentryConfig } = require("@sentry/nextjs"); const nextConfig = { /* your config */ }; module.exports = withSentryConfig(nextConfig, { org: "my-org", project: "my-app", // Upload source maps to Sentry at build time silent: true, widenClientFileUpload: true, hideSourceMaps: true, disableLogger: true, });
# Install npm install @sentry/react

React — Setup

// src/main.tsx (or index.tsx) import * as Sentry from "@sentry/react"; import { useEffect } from "react"; import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from "react-router-dom"; Sentry.init({ dsn: "YOUR_DSN", integrations: [ // React Router v6 integration Sentry.reactRouterV6BrowserTracingIntegration({ useEffect, useLocation, useNavigationType, createRoutesFromChildren, matchRoutes, }), Sentry.replayIntegration(), ], tracesSampleRate: 0.1, tracePropagationTargets: ["localhost", /^https:\/\/api\.myapp\.com/], replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, });

Error Boundary

import { ErrorBoundary } from "@sentry/react"; function App() { return ( <ErrorBoundary fallback={<ErrorPage />} showDialog > <Routes /> </ErrorBoundary> ); }

Profiler (render performance)

import { Profiler } from "@sentry/react"; function MyComponent() { return ( <Profiler id="MyComponent"> <ExpensiveChild /> </Profiler> ); }
# Install pip install --upgrade "sentry-sdk[django]" # Django pip install --upgrade "sentry-sdk[fastapi]" # FastAPI pip install --upgrade "sentry-sdk[flask]" # Flask pip install --upgrade "sentry-sdk[celery]" # Celery workers

Django — settings.py

import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.redis import RedisIntegration sentry_sdk.init( dsn="YOUR_DSN", integrations=[ DjangoIntegration(), CeleryIntegration(), RedisIntegration(), ], traces_sample_rate=0.1, profiles_sample_rate=0.1, # Continuous profiling send_default_pii=False, # Don't send PII by default environment=os.getenv("ENVIRONMENT", "development"), release=os.getenv("GIT_SHA"), )

FastAPI

import sentry_sdk from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.starlette import StarletteIntegration sentry_sdk.init( dsn="YOUR_DSN", integrations=[StarletteIntegration(), FastApiIntegration()], traces_sample_rate=0.2, )
# Install go get github.com/getsentry/sentry-go go get github.com/getsentry/sentry-go/http # net/http middleware go get github.com/getsentry/sentry-go/gin # Gin middleware

Go — Basic Setup

import "github.com/getsentry/sentry-go" func main() { err := sentry.Init(sentry.ClientOptions{ Dsn: "YOUR_DSN", Environment: "production", Release: "my-app@1.0.0", TracesSampleRate: 0.1, }) if err != nil { log.Fatalf("sentry.Init: %s", err) } defer sentry.Flush(2 * time.Second) // Capture panic automatically defer sentry.Recover() } // HTTP middleware sentryHandler := sentryhttp.New(sentryhttp.Options{ Repanic: true, }) http.Handle("/", sentryHandler.Handle(myHandler))

Error Monitoring

Capture Errors Manually

import * as Sentry from "@sentry/node"; try { riskyOperation(); } catch (err) { Sentry.captureException(err); } // Capture a message (not an exception) Sentry.captureMessage( "Payment failed for user", "warning" // fatal|error|warning|info|debug );

Set User Context

// Identify the user — attach to all events Sentry.setUser({ id: "user-123", email: "alice@example.com", username: "alice", }); // Clear on logout Sentry.setUser(null); // Wrap async code with user scope Sentry.withScope((scope) => { scope.setUser({ id: userId }); scope.setTag("plan", "enterprise"); Sentry.captureException(err); });

Context, Tags & Extras

// Tags — indexed, filterable in UI, used for searching Sentry.setTag("feature_flag", "checkout-v2"); Sentry.setTag("region", "us-east-1"); // Context — structured extra data (not indexed) Sentry.setContext("order", { orderId: "ord_abc123", total: 99.99, currency: "USD", items: 3, }); // Extra — arbitrary key-value (not indexed) Sentry.setExtra("rawPayload", JSON.stringify(payload)); // beforeSend — mutate or drop events before sending Sentry.init({ beforeSend(event, hint) { // Drop events from bots if (event.request?.headers?.["user-agent"] ?.includes("bot")) { return null; } // Scrub sensitive field if (event.request?.data) { delete event.request.data.password; } return event; }, });

Fingerprinting (Custom Grouping)

// Override Sentry's automatic grouping Sentry.withScope((scope) => { // Group all DB timeout errors together scope.setFingerprint(["database-timeout"]); Sentry.captureException(err); }); // Config-based fingerprinting rule (in Sentry UI → Project Settings → Issue Grouping) // Or via SDK: Sentry.init({ beforeSend(event) { if (event.exception?.values?.[0].type === "TimeoutError") { event.fingerprint = ["timeout-error", event.tags?.service]; } return event; }, });

Performance Monitoring

Sentry's performance monitoring is built on distributed tracing — every request gets a trace ID that flows through all services, producing a full picture of latency across your stack.

Transactions & Spans

Manual transaction

const transaction = Sentry.startTransaction({ name: "process-order", op: "task", }); // Add child spans const dbSpan = transaction.startChild({ op: "db.query", description: "SELECT * FROM orders", }); await queryDatabase(); dbSpan.finish(); const emailSpan = transaction.startChild({ op: "http.client", description: "POST /send-email", }); await sendEmail(); emailSpan.finish(); transaction.finish();

Modern API (Sentry v8+)

// Recommended: use startSpan const result = await Sentry.startSpan( { name: "process-order", op: "task" }, async (span) => { const order = await Sentry.startSpan( { name: "db.fetch", op: "db.query" }, () => fetchOrder(id) ); span?.setAttribute("order.id", id); span?.setAttribute("order.total", order.total); return order; } ); // Python equivalent with sentry_sdk.start_transaction( name="process-order", op="task" ) as transaction: with transaction.start_child(op="db.query"): order = fetch_order(order_id)

Sampling Strategy

// Static rate — simple but inflexible tracesSampleRate: 0.1, // 10% of all transactions // Dynamic sampler — recommended for production tracesSampler: (samplingContext) => { const { transactionContext, parentSampled } = samplingContext; // Inherit parent's sampling decision for consistency if (parentSampled !== undefined) return parentSampled; // Always trace health checks at 0% if (transactionContext.name === "GET /health") return 0; // High-value transactions — trace 50% if (transactionContext.name.startsWith("POST /checkout")) return 0.5; // Default — 5% return 0.05; },
Cost tip: Sentry charges by event volume. Use tracesSampler to trace important transactions at higher rates (checkout, auth) and noisy read endpoints at lower rates (feed, search).

Web Vitals (Frontend)

The browser SDK automatically captures Core Web Vitals when browserTracingIntegration is enabled.

MetricWhat It MeasuresGoodNeeds WorkPoor
LCP — Largest Contentful PaintLoad performance — when is the largest visible element rendered?< 2.5s2.5–4s> 4s
FID — First Input DelayInteractivity — how quickly does the page respond to first input?< 100ms100–300ms> 300ms
CLS — Cumulative Layout ShiftVisual stability — how much does content shift unexpectedly?< 0.10.1–0.25> 0.25
FCP — First Contentful PaintWhen does any content first appear?< 1.8s1.8–3s> 3s
TTFB — Time To First ByteServer response latency< 800ms800ms–1.8s> 1.8s
INP — Interaction to Next PaintResponsiveness throughout the page lifecycle (replaces FID)< 200ms200–500ms> 500ms

Performance Lab Testing — See Results Instantly

Sentry's performance data comes from real user sessions (RUM / field data) — which means you must deploy and wait for traffic before seeing the effect of any change.

Lab testing simulates a user visit in a controlled environment and gives you scores immediately on localhost or in CI — no real traffic needed. Use lab tools to iterate fast, then use Sentry to confirm the improvement in production.

── Development Loop ────────────────────────────────────────────── Code change │ ▼ Lab Test ← runs on localhost or CI, results in seconds Lighthouse CLI / PerformanceObserver / Playwright │ Score +/- instantly visible ▼ Deploy to production │ ▼ Sentry RUM ← confirms real-user impact over hours/days Web Vitals · Transactions · p75/p95 latency ── Lab ≠ Field (but lab predicts field direction) ─────────────── Lab: synthetic, single run, throttled CPU+network, no caching Field: real devices, real networks, real cache states — averages

1. Lighthouse CLI — Instant Scores on Localhost

Lighthouse audits a URL and returns LCP, CLS, TBT (proxy for FID/INP), FCP, SI, and a total Performance score 0–100 in ~30 seconds.

# Install globally npm install -g lighthouse # Audit localhost (dev server must be running) lighthouse http://localhost:3000 --view # JSON output for scripting lighthouse http://localhost:3000 --output=json --output-path=./lh.json # Throttle to simulate mobile (Moto G4 + 4G) lighthouse http://localhost:3000 \ --emulated-form-factor=mobile \ --throttling-method=simulate \ --view # Only run performance (skip a11y, SEO, PWA) lighthouse http://localhost:3000 --only-categories=performance --view # Run 3 times and average (reduces variance) for i in 1 2 3; do lighthouse http://localhost:3000 --output=json --output-path=./lh$i.json done

Read scores from JSON output

# Extract score from JSON node -e " const r = require('./lh.json'); const c = r.categories.performance; console.log('Score:', Math.round(c.score * 100)); const a = r.audits; console.log('LCP:', a['largest-contentful-paint'].displayValue); console.log('TBT:', a['total-blocking-time'].displayValue); console.log('CLS:', a['cumulative-layout-shift'].displayValue); console.log('FCP:', a['first-contentful-paint'].displayValue); console.log('SI:', a['speed-index'].displayValue); "

PageSpeed Insights API (field + lab)

# Uses Chrome UX Report field data + lab curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed\ ?url=https://myapp.com\ &strategy=mobile\ &key=$PSI_API_KEY" | jq ' { score: .lighthouseResult.categories.performance.score, lcp: .loadingExperience.metrics.LARGEST_CONTENTFUL_PAINT_MS.percentile, cls: .loadingExperience.metrics.CUMULATIVE_LAYOUT_SHIFT_SCORE.percentile, inp: .loadingExperience.metrics.INTERACTION_TO_NEXT_PAINT.percentile } '

2. Lighthouse CI — Fail PRs When Score Drops

LHCI runs Lighthouse in your CI pipeline, stores results, and blocks merges when performance regresses below a threshold.

# Install npm install -g @lhci/cli # lighthouserc.json — project config
{ "ci": { "collect": { "url": ["http://localhost:3000", "http://localhost:3000/about"], "numberOfRuns": 3, "startServerCommand": "npm run start", "startServerReadyPattern": "ready on" }, "assert": { "preset": "lighthouse:recommended", "assertions": { "categories:performance": ["error", { "minScore": 0.8 }], "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }], "cumulative-layout-shift": ["warn", { "maxNumericValue": 0.1 }], "total-blocking-time": ["error", { "maxNumericValue": 300 }], "uses-optimized-images": "off" } }, "upload": { "target": "lhci", "serverBaseUrl": "https://lhci.mycompany.com" } } }
# GitHub Actions integration name: Lighthouse CI on: [pull_request] jobs: lhci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci && npm run build - name: Run Lighthouse CI run: | npm install -g @lhci/cli lhci autorun env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
LHCI posts a PR comment with score changes — green if improved, red if regressed. It catches performance regressions before they reach production.

3. PerformanceObserver — Measure Vitals in Your Own Code

The web-vitals library wraps the browser's PerformanceObserver API. Use it locally to print scores to the console as you navigate — no deployment needed.

Install & print to console (dev only)

# Install npm install web-vitals // src/utils/vitals.ts import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'; function logVital(metric: any) { const rating = metric.rating; // 'good'|'needs-improvement'|'poor' const color = rating === 'good' ? 'color:lime' : rating === 'poor' ? 'color:red' : 'color:orange'; console.log( `%c[Vitals] %s: %s (%s)`, color, metric.name, metric.value.toFixed(1), rating ); } onLCP(logVital); onINP(logVital); onCLS(logVital); onFCP(logVital); onTTFB(logVital);

Send to Sentry as custom measurements

import * as Sentry from '@sentry/react'; import { onLCP, onINP, onCLS } from 'web-vitals'; function sendToSentry(metric: any) { Sentry.withScope((scope) => { scope.setTag('vital', metric.name); scope.setTag('rating', metric.rating); scope.setContext('web-vital', { value: metric.value, delta: metric.delta, id: metric.id, entries: metric.entries, }); // Sentry perf transaction measurement const span = Sentry.getActiveSpan(); span?.setAttribute( `web_vital.${metric.name.toLowerCase()}`, metric.value ); }); } onLCP(sendToSentry); onINP(sendToSentry); onCLS(sendToSentry);

Next.js built-in reporting

// app/layout.tsx or pages/_app.tsx export function reportWebVitals(metric: NextWebVitalsMetric) { // metric.name: 'FCP'|'LCP'|'CLS'|'FID'|'TTFB'|'INP' // metric.value, metric.label ('web-vital'|'custom') if (process.env.NODE_ENV === 'development') { console.log(metric); } // Forward to Sentry, GA4, DataDog, etc. Sentry.captureEvent({ message: `Web Vital: ${metric.name}`, level: metric.rating === 'poor' ? 'warning' : 'info', tags: { vital: metric.name, rating: metric.rating }, extra: { value: metric.value }, }); }

Raw PerformanceObserver (no library)

// Observe Long Tasks (> 50ms, blocks main thread) const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { console.warn('Long Task:', { duration: entry.duration.toFixed(1) + 'ms', start: entry.startTime.toFixed(1) + 'ms', }); }); }); observer.observe({ type: 'longtask', buffered: true }); // Observe LCP element new PerformanceObserver((list) => { const entries = list.getEntries(); const last = entries[entries.length - 1]; console.log('LCP element:', last.element, last.startTime); }).observe({ type: 'largest-contentful-paint', buffered: true });

4. Bundle Analysis — Find What's Bloating Your JS

Large JavaScript bundles are the #1 cause of poor TBT (Total Blocking Time) and slow LCP. Analyze your bundle before deploying.

Next.js Bundle Analyzer

# Install npm install @next/bundle-analyzer // next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer(nextConfig); # Run — opens interactive treemap in browser ANALYZE=true npm run build

Vite / Rollup Visualizer

# Install npm install rollup-plugin-visualizer // vite.config.ts import { visualizer } from 'rollup-plugin-visualizer'; export default { plugins: [ visualizer({ open: true, // auto-open in browser gzipSize: true, // show gzip sizes brotliSize: true, template: 'treemap', // treemap|sunburst|network }), ], }; # Run npm run build

bundlephobia — check package cost before installing

# CLI version npx bundlephobia moment npx bundlephobia date-fns npx bundlephobia lodash # Example output: # moment → 72.1 kB (gzip: 18.4 kB) ⚠️ # date-fns → 35.6 kB (gzip: 9.9 kB) ✓ (tree-shakeable) # day.js → 6.5 kB (gzip: 2.6 kB) ✓

Source Map Explorer

# Shows byte breakdown from source maps npx source-map-explorer 'dist/static/js/*.js' # Or for a specific chunk npx source-map-explorer dist/static/js/main.abc123.js # JSON output for CI comparison npx source-map-explorer 'dist/**/*.js' --json > bundle-report.json

5. Playwright — Automated Performance Checks in Tests

Run Playwright tests against your dev/staging build to assert on performance metrics — catches regressions in CI before any real user is affected.

import { test, expect } from '@playwright/test'; test('home page meets Core Web Vitals', async ({ page }) => { // Start collecting performance entries await page.goto('http://localhost:3000'); // Wait for page to fully settle await page.waitForLoadState('networkidle'); // Extract Web Vitals from page context const vitals = await page.evaluate(() => { return new Promise((resolve) => { import('https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js') .then(({ onLCP, onCLS, onTTFB }) => { const results: Record<string, number> = {}; onLCP((m) => { results.lcp = m.value; }); onCLS((m) => { results.cls = m.value; }); onTTFB((m) => { results.ttfb = m.value; }); setTimeout(() => resolve(results), 3000); }); }); }); // Assert thresholds expect(vitals.lcp).toBeLessThan(2500); // < 2.5s expect(vitals.cls).toBeLessThan(0.1); // < 0.1 expect(vitals.ttfb).toBeLessThan(800); // < 800ms }); test('navigation timing', async ({ page }) => { await page.goto('http://localhost:3000'); // Use Navigation Timing API const timing = await page.evaluate(() => { const t = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; return { domContentLoaded: t.domContentLoadedEventEnd - t.startTime, loadComplete: t.loadEventEnd - t.startTime, ttfb: t.responseStart - t.requestStart, }; }); expect(timing.ttfb).toBeLessThan(500); expect(timing.domContentLoaded).toBeLessThan(2000); });

6. Score Improvement — What Actually Moves the Needle

Each audit in Lighthouse maps to a specific metric. Fix the highest-weight issues first.

🖼️

LCP — Largest Contentful Paint

Weight: 25% of score
  • Add fetchpriority="high" to the hero image
  • Preload hero image: <link rel="preload" as="image">
  • Use <Image priority /> in Next.js
  • Convert images to WebP/AVIF
  • Remove render-blocking CSS from <head>
  • Use CDN with edge caching for assets
  • Inline critical CSS above the fold
⏱️

TBT — Total Blocking Time

Weight: 30% of score (FID/INP proxy)
  • Split large JS bundles (dynamic imports)
  • Defer non-critical scripts: defer / async
  • Break up long tasks with scheduler.yield()
  • Move heavy work to Web Workers
  • Remove unused JS (tree-shaking, code splitting)
  • Replace heavy libraries with lighter alternatives
  • Lazy-load off-screen components
📐

CLS — Cumulative Layout Shift

Weight: 15% of score
  • Set explicit width + height on all images
  • Use CSS aspect-ratio for media containers
  • Reserve space for ads / embeds
  • Avoid inserting content above existing content
  • Use font-display: optional or preload fonts
  • Avoid animating properties that trigger layout
🌐

FCP / TTFB — Server Speed

Weight: FCP 10%, SI 10%
  • Enable HTTP/2 or HTTP/3
  • Use Cache-Control: stale-while-revalidate
  • CDN for static assets + SSR edge caching
  • Enable Next.js ISR or full-page CDN caching
  • Preconnect to critical origins: <link rel="preconnect">
  • Compress responses with Brotli (better than gzip)

Quick Wins Checklist

── Images ────────────────────────────────────────────────────────── Use Next.js <Image> or native <img loading="lazy"> for below-fold Add fetchpriority="high" to hero/LCP image Serve WebP (60% smaller than JPEG, same quality) Set explicit width + height on every <img> ── JavaScript ────────────────────────────────────────────────────── Dynamic import for heavy components: import('./HeavyChart') Move analytics / chat scripts to <Script strategy="lazyOnload"> Audit bundle — remove unused packages (lodash → lodash-es, moment → dayjs) Use React.lazy() + Suspense for route-level code splitting ── Fonts ──────────────────────────────────────────────────────────── Use next/font (zero CLS, self-hosted, auto subset) Preload critical font files font-display: swap or optional ── Network ────────────────────────────────────────────────────────── <link rel="preconnect" href="https://api.myapp.com"> <link rel="dns-prefetch" href="https://cdn.myapp.com"> Enable Brotli compression on server / CDN Cache static assets with long TTL (1 year) + content hash in filename ── Rendering ──────────────────────────────────────────────────────── Inline critical above-the-fold CSS Move non-critical CSS to <link rel="preload" onload="..."> Use CSS containment (contain: layout) on isolated widgets

React / Next.js Specific Patterns

// Dynamic import — defer heavy component import dynamic from 'next/dynamic'; const HeavyChart = dynamic( () => import('./HeavyChart'), { loading: () => <ChartSkeleton />, ssr: false, // skip server render } ); // Lazy load below-fold section const Footer = dynamic(() => import('./Footer')); const Comments = dynamic(() => import('./Comments')); // Priority image (marks as LCP element) <Image src="/hero.webp" width={1200} height={630} priority // ← preload + fetchpriority=high alt="Hero" />
// Break long task into microtasks async function processLargeList(items: Item[]) { const CHUNK = 50; for (let i = 0; i < items.length; i += CHUNK) { processChunk(items.slice(i, i + CHUNK)); // Yield to browser between chunks await scheduler.yield(); // or: await new Promise(r => setTimeout(r)) } } // Virtualize long lists (react-window) import { FixedSizeList } from 'react-window'; <FixedSizeList height={600} width="100%" itemCount={10000} itemSize={50} > {({ index, style }) => <Row item={items[index]} style={style} /> } </FixedSizeList>

Tool Comparison: When to Use What

ToolSpeedWhat it measuresBest forNeeds deploy?
Lighthouse CLI ~30s LCP, FCP, CLS, TBT, SI, Score 0–100 Quick local audit, one-off checks No — localhost
Lighthouse CI 2–5 min Same as CLI, averaged over N runs PR gates, regression detection No — CI build
web-vitals library Instant LCP, INP, CLS, FCP, TTFB (real browser) Dev console feedback while coding No — localhost
PerformanceObserver Instant Long tasks, LCP element, layout shifts Debugging specific interactions No — browser DevTools
Bundle Analyzer ~10s JS bundle size by module Finding bloated dependencies No — build step
Chrome DevTools Perf Instant Flame chart, long tasks, layout, paint Deep-dive profiling of specific pages No — localhost
Playwright perf tests 1–3 min Timing APIs, custom vital assertions Automated regression tests in CI No — CI/staging
PageSpeed Insights API ~1 min Lab + field CrUX data (28-day p75) Seeing real user percentiles Yes — public URL
Sentry Web Vitals (RUM) Hours–days Real p50/p75/p95 from actual users Confirming improvement shipped to users Yes — production traffic
Workflow: Use Lighthouse CLI + web-vitals console logging during active development → run Lighthouse CI in PRs to catch regressions → confirm real-user impact in Sentry after deploy. You iterate at dev speed, not traffic speed.

Session Replay

Session Replay records DOM mutations, user interactions, console logs, and network requests — letting you watch exactly what a user experienced before and during an error. Privacy-first: all sensitive data is masked by default.

🎬

How It Works

Uses rrweb to record DOM snapshots and mutations as a compact event stream — not a video. Playback reconstructs the DOM state at any point in time.

  • ~10–50KB per minute (compressed)
  • Buffered on client, flushed on error
  • No video codec — pure DOM events
🔒

Privacy Controls

  • .sentry-mask — mask element contents
  • .sentry-unmask — allow specific elements
  • .sentry-block — replace with empty box
  • .sentry-ignore — skip interactions
  • Input text masked by default
  • Images blocked by default
🎚️

Sampling

  • replaysSessionSampleRate — % of all sessions recorded
  • replaysOnErrorSampleRate — % of error sessions recorded (set to 1.0)
  • Replay linked to error automatically when both are captured in same session

Configuration

Sentry.init({ integrations: [ Sentry.replayIntegration({ // Mask all text content by default maskAllText: true, // Block all media (images, video) blockAllMedia: true, // Custom masking mask: [".credit-card", "[data-sensitive]"], unmask: [".public-text"], block: [".avatar-image"], ignore: [".non-interactive"], // Network request recording networkDetailAllowUrls: ["https://api.myapp.com"], networkCaptureBodies: true, networkRequestHeaders: ["x-request-id"], networkResponseHeaders: ["x-ratelimit-remaining"], }), ], replaysSessionSampleRate: 0.05, // 5% of normal sessions replaysOnErrorSampleRate: 1.0, // 100% of error sessions });

Releases & Source Maps

Releases let Sentry correlate errors with specific commits, show which release introduced a regression, and unminify JavaScript stack traces using source maps.

1

Set the release version in SDK

Sentry.init({ release: process.env.SENTRY_RELEASE, // e.g. "my-app@1.4.2" or git SHA environment: "production", });
2

Create a release and upload source maps in CI

# Install Sentry CLI npm install -g @sentry/cli # Set env vars export SENTRY_AUTH_TOKEN=<token> export SENTRY_ORG=my-org export SENTRY_PROJECT=my-app # Create release, associate commits, upload maps sentry-cli releases new "$RELEASE_VERSION" sentry-cli releases set-commits --auto "$RELEASE_VERSION" sentry-cli releases files "$RELEASE_VERSION" upload-sourcemaps ./dist \ --url-prefix "~/" sentry-cli releases finalize "$RELEASE_VERSION" sentry-cli releases deploys "$RELEASE_VERSION" new -e production
3

Alternative: Use Sentry webpack / vite plugin (auto upload)

# Vite npm install @sentry/vite-plugin // vite.config.ts import { sentryVitePlugin } from "@sentry/vite-plugin"; export default { plugins: [sentryVitePlugin({ org: "my-org", project: "my-app", authToken: process.env.SENTRY_AUTH_TOKEN, })], build: { sourcemap: true }, };
Source maps in production: Upload source maps to Sentry but set sourceMapReferences: false and serve your JS without //# sourceMappingURL= comments — so maps are only accessible to Sentry, not the public.

Alerting & Notifications

Issue Alerts

Triggered by issue lifecycle events — new issue, regression (previously resolved issue re-appears), or issue seen by N users.

Example conditions: - A new issue is created - An issue is seen by more than 100 users - An issue regresses (resolved → reoccurs) - Error count > 50 in 1 hour - Crash rate > 1% for a release Actions: - Email / Slack / PagerDuty / Opsgenie - Create Jira / GitHub / Linear ticket - Send webhook to any URL

Metric Alerts

Threshold-based alerts on aggregate metrics — error rate, latency percentiles, failure rate. Supports anomaly detection.

Example metric alerts: - p95 response time > 2s (15 min window) - Error rate > 5% of transactions - Apdex score drops below 0.8 - Number of unique users affected > 100 Alert types: - Static threshold - Anomaly detection (% change from baseline) - Dynamic threshold (seasonal adjustment)

Uptime Monitoring

# Configure in Sentry UI → Alerts → Uptime Monitors # Or via API curl https://sentry.io/api/0/projects/{org}/{project}/uptime/ \ -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \ -d '{"name":"API Health","url":"https://api.myapp.com/health","intervalSeconds":60,"method":"GET"}' # Or via Cron Monitors (for scheduled jobs) # Wrap your cron job with check-in pings # Node.js example: const checkInId = Sentry.captureCheckIn({ monitorSlug: "daily-report", status: "in_progress", }); await generateReport(); Sentry.captureCheckIn({ checkInId, monitorSlug: "daily-report", status: "ok", duration: 42, // seconds });

Routing Rules

Use Alert Routing (Sentry → Settings → Integrations) to route different alerts to different channels: backend errors → PagerDuty on-call, frontend errors → Slack #frontend-bugs, low-priority issues → email digest.

Self-Hosting Sentry

Sentry is open-source (BSL license since 2024 — free for non-SaaS use) and can be self-hosted via Docker Compose.

1

Install self-hosted Sentry

# Minimum: 4 CPU, 16 GB RAM, 20 GB disk git clone https://github.com/getsentry/self-hosted cd self-hosted ./install.sh # Start all services docker compose up -d # Sentry UI on port 9000 open http://localhost:9000
2

Key services in the stack

Services started by docker compose: web # Django app (Sentry UI + API) worker # Celery workers (event processing) cron # Celery beat (scheduled tasks) relay # Event ingestion proxy kafka # Message queue postgres # Primary database redis # Cache + Celery broker clickhouse # Time-series metrics store memcached # Additional caching layer nginx # Reverse proxy symbolicator # Native crash symbolication vroom # Profiling storage
3

Key configuration: sentry/config.yml + sentry/sentry.conf.py

# sentry/config.yml — YAML config system.url-prefix: 'https://sentry.mycompany.com' system.secret-key: 'long-random-secret' mail.host: 'smtp.mailgun.org' mail.port: 587 mail.username: 'postmaster@mg.mycompany.com' mail.password: '...' mail.use-tls: true mail.from: 'sentry@mycompany.com'
Production considerations: Use external managed PostgreSQL (RDS/Cloud SQL) and Redis (ElastiCache) instead of containerized versions. Scale Kafka for high event volume. Enable backup for PostgreSQL and ClickHouse data.

Sentry vs Alternatives

FeatureSentryDatadog APMRollbarBugsnagNew Relic
Primary focus Error + Perf Full APM Error tracking Error tracking Full APM
Open Source Yes (BSL) No No No No
Self-hostable Yes No No No No
Session Replay Yes Yes No No Limited
Distributed tracing Yes Yes No No Yes
Profiling Yes Yes No No Yes
Source maps Yes Yes Yes Yes Yes
Cron monitoring Yes Yes No No Via synthetics
Free tier 5K errors/mo No 5K/mo 7.5K/mo 100GB/mo
Pricing model Per event volume Per host / per GB Per event Per event Per GB ingest
Best for Dev-first teams needing code-level insight Ops teams with broad infrastructure monitoring Simple error tracking on a budget Mobile + web error tracking Enterprise full-stack monitoring

Tips & Best Practices

1

Use environments to separate dev/staging/production noise

Always set environment in your SDK init. Filter the Sentry UI by environment. Never let dev errors pollute your production issue queue — or you'll ignore real alerts.

2

Set release + link commits for every deploy

Use sentry-cli releases set-commits --auto in CI. This enables "suspect commits" — Sentry tells you which commit likely caused a regression based on the file paths in the stack trace.

3

Use tracesSampler instead of tracesSampleRate

Static sampling treats all transactions equally. Dynamic sampling lets you over-sample high-value flows (checkout, auth) and under-sample cheap read endpoints — controlling costs while retaining insight where it matters.

4

Set up inbound data filters to reduce noise

In Project Settings → Inbound Filters: enable "Filter known browser errors" (ResizeObserver loop, non-error promise rejections) and "Filter localhost errors." Add custom filters for 404s you don't care about.

5

Use beforeSend to scrub PII before it leaves the browser

Never rely on server-side scrubbing alone. Use beforeSend to remove credit card numbers, tokens, and passwords from event data at the SDK level — before any network request is made.

6

Wire up Sentry to your issue tracker

Integrate with Jira, GitHub Issues, or Linear. When a new Sentry issue is created, auto-create a ticket. When the ticket is closed, auto-resolve the Sentry issue. Keeps engineering workflow in sync.

7

Use Ownership Rules to auto-assign issues to teams

In Project Settings → Ownership Rules: map file paths or URL patterns to teams or users. e.g., path:src/payments/** → #payments-team. Issues are auto-assigned so the right people are notified.

8

Monitor cron jobs with Sentry Crons

Wrap every scheduled job with Sentry check-in pings. If a job doesn't check in within the expected window, Sentry alerts you — catching silent failures that logs and metrics often miss.

Useful SDK Methods Cheat Sheet

// ─── Error capture ────────────────────────────────────────── Sentry.captureException(error) // capture Error object Sentry.captureMessage("msg", "warning") // capture arbitrary message Sentry.captureEvent({ message, level }) // fully custom event // ─── Scope ────────────────────────────────────────────────── Sentry.setUser({ id, email }) // attach user to all future events Sentry.setTag(key, value) // indexed tag Sentry.setTags({ key: value }) // multiple tags Sentry.setContext(name, object) // extra context object Sentry.setExtra(key, value) // arbitrary extra data Sentry.addBreadcrumb({ category, message })// manual breadcrumb Sentry.withScope((scope) => { ... }) // isolated scope for one event Sentry.configureScope((scope) => { ... })// modify global scope // ─── Performance ──────────────────────────────────────────── Sentry.startTransaction({ name, op }) // start root span (legacy) Sentry.startSpan({ name, op }, callback) // start span (modern API) Sentry.getActiveSpan() // get current active span // ─── Crons ────────────────────────────────────────────────── Sentry.captureCheckIn({ monitorSlug, status })// check-in ping Sentry.withMonitor(slug, callback) // auto check-in wrapper // ─── Utilities ────────────────────────────────────────────── Sentry.flush(2000) // force flush (serverless) Sentry.close(2000) // flush + close SDK Sentry.isInitialized() // check if SDK is ready Sentry.lastEventId() // get ID of last captured event

Serverless & Edge Considerations

Always call Sentry.flush(2000) at the end of serverless functions (AWS Lambda, Vercel Edge, Cloudflare Workers) — the process exits before the SDK can send buffered events asynchronously. Without it, you'll silently lose errors.
// AWS Lambda wrapper import * as Sentry from "@sentry/aws-serverless"; Sentry.init({ dsn: "...", tracesSampleRate: 1.0 }); export const handler = Sentry.wrapHandler(async (event, context) => { // Your Lambda code here — errors auto-captured, flush handled }); // Vercel Edge / Next.js — flush in middleware export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { await import("./sentry.server.config"); } if (process.env.NEXT_RUNTIME === "edge") { await import("./sentry.edge.config"); } }