sentry/sentry-symfony
The official Sentry SDK for Symfony. Install via Composer, add your SENTRY_DSN to .env, and capture exceptions from anywhere in your app. Get real-time error tracking and performance monitoring with minimal setup.
Installation:
composer require sentry/sentry-symfony
Add to .env:
###> sentry/sentry-symfony ###
SENTRY_DSN="https://<key>@<project>.ingest.sentry.io/<id>"
###< sentry/sentry-symfony ###
First Use Case: Capture an exception in a controller:
use function Sentry\captureException;
try {
$result = $this->riskyOperation();
} catch (\Throwable $e) {
captureException($e);
throw $e; // Re-throw if needed
}
Auto-Instrumentation: The bundle automatically captures:
Scrub Sensitive Data Up Front (v5.14+): Before you send your first event, decide what data is allowed to leave the app:
# config/packages/sentry.yaml
sentry:
options:
data_collection:
user_info: true
http_headers:
mode: allowList
terms:
- x-request-id
http_bodies: []
database_query_data: false
config/packages/sentry.yaml (auto-generated)Sentry\SentryBundle\SentryBundle (main bundle)Sentry\SentryBundle\EventListener\ExceptionListener (exception handling)Sentry\SentryBundle\EventListener\RequestListener (HTTP instrumentation)Sentry\SentryBundle\EventListener\UserContextListener (user/IP context, now runs pre-routing)// Manual capture (recommended for business logic errors)
use function Sentry\captureException;
use function Sentry\captureMessage;
try {
$user = User::findOrFail($id);
} catch (\InvalidArgumentException $e) {
captureMessage('Invalid user ID format', ['user_id' => $id]);
throw $e;
}
// Auto-capture (for framework exceptions)
public function __invoke(Request $request, $id)
{
// No try-catch needed; exceptions bubble up to Sentry
return $this->userService->get($id);
}
use function Sentry\trace;
trace('user.profile', function () use ($userId) {
$user = User::find($userId);
$orders = $user->orders()->limit(10)->get();
return ['user' => $user, 'orders' => $orders];
});
use function Sentry\trace_metrics;
trace_metrics()->count('api.calls', 1, ['endpoint' => 'users']);
trace_metrics()->gauge('queue.size', $queue->count(), ['queue' => 'orders']);
trace_metrics()->distribution('response.time', $durationMs, ['unit' => 'ms']);
Centralize PII/secret handling instead of hand-scrubbing every event:
# config/packages/sentry.yaml
sentry:
options:
data_collection:
user_info: true # bool — attach id/username/email/ip
http_headers:
mode: allowList # or denyList
terms:
- x-request-id
- traceparent
http_bodies: [] # empty = never attach request/response bodies
database_query_data: false
Typical patterns:
# Strict production posture: no bodies, no query params, headers by allow-list only
data_collection:
user_info: true
http_headers: { mode: allowList, terms: [x-request-id] }
http_bodies: []
database_query_data: false
# Deny-list posture for internal/staging environments
data_collection:
user_info: true
http_headers:
mode: denyList
terms: [authorization, cookie, x-api-key]
http_bodies: ['application/json']
database_query_data: true
# config/packages/monolog.yaml
monolog:
handlers:
sentry:
type: service
id: Sentry\SentryBundle\Monolog\LogsHandler
arguments:
- !php/const Monolog\Logger::DEBUG
$this->logger->info('User logged in', [
'user_id' => $user->id,
'ip' => $request->getClientIp(),
'metadata' => ['device' => $deviceType]
]);
# config/packages/sentry.yaml
sentry:
options:
integrations:
- Sentry\Integration\Doctrine\DoctrineIntegration
Pair
DoctrineIntegrationwithdata_collection.database_query_data: falseto keep SQL text/parameters out of spans while retaining timing.
sentry:
messenger:
isolate_context_by_message: true # Prevents scope leakage
services:
App\Sentry\CustomIntegration:
tags: ['sentry.integration']
sentry:
options:
integrations: ['App\Sentry\CustomIntegration']
use Sentry\State\Scope;
public function __invoke(Request $request, Response $response, callable $next): Response
{
$scope = Scope::getCurrentScope();
$scope->setTag('api_version', $request->headers->get('X-API-Version'));
return $next($request, $response);
}
User context is now set before the router runs, so 404/405 events include the client IP. If you maintain a custom kernel.exception listener that overwrites the user scope, run it after UserContextListener (default priority) or you will lose the IP on unmatched routes.
# config/packages/sentry.yaml
sentry:
dsn: "%env(SENTRY_DSN)%"
options:
traces_sample_rate: "%env(float::SENTRY_TRACES_SAMPLE_RATE)%"
# config/packages/sentry.yaml
sentry:
options:
data_collection:
user_info: true
http_headers:
mode: allowList
terms: ['x-request-id']
http_bodies: []
database_query_data: "%kernel.debug%"
sentry:
options:
integrations:
%kernel.debug% ? [] : ['Sentry\Integration\Doctrine\DoctrineIntegration']
sentry:
options:
ignore_exceptions:
- Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
- App\Exception\ValidationException
DSN Validation:
SENTRY_DSN in .env causes silent failures.SENTRY_DSN="https://invalid@localhost" in dev to test error handling.Performance Overhead:
traces_sample_rate (default: 1.0):
sentry:
options:
traces_sample_rate: 0.1 # 10% sampling
Sensitive Data in Errors:
data_collection once and stop writing regex scrubbers:
sentry:
options:
data_collection:
user_info: true
http_headers: { mode: allowList, terms: [x-request-id] }
http_bodies: []
database_query_data: false
before_send for app-specific keys the SDK can't know about:
Sentry\configureScope(function (Scope $scope) {
$scope->setBeforeSendCallback(function ($event) {
unset($event['extra']['password']);
return $event;
});
});
Messenger Context Leakage:
sentry:
messenger:
isolate_context_by_message: true
Cache Instrumentation:
NamespacedPoolInterface caches breaking instrumentation.TraceableCacheAdapterForV3 (v5.3+):
$cache = new TraceableCacheAdapterForV3($pool);
Over-Restrictive data_collection:
http_bodies: [] and an empty header allow-list makes 4xx/5xx issues hard to triage ("what payload caused this 422?").%kernel.debug%, and mirror the config in your staging Sentry project so it's obvious which posture produced an event.Local Testing:
SENTRY_DSN="http://localhost:3000" with Sentry Mock Server.sentry:
options:
logger: 'sentry.logger'
services:
sentry.logger:
class: 'Sentry\Logger\DebugFileLogger'
arguments:
$filePath: '%kernel.logs_dir%/sentry.log'
Event Inspection:
Sentry\configureScope(function (Scope $scope) {
$scope->setBeforeSendCallback(function ($event) {
file_put_contents(
__DIR__.'/sentry_event.json',
json_encode($event, JSON_PRETTY_PRINT)
);
return null; // Prevent sending
});
});
data_collection actually redacted the fields you expect before shipping the config.Performance Profiling:
sentry-performance metrics in Sentry dashboard.X-Sentry-Trace header to correlate requests:
$scope->setTag('sentry.trace_id', $request->headers->get('X-Sentry-Trace'));
Missing IP on 404s:
user.ip_address on an unmatched route means a custom listener is clobbering the scope after UserContextListener. Check listener priorities first, not config.max_breadcrumbs:
100 (no upper limit in v5.6+).sentry:
options:
max_breadcrumbs: 500
ignore_errors vs ignore_exceptions:
ignore_errors: Catches set_error_handler errors (legacy).ignore_exceptions: Uses is_a() for type matching (recommended).Monolog Levels:
'info'), PSR constants (Psr\Log\LogLevel::INFO), or Monolog constants (Monolog\Level::INFO).data_collection Structure:
user_info and database_query_data are plain booleans; http_headers is a {mode, terms} map and http_bodies is a list of content types.http_headers.mode is case-sensitive (allowList / denyList); a typo silently falls back to the SDK default rather than erroring.sentry-php >= 4.34.0 — pin accordingly if you constrain the SDK transitively.before_send; anything you put into extra yourself is still sent verbatim.OTLP Integration:
sentry-php v4.23+:
sentry:
options:
integrations:
- Sentry\Integration\OTLPIntegration
Custom Integrations:
Sentry\IntegrationInterface:
class MyIntegration implements IntegrationInterface {
public function setupOnce(): void {
// One-time setup (e.g., register error handler)
}
public function setup(): void {
// Per-request setup (e.g., add breadcrumbs)
}
}
before_send for App-Specific Redaction:
data_collection for anything the SDK can't classify:
$scope->setBeforeSendCallback(function ($event) {
if (isset($event['extra']['card_number'])) {
unset($event['extra']['card_number']);
}
return $event;
});
User Context:
LoginSuccessEvent); as of v5.14 the SDK seeds it before routing, so events raised during routing/404 handling inherit it.How can I help you explore Laravel packages today?