sentry/sdk
Official Sentry SDK for PHP. Capture errors, exceptions, and performance traces from Laravel, Symfony, or plain PHP apps, with breadcrumbs, stack traces, and release tracking to find and fix issues faster.
sentry/sdk contains no code of its own. It is a Composer metapackage that requires sentry/sentry plus a recommended PSR-18/async-capable HTTP client (symfony/http-client). Its entire job is to make composer require produce a working SDK without you having to reason about transports.
The first thing a Laravel developer should do is check whether they need it at all:
sentry/sentry-laravel instead. That package depends on sentry/sentry directly, registers the service provider, wires the exception handler, queue/job context, and Artisan instrumentation. Adding sentry/sdk alongside it is redundant.sentry/sdk is the right, minimal choice.Minimal usage once installed:
composer require sentry/sdk
// bootstrap/app.php or a dedicated bootstrapper
\Sentry\init([
'dsn' => env('SENTRY_DSN'),
'environment' => env('APP_ENV'),
'release' => env('SENTRY_RELEASE'),
'traces_sample_rate' => 0.1,
]);
First useful use case, and the one that gives immediate value:
try {
$this->chargeCustomer($order);
} catch (\Throwable $e) {
\Sentry\captureException($e);
throw $e; // or report and continue
}
Look first at Sentry\init() options (Sentry\Options) and Sentry\ClientBuilder — that is where all day-to-day configuration lives. The sentry/sentry docs are the real documentation; the sentry/sdk README is three paragraphs.
Initialize once, as early as possible. In a Laravel app, do it in a service provider's register() or in bootstrap/app.php before the exception handler can fire. Errors thrown during boot are exactly the ones you want captured.
Automatic capture vs manual. The plain SDK registers a global exception handler only if you call Sentry\init() with 'default_integrations' => true and no framework handler is already installed. Inside Laravel, the framework's handler wins — so use captureException in App\Exceptions\Handler::report(), or use sentry/sentry-laravel and let it do this for you.
Triage with context before the exception:
\Sentry\configureScope(function (\Sentry\State\Scope $scope) use ($user, $order): void {
$scope->setUser(['id' => (string) $user->id]); // never email/name — PII
$scope->setTag('tenant', $user->tenant_id);
$scope->setContext('order', ['id' => $order->id, 'total' => $order->total]);
});
Set this once per request in middleware rather than sprinkling it through controllers.
Breadcrumbs are cheap and high value:
\Sentry\addBreadcrumb(new \Sentry\Breadcrumb(
\Sentry\Breadcrumb::LEVEL_INFO,
\Sentry\Breadcrumb::TYPE_USER,
'billing',
'Retrying charge',
['attempt' => $attempt],
));
Performance tracing. Set traces_sample_rate and wrap slow units of work in transactions — a queued job, an Artisan command, a single expensive report query:
$tx = \Sentry\startTransaction(new \Sentry\Tracing\TransactionContext());
$tx->setName('reports.monthly');
$tx->setOp('task');
\Sentry\SentrySdk::getCurrentHub()->setSpan($tx);
$span = $tx->startChild(new \Sentry\Tracing\SpanContext());
$span->setOp('db.query')->setDescription('aggregate revenue');
// ... work ...
$span->finish();
$tx->finish();
HTTP client choice. This is the metapackage's one real decision. sentry/sdk pulls symfony/http-client, which supports background/async sending and is generally the better default under PHP-FPM. If you already ship Guzzle and want to avoid a second HTTP stack, composer require sentry/sentry guzzlehttp/guzzle and skip the metapackage — the SDK discovers PSR-18 clients. The transport is swappable via ClientBuilder::setTransportFactory() if you want to route events through your own proxy.
Filtering beats muting. Use before_send for noise you can't stop emitting:
'before_send' => function (\Sentry\Event $event, ?\Sentry\EventHint $hint): ?\Sentry\Event {
if ($hint?->exception instanceof \Illuminate\Validation\ValidationException) {
return null;
}
return $event;
},
Queue workers and Artisan. Long-lived processes (Horizon, Octane) keep the hub alive across jobs. Call Sentry\SentrySdk::getCurrentHub()->reset() or \Sentry\flush() between jobs, otherwise breadcrumbs and user context bleed from one job into the next.
sentry/sentry-laravel is the supported path; sentry/sdk gives you no integration, no service provider, and no framework context.sentry/sentry ships frequently and is pulled in transitively. Check composer show sentry/sentry for the version that actually matters. Pin sentry/sentry explicitly if you need reproducible behavior.sentry/sentry-laravel reads SENTRY_LARAVEL_DSN; the plain SDK config key is dsn and the convention is SENTRY_DSN. Mixing these in one codebase is a classic "why is nothing showing up" bug.SENTRY_DSN=null is the correct local dev setting, not an empty string. The SDK disables itself cleanly when the DSN is null; an empty string produces startup warnings.error_reporting vs error_types. The SDK respects PHP's error_reporting() at init time, then applies error_types. If deprecation notices vanish in Sentry but appear locally, that's the interaction — check both.setUser with an email address, request bodies in setExtra, or full SQL in span descriptions will land in a third party. Use send_default_pii => false (the default) and be deliberate.traces_sample_rate to 0.0 you lose the performance view but keep exception capture — a safe starting point for high-traffic apps.\Sentry\flush() (or rely on the shutdown handler) at the end of long CLI jobs; otherwise buffered events are lost when the worker is killed mid-shutdown.before_send, before_send_transaction, before_breadcrumb, custom TransportFactoryInterface, and IntegrationInterface for anything reusable across projects.max_breadcrumbs (default 100).'debug' => true on init() — the SDK logs the transport handshake, HTTP status, and rate-limit headers to stderr.How can I help you explore Laravel packages today?