Skip to content

Commit 41b7642

Browse files
[FDC Metrics] Move the app detection into 4 counter labels (#9318)
--------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 902570b commit 41b7642

File tree

7 files changed

+97
-49
lines changed

7 files changed

+97
-49
lines changed

src/command.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,15 @@ export class Command {
224224
});
225225
}
226226
const duration = Math.floor((process.uptime() - start) * 1000);
227-
const trackSuccess = trackGA4("command_execution", {
228-
command_name: this.name,
229-
result: "success",
227+
const trackSuccess = trackGA4(
228+
"command_execution",
229+
{
230+
command_name: this.name,
231+
result: "success",
232+
interactive: getInheritedOption(options, "nonInteractive") ? "false" : "true",
233+
},
230234
duration,
231-
interactive: getInheritedOption(options, "nonInteractive") ? "false" : "true",
232-
});
235+
);
233236
if (!isEmulator) {
234237
await withTimeout(5000, trackSuccess);
235238
} else {

src/dataconnect/provisionCloudSql.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export async function setupCloudSql(args: {
2424
instanceId: string;
2525
databaseId: string;
2626
requireGoogleMlIntegration: boolean;
27-
source: "init" | "mcp_init" | "deploy";
27+
source: "mcp_init" | "init" | "init_sdk" | "deploy";
2828
dryRun?: boolean;
2929
}): Promise<void> {
3030
const { projectId, instanceId, requireGoogleMlIntegration, dryRun } = args;
@@ -37,7 +37,7 @@ export async function setupCloudSql(args: {
3737
success = true;
3838
} finally {
3939
if (!dryRun) {
40-
await trackGA4(
40+
void trackGA4(
4141
"dataconnect_cloud_sql",
4242
{
4343
source: args.source,
@@ -76,9 +76,10 @@ async function upsertInstance(
7676
`Found existing Cloud SQL instance ${clc.bold(instanceId)}.`,
7777
);
7878
stats.databaseVersion = existingInstance.databaseVersion;
79-
stats.dataconnectLabel = existingInstance.settings?.userLabels?.["firebase-data-connect"] as
80-
| cloudSqlAdminClient.DataConnectLabel
81-
| undefined;
79+
stats.dataconnectLabel =
80+
(existingInstance.settings?.userLabels?.[
81+
"firebase-data-connect"
82+
] as cloudSqlAdminClient.DataConnectLabel) || "absent";
8283

8384
const why = getUpdateReason(existingInstance, requireGoogleMlIntegration);
8485
if (why) {

src/init/features/dataconnect/index.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,8 @@ function mockConfig(data: Record<string, any> = {}): Config {
247247
}
248248
function mockRequiredInfo(info: Partial<init.RequiredInfo> = {}): init.RequiredInfo {
249249
return {
250-
analyticsFlow: "test",
250+
source: "init",
251+
flow: "test",
251252
appDescription: "",
252253
serviceId: "test-service",
253254
locationId: "europe-north3",

src/init/features/dataconnect/index.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ const SEED_DATA_TEMPLATE = readTemplateSync("init/dataconnect/seed_data.gql");
5252

5353
export interface RequiredInfo {
5454
// The GA analytics metric to track how developers go through `init dataconnect`.
55-
analyticsFlow: string;
55+
source: "mcp_init" | "init" | "init_sdk";
56+
flow: string;
5657
appDescription: string;
5758
serviceId: string;
5859
locationId: string;
@@ -99,7 +100,8 @@ const templateServiceInfo: ServiceGQL = {
99100
// logic should live here, and _no_ actuation logic should live here.
100101
export async function askQuestions(setup: Setup): Promise<void> {
101102
const info: RequiredInfo = {
102-
analyticsFlow: "cli",
103+
source: "init",
104+
flow: "",
103105
appDescription: "",
104106
serviceId: "",
105107
locationId: "",
@@ -166,20 +168,28 @@ export async function actuate(setup: Setup, config: Config, options: any): Promi
166168
info.locationId = info.locationId || FDC_DEFAULT_REGION;
167169
info.cloudSqlDatabase = info.cloudSqlDatabase || `fdcdb`;
168170

171+
const startTime = Date.now();
169172
try {
170173
await actuateWithInfo(setup, config, info, options);
171174
await sdk.actuate(setup, config);
172175
} finally {
173-
void trackGA4("dataconnect_init", {
174-
flow: info.analyticsFlow,
175-
project_status: setup.projectId
176-
? setup.isBillingEnabled
177-
? info.shouldProvisionCSQL
178-
? "blaze_provisioned_csql"
179-
: "blaze"
180-
: "spark"
181-
: "missing",
182-
});
176+
const sdkInfo = setup.featureInfo?.dataconnectSdk;
177+
void trackGA4(
178+
"dataconnect_init",
179+
{
180+
source: info.source,
181+
flow: info.flow.substring(1), // Trim the leading `_`
182+
project_status: setup.projectId
183+
? setup.isBillingEnabled
184+
? info.shouldProvisionCSQL
185+
? "blaze_provisioned_csql"
186+
: "blaze"
187+
: "spark"
188+
: "missing",
189+
...(sdkInfo ? sdk.initAppCounters(sdkInfo) : {}),
190+
},
191+
Date.now() - startTime,
192+
);
183193
}
184194

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

@@ -220,7 +230,7 @@ async function actuateWithInfo(
220230
instanceId: info.cloudSqlInstanceId,
221231
databaseId: info.cloudSqlDatabase,
222232
requireGoogleMlIntegration: false,
223-
source: info.analyticsFlow.startsWith("mcp") ? "mcp_init" : "init",
233+
source: info.source,
224234
});
225235
}
226236

@@ -233,11 +243,11 @@ async function actuateWithInfo(
233243
}
234244
if (info.serviceGql) {
235245
// Save the downloaded service from the backend.
236-
info.analyticsFlow += "_save_downloaded";
246+
info.flow += "_save_downloaded";
237247
return await writeFiles(config, info, info.serviceGql, options);
238248
}
239249
// Use the static template if it starts from scratch or the existing service has no GQL source.
240-
info.analyticsFlow += "_save_template";
250+
info.flow += "_save_template";
241251
return await writeFiles(config, info, templateServiceInfo, options);
242252
}
243253
const serviceAlreadyExists = !(await createService(projectId, info.locationId, info.serviceId));
@@ -259,7 +269,7 @@ async function actuateWithInfo(
259269
"dataconnect",
260270
`Data Connect Service ${serviceName} already exists. Skip saving them...`,
261271
);
262-
info.analyticsFlow += "_save_gemini_service_already_exists";
272+
info.flow += "_save_gemini_service_already_exists";
263273
return await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options);
264274
}
265275

@@ -301,7 +311,7 @@ async function actuateWithInfo(
301311
],
302312
},
303313
];
304-
info.analyticsFlow += "_save_gemini";
314+
info.flow += "_save_gemini";
305315
await writeFiles(
306316
config,
307317
info,
@@ -312,7 +322,7 @@ async function actuateWithInfo(
312322
logLabeledError("dataconnect", `Operation Generation failed...`);
313323
// GiF generate operation API has stability concerns.
314324
// Fallback to save only the generated schema.
315-
info.analyticsFlow += "_save_gemini_operation_error";
325+
info.flow += "_save_gemini_operation_error";
316326
await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options);
317327
throw err;
318328
}
@@ -492,11 +502,11 @@ async function promptForExistingServices(setup: Setup, info: RequiredInfo): Prom
492502
if (!choice) {
493503
const existingServiceIds = existingServices.map((s) => s.name.split("/").pop()!);
494504
info.serviceId = newUniqueId(defaultServiceId(), existingServiceIds);
495-
info.analyticsFlow += "_pick_new_service";
505+
info.flow += "_pick_new_service";
496506
return;
497507
}
498508
// Choose to use an existing service.
499-
info.analyticsFlow += "_pick_existing_service";
509+
info.flow += "_pick_existing_service";
500510
const serviceName = parseServiceName(choice.name);
501511
info.serviceId = serviceName.serviceId;
502512
info.locationId = serviceName.location;
@@ -618,11 +628,11 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<void
618628
choices,
619629
});
620630
if (info.cloudSqlInstanceId !== "") {
621-
info.analyticsFlow += "_pick_existing_csql";
631+
info.flow += "_pick_existing_csql";
622632
// Infer location if a CloudSQL instance is chosen.
623633
info.locationId = choices.find((c) => c.value === info.cloudSqlInstanceId)!.location;
624634
} else {
625-
info.analyticsFlow += "_pick_new_csql";
635+
info.flow += "_pick_new_csql";
626636
info.cloudSqlInstanceId = await input({
627637
message: `What ID would you like to use for your new CloudSQL instance?`,
628638
default: newUniqueId(

src/init/features/dataconnect/sdk.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,28 +155,60 @@ export async function chooseApp(): Promise<App[]> {
155155
}
156156

157157
export async function actuate(setup: Setup, config: Config) {
158-
const fdcInfo = setup.featureInfo?.dataconnect;
159158
const sdkInfo = setup.featureInfo?.dataconnectSdk;
160159
if (!sdkInfo) {
161160
throw new Error("Data Connect SDK feature RequiredInfo is not provided");
162161
}
162+
const startTime = Date.now();
163163
try {
164164
await actuateWithInfo(setup, config, sdkInfo);
165165
} finally {
166-
let flow = "no_app";
167-
if (sdkInfo.apps.length) {
168-
const platforms = sdkInfo.apps.map((a) => a.platform.toLowerCase()).sort();
169-
flow = `${platforms.join("_")}_app`;
166+
// If `firebase init dataconnect:sdk` is run alone, emit GA stats.
167+
// Otherwise, `firebase init dataconnect` will emit those stats.
168+
const fdcInfo = setup.featureInfo?.dataconnect;
169+
if (!fdcInfo) {
170+
void trackGA4(
171+
"dataconnect_init",
172+
{
173+
flow: "cli_sdk",
174+
project_status: setup.projectId
175+
? setup.isBillingEnabled
176+
? "blaze"
177+
: "spark"
178+
: "missing",
179+
...initAppCounters(sdkInfo),
180+
},
181+
Date.now() - startTime,
182+
);
170183
}
171-
if (fdcInfo) {
172-
fdcInfo.analyticsFlow += `_${flow}`;
173-
} else {
174-
void trackGA4("dataconnect_init", {
175-
project_status: setup.projectId ? (setup.isBillingEnabled ? "blaze" : "spark") : "missing",
176-
flow: `cli_sdk_${flow}`,
177-
});
184+
}
185+
}
186+
187+
export function initAppCounters(info: SdkRequiredInfo): { [key: string]: number } {
188+
const counts = {
189+
num_web_apps: 0,
190+
num_android_apps: 0,
191+
num_ios_apps: 0,
192+
num_flutter_apps: 0,
193+
};
194+
195+
for (const app of info.apps ?? []) {
196+
switch (app.platform) {
197+
case Platform.WEB:
198+
counts.num_web_apps++;
199+
break;
200+
case Platform.ANDROID:
201+
counts.num_android_apps++;
202+
break;
203+
case Platform.IOS:
204+
counts.num_ios_apps++;
205+
break;
206+
case Platform.FLUTTER:
207+
counts.num_flutter_apps++;
208+
break;
178209
}
179210
}
211+
return counts;
180212
}
181213

182214
async function actuateWithInfo(setup: Setup, config: Config, info: SdkRequiredInfo) {

src/init/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export async function init(setup: Setup, config: Config, options: any): Promise<
141141
}
142142

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

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

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

172172
return actuate(setup, config, options);
173173
}

src/mcp/tools/core/init.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ export const init = tool(
183183
}
184184
featuresList.push("dataconnect");
185185
featureInfo.dataconnect = {
186-
analyticsFlow: "mcp",
186+
source: "mcp_init",
187+
flow: "",
187188
appDescription: features.dataconnect.app_description || "",
188189
serviceId: features.dataconnect.service_id || "",
189190
locationId: features.dataconnect.location_id || "",

0 commit comments

Comments
 (0)