Skip to content

Commit 053880e

Browse files
fix(api-client): allow null firstSeen/lastSeen on IssueSchema (#910)
## Summary Sentry's API can return `null` for `firstSeen` / `lastSeen` on issues (e.g. when events have aged out), which currently causes `IssueSchema.parse()` in `SentryApiService.getIssue()` to throw a `ZodError: Failed to validate keys: lastSeen` and breaks `get_issue_details` for those issues. Changes: - Relax `IssueSchema.firstSeen` and `IssueSchema.lastSeen` to `z.string().datetime().nullable()`. - Update `formatIssueOutput` in `internal/formatting.ts` to omit the "First Seen" / "Last Seen" lines when the values are null (previously would have emitted `Invalid Date`). The `search-issues` formatter already handles null via its `formatDate` helper, so no change needed there. - Add a regression test for nullable `firstSeen` / `lastSeen`. Fixes MCP-SERVER-EWN (1136 events / 154 users affected since 2025-11-23). ## Review & Testing Checklist for Human - [ ] Confirm that returning `null` for `firstSeen`/`lastSeen` in the issue response is acceptable downstream (i.e. no other consumer of `IssueSchema` assumes these are always strings). I grepped for `issue.firstSeen` / `issue.lastSeen` and only found the two formatters; please sanity-check. - [ ] Spot-check `get_issue_details` output formatting by pointing at a real issue where `lastSeen` is null (e.g. one of the events under MCP-SERVER-EWN). ### Notes The fix is intentionally minimal — same pattern already used by `IssueTagValueSchema` for its own `firstSeen`/`lastSeen` fields. Link to Devin session: https://app.devin.ai/sessions/95257bed9fb74714955a56f9a5d7b606 Requested by: @dcramer Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Devin <devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: David Cramer <dcramer@gmail.com>
1 parent 7b0b2de commit 053880e

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

‎packages/mcp-core/src/api-client/schema.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,34 @@ describe("IssueSchema", () => {
210210
name: "Backend Team",
211211
});
212212
});
213+
214+
it("should handle issues with null firstSeen and lastSeen", () => {
215+
// Sentry's API can return null for firstSeen/lastSeen in some cases
216+
// (e.g. issues with no events). Regression test for MCP-SERVER-EWN.
217+
const issue = {
218+
id: "777",
219+
shortId: "TEST-77",
220+
title: "Test Issue",
221+
firstSeen: null,
222+
lastSeen: null,
223+
count: 1,
224+
userCount: 1,
225+
permalink: "https://sentry.io/issues/777/",
226+
project: {
227+
id: "5",
228+
name: "test",
229+
slug: "test",
230+
platform: "node",
231+
},
232+
status: "unresolved",
233+
culprit: "test.js",
234+
type: "error",
235+
};
236+
237+
const result = IssueSchema.parse(issue);
238+
expect(result.firstSeen).toBeNull();
239+
expect(result.lastSeen).toBeNull();
240+
});
213241
});
214242

215243
describe("EventSchema", () => {

‎packages/mcp-core/src/api-client/schema.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,8 @@ export const IssueSchema = z
306306
id: z.union([z.string(), z.number()]),
307307
shortId: z.string(),
308308
title: z.string(),
309-
firstSeen: z.string().datetime(),
310-
lastSeen: z.string().datetime(),
309+
firstSeen: z.string().datetime().nullable(),
310+
lastSeen: z.string().datetime().nullable(),
311311
count: z.union([z.string(), z.number()]),
312312
userCount: z.union([z.string(), z.number()]),
313313
permalink: z.string().url(),

‎packages/mcp-core/src/internal/formatting.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,8 +1787,12 @@ export function formatIssueOutput({
17871787
output += `**Culprit**: ${issue.culprit}\n`;
17881788
}
17891789

1790-
output += `**First Seen**: ${new Date(issue.firstSeen).toISOString()}\n`;
1791-
output += `**Last Seen**: ${new Date(issue.lastSeen).toISOString()}\n`;
1790+
if (issue.firstSeen) {
1791+
output += `**First Seen**: ${new Date(issue.firstSeen).toISOString()}\n`;
1792+
}
1793+
if (issue.lastSeen) {
1794+
output += `**Last Seen**: ${new Date(issue.lastSeen).toISOString()}\n`;
1795+
}
17921796
output += `**Occurrences**: ${issue.count}\n`;
17931797
output += `**Users Impacted**: ${issue.userCount}\n`;
17941798
output += `**Status**: ${issue.status}\n`;

0 commit comments

Comments
 (0)