Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ cli/
│ │ ├── issue/ # archive, events, explain, list, merge, plan, resolve, unresolve, view
│ │ ├── local/ # run, serve
│ │ ├── log/ # list, view
│ │ ├── monitor/ # list, run
│ │ ├── org/ # list, view
│ │ ├── proguard/ # uuid
│ │ ├── project/ # create, delete, list, view
Expand Down
33 changes: 33 additions & 0 deletions docs/src/fragments/commands/monitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@


## Examples

```bash
# Wrap a command with cron monitor check-ins (DSN-based)
SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 \
sentry monitor run nightly-job -- python manage.py cron

# The -- separator is optional when the command has no flags
sentry monitor run nightly-job npm run task

# Create/update the monitor on the first check-in via --schedule (crontab)
sentry monitor run nightly-job -s "0 0 * * *" --max-runtime 30 --timezone UTC -- ./backup.sh

# List cron monitors in an org
sentry monitor list my-org/

# Paginate through monitors
sentry monitor list my-org/ -c next

# Output as JSON
sentry monitor list --json
```

## Check-in lifecycle

`monitor run` sends an `in_progress` check-in when the wrapped command starts,
then an `ok` or `error` check-in (with duration) when it finishes, based on the
exit code. The wrapped command inherits stdio, has `SIGINT`/`SIGTERM`
forwarded, receives the `SENTRY_MONITOR_SLUG` environment variable, and its
exit code is preserved. Check-in delivery failures are non-fatal — the wrapped
command still runs and exits with its own code.
9 changes: 9 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,15 @@ View Sentry logs

→ Full flags and examples: `references/log.md`

### Monitor

Work with Sentry cron monitors

- `sentry monitor run <monitor-slug command...>` — Wrap a command with cron monitor check-ins
- `sentry monitor list <org/project>` — List cron monitors

→ Full flags and examples: `references/monitor.md`

### Sourcemap

Manage sourcemaps
Expand Down
73 changes: 73 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/monitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
name: sentry-cli-monitor
version: 0.36.0-dev.0
description: Work with Sentry cron monitors
requires:
bins: ["sentry"]
auth: true
---

# Monitor Commands

Work with Sentry cron monitors

### `sentry monitor run <monitor-slug command...>`

Wrap a command with cron monitor check-ins

**Flags:**
- `--dsn <value> - DSN to send check-ins to (overrides SENTRY_DSN env var)`
- `-e, --environment <value> - Environment of the monitor - (default: "production")`
- `-s, --schedule <value> - Upsert the monitor with this crontab schedule (e.g. '0 * * * *')`
- `--check-in-margin <value> - Minutes after the expected check-in before it is missed (requires --schedule)`
- `--max-runtime <value> - Minutes a check-in may run before timing out (requires --schedule)`
- `--timezone <value> - Timezone of the schedule, tz database string (requires --schedule)`
- `--failure-issue-threshold <value> - Consecutive failures before an issue is created (requires --schedule)`
- `--recovery-threshold <value> - Consecutive successes before an issue is resolved (requires --schedule)`

### `sentry monitor list <org/project>`

List cron monitors

**Flags:**
- `-n, --limit <value> - Maximum number of monitors to list - (default: "25")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Monitor ID |
| `slug` | string | Monitor slug |
| `name` | string | Monitor name |
| `status` | string | Monitor status (e.g. active, disabled) |
| `isMuted` | boolean | Whether the monitor is muted |
| `config` | object | Schedule configuration |
| `dateCreated` | string | Creation date (ISO 8601) |
| `project` | object | Owning project |

**Examples:**

```bash
# Wrap a command with cron monitor check-ins (DSN-based)
SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 \
sentry monitor run nightly-job -- python manage.py cron

# The -- separator is optional when the command has no flags
sentry monitor run nightly-job npm run task

# Create/update the monitor on the first check-in via --schedule (crontab)
sentry monitor run nightly-job -s "0 0 * * *" --max-runtime 30 --timezone UTC -- ./backup.sh

# List cron monitors in an org
sentry monitor list my-org/

# Paginate through monitors
sentry monitor list my-org/ -c next

