Skip to content
Merged
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
13 changes: 8 additions & 5 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@

/**
* Sets an alias for a command.
* @param aliases an alternativre name for the command. Users will be able to call the command via this name.

Check warning on line 72 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Expected @param names to be "alias". Got "aliases"
* @return the command, for chaining.
*/
alias(alias: string): Command {
Expand All @@ -78,10 +78,10 @@
}

/**
* Sets any options for the command.

Check warning on line 81 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Expected only 0 line after block description
*
* @example
* command.option("-d, --debug", "turn on debugging", false)

Check warning on line 84 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Expected no lines between tags
*
* @param args the commander-style option definition.
* @return the command, for chaining.
Expand All @@ -93,10 +93,10 @@
}

/**
* Sets up --force flag for the command.

Check warning on line 96 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Expected only 0 line after block description
*
* @param message overrides the description for --force for this command
* @returns the command, for chaining

Check warning on line 99 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid JSDoc tag (preference). Replace "returns" JSDoc tag with "return"
*/
withForce(message?: string): Command {
this.options.push(["-f, --force", message || "automatically accept all interactive prompts"]);
Expand All @@ -120,7 +120,7 @@
*
* This text is displayed when:
* - the `--help` flag is passed to the command, or
* - the `help <command>` command is used.

Check warning on line 123 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Expected only 0 line after block description
*
* @param t the human-readable help text.
* @return the command, for chaining.
Expand Down Expand Up @@ -157,8 +157,8 @@
cmd.aliases(this.aliases);
}
this.options.forEach((args) => {
const flags = args.shift();

Check warning on line 160 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
cmd.option(flags, ...args);

Check warning on line 161 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe spread of an `any` array type

Check warning on line 161 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `string`
});

if (this.helpText) {
Expand All @@ -169,7 +169,7 @@
}

// See below about using this private property
this.positionalArgs = cmd._args;

Check warning on line 172 in src/command.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

