Skip to content

Commit ece5bcd

Browse files
maxl0rdbkendall
authored andcommitted
(feat) Crashlytics tools improvements (#9138)
- Improving instructions and return values in crashlytics tools - Making interval handling more robust - Adding return messages to avoid undefined tool responses
1 parent 05c53e8 commit ece5bcd

File tree

10 files changed

+137
-22
lines changed

10 files changed

+137
-22
lines changed

src/crashlytics/events.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as chai from "chai";
22
import * as nock from "nock";
33
import * as chaiAsPromised from "chai-as-promised";
44

5-
import { listEvents } from "./events";
5+
import { listEvents, batchGetEvents } from "./events";
66
import { FirebaseError } from "../error";
77
import { crashlyticsApiOrigin } from "../api";
88

@@ -82,4 +82,54 @@ describe("events", () => {
8282
).to.be.rejectedWith(FirebaseError, "Unable to get the projectId from the AppId.");
8383
});
8484
});
85+
86+
describe("batchGetEvents", () => {
87+
const eventNames = [
88+
"projects/1234567890/apps/1:1234567890:android:abcdef1234567890/events/test_event_id_1",
89+
"projects/1234567890/apps/1:1234567890:android:abcdef1234567890/events/test_event_id_2",
90+
];
91+
92+
it("should resolve with the response body on success", async () => {
93+
const mockResponse = {
94+
events: [{ id: "test_event_id_1" }, { id: "test_event_id_2" }],
95+
};
96+
97+
nock(crashlyticsApiOrigin())
98+
.get(`/v1alpha/projects/${requestProjectNumber}/apps/${appId}/events:batchGet`)
99+
.query({
100+
"event.names": eventNames,
101+
})
102+
.reply(200, mockResponse);
103+
104+
const result = await batchGetEvents(appId, eventNames);
105+
106+
expect(result).to.deep.equal(mockResponse);
107+
expect(nock.isDone()).to.be.true;
108+
});
109+
110+
it("should throw a FirebaseError if the API call fails", async () => {
111+
nock(crashlyticsApiOrigin())
112+
.get(`/v1alpha/projects/${requestProjectNumber}/apps/${appId}/events:batchGet`)
113+
.query({
114+
"event.names": eventNames,
115+
})
116+
.reply(500, { error: "Internal Server Error" });
117+
118+
await expect(batchGetEvents(appId, eventNames)).to.be.rejectedWith(
119+
FirebaseError,
120+
`Failed to batch get events for app_id ${appId}.`,
121+
);
122+
});
123+
124+
it("should throw a FirebaseError if there are too many events", async () => {
125+
const tooManyEventNames = Array.from(Array(101).keys()).map(
126+
(i) =>
127+
`projects/1234567890/apps/1:1234567890:android:abcdef1234567890/events/test_event_id_${i}`,
128+
);
129+
await expect(batchGetEvents(appId, tooManyEventNames)).to.be.rejectedWith(
130+
FirebaseError,
131+
"Too many events in batchGet request",
132+
);
133+
});
134+
});
85135
});

src/crashlytics/events.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { logger } from "../logger";
22
import { FirebaseError, getError } from "../error";
33
import { CRASHLYTICS_API_CLIENT, parseProjectNumber, TIMEOUT } from "./utils";
4-
import { ListEventsResponse } from "./types";
4+
import { BatchGetEventsResponse, ListEventsResponse } from "./types";
55
import { EventFilter, filterToUrlSearchParams } from "./filters";
66

