Skip to content
Draft
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
349 changes: 1 addition & 348 deletions bun.lock

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions docs/src/fragments/commands/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ sentry init acme/my-app
# Assign a team when creating a new project
sentry init acme/ --team backend

# Enable specific features
sentry init --features profiling,replay
# Skip the agent-driven feature picker and use a fixed list (CI / non-interactive)
sentry init --features tracing,replay,sourcemaps
```

## Target Syntax
Expand Down Expand Up @@ -60,9 +60,13 @@ Path-like arguments (starting with `.`, `/`, or `~`) are always treated as the d

## What the Wizard Does

1. **Detects your framework** — scans your project files to identify the platform and framework
2. **Installs the SDK** — adds the appropriate Sentry SDK package to your project
3. **Instruments your code** — configures error monitoring, tracing, and any selected features
1. **Detects your framework** — scans your project files to identify the platform, framework, runtime, and relevant libraries
2. **Researches the docs** — walks the Sentry docs to figure out which features are supported and useful for *your* project
3. **Asks which features to enable** — proposes only the features that fit your stack (e.g. no Session Replay on a server-only Node app), with a short reason next to each one. Error monitoring is always on
4. **Installs the SDK** — adds the appropriate Sentry SDK package to your project
5. **Instruments your code** — configures error monitoring and the features you picked

Use `--features <list>` to skip the analyze-and-pick step (handy for `--yes` / CI). When provided, the wizard treats it as canonical and the agent goes straight to instrumentation.

### Supported Platforms

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"@anthropic-ai/sdk": "^0.39.0",
"@biomejs/biome": "2.3.8",
"@clack/prompts": "^0.11.0",
"@mastra/client-js": "^1.4.0",
"@sentry/api": "^0.113.0",
"@sentry/node-core": "10.50.0",
"@sentry/sqlish": "^1.0.0",
Expand Down
4 changes: 2 additions & 2 deletions plugins/sentry-cli/skills/sentry-cli/references/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ sentry init acme/my-app
# Assign a team when creating a new project
sentry init acme/ --team backend

# Enable specific features
sentry init --features profiling,replay
# Skip the agent-driven feature picker and use a fixed list (CI / non-interactive)
sentry init --features tracing,replay,sourcemaps
```

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
14 changes: 7 additions & 7 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* sentry init
*
* Initialize Sentry in a project using the remote wizard workflow.
* Communicates with the Mastra API via suspend/resume to perform
* local filesystem operations and interactive prompts.
* Initialize Sentry in a project using the remote wizard.
* Starts a Vercel Workflow that runs a Claude Agent SDK session in a
* sandbox and bridges file/command operations back to this CLI.
*
* Supports two optional positionals with smart disambiguation:
* sentry init — auto-detect everything, dir = cwd
Expand All @@ -24,7 +24,7 @@ import { looksLikePath, parseOrgProjectArg } from "../lib/arg-parsing.js";
import { buildCommand } from "../lib/command.js";
import { ContextError, ValidationError } from "../lib/errors.js";
import { warmOrgDetection } from "../lib/init/org-prefetch.js";
import { runWizard } from "../lib/init/wizard-runner.js";
import { runInit } from "../lib/init/init-runner.js";
import { validateResourceId } from "../lib/input-validation.js";
import { logger } from "../lib/logger.js";
import {
Expand Down Expand Up @@ -277,7 +277,7 @@ export const initCommand = buildCommand<
// would skip the timer and the process would hang on the error
// display — exactly what Cursor Bugbot flagged on an earlier revision.
try {
await runWizard({
await runInit({
directory: targetDir,
yes: flags.yes,
dryRun: flags["dry-run"],
Expand All @@ -289,11 +289,11 @@ export const initCommand = buildCommand<
} finally {
// 7. macOS-only force-exit safety net.
//
// On Darwin, `runWizard` installs the `/dev/tty` forwarding
// On Darwin, `runInit` installs the `/dev/tty` forwarding
// workaround from stdin-reopen.ts to get keystrokes through to
// clack. That workaround opens a second `tty.ReadStream` which
// leaks a libuv handle on Bun 1.3.11 — no userland cleanup
// releases it (upstream oven-sh/bun#29126). After `runWizard`
// releases it (upstream oven-sh/bun#29126). After `runInit`
// returns (or throws), the event loop stays ref'd and the process
// hangs until the user presses a key.
//
Expand Down
2 changes: 1 addition & 1 deletion src/lib/init/clack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const STEP_LABELS: Record<string, string> = {
"check-existing-sentry": "Checking for existing Sentry installation",
"detect-platform": "Detecting platform and framework",
"ensure-sentry-project": "Setting up Sentry project",
"select-features": "Selecting features",
"propose-features": "Selecting features",
"install-deps": "Installing dependencies",
"plan-codemods": "Planning code modifications",
"apply-codemods": "Applying code modifications",
Expand Down
42 changes: 29 additions & 13 deletions src/lib/init/constants.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
export const MASTRA_API_URL =
process.env.MASTRA_API_URL ??
"https://sentry-init-agent.getsentry.workers.dev";
export const INIT_API_URL =
process.env.SENTRY_INIT_API_URL ??
process.env.INIT_API_URL ??
"https://sentry-init-agent.vercel.app";

export const WORKFLOW_ID = "sentry-wizard";
/**
* Initial-handshake timeout for `GET /api/init/:runId/stream` and
* `GET /api/init/:runId` (status). The stream body is a long-lived
* NDJSON pipe that idles for minutes between events; the runner
* handles that via the `handleStreamClosure` -> status check ->
* `resumeRun` loop, so this only protects the request *connect* phase.
*/
export const STREAM_CONNECT_TIMEOUT_MS = 30_000;

/**
* Maximum consecutive failures of `GET /api/init/:runId` (the status
* endpoint) before we give up. Stream drops themselves are normal and
* NOT counted: they flow into `handleStreamClosure` which fetches
* status, branches on running/completed/failed/cancelled, and
* reconnects when appropriate. Mirrors birthday-card-generator's
* `maxConsecutiveErrors: 5` on `WorkflowChatTransport`.
*/
export const MAX_STATUS_FAILURES = 5;

/**
* Cap exponential backoff between status-failure reconnect attempts so
* we don't sleep for minutes after a flake.
*/
export const MAX_RECONNECT_DELAY_MS = 30_000;

export const SENTRY_DOCS_URL = "https://docs.sentry.io/platforms/";

export const MAX_FILE_BYTES = 262_144; // 256KB per file
export const MAX_OUTPUT_BYTES = 65_536; // 64KB stdout/stderr truncation
export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000; // 2 minutes
export const API_TIMEOUT_MS = 120_000; // 2 minutes timeout for Mastra API calls

// Exit codes returned by the remote workflow
export const EXIT_PLATFORM_NOT_DETECTED = 20;
export const EXIT_DEPENDENCY_INSTALL_FAILED = 30;
export const EXIT_VERIFICATION_FAILED = 50;

// Step ID used in dry-run special-case logic
export const VERIFY_CHANGES_STEP = "verify-changes";
export const API_TIMEOUT_MS = 120_000; // 2 minutes timeout for API calls

// The feature that is always included in every setup
export const REQUIRED_FEATURE = "errorMonitoring";
135 changes: 135 additions & 0 deletions src/lib/init/ensure-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Ensure a Sentry project + DSN exist before the workflow starts.
*
* Mirrors the legacy server-side `create-sentry-project` tool, but
* runs entirely in CLI preflight so the workflow input is complete
* by the time we POST `/api/init`. The agent never has to ask the
* user to pick an org/team/project mid-run.
*/

import { log } from "@clack/prompts";
import { createProjectWithDsn } from "../api-client.js";
import { ApiError, WizardError } from "../errors.js";
import { resolveOrCreateTeam } from "../resolve-team.js";
import { slugify } from "../utils.js";
import { tryGetExistingProjectData } from "./existing-project.js";
import type { ExistingProjectData, ResolvedInitContext } from "./types.js";

/** Default platform slug used to create new projects from `sentry init`. */
const DEFAULT_CREATE_PLATFORM = "javascript";

export type EnsuredProject = {
orgSlug: string;
teamSlug?: string;
projectSlug: string;
projectId: string;
dsn: string;
url: string;
/** True if the project existed before this run. */
preExisting: boolean;
};

export async function ensureSentryProject(
ctx: ResolvedInitContext
): Promise<EnsuredProject> {
const explicit = ctx.existingProject;
if (explicit) {
return projectFromExisting(explicit, ctx.team, true);
}

const projectName = ctx.project ?? deriveProjectName(ctx.directory);
const slug = slugify(projectName);
if (!slug) {
throw new WizardError(
`Cannot create project: "${projectName}" produces an empty slug.`
);
}

// First check if it already exists under the resolved org.
try {
const existing = await tryGetExistingProjectData(ctx.org, slug);
if (existing) {
return projectFromExisting(existing, ctx.team, true);
}
} catch (err) {
if (!(err instanceof ApiError && err.status === 404)) {
throw err;
}
}

if (ctx.dryRun) {
return {
orgSlug: ctx.org,
teamSlug: ctx.team,
projectSlug: slug,
projectId: "(dry-run)",
dsn: "https://key@o0.ingest.sentry.io/0",
url: "https://sentry.io/dry-run",
preExisting: false,
};
}

// Create the project. Resolve the team if it wasn't already.
const teamSlug = ctx.team
? ctx.team
: (
await resolveOrCreateTeam(ctx.org, {
autoCreateSlug: slug,
usageHint: "sentry init",
dryRun: ctx.dryRun,
})
).slug;

log.info(`Creating Sentry project '${slug}' in ${ctx.org}/${teamSlug}...`);

const { project, dsn, url } = await createProjectWithDsn(ctx.org, teamSlug, {
name: projectName,
platform: DEFAULT_CREATE_PLATFORM,
});

if (!dsn) {
throw new WizardError(
`Project '${project.slug}' created in ${ctx.org} but no DSN was issued.`
);
}

return {
orgSlug: ctx.org,
teamSlug,
projectSlug: project.slug,
projectId: project.id,
dsn,
url,
preExisting: false,
};
}

function projectFromExisting(
existing: ExistingProjectData,
team: string | undefined,
preExisting: boolean
): EnsuredProject {
if (!existing.dsn) {
throw new WizardError(
`Existing project '${existing.projectSlug}' has no DSN configured.`
);
}
return {
orgSlug: existing.orgSlug,
teamSlug: team,
projectSlug: existing.projectSlug,
projectId: existing.projectId,
dsn: existing.dsn,
url: existing.url,
preExisting,
};
}

function deriveProjectName(directory: string): string {
// Last non-empty path segment. `path.basename` works on Posix and Windows.
const parts = directory
.replaceAll("\\", "/")
.split("/")
.filter((p) => p.length > 0);
return parts.at(-1) ?? "sentry-project";
}
39 changes: 9 additions & 30 deletions src/lib/init/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ import { cancel, log, outro } from "@clack/prompts";
import { terminalLink } from "../formatters/colors.js";
import { colorTag, mdKvTable, renderMarkdown } from "../formatters/markdown.js";
import { featureLabel } from "./clack-utils.js";
import {
EXIT_DEPENDENCY_INSTALL_FAILED,
EXIT_PLATFORM_NOT_DETECTED,
EXIT_VERIFICATION_FAILED,
} from "./constants.js";
import type { WizardOutput, WorkflowRunResult } from "./types.js";
import type { InitErrorEvent, WizardOutput } from "./types.js";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary formatter uses stale feature label map

Medium Severity

formatters.ts imports featureLabel from clack-utils.ts, which uses the old FEATURE_INFO map. That map lacks the new canonical tracing ID introduced by select-features.ts. If the workflow summary includes tracing in its features list, the final output renders the raw string "tracing" instead of "Performance Monitoring (Tracing)". The interactive prompt in interactive.ts already switched to FEATURE_LABELS from select-features.ts, creating an inconsistency between the picker and the summary.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 33a8892. Configure here.


type ChangedFile = NonNullable<WizardOutput["changedFiles"]>[number];

Expand Down Expand Up @@ -160,8 +155,7 @@ function buildSummary(output: WizardOutput): string {
return sections.join("\n\n");
}

export function formatResult(result: WorkflowRunResult): void {
const output: WizardOutput = result.result ?? {};
export function formatResult(output: WizardOutput): void {
const md = buildSummary(output);

if (md.length > 0) {
Expand All @@ -182,32 +176,17 @@ export function formatResult(result: WorkflowRunResult): void {
outro("Sentry SDK installed successfully!");
}

export function formatError(result: WorkflowRunResult): void {
const inner = result.result;
const message =
result.error ?? inner?.message ?? "Wizard failed with an unknown error";
const exitCode = inner?.exitCode ?? 1;
export function formatError(error: InitErrorEvent): void {
log.error(error.message);

log.error(String(message));

if (exitCode === EXIT_PLATFORM_NOT_DETECTED) {
log.warn(
"Hint: Could not detect your project's platform. Check that the directory contains a valid project."
);
} else if (exitCode === EXIT_DEPENDENCY_INSTALL_FAILED) {
const commands = inner?.commands;
if (commands?.length) {
log.warn(
`You can install dependencies manually:\n${commands.map((cmd) => ` $ ${cmd}`).join("\n")}`
);
if (error.output?.warnings?.length) {
for (const w of error.output.warnings) {
log.warn(w);
}
} else if (exitCode === EXIT_VERIFICATION_FAILED) {
log.warn("Hint: Fix the verification issues and run 'sentry init' again.");
}

const docsUrl = inner?.docsUrl;
if (docsUrl) {
log.info(`Docs: ${terminalLink(docsUrl)}`);
if (error.docsUrl) {
log.info(`Docs: ${terminalLink(error.docsUrl)}`);
}

cancel("Setup failed");
Expand Down
Loading
Loading