diff --git a/src/dataconnect/provisionCloudSql.ts b/src/dataconnect/provisionCloudSql.ts index 4b5fbe4a045..0b22170d61f 100755 --- a/src/dataconnect/provisionCloudSql.ts +++ b/src/dataconnect/provisionCloudSql.ts @@ -1,14 +1,22 @@ -import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin"; -import * as utils from "../utils"; import * as clc from "colorette"; -import { grantRolesToCloudSqlServiceAccount } from "./checkIam"; + +import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin"; import { Instance } from "../gcp/cloudsql/types"; -import { promiseWithSpinner } from "../utils"; import { logger } from "../logger"; -import { freeTrialTermsLink, checkFreeTrialInstanceUsed } from "./freeTrial"; +import { grantRolesToCloudSqlServiceAccount } from "./checkIam"; +import { checkFreeTrialInstanceUsed, freeTrialTermsLink } from "./freeTrial"; +import { promiseWithSpinner } from "../utils"; +import { trackGA4 } from "../track"; +import * as utils from "../utils"; const GOOGLE_ML_INTEGRATION_ROLE = "roles/aiplatform.user"; +type SetupStats = { + action: "get" | "update" | "create"; + databaseVersion?: string; + dataconnectLabel?: cloudSqlAdminClient.DataConnectLabel; +}; + /** Sets up a Cloud SQL instance, database and its permissions. */ export async function setupCloudSql(args: { projectId: string; @@ -16,23 +24,50 @@ export async function setupCloudSql(args: { instanceId: string; databaseId: string; requireGoogleMlIntegration: boolean; + source: "init" | "mcp_init" | "deploy"; dryRun?: boolean; }): Promise { - await upsertInstance({ ...args }); const { projectId, instanceId, requireGoogleMlIntegration, dryRun } = args; + + const startTime = Date.now(); + const stats: SetupStats = { action: "get" }; + let success = false; + try { + await upsertInstance(stats, { ...args }); + success = true; + } finally { + if (!dryRun) { + await trackGA4( + "dataconnect_cloud_sql", + { + source: args.source, + action: success ? stats.action : `${stats.action}_error`, + location: args.location, + enable_google_ml_integration: args.requireGoogleMlIntegration.toString(), + database_version: stats.databaseVersion?.toLowerCase() || "unknown", + dataconnect_label: stats.dataconnectLabel || "unknown", + }, + Date.now() - startTime, + ); + } + } + if (requireGoogleMlIntegration && !dryRun) { await grantRolesToCloudSqlServiceAccount(projectId, instanceId, [GOOGLE_ML_INTEGRATION_ROLE]); } } -async function upsertInstance(args: { - projectId: string; - location: string; - instanceId: string; - databaseId: string; - requireGoogleMlIntegration: boolean; - dryRun?: boolean; -}): Promise { +async function upsertInstance( + stats: SetupStats, + args: { + projectId: string; + location: string; + instanceId: string; + databaseId: string; + requireGoogleMlIntegration: boolean; + dryRun?: boolean; + }, +): Promise { const { projectId, instanceId, requireGoogleMlIntegration, dryRun } = args; try { const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId); @@ -40,6 +75,11 @@ async function upsertInstance(args: { "dataconnect", `Found existing Cloud SQL instance ${clc.bold(instanceId)}.`, ); + stats.databaseVersion = existingInstance.databaseVersion; + stats.dataconnectLabel = existingInstance.settings?.userLabels?.["firebase-data-connect"] as + | cloudSqlAdminClient.DataConnectLabel + | undefined; + const why = getUpdateReason(existingInstance, requireGoogleMlIntegration); if (why) { if (dryRun) { @@ -55,6 +95,7 @@ async function upsertInstance(args: { `Cloud SQL instance ${clc.bold(instanceId)} settings are not compatible with Firebase Data Connect. ` + why, ); + stats.action = "update"; await promiseWithSpinner( () => cloudSqlAdminClient.updateInstanceForDataConnect( @@ -71,7 +112,11 @@ async function upsertInstance(args: { throw err; } // Cloud SQL instance is not found, start its creation. - await createInstance({ ...args }); + stats.action = "create"; + stats.databaseVersion = cloudSqlAdminClient.DEFAULT_DATABASE_VERSION; + const freeTrialUsed = await checkFreeTrialInstanceUsed(projectId); + stats.dataconnectLabel = freeTrialUsed ? "nt" : "ft"; + await createInstance({ ...args, freeTrialLabel: stats.dataconnectLabel }); } } @@ -80,10 +125,11 @@ async function createInstance(args: { location: string; instanceId: string; requireGoogleMlIntegration: boolean; + freeTrialLabel: cloudSqlAdminClient.DataConnectLabel; dryRun?: boolean; }): Promise { - const { projectId, location, instanceId, requireGoogleMlIntegration, dryRun } = args; - const freeTrialUsed = await checkFreeTrialInstanceUsed(projectId); + const { projectId, location, instanceId, requireGoogleMlIntegration, dryRun, freeTrialLabel } = + args; if (dryRun) { utils.logLabeledBullet( "dataconnect", @@ -95,11 +141,11 @@ async function createInstance(args: { location, instanceId, enableGoogleMlIntegration: requireGoogleMlIntegration, - freeTrial: !freeTrialUsed, + freeTrialLabel, }); utils.logLabeledBullet( "dataconnect", - cloudSQLBeingCreated(projectId, instanceId, !freeTrialUsed), + cloudSQLBeingCreated(projectId, instanceId, freeTrialLabel === "ft"), ); } } diff --git a/src/deploy/dataconnect/deploy.ts b/src/deploy/dataconnect/deploy.ts index 6cf5f4cc64b..91cafd11ac1 100644 --- a/src/deploy/dataconnect/deploy.ts +++ b/src/deploy/dataconnect/deploy.ts @@ -96,6 +96,7 @@ export default async function ( instanceId, databaseId, requireGoogleMlIntegration: requiresVector(s.deploymentMetadata), + source: "deploy", }); } }), diff --git a/src/deploy/dataconnect/prepare.ts b/src/deploy/dataconnect/prepare.ts index 869426fcd3a..63fa87cf3ab 100644 --- a/src/deploy/dataconnect/prepare.ts +++ b/src/deploy/dataconnect/prepare.ts @@ -88,6 +88,7 @@ export default async function (context: any, options: DeployOptions): Promise { location: "us-central", instanceId: INSTANCE_ID, enableGoogleMlIntegration: false, - freeTrial: false, + freeTrialLabel: "nt", }), ).to.be.rejectedWith("Cloud SQL free trial instances are not yet available in us-central"); expect(nock.isDone()).to.be.true; @@ -160,7 +160,7 @@ describe("cloudsqladmin", () => { location: "us-central", instanceId: INSTANCE_ID, enableGoogleMlIntegration: false, - freeTrial: false, + freeTrialLabel: "nt", }); expect(nock.isDone()).to.be.true; diff --git a/src/gcp/cloudsql/cloudsqladmin.ts b/src/gcp/cloudsql/cloudsqladmin.ts index 8a7e1e5e97c..39340255336 100755 --- a/src/gcp/cloudsql/cloudsqladmin.ts +++ b/src/gcp/cloudsql/cloudsqladmin.ts @@ -59,12 +59,15 @@ export function instanceConsoleLink(projectId: string, instanceId: string) { return `https://console.cloud.google.com/sql/instances/${instanceId}/overview?project=${projectId}`; } +export type DataConnectLabel = "ft" | "nt"; +export const DEFAULT_DATABASE_VERSION = "POSTGRES_15"; + export async function createInstance(args: { projectId: string; location: string; instanceId: string; enableGoogleMlIntegration: boolean; - freeTrial: boolean; + freeTrialLabel: DataConnectLabel; }): Promise { const databaseFlags = [{ name: "cloudsql.iam_authentication", value: "on" }]; if (args.enableGoogleMlIntegration) { @@ -74,7 +77,7 @@ export async function createInstance(args: { await client.post, Operation>(`projects/${args.projectId}/instances`, { name: args.instanceId, region: args.location, - databaseVersion: "POSTGRES_15", + databaseVersion: DEFAULT_DATABASE_VERSION, settings: { tier: "db-f1-micro", edition: "ENTERPRISE", @@ -84,7 +87,7 @@ export async function createInstance(args: { enableGoogleMlIntegration: args.enableGoogleMlIntegration, databaseFlags, storageAutoResize: false, - userLabels: { "firebase-data-connect": args.freeTrial ? "ft" : "nt" }, + userLabels: { "firebase-data-connect": args.freeTrialLabel }, insightsConfig: { queryInsightsEnabled: true, queryPlansPerMinute: 5, // Match the default settings diff --git a/src/init/features/dataconnect/index.ts b/src/init/features/dataconnect/index.ts index 20d072e99cc..7171ce4b3b1 100644 --- a/src/init/features/dataconnect/index.ts +++ b/src/init/features/dataconnect/index.ts @@ -220,6 +220,7 @@ async function actuateWithInfo( instanceId: info.cloudSqlInstanceId, databaseId: info.cloudSqlDatabase, requireGoogleMlIntegration: false, + source: info.analyticsFlow.startsWith("mcp") ? "mcp_init" : "init", }); } diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts index 8bc6ab36419..d5a9eb5b58c 100644 --- a/src/init/features/dataconnect/sdk.ts +++ b/src/init/features/dataconnect/sdk.ts @@ -165,7 +165,7 @@ export async function actuate(setup: Setup, config: Config) { } finally { let flow = "no_app"; if (sdkInfo.apps.length) { - const platforms = sdkInfo.apps.map(appDescription).sort(); + const platforms = sdkInfo.apps.map((a) => a.platform.toLowerCase()).sort(); flow = `${platforms.join("_")}_app`; } if (fdcInfo) { diff --git a/src/track.ts b/src/track.ts index e521ca11af2..38013f68be9 100644 --- a/src/track.ts +++ b/src/track.ts @@ -24,6 +24,7 @@ type cliEventNames = | "product_init" | "product_init_mcp" | "dataconnect_init" + | "dataconnect_cloud_sql" | "error" | "login" | "api_enabled"