Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3ddb979
docs(doctor): add sentry doctor design spec
romtsn Aug 18, 2026
26e9989
docs(doctor): split remediation, fix report side effect, add PostHog …
romtsn Aug 18, 2026
51109fd
docs(doctor): liveness by default, seven flags down to three
romtsn Aug 18, 2026
514e146
docs: add sentry doctor implementation plan
romtsn Aug 18, 2026
31b25b2
fix(init): skip post-init verification under --dry-run
romtsn Aug 18, 2026
2795d94
docs(plan): narrow Task 1's dry-run guarantee to what is actually true
romtsn Aug 18, 2026
8642384
test(init): assert verifySetup gating behaviourally, not via source r…
romtsn Aug 18, 2026
0fe64c9
feat(doctor): add check types and isolated check runner
romtsn Aug 18, 2026
136bee3
feat(doctor): add capture-boundary redaction and input allowlists
romtsn Aug 18, 2026
d26db33
feat(doctor): add delimiter-table config block scanner
romtsn Aug 18, 2026
c3429e5
feat(doctor): add init/build marker tables and manifest parsing
romtsn Aug 18, 2026
afb26df
feat(doctor): add filesystem capture stage
romtsn Aug 18, 2026
7abfe68
feat(doctor): add Sentry API resolve stage
romtsn Aug 18, 2026
24ef313
feat(doctor): add tier-1 server-truth checks
romtsn Aug 18, 2026
3c3e7fe
feat(doctor): add tier-2 ecosystem checks and the check registry
romtsn Aug 18, 2026
6047803
feat(doctor): add human and JSON renderers
romtsn Aug 18, 2026
b4bc925
fix(doctor): validate evidence paths in fixBlock via safeFilePath
romtsn Aug 19, 2026
360c73b
feat(doctor): wire up the sentry doctor command
romtsn Aug 19, 2026
8efdb7c
feat(doctor): add --send-test-event round-trip check
romtsn Aug 19, 2026
7b46e55
feat(doctor): add tier-3 configuration judgement
romtsn Aug 19, 2026
514939e
feat(doctor): add consent-gated support export
romtsn Aug 19, 2026
000a75c
feat(doctor): add --fix escalation to the setup workflow
romtsn Aug 19, 2026
daa5644
test(doctor): add integration coverage against a real template
romtsn Aug 19, 2026
8f89c30
fix(doctor): pipe human output through renderMarkdown for ANSI colors
romtsn Aug 19, 2026
9b70b47
style(doctor): render title as a markdown heading
romtsn Aug 19, 2026
6c51854
fix(doctor): limit grep to 1 match per file, send report as attachment
romtsn Aug 19, 2026
11eb7e8
fix(doctor): make test event title human-friendly
romtsn Aug 19, 2026
67f22a9
fix(doctor): send test event as TestError exception
romtsn Aug 19, 2026
64afe3c
style(doctor): add stethoscope emoji to title
romtsn Aug 19, 2026
7aa32ae
docs(doctor): flesh out agent skill reference
romtsn Aug 19, 2026
3893f86
fix(doctor): explain why server checks were skipped
romtsn Aug 19, 2026
20228d8
fix(doctor): dogfood capture, report, and DSN host gaps
romtsn Aug 28, 2026
6875d3a
chore(doctor): drop superpowers spec and plan from the PR
romtsn Aug 28, 2026
189b1c4
test(init): match formatResult args when verifySetup is stubbed
romtsn Aug 28, 2026
cc56445
chore: regenerate docs
github-actions[bot] Aug 28, 2026
3a9f554
fix(doctor): Close capture and artifact gaps from review
romtsn Aug 28, 2026
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 apps/cli-docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ cli/
│ │ ├── trace/ # list, logs, view
│ │ ├── trial/ # list, start
│ │ ├── api.ts # Make an authenticated API request
│ │ ├── doctor.ts # Check whether Sentry is correctly set up and actually working
│ │ ├── explore.ts # Query aggregate event data (Explore)
│ │ ├── help.ts # Help command
│ │ ├── info.ts # Print configuration and verify authentication
Expand Down
52 changes: 52 additions & 0 deletions apps/cli-docs/src/fragments/commands/doctor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## Examples

