UNPKG

firebase-tools

Version:
196 lines (195 loc) • 9.48 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_LIFECYCLE_RULES = exports.DEFAULT_CORS_RULES = exports.DEFAULT_FILE_TTL_DAYS = exports.DEFAULT_BUCKET_LOCATION = exports.DEFAULT_CORS_ORIGINS = exports.HEAP_DUMP_BUCKET_PREFIX = exports.STORAGE_OBJECT_CREATOR_ROLE = exports.CRASHLYTICS_SERVICE_NAME = void 0; exports.getCrashlyticsV1Client = getCrashlyticsV1Client; exports.parseAndroidHashedId = parseAndroidHashedId; exports.createBucketName = createBucketName; exports.getCrashlyticsP4sa = getCrashlyticsP4sa; exports.resolveAndroidAppId = resolveAndroidAppId; exports.getProfilingManagerConfig = getProfilingManagerConfig; exports.updateProfilingManagerConfig = updateProfilingManagerConfig; exports.ensureHeapDumpStorageBucket = ensureHeapDumpStorageBucket; exports.ensureHeapDumpP4saRole = ensureHeapDumpP4saRole; exports.enableHeapDumpCollection = enableHeapDumpCollection; exports.disableHeapDumpCollection = disableHeapDumpCollection; const apiv2_1 = require("../apiv2"); const api_1 = require("../api"); const error_1 = require("../error"); const logger_1 = require("../logger"); const utils_1 = require("../utils"); const utils_2 = require("./utils"); const storage = require("../gcp/storage"); const resourceManager = require("../gcp/resourceManager"); const serviceusage = require("../gcp/serviceusage"); const apps_1 = require("../management/apps"); function getCrashlyticsV1Client() { return new apiv2_1.Client({ urlPrefix: (0, api_1.crashlyticsApiOrigin)(), apiVersion: "v1", }); } exports.CRASHLYTICS_SERVICE_NAME = "firebasecrashlytics.googleapis.com"; exports.STORAGE_OBJECT_CREATOR_ROLE = "roles/storage.objectCreator"; exports.HEAP_DUMP_BUCKET_PREFIX = "firebasecrashlytics-heap-dumps-"; exports.DEFAULT_CORS_ORIGINS = ["https://console.firebase.google.com"]; exports.DEFAULT_BUCKET_LOCATION = "us-east1"; exports.DEFAULT_FILE_TTL_DAYS = 90; exports.DEFAULT_CORS_RULES = [ { origin: exports.DEFAULT_CORS_ORIGINS, method: ["GET", "HEAD", "OPTIONS"], responseHeader: ["Content-Type", "Access-Control-Allow-Origin", "Content-Length"], }, ]; exports.DEFAULT_LIFECYCLE_RULES = [ { action: { type: "Delete" }, condition: { age: exports.DEFAULT_FILE_TTL_DAYS }, }, ]; const ANDROID_APP_ID_REGEX = /^\d+:(\d+):android:([a-fA-F0-9]+)$/; function parseAndroidHashedId(appId) { const match = ANDROID_APP_ID_REGEX.exec(appId); if (!match) { throw new error_1.FirebaseError(`App ID '${appId}' is not a valid Android app ID (expected format '1:<project-number>:android:<hash>'). ` + "Heap dump collection is only supported for Android apps. " + "Run 'firebase apps:list ANDROID' to view valid Android app IDs for your project."); } return match[2].toLowerCase(); } function createBucketName(appId) { return `${exports.HEAP_DUMP_BUCKET_PREFIX}${parseAndroidHashedId(appId)}`; } function getCrashlyticsP4sa(projectNumber) { return `service-${projectNumber}@gcp-sa-crashlytics.iam.gserviceaccount.com`; } async function resolveAndroidAppId(projectId, options) { if (options.app) { parseAndroidHashedId(options.app); return options.app; } const apps = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.ANDROID); if (apps.length === 0) { throw new error_1.FirebaseError(`No Android apps found in project '${projectId}'. Heap dump collection is only supported for Android apps.\n` + "You can register an Android app using 'firebase apps:create ANDROID <package-name>'"); } if (apps.length === 1) { return apps[0].appId; } if (options.nonInteractive) { throw new error_1.FirebaseError(`Project '${projectId}' has multiple Android apps. Please specify an app ID with '--app <appID>'.`); } const selectedApp = await (0, apps_1.selectAppInteractively)(apps, apps_1.AppPlatform.ANDROID, { message: "Select an Android app:", }); return selectedApp.appId; } async function getProfilingManagerConfig(appId) { const projectNumber = (0, utils_2.parseProjectNumber)(appId); logger_1.logger.debug(`[crashlytics] getProfilingManagerConfig called with appId: ${appId}`); const response = await getCrashlyticsV1Client().request({ method: "GET", headers: { "Content-Type": "application/json", }, path: `/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`, timeout: utils_2.TIMEOUT, }); const config = response.body?.configuration || {}; return { gcsBucket: config.gcsBucket || "", heapDumpCollectionEnabled: config.heapDumpCollectionEnabled ?? false, }; } async function updateProfilingManagerConfig(appId, config) { const projectNumber = (0, utils_2.parseProjectNumber)(appId); logger_1.logger.debug(`[crashlytics] updateProfilingManagerConfig called with appId: ${appId}, config: ${JSON.stringify(config)}`); await getCrashlyticsV1Client().request({ method: "POST", headers: { "Content-Type": "application/json", }, path: `/projects/${projectNumber}/apps/${appId}/appconfig:profilingManager`, body: { projectNumber, gmpAppId: appId, configuration: { gcsBucket: config.gcsBucket ?? "", heapDumpCollectionEnabled: config.heapDumpCollectionEnabled ?? false, }, }, timeout: utils_2.TIMEOUT, }); } async function ensureHeapDumpStorageBucket(projectId, appId, location = exports.DEFAULT_BUCKET_LOCATION) { const bucketName = createBucketName(appId); const projectNumber = (0, utils_2.parseProjectNumber)(appId); try { const bucket = await storage.getBucket(bucketName); if (bucket.projectNumber && String(bucket.projectNumber) !== projectNumber) { throw new error_1.FirebaseError(`Cloud Storage bucket '${bucketName}' already exists and is owned by another project.`); } const patchPayload = {}; const hasConsoleCors = bucket.cors?.some((rule) => exports.DEFAULT_CORS_ORIGINS.every((origin) => rule.origin?.includes(origin))); if (!hasConsoleCors) { patchPayload.cors = [...(bucket.cors ?? []), ...exports.DEFAULT_CORS_RULES]; } const hasDeleteLifecycle = bucket.lifecycle?.rule?.some((rule) => rule.action.type === "Delete" && rule.condition.age !== undefined); if (!hasDeleteLifecycle) { patchPayload.lifecycle = { rule: [...(bucket.lifecycle?.rule ?? []), ...exports.DEFAULT_LIFECYCLE_RULES], }; } if (Object.keys(patchPayload).length > 0) { logger_1.logger.debug(`[crashlytics] Bucket ${bucketName} found without full configuration. Patching ${Object.keys(patchPayload).join(", ")}.`); await storage.patchBucket(bucketName, patchPayload); } return bucketName; } catch (err) { const status = (0, error_1.getErrStatus)(err); if (status !== 404) { throw err; } } logger_1.logger.debug(`[crashlytics] Creating GCS bucket ${bucketName} in ${location}...`); await storage.createBucket(projectId, { name: bucketName, location, cors: exports.DEFAULT_CORS_RULES, lifecycle: { rule: exports.DEFAULT_LIFECYCLE_RULES }, }, true); return bucketName; } async function ensureHeapDumpP4saRole(projectId, projectNumber) { await serviceusage.generateServiceIdentityAndPoll(projectNumber, exports.CRASHLYTICS_SERVICE_NAME, "crashlytics"); const p4saEmail = getCrashlyticsP4sa(projectNumber); const hasRole = await resourceManager.serviceAccountHasRoles(projectId, p4saEmail, [exports.STORAGE_OBJECT_CREATOR_ROLE], true); if (!hasRole) { logger_1.logger.debug(`[crashlytics] Adding ${exports.STORAGE_OBJECT_CREATOR_ROLE} to ${p4saEmail}...`); await resourceManager.addServiceAccountToRoles(projectId, p4saEmail, [exports.STORAGE_OBJECT_CREATOR_ROLE], true); } } async function enableHeapDumpCollection(projectId, appId, location = exports.DEFAULT_BUCKET_LOCATION) { const projectNumber = (0, utils_2.parseProjectNumber)(appId); (0, utils_1.logLabeledBullet)("crashlytics", "Configuring Google Cloud Storage bucket..."); const bucketName = await ensureHeapDumpStorageBucket(projectId, appId, location); (0, utils_1.logLabeledBullet)("crashlytics", "Configuring service agent permissions..."); await ensureHeapDumpP4saRole(projectId, projectNumber); (0, utils_1.logLabeledBullet)("crashlytics", "Enabling Crashlytics heap dump collection..."); await updateProfilingManagerConfig(appId, { gcsBucket: bucketName, heapDumpCollectionEnabled: true, }); (0, utils_1.logLabeledSuccess)("crashlytics", "Successfully enabled Crashlytics heap dump collection!"); return bucketName; } async function disableHeapDumpCollection(appId) { (0, utils_1.logLabeledBullet)("crashlytics", "Disabling Crashlytics heap dump collection..."); const currentConfig = await getProfilingManagerConfig(appId); await updateProfilingManagerConfig(appId, { gcsBucket: currentConfig.gcsBucket, heapDumpCollectionEnabled: false, }); (0, utils_1.logLabeledSuccess)("crashlytics", "Successfully disabled Crashlytics heap dump collection!"); }