Configuring Error Sampling Rates in Sentry Browser SDK

Sentry exposes three distinct sampling controls — sampleRate, tracesSampleRate, and tracesSampler — each governing a different class of data. Conflating them causes either quota exhaustion or invisible gaps in error coverage. This page covers each option precisely, including dynamic sampling by error severity and user tier. It is a companion to Integrating Observability SDKs: Sentry, Datadog RUM, and OpenTelemetry and the broader Core JavaScript Error Handling & Boundaries architecture.

Sentry Sampling Decision Flow A decision tree showing how Sentry routes captured events. Error events pass through sampleRate (0.0–1.0). Transaction events pass through tracesSampleRate or a tracesSampler function. Both paths end at either "Send to Sentry" or "Drop". Captured Event error | transaction error event transaction sampleRate float 0.0 – 1.0 tracesSampleRate or tracesSampler fn float | function Send Drop Send Drop

Symptom / Trigger

The clearest signal that sampling is misconfigured is a Sentry quota alert or a billing spike immediately after a traffic surge. The other failure mode is the opposite: an error rate that is implausibly flat or zero during an incident, caused by setting sampleRate too low or accidentally using a client-side beforeSend that returns null for all events.

Sentry SDK warning: You have exceeded your error quota for this billing period.
Dropped 1,247 events in the last hour due to rate limiting.

At the SDK level you may also see a console warning in development:

[Sentry] sampleRate must be between 0 and 1. Got: 10. Defaulting to 1.0.

This occurs when developers pass a percentage integer (10 meaning “10%”) instead of a float (0.1). Sentry clamps the value to 1.0, resulting in 100% capture instead of the intended 10%.

Root Cause Explanation

Sentry’s three sampling knobs operate at different stages of the event lifecycle and control independent data types:

Sentry.init({
  sampleRate: 1.0,           // WRONG for high-traffic: 100% of errors, no cap
  tracesSampleRate: 1.0,     // WRONG for high-traffic: 100% of transactions
  // tracesSampler is absent — no per-route logic
});

sampleRate is evaluated before beforeSend. If sampleRate is 0.5 and the random draw fails, beforeSend is never called — the event is discarded before any application logic runs. This means you cannot use beforeSend to “rescue” events that failed the sample draw.

tracesSampleRate and tracesSampler are mutually exclusive. If both are provided, tracesSampler takes precedence entirely and tracesSampleRate is ignored.

Step-by-Step Fix

1. Set sampleRate for error events by severity

The most defensive production configuration captures 100% of unhandled errors and applies a lower rate to manually captured informational events.

import * as Sentry from '@sentry/browser';

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  sampleRate: 1.0,            // capture every unhandled exception
  tracesSampleRate: 0.05,     // only 5% of page loads carry a full transaction
});

// For informational / expected errors, use a manual hint to force a lower rate
function captureExpectedError(err) {
  // Pass a numeric hint that beforeSend can inspect
  Sentry.captureException(err, { tags: { expected: 'true' } });
}

Then in beforeSend, drop expected errors at a configurable rate:

beforeSend(event, hint) {
  // Drop 90% of events tagged as expected to reduce volume
  if (event.tags?.expected === 'true' && Math.random() > 0.1) {
    return null; // drop this event
  }
  return event;
},

2. Replace tracesSampleRate with tracesSampler for per-route control

A flat tracesSampleRate applies the same percentage to every route. A tracesSampler function receives a samplingContext object that includes the transaction name, parent sampling decision, and custom attributes you set on the transaction.

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  sampleRate: 1.0,
  // tracesSampleRate is omitted — tracesSampler takes full control
  tracesSampler(samplingContext) {
    const name = samplingContext.transactionContext?.name ?? '';

    // Health-check and analytics routes generate enormous volume — exclude them
    if (name.startsWith('/healthz') || name.startsWith('/metrics')) {
      return 0; // 0 = always drop
    }

    // Checkout and payment flows are business-critical — always trace
    if (name.startsWith('/checkout') || name.startsWith('/payment')) {
      return 1.0; // 1.0 = always sample
    }

    // If the parent transaction was sampled, propagate that decision
    if (samplingContext.parentSampled !== undefined) {
      return samplingContext.parentSampled ? 1.0 : 0;
    }

    // Default: 2% of all other transactions
    return 0.02;
  },
});

samplingContext.parentSampled reflects the upstream sampling decision carried in the sentry-trace HTTP header. Propagating the parent decision ensures that distributed traces are either fully sampled or fully dropped — partial traces (where only some service hops are sampled) are nearly useless for debugging.

3. Implement dynamic sampling by user tier

Enterprise or paid users typically require higher fidelity error data than anonymous visitors. Attach user tier information to the Sentry scope and read it in tracesSampler.

// After authentication resolves, set user context on the Sentry scope
Sentry.setUser({
  id: user.id,
  email: user.email,         // Sentry scrubs this server-side if PII scrubbing is enabled
  subscription: user.plan,  // 'free' | 'pro' | 'enterprise'
});