```bash
# Read-only health check
sentry doctor

# Machine-readable report (every result, including passes)
sentry doctor --json

# Send a test event and confirm ingest (a write)
sentry doctor --send-test-event
```

`--json` is this run's results (`id`, `status`, `detail`, optional `evidence` / `remediation`) plus `capture` and `server`. It is not a catalog of what each check means.

`skip` = could not tell. `warn` never fails the run. Only `fail` exits 1.

## Checks

### Server (what Sentry knows)

| Check | Means |
|---|---|
| `dsn.present` | A DSN exists somewhere in the project |
| `dsn.placeholder` | That DSN is not the docs example |
| `dsn.conflict` | More than one distinct DSN — events may split |
| `dsn.resolves` | The DSN maps to a project you can access |
| `project.first_event` | That project has received at least one event, ever |
| `project.last_event` | Recent activity (warns if last issue is >30 days old) |
| `project.key_active` | This DSN's key still exists and is enabled |
| `project.environments` | Events are tagged with an environment |
| `release.attribution` | Events are tied to a release |
| `artifacts.uploaded` | Source maps / debug files exist on the project |

### Local (what the repo says)

| Check | Means |
|---|---|
| `init.present` | An init call (or platform auto-init) exists |
| `config.dsn_set` | That init actually sets a DSN |
| `config.environment` | `environment` is set (else local + prod mix) |
| `config.debug` | `debug: true` is not hardcoded on |
| `config.sample_rate` | Trace sample rate isn't 0 or 1.0 |
| `build.upload_configured` | A bundler / dSYM / Proguard upload plugin is present |
| `capture.complete` | Doctor finished scanning the tree |

### Opt-in / extra

| Check | Means |
|---|---|
| `live.roundtrip` | `--send-test-event`: ingest accepted it, and (if DSN resolved) search found it |
| `judge.*` | LLM pass over captured config; skip if no model |
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"check:stale-refs": "pnpm tsx script/check-stale-references.ts"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@anthropic-ai/sdk": "^0.117.1",
"@biomejs/biome": "2.3.8",
"@clack/prompts": "0.11.0",
"@hono/node-server": "^2.0.10",
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,14 @@ Work with debug information files

→ Full flags and examples: `references/debug-files.md`

### Doctor

Check whether Sentry is correctly set up and actually working

- `sentry doctor` — Check whether Sentry is correctly set up and actually working

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

### Dashboard

Manage Sentry dashboards
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
name: sentry-cli-doctor
version: 0.45.0-dev.0
description: Check whether Sentry is correctly set up and actually working
requires:
bins: ["sentry"]
auth: true
---

# Doctor Commands

Check whether Sentry is correctly set up and actually working

### `sentry doctor`

Check whether Sentry is correctly set up and actually working

**Flags:**
- `--sendTestEvent - Send a synthetic event to the configured DSN and confirm it arrives (a write)`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename this to online or something to make it not sound like a subcommand.

- `--fix - After reporting, run the setup workflow to produce a fix plan`

**Examples:**

