feat(monitor): add cron monitor check-ins (monitor run / list)#1069
Conversation
Implement the cron monitor command group for full parity with the legacy Rust sentry-cli `monitors` commands. `sentry monitor run <slug> -- <command>`: - Wraps a command with cron check-in envelopes (in_progress on start, ok/error with duration on completion) - DSN-based auth with auto-detect fallback - Forwards stdio, SIGINT/SIGTERM; preserves child exit code - Sets SENTRY_MONITOR_SLUG in child environment - Non-fatal check-in delivery (logged, never aborts wrapped command) - --schedule (crontab) upserts monitor config on the open check-in - Dependent flags: --check-in-margin, --max-runtime, --timezone, --failure-issue-threshold, --recovery-threshold (all require --schedule) - -e/--environment (default production) - -- separator is optional (enabled allowArgumentEscapeSequence globally) `sentry monitor list [<org>/]`: - Org-scoped via buildOrgListCommand using retrieveMonitorsForAnOrganization - Columns: ID, SLUG, NAME, STATUS, SCHEDULE - Cursor pagination, --json/--fields, auto-detect/project-search modes - `sentry monitors` plural alias Supporting changes: - SentryMonitor + MonitorConfig Zod schemas in src/types/sentry.ts - src/lib/envelope/checkin-builder.ts (buildMonitorConfig, buildCheckIn) - src/lib/api/monitors.ts (listMonitors, listMonitorsPaginated) - requireDsn() now accepts an optional usageHint parameter - allowArgumentEscapeSequence enabled in Stricli scanner config - monitor added to ORG_ONLY_COMMANDS in complete.ts Tests: - Property tests for buildMonitorConfig + buildCheckIn invariants - Unit tests for monitor run (exit code, env var, check-in lifecycle, non-fatal send failures, schedule upsert, spawn error handling) - Schema validation tests for SentryMonitorSchema Closes #1052
|
Codecov Results 📊❌ Patch coverage is 70.80%. Project has 4594 uncovered lines. Files with missing lines (4)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
- Coverage 82.09% 82.01% -0.08%
==========================================
Files 362 367 +5
Lines 25398 25537 +139
Branches 16685 16750 +65
==========================================
+ Hits 20849 20943 +94
- Misses 4549 4594 +45
- Partials 1751 1759 +8Generated by Codecov Action |
There was a problem hiding this comment.
No fetch timeout in sendEnvelopeRequest blocks child process startup indefinitely (src/types/index.ts:127)
The fetch call in sendEnvelopeRequest has no timeout; since sendCheckInSafely for the in-progress check-in is awaited before spawn (run.ts:282), a hung TCP connection to the ingest endpoint will prevent the wrapped command from ever starting — contradicting the PR's 'non-fatal check-in delivery' goal.
Evidence
sendEnvelopeRequestat transport.ts:127 callsfetch(new Request(url, {...}))with nosignal/ AbortController timeout.sendCheckInSafelycallssendEnvelopeRequestand does not race it against a timeout itself.- In run.ts,
await sendCheckInSafely(..., 'in-progress')at line 266–276 is fully awaited beforespawn(cmd, cmdArgs, ...)at line 282. - If the Sentry ingest endpoint accepts the TCP connection but never responds,
fetchnever settles,spawnis never reached, and the wrapped cron job never runs. - The closing check-in (run.ts:327) has the same defect: a hung response prevents the parent process from exiting after the command finishes.
Identified by Warden find-bugs
There was a problem hiding this comment.
process.once signal handlers allow a second Ctrl+C to kill the parent before the closing check-in is sent
In src/commands/monitor/run.ts, process.once('SIGINT', onSigint) removes itself after the first SIGINT. If the child is slow to exit and the user presses Ctrl+C a second time, Node.js falls back to its default SIGINT handler (process.exit(130)), which terminates the parent synchronously—skipping both the finally cleanup and the subsequent sendCheckInSafely call, leaving the monitor permanently stuck in in_progress.
Fix: replace process.once with process.on and keep the removeListener calls in the finally block as they already are, so all signals during the child's lifetime are forwarded.
Evidence
process.once('SIGINT', onSigint)at run.ts:293 fires once and auto-removes the handler.- The
await new Promise<number>at run.ts:299 is still pending when the second SIGINT arrives. - With no listener registered, Node.js invokes its default SIGINT behaviour:
process.exit(130). process.exit()is synchronous and does not await pending async continuations, so thefinallyblock at run.ts:318 and thesendCheckInSafelycall at run.ts:327 are never reached.- The same pattern applies to
process.once('SIGTERM', onSigterm)at run.ts:294.
Identified by Warden find-bugs
- Wrap makeDsn() in try/catch to surface ValidationError on malformed DSNs (matching event/send.ts and transport.ts patterns) - Map signal-killed child to 128+N exit code (Unix convention) instead of always exiting 1 when the child is terminated by SIGINT/SIGTERM - Use autoPaginate + API_MAX_PER_PAGE in listMonitors() to fetch all monitors for large orgs (matching listProjects pattern)
- Use os.constants.signals for complete signal→number mapping (covers SIGKILL=137, SIGPIPE=141, etc. — not just the 4 hardcoded signals) - Add 30s timeout to sendCheckInSafely so a slow/unreachable ingest endpoint cannot stall the child process spawn or exit
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 146b8d5. Configure here.
The setTimeout used for the check-in send timeout was never cleared, keeping the Node event loop alive for up to 30s after the wrapped command exits. Clear it in a finally block so the process exits promptly.
- Prevent unhandled rejection from orphaned fetch when timeout wins Promise.race (add no-op .catch() handler) - Use process.on instead of process.once for signal forwarding so repeated SIGINT/SIGTERM still reach the child (prevents parent from exiting before the closing check-in is sent) - Downgrade log.error to log.warn and log.info to log.debug in sendCheckInSafely (non-fatal path should not alarm users) - Improve interval schedule display: 'every 1 hour' instead of '1 hour' - Remove empty hideRoute from monitor route map

