PHP and Laravel
Verified against sentry/sentry 4.29 and sentry/sentry-laravel 4.27. Newer releases have not changed the check-in API.
Install and point at Parsemend
composer require sentry/sentrycomposer require sentry/sentry-laravel\Sentry\init([
'dsn' => 'https://a1b2c3d4e5f60718293a4b5c6d7e8f90@ingest.parsemend.com/42',
'environment' => 'production',
'release' => '1.4.2',
]);On Laravel, set SENTRY_LARAVEL_DSN in .env and let the package's own config handle the rest. Do not commit a vendor-identical config/sentry.php; the env vars are enough.
Laravel: the scheduler macro
If the job is a scheduled task, this is the whole integration:
// routes/console.php
Schedule::command('reports:nightly')
->dailyAt('03:00')
->timezone('Europe/Sofia')
->sentryMonitor('nightly-reports');sentryMonitor() hooks the task's own lifecycle and does the rest:
| Task event | Check-in sent |
|---|---|
before | in_progress, with monitor_config |
onSuccess | ok |
onFailure | error |
The monitor_config it sends is derived from the scheduled task itself: the schedule is crontab built from the task's own cron expression, and the timezone is the task's ->timezone(). That is the strongest reason to use the macro. The schedule Parsemend expects and the schedule Laravel actually runs cannot disagree, because they are the same value.
Arguments
->sentryMonitor(
monitorSlug: 'nightly-reports',
checkInMargin: 10,
maxRuntime: 60,
updateMonitorConfig: true,
schedule: null, // override the derived cron expression
)failureIssueThreshold and recoveryThreshold are also accepted, and Parsemend ignores both. See what Parsemend ignores.
Always pass an explicit slug
The slug argument is optional. Left out, the macro derives one from the command string, producing something like scheduled_artisan-reports-nightly.
Pass one anyway. The derived slug is a function of the command string, so renaming reports:nightly to reports:daily silently creates a second monitor and abandons the first, which then sits in the list going missed forever with nobody able to say what it was for. An explicit slug makes the monitor's identity survive refactoring.
Background tasks
->runInBackground() runs the task in a separate process, so the check-in id has to survive the process boundary. The package stores it in your cache store (or, on Laravel 12.40.2 and newer, in hidden context). Backgrounded tasks therefore need a cache store shared across processes. On array, the terminal check-in is dropped and every run looks like a hang.
Plain PHP: manual check-ins
For jobs that are not Laravel scheduled tasks, call the two check-ins yourself.
use Sentry\CheckInStatus;
use Sentry\MonitorConfig;
use Sentry\MonitorSchedule;
$config = new MonitorConfig(
MonitorSchedule::crontab('*/15 * * * *'),
checkinMargin: 3,
maxRuntime: 10,
timezone: 'UTC',
);
$checkInId = \Sentry\captureCheckIn(
slug: 'inventory-sync',
status: CheckInStatus::inProgress(),
monitorConfig: $config,
);
$started = microtime(true);
try {
sync_inventory();
$status = CheckInStatus::ok();
} catch (\Throwable $e) {
$status = CheckInStatus::error();
\Sentry\captureException($e); // the crash itself, as an ordinary issue
throw $e;
} finally {
\Sentry\captureCheckIn(
slug: 'inventory-sync',
status: $status,
duration: microtime(true) - $started,
checkInId: $checkInId,
);
\Sentry\flush();
}Four things in there are load-bearing:
$checkInIdgoes back in. It is what makes the second call update the first run rather than open a new one.durationis in seconds, as a float. Parsemend stores it as milliseconds and the check-in history shows it that way.captureExceptionis separate. The check-in records that the run failed; the exception records why, with a stack trace the agent can work from. Send both.\Sentry\flush()before the process exits. The SDK sends in the background, and a short CLI script can exit before the request goes out. In a long-lived process this is unnecessary; in a cron script it is the difference between a monitor that works and one that never receives anything.
The MonitorConfig constructor
Positional, in this order:
new MonitorConfig(
MonitorSchedule $schedule,
?int $checkinMargin = null,
?int $maxRuntime = null,
?string $timezone = null,
?int $failureIssueThreshold = null,
?int $recoveryThreshold = null,
);Schedules:
MonitorSchedule::crontab('0 3 * * 1'); // Mondays at 03:00
MonitorSchedule::interval(30, MonitorScheduleUnit::minute()); // every 30 minutesMonitorScheduleUnit offers minute(), hour(), day(), week(), month() and year(). Parsemend does not support year(); use a crontab expression for annual jobs.
Heartbeat only
A daemon that has no meaningful start and end can send a single check-in per tick:
\Sentry\captureCheckIn(
slug: 'queue-worker-heartbeat',
status: CheckInStatus::ok(),
monitorConfig: new MonitorConfig(
MonitorSchedule::interval(5, MonitorScheduleUnit::minute()),
checkinMargin: 2,
),
);This gives you missed detection and nothing else. There is no in_progress, so there is no timeout detection, and no duration to trend. For a heartbeat that is the right trade.
Queue jobs
A queued job is not a scheduled job, and wrapping every dispatch in a check-in produces a monitor whose schedule is meaningless. Monitor the dispatcher instead: the scheduled command that enqueues the work is on a clock, so put the monitor there and let ordinary exception reporting cover the workers.
Verifying
Run the job once by hand:
php artisan schedule:test --name="reports:nightly"Then check Monitoring → Monitors for a row with a populated Next expected. If it is empty, the config did not arrive. See troubleshooting.
