SDK context
Okapi shows the context that your Sentry SDK records with an error. It cannot recover data that the SDK did not send.
| Event data | Where Okapi shows it |
|---|---|
| Exception type, message, and stack frames | The title, What happened, Crashed in, and the full stack trace |
| Request URL, method, headers, cookies, and body | The Request summary and the Request details, including a safe curl command |
| Job and command signals | The origin, What happened, and the Job / command details |
| Breadcrumbs | Leading up to it and the full breadcrumb timeline |
| Custom contexts, extra fields, and tags | Custom data and the Tags / contexts details |
| User, runtime, framework, and release | The User and App / runtime details and the Properties rail |
| Trace and span IDs | In this trace, with stored spans, other errors, and logs from the same organization |
A trace ID links records that Okapi already stores. It does not create spans or log lines. Those records must also reach Okapi.
With default project settings, Okapi scrubs new events at ingest. Raw JSON includes Sentry _meta remarks for removed values.
Filtered text becomes [Filtered], and filtered IP addresses become [ip] when IP scrubbing is enabled.
Filtered numbers and booleans become JSON null. A URL password becomes %5BFiltered%5D, which keeps the URL parseable.
SDK-side filtering still helps: values it removes never leave your application. Okapi cannot reveal values removed by the SDK or by its own scrubber.
Existing events do not change. The issue page still hides values under sensitive keys, such as password, api.key, and session.id. Some retained values have an explicit Show action. Raw JSON can still contain sensitive values in old events.
The examples follow SDK source reviewed on November 1, 2026:
Job details
Laravel records a queue.job breadcrumb by default. That breadcrumb contains the job class, queue, connection, and attempt count. It can disappear when breadcrumbs.queue_info is off or later breadcrumbs replace it. A job context keeps these facts separate from the breadcrumb limit.
Laravel
Register this listener in AppServiceProvider::boot(), after the Sentry provider:
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\Facades\Queue;
use Sentry\State\Scope;
Queue::before(function (JobProcessing $event): void {
\Sentry\configureScope(function (Scope $scope) use ($event): void {
$scope->setContext('job', [
'name' => $event->job->resolveName(),
'id' => $event->job->uuid(),
'queue' => $event->job->getQueue(),
'connection' => $event->connectionName,
'attempts' => $event->job->attempts(),
'max_tries' => $event->job->maxTries(),
]);
});
});
The Laravel SDK creates a separate scope for each job. Listener order depends on your application providers. Check that a captured test job contains contexts.job. The origin uses name, id, queue, connection, and attempts. Custom data retains max_tries.
Plain PHP
Wrap each job in its own scope. Capture the error before that scope closes:
\Sentry\withScope(function (\Sentry\State\Scope $scope) use ($job, $jobId): void {
$scope->setContext('job', ['name' => $job::class, 'id' => $jobId]);
try {
$job->handle(); // Your existing processor.
} catch (\Throwable $error) {
\Sentry\captureException($error);
throw $error;
}
});
Use one exception capture path for this processor.
BullMQ and Node.js
Node does not record job details by default. Wrap the processor in a scope and capture the error before that scope closes:
import * as Sentry from "@sentry/node";
import { Worker } from "bullmq";
const worker = new Worker("contacts", job => Sentry.withScope(async scope => {
scope.setContext("job", {
name: job.name,
id: job.id,
queue: job.queueName,
attempts: job.attemptsMade + 1,
max_tries: job.opts.attempts ?? 1,
});
try {
return await syncContact(job.data); // Your existing processor.
} catch (error) {
Sentry.captureException(error);
throw error;
}
}));
Use one exception capture path for this processor. An additional failed-job listener or telemetry integration can otherwise report the same failure again.
Celery
Celery already reports the task name and ID. Queue and retry details are less consistent without tracing. Connect this receiver in a module that your worker imports:
from celery.signals import task_prerun
import sentry_sdk
@task_prerun.connect
def add_job_context(task_id, task, **kwargs):
sentry_sdk.set_context("job", {
"name": task.name,
"id": task_id,
"queue": (task.request.delivery_info or {}).get("routing_key"),
"attempts": task.request.retries + 1,
"max_tries": None if task.max_retries is None else task.max_retries + 1,
})
The receiver runs inside the SDK scope for that task. max_retries counts retries after the first attempt.
Command input
Laravel records command input in an artisan.command breadcrumb. An early bootstrap error can happen before that breadcrumb exists. A listener can preserve input after Laravel starts the command. It cannot recover input from an earlier bootstrap failure.
Register this listener in AppServiceProvider::boot():
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Console\Events\CommandFinished;
use Illuminate\Support\Facades\Event;
use Sentry\State\Scope;
Event::listen(CommandStarting::class, function (CommandStarting $event): void {
\Sentry\configureScope(function (Scope $scope) use ($event): void {
$scope->setContext('command', [
'name' => $event->command,
'input' => (string) $event->input,
'argv' => $_SERVER['argv'] ?? [],
]);
});
});
Event::listen(CommandFinished::class, function (CommandFinished $event): void {
if ($event->exitCode === 0) {
\Sentry\configureScope(fn (Scope $scope) => $scope->removeContext('command'));
}
});
Failed commands keep their context until the SDK reports the exception.
For plain PHP, add this after SDK initialization and before the command runs:
\Sentry\configureScope(function (\Sentry\State\Scope $scope): void {
$scope->setContext('command', ['argv' => $_SERVER['argv'] ?? []]);
});
For Node.js, add this after SDK initialization:
Sentry.setContext("command", { argv: process.argv });
Python 2.73.0 includes extra["sys.argv"] through its default argv integration. Okapi uses that array as command input. Command arguments can contain credentials. The default scrubbers remove values after sensitive option names, such as --password, before storage. The issue page also hides these values in old events.
Session data
These SDKs do not send framework session contents by default. An SDK session for release health is a different record. The issue page reads framework data from contexts.session.
Select only the session fields that help diagnose your errors. The examples use cart_id and currency as an allowlist.
Laravel middleware
Register this middleware after Illuminate\Session\Middleware\StartSession in the web middleware group:
public function handle($request, \Closure $next)
{
\Sentry\configureScope(function (\Sentry\State\Scope $scope) use ($request): void {
$scope->setContext('session', \Illuminate\Support\Arr::only(
$request->session()->all(), ['cart_id', 'currency']
));
});
return $next($request);
}
Express with express-session
Register this middleware after your existing express-session middleware, before your routes:
app.use((req, _res, next) => {
const session = Object.fromEntries(
["cart_id", "currency"].filter(key => req.session?.[key] !== undefined)
.map(key => [key, req.session[key]])
);
if (Object.keys(session).length) Sentry.setContext("session", session);
next();
});
The Node SDK keeps this context in the request scope. These examples capture the session state before the route runs.
Django middleware
Register this middleware after django.contrib.sessions.middleware.SessionMiddleware in MIDDLEWARE:
import sentry_sdk
def sentry_session(get_response):
def middleware(request):
sentry_sdk.set_context("session", {
key: request.session[key]
for key in ("cart_id", "currency") if key in request.session
})
return get_response(request)
return middleware
Use set_context("session", ...). The Python scrubber can replace an extra field named session with [Filtered].
Custom data
A named context groups related facts. Extra fields hold individual values. Tags support short labels for filters.
Laravel and PHP
\Sentry\configureScope(function (\Sentry\State\Scope $scope): void {
$scope->setContext('billing', ['invoice_id' => 'inv-demo-42', 'currency' => 'EUR']);
$scope->setExtra('retry_count', 2);
$scope->setTag('checkout_flow', 'express');
});
Node.js
Sentry.setContext("billing", { invoice_id: "inv-demo-42", currency: "EUR" });
Sentry.setExtra("retry_count", 2);
Sentry.setTag("checkout_flow", "express");
Python
sentry_sdk.set_context("billing", {"invoice_id": "inv-demo-42", "currency": "EUR"})
sentry_sdk.set_extra("retry_count", 2)
sentry_sdk.set_tag("checkout_flow", "express")
Add these values before the error capture. A named context must be an object. Okapi separates standard SDK contexts from custom contexts and shows each fact once.
User data
Okapi shows the user object that the SDK sends. Set only the application identity that your error reports need.
IP scrubbing is off by default. Names, email addresses, and other personal data can remain. SDK-side filtering and limited collection still help protect this data.
Laravel and PHP
Laravel can attach the authenticated user with this legacy configuration:
// config/sentry.php
'send_default_pii' => true,
In PHP 4.35.0, an explicit data_collection configuration overrides this switch. To attach only an application user ID, set it in the active scope:
\Sentry\configureScope(fn (\Sentry\State\Scope $scope) => $scope->setUser(['id' => 'user-demo-42']));
If the scope outlives the login, clear the user after logout:
\Sentry\configureScope(fn ($scope) => $scope->removeUser());
Node.js
Node 11.10.0 uses dataCollection and no longer supports sendDefaultPii. Node 10 used sendDefaultPii, so its defaults differ.
Sentry.setUser({ id: "user-demo-42" });
// After logout:
Sentry.setUser(null);
Python
Django can attach the authenticated user with this legacy initialization:
sentry_sdk.init(dsn=dsn, send_default_pii=True)
In Python 2.73.0, an explicit data_collection configuration overrides this switch. To attach only an application user ID:
sentry_sdk.set_user({"id": "user-demo-42"})
# After logout:
sentry_sdk.set_user(None)
An SDK filter can still remove fields before transmission.
Next: Issues & logs.