Skip to content

Node and JavaScript

Verified against @sentry/node 10.65 and 10.70. The check-in API lives in @sentry/core, so the same calls are available from the framework packages that re-export it (@sentry/nextjs, @sentry/bun, @sentry/aws-serverless and friends).

Install and point at Parsemend

bash
npm install @sentry/node
js
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: 'https://[email protected]/42',
  environment: 'production',
  release: '1.4.2',
});

withMonitor, the short version

For most jobs this is all of it:

js
await Sentry.withMonitor(
  'nightly-reports',
  async () => {
    await buildReports();
  },
  {
    schedule: { type: 'crontab', value: '0 3 * * *' },
    checkinMargin: 10,
    maxRuntime: 60,
    timezone: 'Europe/Sofia',
  },
);

withMonitor sends in_progress before the callback, then ok or error after, with the measured duration attached. It handles sync and async callbacks alike, returns whatever the callback returns, and rethrows on failure, so wrapping an existing function does not change its behaviour.

That rethrow matters: withMonitor records that the run failed, it does not swallow the error. Whatever error handling you already have still runs, and the exception still reaches Parsemend as an ordinary issue if you have the SDK's error capture on.

Manual check-ins

When you need the two calls apart, for instance because the work spans a process boundary:

js
const checkInId = Sentry.captureCheckIn(
  { monitorSlug: 'inventory-sync', status: 'in_progress' },
  {
    schedule: { type: 'interval', value: 15, unit: 'minute' },
    checkinMargin: 3,
    maxRuntime: 10,
  },
);

const started = Date.now();

try {
  await syncInventory();

  Sentry.captureCheckIn({
    monitorSlug: 'inventory-sync',
    status: 'ok',
    checkInId,
    duration: (Date.now() - started) / 1000,
  });
} catch (error) {
  Sentry.captureCheckIn({
    monitorSlug: 'inventory-sync',
    status: 'error',
    checkInId,
    duration: (Date.now() - started) / 1000,
  });

  throw error;
}

captureCheckIn returns the check-in id as a string. Passing it back on the terminal call is what ties both to one run.

duration is seconds, not milliseconds, which is easy to get wrong when Date.now() is right there. Parsemend stores it as milliseconds and the check-in history displays it that way, so a run showing 900000 ms for a 15-minute job means the conversion is missing.

The three check-in shapes

TypeScript enforces these; JavaScript will let you send nonsense.

ts
// Heartbeat: one call, no id, no duration.
{ monitorSlug: string, status: 'ok' | 'error' }

// Start of a run. Returns the id you need for the next one.
{ monitorSlug: string, status: 'in_progress' }

// End of a run.
{ monitorSlug: string, status: 'ok' | 'error', checkInId: string, duration?: number }

A heartbeat check-in gives you missed detection only. No start means no timeout detection and no duration history.

Monitor config

ts
{
  schedule: { type: 'crontab', value: '0 * * * *' }
             | { type: 'interval', value: number, unit: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' },
  checkinMargin?: number,   // minutes, defaults to 5 in Parsemend
  maxRuntime?: number,      // minutes, defaults to 30 in Parsemend
  timezone?: string,        // tz database name, defaults to UTC
  isolateTrace?: boolean,   // client-side only, starts a new trace per run
}

Two caveats specific to Parsemend:

  • unit: 'year' typechecks and sends, and Parsemend cannot compute a next expected time from it, which leaves the monitor undetectable. Use { type: 'crontab', value: '0 3 1 1 *' } instead.
  • failureIssueThreshold and recoveryThreshold are accepted on the wire and ignored. An issue opens on the first failure and closes on the first success.

isolateTrace is handled entirely inside the SDK and never reaches Parsemend.

Short-lived processes

A script that exits immediately after its work can exit before the check-in has left the process. Flush first:

js
await Sentry.withMonitor('nightly-reports', async () => {
  await buildReports();
}, config);

await Sentry.flush(2000);

flush resolves once queued events have been sent, or after the timeout, whichever comes first. Long-lived servers do not need it.

This bites hardest in serverless. On Lambda, Cloud Run jobs and similar, the runtime can freeze the process the moment your handler resolves, and a check-in still sitting in the queue is simply lost. Always flush before returning.

Which job to monitor

Monitor the thing that is on a clock. A worker consuming a queue is not on a clock, so a monitor around it will fire whenever the queue is quiet, which is not a fault. Put the monitor on the scheduled producer instead, and let ordinary error reporting cover the consumer.

Verifying

Run the job once, then open Monitoring → Monitors. A row with a populated Next expected means detection is live. Empty means the config never arrived, usually because the process exited before the flush. See troubleshooting.

Parsemend, by MAVA Design