diff --git a/CHANGELOG.md b/CHANGELOG.md index c02f6396..e33a9a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [Zephyr] Added a tool `update-test-steps` for updating test execution test steps [#386](https://github.com/SmartBear/smartbear-mcp/pull/386) +- [BugSnag] Added "Get Events on an Error" tool for listing the events that have grouped into a specified error [#406](https://github.com/SmartBear/smartbear-mcp/pull/406) ### Changed diff --git a/docs/products/SmartBear MCP Server/bugsnag-integration.md b/docs/products/SmartBear MCP Server/bugsnag-integration.md index 3d7f2042..d893b33c 100644 --- a/docs/products/SmartBear MCP Server/bugsnag-integration.md +++ b/docs/products/SmartBear MCP Server/bugsnag-integration.md @@ -25,6 +25,11 @@ If you wish to interact with only one BugSnag project, we also recommend setting - Link to the error on the dashboard. - This tool also takes filter parameters in the same format as List Errors to specialize the results returned in the summaries/pivots. +### Get Events on an Error + +- Retrieves a list of events (occurrences) for a specified error, with complete details for each event. +- This tool also takes filter parameters in the same format as List Errors to filter the events returned. + ### Get Event Details - Retrieve event (occurrence) details of a specific event by ID. diff --git a/src/bugsnag/client.ts b/src/bugsnag/client.ts index 0ef48055..27cf1dd7 100644 --- a/src/bugsnag/client.ts +++ b/src/bugsnag/client.ts @@ -27,6 +27,7 @@ import { ListProjectErrors } from "./tool/error/list-project-errors"; import { UpdateError } from "./tool/error/update-error"; import { GetEvent } from "./tool/event/get-event"; import { GetEventDetailsFromDashboardUrl } from "./tool/event/get-event-details-from-dashboard-url"; +import { ListErrorEvents } from "./tool/event/list-error-events"; import { GetNetworkEndpointGroupings } from "./tool/performance/get-network-endpoint-groupings"; import { GetSpanGroup } from "./tool/performance/get-span-group"; import { GetTrace } from "./tool/performance/get-trace"; @@ -376,6 +377,7 @@ export class BugsnagClient implements Client { new UpdateError(this, getInput), new GetEvent(this), new GetEventDetailsFromDashboardUrl(this), + new ListErrorEvents(this), new ListReleases(this), new GetRelease(this), new GetBuild(this), diff --git a/src/bugsnag/client/api/Error.ts b/src/bugsnag/client/api/Error.ts index 82e8f52e..260cbb1d 100644 --- a/src/bugsnag/client/api/Error.ts +++ b/src/bugsnag/client/api/Error.ts @@ -152,6 +152,61 @@ export class ErrorAPI extends BaseAPI { ); } + /** + * List the Events on an Error + * GET /projects/{project_id}/errors/{error_id}/events + */ + async listErrorEvents( + projectId: string, + errorId: string, + base?: Date | null, + sort?: string, + direction?: string, + perPage?: number, + filters?: FilterObject, + nextUrl?: string, + ): Promise> { + if (nextUrl) { + // Don't allow override of these params when using nextUrl + direction = undefined; + sort = undefined; + base = undefined; + } + const localVarFetchArgs = ErrorsApiFetchParamCreator( + this.configuration, + ).listEventsOnError( + projectId, + errorId, + base ?? undefined, + sort, + direction, + undefined, + undefined, // Filters are encoded separately below + undefined, + undefined, + ); + + const url = new URL( + nextUrl ?? localVarFetchArgs.url, + this.configuration.basePath, + ); + if (perPage) { + // Allow override of per page, even with nextUrl + url.searchParams.set("per_page", perPage.toString()); + } + if (!nextUrl && filters) { + // Apply our own encoding of filters + toUrlSearchParams(filters).forEach((value, key) => { + url.searchParams.append(key, value); + }); + } + return await this.requestArray( + url.toString(), + localVarFetchArgs.options, + false, // Paginate results + ); + } + /** * Update an Error on a Project * PATCH /projects/{project_id}/errors/{error_id} diff --git a/src/bugsnag/tool/event/list-error-events.ts b/src/bugsnag/tool/event/list-error-events.ts new file mode 100644 index 00000000..830950e7 --- /dev/null +++ b/src/bugsnag/tool/event/list-error-events.ts @@ -0,0 +1,69 @@ +import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ZodRawShape } from "zod"; +import { z } from "zod"; +import { Tool } from "../../../common/tools"; +import type { ToolParams } from "../../../common/types"; +import type { BugsnagClient } from "../../client"; +import { toolInputParameters } from "../../input-schemas"; + +const inputSchema = z.object({ + projectId: toolInputParameters.projectId, + errorId: toolInputParameters.errorId, + filters: toolInputParameters.filters.describe( + "Apply filters to narrow down the event list. Use the List Project Event Filters tool to discover available filter fields. " + + "Time filters support extended ISO 8601 format (e.g. 2018-05-20T00:00:00Z) or relative format (e.g. 7d, 24h).", + ), + direction: toolInputParameters.direction, + perPage: toolInputParameters.perPage, + nextUrl: toolInputParameters.nextUrl, +}); + +// Fetches full details for a single event by its ID, including stack trace and metadata. +export class ListErrorEvents extends Tool { + specification: ToolParams = { + title: "Get Events on an Error", + summary: "Gets a list of events that have grouped into the specified error", + purpose: + "Show the events that make up an error to see individual occurrences of the error for detailed analysis", + useCases: [ + "Retrieving all the events for comparison to find commonalities or differences in stack traces, breadcrumbs and metadata", + ], + inputSchema, + examples: [ + { + description: "Get events of an error", + parameters: { + projectId: "1234567890abcdef12345678", + errorId: "6863e2af012caf1d5c320000", + }, + expectedOutput: + "A list of events, ordered by timestamp, with complete details including stack trace, breadcrumbs, metadata, and context", + }, + ], + }; + + handle: ToolCallback = async (args, _extra) => { + const params = inputSchema.parse(args); + const project = await this.client.getInputProject(params.projectId); + const response = await this.client.errorsApi.listErrorEvents( + project.id, + params.errorId, + undefined, // base + "timestamp", // sort (the only available option) + params.direction, + params.perPage, + params.filters, + params.nextUrl, + ); + + const result = { + data: response.body, + next_url: response.nextUrl ?? undefined, + data_count: response.body?.length, + total_count: response.totalCount ?? undefined, + }; + return { + content: [{ type: "text", text: JSON.stringify(result) }], + }; + }; +} diff --git a/src/tests/unit/bugsnag/client.test.ts b/src/tests/unit/bugsnag/client.test.ts index e2eec5af..6e72368b 100644 --- a/src/tests/unit/bugsnag/client.test.ts +++ b/src/tests/unit/bugsnag/client.test.ts @@ -40,6 +40,7 @@ const mockErrorAPI = { listEventsOnProject: vi.fn(), viewEventById: vi.fn(), listProjectErrors: vi.fn(), + listErrorEvents: vi.fn(), updateErrorOnProject: vi.fn(), getPivotValuesOnAnError: vi.fn(), } satisfies Omit; @@ -729,6 +730,7 @@ describe("BugsnagClient", () => { expect(registeredTools).toContain("Get Event"); expect(registeredTools).toContain("Get Event Details From Dashboard URL"); expect(registeredTools).toContain("List Project Errors"); + expect(registeredTools).toContain("Get Events on an Error"); expect(registeredTools).toContain("List Project Event Filters"); expect(registeredTools).toContain("Update Error"); expect(registeredTools).toContain("Get Build"); @@ -741,7 +743,7 @@ describe("BugsnagClient", () => { expect(registeredTools).toContain("List Trace Fields"); expect(registeredTools).toContain("Get Network Endpoint Groupings"); expect(registeredTools).toContain("Set Network Endpoint Groupings"); - expect(registeredTools.length).toBe(18); + expect(registeredTools.length).toBe(19); }); }); @@ -1271,6 +1273,55 @@ describe("BugsnagClient", () => { }); }); + describe("Get Events on an Error tool handler", () => { + const mockProject = getMockProject("proj-1", "Project 1"); + + it("should list error events with supplied parameters", async () => { + const mockEvents: EventApiView[] = [ + getMockEvent("event-1"), + getMockEvent("event-2"), + ]; + const filters = { + "event.since": [{ type: "eq" as const, value: "7d" }], + }; + + mockCache.get.mockReturnValueOnce(mockProject); // current project + mockErrorAPI.listErrorEvents.mockResolvedValue({ + body: mockEvents, + totalCount: 2, + }); + + client.registerTools(registerToolsSpy, getInputFunctionSpy); + const toolHandler = registerToolsSpy.mock.calls.find( + (call: any) => call[0].title === "Get Events on an Error", + )[1]; + + const result = await toolHandler({ + errorId: "error-1", + filters, + direction: "desc", + perPage: 50, + }); + + expect(mockErrorAPI.listErrorEvents).toHaveBeenCalledWith( + "proj-1", + "error-1", + undefined, + "timestamp", + "desc", + 50, + filters, + undefined, + ); + const expectedResult = { + data: mockEvents, + data_count: 2, + total_count: 2, + }; + expect(result.content[0].text).toBe(JSON.stringify(expectedResult)); + }); + }); + describe("List Project Event Filters tool handler", () => { const mockProject = getMockProject("proj-1", "Project 1");