feat(init): add init command for guided Sentry project setup#283
feat(init): add init command for guided Sentry project setup#283
Conversation
Adds `sentry init` wizard that walks users through project setup via the Mastra API, handling DSN configuration, SDK installation prompts, and local file operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sends tags and metadata (CLI version, OS, arch, node version) with startAsync and resumeAsync calls so workflow runs are visible and filterable in Mastra Studio. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Import randomBytes and generate a hex trace ID so all suspend/resume calls within a single wizard run share one trace. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a synthetic parentSpanId to tracingOptions so all workflow run spans become siblings under the same parent instead of nesting by timestamp containment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The parentSpanId was creating artificial nesting - let the workflow engine handle span hierarchy naturally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Display the branded SENTRY ASCII banner before the intro line for visual consistency with `sentry --help`. Make the "errors" feature always enabled in the feature multi-select so users cannot deselect error monitoring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pt, and source maps hint Route success-with-exitCode results to formatError so the --force hint is shown when Sentry is already installed. Fold the "Error Monitoring is always included" note into the multiselect prompt. Use a more approachable Source Maps hint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show a non-blocking info note about AI usage with a docs link before the first network call, and a review reminder before the success outro. Extract SENTRY_DOCS_URL constant to share between wizard-runner and clack-utils cancel message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add @anthropic-ai/sdk and openai as devDependencies for the LLM-as-judge eval framework. Add opencode-lore dependency. Exclude test/init-eval/templates from biome linting since they are fixture apps, not source code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add LLM-as-judge eval tests for the init wizard across all five platforms (Express, Next.js, Flask, React+Vite, SvelteKit). Each test runs the wizard end-to-end and asserts on SDK installation, Sentry.init presence, build success, and documentation accuracy via an LLM judge. Includes template apps, helper utilities (assertions, doc-fetcher, judge, platform configs), and feature-docs.json mapping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a separate workflow for running init-eval tests on demand. Supports running a single platform or all platforms via matrix. Uses the init-eval GitHub environment for MASTRA_API_URL and OPENAI_API_KEY secrets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store python-fastapi doc URLs as base paths (with trailing slash) like other platforms, and convert to .md at fetch time. This mirrors the pattern in cli-init-api and lets us return clean markdown directly instead of stripping HTML tags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Sentry doc URLs for python-flask (getting-started, errors, tracing, logs, profiling) and add the shared python/profiling page to both flask and fastapi profiling entries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Sentry doc URLs for all nextjs features: getting-started, errors, logs, tracing, session replay, metrics, and profiling (browser + node). Sourcemaps left empty for now. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Sentry doc URLs for sveltekit features and add missing logs, metrics, and profiling features to the platform entry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Sentry doc URLs for react-vite features and add missing logs, metrics, and profiling features to the platform entry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Flask eval was using bare `pip install` which fails when pip isn't on PATH. Use the same venv pattern as fastapi. Also remove accidental opencode-lore runtime dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛
🤖 This preview updates automatically when you update the PR. |
Codecov Results 📊✅ 104 passed | Total: 104 | Pass Rate: 100% | Execution Time: 0ms 📊 Comparison with Base Branch
✨ No test changes detected All tests are passing successfully. ✅ Patch coverage is 95.72%. Project has 4049 uncovered lines. Files with missing lines (7)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
+ Coverage 80.79% 81.98% +1.19%
==========================================
Files 132 140 +8
Lines 21382 22473 +1091
Branches 0 0 —
==========================================
+ Hits 17274 18424 +1150
- Misses 4108 4049 -59
- Partials 0 0 —Generated by Codecov Action |
Restrict GITHUB_TOKEN to contents:read as flagged by CodeQL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update SvelteKit template with working deps (adapter-node, latest svelte/vite) and add required src files (app.d.ts, app.html). Use python3 instead of python for venv creation in Flask/FastAPI platforms. Add --concurrency 6 to init-eval test runner. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add push/pull_request triggers so the eval runs automatically alongside other CI checks. Keep workflow_dispatch for manual single-platform runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…llback (#333) ## Summary Adds the `create-sentry-project` local operation so the remote workflow can ask the CLI to create a Sentry project. Resolves the org via local config / env vars first, falling back to listing orgs from the API (auto-selects if only one, prompts interactively if multiple). ## Changes - New `createSentryProject` handler in `local-ops.ts` with extracted `resolveOrgSlug` helper that handles all org resolution paths (config, single-org auto-select, multi-org interactive prompt, `--yes` guard) - `CreateSentryProjectPayload` type added to `types.ts` - Test suite covering success, single-org fallback, no-orgs, multi-org `--yes`, interactive select, user cancel, API error, and missing DSN paths - Downstream mock setup extracted into `mockDownstreamSuccess` helper to reduce test duplication ## Test Plan ```bash bun test test/lib/init/local-ops.create-sentry-project.test.ts # 8 pass bun run lint # clean ``` --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
#354) ## Summary - Add `getOrgBaseUrl()` helper that builds org-scoped subdomain URLs for SaaS (e.g. `https://my-org.sentry.io`) while returning the base URL unchanged for self-hosted instances - Update all 8 URL builder functions in `sentry-urls.ts` to branch on SaaS vs self-hosted: - **SaaS**: subdomain pattern (`https://my-org.sentry.io/issues/...`) - **Self-hosted**: path-based pattern (`https://sentry.company.com/organizations/my-org/issues/...`) - Add `isSaaS()` private helper that checks the current base URL against `isSentrySaasUrl()` - Add self-hosted test coverage verifying all builders produce path-based URLs with no subdomain prepended ## How this affected our `sentry init` command - before: `https://sentry.io/settings/bete-dev/projects/project-created/` - after: `https://bete-dev.sentry.io/settings/projects/project-created/` ## Test plan - [x] `bun run typecheck` passes - [x] `bun run lint` passes - [x] `bun test test/lib/sentry-urls.property.test.ts` passes (45 tests, including 10 new self-hosted tests) - [x] SaaS URLs still use `{org}.sentry.io` subdomain pattern - [x] Self-hosted URLs use `/organizations/{org}/` or `/settings/{org}/` path patterns
## Summary Removes `add-example-trigger` references from the CLI to match the API repo, where this step was already removed. Cleans up the step label, confirm handler logic, `purpose` field on `ConfirmPayload`, and 5 related test cases. ## Changes - Removed `"add-example-trigger"` from `STEP_LABELS` in `clack-utils.ts` - Removed `addExample` / `isExample` logic from `handleConfirm` in `interactive.ts` - Removed optional `purpose` field from `ConfirmPayload` in `types.ts` - Removed 5 test cases covering example-trigger confirm behavior ## Test plan - `bun run lint` passes - `bun test test/lib/init/interactive.test.ts` — remaining confirm tests pass - `git grep "addExample\|add-example-trigger\|add-example" -- src/ test/` returns 0 matches 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…hset paths Previously, applyPatchsetDryRun recorded every patch as applied regardless of its action, while the real applyPatchset silently skipped unknown actions via default: break. Both paths now return an explicit error on unrecognized patch actions so behavior is consistent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The catch block in the suspend/resume loop was showing "Cancelled" for errors like network timeouts and API failures. Use "Error" to match the label used in other error paths in the same file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # .github/workflows/ci.yml # bun.lock # src/commands/project/create.ts
The merge conflict resolution inadvertently upgraded @mastra/client-js to 1.7.2, pulling in @mastra/core@1.9.0 which introduces a quansync peer dependency that bun 1.3.9 can't resolve with --frozen-lockfile. Restored lockfile from ba6bd73 and re-resolved to keep @mastra/client-js pinned at 1.7.1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…into try/catch Track spinner running state via a mutable SpinState object to guard against calling spin.stop() twice when handleInteractive throws after the spinner was already stopped. Also moved precomputeDirListing inside the existing try/catch as a defensive improvement. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Skip all API calls (org resolution, team creation, project creation, DSN fetch) when dryRun is true and return placeholder data instead. Slug validation is kept before the guard so invalid names are still caught in dry-run mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
When applyPatchset fails mid-application, the error response now includes the list of already-applied patches so the server can track partial progress. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Previously, when createProject returned a 400, only the generic status message was shown (e.g. "Failed to create project: 400 Bad Request"). Now uses ApiError.format() to include the response detail, giving users visibility into why the request failed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
# Conflicts: # bun.lock # package.json # src/commands/project/create.ts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Self-hosted
getOrgBaseUrlsilently drops org slug- Added explicit error throw for self-hosted instances to prevent silent failure when org slug would be dropped.
- ✅ Fixed: Eval judge score zero when all criteria unknown
- Changed default score from 0 to 1.0 when no criteria are gradable, treating inconclusive judge as pass rather than fail.
Or push these changes by commenting:
@cursor push 83acd8e5a2
Preview (83acd8e5a2)
diff --git a/src/lib/sentry-urls.ts b/src/lib/sentry-urls.ts
--- a/src/lib/sentry-urls.ts
+++ b/src/lib/sentry-urls.ts
@@ -21,11 +21,14 @@
*
* @param orgSlug - Organization slug
* @returns Origin URL with org as subdomain
+ * @throws Error if called for self-hosted instances (subdomain pattern only works for SaaS)
*/
export function getOrgBaseUrl(orgSlug: string): string {
const base = getSentryBaseUrl();
if (!isSentrySaasUrl(base)) {
- return base;
+ throw new Error(
+ `getOrgBaseUrl() only supports Sentry SaaS URLs. For self-hosted instances, use path-based URLs with /organizations/${orgSlug}/`
+ );
}
const parsed = new URL(base);
parsed.hostname = `${orgSlug}.${parsed.hostname}`;
diff --git a/test/init-eval/helpers/judge.ts b/test/init-eval/helpers/judge.ts
--- a/test/init-eval/helpers/judge.ts
+++ b/test/init-eval/helpers/judge.ts
@@ -119,7 +119,7 @@
const gradable = parsed.criteria.filter((c) => c.pass !== "unknown");
const passing = gradable.filter((c) => c.pass === true).length;
const total = gradable.length;
- const score = total > 0 ? passing / total : 0;
+ const score = total > 0 ? passing / total : 1.0;
const verdict: JudgeVerdict = {
criteria: parsed.criteria,Move single-char `&` pattern right after `&&`, `||`, `|` so multi-char operators are checked before their single-char prefixes, matching the stated design principle in the comment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| export function buildOrgUrl(orgSlug: string): string { | ||
| if (isSaaS()) { | ||
| return `${getOrgBaseUrl(orgSlug)}/`; | ||
| } |
There was a problem hiding this comment.
Redundant SaaS check in every URL construction call
Low Severity
Every URL-building function (e.g. buildOrgUrl, buildProjectUrl) calls isSaaS() which internally calls getSentryBaseUrl() + isSentrySaasUrl(), and then calls getOrgBaseUrl() which internally calls getSentryBaseUrl() + isSentrySaasUrl() again. This means each URL construction makes two redundant calls to read the environment variable and validate the hostname. The getOrgBaseUrl function could accept the already-resolved base URL as a parameter, or the SaaS check result could be passed through.
Additional Locations (1)
| result.error ?? inner?.message ?? "Wizard failed with an unknown error"; | ||
| const exitCode = inner?.exitCode ?? 1; | ||
|
|
||
| log.error(String(message)); |
There was a problem hiding this comment.
Empty error string shows blank message to user
Low Severity
formatError uses ?? (nullish coalescing) for the error message fallback chain: result.error ?? inner?.message ?? "Wizard failed...". If the server returns error: "" (empty string), ?? does not treat it as nullish, so message becomes "" and log.error("") displays a blank error line. Using || instead of the first ?? would fall through to the next alternative for empty strings.
BYK
left a comment
There was a problem hiding this comment.
I think we are in a good place, I don't think any of my comments are blockers to get this out of the door with a huge "experimental" banner.
One thing I noticed is we don't check if we are operating in a git repo, whether it is clear and safe for us to do things and roll back etc. I remember sentry-wizard having some code for this so I think we can just steal that for increased safety.
Once we are out of experimental mode, we should definitely re-evaluate the safety of our generic command call helper. On Linux systems, we might be able to easily jail that if we want to keep using generic bash commands.
| Initialize Sentry in your project | ||
|
|
||
| **Flags:** | ||
| - `--force - Continue even if Sentry is already installed` |
There was a problem hiding this comment.
This is a bit weird as we may want to extend existing Sentry installation (only errors to errors + tracing etc)
| fullDescription: | ||
| "Runs the Sentry setup wizard to detect your project's framework, " + | ||
| "install the SDK, and configure error monitoring. Uses a remote " + | ||
| "workflow that coordinates local file operations through the CLI.", |
There was a problem hiding this comment.
Uses a remote workflow that coordinates local file operations through the CLI.
Do we really need to call this out in the help text?
| flags: { | ||
| force: { | ||
| kind: "boolean", | ||
| brief: "Continue even if Sentry is already installed", | ||
| default: false, | ||
| }, |
There was a problem hiding this comment.
As mentioned earlier, I vote to remove this.
| features: { | ||
| kind: "parsed", | ||
| parse: String, | ||
| brief: "Comma-separated features: errors,tracing,logs,replay,metrics", |
There was a problem hiding this comment.
I'd also allow + and (for quoted args) for defensive parsing. We may also consider allowing multiple --features or --feature flags to join them. Similar to what we do with --field in the api command
|
|
||
| export function abortIfCancelled<T>(value: T | symbol): T { | ||
| if (isCancel(value)) { | ||
| cancel(`Setup cancelled. Visit ${SENTRY_DOCS_URL} to set up manually.`); |
There was a problem hiding this comment.
| cancel(`Setup cancelled. Visit ${SENTRY_DOCS_URL} to set up manually.`); | |
| cancel(`Setup cancelled. You can visit ${SENTRY_DOCS_URL} to set up manually.`); |
| const { directory, force, yes, dryRun, features } = options; | ||
|
|
||
| if (!(yes || process.stdin.isTTY)) { | ||
| process.stderr.write( |
| return; | ||
| } | ||
|
|
||
| process.stderr.write(`\n${formatBanner()}\n\n`); |
| } | ||
|
|
||
| process.stderr.write(`\n${formatBanner()}\n\n`); | ||
| intro("sentry init"); |
There was a problem hiding this comment.
Why is the banner printing part not part of the intro helper already?
|
|
||
| const tracingOptions = { | ||
| traceId: randomBytes(16).toString("hex"), | ||
| tags: ["sentry-cli", "init-wizard"], |
There was a problem hiding this comment.
Okay, what? What is the sentry-cli tag? What are we doing here with a custom trace id etc?
| return; | ||
| } | ||
|
|
||
| const resumeData = await handleSuspendedStep( |
There was a problem hiding this comment.
I'd say if we can, we should support resuming from suspension from information stored on disk in case of an unexpected process termination



Summary
Adds
sentry init— an AI-powered wizard that walks users through adding Sentry to their project. It detects the platform, installs the SDK, instruments the code, and configures error monitoring, tracing, and session replay.Changes
Core wizard
initcommand backed by a Mastra AI workflow (hosted at getsentry/cli-init-api) that handles platform detection, SDK installation, and code instrumentationconstants.ts)Security hardening
()subshell bypass,><&redirection/background,$'"\expansion/escaping,{}*?glob/brace expansion,#shell commentVAR=value cmdpattern)cwdvalidation against project directoryreadSyncfailure{ shell: true }instead of hardcodedsh)Performance
list-dirround-trip_prevPhasescross-phase caching to reuse results across workflow steps (perf(init): pre-compute dir listing and send _prevPhases for cross-phase caching #307)Testing & CI
src/lib/banner.tsto break circular importspyOninstead ofmock.modulewhere possible)Eval suite
The eval suite validates that the wizard produces correct, buildable Sentry instrumentation for each supported platform. It uses a 3-phase test architecture:
Phase 1: Wizard run
Each test scaffolds a fresh project from a platform template, then runs the full
sentry initwizard against it. The wizard output (exit code, stdout/stderr, git diff, new files) is captured for the next phases.Phase 2: Hard assertions (deterministic)
Five code-based pass/fail checks that run without any LLM:
package.json/requirements.txt)Sentry.init(orsentry_sdk.init) appears in changed or new files___PUBLIC_DSN___,YOUR_DSN_HERE, etc.)npm run build/ equivalent passes after the wizard's changesPhase 3: LLM judge (per-feature)
For each feature (errors, tracing, replay, logs, profiling, etc.), an LLM judge scores correctness:
feature-docs.json)Platforms
6 platform templates are covered:
express/@sentry/nodenextjs/@sentry/nextjssveltekit/@sentry/sveltekitreact-vite/@sentry/reactpython-flask/sentry-sdkpython-fastapi/sentry-sdkRunning
bun run test:init-eval # all platformsRequires
SENTRY_AUTH_TOKEN,SENTRY_ORG,SENTRY_PROJECT, and optionallyOPENAI_API_KEY(LLM judge is skipped without it).Test Plan
bun run lintandbun run typecheckpass🤖 Generated with Claude Code