# Output as JSON
sentry monitor list --json
```

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
10 changes: 10 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { listCommand as issueListCommand } from "./commands/issue/list.js";
import { localRoute } from "./commands/local/index.js";
import { logRoute } from "./commands/log/index.js";
import { listCommand as logListCommand } from "./commands/log/list.js";
import { monitorRoute } from "./commands/monitor/index.js";
import { listCommand as monitorListCommand } from "./commands/monitor/list.js";
import { orgRoute } from "./commands/org/index.js";
import { listCommand as orgListCommand } from "./commands/org/list.js";
import { proguardRoute } from "./commands/proguard/index.js";
Expand Down Expand Up @@ -77,6 +79,7 @@ const PLURAL_TO_SINGULAR: Record<string, string> = {
repos: "repo",
teams: "team",
logs: "log",
monitors: "monitor",
replays: "replay",

spans: "span",
Expand Down Expand Up @@ -104,6 +107,7 @@ export const routes = buildRouteMap({
events: eventListCommand,
explore: exploreCommand,
log: logRoute,
monitor: monitorRoute,
sourcemap: sourcemapRoute,
sourcemaps: sourcemapRoute,
span: spanRoute,
Expand All @@ -125,6 +129,7 @@ export const routes = buildRouteMap({
repos: repoListCommand,
teams: teamListCommand,
logs: logListCommand,
monitors: monitorListCommand,
spans: spanListCommand,
traces: traceListCommand,
trials: trialListCommand,
Expand All @@ -147,6 +152,7 @@ export const routes = buildRouteMap({
repos: true,
teams: true,
logs: true,
monitors: true,
spans: true,
traces: true,
trials: true,
Expand Down Expand Up @@ -370,6 +376,10 @@ export const app = buildApplication(routes, {
},
scanner: {
caseStyle: "allow-kebab-for-camel",
// Allow `--` to stop flag parsing so wrapper commands (e.g.
// `sentry monitor run <slug> -- <command>`) can pass through flags
// like `-e` or `--verbose` to the wrapped command unambiguously.
allowArgumentEscapeSequence: true,
},
determineExitCode: getExitCode,
localization: {
Expand Down
19 changes: 19 additions & 0 deletions src/commands/monitor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { buildRouteMap } from "../../lib/route-map.js";
import { listCommand } from "./list.js";
import { runCommand } from "./run.js";

export const monitorRoute = buildRouteMap({
routes: {

Check warning on line 6 in src/commands/monitor/index.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

SIGINT/SIGTERM forwarded to child that already received the signal from the terminal

Because the child is spawned with `stdio: 'inherit'` and no `detached: true`, it shares the parent's process group; pressing Ctrl+C causes the terminal to deliver SIGINT to **both** processes simultaneously, and then `onSigint` in `run.ts` (line 321) sends a second SIGINT to the child — causing double delivery. Programs that use "second SIGINT = force exit" (e.g. Python, many interactive CLIs) will be unconditionally force-killed on a single Ctrl+C press.
Comment thread
BYK marked this conversation as resolved.
run: runCommand,
list: listCommand,
},
defaultCommand: "list",
docs: {
brief: "Work with Sentry cron monitors",
fullDescription:
"Run commands with cron monitor check-ins and list configured monitors.\n\n" +
" sentry monitor run <slug> -- <command> # wrap a command with check-ins\n" +
" sentry monitor list # list configured monitors\n\n" +
"Alias: `sentry monitors` → `sentry monitor list`",
},
});
96 changes: 96 additions & 0 deletions src/commands/monitor/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* sentry monitor list
*
* List cron monitors in an organization, with flexible targeting and cursor
* pagination.
*
* Supports:
* - Auto-detection from DSN/config
* - Org-scoped listing with cursor pagination (e.g., sentry/)
* - Project-scoped targeting (e.g., sentry/cli) — monitors are org-scoped, so
* this lists the org's monitors
* - Cross-org project search (e.g., sentry)
*/

import { listMonitors, listMonitorsPaginated } from "../../lib/api-client.js";
import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
import { type Column, formatTable } from "../../lib/formatters/table.js";
import {
buildOrgListCommand,
type OrgListCommandDocs,
} from "../../lib/list-command.js";
import type { OrgListConfig } from "../../lib/org-list.js";
import { type SentryMonitor, SentryMonitorSchema } from "../../types/index.js";

/** Command key for pagination cursor storage */
export const PAGINATION_KEY = "monitor-list";

/** Monitor with its organization context for display */
type MonitorWithOrg = SentryMonitor & { orgSlug?: string };

/**
* Render a monitor's schedule for display.
*
* Crontab schedules show the raw expression (e.g. `"0 * * * *"`).
* Interval schedules show `"every <value> <unit>"` (e.g. `"every 1 hour"`).
* Returns an empty string when no schedule is configured.
*/
function formatSchedule(monitor: MonitorWithOrg): string {
const config = monitor.config;
if (!config?.schedule) {
return "";
}
if (Array.isArray(config.schedule)) {
return `every ${config.schedule[0]} ${config.schedule[1]}`;
}
return config.schedule;
}

/** Column definitions for the monitor table. */
const MONITOR_COLUMNS: Column<MonitorWithOrg>[] = [
{ header: "ID", value: (m) => m.id },
{ header: "SLUG", value: (m) => m.slug },
{ header: "NAME", value: (m) => escapeMarkdownCell(m.name) },
{ header: "STATUS", value: (m) => m.status },
{ header: "SCHEDULE", value: (m) => escapeMarkdownCell(formatSchedule(m)) },
];

/** Shared config that plugs into the org-list framework. */
const monitorListConfig: OrgListConfig<SentryMonitor, MonitorWithOrg> = {
paginationKey: PAGINATION_KEY,
entityName: "monitor",
entityPlural: "monitors",
commandPrefix: "sentry monitor list",
listForOrg: (org) => listMonitors(org),
listPaginated: (org, opts) => listMonitorsPaginated(org, opts),
withOrg: (monitor, orgSlug) => ({ ...monitor, orgSlug }),
displayTable: (monitors: MonitorWithOrg[]) =>
formatTable(monitors, MONITOR_COLUMNS),
schema: SentryMonitorSchema,
};

const docs: OrgListCommandDocs = {
brief: "List cron monitors",
fullDescription:
"List cron monitors in an organization.\n\n" +
"Target specification:\n" +
" sentry monitor list # auto-detect from DSN or config\n" +
" sentry monitor list <org>/ # list all monitors in org (paginated)\n" +
" sentry monitor list <org>/<proj> # list monitors in org (project context)\n" +
" sentry monitor list <org> # list monitors in org\n\n" +
"Pagination:\n" +
" sentry monitor list <org>/ -c next # fetch next page\n" +
" sentry monitor list <org>/ -c prev # fetch previous page\n\n" +
"Examples:\n" +
" sentry monitor list # auto-detect or list all\n" +
" sentry monitor list my-org/ # list monitors in my-org (paginated)\n" +
" sentry monitor list --limit 10\n" +
" sentry monitor list --json\n\n" +
"Alias: `sentry monitors` → `sentry monitor list`",
};

export const listCommand = buildOrgListCommand(
monitorListConfig,
docs,
"monitor"
);
Loading
Loading