```bash
# Read-only health check
sentry doctor

# Machine-readable report (every result, including passes)
sentry doctor --json

# Send a test event and confirm ingest (a write)
sentry doctor --send-test-event
```

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
2 changes: 2 additions & 0 deletions packages/cli/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { dartSymbolMapRoute } from "./commands/dart-symbol-map/index.js";
import { dashboardRoute } from "./commands/dashboard/index.js";
import { listCommand as dashboardListCommand } from "./commands/dashboard/list.js";
import { debugFilesRoute } from "./commands/debug-files/index.js";
import { doctorCommand } from "./commands/doctor.js";
import { docsRoute } from "./commands/docs/index.js";
import { eventRoute } from "./commands/event/index.js";
import { listCommand as eventListCommand } from "./commands/event/list.js";
Expand Down Expand Up @@ -118,6 +119,7 @@ export const routes = buildRouteMap({
"agent-conversation": conversationRoute,
"dart-symbol-map": dartSymbolMapRoute,
"debug-files": debugFilesRoute,
doctor: doctorCommand,
dashboard: dashboardRoute,
docs: docsRoute,
org: orgRoute,
Expand Down
127 changes: 127 additions & 0 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* `sentry doctor` — is Sentry actually working in this project?
*
* Four stages, only the first two do I/O. `auth: false` so an unauthenticated
* run reports "unauthorized" as a finding rather than crashing, following the
* `info.ts` pattern.
*/

import type { SentryContext } from "../context.js";
import { buildCommand } from "../lib/command.js";
import { CLI_VERSION } from "../lib/constants.js";
import { capture } from "../lib/doctor/capture.js";
import { REGISTRY } from "../lib/doctor/checks/index.js";
import {
buildReport,
type DoctorReport,
exitCodeFor,
formatDoctorReport,
} from "../lib/doctor/render.js";
import { resolveServerFacts } from "../lib/doctor/resolve.js";
import { runChecks } from "../lib/doctor/types.js";
import { CommandOutput } from "../lib/formatters/output.js";
import { withProgress } from "../lib/polling.js";

export type DoctorFlags = {
sendTestEvent: boolean;
fix: boolean;
json?: boolean;
};

/** The whole command, minus presentation — so tests never touch the CLI. */
export async function runDoctor(
ctx: SentryContext,
flags: Partial<DoctorFlags> = {},
setMessage: (msg: string) => void = () => {
/* tests / JSON mode */
}
): Promise<{ report: DoctorReport; exitCode: 0 | 1 }> {
const started = Date.now();

setMessage("Scanning this project...");
const captured = await capture(ctx.cwd);
setMessage("Asking Sentry...");
const server = await resolveServerFacts(captured);
setMessage("Checking configuration...");
const results = runChecks(REGISTRY, { capture: captured, server });

const { judge } = await import("../lib/doctor/judge.js");
results.push(...(await judge(captured)));

if (flags.sendTestEvent) {
setMessage("Sending a test event...");
const { liveRoundtripCheck } = await import("../lib/doctor/live.js");
results.push(await liveRoundtripCheck(captured, server));
} else {
results.push({
id: "live.roundtrip",
status: "skip",
detail: "Not requested. Run with --send-test-event.",
});
}

return {
report: buildReport({
capture: captured,
server,
results,
cliVersion: CLI_VERSION,
timestamp: new Date(started).toISOString(),
elapsedMs: Date.now() - started,
}),
exitCode: exitCodeFor(results),
};
}

export const doctorCommand = buildCommand({
// Runs unauthenticated; a missing session becomes a finding, not a crash.
auth: false,
docs: {
brief: "Check whether Sentry is correctly set up and actually working",
fullDescription:
"Inspects this project's Sentry configuration, asks Sentry what it has " +
"actually received, and reports what is wrong along with instructions " +
"to fix it. Reads only, unless you pass --send-test-event.",
},
output: { human: formatDoctorReport },
parameters: {
flags: {
sendTestEvent: {
kind: "boolean",
brief:
"Send a synthetic event to the configured DSN and confirm it arrives (a write)",
default: false,
},
fix: {
kind: "boolean",
brief: "After reporting, run the setup workflow to produce a fix plan",
default: false,
},
},
positional: { kind: "tuple", parameters: [] },
},
async *func(this: SentryContext, flags: DoctorFlags) {
const { report, exitCode } = await withProgress(
{ message: "Scanning this project...", json: flags.json },
(setMessage) => runDoctor(this, flags, setMessage)
);

yield new CommandOutput(report);

const { offerSupportExport } = await import("../lib/doctor/report.js");
await offerSupportExport(report, flags.json);

if (
flags.fix &&
report.results.some((r) => r.status === "fail" || r.status === "warn")
) {
const { runFix } = await import("../lib/doctor/fix.js");
await runFix(this, report);
}

// Set last: a broken project is a finding, and the report is the payload.
this.process.exitCode = exitCode;
},
});

export default doctorCommand;
Loading
Loading