From f7ae40b8f1c9b464f20ef5131eb30eeb691ed7e0 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:23:38 +0000 Subject: [PATCH] fix(cli): Pass trace command names instead of parsing usage hints #2085 made trace usage hints start with the running program, but commandNameFromUsageHint still stripped only a leading `sentry `. Under npx or sentry-mcp, ambiguous-project suggestions from log list, span list, span view, trace logs and trace view named the program twice. The trace resolvers now take the bare command name from each command, and commandNameFromUsageHint is gone. Refs #2085 Refs #2050 --- packages/cli/src/commands/log/list.ts | 1 + packages/cli/src/commands/span/list.ts | 1 + packages/cli/src/commands/span/view.ts | 1 + packages/cli/src/commands/trace/logs.ts | 1 + packages/cli/src/commands/trace/view.ts | 1 + packages/cli/src/lib/trace-target.ts | 36 ++++++------- packages/cli/test/commands/log/list.test.ts | 1 + packages/cli/test/lib/trace-target.test.ts | 58 ++++++++++++++++++++- 8 files changed, 78 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/commands/log/list.ts b/packages/cli/src/commands/log/list.ts index 6efb60c7c..be33afb80 100644 --- a/packages/cli/src/commands/log/list.ts +++ b/packages/cli/src/commands/log/list.ts @@ -212,6 +212,7 @@ async function resolveLogListTarget( const { traceId, org } = await resolveTraceOrg( parsed, context.cwd, + "log list", traceUsageHint(), ); return { diff --git a/packages/cli/src/commands/span/list.ts b/packages/cli/src/commands/span/list.ts index 136a393ba..14de6173b 100644 --- a/packages/cli/src/commands/span/list.ts +++ b/packages/cli/src/commands/span/list.ts @@ -253,6 +253,7 @@ async function resolveSpanListTarget( const resolved = await resolveTraceOrgProject( parsed, context.cwd, + "span list", traceUsageHint(), ); return { mode: "trace", ...resolved }; diff --git a/packages/cli/src/commands/span/view.ts b/packages/cli/src/commands/span/view.ts index c26967fa4..712621950 100644 --- a/packages/cli/src/commands/span/view.ts +++ b/packages/cli/src/commands/span/view.ts @@ -170,6 +170,7 @@ async function resolveSpanViewInput( const { traceId, org, project } = await resolveTraceOrgProject( traceTarget, context.cwd, + "span view", usageHint(), ); // Span IDs are unique only within a trace, so recovery scopes its fuzzy diff --git a/packages/cli/src/commands/trace/logs.ts b/packages/cli/src/commands/trace/logs.ts index cc6f1edbb..e3506cabe 100644 --- a/packages/cli/src/commands/trace/logs.ts +++ b/packages/cli/src/commands/trace/logs.ts @@ -132,6 +132,7 @@ async function resolveTraceLogsTarget( const { traceId, org } = await resolveTraceOrg( parsed, context.cwd, + "trace logs", usageHint(), ); return { diff --git a/packages/cli/src/commands/trace/view.ts b/packages/cli/src/commands/trace/view.ts index bba353485..d886b21d2 100644 --- a/packages/cli/src/commands/trace/view.ts +++ b/packages/cli/src/commands/trace/view.ts @@ -149,6 +149,7 @@ async function resolveTraceViewInput( const target = await resolveTraceOrgOptionalProject( parsed, context.cwd, + "trace view", usageHint(), ); return { diff --git a/packages/cli/src/lib/trace-target.ts b/packages/cli/src/lib/trace-target.ts index 79e845d16..652f7964f 100644 --- a/packages/cli/src/lib/trace-target.ts +++ b/packages/cli/src/lib/trace-target.ts @@ -44,8 +44,6 @@ const log = logger.withTag("trace-target"); /** Match `[]` in usageHint — captures bracket content + trailing placeholder */ const USAGE_TARGET_RE = /\[.*\]<[^>]+>/; -const SENTRY_COMMAND_PREFIX_RE = /^sentry\s+/; -const USAGE_ARGUMENT_START_RE = /\s+(?:\[|<)/; // --------------------------------------------------------------------------- // Types @@ -354,14 +352,6 @@ export function warnIfNormalized(parsed: ParsedTraceTarget, tag: string): void { // Resolution — org + project // --------------------------------------------------------------------------- -function commandNameFromUsageHint(usageHint: string): string { - const withoutPrefix = usageHint.replace(SENTRY_COMMAND_PREFIX_RE, ""); - const targetIndex = withoutPrefix.search(USAGE_ARGUMENT_START_RE); - return targetIndex === -1 - ? withoutPrefix - : withoutPrefix.slice(0, targetIndex); -} - /** * Resolve a parsed trace target to org + optional project. * @@ -374,6 +364,7 @@ function commandNameFromUsageHint(usageHint: string): string { export async function resolveTraceOrgOptionalProject( parsed: ParsedTraceTarget, cwd: string, + commandName: string, usageHint: string, ): Promise { if (parsed.type === "org-scoped") { @@ -384,12 +375,12 @@ export async function resolveTraceOrgOptionalProject( const resolved = await resolveOrgOptionalTarget( parsed, cwd, - commandNameFromUsageHint(usageHint), + commandName, usageHint, ); return { traceId: parsed.traceId, ...resolved }; } - return await resolveTraceOrgProject(parsed, cwd, usageHint); + return await resolveTraceOrgProject(parsed, cwd, commandName, usageHint); } /** @@ -404,6 +395,7 @@ export async function resolveTraceOrgOptionalProject( export async function resolveTraceOrgProject( parsed: ParsedTraceTarget, cwd: string, + commandName: string, usageHint: string, ): Promise { switch (parsed.type) { @@ -417,7 +409,12 @@ export async function resolveTraceOrgProject( case "project-search": // resolveProjectBoundTarget (called inside) sets telemetry context. - return await resolveProjectSearchTarget(parsed, cwd, usageHint); + return await resolveProjectSearchTarget( + parsed, + cwd, + commandName, + usageHint, + ); case "org-scoped": throw new ContextError("Specific project", usageHint, [ @@ -450,14 +447,12 @@ export async function resolveTraceOrgProject( async function resolveProjectSearchTarget( parsed: Extract, cwd: string, + commandName: string, usageHint: string, ): Promise { - const target = await resolveProjectBoundTarget( - parsed, - cwd, - commandNameFromUsageHint(usageHint), - { usageHint }, - ); + const target = await resolveProjectBoundTarget(parsed, cwd, commandName, { + usageHint, + }); return { traceId: parsed.traceId, org: target.org, @@ -480,6 +475,7 @@ async function resolveProjectSearchTarget( export async function resolveTraceOrg( parsed: ParsedTraceTarget, cwd: string, + commandName: string, usageHint: string, ): Promise { switch (parsed.type) { @@ -495,7 +491,7 @@ export async function resolveTraceOrg( const org = await resolveOrgOnlyTarget( parsed, cwd, - commandNameFromUsageHint(usageHint), + commandName, usageHint, ); return { traceId: parsed.traceId, org }; diff --git a/packages/cli/test/commands/log/list.test.ts b/packages/cli/test/commands/log/list.test.ts index 5748b6cb2..693948881 100644 --- a/packages/cli/test/commands/log/list.test.ts +++ b/packages/cli/test/commands/log/list.test.ts @@ -734,6 +734,7 @@ describe("listCommand.func — trace mode", () => { traceId: TRACE_ID, }), "/tmp", + "log list", expect.any(String), ); }); diff --git a/packages/cli/test/lib/trace-target.test.ts b/packages/cli/test/lib/trace-target.test.ts index 2ac3d92da..37694d042 100644 --- a/packages/cli/test/lib/trace-target.test.ts +++ b/packages/cli/test/lib/trace-target.test.ts @@ -30,6 +30,7 @@ vi.mock("../../src/lib/region.js", async (importOriginal) => { // oxlint-disable-next-line sentry-cli/no-namespace-import -- needed for spyOn mocking import * as apiClient from "../../src/lib/api-client.js"; +import { getAppIdentity, setAppIdentity } from "../../src/lib/app-identity.js"; import { ContextError, ResolutionError, @@ -49,6 +50,7 @@ import { } from "../../src/lib/trace-target.js"; const VALID_TRACE_ID = "aaaa1111bbbb2222cccc3333dddd4444"; +const defaultIdentity = getAppIdentity(); /** The target {@link parseTraceTargetInput} parses `args` into. */ function parsedTraceTarget( @@ -216,6 +218,7 @@ describe("trace target resolution", () => { const resolved = await resolveTraceOrg( parsed, "/tmp", + "trace logs", "sentry trace logs [/[/]]", ); @@ -231,7 +234,7 @@ describe("trace target resolution", () => { const usageHint = "sentry span view [//]"; try { - await resolveTraceOrgProject(parsed, "/tmp", usageHint); + await resolveTraceOrgProject(parsed, "/tmp", "span view", usageHint); expect.unreachable("should have thrown"); } catch (error) { expect(error).toBeInstanceOf(ResolutionError); @@ -248,13 +251,64 @@ describe("trace target resolution", () => { const usageHint = "sentry trace view [//]"; try { - await resolveTraceOrgOptionalProject(parsed, "/tmp", usageHint); + await resolveTraceOrgOptionalProject( + parsed, + "/tmp", + "trace view", + usageHint, + ); expect.unreachable("should have thrown"); } catch (error) { expect(error).toBeInstanceOf(ResolutionError); expect((error as ResolutionError).hint).toBe(usageHint); } }); + + describe("under another program name", () => { + beforeEach(() => { + setAppIdentity({ name: "sentry-mcp", packageName: "@sentry/mcp-server" }); + findProjectsBySlugSpy.mockResolvedValue({ + projects: [ + { id: "1", slug: "frontend", name: "Frontend", orgSlug: "org-a" }, + { id: "2", slug: "frontend", name: "Frontend", orgSlug: "org-b" }, + ], + orgs: [ + { slug: "org-a", name: "Org A" }, + { slug: "org-b", name: "Org B" }, + ], + }); + }); + + afterEach(() => { + setAppIdentity(defaultIdentity); + }); + + test.each([ + ["log list", resolveTraceOrg], + ["span list", resolveTraceOrgProject], + ["trace view", resolveTraceOrgOptionalProject], + ])( + "an ambiguous project names the program once in %s suggestions", + async (commandName, resolve) => { + const parsed = parsedTraceTarget( + ["frontend", VALID_TRACE_ID], + "sentry trace view ", + ); + + const error = await resolve( + parsed, + "/tmp", + commandName, + `sentry-mcp ${commandName} [//]`, + ).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(ResolutionError); + const message = (error as ResolutionError).message; + expect(message).toContain(`sentry-mcp ${commandName} org-a/frontend`); + expect(message).not.toContain("sentry-mcp sentry-mcp"); + }, + ); + }); }); describe("parseTraceTargetInput", () => {