// In tracesSampler, read the current scope's user
tracesSampler(samplingContext) {
  const scope = Sentry.getCurrentHub().getScope();
  const user = scope?.getUser();

  if (user?.subscription === 'enterprise') return 0.5;  // 50% for enterprise
  if (user?.subscription === 'pro') return 0.1;          // 10% for pro
  return 0.02;                                            // 2% for free / anonymous
},

This approach keeps sampling logic co-located with SDK configuration rather than scattered across route handlers or middleware.

4. Combine ignoreErrors with sampling for volume control

ignoreErrors is a pre-sampling filter. Patterns in this array are matched against the error message string before sampleRate is evaluated. Removing known-noisy errors here is more efficient than filtering them in beforeSend, because they are discarded without entering the sampling pipeline at all.

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  sampleRate: 1.0,
  tracesSampler(samplingContext) { /* ... */ },
  ignoreErrors: [
    // ResizeObserver warnings from browser extensions
    'ResizeObserver loop limit exceeded',
    'ResizeObserver loop completed with undelivered notifications',
    // Safari-specific non-errors
    /^undefined is not an object \(evaluating 'window\.__gCrWeb/,
    // Network cancellations — not actionable
    'Network request failed',
    'Failed to fetch',
    'Load failed',
  ],
});

Use exact strings for predictable message matches, and regex literals for pattern-based filters. The ignoreErrors filter is case-sensitive and matches against exception.value (the error message), not exception.type (the error class name).

5. Validate the effective sample rate in development

Sentry does not log the sampling decision to the console by default. Add a temporary beforeSend shim during development to confirm events are reaching the hook.

if (import.meta.env.DEV) {
  const originalBeforeSend = options.beforeSend;
  options.beforeSend = (event, hint) => {
    console.debug('[Sentry] beforeSend called for:', event.exception?.values?.[0]?.type);
    return originalBeforeSend ? originalBeforeSend(event, hint) : event;
  };
}

For transaction sampling, wrap tracesSampler in development to log every sampling decision:

const productionSampler = (ctx) => { /* your logic */ };

tracesSampler: import.meta.env.DEV
  ? (ctx) => {
      const decision = productionSampler(ctx);
      console.debug(`[Sentry] tracesSampler: ${ctx.transactionContext?.name}${decision}`);
      return decision;
    }
  : productionSampler,

Remove or tree-shake the debug wrapper before production builds — the import.meta.env.DEV check ensures bundlers eliminate the branch in production mode.

Verification

After deploying a sampling configuration change, use the Sentry Stats dashboard (Settings → Stats → Error counts by project) to confirm the ingestion rate dropped proportionally to the configured sampleRate. For transaction sampling, check Performance → Overview and compare the transaction count before and after.

// Console verification: trigger a known error and confirm it appears in Sentry
Sentry.captureException(new TypeError('sampling-verification-test'));
// Then search Sentry for: 'sampling-verification-test'
// Expected: appears in Issues within 30 seconds

If the event does not appear, check: DSN is correct, sampleRate is not 0, beforeSend does not return null for all events, and the Sentry project is not paused.

Edge Cases & Gotchas

  • sampleRate: 0 silently drops everything. A typo of 0 instead of 1.0 results in zero events reaching Sentry with no SDK warning. Always validate the value comes from an environment variable with a non-zero default.
  • tracesSampler returning undefined is treated as “inherit parent or fall back to 0.” If your sampler function has a code path that returns undefined (for example a missing return in an if branch), those transactions are dropped. Always include an explicit return 0.02 default at the end of the function.
  • Error events created by Sentry.captureException are subject to sampleRate, but events created by integrations (e.g. the BrowserTracing integration’s automatic transaction creation) respect only tracesSampleRate / tracesSampler. These are not interchangeable controls.
  • The ignoreErrors option is evaluated before sampleRate. If an error type matches ignoreErrors, it is dropped before the sample draw and before beforeSend. Use ignoreErrors to eliminate known-noisy browser errors (e.g., ResizeObserver loop limit exceeded) rather than filtering them in beforeSend.

FAQ

Does lowering sampleRate affect the accuracy of Sentry’s error rate graphs? Yes. Sentry adjusts displayed counts using the sample rate as a multiplier, so the dashboard shows estimated event counts rather than raw ingested counts. The estimation is accurate when sampleRate is stable but will be wrong immediately after a rate change until enough samples accumulate.

Can I change sampleRate at runtime without re-initializing the SDK? No. Sentry reads sampling configuration once during Sentry.init. To change rates at runtime, call Sentry.init again with a new configuration — this reinitializes the SDK hub and resets all integrations. A safer pattern is to implement dynamic logic inside a tracesSampler function that reads from a feature flag evaluated on each transaction.

What happens to errors that are dropped by sampleRate? They are discarded client-side and never transmitted. Sentry has no visibility into dropped events. If you need a count of dropped events for capacity planning, implement a lightweight counter in your own analytics pipeline before the Sentry beforeSend hook.