77
/**
@@ -43,3 +43,44 @@ export async function listEvents(
4343
});
4444
}
4545
}
46+
47+
/**
48+
* Get multiple events by resource name.
49+
* Can be used with the `sampleEvent` resource included in topIssues reports.
50+
* @param appId Firebase app_id
51+
* @param eventNames the resource names for the desired events.
52+
* Format: "projects/{project}/apps/{app_id}/events/{event_id}"
53+
* @return A BatchGetEventsResponse including an array of Event.
54+
*/
55+
export async function batchGetEvents(
56+
appId: string,
57+
eventNames: string[],
58+
): Promise<BatchGetEventsResponse> {
59+
const requestProjectNumber = parseProjectNumber(appId);
60+
if (eventNames.length > 100) throw new FirebaseError("Too many events in batchGet request");
61+
logger.debug(
62+
`[crashlytics] batchGetEvents called with appId: ${appId}, eventNames: ${eventNames.join(", ")}`,
63+
);
64+
const queryParams = new URLSearchParams();
65+
eventNames.forEach((en) => {
66+
queryParams.append("event.names", en);
67+
});
68+
69+
try {
70+
const response = await CRASHLYTICS_API_CLIENT.request<void, BatchGetEventsResponse>({
71+
method: "GET",
72+
headers: {
73+
"Content-Type": "application/json",
74+
},
75+
path: `/projects/${requestProjectNumber}/apps/${appId}/events:batchGet`,
76+
queryParams: queryParams,
77+
timeout: TIMEOUT,
78+
});
79+
80+
return response.body;
81+
} catch (err: unknown) {
82+
throw new FirebaseError(`Failed to batch get events for app_id ${appId}.`, {
83+
original: getError(err),
84+
});
85+
}
86+
}

