Skip to content

Commit 5964215

Browse files
fix(tools): use resolved numeric IDs for issue detail requests
Keep event, external-link, and Seer lookups on the resolved numeric issue ID so legacy short-ID resolution cannot break a successful lookup. Co-Authored-By: David Cramer <david@sentry.io>
1 parent c3e7139 commit 5964215

5 files changed

Lines changed: 173 additions & 40 deletions

File tree

‎docs/contributing/api-patterns.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ await api.issues.update({
6363
});
6464
```
6565

66+
### Resolved Issue IDs
67+
68+
After resolving an issue, use `String(issue.id)` for follow-up API requests,
69+
including events, external links, and Seer analysis. Do not switch back to
70+
`issue.shortId`: short-ID resolution can fail for legacy mixed-case project
71+
slugs even when numeric-ID lookup succeeds. Keep short IDs for display.
72+
6673
### Multi-Region Support
6774

6875
Sentry uses region-specific URLs:
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
autofixStateFixture,
3+
eventsFixture,
4+
issueFixture,
5+
mswServer,
6+
} from "@sentry/mcp-server-mocks";
7+
import { http, HttpResponse } from "msw";
8+
import { describe, expect, it } from "vitest";
9+
import getIssueDetails from "./get-issue-details";
10+
import getSentryResource from "./get-sentry-resource";
11+
12+
const organizationSlug = "sentry-mcp-evals";
13+
const issueId = String(issueFixture.id);
14+
const eventId = eventsFixture.id;
15+
const issueUrl = `https://${organizationSlug}.sentry.io/issues/${issueId}/`;
16+
const context = {
17+
constraints: { organizationSlug: undefined },
18+
accessToken: "access-token",
19+
userId: "1",
20+
};
21+
22+
// Short IDs for legacy mixed-case project slugs can fail to resolve even though
23+
// the numeric ID works. Reject every short-ID follow-up, including optional data.
24+
describe("issue details with an unresolvable short ID", () => {
25+
it.each([
26+
{
27+
name: "numeric issue ID",
28+
call: () =>
29+
getIssueDetails.handler(
30+
{ organizationSlug, issueId, regionUrl: null },
31+
context,
32+
),
33+
},
34+
{
35+
name: "issue URL",
36+
call: () =>
37+
getIssueDetails.handler({ issueUrl, regionUrl: null }, context),
38+
},
39+
{
40+
name: "numeric issue ID and explicit event ID",
41+
call: () =>
42+
getIssueDetails.handler(
43+
{ organizationSlug, issueId, eventId, regionUrl: null },
44+
context,
45+
),
46+
},
47+
{
48+
name: "event ID resolved through issue search",
49+
call: () =>
50+
getIssueDetails.handler(
51+
{ organizationSlug, eventId, regionUrl: null },
52+
context,
53+
),
54+
},
55+
{
56+
name: "resource issue URL",
57+
call: () => getSentryResource.handler({ url: issueUrl }, context),
58+
},
59+
{
60+
name: "resource event URL",
61+
call: () =>
62+
getSentryResource.handler(
63+
{ url: `${issueUrl}events/${eventId}/` },
64+
context,
65+
),
66+
},
67+
])("loads $name and enrichment by numeric ID", async ({ call }) => {
68+
const requestedIssueIds: string[] = [];
69+
const base = `https://sentry.io/api/0/organizations/${organizationSlug}`;
70+
mswServer.use(
71+
http.get(`${base}/issues/`, () => HttpResponse.json([issueFixture])),
72+
http.get(`${base}/issues/:issueId/*`, ({ params, request }) => {
73+
const requestedId = String(params.issueId);
74+
requestedIssueIds.push(requestedId);
75+
if (requestedId !== issueId) {
76+
return HttpResponse.json(
77+
{ detail: "The requested resource does not exist" },
78+
{ status: 404 },
79+
);
80+
}
81+
const path = new URL(request.url).pathname;
82+
if (path.includes("/events/")) {
83+
return HttpResponse.json(eventsFixture);
84+
}
85+
if (path.endsWith("/external-issues/")) {
86+
return HttpResponse.json([
87+
{
88+
id: "123",
89+
issueId,
90+
serviceType: "github",
91+
displayName: "example/app#123",
92+
webUrl: "https://github.com/example/app/issues/123",
93+
},
94+
]);
95+
}
96+
if (path.endsWith("/autofix/")) {
97+
return HttpResponse.json(autofixStateFixture);
98+
}
99+
return HttpResponse.json(issueFixture);
100+
}),
101+
);
102+
103+
const result = await call();
104+
105+
expect(result).toContain(`Issue ${issueFixture.shortId}`);
106+
expect(result).toContain(eventsFixture.id);
107+
expect(result).toContain("## Seer Analysis");
108+
expect(result).toContain("**example/app#123** (github)");
109+
expect(requestedIssueIds.length).toBeGreaterThanOrEqual(3);
110+
expect(new Set(requestedIssueIds)).toEqual(new Set([issueId]));
111+
});
112+
});

0 commit comments

Comments
 (0)