// args is an array of all the arguments provided for the command PLUS the
// options object as provided by Commander (on the end).
Expand Down Expand Up @@ -224,12 +224,15 @@
});
}
const duration = Math.floor((process.uptime() - start) * 1000);
const trackSuccess = trackGA4("command_execution", {
command_name: this.name,
result: "success",
const trackSuccess = trackGA4(
"command_execution",
{
command_name: this.name,
result: "success",
interactive: getInheritedOption(options, "nonInteractive") ? "false" : "true",
},
duration,
interactive: getInheritedOption(options, "nonInteractive") ? "false" : "true",
});
);
if (!isEmulator) {
await withTimeout(5000, trackSuccess);
} else {
Expand Down
11 changes: 6 additions & 5 deletions src/dataconnect/provisionCloudSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function setupCloudSql(args: {
instanceId: string;
databaseId: string;
requireGoogleMlIntegration: boolean;
source: "init" | "mcp_init" | "deploy";
source: "mcp_init" | "init" | "init_sdk" | "deploy";
dryRun?: boolean;
}): Promise<void> {
const { projectId, instanceId, requireGoogleMlIntegration, dryRun } = args;
Expand All @@ -37,7 +37,7 @@ export async function setupCloudSql(args: {
success = true;
} finally {
if (!dryRun) {
await trackGA4(
void trackGA4(
"dataconnect_cloud_sql",
{
source: args.source,
Expand Down Expand Up @@ -76,9 +76,10 @@ async function upsertInstance(
`Found existing Cloud SQL instance ${clc.bold(instanceId)}.`,
);
stats.databaseVersion = existingInstance.databaseVersion;
stats.dataconnectLabel = existingInstance.settings?.userLabels?.["firebase-data-connect"] as
| cloudSqlAdminClient.DataConnectLabel
| undefined;
stats.dataconnectLabel =
(existingInstance.settings?.userLabels?.[
"firebase-data-connect"
] as cloudSqlAdminClient.DataConnectLabel) || "absent";

const why = getUpdateReason(existingInstance, requireGoogleMlIntegration);
if (why) {
Expand Down
3 changes: 2 additions & 1 deletion src/init/features/dataconnect/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ function mockConfig(data: Record<string, any> = {}): Config {
}
function mockRequiredInfo(info: Partial<init.RequiredInfo> = {}): init.RequiredInfo {
return {
analyticsFlow: "test",
source: "init",
flow: "test",
appDescription: "",
serviceId: "test-service",
locationId: "europe-north3",
Expand Down
56 changes: 33 additions & 23 deletions src/init/features/dataconnect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ const SEED_DATA_TEMPLATE = readTemplateSync("init/dataconnect/seed_data.gql");

export interface RequiredInfo {
// The GA analytics metric to track how developers go through `init dataconnect`.
analyticsFlow: string;
source: "mcp_init" | "init" | "init_sdk";
flow: string;
appDescription: string;
serviceId: string;
locationId: string;
Expand Down Expand Up @@ -99,7 +100,8 @@ const templateServiceInfo: ServiceGQL = {
// logic should live here, and _no_ actuation logic should live here.
export async function askQuestions(setup: Setup): Promise<void> {
const info: RequiredInfo = {
analyticsFlow: "cli",
source: "init",
flow: "",
appDescription: "",
serviceId: "",
locationId: "",
Expand Down Expand Up @@ -166,20 +168,28 @@ export async function actuate(setup: Setup, config: Config, options: any): Promi
info.locationId = info.locationId || FDC_DEFAULT_REGION;
info.cloudSqlDatabase = info.cloudSqlDatabase || `fdcdb`;

const startTime = Date.now();
try {
await actuateWithInfo(setup, config, info, options);
await sdk.actuate(setup, config);
} finally {
void trackGA4("dataconnect_init", {
flow: info.analyticsFlow,
project_status: setup.projectId
? setup.isBillingEnabled
? info.shouldProvisionCSQL
? "blaze_provisioned_csql"
: "blaze"
: "spark"
: "missing",
});
const sdkInfo = setup.featureInfo?.dataconnectSdk;
void trackGA4(
"dataconnect_init",
{
source: info.source,
flow: info.flow.substring(1), // Trim the leading `_`
project_status: setup.projectId
? setup.isBillingEnabled
? info.shouldProvisionCSQL
? "blaze_provisioned_csql"
: "blaze"
: "spark"
: "missing",
...(sdkInfo ? sdk.initAppCounters(sdkInfo) : {}),
},
Date.now() - startTime,
);
}

if (info.appDescription) {
Expand All @@ -206,7 +216,7 @@ async function actuateWithInfo(
const projectId = setup.projectId;
if (!projectId) {
// If no project is present, just save the template files.
info.analyticsFlow += "_save_template";
info.flow += "_save_template";
return await writeFiles(config, info, templateServiceInfo, options);
}

Expand All @@ -220,7 +230,7 @@ async function actuateWithInfo(
instanceId: info.cloudSqlInstanceId,
databaseId: info.cloudSqlDatabase,
requireGoogleMlIntegration: false,
source: info.analyticsFlow.startsWith("mcp") ? "mcp_init" : "init",
source: info.source,
});
}

Expand All @@ -233,11 +243,11 @@ async function actuateWithInfo(
}
if (info.serviceGql) {
// Save the downloaded service from the backend.
info.analyticsFlow += "_save_downloaded";
info.flow += "_save_downloaded";
return await writeFiles(config, info, info.serviceGql, options);
}
// Use the static template if it starts from scratch or the existing service has no GQL source.
info.analyticsFlow += "_save_template";
info.flow += "_save_template";
return await writeFiles(config, info, templateServiceInfo, options);
}
const serviceAlreadyExists = !(await createService(projectId, info.locationId, info.serviceId));
Expand All @@ -259,7 +269,7 @@ async function actuateWithInfo(
"dataconnect",
`Data Connect Service ${serviceName} already exists. Skip saving them...`,
);
info.analyticsFlow += "_save_gemini_service_already_exists";
info.flow += "_save_gemini_service_already_exists";
return await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options);
}

Expand Down Expand Up @@ -301,7 +311,7 @@ async function actuateWithInfo(
],
},
];
info.analyticsFlow += "_save_gemini";
info.flow += "_save_gemini";
await writeFiles(
config,
info,
Expand All @@ -312,7 +322,7 @@ async function actuateWithInfo(
logLabeledError("dataconnect", `Operation Generation failed...`);
// GiF generate operation API has stability concerns.
// Fallback to save only the generated schema.
info.analyticsFlow += "_save_gemini_operation_error";
info.flow += "_save_gemini_operation_error";
await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options);
throw err;
}
Expand Down Expand Up @@ -492,11 +502,11 @@ async function promptForExistingServices(setup: Setup, info: RequiredInfo): Prom
if (!choice) {
const existingServiceIds = existingServices.map((s) => s.name.split("/").pop()!);
info.serviceId = newUniqueId(defaultServiceId(), existingServiceIds);
info.analyticsFlow += "_pick_new_service";
info.flow += "_pick_new_service";
return;
}
// Choose to use an existing service.
info.analyticsFlow += "_pick_existing_service";
info.flow += "_pick_existing_service";
const serviceName = parseServiceName(choice.name);
info.serviceId = serviceName.serviceId;
info.locationId = serviceName.location;
Expand Down Expand Up @@ -618,11 +628,11 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<void
choices,
});
if (info.cloudSqlInstanceId !== "") {
info.analyticsFlow += "_pick_existing_csql";
info.flow += "_pick_existing_csql";
// Infer location if a CloudSQL instance is chosen.
info.locationId = choices.find((c) => c.value === info.cloudSqlInstanceId)!.location;
} else {
info.analyticsFlow += "_pick_new_csql";
info.flow += "_pick_new_csql";
info.cloudSqlInstanceId = await input({
message: `What ID would you like to use for your new CloudSQL instance?`,
default: newUniqueId(
Expand Down
56 changes: 44 additions & 12 deletions src/init/features/dataconnect/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,28 +155,60 @@ export async function chooseApp(): Promise<App[]> {
}

export async function actuate(setup: Setup, config: Config) {
const fdcInfo = setup.featureInfo?.dataconnect;
const sdkInfo = setup.featureInfo?.dataconnectSdk;
if (!sdkInfo) {
throw new Error("Data Connect SDK feature RequiredInfo is not provided");
}
const startTime = Date.now();
try {
await actuateWithInfo(setup, config, sdkInfo);
} finally {
let flow = "no_app";
if (sdkInfo.apps.length) {
const platforms = sdkInfo.apps.map((a) => a.platform.toLowerCase()).sort();
flow = `${platforms.join("_")}_app`;
// If `firebase init dataconnect:sdk` is run alone, emit GA stats.
// Otherwise, `firebase init dataconnect` will emit those stats.
const fdcInfo = setup.featureInfo?.dataconnect;
if (!fdcInfo) {
void trackGA4(
"dataconnect_init",
{
flow: "cli_sdk",
project_status: setup.projectId
? setup.isBillingEnabled
? "blaze"
: "spark"
: "missing",
...initAppCounters(sdkInfo),
},
Date.now() - startTime,
);
}
if (fdcInfo) {
fdcInfo.analyticsFlow += `_${flow}`;
} else {
void trackGA4("dataconnect_init", {
project_status: setup.projectId ? (setup.isBillingEnabled ? "blaze" : "spark") : "missing",
flow: `cli_sdk_${flow}`,
});
}
}

export function initAppCounters(info: SdkRequiredInfo): { [key: string]: number } {
const counts = {
num_web_apps: 0,
num_android_apps: 0,
num_ios_apps: 0,
num_flutter_apps: 0,
};

for (const app of info.apps ?? []) {
switch (app.platform) {
case Platform.WEB:
counts.num_web_apps++;
break;
case Platform.ANDROID:
counts.num_android_apps++;
break;
case Platform.IOS:
counts.num_ios_apps++;
break;
case Platform.FLUTTER:
counts.num_flutter_apps++;
break;
}
}
return counts;
}

async function actuateWithInfo(setup: Setup, config: Config, info: SdkRequiredInfo) {
Expand Down
4 changes: 2 additions & 2 deletions src/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export async function init(setup: Setup, config: Config, options: any): Promise<
}

const duration = Math.floor((process.uptime() - start) * 1000);
await trackGA4("product_init", { feature: nextFeature }, duration);
void trackGA4("product_init", { feature: nextFeature }, duration);

return init(setup, config, options);
}
Expand All @@ -167,7 +167,7 @@ export async function actuate(setup: Setup, config: Config, options: any): Promi
}

const duration = Math.floor((process.uptime() - start) * 1000);
await trackGA4("product_init_mcp", { feature: nextFeature }, duration);
void trackGA4("product_init_mcp", { feature: nextFeature }, duration);

return actuate(setup, config, options);
}
Expand Down
3 changes: 2 additions & 1 deletion src/mcp/tools/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ export const init = tool(
}
featuresList.push("dataconnect");
featureInfo.dataconnect = {
analyticsFlow: "mcp",
source: "mcp_init",
flow: "",
appDescription: features.dataconnect.app_description || "",
serviceId: features.dataconnect.service_id || "",
locationId: features.dataconnect.location_id || "",
Expand Down
Loading