Skip to content

Commit 4672d62

Browse files
[FDC] feat: Add analytics for dataconnect deploy (#9303)
* feat: Add analytics for dataconnect deploy Adds a new analytics event, `dataconnect_deploy`, to track the outcomes of various steps within the `firebase deploy` command for Data Connect services. This includes tracking for: - Billing errors - Build errors and warnings - Service creation and deletion - Schema migration events - Connector updates - User choices in interactive prompts Also includes tracking for the --dry-run and --force flags. * ok * stats * m * m * refine * m * m * removelogging * m * Update index.ts * m * use void trackGA --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 34d9d4e commit 4672d62

File tree

11 files changed

+208
-71
lines changed

11 files changed

+208
-71
lines changed

src/dataconnect/build.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { DataConnectBuildArgs, DataConnectEmulator } from "../emulator/dataconnectEmulator";
2-
import { Options } from "../options";
32
import { FirebaseError } from "../error";
43
import { select } from "../prompt";
54
import * as utils from "../utils";
65
import { prettify, prettifyTable } from "./graphqlError";
7-
import { DeploymentMetadata, GraphqlError } from "./types";
86
import { getProjectDefaultAccount } from "../auth";
7+
import { DeployOptions } from "../deploy";
8+
import { DeployStats } from "../deploy/dataconnect/context";
9+
import { DeploymentMetadata, GraphqlError } from "./types";
910

1011
export async function build(
11-
options: Options,
12+
options: DeployOptions,
1213
configDir: string,
13-
dryRun?: boolean,
14+
deployStats: DeployStats,
1415
): Promise<DeploymentMetadata> {
1516
const account = getProjectDefaultAccount(options.projectRoot);
1617
const args: DataConnectBuildArgs = { configDir, account };
@@ -19,7 +20,24 @@ export async function build(
1920
}
2021
const buildResult = await DataConnectEmulator.build(args);
2122
if (buildResult?.errors?.length) {
22-
await handleBuildErrors(buildResult.errors, options.nonInteractive, options.force, dryRun);
23+
buildResult.errors.forEach((e) => {
24+
if (e.extensions?.warningLevel) {
25+
let key = e.extensions.warningLevel.toLowerCase();
26+
const msgSp = e.message.split(": ");
27+
if (msgSp.length >= 2) {
28+
key += `_${msgSp[0].toLowerCase()}`;
29+
}
30+
deployStats.numBuildWarnings.set(key, (deployStats.numBuildWarnings.get(key) ?? 0) + 1);
31+
} else {
32+
deployStats.numBuildErrors += 1;
33+
}
34+
});
35+
await handleBuildErrors(
36+
buildResult.errors,
37+
options.nonInteractive,
38+
options.force,
39+
options.dryRun,
40+
);
2341
}
2442
return buildResult?.metadata ?? {};
2543
}

src/dataconnect/schemaMigration.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { DEFAULT_SCHEMA, firebaseowner } from "../gcp/cloudsql/permissions";
2121
import { select, confirm } from "../prompt";
2222
import { logger } from "../logger";
2323
import { Schema } from "./types";
24+
import { DeployStats } from "../deploy/dataconnect/context";
2425
import { Options } from "../options";
2526
import { FirebaseError } from "../error";
2627
import { logLabeledBullet, logLabeledWarning, logLabeledSuccess } from "../utils";
@@ -150,8 +151,9 @@ export async function migrateSchema(args: {
150151
/** true for `dataconnect:sql:migrate`, false for `deploy` */
151152
validateOnly: boolean;
152153
schemaValidation?: SchemaValidation;
154+
stats?: DeployStats;
153155
}): Promise<Diff[]> {
154-
const { options, schema, validateOnly, schemaValidation } = args;
156+
const { options, schema, validateOnly, schemaValidation, stats } = args;
155157

156158
// If the schema validation mode is unset, we prompt COMPATIBLE SQL diffs and then STRICT diffs.
157159
let validationMode: SchemaValidation = schemaValidation ?? "COMPATIBLE";
@@ -170,6 +172,9 @@ export async function migrateSchema(args: {
170172
// Check if Cloud SQL instance is still being created.
171173
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
172174
if (existingInstance.state === "PENDING_CREATE") {
175+
if (stats) {
176+
stats.numSchemaSkippedDueToPendingCreate++;
177+
}
173178
const postgresql = schema.datasources.find((d) => d.postgresql)?.postgresql;
174179
if (!postgresql) {
175180
throw new FirebaseError(
@@ -211,6 +216,14 @@ export async function migrateSchema(args: {
211216
}
212217
throw err;
213218
}
219+
if (stats) {
220+
if (incompatible) {
221+
stats.numSchemaSqlDiffs += incompatible.diffs.length;
222+
}
223+
if (invalidConnectors.length) {
224+
stats.numSchemaInvalidConnectors += invalidConnectors.length;
225+
}
226+
}
214227

215228
const migrationMode = await promptForSchemaMigration(
216229
options,
@@ -259,11 +272,13 @@ export async function migrateSchema(args: {
259272
}
260273
// Parse and handle failed precondition errors, then retry.
261274
const incompatible = errors.getIncompatibleSchemaError(err);
262-
const invalidConnectors = errors.getInvalidConnectors(err);
263-
if (!incompatible && !invalidConnectors.length) {
275+
if (!incompatible) {
264276
// If we got a different type of error, throw it
265277
throw err;
266278
}
279+
if (stats && incompatible) {
280+
stats.numSchemaSqlDiffs += incompatible.diffs.length;
281+
}
267282

268283
const migrationMode = await promptForSchemaMigration(
269284
options,

src/deploy/dataconnect/context.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ResourceFilter } from "../../dataconnect/filters";
2+
import { ServiceInfo } from "../../dataconnect/types";
3+
import { AnalyticsParams } from "../../track";
4+
5+
export interface Context {
6+
dataconnect?: {
7+
serviceInfos: ServiceInfo[];
8+
filters?: ResourceFilter[];
9+
deployStats: DeployStats;
10+
};
11+
}
12+
13+
export interface DeployStats {
14+
// prepare.ts
15+
missingBilling?: boolean;
16+
numBuildErrors: number;
17+
numBuildWarnings: Map<string, number>;
18+
19+
// deploy.ts
20+
numServiceCreated: number;
21+
numServiceDeleted: number;
22+
23+
// release.ts
24+
numSchemaMigrated: number;
25+
numConnectorUpdatedBeforeSchema: number;
26+
numConnectorUpdatedAfterSchema: number;
27+
28+
// migrateSchema.ts
29+
numSchemaSkippedDueToPendingCreate: number;
30+
numSchemaSqlDiffs: number;
31+
numSchemaInvalidConnectors: number;
32+
}
33+
34+
export function initDeployStats(): DeployStats {
35+
return {
36+
numBuildErrors: 0,
37+
numBuildWarnings: new Map<string, number>(),
38+
numServiceCreated: 0,
39+
numServiceDeleted: 0,
40+
numSchemaMigrated: 0,
41+
numConnectorUpdatedBeforeSchema: 0,
42+
numConnectorUpdatedAfterSchema: 0,
43+
numSchemaSkippedDueToPendingCreate: 0,
44+
numSchemaSqlDiffs: 0,
45+
numSchemaInvalidConnectors: 0,
46+
};
47+
}
48+
49+
export function deployStatsParams(stats: DeployStats): AnalyticsParams {
50+
const buildWarnings: AnalyticsParams = {};
51+
for (const [type, num] of stats.numBuildWarnings.entries()) {
52+
buildWarnings[`num_build_warnings_${type}`] = num;
53+
}
54+
return {
55+
missing_billing: (!!stats.missingBilling).toString(),
56+
num_service_created: stats.numServiceCreated,
57+
num_service_deleted: stats.numServiceDeleted,
58+
num_schema_migrated: stats.numSchemaMigrated,
59+
num_connector_updated_before_schema: stats.numConnectorUpdatedBeforeSchema,
60+
num_connector_updated_after_schema: stats.numConnectorUpdatedAfterSchema,
61+
num_schema_skipped_due_to_pending_create: stats.numSchemaSkippedDueToPendingCreate,
62+
num_schema_sql_diffs: stats.numSchemaSqlDiffs,
63+
num_schema_invalid_connectors: stats.numSchemaInvalidConnectors,
64+
num_build_errors: stats.numBuildErrors,
65+
...buildWarnings,
66+
};
67+
}

src/deploy/dataconnect/deploy.spec.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as ensureApiEnabled from "../../ensureApiEnabled";
99
import * as prompt from "../../prompt";
1010
import * as poller from "../../operation-poller";
1111
import { dataconnectOrigin } from "../../api";
12+
import { initDeployStats } from "./context";
1213

1314
describe("dataconnect deploy", () => {
1415
let sandbox: sinon.SinonSandbox;
@@ -47,7 +48,7 @@ describe("dataconnect deploy", () => {
4748
dataConnectYaml: { serviceId: "s1" },
4849
},
4950
];
50-
const context = { dataconnect: { serviceInfos } };
51+
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
5152
const options = {} as any;
5253

5354
await deploy.default(context as any, options);
@@ -66,7 +67,7 @@ describe("dataconnect deploy", () => {
6667

6768
confirmStub.resolves(true);
6869
const serviceInfos: any[] = [];
69-
const context = { dataconnect: { serviceInfos } };
70+
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
7071
const options = {} as any;
7172

7273
await deploy.default(context as any, options);
@@ -83,7 +84,7 @@ describe("dataconnect deploy", () => {
8384

8485
confirmStub.resolves(false);
8586
const serviceInfos: any[] = [];
86-
const context = { dataconnect: { serviceInfos } };
87+
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
8788
const options = {} as any;
8889

8990
await deploy.default(context as any, options);
@@ -116,7 +117,7 @@ describe("dataconnect deploy", () => {
116117
dataConnectYaml: { serviceId: "s1" },
117118
},
118119
];
119-
const context = { dataconnect: { serviceInfos } };
120+
const context = { dataconnect: { serviceInfos, deployStats: initDeployStats() } };
120121
const options = {} as any;
121122

122123
await deploy.default(context as any, options);
@@ -131,7 +132,9 @@ describe("dataconnect deploy", () => {
131132
.reply(200, { services: existingServices });
132133

133134
const serviceInfos: any[] = [];
134-
const context = { dataconnect: { serviceInfos, filters: [{ serviceId: "s1" }] } };
135+
const context = {
136+
dataconnect: { serviceInfos, filters: [{ serviceId: "s1" }], deployStats: initDeployStats() },
137+
};
135138
const options = {} as any;
136139

137140
await deploy.default(context as any, options);

src/deploy/dataconnect/deploy.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,23 @@ import { ResourceFilter } from "../../dataconnect/filters";
99
import { vertexAIOrigin } from "../../api";
1010
import * as ensureApiEnabled from "../../ensureApiEnabled";
1111
import { confirm } from "../../prompt";
12+
import { Context } from "./context";
1213

1314
/**
1415
* Checks for and creates a Firebase DataConnect service, if needed.
1516
* TODO: Also checks for and creates a CloudSQL instance and database.
1617
* @param context The deploy context.
1718
* @param options The CLI options object.
1819
*/
19-
export default async function (
20-
context: {
21-
dataconnect: {
22-
serviceInfos: ServiceInfo[];
23-
filters?: ResourceFilter[];
24-
};
25-
},
26-
options: Options,
27-
): Promise<void> {
20+
export default async function (context: Context, options: Options): Promise<void> {
21+
const dataconnect = context.dataconnect;
22+
if (!dataconnect) {
23+
throw new Error("dataconnect.prepare must be run before dataconnect.deploy");
24+
}
2825
const projectId = needProjectId(options);
29-
const serviceInfos = context.dataconnect.serviceInfos as ServiceInfo[];
26+
const serviceInfos = dataconnect.serviceInfos as ServiceInfo[];
3027
const services = await client.listAllServices(projectId);
31-
const filters = context.dataconnect.filters;
28+
const filters = dataconnect.filters;
3229

3330
if (
3431
serviceInfos.some((si) => {
@@ -41,12 +38,17 @@ export default async function (
4138
const servicesToCreate = serviceInfos
4239
.filter((si) => !services.some((s) => matches(si, s)))
4340
.filter((si) => {
44-
return !filters || filters?.some((f) => si.dataConnectYaml.serviceId === f.serviceId);
41+
return (
42+
!filters ||
43+
filters?.some((f: ResourceFilter) => si.dataConnectYaml.serviceId === f.serviceId)
44+
);
4545
});
46+
dataconnect.deployStats.numServiceCreated = servicesToCreate.length;
4647

4748
const servicesToDelete = filters
4849
? []
4950
: services.filter((s) => !serviceInfos.some((si) => matches(si, s)));
51+
dataconnect.deployStats.numServiceDeleted = servicesToDelete.length;
5052
await Promise.all(
5153
servicesToCreate.map(async (s) => {
5254
const { projectId, locationId, serviceId } = splitName(s.serviceName);
@@ -80,7 +82,10 @@ export default async function (
8082
await Promise.all(
8183
serviceInfos
8284
.filter((si) => {
83-
return !filters || filters?.some((f) => si.dataConnectYaml.serviceId === f.serviceId);
85+
return (
86+
!filters ||
87+
filters?.some((f: ResourceFilter) => si.dataConnectYaml.serviceId === f.serviceId)
88+
);
8489
})
8590
.map(async (s) => {
8691
const postgresDatasource = s.schema.datasources.find((d) => d.postgresql);

src/deploy/dataconnect/prepare.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe("dataconnect prepare", () => {
5252
dataconnect: {
5353
serviceInfos: [],
5454
filters: undefined,
55+
deployStats: (context as any).dataconnect.deployStats,
5556
},
5657
});
5758
});
@@ -77,6 +78,7 @@ describe("dataconnect prepare", () => {
7778
dataconnect: {
7879
serviceInfos: serviceInfos,
7980
filters: undefined,
81+
deployStats: (context as any).dataconnect.deployStats,
8082
},
8183
});
8284
});

src/deploy/dataconnect/prepare.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,29 @@ import { FirebaseError } from "../../error";
1717
import { requiresVector } from "../../dataconnect/types";
1818
import { diffSchema } from "../../dataconnect/schemaMigration";
1919
import { upgradeInstructions } from "../../dataconnect/freeTrial";
20+
import { Context, initDeployStats } from "./context";
2021

2122
/**
2223
* Prepares for a Firebase DataConnect deployment by loading schemas and connectors from file.
2324
* @param context The deploy context.
2425
* @param options The CLI options object.
2526
*/
26-
export default async function (context: any, options: DeployOptions): Promise<void> {
27+
export default async function (context: Context, options: DeployOptions): Promise<void> {
2728
const projectId = needProjectId(options);
29+
await ensureApis(projectId);
30+
context.dataconnect = {
31+
serviceInfos: await loadAll(projectId, options.config),
32+
filters: getResourceFilters(options),
33+
deployStats: initDeployStats(),
34+
};
35+
const { serviceInfos, filters, deployStats } = context.dataconnect;
2836
if (!(await checkBillingEnabled(projectId))) {
37+
deployStats.missingBilling = true;
2938
throw new FirebaseError(upgradeInstructions(projectId));
3039
}
31-
await ensureApis(projectId);
3240
await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
33-
const filters = getResourceFilters(options);
34-
const serviceInfos = await loadAll(projectId, options.config);
3541
for (const si of serviceInfos) {
36-
si.deploymentMetadata = await build(options, si.sourceDirectory, options.dryRun);
42+
si.deploymentMetadata = await build(options, si.sourceDirectory, deployStats);
3743
}
3844
const unmatchedFilters = filters?.filter((f) => {
3945
// filter out all filters that match no service
@@ -54,10 +60,6 @@ export default async function (context: any, options: DeployOptions): Promise<vo
5460
);
5561
// TODO: Did you mean?
5662
}
57-
context.dataconnect = {
58-
serviceInfos,
59-
filters,
60-
};
6163
utils.logLabeledBullet("dataconnect", `Successfully compiled schema and connectors`);
6264
if (options.dryRun) {
6365
for (const si of serviceInfos) {

0 commit comments

Comments
 (0)