Skip to content

Commit d83bc88

Browse files
authored
(feat) Reorganize and improve crashlytics tools (#9127)
- Importing interfaces for the Crashlytics public API - Setting appropriate return types in client responses - When retrieving native crash sessions, pruning all superfluous threads to dramatically reduce token usage. - Providing schemas for comprehensive filtering options - Consoldidating report client calls into a single method - Consolidating tool definitions and sharing schema defs
1 parent 9051937 commit d83bc88

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1694
-1433
lines changed

src/crashlytics/addNote.spec.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/crashlytics/addNote.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/crashlytics/deleteNote.spec.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

src/crashlytics/deleteNote.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/crashlytics/events.spec.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as chai from "chai";
2+
import * as nock from "nock";
3+
import * as chaiAsPromised from "chai-as-promised";
4+
5+
import { listEvents } from "./events";
6+
import { FirebaseError } from "../error";
7+
import { crashlyticsApiOrigin } from "../api";
8+
9+
chai.use(chaiAsPromised);
10+
const expect = chai.expect;
11+
12+
describe("events", () => {
13+
const appId = "1:1234567890:android:abcdef1234567890";
14+
const requestProjectNumber = "1234567890";
15+
const issueId = "test_issue_id";
16+
const variantId = "test_variant_id";
17+
const pageSize = 2;
18+
19+
afterEach(() => {
20+
nock.cleanAll();
21+
});
22+
23+
describe("listEvents", () => {
24+
it("should resolve with the response body on success", async () => {
25+
const mockResponse = { events: [{ event_id: "1" }] };
26+
27+
nock(crashlyticsApiOrigin())
28+
.get(`/v1alpha/projects/${requestProjectNumber}/apps/${appId}/events`)
29+
.query({
30+
"filter.issue.id": issueId,
31+
page_size: String(pageSize),
32+
})
33+
.reply(200, mockResponse);
34+
35+
const result = await listEvents(appId, { issueId: issueId }, pageSize);
36+
37+
expect(result).to.deep.equal(mockResponse);
38+
expect(nock.isDone()).to.be.true;
39+
});
40+
41+
it("should resolve with the response body on success with variantId", async () => {
42+
const mockResponse = { events: [{ event_id: "1" }] };
43+
44+
nock(crashlyticsApiOrigin())
45+
.get(`/v1alpha/projects/${requestProjectNumber}/apps/${appId}/events`)
46+
.query({
47+
"filter.issue.id": issueId,
48+
"filter.issue.variant_id": variantId,
49+
page_size: String(pageSize),
50+
})
51+
.reply(200, mockResponse);
52+
53+
const result = await listEvents(
54+
appId,
55+
{ issueId: issueId, issueVariantId: variantId },
56+
pageSize,
57+
);
58+
59+
expect(result).to.deep.equal(mockResponse);
60+
expect(nock.isDone()).to.be.true;
61+
});
62+
63+
it("should throw a FirebaseError if the API call fails", async () => {
64+
nock(crashlyticsApiOrigin())
65+
.get(`/v1alpha/projects/${requestProjectNumber}/apps/${appId}/events`)
66+
.query({
67+
page_size: String(pageSize),
68+
})
69+
.reply(500, { error: "Internal Server Error" });
70+
71+
await expect(listEvents(appId, {}, pageSize)).to.be.rejectedWith(
72+
FirebaseError,
73+
`Failed to list events for app_id ${appId}.`,
74+
);
75+
});
76+
77+
it("should throw a FirebaseError if the appId is invalid", async () => {
78+
const invalidAppId = "invalid-app-id";
79+
80+
await expect(
81+
listEvents(invalidAppId, { issueId: issueId, issueVariantId: variantId }),
82+
).to.be.rejectedWith(FirebaseError, "Unable to get the projectId from the AppId.");
83+
});
84+
});
85+
});

src/crashlytics/events.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { logger } from "../logger";
2+
import { FirebaseError, getError } from "../error";
3+
import { CRASHLYTICS_API_CLIENT, parseProjectNumber, TIMEOUT } from "./utils";
4+
import { ListEventsResponse } from "./types";
5+
import { EventFilter, filterToUrlSearchParams } from "./filters";
6+
7+
/**
8+
* List Crashlytics events matching the given filters.
9+
* @param appId Firebase app_id
10+
* @param filter An optional EventFilter to selectively filter the sample events.
11+
* @param pageSize optional, number of events to return
12+
* @return A ListEventsResponse containing the most recent events matching the filters.
13+
*/
14+
export async function listEvents(
15+
appId: string,
16+
filter: EventFilter,
17+
pageSize = 1,
18+
): Promise<ListEventsResponse> {
19+
const requestProjectNumber = parseProjectNumber(appId);
20+
21+
try {
22+
const queryParams = filterToUrlSearchParams(filter);
23+
queryParams.set("page_size", `${pageSize}`);
24+
25+
logger.debug(
26+
`[crashlytics] listEvents called with appId: ${appId}, filter: ${queryParams.toString()}, pageSize: ${pageSize}`,
27+
);
28+
29+
const response = await CRASHLYTICS_API_CLIENT.request<void, ListEventsResponse>({
30+
method: "GET",
31+
headers: {
32+
"Content-Type": "application/json",
33+
},
34+
path: `/projects/${requestProjectNumber}/apps/${appId}/events`,
35+
queryParams: queryParams,
36+
timeout: TIMEOUT,
37+
});
38+
39+
return response.body;
40+
} catch (err: unknown) {
41+
throw new FirebaseError(`Failed to list events for app_id ${appId}.`, {
42+
original: getError(err),
43+
});
44+
}
45+
}

src/crashlytics/filters.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { z } from "zod";
2+
3+
export const ApplicationIdSchema = z
4+
.string()
5+
.describe(
6+
"Firebase app id. For an Android application, read the " +
7+
"mobilesdk_app_id value specified in the google-services.json file for " +
8+
"the current package name. For an iOS Application, read the GOOGLE_APP_ID " +
9+
"from GoogleService-Info.plist. If neither is available, ask the user to " +
10+
"provide the app id.",
11+
);
12+
13+
export const IssueIdSchema = z.string().describe("Crashlytics issue id, as hexidecimal uuid");
14+
15+
export const EventFilterSchema = z
16+
.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.`),
19+
versionDisplayNames: z
20+
.array(z.string())
21+
.optional()
22+
.describe(`The version display names should be obtained from an API response.`),
23+
issueId: z.string().optional().describe(`Count events for the given issue`),
24+
issueVariantId: z.string().optional().describe(`Count events for the given issue variant`),
25+
issueErrorTypes: z
26+
.array(z.enum(["FATAL", "NON_FATAL", "ANR"]))
27+
.optional()
28+
.describe(
29+
`Count FATAL events (crashes), NON_FATAL events (exceptions) or ANR events (application not responding)`,
30+
),
31+
issueSignals: z
32+
.array(z.enum(["SIGNAL_EARLY", "SIGNAL_FRESH", "SIGNAL_REGRESSED", "SIGNAL_REPETITIVE"]))
33+
.optional()
34+
.describe(`Count events matching the given signals`),
35+
operatingSystemDisplayNames: z
36+
.array(z.string())
37+
.optional()
38+
.describe(`The operating system displayNames should be obtained from an API response`),
39+
deviceDisplayNames: z
40+
.array(z.string())
41+
.optional()
42+
.describe(`The operating system displayNames should be obtained from an API response`),
43+
deviceFormFactors: z
44+
.array(z.enum(["PHONE", "TABLET", "DESKTOP", "TV", "WATCH"]))
45+
.optional()
46+
.describe(`Count events originating from the given device form factors`),
47+
})
48+
.optional()
49+
.describe(`Only events matching the given filters will be counted. All filters are optional.
50+
If setting a time interval, set both intervalStartTime and intervalEndTime.`);
51+
52+
export type EventFilter = z.infer<typeof EventFilterSchema>;
53+
54+
// Most models seem to understand the flattened, camelCase representation better.
55+
// This maps those strings to the filter params the API expects.
56+
57+
const toolToParamMap: Record<string, string> = {
58+
intervalStartTime: "filter.interval.start_time",
59+
intervalEndTime: "filter.interval.end_time",
60+
versionDisplayNames: "filter.version.display_names",
61+
issueId: "filter.issue.id",
62+
issueVariantId: "filter.issue.variant_id",
63+
issueErrorTypes: "filter.issue.error_types",
64+
issueSignals: "filter.issue.signals",
65+
operatingSystemDisplayNames: "filter.operating_system.display_names",
66+
deviceDisplayNames: "filter.device.display_names",
67+
deviceFormFactors: "filter.device.form_factors",
68+
};
69+
70+
/**
71+
* Converts the model-friendly, flattened camelCase tool parameters into the
72+
* AIP-160 style url parameters for all of the filtering options.
73+
* @param filter an EventFilter
74+
* @return URLSearchParams for a request to the GetReport endpoint
75+
*/
76+
export function filterToUrlSearchParams(filter: EventFilter): URLSearchParams {
77+
const params = new URLSearchParams();
78+
for (const [key, value] of Object.entries(filter || {})) {
79+
if (value === undefined) {
80+
continue;
81+
}
82+
const paramKey: string = toolToParamMap[key];
83+
if (Array.isArray(value)) {
84+
for (const v of value) {
85+
params.append(paramKey, v);
86+
}
87+
} else if (value) {
88+
params.set(paramKey, value);
89+
}
90+
}
91+
return params;
92+
}

0 commit comments

Comments
 (0)