Optimize Sentry's performance monitoring pipeline to maximize signal quality while minimizing SDK overhead and event volume costs. Covers the v8 SDK API for @sentry/node, @sentry/browser, and sentry-sdk (Python), targeting sentry.io or self-hosted Sentry 24.1+.
Sentry.init() called with a valid DSN before any application code runs
Performance monitoring enabled (tracesSampleRate > 0 or a tracesSampler function)
Access to the Sentry Performance dashboard to verify changes
Instructions
Step 1 — Replace Static tracesSampleRate with Dynamic tracesSampler
A flat tracesSampleRate: 0.1 samples all routes equally. The tracesSampler callback makes per-transaction decisions based on route, operation type, and upstream trace context.
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
// tracesSampler replaces tracesSampleRate — do not set both
tracesSampler: (samplingContext) => {
const { name, attributes, parentSampled } = samplingContext;
// Honor parent sampling for distributed trace consistency
if (parentSampled !== undefined) return parentSampled ? 1.0 : 0;
// Drop noise — health probes, static assets
if (name?.match(/\/(health|ready|alive|ping|metrics)$/)) return 0;
if (name?.match(/\.(js|css|png|jpg|svg|woff2?|ico)$/)) return 0;
// Always sample business-critical paths
if (name?.includes('/checkout') || name?.includes('/payment')) return 1.0;
// Higher sampling for write operations (mutations are riskier)
if (name?.startsWith('POST ') || name?.startsWith('PUT ')) return 0.25;
// Moderate sampling for read APIs
if (name?.startsWith('GET /api/')) return 0.1;
// Low sampling for background work
if (name?.startsWith('job:') || name?.startsWith('queue:')) return 0.05;
// User-tier sampling (via custom attributes from middleware)
if (attributes?.['user.plan'] === 'enterprise') return 0.5;
return 0.05; // Default: 5%
},
});
Step 2 — Configure Profiling with profilesSampleRate
The profilesSampleRate controls what fraction of traced transactions get profiled. Setting it to 1.0 with a 5% tracesSampler means 5% of traffic is profiled.
import { nodeProfilingIntegration } from '@sentry/profiling-node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
tracesSampler: (ctx) => { /* ... from Step 1 ... */ },
// Effective rate = tracesSampler rate * profilesSampleRate
profilesSampleRate: 1.0,
// Alternative: Continuous profiling (v8.7.0+) — profiles the entire process
// profileSessionSampleRate: 0.1, // 10% of server instances
});
Tuning: Start at profilesSampleRate: 0.1 in production. Profiling adds ~3-5% CPU overhead per profiled transaction. Continuous profiling (profileSessionSampleRate) has lower per-transaction cost but runs on sampled instances continuously.
Names with dynamic IDs (/api/users/12345) create thousands of unique entries, degrading dashboard performance and inflating quota. Route templates go in the name, dynamic values go in attributes.
// BAD — creates thousands of unique transaction entries
// GET /api/users/12345, GET /api/users/67890, ...
// GOOD — Sentry auto-parameterizes Express/Koa/Fastify routes
// GET /api/users/:userId
// For custom spans, always parameterize:
Sentry.startSpan(
{
name: 'order.process', // No dynamic IDs in name
op: 'task',
attributes: {
'order.id': orderId, // Filterable in Discover queries
'order.total_cents': totalCents,
'customer.tier': customerTier,
},
},
async (span) => {
const result = await processOrder(orderId);
span.setAttribute('order.status', result.status);
return result;
}
);
Detect cardinality issues with a Discover query:
SELECT count(), transaction FROM transactions GROUP BY transaction ORDER BY count() DESC
Step 4 — Add Custom Measurements
Custom measurements appear in the Performance dashboard and can be charted, alerted on, and queried in Discover. Unit types: 'millisecond', 'byte', 'none' (count), 'percent'.
-- Slowest transactions (p95)
SELECT transaction, p95(transaction.duration), count()
FROM transactions WHERE transaction.duration:>1000
ORDER BY p95(transaction.duration) DESC
-- Regression detection (20%+ slower vs last week)
SELECT transaction, p75(transaction.duration),
compare(p75(transaction.duration), -7d) as vs_last_week
FROM transactions GROUP BY transaction
HAVING compare(p75(transaction.duration), -7d) > 1.2
-- Span breakdown for a route
SELECT span.op, span.description, p75(span.duration), count()
FROM spans WHERE transaction:/api/checkout
ORDER BY p75(span.duration) DESC
Output
Dynamic sampling active — health checks at 0%, payments at 100%, defaults at 5%
Profiling enabled with profilesSampleRate or continuous profileSessionSampleRate
Transaction names parameterized — cardinality under 500 unique names
Custom measurements tracking business KPIs alongside latency