Sentry: never miss an error again
Sentry is an error monitoring tool that captures, aggregates and alerts on your application's exceptions in real time.
Without a tool like this, a production error is discovered in one of two ways: a user reports it, or someone eventually reads the logs. Either way, most of the context is missing. Sentry changes that: every exception is sent with its full stack trace, the HTTP request, the affected user, the deployed version and the environment. Identical occurrences are grouped into a single issue, so you can immediately see that an error has affected hundreds of users since the last deployment, instead of receiving hundreds of emails.
Sentry is available as SaaS and as a self-hosted version. The SDK and configuration shown here are identical in both cases; only the DSN changes.
Symfony installation
composer require sentry/sentry-symfony
The package installs the PHP SDK and the Symfony bundle. With Symfony Flex, the recipe creates the configuration file and adds the SENTRY_DSN variable to the .env file. The DSN is the address the SDK sends events to; you will find it in the Sentry project settings. Leave it empty in development: without a DSN, no events are sent.
# .env
SENTRY_DSN=
APP_VERSION=dev
# .env.local on the production server (or environment variables)
SENTRY_DSN=https://publicKey@o0.ingest.sentry.io/0
APP_VERSION=1.4.2
Configuration
# config/packages/sentry.yaml
sentry:
dsn: '%env(SENTRY_DSN)%'
options:
environment: '%kernel.environment%'
release: '%env(APP_VERSION)%'
traces_sample_rate: 0.2
profiles_sample_rate: 0.1
Each option has a specific role:
environmentseparates production, staging and test errors in the interface, so you can filter easily and alert on production only.releaseties each error to the deployed version. This is what allows Sentry to report that an error appeared with a given release, or that an error marked as resolved has come back.traces_sample_ratesets the proportion of requests for which a performance trace is recorded:0.2means 20%. Errors, on the other hand, are always all sent. On a high-traffic site, a low value is enough and preserves your quota.profiles_sample_rateenables profiling for a share of traced requests. It requires theexcimerPHP extension on the server; without it, the option has no effect.
Some exceptions do not deserve an alert: a page not found or an access denied are part of normal operation. It is better to ignore them explicitly, in production only:
# config/packages/prod/sentry.yaml
sentry:
options:
ignore_exceptions:
- Symfony\Component\HttpKernel\Exception\NotFoundHttpException
- Symfony\Component\Security\Core\Exception\AccessDeniedException
User context
Knowing that an error occurred is useful; knowing who it affects is even more so. This listener adds the logged-in user to the Sentry scope on every request:
use Sentry\State\Scope;
class SentryUserListener
{
#[AsEventListener(event: KernelEvents::REQUEST)]
public function onRequest(RequestEvent $event): void
{
$user = $this->security->getUser();
if ($user) {
\Sentry\configureScope(function (Scope $scope) use ($user): void {
$scope->setUser([
'id' => $user->getId(),
'email' => $user->getEmail(),
]);
});
}
}
}
Mind the GDPR: the email address is personal data that will be stored by Sentry. If the ID is enough to find the account in your back office, send only that. Also check the send_default_pii option, disabled by default, which controls the automatic sending of information such as the IP address.
Enriching a manually captured error
Not every error surfaces as an unhandled exception. When you catch an exception to show a clean message to the user, still send it to Sentry, along with the business context useful for diagnosis:
use Sentry\State\Scope;
try {
$this->paymentGateway->charge($order);
} catch (PaymentException $e) {
\Sentry\withScope(function (Scope $scope) use ($e, $order): void {
$scope->setTag('payment.provider', $order->getProvider());
$scope->setContext('order', [
'id' => $order->getId(),
'amount' => $order->getAmount(),
]);
\Sentry\captureException($e);
});
throw new OrderNotPaidException($order, previous: $e);
}
withScope creates a temporary scope: the tag and context only apply to this event, without polluting subsequent errors. Tags are indexed and used to filter or group issues; context is simply displayed in the event details.
Alerts and notifications
- Set up Slack alerts for new errors
- Define error volume thresholds
- Use performance traces to identify bottlenecks
- Integrate with your GitHub workflow to track fixes
The classic trap is alert fatigue: if every error triggers a notification, the team ends up ignoring them all. I recommend alerting on new issues and regressions in production, and reserving volume alerts for critical flows such as payment or authentication.
Performance Monitoring
Beyond errors, Sentry measures the duration of requests and their steps. The Symfony bundle automatically traces HTTP requests and, depending on configuration, Doctrine queries, cache and outgoing HTTP calls. For a specific business operation, you can create your own transaction with the context objects of version 4 of the PHP SDK, and wrap each step in a span with the \Sentry\trace() function:
use Sentry\SentrySdk;
use Sentry\Tracing\SpanContext;
use Sentry\Tracing\TransactionContext;
$transaction = \Sentry\startTransaction(
TransactionContext::make()
->setName('process-order')
->setOp('task')
);
SentrySdk::getCurrentHub()->setSpan($transaction);
try {
$orders = \Sentry\trace(
fn () => $this->orderRepository->findPending(),
SpanContext::make()->setOp('db.query')->setDescription('Pending orders'),
);
// ... process the orders
} finally {
$transaction->finish();
}
The finally block guarantees the transaction is closed even if an exception occurs: a transaction that is never finished is never sent.
Linking errors to deployments
The release option shows its full value when the release is also declared in Sentry with its commits. Sentry can then suggest the commit most likely responsible for an error. With sentry-cli, in the deployment pipeline:
export SENTRY_AUTH_TOKEN=... # API token, stored in the CI secrets
export SENTRY_ORG=my-organization
export SENTRY_PROJECT=my-project
VERSION="$(git rev-parse --short HEAD)"
sentry-cli releases new "$VERSION"
sentry-cli releases set-commits "$VERSION" --auto
sentry-cli releases finalize "$VERSION"
The same value must be passed to the application through APP_VERSION, otherwise errors will not be attached to the right release.
Production checklist
- DSN set only in the environments that should report errors
environmentandreleasefilled in on every deployment- Expected exceptions (404, 403) ignored
- Personal data limited to the strict minimum
- Trace sampling rate adapted to traffic and quota
- Alerts focused on new errors and regressions
Sentry replaces neither logs nor infrastructure monitoring: it will not tell you the disk is full or the server has stopped responding. It does remain the most effective tool for knowing, before your users do, that a line of code is causing trouble in production.