src/crashlytics/filters.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ export const IssueIdSchema = z.string().describe("Crashlytics issue id, as hexid
1414

1515
export const EventFilterSchema = z
1616
.object({
17-
intervalStartTime: z.string().optional().describe(`A timestamp in ISO 8601 string format`),
18-
intervalEndTime: z.string().optional().describe(`A timestamp in ISO 8601 string format.`),
17+
intervalStartTime: z
18+
.string()
19+
.optional()
20+
.describe(`A timestamp in ISO 8601 string format. Defaults to 7 days ago.`),
21+
intervalEndTime: z
22+
.string()
23+
.optional()
24+
.describe(`A timestamp in ISO 8601 string format. Defaults to now.`),
1925
versionDisplayNames: z
2026
.array(z.string())
2127
.optional()

src/crashlytics/issues.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe("issues", () => {
6868

6969
nock(crashlyticsApiOrigin())
7070
.patch(`/v1alpha/projects/${requestProjectNumber}/apps/${appId}/issues/${issueId}`, {
71-
issue: { state: state },
71+
state,
7272
})
7373
.query({ updateMask: "state" })
7474
.reply(200, mockResponse);

src/crashlytics/issues.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export async function updateIssue(appId: string, issueId: string, state: State):
5151
},
5252
path: `/projects/${requestProjectNumber}/apps/${appId}/issues/${issueId}`,
5353
queryParams: { updateMask: "state" },
54-
body: { issue: { state } },
54+
body: { state },
5555
timeout: TIMEOUT,
5656
});
5757

src/crashlytics/notes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export async function createNote(appId: string, issueId: string, note: string):
4444
* @param issueId Crashlytics issue id
4545
* @param noteId Crashlytics note id
4646
*/
47-
export async function deleteNote(appId: string, issueId: string, noteId: string): Promise<void> {
47+
export async function deleteNote(appId: string, issueId: string, noteId: string): Promise<string> {
4848
const requestProjectNumber = parseProjectNumber(appId);
4949

5050
logger.debug(
@@ -56,6 +56,7 @@ export async function deleteNote(appId: string, issueId: string, noteId: string)
5656
path: `/projects/${requestProjectNumber}/apps/${appId}/issues/${issueId}/notes/${noteId}`,
5757
timeout: TIMEOUT,
5858
});
59+
return `Deleted note ${noteId}`;
5960
} catch (err: unknown) {
6061
throw new FirebaseError(
6162
`Failed to delete note ${noteId} from issue ${issueId} for app ${appId}`,

src/crashlytics/types.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -458,16 +458,12 @@ export enum ThreadState {
458458

459459
/** Request message for the ListEvents method. */
460460
export interface ListEventsRequest {
461-
/** The Firebase application. Formatted like "projects/{project}/apps/{app_id}" */
462-
parent: string;
463461
/** The maximum number of events per page. If omitted, defaults to 10. */
464462
pageSize?: number;
465463
/** A page token, received from a previous calls. */
466464
pageToken?: string;
467465
/** Filter only the desired events. */
468466
filter?: EventFilters;
469-
/** The list of Event fields to include in the response. If omitted, the full event is returned. */
470-
readMask?: string;
471467
}
472468

473469
/** Response message for the ListEvents method. */
@@ -478,6 +474,22 @@ export interface ListEventsResponse {
478474
nextPageToken?: string;
479475
}
480476

477+
/** Request message for the BatchGetEvents method. */
478+
export interface BatchGetEventsRequest {
479+
/**
480+
* The resource names of the desired events.
481+
* A maximum of 100 events can be retrieved in a batch.
482+
* Format: "projects/{project}/apps/{app_id}/events/{event_id}"
483+
*/
484+
names: string[];
485+
}
486+
487+
/** Response message for the BatchGetEvents method. */
488+
export interface BatchGetEventsResponse {
489+
/** Returns one or more events. */
490+
events: Event[];
491+
}
492+
481493
/**
482494
* Filters for ListEvents method.
483495
* Multiple conditions for the same field are combined in an ‘OR’ expr
@@ -590,8 +602,6 @@ export interface DeviceFilter {
590602

591603
/** The request method for the GetReport method. */
592604
export interface GetReportRequest {
593-
/** The report name. Formatted like "projects/{project}/apps/{app_id}/reports/{report}". */
594-
name: string;
595605
/** Filters to customize the report. */
596606
filter?: ReportFilters;
597607
/** The maximum number of result groups to return. If omitted, defaults to 25. */
@@ -637,8 +647,6 @@ export interface ReportFilters {
637647

638648
/** Request message for the UpdateIssue method. */
639649
export interface UpdateIssueRequest {
640-
/** The issue to update. */
641-
issue: Issue;
642-
/** The list of Issue fields to update. Currently only "state" is mutable. */
643-
updateMask?: string;
650+
/** Only the "state" field is mutable. */
651+
state: State;
644652
}

src/mcp/prompts/crashlytics/connect.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ they would like to perform. Here are some possibilities and instructions follow
5353
5454
Follow these steps to fetch issues and prioritize them.
5555
56-
1. Use the 'crashlytics_list_top_issues' tool to fetch up to 20 issues.
57-
2. Use the 'crashlytics_list_top_versions' tool to fetch the top versions for this app.
56+
1. Use the 'crashlytics_get_top_issues' tool to fetch up to 20 issues.
57+
1a. Analyze the user's query and apply the appropriate filters.
58+
1b. If the user asks for crashes, then set the issueErrorType filter to *FATAL*.
59+
1c. If the user asks about a particular time range, then set both the intervalStartTime and intervalEndTime.
60+
2. Use the 'crashlytics_get_top_versions' tool to fetch the top versions for this app.
5861
3. If the user instructions include statements about prioritization, use those instructions.
5962
4. If the user instructions do not include statements about prioritization,
6063
then prioritize the returned issues using the following criteria:
@@ -73,8 +76,9 @@ Follow these steps to fetch issues and prioritize them.
7376
Follow these steps to diagnose and fix issues.
7477
7578
1. Make sure you have a good understanding of the code structure and where different functionality exists
76-
2. Use the 'crashlytics_get_issue_details' tool to get more context on the issue.
77-
3. Use the 'crashlytics_get_sample_crash_for_issue' tool to get 3 example crashes for this issue.
79+
2. Use the 'crashlytics_get_issue' tool to get more context on the issue.
80+
3. Use the 'crashlytics_list_events' tool to get an example crash for this issue.
81+
3a. Apply the same filtering criteria that you used to find the issue, so that you find an appropriate event.
7882
4. Read the files that exist in the stack trace of the issue to understand the crash deeply.
7983
5. Determine the root cause of the crash.
8084
6. Write out a plan using the following criteria:

src/mcp/tools/crashlytics/notes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export const delete_note = tool(
6666
}),
6767
annotations: {
6868
title: "Delete Crashlytics Issue Note",
69-
readOnlyHint: true,
69+
readOnlyHint: false,
70+
destructiveHint: true,
7071
},
7172
_meta: {
7273
requiresAuth: true,

src/mcp/tools/crashlytics/reports.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ function getReportContent(
1616
return async ({ appId, filter, pageSize }) => {
1717
if (!appId) return mcpError(`Must specify 'appId' parameter.`);
1818
filter ??= {};
19+
if (!!filter.intervalStartTime && !filter.intervalEndTime) {
20+
// interval.end_time is required if interval.start_time is set but the agent likes to forget it
21+
filter.intervalEndTime = new Date().toISOString();
22+
}
1923
return toContent(await getReport(report, appId, filter, pageSize));
2024
};
2125
}

0 commit comments

Comments
 (0)