diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md index 9edc15c52..92d989351 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md @@ -18,7 +18,7 @@ Initialize Sentry in your project (experimental) **Flags:** - `-y, --yes - Accept non-interactive defaults (requires --features outside a TTY)` - `-n, --dry-run - Show what would happen without making changes` -- `--features ... - Features to enable: errors,tracing,logs,replay,metrics,profiling,sourcemaps,crons,ai-monitoring` +- `--features ... - Features to enable: errors,tracing,logs,replay,metrics,profiling,sourcemaps,crons,attachments,agent-tracing,mcp-observability` - `-t, --team - Team slug to create the project under` - `--app - App to initialize in a monorepo (required with --yes when multiple apps are detected)` - `--tui - Use the Ink-based interactive UI (default). Pass --no-tui to fall back to plain log output.` diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 5501596fc..e5e5d582d 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -56,8 +56,13 @@ const FEATURE_ALIASES = { sourcemaps: "sourceMaps", sourceMaps: "sourceMaps", crons: "crons", + attachments: "attachments", "ai-monitoring": "aiMonitoring", aiMonitoring: "aiMonitoring", + "agent-tracing": "aiMonitoring", + agentTracing: "aiMonitoring", + "mcp-observability": "mcpObservability", + mcpObservability: "mcpObservability", } as const; const SUPPORTED_FEATURE_NAMES = [ @@ -69,7 +74,10 @@ const SUPPORTED_FEATURE_NAMES = [ "profiling", "sourcemaps", "crons", + "attachments", "ai-monitoring", + "agent-tracing", + "mcp-observability", ] as const; const SUPPORTED_FEATURE_TEXT = SUPPORTED_FEATURE_NAMES.join(", "); @@ -334,7 +342,7 @@ export const initCommand = buildCommand< kind: "parsed", parse: String, brief: - "Features to enable: errors,tracing,logs,replay,metrics,profiling,sourcemaps,crons,ai-monitoring", + "Features to enable: errors,tracing,logs,replay,metrics,profiling,sourcemaps,crons,attachments,agent-tracing,mcp-observability", variadic: true, optional: true, }, diff --git a/packages/cli/src/lib/init/clack-utils.ts b/packages/cli/src/lib/init/clack-utils.ts index 8ba347fc2..25c103033 100644 --- a/packages/cli/src/lib/init/clack-utils.ts +++ b/packages/cli/src/lib/init/clack-utils.ts @@ -54,7 +54,7 @@ const FEATURE_INFO: Record = { "Pinpoint the functions and lines of code responsible for performance issues", }, logs: { - label: "Logging", + label: "Logs", description: "See logs in context with errors and performance issues", }, metrics: { @@ -68,11 +68,15 @@ const FEATURE_INFO: Record = { "Turn minified production stack traces back into your original source code", }, crons: { - label: "Crons", + label: "Crons & Uptime Monitors", description: "Detect failed, missed, or delayed scheduled jobs", }, + attachments: { + label: "Attachments", + description: "Link user-supplied data to captured events", + }, aiMonitoring: { - label: "AI Monitoring", + label: "Agent Tracing", description: "Understand AI calls, latency, token usage, cost, and failures", }, @@ -104,17 +108,18 @@ export function featureDescription(id: string): string | undefined { const FEATURE_DISPLAY_ORDER = [ "errorMonitoring", + "logs", "sessionReplay", "performanceMonitoring", - "logs", + "aiMonitoring", + "attachments", + "crons", "metrics", + "mcpObservability", "profiling", + "reactFeatures", "sourceMaps", - "crons", - "aiMonitoring", - "mcpObservability", "userFeedback", - "reactFeatures", ]; /** Sort features into the canonical order used by summaries and final output. */ @@ -134,7 +139,7 @@ export const STEP_LABELS: Record = { "select-target-app": "Selecting target application", "resolve-dir": "Resolving project directory", "check-existing-sentry": "Checking for existing Sentry installation", - "detect-platform": "Detecting platform and framework", + "detect-platform": "Analyzing project and Sentry features", "ensure-sentry-project": "Setting up Sentry project", "select-features": "Selecting features", "plan-codemods": "Planning code modifications", @@ -204,7 +209,7 @@ export const STEP_ACTIVE_LABELS: Record = { "select-target-app": "Selecting target application...", "resolve-dir": "Resolving project directory...", "check-existing-sentry": "Checking for existing Sentry setup...", - "detect-platform": "Detecting framework and platform...", + "detect-platform": "Analyzing project and Sentry support...", "ensure-sentry-project": "Configuring Sentry project...", "select-features": "Preparing feature selection...", "plan-codemods": "Planning code changes...", @@ -221,8 +226,8 @@ export const STEP_ACTIVE_LABELS: Record = { * Falls back to the full label if a step isn't listed here. */ export const STEP_LABELS_SHORT: Record = { - "discover-context": "Analyzing project", - "detect-platform": "Detecting platform", + "discover-context": "Discovering project", + "detect-platform": "Checking Sentry support", "ensure-sentry-project": "Setting up project", "select-features": "Selecting features", "plan-codemods": "Planning changes", @@ -256,9 +261,11 @@ export const STEP_PROGRESS_MESSAGES: Record = { ], "detect-platform": [ "Scanning project files...", - "Identifying framework and language...", - "Analyzing project configuration...", - "Determining SDK compatibility...", + "Identifying framework and runtime...", + "Matching the Sentry SDK...", + "Searching official Sentry docs...", + "Checking feature support...", + "Validating project recommendations...", ], }; diff --git a/packages/cli/src/lib/init/interactive.ts b/packages/cli/src/lib/init/interactive.ts index acd8ff66b..eb98b930f 100644 --- a/packages/cli/src/lib/init/interactive.ts +++ b/packages/cli/src/lib/init/interactive.ts @@ -46,8 +46,12 @@ const DEFAULT_FEATURES = new Set(DEFAULT_FEATURE_ORDER); const DEFAULT_FEATURE_RANK = new Map( DEFAULT_FEATURE_ORDER.map((feature, index) => [feature, index]) ); -// Feedback setup needs an in-app placement choice this wizard cannot make yet. -const UNSUPPORTED_INIT_FEATURES = new Set(["userFeedback"]); +// These features need code-level choices this selector cannot configure yet. +const UNSUPPORTED_INIT_FEATURES = new Set([ + "attachments", + "metrics", + "userFeedback", +]); const FEATURE_SELECTION_CONTEXT = "Based on your project, these features are available to set up."; @@ -69,7 +73,8 @@ function normalizeFeatureSelection(features: string[]): string[] { // the reviewed, resumed, and restored-on-Back selections identical. if ( features.includes("aiMonitoring") || - features.includes("mcpObservability") + features.includes("mcpObservability") || + features.includes("profiling") ) { normalized.add("performanceMonitoring"); } diff --git a/packages/cli/src/lib/init/tools/agent-checkpoint.ts b/packages/cli/src/lib/init/tools/agent-checkpoint.ts new file mode 100644 index 000000000..b97ddca4b --- /dev/null +++ b/packages/cli/src/lib/init/tools/agent-checkpoint.ts @@ -0,0 +1,16 @@ +/** + * No-op init tool for acknowledging durable server-side agent checkpoints. + */ + +import type { AgentCheckpointData, ToolResult } from "../types.js"; +import type { InitToolDefinition } from "./types.js"; + +/** Resume a persisted agent session without performing local work. */ +export const agentCheckpointTool: InitToolDefinition<"agent-checkpoint"> = { + operation: "agent-checkpoint", + describe: (payload) => payload.detail ?? "Continuing repository analysis...", + execute: async (): Promise => ({ + data: { acknowledged: true } satisfies AgentCheckpointData, + ok: true, + }), +}; diff --git a/packages/cli/src/lib/init/tools/registry.ts b/packages/cli/src/lib/init/tools/registry.ts index e14feab56..340445a3d 100644 --- a/packages/cli/src/lib/init/tools/registry.ts +++ b/packages/cli/src/lib/init/tools/registry.ts @@ -1,5 +1,6 @@ import { ApiError } from "../../errors.js"; import type { ToolOperation, ToolPayload, ToolResult } from "../types.js"; +import { agentCheckpointTool } from "./agent-checkpoint.js"; import { applyPatchsetTool } from "./apply-patchset.js"; import { createSentryProjectTool, @@ -16,6 +17,7 @@ import { formatToolError, validateToolSandbox } from "./shared.js"; import type { AnyInitToolDefinition, ToolContext } from "./types.js"; const toolDefinitions = [ + agentCheckpointTool, listDirTool, readFilesTool, fileExistsBatchTool, @@ -34,6 +36,7 @@ const toolRegistry = new Map( /** Sentry API operations never inspect or mutate the local filesystem. */ const CWD_INDEPENDENT_OPERATIONS = new Set([ + "agent-checkpoint", "create-sentry-project", "ensure-sentry-project", ]); diff --git a/packages/cli/src/lib/init/types.ts b/packages/cli/src/lib/init/types.ts index a62ce5d8a..965e09a88 100644 --- a/packages/cli/src/lib/init/types.ts +++ b/packages/cli/src/lib/init/types.ts @@ -76,6 +76,7 @@ export type InitProtocolEnvelope = { // Tool suspend payloads export type ToolPayload = + | AgentCheckpointPayload | ListDirPayload | ReadFilesPayload | FileExistsBatchPayload @@ -89,6 +90,28 @@ export type ToolPayload = export type ToolOperation = ToolPayload["operation"]; +/** + * Suspend request used to continue a persisted server-side agent session. + * The CLI acknowledges it without inspecting or mutating the local project. + */ +export type AgentCheckpointPayload = { + /** Discriminator for locally handled workflow tools. */ + type: "tool"; + /** Stable no-op operation name. */ + operation: "agent-checkpoint"; + /** Optional progress copy displayed while the checkpoint is acknowledged. */ + detail?: string; + /** Workflow filesystem root; not accessed by this operation. */ + cwd: string; + /** Reserved operation arguments; currently always empty. */ + params: Record; +}; + +/** Explicit no-op acknowledgement validated by the init service boundary. */ +export type AgentCheckpointData = { + acknowledged: true; +}; + export type ListDirPayload = { type: "tool"; operation: "list-dir"; diff --git a/packages/cli/test/commands/init.test.ts b/packages/cli/test/commands/init.test.ts index dfe699c28..583ecd15b 100644 --- a/packages/cli/test/commands/init.test.ts +++ b/packages/cli/test/commands/init.test.ts @@ -189,13 +189,18 @@ describe("init command func", () => { const ctx = makeContext(); await func.call(ctx, { ...DEFAULT_FLAGS, - features: ["errors,tracing,replay,sourcemaps"], + features: [ + "errors,tracing,replay,sourcemaps,attachments,agent-tracing,mcp-observability", + ], }); expect(capturedArgs?.features).toEqual([ "errorMonitoring", "performanceMonitoring", "sessionReplay", "sourceMaps", + "attachments", + "aiMonitoring", + "mcpObservability", ]); }); @@ -204,7 +209,7 @@ describe("init command func", () => { await func.call(ctx, { ...DEFAULT_FLAGS, features: [ - "errorMonitoring,performanceMonitoring,sessionReplay,sourceMaps", + "errorMonitoring,performanceMonitoring,sessionReplay,sourceMaps,attachments,aiMonitoring,mcpObservability", ], }); expect(capturedArgs?.features).toEqual([ @@ -212,6 +217,9 @@ describe("init command func", () => { "performanceMonitoring", "sessionReplay", "sourceMaps", + "attachments", + "aiMonitoring", + "mcpObservability", ]); }); @@ -223,7 +231,7 @@ describe("init command func", () => { }); await expect(promise).rejects.toThrow(ValidationError); await expect(promise).rejects.toThrow( - "Supported features: errors, tracing, logs, replay, metrics, profiling, sourcemaps, crons, ai-monitoring" + "Supported features: errors, tracing, logs, replay, metrics, profiling, sourcemaps, crons, attachments, ai-monitoring, agent-tracing, mcp-observability" ); expect(runWizardSpy).not.toHaveBeenCalled(); expect(findProjectsSpy).not.toHaveBeenCalled(); diff --git a/packages/cli/test/lib/init/clack-utils.test.ts b/packages/cli/test/lib/init/clack-utils.test.ts index 81a411009..82935cdaa 100644 --- a/packages/cli/test/lib/init/clack-utils.test.ts +++ b/packages/cli/test/lib/init/clack-utils.test.ts @@ -50,9 +50,11 @@ describe("featureLabel", () => { test("returns label for known feature", () => { expect(featureLabel("errorMonitoring")).toBe("Error Monitoring"); expect(featureLabel("performanceMonitoring")).toBe("Tracing"); - expect(featureLabel("logs")).toBe("Logging"); - expect(featureLabel("crons")).toBe("Crons"); - expect(featureLabel("aiMonitoring")).toBe("AI Monitoring"); + expect(featureLabel("logs")).toBe("Logs"); + expect(featureLabel("crons")).toBe("Crons & Uptime Monitors"); + expect(featureLabel("attachments")).toBe("Attachments"); + expect(featureLabel("aiMonitoring")).toBe("Agent Tracing"); + expect(featureLabel("mcpObservability")).toBe("MCP Observability"); expect(featureLabel("userFeedback")).toBe("User Feedback"); }); @@ -87,6 +89,9 @@ describe("featureDescription", () => { expect(featureDescription("crons")).toBe( "Detect failed, missed, or delayed scheduled jobs" ); + expect(featureDescription("attachments")).toBe( + "Link user-supplied data to captured events" + ); expect(featureDescription("aiMonitoring")).toBe( "Understand AI calls, latency, token usage, cost, and failures" ); @@ -115,16 +120,18 @@ describe("sortFeatures", () => { "errorMonitoring", "sourceMaps", "crons", + "attachments", "aiMonitoring", "mcpObservability", ]) ).toEqual([ "errorMonitoring", "logs", - "sourceMaps", - "crons", "aiMonitoring", + "attachments", + "crons", "mcpObservability", + "sourceMaps", "userFeedback", ]); }); diff --git a/packages/cli/test/lib/init/interactive.test.ts b/packages/cli/test/lib/init/interactive.test.ts index b3fc40f06..108b810c3 100644 --- a/packages/cli/test/lib/init/interactive.test.ts +++ b/packages/cli/test/lib/init/interactive.test.ts @@ -490,7 +490,7 @@ describe("handleMultiSelect", () => { }); }); - test("shows defaults first, sorts optional features, and omits User Feedback", async () => { + test("shows defaults first, sorts optional features, and omits unsupported features", async () => { const { ui, calls, respond } = createMockUI(); respond.multiselect(["sessionReplay"]); respond.select("continue"); @@ -509,7 +509,9 @@ describe("handleMultiSelect", () => { "sessionReplay", "logs", "crons", + "attachments", "aiMonitoring", + "mcpObservability", "userFeedback", ], }, @@ -528,8 +530,8 @@ describe("handleMultiSelect", () => { "sessionReplay", "performanceMonitoring", "aiMonitoring", - "metrics", "crons", + "mcpObservability", "profiling", "sourceMaps", ]); @@ -544,6 +546,8 @@ describe("handleMultiSelect", () => { text: "Based on your project, these features are available to set up.", }, ]); + expect(multiselectCall?.options).not.toContain("metrics"); + expect(multiselectCall?.options).not.toContain("attachments"); expect(multiselectCall?.options).not.toContain("userFeedback"); const reviewCall = calls.find((call) => call.kind === "select"); @@ -607,9 +611,10 @@ describe("handleMultiSelect", () => { }); test.each([ - "aiMonitoring", - "mcpObservability", - ])("review includes tracing when %s enables it implicitly", async (dependencyFeature) => { + ["aiMonitoring", "Agent Tracing"], + ["mcpObservability", "MCP Observability"], + ["profiling", "Profiling"], + ])("review includes tracing when %s enables it implicitly", async (dependencyFeature, dependencyLabel) => { const { ui, calls, respond } = createMockUI(); respond.multiselect([dependencyFeature]); respond.select("continue"); @@ -636,9 +641,7 @@ describe("handleMultiSelect", () => { const reviewDetails = reviewCall?.details?.map((detail) => detail.text); expect(reviewDetails).toContain("✓ Error Monitoring"); expect(reviewDetails).toContain("✓ Tracing"); - expect(reviewDetails).toContain( - `✓ ${dependencyFeature === "aiMonitoring" ? "AI Monitoring" : "MCP Observability"}` - ); + expect(reviewDetails).toContain(`✓ ${dependencyLabel}`); }); test("Back restores the normalized AI selection including Tracing", async () => { diff --git a/packages/cli/test/lib/init/tools/registry.test.ts b/packages/cli/test/lib/init/tools/registry.test.ts index 7d89f7aa2..1d8689a43 100644 --- a/packages/cli/test/lib/init/tools/registry.test.ts +++ b/packages/cli/test/lib/init/tools/registry.test.ts @@ -19,6 +19,24 @@ function makeContext(): ResolvedInitContext { } describe("tool registry", () => { + test("acknowledges an agent checkpoint without local work", async () => { + const payload: ToolPayload = { + cwd: "/outside/project", + detail: "Reviewing official Sentry feature support...", + operation: "agent-checkpoint", + params: {}, + type: "tool", + }; + + expect(describeTool(payload)).toBe( + "Reviewing official Sentry feature support..." + ); + await expect(executeTool(payload, makeContext())).resolves.toEqual({ + data: { acknowledged: true }, + ok: true, + }); + }); + test("describes tool payloads via the registered definition", () => { const payload: ToolPayload = { type: "tool", diff --git a/packages/cli/test/lib/init/ui/ink-app.snapshot.test.tsx b/packages/cli/test/lib/init/ui/ink-app.snapshot.test.tsx index 31a24e469..0b3d45556 100644 --- a/packages/cli/test/lib/init/ui/ink-app.snapshot.test.tsx +++ b/packages/cli/test/lib/init/ui/ink-app.snapshot.test.tsx @@ -275,10 +275,10 @@ describe("Ink App snapshot", () => { vi.useFakeTimers(); try { const initialFrame = await renderActiveTaskFrameAfter(1); - expect(initialFrame).toContain("▶ Detecting platform"); + expect(initialFrame).toContain("▶ Checking Sentry support"); const pulsedFrame = await renderActiveTaskFrameAfter(601); - expect(pulsedFrame).toContain("▷ Detecting platform"); + expect(pulsedFrame).toContain("▷ Checking Sentry support"); } finally { vi.useRealTimers(); } diff --git a/packages/cli/test/lib/init/wizard-runner.test.ts b/packages/cli/test/lib/init/wizard-runner.test.ts index e1a8e2869..9d34f52c4 100644 --- a/packages/cli/test/lib/init/wizard-runner.test.ts +++ b/packages/cli/test/lib/init/wizard-runner.test.ts @@ -1384,6 +1384,47 @@ describe("runWizard — resumeWithRetry stale-step recovery", () => { }); }); + test("sends the explicit agent-checkpoint acknowledgement", async () => { + const checkpointPayload: SuspendPayload = { + cwd: "/tmp/test", + detail: "Checking Sentry support for the detected project", + operation: "agent-checkpoint", + params: {}, + protocolVersion: 1, + requestId: "5f61cbd5-1051-4b52-928f-06eb78ba40ee", + type: "tool", + }; + mockStartResult = { + status: "suspended", + suspended: [["detect-platform"]], + steps: { + "detect-platform": { suspendPayload: checkpointPayload }, + }, + }; + executeToolSpy.mockResolvedValue({ + data: { acknowledged: true }, + ok: true, + }); + let capturedResume: Record | undefined; + makeStaleStepRun((args) => { + capturedResume = args.resumeData as Record; + return Promise.resolve({ status: "success", result: { exitCode: 0 } }); + }); + + await runWizard(makeOptions()); + + expect(executeToolSpy).toHaveBeenCalledWith( + expect.objectContaining({ operation: "agent-checkpoint" }), + expect.anything() + ); + expect(capturedResume).toMatchObject({ + data: { acknowledged: true }, + ok: true, + protocolVersion: 1, + requestId: checkpointPayload.requestId, + }); + }); + test("uses request identity instead of mutable payload details during recovery", async () => { vi.useFakeTimers(); const requestId = "8c7ee6b9-e955-4514-9164-f01844584a28";