Summary
Implements the
sentry monitorcommand group for full parity with the legacy Rustsentry-cli monitorscommands. Closes #1052.sentry monitor run <slug> -- <command>Wraps an arbitrary command with cron monitor check-in envelopes:
in_progresscheck-in on start, thenok/error(with duration) on completion based on exit code--dsn/SENTRY_DSN/ auto-detect fallback)SENTRY_MONITOR_SLUGin child environment--schedule(crontab format) upserts the monitor config on the open check-in--check-in-margin,--max-runtime,--timezone,--failure-issue-threshold,--recovery-threshold(all require--schedule)-e/--environment(defaultproduction)--separator is optional — enabledallowArgumentEscapeSequenceglobally so flags after--are passed to the wrapped commandsentry monitor list [<org>/]Org-scoped list using the stable
retrieveMonitorsForAnOrganizationSDK endpoint:-c next/prev),--json/--fields, auto-detect/project-search modessentry monitorsplural aliasSupporting changes
SentryMonitor+MonitorConfigZod schemas insrc/types/sentry.tssrc/lib/envelope/checkin-builder.ts—buildMonitorConfig(),buildCheckIn()src/lib/api/monitors.ts—listMonitors(),listMonitorsPaginated()requireDsn()now accepts an optionalusageHintparameterallowArgumentEscapeSequenceenabled in Stricli scanner configmonitor listadded toORG_ONLY_COMMANDSincomplete.tsTests
buildMonitorConfig+buildCheckIninvariants (dependent-flag validation, schedule round-tripping, open/close field presence)monitor run(exit-code propagation, env var, check-in lifecycle, non-fatal send failures, schedule upsert, spawn error handling)SentryMonitorSchema(crontab + interval responses, passthrough, rejection of missing core fields)completions.property.test.tsfor drift detectionE2E verified
Tested end-to-end against a mock ingest server — confirmed two correct
check_inenvelopes with sharedcheck_in_id,monitor_configon open only,durationon close, correct environment, schedule upsert, and exit-code preservation.