Skip to content

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

bash
composer require sentry/sentry
bash
composer require sentry/sentry-laravel
php
\Sentry\init([
    'dsn' => 'https://[email protected]/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:

php
// 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 eventCheck-in sent
beforein_progress, with monitor_config
onSuccessok
onFailureerror

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

php
->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.

php
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:

  • $checkInId goes back in. It is what makes the second call update the first run rather than open a new one.
  • duration is in seconds, as a float. Parsemend stores it as milliseconds and the check-in history shows it that way.
  • captureException is 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:

php
new MonitorConfig(
    MonitorSchedule $schedule,
    ?int $checkinMargin = null,
    ?int $maxRuntime = null,
    ?string $timezone = null,
    ?int $failureIssueThreshold = null,
    ?int $recoveryThreshold = null,
);

Schedules:

php
MonitorSchedule::crontab('0 3 * * 1');                        // Mondays at 03:00
MonitorSchedule::interval(30, MonitorScheduleUnit::minute()); // every 30 minutes

MonitorScheduleUnit 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:

php
\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:

bash
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.

Parsemend, by MAVA Design