Skip to content
Open
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ lib/
dev/
clean/
.gemini/
.env
1 change: 1 addition & 0 deletions src/appUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
const adminAndWebApps = (
await Promise.all(packageJsonFiles.map((p) => packageJsonToAdminOrWebApp(dirPath, p)))
).flat();

const flutterAppPromises = await Promise.all(
pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f)),
);
Expand Down Expand Up @@ -166,7 +167,7 @@
return !relativePath.startsWith(`..`);
}

export function getAllDepsFromPackageJson(packageJson: PackageJSON) {

Check warning on line 170 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment

Check warning on line 170 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
const devDependencies = Object.keys(packageJson.devDependencies ?? {});
const dependencies = Object.keys(packageJson.dependencies ?? {});
const allDeps = Array.from(new Set([...devDependencies, ...dependencies]));
Expand Down Expand Up @@ -283,8 +284,8 @@
export function extractAppIdentifierIos(fileContent: string): AppIdentifier[] {
const appIdRegex = /<key>GOOGLE_APP_ID<\/key>\s*<string>([^<]*)<\/string>/;
const bundleIdRegex = /<key>BUNDLE_ID<\/key>\s*<string>([^<]*)<\/string>/;
const appIdMatch = fileContent.match(appIdRegex);

Check warning on line 287 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Use the `RegExp#exec()` method instead
const bundleIdMatch = fileContent.match(bundleIdRegex);

Check warning on line 288 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Use the `RegExp#exec()` method instead
if (appIdMatch?.[1]) {
return [
{
Expand All @@ -304,12 +305,12 @@
export function extractAppIdentifiersAndroid(fileContent: string): AppIdentifier[] {
const identifiers: AppIdentifier[] = [];
try {
const config = JSON.parse(fileContent);

Check warning on line 308 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
if (config.client && Array.isArray(config.client)) {

Check warning on line 309 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .client on an `any` value

Check warning on line 309 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .client on an `any` value
for (const client of config.client) {

Check warning on line 310 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .client on an `any` value
if (client.client_info?.mobilesdk_app_id) {

Check warning on line 311 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .client_info on an `any` value
identifiers.push({
appId: client.client_info.mobilesdk_app_id,

Check warning on line 313 in src/appUtils.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
bundleId: client.client_info.android_client_info?.package_name,
});
}
Expand Down
20 changes: 9 additions & 11 deletions src/crashlytics/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@ import { FirebaseError } from "../error";

export const ApplicationIdSchema = z
.string()
.describe(
"Firebase app id. For an Android application, read the " +
"mobilesdk_app_id value specified in the google-services.json file for " +
"the current package name. For an iOS Application, read the GOOGLE_APP_ID " +
"from GoogleService-Info.plist. If neither is available, ask the user to " +
"provide the app id.",
);
.describe("Firebase App Id. Strictly required for all API calls.");

export const IssueIdSchema = z.string().describe("Crashlytics issue id, as hexidecimal uuid");
export const IssueIdSchema = z.string().describe("Crashlytics issue id, as hexidecimal UUID");

export const EventFilterSchema = z
.object({
Expand Down Expand Up @@ -109,12 +103,15 @@ export function filterToUrlSearchParams(filter: EventFilter): URLSearchParams {
const displayNamePattern = /^[^()]+\s+\([^()]+\)$/; // Regular expression like "xxxx (yyy)"

/**
* Perform some simplistic validation on filters.
* Perform some simplistic validation on filters and fill missing values.
* @param filter filters to validate
* @throws FirebaseError if any of the filters are invalid.
*/
export function validateEventFilters(filter: EventFilter): void {
if (!filter) return;
export function validateEventFilters(filter: EventFilter = {}): EventFilter {
if (!!filter.intervalStartTime && !filter.intervalEndTime) {
// interval.end_time is required if interval.start_time is set but the agent likes to forget it
filter.intervalEndTime = new Date().toISOString();
}
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
if (filter.intervalStartTime && new Date(filter.intervalStartTime) < ninetyDaysAgo) {
throw new FirebaseError("intervalStartTime must be less than 90 days in the past");
Expand All @@ -140,4 +137,5 @@ export function validateEventFilters(filter: EventFilter): void {
}
});
}
return filter;
}
4 changes: 2 additions & 2 deletions src/crashlytics/reports.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe("getReport", () => {
.reply(200, mockResponse);

const result = await getReport(
CrashlyticsReport.TopIssues,
CrashlyticsReport.TOP_ISSUES,
appId,
{ issueErrorTypes: [issueType] },
pageSize,
Expand All @@ -153,7 +153,7 @@ describe("getReport", () => {
it("should throw a FirebaseError if the appId is invalid", async () => {
const invalidAppId = "invalid-app-id";

await expect(getReport(CrashlyticsReport.TopIssues, invalidAppId, {})).to.be.rejectedWith(
await expect(getReport(CrashlyticsReport.TOP_ISSUES, invalidAppId, {})).to.be.rejectedWith(
FirebaseError,
"Unable to get the projectId from the AppId.",
);
Expand Down
31 changes: 19 additions & 12 deletions src/crashlytics/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,30 @@ import {
EventFilterSchema,
filterToUrlSearchParams,
} from "./filters";
import { FirebaseError } from "../error";

const DEFAULT_PAGE_SIZE = 10;

export enum CrashlyticsReport {
TOP_ISSUES = "topIssues",
TOP_VARIANTS = "topVariants",
TOP_VERSIONS = "topVersions",
TOP_OPERATING_SYSTEMS = "topOperatingSystems",
TOP_APPLE_DEVICES = "topAppleDevices",
TOP_ANDROID_DEVICES = "topAndroidDevices",
}

export const CrashlyticsReportSchema = z.nativeEnum(CrashlyticsReport);

export const ReportInputSchema = z.object({
appId: ApplicationIdSchema,
report: CrashlyticsReportSchema,
filter: EventFilterSchema,
pageSize: z.number().optional().describe("Number of rows to return").default(DEFAULT_PAGE_SIZE),
});

export type ReportInput = z.infer<typeof ReportInputSchema>;

export enum CrashlyticsReport {
TopIssues = "topIssues",
TopVariants = "topVariants",
TopVersions = "topVersions",
TopOperatingSystems = "topOperatingSystems",
TopAppleDevices = "topAppleDevices",
TopAndroidDevices = "topAndroidDevices",
}

/**
* Returns a report for Crashlytics events.
* @param report One of the supported reports in the CrashlyticsReport enum
Expand Down Expand Up @@ -62,23 +66,26 @@ export function simplifyReport(report: Report): Report {
}

export async function getReport(
report: CrashlyticsReport,
reportName: CrashlyticsReport,
appId: string,
filter: EventFilter,
pageSize = DEFAULT_PAGE_SIZE,
): Promise<Report> {
if (!reportName) {
throw new FirebaseError("Invalid Crashlytics report " + reportName);
}
const requestProjectNumber = parseProjectNumber(appId);
const queryParams = filterToUrlSearchParams(filter);
queryParams.set("page_size", `${pageSize}`);
logger.debug(
`[crashlytics] report ${report} called with appId: ${appId} filter: ${queryParams.toString()}, page_size: ${pageSize}`,
`[crashlytics] report ${reportName} called with appId: ${appId} filter: ${queryParams.toString()}, page_size: ${pageSize}`,
);
const response = await CRASHLYTICS_API_CLIENT.request<void, Report>({
method: "GET",
headers: {
"Content-Type": "application/json",
},
path: `/projects/${requestProjectNumber}/apps/${appId}/reports/${report}`,
path: `/projects/${requestProjectNumber}/apps/${appId}/reports/${reportName}`,
queryParams: queryParams,
timeout: TIMEOUT,
});
Expand Down
137 changes: 16 additions & 121 deletions src/mcp/prompts/crashlytics/connect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { prompt } from "../../prompt";
import { RESOURCE_CONTENT as connectResourceContent } from "../../resources/guides/crashlytics_connect";

export const connect = prompt(
"crashlytics",
Expand All @@ -11,137 +12,31 @@ export const connect = prompt(
},
},
async (unused, { accountEmail, firebaseCliCommand }) => {
const loggedInInstruction = `
**The user is logged into Firebase as ${accountEmail || ""}.
`.trim();

const notLoggedInInstruction = `
**Instruct the User to Log In**
The user is not logged in to Firebase. None of the Crashlytics tools will be able to authenticate until the user has logged in. Instruct the user to run \`${firebaseCliCommand} login\` before continuing, then use the \`firebase_get_environment\` tool to verify that the user is logged in.
`.trim();

return [
{
role: "user" as const,
content: {
type: "text",
text: `
You are going to help a developer prioritize and fix issues in their
mobile application by accessing their Firebase Crashlytics data.

Active user: ${accountEmail || "<NONE>"}

General rules:
**ASK THE USER WHAT THEY WOULD LIKE TO DO BEFORE TAKING ACTION**
**ASK ONLY ONE QUESTION OF THE USER AT A TIME**
**MAKE SURE TO FOLLOW THE INSTRUCTIONS, ESPECIALLY WHERE THEY ASK YOU TO CHECK IN WITH THE USER**
**ADHERE TO SUGGESTED FORMATTING**

## Required first steps! Absolutely required! Incredibly important!

1. **Make sure the user is logged in. No Crashlytics tools will work if the user is not logged in.**
a. Use the \`firebase_get_environment\` tool to verify that the user is logged in.
b. If the Firebase 'Active user' is set to <NONE>, instruct the user to run \`${firebaseCliCommand} login\`
before continuing. Ignore other fields that are set to <NONE>. We are just making sure the
user is logged in.

2. **Get the app ID for the Firebase application.**
a. **PRIORITIZE REMEMBERED APP ID ENTRIES** If an entry for this directory exists in the remembered app ids, use the remembered app id
for this directory without presenting any additional options.
i. If there are multiple remembered app ids for this directory, ask the user to choose one by providing
a numbered list of all the package names. Tell them that these values came from memories and how they can modify those values.
b. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Use the app IDs from the \`firebase_get_environment\` tool.
i. If you've already called this tool, use the previous response from context.
ii. If the 'Detected App IDs' is set to <NONE>, ask the user for the value they want to use.
iii. If there are multiple 'Detected App IDs', ask the user to choose one by providing
a numbered list of all the package names and app ids.
c. **IF THERE IS A REMEMBERED VALUE BUT IT DOES NOT MATCH ANY DETECTED APP IDS** Ask if the user would like to replace the value with one of
the detected values.
i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version
number (typically "1"), a project number, a platform type ("android", "ios", or "web"),
and a sequence of hexadecimal characters.
ii. Replace the value for this directory with this valid app id, the android package name or ios bundle identifier, and the project directory.
c. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Ask if the user would like to remember the app id selection
i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version
number (typically "1"), a project number, a platform type ("android", "ios", or "web"),
and a sequence of hexadecimal characters.
ii. Store the valid app id value, the android package name or ios bundle identifier, and the project directory.

## Next steps

Once you have confirmed that the user is logged in to Firebase, confirmed the
id for the application that they want to access, and asked if they want to remember the app id for this directory,
ask the user what actions they would like to perform.

Use the following format to ask the user what actions they would like to perform:

1. Prioritize the most impactful stability issues
2. Diagnose and propose a fix for a crash

Wait for their response before taking action.

## Instructions for Using Crashlytics Data

### How to prioritize issues

Follow these steps to fetch issues and prioritize them.

1. Use the 'crashlytics_get_top_issues' tool to fetch up to 20 issues.
1a. Analyze the user's query and apply the appropriate filters.
1b. If the user asks for crashes, then set the issueErrorType filter to *FATAL*.
1c. If the user asks about a particular time range, then set both the intervalStartTime and intervalEndTime.
2. Use the 'crashlytics_get_top_versions' tool to fetch the top versions for this app.
3. If the user instructions include statements about prioritization, use those instructions.
4. If the user instructions do not include statements about prioritization,
then prioritize the returned issues using the following criteria:
4a. The app versions for the issue include the most recent version of the app.
4b. The number of users experiencing the issue across variants
4c. The volume of crashes
5. Return the top 5 issues, with a brief description each in a numerical list with the following format:
1. Issue <issue id>
* <the issue title>
* <the issue subtitle>
* **Description:** <a discription of the issue based on information from the tool response>
* **Rationale:** <the reason this issue was prioritized in the way it was>
6. Ask the user if they would like to diagnose and fix any of the issues presented

### How to diagnose and fix issues

Follow these steps to diagnose and fix issues.
You will assist developers in investigating and resolving mobile application issues by leveraging Firebase Crashlytics data.

1. Make sure you have a good understanding of the code structure and where different functionality exists
2. Use the 'crashlytics_get_issue' tool to get more context on the issue.
3. Use the 'crashlytics_batch_get_events' tool to get an example crash for this issue. Use the event names in the sampleEvent fields.
3a. If you need to read more events, use the 'crashlytics_list_events' tool.
3b. Apply the same filtering criteria that you used to find the issue, so that you find a appropriate events.
4. Read the files that exist in the stack trace of the issue to understand the crash deeply.
5. Determine possible root causes for the crash - no more than 5 potential root causes.
6. Critique your own determination, analyzing how plausible each scenario is given the crash details.
7. Choose the most likely root cause given your analysis.
8. Write out a plan for the most likely root cause using the following criteria:
8a. Write out a description of the issue and including
* A brief description of the cause of the issue
* A determination of your level of confidence in the cause of the issue using your analysis.
* A determination of which library is at fault, this codebase or a dependent library
* A determination for how complex the fix will be
8b. The plan should include relevant files to change
8c. The plan should include a test plan for how the user might verify the fix
8d. Use the following format for the plan:
### Required First Steps

## Cause
<A description of the root cause leading to the issue>
- **Fault**: <a determination of whether this code base is at fault or a dependent library is at fault>
- **Complexity**: <one of "simple", "moderately simple", "moderately hard", "hard", "oof, I don't know where to start">

## Fix
<A description of the fix for this issue and a break down of the changes.>
1. <Step 1>
2. <Step 2>
${accountEmail ? loggedInInstruction : notLoggedInInstruction}

## Test
<A plan for how to test that the issue has been fixed and protect against regressions>
1. <Test case 1>
2. <Test case 2>
**Obtain the Firebase App ID.**
If an App ID is not readily available, consult this guide for selection: [Firebase App Id Guide](firebase://guides/app_id).

## Other potential causes
1. <Another possible root cause>
2. <Another possible root cause>

9. Present the plan to the user and get approval before making the change.
10. Only if they approve the plan, create a fix for the issue.
10a. Be mindful of API contracts and do not add fields to resources without a clear way to populate those fields
10b. If there is not enough information in the crash report to find a root cause, describe why you cannot fix the issue instead of making a guess.
${connectResourceContent}
`.trim(),
},
},
Expand Down
42 changes: 42 additions & 0 deletions src/mcp/resources/guides/app_id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { resource } from "../../resource";

export const RESOURCE_CONTENT = `
### Firebase App ID
The Firebase App ID is used to identify a mobile or web client application to Firebase back end services such as Crashlytics or Remote Config. Use the information below to find the developer's App ID.

1. **PRIORITIZE REMEMBERED APP ID ENTRIES** If an entry for this directory exists in the remembered app ids, use the remembered app id
for this directory without presenting any additional options.
i. If there are multiple remembered app ids for this directory, ask the user to choose one by providing
a numbered list of all the package names. Tell them that these values came from memories and how they can modify those values.
2. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Use the app IDs from the \`firebase_get_environment\` tool.
i. If you've already called this tool, use the previous response from context.
ii. If the 'Detected App IDs' is set to <NONE>, ask the user for the value they want to use.
iii. If there are multiple 'Detected App IDs', ask the user to choose one by providing
a numbered list of all the package names and app ids.
3. **IF THERE IS A REMEMBERED VALUE BUT IT DOES NOT MATCH ANY DETECTED APP IDS** Ask if the user would like to replace the value with one of
the detected values.
i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version
number (typically "1"), a project number, a platform type ("android", "ios", or "web"),
and a sequence of hexadecimal characters.
ii. Replace the value for this directory with this valid app id, the android package name or ios bundle identifier, and the project directory.
4. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Ask if the user would like to remember the app id selection
i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version
number (typically "1"), a project number, a platform type ("android", "ios", or "web"),
and a sequence of hexadecimal characters.
ii. Store the valid app id value, the android package name or ios bundle identifier, and the project directory.
`.trim();

export const app_id = resource(
{
uri: "firebase://guides/app_id",
name: "app_id_guide",
title: "Firebase App Id Guide",
description:
"guides the coding agent through choosing a Firebase App ID in the current project",
},
async (uri) => {
return {
contents: [{ uri, type: "text", text: RESOURCE_CONTENT }],
};
},
);
49 changes: 49 additions & 0 deletions src/mcp/resources/guides/crashlytics_connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { resource } from "../../resource";

export const RESOURCE_CONTENT = `
### Instructions for Working with Firebase Crashlytics Tools

Only ask the user one question at a time. Do not proceed without user instructions. Upon receiving user instructions, refer to the relevant resources for guidance.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a strong enough statement? I've seen it ignore this pretty readily without emphasis - asking multiple questions at a time being the main thing it tries to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think we might want to remove this entirely and let the agent and developer determine the working agreement. What if the agent is meant to be autonomous?


Use the \`firebase_read_resources\` tool to access the following guides.

1. [Firebase App Id Guide](firebase://guides/app_id)
This guide provides crucial instructions for obtaining the application's App Id which is required for all API calls.

2. [Firebase Crashlytics Reports Guide](firebase://guides/crashlytics/reports)
This guide details how to request and use aggregated numerical data from Crashlytics. The agent should read this guide before requesting any report.

3. [Firebase Crashlytics Issues Guide](firebase://guides/crashlytics/issues)
This guide details how to work with issues within Crashlytics. The agent should read this guide before prioritizing issues or presenting issue data to the user.

4. [Investigating Crashlytics Issues](firebase://guides/crashlytics/investigations)
This guide provides instructions on investigating the root causes of crashes and exceptions reported in Crashlytics issues.

### Check That You Are Connected

Verify that you can read the app's Crashlytics data by getting the topVersions report. This report will tell you which app versions have the most events.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't have this step before - how has this played out in your testing? My one concern here is that it increases token consumption by default without interaction from the user. That happens not infrequently anyway, but curious if it happens more with this instruction.

a. Call the \`firebase_get_environment\` tool if you need to find the app_id.
b. Call the \`crashlytics_get_report\` tool to read the \`topVersions\` report.
c. If you haven't read the reports guide, then the tool will include it in the response. This is OK. Simply call the tool again.
d. Help the user resolve any issues that arise when trying to connect.

After confirming you can access Crashlytics, inquire about the desired actions. Your capabilities include:

- Reading Crashlytics reports.
- Investigating bug reports using Crashlytics event data.
- Proposing code changes to resolve identified bugs.
`.trim();

export const crashlytics_connect = resource(
{
uri: "firebase://guides/crashlytics/connect",
name: "crashlytics_connect_guide",
title: "Firebase Crashlytics Connect Guide",
description: "Guides the coding agent to connect to Firebase Crashlytics.",
},
async (uri) => {
return {
contents: [{ uri, type: "text", text: RESOURCE_CONTENT }],
};
},
);
Loading
Loading