Sentry: Error Tracking, Triage, and Resolution
Master Sentry for production error tracking. Covers SDK integration in Python, Node.js, and Java, release tracking, source maps, performance monitoring, and alerting.
Introduction
Sentry is the tool you reach for when exceptions slip through to production. It grabs the stack trace, request context, user info, and breadcrumbs that led to the failure, then surfaces it all in one place. This guide covers SDK setup for Python, Node.js, and Java, release tracking with source maps, performance monitoring, alerting rules, and a practical workflow to triage and fix errors.
Already shipped Sentry? Head to release tracking and source maps or alerting rules.
Prerequisites
Before you start, get these ready:
- A Sentry account (cloud or self-hosted). The free tier gives you 5,000 errors per month, plenty for side projects and small teams.
- The Sentry SDK for your stack:
sentry-sdk(Python),@sentry/node(Node.js), orsentry-spring(Java). Grab it viapip,npm, ormaven. - A framework integration if you use one: Flask, Django, Express, Fastify, Spring Boot, or Quarkus. Sentry ships first-class integrations for each.
sentry-cliinstalled globally if you plan to do release tracking and source map uploads. Install it withnpm install -g @sentry/clior download the binary from the Sentry CLI releases page.- A DSN string from your Sentry project settings. It looks like
https://your-key@sentry.io/123.
How Sentry Works
When an error bubbles up in your application, the Sentry SDK intercepts it. It records the stack trace, request context, user context, breadcrumbs, and environment and release tags, then ships the event to Sentry’s servers. From there, Sentry deduplicates and groups similar errors into issues and pings your team in Slack, email, or PagerDuty. A developer opens the issue, traces the root cause, deploys a fix, and Sentry resolves the issue once the new release stops reporting it.
Source maps matter here. They turn minified stack traces into readable file names and line numbers; without them, debugging bundled frontend or Node.js code is mostly guesswork.
SDK Integration
Python: Flask/Django
# sentry_config.py — Sentry SDK for Python
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.celery import CeleryIntegration
def init_sentry():
sentry_sdk.init(
dsn="https://your-dsn@sentry.io/123",
environment="production",
release="order-service@1.2.3",
# Sample 100% of errors, 10% of transactions for performance
traces_sample_rate=0.1,
sample_rate=1.0,
integrations=[
FlaskIntegration(),
SqlalchemyIntegration(),
RedisIntegration(),
CeleryIntegration(),
],
# Send PII (email, IP) — enable with caution
send_default_pii=True,
# Before send hook to filter sensitive data
before_send=filter_sensitive_data,
)
def filter_sensitive_data(event, hint):
"""Remove sensitive data before sending to Sentry."""
if "request" in event:
headers = event["request"].get("headers", {})
# Remove authorization headers
for key in list(headers.keys()):
if key.lower() in ("authorization", "cookie", "x-api-key"):
headers[key] = "[REDACTED]"
# Remove sensitive body fields
body = event["request"].get("data", {})
if isinstance(body, dict):
for key in ("password", "credit_card", "ssn"):
if key in body:
body[key] = "[REDACTED]"
return event
# Manual error capture
from sentry_sdk import capture_exception, capture_message, set_user, add_breadcrumb
class OrderService:
def create_order(self, user_id: int, items: list[dict]) -> dict:
# Set user context
set_user({"id": str(user_id), "email": "user@example.com"})
# Add breadcrumbs for debugging
add_breadcrumb(
category="order",
message=f"Creating order for user {user_id} with {len(items)} items",
level="info",
)
try:
order = self._process_order(user_id, items)
add_breadcrumb(category="order", message="Order created successfully", level="info")
return order
except PaymentError as e:
# Capture with extra context
capture_exception(e, {
"extra": {
"user_id": user_id,
"items_count": len(items),
"total_amount": sum(i["price"] * i["quantity"] for i in items),
},
"tags": {"error_type": "payment", "severity": "high"},
})
raise
except Exception as e:
capture_exception(e)
raise
Node.js: Express
// sentry.ts — Sentry SDK for Node.js
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
export function initSentry() {
Sentry.init({
dsn: "https://your-dsn@sentry.io/123",
environment: process.env.NODE_ENV || "development",
release: `api-gateway@${process.env.APP_VERSION || "1.0.0"}`,
tracesSampleRate: 0.1,
profilesSampleRate: 0.1,
integrations: [
nodeProfilingIntegration(),
Sentry.httpIntegration(),
Sentry.expressIntegration(),
],
beforeSend(event) {
// Redact sensitive headers
if (event.request?.headers) {
delete event.request.headers.authorization;
delete event.request.headers.cookie;
}
// Redact sensitive body fields
if (event.request?.data && typeof event.request.data === "object") {
delete event.request.data.password;
delete event.request.data.creditCard;
}
return event;
},
});
}
// Express middleware — must be before routes
import express from "express";
const app = express();
Sentry.setupExpressErrorHandler(app);
// Manual error capture
import { captureException, setUser, addBreadcrumb } from "@sentry/node";
class OrderService {
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
Sentry.setUser({ id: userId, email: "user@example.com" });
Sentry.addBreadcrumb({
category: "order",
message: `Creating order for user ${userId} with ${items.length} items`,
level: "info",
});
try {
const order = await this.processOrder(userId, items);
Sentry.addBreadcrumb({
category: "order",
message: "Order created successfully",
level: "info",
});
return order;
} catch (error) {
Sentry.captureException(error, {
extra: {
userId,
itemCount: items.length,
totalAmount: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
},
tags: { errorType: "order_creation", severity: "high" },
});
throw error;
}
}
}
// Custom error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
Sentry.captureException(err);
res.status(500).json({ error: "Internal server error" });
});
Java: Spring Boot
// SentryConfig.java — Sentry SDK for Spring Boot
import io.sentry.Sentry;
import io.sentry.spring.tracing.SentryTracingConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SentryConfig {
@Bean
public Sentry.OptionsConfiguration sentryOptions() {
return options -> {
options.setDsn("https://your-dsn@sentry.io/123");
options.setEnvironment("production");
options.setRelease("order-service@1.2.3");
options.setTracesSampleRate(0.1);
options.setBeforeSend((event, hint) -> {
// Redact sensitive data
if (event.getRequest() != null) {
event.getRequest().getHeaders().remove("Authorization");
event.getRequest().getHeaders().remove("Cookie");
}
return event;
});
};
}
}
// Service code with manual capture
@Service
public class OrderService {
public Order createOrder(Long userId, List<OrderItem> items) {
Sentry.setUser(new Sentry.User(
String.valueOf(userId),
"user@example.com",
null,
null
));
Sentry.addBreadcrumb("Creating order for user " + userId, "order");
try {
Order order = processOrder(userId, items);
Sentry.addBreadcrumb("Order created successfully", "order");
return order;
} catch (PaymentException e) {
Sentry.captureException(e, Scope -> {
Scope.setExtra("user_id", userId);
Scope.setExtra("items_count", items.size());
Scope.setTag("error_type", "payment");
});
throw e;
}
}
}
Release Tracking and Source Maps
Release tracking
# Create a Sentry release
sentry-cli releases new "order-service@1.2.3"
# Associate commits with the release
sentry-cli releases set-commits "order-service@1.2.3" \
--auto
# Finalize the release
sentry-cli releases finalize "order-service@1.2.3"
# Mark deployment
sentry-cli releases deploys "order-service@1.2.3" new \
--env production \
--url "https://order-service.example.com"
Source maps for JavaScript
// webpack.config.js — Upload source maps to Sentry
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
module.exports = {
devtool: "source-map",
plugins: [
new SentryWebpackPlugin({
org: "your-org",
project: "api-gateway",
authToken: process.env.SENTRY_AUTH_TOKEN,
release: "api-gateway@1.2.3",
include: "./dist",
ignore: ["node_modules", "webpack.config.js"],
// Strip server-side code paths
urlPrefix: "~/static/js",
}),
],
};
CI/CD integration
# .github/workflows/deploy.yml — Sentry release in CI
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Create Sentry release
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
npm run build
npx sentry-cli releases new "api-gateway@${{ github.sha }}"
npx sentry-cli releases set-commits "api-gateway@${{ github.sha }}" --auto
npx sentry-cli releases files "api-gateway@${{ github.sha }}" upload-sourcemaps ./dist
npx sentry-cli releases finalize "api-gateway@${{ github.sha }}"
- name: Deploy
run: npm run deploy
- name: Mark Sentry deploy
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
npx sentry-cli releases deploys "api-gateway@${{ github.sha }}" new \
--env production
Performance Monitoring
# Python: Custom transactions and spans
import sentry_sdk
@sentry_sdk.trace
def process_order(user_id: int, items: list[dict]) -> dict:
with sentry_sdk.start_span(op="db", description="query_user") as span:
user = db.query(User).get(user_id)
span.set_data("user.id", user_id)
with sentry_sdk.start_span(op="http.client", description="charge_payment") as span:
payment = payment_client.charge(user_id, total)
span.set_data("payment.amount", payment["amount"])
with sentry_sdk.start_span(op="db", description="save_order") as span:
order = order_repo.create(user_id, items, payment["id"])
span.set_data("order.id", order["id"])
return order
// Node.js: Custom spans
import * as Sentry from "@sentry/node";
async function processOrder(userId: string, items: OrderItem[]): Promise<Order> {
return Sentry.startSpan({ op: "function", name: "process_order" }, async () => {
const user = await Sentry.startSpan(
{ op: "db", name: "query_user" },
async () => await userRepo.findById(userId)
);
const payment = await Sentry.startSpan(
{ op: "http.client", name: "charge_payment" },
async () => await paymentService.charge(userId, total)
);
const order = await Sentry.startSpan(
{ op: "db", name: "save_order" },
async () => await orderRepo.create({ userId, items, paymentId: payment.id })
);
return order;
});
}
Alerting Rules
# Sentry alert rules (configured in Sentry UI or via API)
# Example: Alert on high error rate
rules:
- name: "High error rate"
conditions:
- event_level: error
- threshold: 10 # errors in 1 hour
actions:
- notify_slack: "#alerts"
- notify_email: ["oncall@company.com"]
- name: "New error in production"
conditions:
- event_level: error
- environment: production
- is_new: true
actions:
- notify_slack: "#engineering"
- create_issue: true
- name: "Performance regression"
conditions:
- metric: p95_duration
- threshold: 2000 # ms
- comparison: greater_than
actions:
- notify_slack: "#performance"
Triage Workflow
Once the alert hits Slack, email, or PagerDuty, open the issue in Sentry and read the data: the stack trace tells you where the code broke, breadcrumbs replay what led to the crash, user context shows who was affected, request context shows the input, the release tag tells you which version introduced the bug, and tags let you filter by environment, service, or error type. Assign the issue, link it to a Jira or GitHub ticket, write the fix, and ship it. Sentry notices the new release and closes the issue automatically if no events arrive for 72 hours.
Not all issues deserve the same urgency. Sort by impact: how many users hit the error, how often it fires, and whether
it blocks a critical path. A payment error hitting 1,000 users an hour is a P0. A CSS glitch affecting 3 users a week
is a P3. Sentry’s “frequent issues” and “regression” views help you spot the high-impact ones fast.
Tag issues with priority:p0, priority:p1, etc. so your team can filter the queue by urgency instead of by recency.
Best Practices
Always set the release tag; Sentry uses it to auto-resolve issues once the fix is in production. Upload source maps for
minified JavaScript and TypeScript so stack traces point to the actual source. Use before_send to scrub PII like
passwords, tokens, and credit card numbers before the event leaves your server.
Set user context as soon as the request lands, so you can see who got hit. Drop breadcrumbs around operations like
database queries, API calls, and state changes, so the path to the error stays visible. Use tags like service, endpoint, feature_flag, and user_tier to
slice the data. If Grafana is also part of your observability stack, see the
Complete Guide to Observability with the Grafana Stack. For
structured logging that complements Sentry’s error capture, check the
Complete Guide to Structured Logging. If you’re tracing requests across
services, the Complete Guide to Distributed Tracing explains how to
correlate Sentry events with trace IDs.
For transaction sampling, use 10% in high-volume services, and 100% in low-traffic or critical paths. When the default grouping misses, use custom fingerprinting. Set up alerts for new errors so regressions don’t wait for a user report, and filter by environment so development noise stays out of the production project.
Common Mistakes
Without release tracking, you can’t tell which version introduced a bug. Set release every time you initialize the SDK.
Without source maps, minified stack traces are nearly useless. Upload them in your CI pipeline, not by hand after the fact.
Cranking the sample rate to 100% everywhere just creates noise. Use 10% trace sampling for high-traffic services and raise it to 100% only for low-traffic or critical paths.
Failing to redact PII sends passwords, tokens, and personal information to Sentry. Use the before_send hook to strip
sensitive fields before the event leaves your server.
It’s tempting to skip breadcrumbs, but they show the path to the error. Add them for the operations that matter, and keep their messages short enough to scan.
Resist the urge to pile every service into one Sentry project; that just mixes unrelated errors. Split projects by service or team so triage stays focused.
Sampling Strategies
Sampling controls how many transactions and errors Sentry receives. Get it wrong and you either drown in noise or miss
the errors that matter. The SDK gives you two knobs: sample_rate for errors and traces_sample_rate for transactions.
Run errors at 100% (sample_rate=1.0) in production. You want every crash, every time. The volume is naturally
lower than transactions, and missing an error means missing a user-facing bug.
Transactions are different. A high-traffic API can generate millions of transactions per hour, and Sentry’s pricing
scales with event volume. Set traces_sample_rate=0.1 (10%) for high-traffic services. For low-traffic or critical
paths (payments, auth, admin endpoints), push it to 1.0 (100%) so you catch every slow request.
# Dynamic sampling based on route
def traces_sampler(sampling_context):
transaction = sampling_context.get("transaction_context", {})
name = transaction.get("name", "")
# 100% for critical paths
if name.startswith("/api/payment") or name.startswith("/api/auth"):
return 1.0
# 1% for health checks and static assets
if name.startswith("/health") or name.startswith("/static"):
return 0.01
# 10% for everything else
return 0.1
sentry_sdk.init(
dsn="https://your-dsn@sentry.io/123",
traces_sample_rate=0.1,
traces_sampler=traces_sampler,
)
Dynamic sampling lets you override the global rate per transaction. The sampler function receives context about the transaction (name, URL, method, headers) and returns a rate between 0.0 and 1.0. This keeps costs down on noisy endpoints while keeping full visibility on the critical ones.
Custom Fingerprinting
Sentry groups errors by stack trace similarity. Most of the time this works, but some errors need manual grouping. A database timeout thrown from the same code path but with different queries gets split into dozens of issues. Custom fingerprinting fixes this by overriding the default grouping.
# Python: Custom fingerprint via before_send
def before_send(event, hint):
if "exception" in event:
exc = event["exception"]["values"][0]
# Group all database timeouts together
if exc.get("type") == "TimeoutError":
event["fingerprint"] = ["database-timeout"]
# Group by error type and endpoint
elif exc.get("type") == "HTTPError":
endpoint = event.get("request", {}).get("url", "unknown")
event["fingerprint"] = ["http-error", endpoint]
return event
sentry_sdk.init(
dsn="https://your-dsn@sentry.io/123",
before_send=before_send,
)
// Node.js: Custom fingerprint via beforeSend
function beforeSend(event: any) {
if (event.exception?.values?.[0]) {
const exc = event.exception.values[0];
if (exc.type === "TimeoutError") {
event.fingerprint = ["database-timeout"];
} else if (exc.type === "HTTPError") {
const endpoint = event.request?.url || "unknown";
event.fingerprint = ["http-error", endpoint];
}
}
return event;
}
Sentry.init({
dsn: "https://your-dsn@sentry.io/123",
beforeSend,
});
Set fingerprints in before_send when the default grouping scatters similar errors across too many issues. Keep
fingerprints short and descriptive: ["database-timeout"], ["http-error", endpoint], ["payment-failure", error_code]. Avoid fingerprints that change per request (URLs with IDs, timestamps) because they defeat grouping.
Troubleshooting Source Maps
Source maps break more often than they work. When they break, you see minified stack traces like app.a1b2c3d.js:1:4567
instead of OrderService.ts:42. Let’s debug them.
Check 1: Are source maps uploaded? Run sentry-cli releases files "your-release" list and verify the .map
files appear. If not, the upload step in your CI failed silently.
Check 2: Does the release tag match? The SDK’s release value must match the release you uploaded source maps
to. A mismatch (e.g., api-gateway@1.2.3 vs api-gateway@abc123) means Sentry can’t find the maps.
Check 3: Is urlPrefix correct? The urlPrefix in the upload command must match how your server serves the
files. If your JS lives at https://cdn.example.com/static/js/app.js, set urlPrefix: "~/static/js". A wrong
prefix means Sentry looks for maps at the wrong URL.
Check 4: Are source maps valid? Run npx source-map-explorer dist/app.js dist/app.js.map locally. If it fails,
your build tool generated broken maps. Check your devtool: "source-map" setting in webpack or vite.
# Debug source map upload
sentry-cli releases files "api-gateway@1.2.3" list
sentry-cli releases files "api-gateway@1.2.3" upload-sourcemaps ./dist \
--url-prefix "~/static/js" \
--rewrite \
--validate
The --validate flag checks that the uploaded maps are valid before finalizing. Use it in CI to catch broken
uploads before they reach production.
Privacy and PII Scrubbing
Sentry captures request bodies, headers, and user context by default. Great for debugging, but it means passwords, tokens, and personal data can leak into your Sentry project. GDPR and CCPA make this a legal problem, not just a technical one.
The before_send hook is your last line of defense. It runs before every event leaves your server, giving you a
chance to strip sensitive fields. The hook receives the full event and a hint with the original exception.
# Python: Comprehensive PII scrubbing
import re
SENSITIVE_HEADERS = {"authorization", "cookie", "x-api-key", "x-auth-token"}
SENSITIVE_BODY_FIELDS = {"password", "confirm_password", "credit_card", "ssn", "api_key", "token"}
SENSITIVE_QUERY_PARAMS = {"token", "api_key", "session_id"}
def scrub_pii(event, hint):
# Scrub headers
request = event.get("request", {})
headers = request.get("headers", {})
for key in list(headers.keys()):
if key.lower() in SENSITIVE_HEADERS:
headers[key] = "[REDACTED]"
# Scrub body fields
body = request.get("data", {})
if isinstance(body, dict):
for key in SENSITIVE_BODY_FIELDS:
if key in body:
body[key] = "[REDACTED]"
# Scrub query string
query_string = request.get("query_string", "")
if query_string:
for param in SENSITIVE_QUERY_PARAMS:
query_string = re.sub(
rf"{param}=[^&]+",
f"{param}=[REDACTED]",
query_string,
flags=re.IGNORECASE,
)
request["query_string"] = query_string
# Scrub user email to just domain
user = event.get("user", {})
if "email" in user:
email = user["email"]
if "@" in email:
domain = email.split("@")[1]
user["email"] = f"[redacted]@{domain}"
return event
sentry_sdk.init(
dsn="https://your-dsn@sentry.io/123",
before_send=scrub_pii,
send_default_pii=False,
)
Set send_default_pii=False to stop Sentry from collecting IPs and cookies by default. Then use
before_send to add back only what you need, scrubbed. For high-compliance environments (healthcare, finance), pair
this with a self-hosted Sentry instance so event data never leaves your infrastructure.
Test your scrubbing with synthetic events before deploying. Send a fake event with a password field and verify it
arrives in Sentry as [REDACTED]. If your before_send hook crashes silently, you’ll lose events entirely. Always
return the event (even on error) and log failures separately.
Summary
- Set the
releasetag on every SDK init so Sentry can auto-resolve issues when the fix ships. - Upload source maps in CI, not by hand, so minified stack traces stay readable.
- Use
before_sendto scrub PII (passwords, tokens, credit cards) before events leave your server. - Sample at 10% for high-traffic services, and 100% for low-traffic or critical paths.
- Add breadcrumbs around DB queries, API calls, and state changes to reconstruct the path to the error.
- Split Sentry projects by service or team so triage stays focused.
- Set alerts on new errors so regressions don’t wait for a user report.
- Use custom fingerprinting when the default grouping misses.
See Also
- Sentry official documentation: full reference for SDKs, CLI, and platform integrations.
- Sentry SDK for Python: Python-specific setup, integrations, and performance monitoring.
- Sentry SDK for Node.js: Node.js setup, Express/Fastify integrations, and source maps.
- Sentry SDK for Java: Spring Boot integration, tracing, and manual capture.
- Sentry CLI documentation: release management, source map uploads, and deploys.
- Sentry source maps guide: how source maps work and how to upload them.
Frequently Asked Questions
What is Sentry?
Sentry tracks errors and watches performance in production apps. It grabs exceptions, crashes, and performance issues with stack traces, breadcrumbs, user context, and release tracking.
How does Sentry differ from logging?
Logging writes events to an aggregation system for audit and debugging. Sentry grabs errors and performance problems with enough context to triage them fast. Use both.
What are breadcrumbs?
Breadcrumbs are the trail of events that led to an error: HTTP requests, database queries, UI clicks, and log messages. They trace the sequence of actions that led to the issue.
How does Sentry resolve issues?
Sentry marks an issue resolved when you deploy a new release and no new events show up for a set period, usually 72 hours. Or close them yourself.
What is Sentry's pricing model?
Sentry has a free tier, paid Team and Business tiers, and Enterprise plans. Pricing scales with the number of events you send, and you can self-host the open-source version if you need full control.
Why should I use Sentry instead of just logging?
Logging records events for audit and debugging, but it doesn't group, deduplicate, or alert on errors. Sentry adds the triage layer: it groups similar errors into issues, shows you the exact commit that introduced a bug, and pings your team when something new breaks. Run both: logs for audit, Sentry for triage.
Can I self-host Sentry?
Yes. Sentry's open-source version runs self-hosted via Docker. It's got most of what the cloud tier ships with, but you manage upgrades, storage, and scaling yourself. For teams under 50 engineers, the free cloud tier is usually simpler.
Related Resources
Structured Logging: JSON Logs, Correlation IDs, Aggregation
Master structured logging with JSON format, correlation IDs, log levels, and aggregation. Covers Python structlog, Node.js pino, Java SLF4J, ELK and Loki stacks.
GuideDistributed Tracing: OpenTelemetry, Jaeger, Zipkin
Master distributed tracing with OpenTelemetry, Jaeger, and Zipkin. Trace propagation across services, span context, sampling strategies, and production debugging.
RecipeReal User Monitoring (RUM) Setup with web-vitals
Monitor actual user experiences with Core Web Vitals, session replay, and performance analytics to identify real-world bottlenecks.
GuideComplete Guide to Monitoring and Alerting
Build a production monitoring stack. Covers Prometheus, Grafana, AlertManager, metrics instrumentation, alert rules, runbooks, SLI/SLO/SLA, distributed tracing with Jaeger, log aggregation, and on-call best practices with practical configuration examples.
GuideComplete Guide to Observability with the Grafana Stack
Set up metrics, logs, and traces with Grafana, Prometheus, Loki, and Tempo. Covers instrumentation, dashboards, alerting, and distributed tracing for production systems.
GuidePrometheus and Grafana: Metrics, Dashboards, Alerting
Instrument services with Prometheus metrics, query them in PromQL, build Grafana dashboards, and ship alerting rules that don't flap.