From 89118a769d543b52bc416ec48041323b5f7bca95 Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Wed, 4 Nov 2026 15:32:04 -0500 Subject: [PATCH 1/2] fix(crashlytics): use v1alpha profilingManagerConfig endpoint for heap dump commands Update `getProfilingManagerConfig` and `updateProfilingManagerConfig` to use the shared `CRASHLYTICS_API_CLIENT` (`v1alpha`) and the singleton resource endpoint `/v1alpha/projects/{projectNumber}/apps/{appId}/profilingManagerConfig`: - Switch `getProfilingManagerConfig` to `GET /v1alpha/projects/{projectNumber}/apps/{appId}/profilingManagerConfig` and remove `getCrashlyticsV1Client()`. - Switch `updateProfilingManagerConfig` to `PATCH /v1alpha/projects/{projectNumber}/apps/{appId}/profilingManagerConfig` with `updateMask`. - Translate `gcsBucket` between bare bucket IDs used by the CLI and canonical `projects/_/buckets/{bucket}` resource names expected by the API. - Simplify `disableHeapDumpCollection` to send a partial `PATCH` (`updateMask=heapDumpCollectionEnabled`) instead of a `GET` + `POST` pair so the existing `gcsBucket` is preserved atomically. - Update `profilingManager.spec.ts` unit tests to cover the `v1alpha` resource paths, bucket resource-name formatting, and partial `updateMask` updates. --- src/crashlytics/profilingManager.spec.ts | 117 +++++++++++++---------- src/crashlytics/profilingManager.ts | 98 +++++++++++-------- 2 files changed, 122 insertions(+), 93 deletions(-) diff --git a/src/crashlytics/profilingManager.spec.ts b/src/crashlytics/profilingManager.spec.ts index a5d94954b41..777004dff2b 100644 --- a/src/crashlytics/profilingManager.spec.ts +++ b/src/crashlytics/profilingManager.spec.ts @@ -35,7 +35,9 @@ describe("profilingManager", () => { const hashedAppId = "abcdef123456"; const appId = `1:${projectNumber}:android:${hashedAppId}`; const expectedBucketName = `firebasecrashlytics-heap-dumps-${hashedAppId}`; + const expectedBucketResourceName = `projects/_/buckets/${expectedBucketName}`; const projectId = "my-test-project"; + const resourcePath = `/v1alpha/projects/${projectNumber}/apps/${appId}/profilingManagerConfig`; afterEach(() => { nock.cleanAll(); @@ -175,15 +177,12 @@ describe("profilingManager", () => { describe("getProfilingManagerConfig", () => { it("should return the configuration on successful response", async () => { const mockResponse = { - configuration: { - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: true, - }, + name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, + gcsBucket: expectedBucketResourceName, + heapDumpCollectionEnabled: true, }; - nock(crashlyticsApiOrigin()) - .get(`/v1/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`) - .reply(200, mockResponse); + nock(crashlyticsApiOrigin()).get(resourcePath).reply(200, mockResponse); const result = await getProfilingManagerConfig(appId); expect(result).to.deep.equal({ @@ -194,9 +193,7 @@ describe("profilingManager", () => { }); it("should return default values when configuration is empty", async () => { - nock(crashlyticsApiOrigin()) - .get(`/v1/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`) - .reply(200, {}); + nock(crashlyticsApiOrigin()).get(resourcePath).reply(200, {}); const result = await getProfilingManagerConfig(appId); expect(result).to.deep.equal({ @@ -215,36 +212,61 @@ describe("profilingManager", () => { }); describe("updateProfilingManagerConfig", () => { - it("should send the update request successfully", async () => { + it("should send the PATCH update request with field mask successfully", async () => { nock(crashlyticsApiOrigin()) - .post(`/v1/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`, { - projectNumber, - gmpAppId: appId, - configuration: { - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: true, - }, + .patch(resourcePath, { + gcsBucket: expectedBucketResourceName, + heapDumpCollectionEnabled: true, }) - .reply(200, {}); + .query({ updateMask: "gcsBucket,heapDumpCollectionEnabled" }) + .reply(200, { + name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, + gcsBucket: expectedBucketResourceName, + heapDumpCollectionEnabled: true, + }); + + const result = await updateProfilingManagerConfig(appId, { + gcsBucket: expectedBucketName, + heapDumpCollectionEnabled: true, + }); - await updateProfilingManagerConfig(appId, { + expect(result).to.deep.equal({ gcsBucket: expectedBucketName, heapDumpCollectionEnabled: true, }); + expect(nock.isDone()).to.be.true; + }); + + it("should send partial updateMask when updating only heapDumpCollectionEnabled", async () => { + nock(crashlyticsApiOrigin()) + .patch(resourcePath, { + heapDumpCollectionEnabled: false, + }) + .query({ updateMask: "heapDumpCollectionEnabled" }) + .reply(200, { + name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, + gcsBucket: expectedBucketResourceName, + heapDumpCollectionEnabled: false, + }); + + const result = await updateProfilingManagerConfig(appId, { + heapDumpCollectionEnabled: false, + }); + expect(result).to.deep.equal({ + gcsBucket: expectedBucketName, + heapDumpCollectionEnabled: false, + }); expect(nock.isDone()).to.be.true; }); it("should default empty configuration fields when omitted", async () => { nock(crashlyticsApiOrigin()) - .post(`/v1/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`, { - projectNumber, - gmpAppId: appId, - configuration: { - gcsBucket: "", - heapDumpCollectionEnabled: false, - }, + .patch(resourcePath, { + gcsBucket: "", + heapDumpCollectionEnabled: false, }) + .query({ updateMask: "gcsBucket,heapDumpCollectionEnabled" }) .reply(200, {}); await updateProfilingManagerConfig(appId, {}); @@ -453,15 +475,16 @@ describe("profilingManager", () => { sinon.stub(serviceusage, "generateServiceIdentityAndPoll").resolves(); sinon.stub(resourceManager, "serviceAccountHasRoles").resolves(true); nock(crashlyticsApiOrigin()) - .post(`/v1/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`, { - projectNumber, - gmpAppId: appId, - configuration: { - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: true, - }, + .patch(resourcePath, { + gcsBucket: expectedBucketResourceName, + heapDumpCollectionEnabled: true, }) - .reply(200, {}); + .query({ updateMask: "gcsBucket,heapDumpCollectionEnabled" }) + .reply(200, { + name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, + gcsBucket: expectedBucketResourceName, + heapDumpCollectionEnabled: true, + }); const result = await enableHeapDumpCollection(projectId, appId); @@ -469,25 +492,17 @@ describe("profilingManager", () => { expect(nock.isDone()).to.be.true; }); - it("should preserve existing bucket name and disable heap dump collection", async () => { + it("should disable heap dump collection via partial updateMask while preserving bucket", async () => { nock(crashlyticsApiOrigin()) - .get(`/v1/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`) + .patch(resourcePath, { + heapDumpCollectionEnabled: false, + }) + .query({ updateMask: "heapDumpCollectionEnabled" }) .reply(200, { - configuration: { - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: true, - }, + name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, + gcsBucket: expectedBucketResourceName, + heapDumpCollectionEnabled: false, }); - nock(crashlyticsApiOrigin()) - .post(`/v1/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`, { - projectNumber, - gmpAppId: appId, - configuration: { - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: false, - }, - }) - .reply(200, {}); await disableHeapDumpCollection(appId); diff --git a/src/crashlytics/profilingManager.ts b/src/crashlytics/profilingManager.ts index 961c7a65661..6d48fd08869 100644 --- a/src/crashlytics/profilingManager.ts +++ b/src/crashlytics/profilingManager.ts @@ -1,24 +1,12 @@ -import { Client } from "../apiv2"; -import { crashlyticsApiOrigin } from "../api"; import { FirebaseError, getErrStatus } from "../error"; import { logger } from "../logger"; import { logLabeledBullet, logLabeledSuccess } from "../utils"; -import { parseProjectNumber, TIMEOUT } from "./utils"; +import { CRASHLYTICS_API_CLIENT, parseProjectNumber, TIMEOUT } from "./utils"; import * as storage from "../gcp/storage"; import * as resourceManager from "../gcp/resourceManager"; import * as serviceusage from "../gcp/serviceusage"; import { AppPlatform, listFirebaseApps, selectAppInteractively } from "../management/apps"; -/** - * Returns an authenticated v1 API client for the Firebase Crashlytics API. - */ -export function getCrashlyticsV1Client(): Client { - return new Client({ - urlPrefix: crashlyticsApiOrigin(), - apiVersion: "v1", - }); -} - export const CRASHLYTICS_SERVICE_NAME = "firebasecrashlytics.googleapis.com"; export const STORAGE_OBJECT_CREATOR_ROLE = "roles/storage.objectCreator"; export const HEAP_DUMP_BUCKET_PREFIX = "firebasecrashlytics-heap-dumps-"; @@ -40,23 +28,34 @@ export const DEFAULT_LIFECYCLE_RULES: storage.LifecycleRule[] = [ ]; const ANDROID_APP_ID_REGEX = /^\d+:(\d+):android:([a-fA-F0-9]+)$/; +const GCS_BUCKET_RESOURCE_PREFIX = "projects/_/buckets/"; +const GCS_BUCKET_RESOURCE_REGEX = /^projects\/[^/]+\/buckets\/(.+)$/; export interface ProfilingManagerConfig { gcsBucket?: string; heapDumpCollectionEnabled?: boolean; } -export interface GetProfilingManagerConfigResponse { - configuration?: ProfilingManagerConfig; +interface ProfilingManagerConfigResource extends ProfilingManagerConfig { + name?: string; } -export interface UpdateProfilingManagerConfigRequest { - projectNumber: string; - gmpAppId: string; - configuration: { - gcsBucket: string; - heapDumpCollectionEnabled: boolean; - }; +function toBucketResourceName(bucketName: string): string { + if (!bucketName) { + return ""; + } + if (GCS_BUCKET_RESOURCE_REGEX.test(bucketName)) { + return bucketName; + } + return `${GCS_BUCKET_RESOURCE_PREFIX}${bucketName}`; +} + +function fromBucketResourceName(bucketResource?: string): string { + if (!bucketResource) { + return ""; + } + const match = GCS_BUCKET_RESOURCE_REGEX.exec(bucketResource); + return match ? match[1] : bucketResource; } /** @@ -135,50 +134,67 @@ export async function resolveAndroidAppId( export async function getProfilingManagerConfig(appId: string): Promise { const projectNumber = parseProjectNumber(appId); logger.debug(`[crashlytics] getProfilingManagerConfig called with appId: ${appId}`); - const response = await getCrashlyticsV1Client().request({ + const response = await CRASHLYTICS_API_CLIENT.request({ method: "GET", headers: { "Content-Type": "application/json", }, - path: `/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`, + path: `/projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, timeout: TIMEOUT, }); - const config = response.body?.configuration || {}; return { - gcsBucket: config.gcsBucket || "", - heapDumpCollectionEnabled: config.heapDumpCollectionEnabled ?? false, + gcsBucket: fromBucketResourceName(response.body?.gcsBucket), + heapDumpCollectionEnabled: response.body?.heapDumpCollectionEnabled ?? false, }; } /** * Updates the Crashlytics Profiling Manager configuration for an app. * @param appId GMP App ID - * @param config Profiling manager configuration + * @param config Profiling manager configuration fields to update */ export async function updateProfilingManagerConfig( appId: string, config: ProfilingManagerConfig, -): Promise { +): Promise { const projectNumber = parseProjectNumber(appId); logger.debug( `[crashlytics] updateProfilingManagerConfig called with appId: ${appId}, config: ${JSON.stringify(config)}`, ); - await getCrashlyticsV1Client().request({ - method: "POST", + + const maskPaths: string[] = []; + const body: ProfilingManagerConfig = {}; + if (config.gcsBucket !== undefined) { + maskPaths.push("gcsBucket"); + body.gcsBucket = toBucketResourceName(config.gcsBucket); + } + if (config.heapDumpCollectionEnabled !== undefined) { + maskPaths.push("heapDumpCollectionEnabled"); + body.heapDumpCollectionEnabled = config.heapDumpCollectionEnabled; + } + if (maskPaths.length === 0) { + maskPaths.push("gcsBucket", "heapDumpCollectionEnabled"); + body.gcsBucket = ""; + body.heapDumpCollectionEnabled = false; + } + + const response = await CRASHLYTICS_API_CLIENT.request< + ProfilingManagerConfig, + ProfilingManagerConfigResource + >({ + method: "PATCH", headers: { "Content-Type": "application/json", }, - path: `/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`, - body: { - projectNumber, - gmpAppId: appId, - configuration: { - gcsBucket: config.gcsBucket ?? "", - heapDumpCollectionEnabled: config.heapDumpCollectionEnabled ?? false, - }, - }, + path: `/projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, + queryParams: { updateMask: maskPaths.join(",") }, + body, timeout: TIMEOUT, }); + return { + gcsBucket: fromBucketResourceName(response.body?.gcsBucket), + heapDumpCollectionEnabled: response.body?.heapDumpCollectionEnabled ?? false, + }; } /** @@ -310,9 +326,7 @@ export async function enableHeapDumpCollection( */ export async function disableHeapDumpCollection(appId: string): Promise { logLabeledBullet("crashlytics", "Disabling Crashlytics heap dump collection..."); - const currentConfig = await getProfilingManagerConfig(appId); await updateProfilingManagerConfig(appId, { - gcsBucket: currentConfig.gcsBucket, heapDumpCollectionEnabled: false, }); logLabeledSuccess("crashlytics", "Successfully disabled Crashlytics heap dump collection!"); From b3ca889f52ab8c08c567e2946f571e74ffa67946 Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Thu, 5 Nov 2026 11:36:54 -0500 Subject: [PATCH 2/2] fix(crashlytics): address PR review feedback for profilingManagerConfig - Rename gcsBucket to bucket on ProfilingManagerConfig and in the updateMask field paths to match the v1alpha API schema (AIP-122). - Return response.body directly from getProfilingManagerConfig. - Simplify updateProfilingManagerConfig to accept an explicit updateMask parameter and return Promise. - Update crashlytics:heapdumps:status and unit tests to use bucket. --- src/commands/crashlytics-heapdumps-status.ts | 2 +- src/commands/crashlytics-heapdumps.spec.ts | 12 +-- src/crashlytics/profilingManager.spec.ts | 84 ++++++++---------- src/crashlytics/profilingManager.ts | 91 ++++++-------------- 4 files changed, 67 insertions(+), 122 deletions(-) diff --git a/src/commands/crashlytics-heapdumps-status.ts b/src/commands/crashlytics-heapdumps-status.ts index a326b5645b4..0b0c8a802d6 100644 --- a/src/commands/crashlytics-heapdumps-status.ts +++ b/src/commands/crashlytics-heapdumps-status.ts @@ -27,7 +27,7 @@ export const command = new Command("crashlytics:heapdumps:status") table.push([ appId, config.heapDumpCollectionEnabled ? clc.green("Enabled") : clc.red("Disabled"), - config.gcsBucket || clc.yellow("(none)"), + config.bucket || clc.yellow("(none)"), ]); logger.info(""); diff --git a/src/commands/crashlytics-heapdumps.spec.ts b/src/commands/crashlytics-heapdumps.spec.ts index a474614f04d..98a123046b5 100644 --- a/src/commands/crashlytics-heapdumps.spec.ts +++ b/src/commands/crashlytics-heapdumps.spec.ts @@ -178,7 +178,7 @@ describe("crashlytics:heapdumps commands", () => { describe("crashlytics:heapdumps:status", () => { it("should return the status and configuration when enabled", async () => { sinon.stub(profilingManager, "getProfilingManagerConfig").resolves({ - gcsBucket: bucketName, + bucket: bucketName, heapDumpCollectionEnabled: true, }); @@ -189,14 +189,14 @@ describe("crashlytics:heapdumps commands", () => { expect(result).to.deep.equal({ appId, - gcsBucket: bucketName, + bucket: bucketName, heapDumpCollectionEnabled: true, }); }); it("should return the status and configuration when disabled with no bucket", async () => { sinon.stub(profilingManager, "getProfilingManagerConfig").resolves({ - gcsBucket: "", + bucket: "", heapDumpCollectionEnabled: false, }); @@ -207,14 +207,14 @@ describe("crashlytics:heapdumps commands", () => { expect(result).to.deep.equal({ appId, - gcsBucket: "", + bucket: "", heapDumpCollectionEnabled: false, }); }); it("should register and run crashlytics.heapdumps commands via command loader", async () => { sinon.stub(profilingManager, "getProfilingManagerConfig").resolves({ - gcsBucket: bucketName, + bucket: bucketName, heapDumpCollectionEnabled: true, }); @@ -239,7 +239,7 @@ describe("crashlytics:heapdumps commands", () => { expect(result).to.deep.equal({ appId, - gcsBucket: bucketName, + bucket: bucketName, heapDumpCollectionEnabled: true, }); }); diff --git a/src/crashlytics/profilingManager.spec.ts b/src/crashlytics/profilingManager.spec.ts index 777004dff2b..0248c13aad9 100644 --- a/src/crashlytics/profilingManager.spec.ts +++ b/src/crashlytics/profilingManager.spec.ts @@ -178,28 +178,22 @@ describe("profilingManager", () => { it("should return the configuration on successful response", async () => { const mockResponse = { name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, - gcsBucket: expectedBucketResourceName, + bucket: expectedBucketResourceName, heapDumpCollectionEnabled: true, }; nock(crashlyticsApiOrigin()).get(resourcePath).reply(200, mockResponse); const result = await getProfilingManagerConfig(appId); - expect(result).to.deep.equal({ - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: true, - }); + expect(result).to.deep.equal(mockResponse); expect(nock.isDone()).to.be.true; }); - it("should return default values when configuration is empty", async () => { + it("should return empty object when configuration is empty", async () => { nock(crashlyticsApiOrigin()).get(resourcePath).reply(200, {}); const result = await getProfilingManagerConfig(appId); - expect(result).to.deep.equal({ - gcsBucket: "", - heapDumpCollectionEnabled: false, - }); + expect(result).to.deep.equal({}); expect(nock.isDone()).to.be.true; }); @@ -215,25 +209,25 @@ describe("profilingManager", () => { it("should send the PATCH update request with field mask successfully", async () => { nock(crashlyticsApiOrigin()) .patch(resourcePath, { - gcsBucket: expectedBucketResourceName, + bucket: expectedBucketResourceName, heapDumpCollectionEnabled: true, }) - .query({ updateMask: "gcsBucket,heapDumpCollectionEnabled" }) + .query({ updateMask: "bucket,heapDumpCollectionEnabled" }) .reply(200, { name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, - gcsBucket: expectedBucketResourceName, + bucket: expectedBucketResourceName, heapDumpCollectionEnabled: true, }); - const result = await updateProfilingManagerConfig(appId, { - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: true, - }); + await updateProfilingManagerConfig( + appId, + { + bucket: expectedBucketResourceName, + heapDumpCollectionEnabled: true, + }, + "bucket,heapDumpCollectionEnabled", + ); - expect(result).to.deep.equal({ - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: true, - }); expect(nock.isDone()).to.be.true; }); @@ -245,41 +239,31 @@ describe("profilingManager", () => { .query({ updateMask: "heapDumpCollectionEnabled" }) .reply(200, { name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, - gcsBucket: expectedBucketResourceName, + bucket: expectedBucketResourceName, heapDumpCollectionEnabled: false, }); - const result = await updateProfilingManagerConfig(appId, { - heapDumpCollectionEnabled: false, - }); - - expect(result).to.deep.equal({ - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: false, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should default empty configuration fields when omitted", async () => { - nock(crashlyticsApiOrigin()) - .patch(resourcePath, { - gcsBucket: "", + await updateProfilingManagerConfig( + appId, + { heapDumpCollectionEnabled: false, - }) - .query({ updateMask: "gcsBucket,heapDumpCollectionEnabled" }) - .reply(200, {}); - - await updateProfilingManagerConfig(appId, {}); + }, + "heapDumpCollectionEnabled", + ); expect(nock.isDone()).to.be.true; }); it("should throw a FirebaseError if the appId is invalid", async () => { await expect( - updateProfilingManagerConfig("invalid-app", { - gcsBucket: expectedBucketName, - heapDumpCollectionEnabled: true, - }), + updateProfilingManagerConfig( + "invalid-app", + { + bucket: expectedBucketResourceName, + heapDumpCollectionEnabled: true, + }, + "bucket,heapDumpCollectionEnabled", + ), ).to.be.rejectedWith(FirebaseError, "Unable to get the projectId from the AppId."); }); }); @@ -476,13 +460,13 @@ describe("profilingManager", () => { sinon.stub(resourceManager, "serviceAccountHasRoles").resolves(true); nock(crashlyticsApiOrigin()) .patch(resourcePath, { - gcsBucket: expectedBucketResourceName, + bucket: expectedBucketResourceName, heapDumpCollectionEnabled: true, }) - .query({ updateMask: "gcsBucket,heapDumpCollectionEnabled" }) + .query({ updateMask: "bucket,heapDumpCollectionEnabled" }) .reply(200, { name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, - gcsBucket: expectedBucketResourceName, + bucket: expectedBucketResourceName, heapDumpCollectionEnabled: true, }); @@ -500,7 +484,7 @@ describe("profilingManager", () => { .query({ updateMask: "heapDumpCollectionEnabled" }) .reply(200, { name: `projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, - gcsBucket: expectedBucketResourceName, + bucket: expectedBucketResourceName, heapDumpCollectionEnabled: false, }); diff --git a/src/crashlytics/profilingManager.ts b/src/crashlytics/profilingManager.ts index 6d48fd08869..218d44fa691 100644 --- a/src/crashlytics/profilingManager.ts +++ b/src/crashlytics/profilingManager.ts @@ -29,33 +29,11 @@ export const DEFAULT_LIFECYCLE_RULES: storage.LifecycleRule[] = [ const ANDROID_APP_ID_REGEX = /^\d+:(\d+):android:([a-fA-F0-9]+)$/; const GCS_BUCKET_RESOURCE_PREFIX = "projects/_/buckets/"; -const GCS_BUCKET_RESOURCE_REGEX = /^projects\/[^/]+\/buckets\/(.+)$/; export interface ProfilingManagerConfig { - gcsBucket?: string; - heapDumpCollectionEnabled?: boolean; -} - -interface ProfilingManagerConfigResource extends ProfilingManagerConfig { name?: string; -} - -function toBucketResourceName(bucketName: string): string { - if (!bucketName) { - return ""; - } - if (GCS_BUCKET_RESOURCE_REGEX.test(bucketName)) { - return bucketName; - } - return `${GCS_BUCKET_RESOURCE_PREFIX}${bucketName}`; -} - -function fromBucketResourceName(bucketResource?: string): string { - if (!bucketResource) { - return ""; - } - const match = GCS_BUCKET_RESOURCE_REGEX.exec(bucketResource); - return match ? match[1] : bucketResource; + bucket?: string; + heapDumpCollectionEnabled?: boolean; } /** @@ -134,7 +112,7 @@ export async function resolveAndroidAppId( export async function getProfilingManagerConfig(appId: string): Promise { const projectNumber = parseProjectNumber(appId); logger.debug(`[crashlytics] getProfilingManagerConfig called with appId: ${appId}`); - const response = await CRASHLYTICS_API_CLIENT.request({ + const response = await CRASHLYTICS_API_CLIENT.request({ method: "GET", headers: { "Content-Type": "application/json", @@ -142,59 +120,34 @@ export async function getProfilingManagerConfig(appId: string): Promise { + updateMask: string, +): Promise { const projectNumber = parseProjectNumber(appId); logger.debug( - `[crashlytics] updateProfilingManagerConfig called with appId: ${appId}, config: ${JSON.stringify(config)}`, + `[crashlytics] updateProfilingManagerConfig called with appId: ${appId}, config: ${JSON.stringify(config)}, updateMask: ${updateMask}`, ); - - const maskPaths: string[] = []; - const body: ProfilingManagerConfig = {}; - if (config.gcsBucket !== undefined) { - maskPaths.push("gcsBucket"); - body.gcsBucket = toBucketResourceName(config.gcsBucket); - } - if (config.heapDumpCollectionEnabled !== undefined) { - maskPaths.push("heapDumpCollectionEnabled"); - body.heapDumpCollectionEnabled = config.heapDumpCollectionEnabled; - } - if (maskPaths.length === 0) { - maskPaths.push("gcsBucket", "heapDumpCollectionEnabled"); - body.gcsBucket = ""; - body.heapDumpCollectionEnabled = false; - } - - const response = await CRASHLYTICS_API_CLIENT.request< - ProfilingManagerConfig, - ProfilingManagerConfigResource - >({ + await CRASHLYTICS_API_CLIENT.request({ method: "PATCH", headers: { "Content-Type": "application/json", }, path: `/projects/${projectNumber}/apps/${appId}/profilingManagerConfig`, - queryParams: { updateMask: maskPaths.join(",") }, - body, + queryParams: { updateMask }, + body: config, timeout: TIMEOUT, }); - return { - gcsBucket: fromBucketResourceName(response.body?.gcsBucket), - heapDumpCollectionEnabled: response.body?.heapDumpCollectionEnabled ?? false, - }; } /** @@ -312,10 +265,14 @@ export async function enableHeapDumpCollection( logLabeledBullet("crashlytics", "Configuring service agent permissions..."); await ensureHeapDumpP4saRole(projectId, projectNumber); logLabeledBullet("crashlytics", "Enabling Crashlytics heap dump collection..."); - await updateProfilingManagerConfig(appId, { - gcsBucket: bucketName, - heapDumpCollectionEnabled: true, - }); + await updateProfilingManagerConfig( + appId, + { + bucket: `${GCS_BUCKET_RESOURCE_PREFIX}${bucketName}`, + heapDumpCollectionEnabled: true, + }, + "bucket,heapDumpCollectionEnabled", + ); logLabeledSuccess("crashlytics", "Successfully enabled Crashlytics heap dump collection!"); return bucketName; } @@ -326,8 +283,12 @@ export async function enableHeapDumpCollection( */ export async function disableHeapDumpCollection(appId: string): Promise { logLabeledBullet("crashlytics", "Disabling Crashlytics heap dump collection..."); - await updateProfilingManagerConfig(appId, { - heapDumpCollectionEnabled: false, - }); + await updateProfilingManagerConfig( + appId, + { + heapDumpCollectionEnabled: false, + }, + "heapDumpCollectionEnabled", + ); logLabeledSuccess("crashlytics", "Successfully disabled Crashlytics heap dump collection!"); }