Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/cli/src/commands/log/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ async function resolveLogListTarget(
const { traceId, org } = await resolveTraceOrg(
parsed,
context.cwd,
"log list",
traceUsageHint(),
);
return {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/span/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ async function resolveSpanListTarget(
const resolved = await resolveTraceOrgProject(
parsed,
context.cwd,
"span list",
traceUsageHint(),
);
return { mode: "trace", ...resolved };
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/span/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/trace/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ async function resolveTraceLogsTarget(
const { traceId, org } = await resolveTraceOrg(
parsed,
context.cwd,
"trace logs",
usageHint(),
);
return {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/trace/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ async function resolveTraceViewInput(
const target = await resolveTraceOrgOptionalProject(
parsed,
context.cwd,
"trace view",
usageHint(),
);
return {
Expand Down
36 changes: 16 additions & 20 deletions packages/cli/src/lib/trace-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ const log = logger.withTag("trace-target");

/** Match `[<prefix>]<trail>` 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
Expand Down Expand Up @@ -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.
*
Expand All @@ -374,6 +364,7 @@ function commandNameFromUsageHint(usageHint: string): string {
export async function resolveTraceOrgOptionalProject(
parsed: ParsedTraceTarget,
cwd: string,
commandName: string,
usageHint: string,
): Promise<ResolvedTraceOrgProject | ResolvedTraceOrg> {
if (parsed.type === "org-scoped") {
Expand All @@ -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);
}

/**
Expand All @@ -404,6 +395,7 @@ export async function resolveTraceOrgOptionalProject(
export async function resolveTraceOrgProject(
parsed: ParsedTraceTarget,
cwd: string,
commandName: string,
usageHint: string,
): Promise<ResolvedTraceOrgProject> {
switch (parsed.type) {
Expand All @@ -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, [
Expand Down Expand Up @@ -450,14 +447,12 @@ export async function resolveTraceOrgProject(
async function resolveProjectSearchTarget(
parsed: Extract<ParsedTraceTarget, { type: "project-search" }>,
cwd: string,
commandName: string,
usageHint: string,
): Promise<ResolvedTraceOrgProject> {
const target = await resolveProjectBoundTarget(
parsed,
cwd,
commandNameFromUsageHint(usageHint),
{ usageHint },
);
const target = await resolveProjectBoundTarget(parsed, cwd, commandName, {
usageHint,
});
return {
traceId: parsed.traceId,
org: target.org,
Expand All @@ -480,6 +475,7 @@ async function resolveProjectSearchTarget(
export async function resolveTraceOrg(
parsed: ParsedTraceTarget,
cwd: string,
commandName: string,
usageHint: string,
): Promise<ResolvedTraceOrg> {
switch (parsed.type) {
Expand All @@ -495,7 +491,7 @@ export async function resolveTraceOrg(
const org = await resolveOrgOnlyTarget(
parsed,
cwd,
commandNameFromUsageHint(usageHint),
commandName,
usageHint,
);
return { traceId: parsed.traceId, org };
Expand Down
1 change: 1 addition & 0 deletions packages/cli/test/commands/log/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ describe("listCommand.func — trace mode", () => {
traceId: TRACE_ID,
}),
"/tmp",
"log list",
expect.any(String),
);
});
Expand Down
58 changes: 56 additions & 2 deletions packages/cli/test/lib/trace-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -216,6 +218,7 @@ describe("trace target resolution", () => {
const resolved = await resolveTraceOrg(
parsed,
"/tmp",
"trace logs",
"sentry trace logs [<org>/[<project>/]]<trace-id>",
);

Expand All @@ -231,7 +234,7 @@ describe("trace target resolution", () => {
const usageHint = "sentry span view [<org>/<project>/]<span-id>";

try {
await resolveTraceOrgProject(parsed, "/tmp", usageHint);
await resolveTraceOrgProject(parsed, "/tmp", "span view", usageHint);
expect.unreachable("should have thrown");
} catch (error) {
expect(error).toBeInstanceOf(ResolutionError);
Expand All @@ -248,13 +251,64 @@ describe("trace target resolution", () => {
const usageHint = "sentry trace view [<org>/<project>/]<trace-id>";

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 <target> <trace-id>",
);

const error = await resolve(
parsed,
"/tmp",
commandName,
`sentry-mcp ${commandName} [<org>/<project>/]<trace-id>`,
).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", () => {
Expand Down
Loading