Skip to content

Commit 197506a

Browse files
authored
Migrates functions metrics to GA4 (#6053)
* track functions metrics with ga4 * remove old track() calls
1 parent 654e884 commit 197506a

File tree

10 files changed

+159
-147
lines changed

10 files changed

+159
-147
lines changed

src/deploy/functions/args.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as backend from "./backend";
22
import * as gcfV2 from "../../gcp/cloudfunctionsv2";
33
import * as projectConfig from "../../functions/projectConfig";
44
import * as deployHelper from "./functionsDeployHelper";
5+
import { Runtime } from "./runtimes";
56

67
// These types should probably be in a root deploy.ts, but we can only boil the ocean one bit at a time.
78
interface CodebasePayload {
@@ -49,6 +50,19 @@ export interface Context {
4950
gcfV1: string[];
5051
gcfV2: string[];
5152
};
53+
54+
// Tracks metrics about codebase deployments to send to GA4
55+
codebaseDeployEvents?: Record<string, CodebaseDeployEvent>;
56+
}
57+
58+
export interface CodebaseDeployEvent {
59+
params?: "env_only" | "with_secrets" | "none";
60+
runtime?: Runtime;
61+
runtime_notice?: string;
62+
fn_deploy_num_successes: number;
63+
fn_deploy_num_failures: number;
64+
fn_deploy_num_canceled: number;
65+
fn_deploy_num_skipped: number;
5266
}
5367

5468
export interface FirebaseConfig {

src/deploy/functions/build.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { FirebaseError } from "../../error";
66
import { assertExhaustive, mapObject, nullsafeVisitor } from "../../functional";
77
import { UserEnvsOpts, writeUserEnvs } from "../../functions/env";
88
import { FirebaseConfig } from "./args";
9+
import { Runtime } from "./runtimes";
910

1011
/* The union of a customer-controlled deployment and potentially deploy-time defined parameters */
1112
export interface Build {
1213
requiredAPIs: RequiredApi[];
1314
endpoints: Record<string, Endpoint>;
1415
params: params.Param[];
16+
runtime?: Runtime;
1517
}
1618

1719
/**

src/deploy/functions/ensure.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { logLabeledBullet, logLabeledSuccess } from "../../utils";
88
import { ensureServiceAgentRole } from "../../gcp/secretManager";
99
import { getFirebaseProject } from "../../management/projects";
1010
import { assertExhaustive } from "../../functional";
11-
import { track } from "../../track";
1211
import * as backend from "./backend";
1312

1413
const FAQ_URL = "https://firebase.google.com/support/faq#functions-runtime";
@@ -37,7 +36,6 @@ export async function defaultServiceAccount(e: backend.Endpoint): Promise<string
3736
}
3837

3938
function nodeBillingError(projectId: string): FirebaseError {
40-
void track("functions_runtime_notices", "nodejs10_billing_error");
4139
return new FirebaseError(
4240
`Cloud Functions deployment requires the pay-as-you-go (Blaze) billing plan. To upgrade your project, visit the following URL:
4341
@@ -51,7 +49,6 @@ ${FAQ_URL}`,
5149
}
5250

5351
function nodePermissionError(projectId: string): FirebaseError {
54-
void track("functions_runtime_notices", "nodejs10_permission_error");
5552
return new FirebaseError(`Cloud Functions deployment requires the Cloud Build API to be enabled. The current credentials do not have permission to enable APIs for project ${clc.bold(
5653
projectId
5754
)}.

src/deploy/functions/prepare.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { logLabeledBullet } from "../../utils";
2121
import { getFunctionsConfig, prepareFunctionsUpload } from "./prepareFunctionsUpload";
2222
import { promptForFailurePolicies, promptForMinInstances } from "./prompts";
2323
import { needProjectId, needProjectNumber } from "../../projectUtils";
24-
import { track } from "../../track";
2524
import { logger } from "../../logger";
2625
import { ensureTriggerRegions } from "./triggerRegionHelper";
2726
import { ensureServiceAgentRoles } from "./checkIam";
@@ -38,11 +37,6 @@ import { allEndpoints, Backend } from "./backend";
3837
import { assertExhaustive } from "../../functional";
3938

4039
export const EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE";
41-
function hasUserConfig(config: Record<string, unknown>): boolean {
42-
// "firebase" key is always going to exist in runtime config.
43-
// If any other key exists, we can assume that user is using runtime config.
44-
return Object.keys(config).length > 1;
45-
}
4640

4741
/**
4842
* Prepare functions codebases for deploy.
@@ -88,6 +82,8 @@ export async function prepare(
8882
runtimeConfig = { ...runtimeConfig, ...(await getFunctionsConfig(projectId)) };
8983
}
9084

85+
context.codebaseDeployEvents = {};
86+
9187
// ===Phase 1. Load codebases from source.
9288
const wantBuilds = await loadCodebases(
9389
context.config,
@@ -155,15 +151,23 @@ export async function prepare(
155151
codebaseUsesEnvs.push(codebase);
156152
}
157153

154+
context.codebaseDeployEvents[codebase] = {
155+
fn_deploy_num_successes: 0,
156+
fn_deploy_num_failures: 0,
157+
fn_deploy_num_canceled: 0,
158+
fn_deploy_num_skipped: 0,
159+
};
160+
158161
if (wantBuild.params.length > 0) {
159162
if (wantBuild.params.every((p) => p.type !== "secret")) {
160-
void track("functions_params_in_build", "env_only");
163+
context.codebaseDeployEvents[codebase].params = "env_only";
161164
} else {
162-
void track("functions_params_in_build", "with_secrets");
165+
context.codebaseDeployEvents[codebase].params = "with_secrets";
163166
}
164167
} else {
165-
void track("functions_params_in_build", "none");
168+
context.codebaseDeployEvents[codebase].params = "none";
166169
}
170+
context.codebaseDeployEvents[codebase].runtime = wantBuild.runtime;
167171
}
168172

169173
// ===Phase 2.5. Before proceeding further, let's make sure that we don't have conflicting function names.
@@ -214,18 +218,6 @@ export async function prepare(
214218
inferBlockingDetails(wantBackend);
215219
}
216220

217-
const tag = hasUserConfig(runtimeConfig)
218-
? codebaseUsesEnvs.length > 0
219-
? "mixed"
220-
: "runtime_config"
221-
: codebaseUsesEnvs.length > 0
222-
? "dotenv"
223-
: "none";
224-
void track("functions_codebase_deploy_env_method", tag);
225-
226-
const codebaseCnt = Object.keys(payload.functions).length;
227-
void track("functions_codebase_deploy_count", codebaseCnt >= 5 ? "5+" : codebaseCnt.toString());
228-
229221
// ===Phase 5. Enable APIs required by the deploying backends.
230222
const wantBackend = backend.merge(...Object.values(wantBackends));
231223
const haveBackend = backend.merge(...Object.values(haveBackends));
@@ -468,6 +460,7 @@ export async function loadCodebases(
468460
// in order for .init() calls to succeed.
469461
GOOGLE_CLOUD_QUOTA_PROJECT: projectId,
470462
});
463+
wantBuilds[codebase].runtime = codebaseConfig.runtime;
471464
}
472465
return wantBuilds;
473466
}

src/deploy/functions/release/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export async function release(
7676

7777
const summary = await fab.applyPlan(plan);
7878

79-
await reporter.logAndTrackDeployStats(summary);
79+
await reporter.logAndTrackDeployStats(summary, context);
8080
reporter.printErrors(summary);
8181

8282
// N.B. Fabricator::applyPlan updates the endpoints it deploys to include the

src/deploy/functions/release/reporter.ts

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as backend from "../backend";
22
import * as clc from "colorette";
33

4+
import * as args from "../args";
45
import { logger } from "../../../logger";
5-
import { track } from "../../../track";
6+
import { trackGA4 } from "../../../track";
67
import * as utils from "../../../utils";
78
import { getFunctionLabel } from "../functionsDeployHelper";
89

@@ -56,62 +57,76 @@ export class AbortedDeploymentError extends DeploymentError {
5657
}
5758

5859
/** Add debugger logs and GA metrics for deploy stats. */
59-
export async function logAndTrackDeployStats(summary: Summary): Promise<void> {
60+
export async function logAndTrackDeployStats(
61+
summary: Summary,
62+
context?: args.Context
63+
): Promise<void> {
6064
let totalTime = 0;
6165
let totalErrors = 0;
6266
let totalSuccesses = 0;
6367
let totalAborts = 0;
6468
const reports: Array<Promise<void>> = [];
6569

6670
const regions = new Set<string>();
71+
const codebases = new Set<string>();
6772
for (const result of summary.results) {
68-
const tag = triggerTag(result.endpoint);
73+
const fnDeployEvent = {
74+
platform: result.endpoint.platform,
75+
trigger_type: backend.endpointTriggerType(result.endpoint),
76+
region: result.endpoint.region,
77+
runtime: result.endpoint.runtime,
78+
status: !result.error
79+
? "success"
80+
: result.error instanceof AbortedDeploymentError
81+
? "aborted"
82+
: "failure",
83+
duration: result.durationMs,
84+
};
85+
reports.push(trackGA4("function_deploy", fnDeployEvent));
86+
6987
regions.add(result.endpoint.region);
88+
codebases.add(result.endpoint.codebase || "default");
7089
totalTime += result.durationMs;
7190
if (!result.error) {
7291
totalSuccesses++;
73-
reports.push(track("function_deploy_success", tag, result.durationMs));
92+
if (context?.codebaseDeployEvents?.[result.endpoint.codebase || "default"] !== undefined) {
93+
context.codebaseDeployEvents[result.endpoint.codebase || "default"]
94+
.fn_deploy_num_successes++;
95+
}
7496
} else if (result.error instanceof AbortedDeploymentError) {
7597
totalAborts++;
76-
reports.push(track("function_deploy_abort", tag, result.durationMs));
98+
if (context?.codebaseDeployEvents?.[result.endpoint.codebase || "default"] !== undefined) {
99+
context.codebaseDeployEvents[result.endpoint.codebase || "default"]
100+
.fn_deploy_num_canceled++;
101+
}
77102
} else {
78103
totalErrors++;
79-
reports.push(track("function_deploy_failure", tag, result.durationMs));
104+
if (context?.codebaseDeployEvents?.[result.endpoint.codebase || "default"] !== undefined) {
105+
context.codebaseDeployEvents[result.endpoint.codebase || "default"]
106+
.fn_deploy_num_failures++;
107+
}
80108
}
81109
}
82110

83-
const regionCountTag = regions.size < 5 ? regions.size.toString() : ">=5";
84-
reports.push(track("functions_region_count", regionCountTag, 1));
85-
86-
const gcfv1 = summary.results.find((r) => r.endpoint.platform === "gcfv1");
87-
const gcfv2 = summary.results.find((r) => r.endpoint.platform === "gcfv2");
88-
const tag = gcfv1 && gcfv2 ? "v1+v2" : gcfv1 ? "v1" : "v2";
89-
reports.push(track("functions_codebase_deploy", tag, summary.results.length));
111+
for (const codebase of codebases) {
112+
if (context?.codebaseDeployEvents) {
113+
reports.push(trackGA4("codebase_deploy", { ...context.codebaseDeployEvents[codebase] }));
114+
}
115+
}
116+
const fnDeployGroupEvent = {
117+
codebase_deploy_count: codebases.size >= 5 ? "5+" : codebases.size.toString(),
118+
fn_deploy_num_successes: totalSuccesses,
119+
fn_deploy_num_canceled: totalAborts,
120+
fn_deploy_num_failures: totalErrors,
121+
};
122+
reports.push(trackGA4("function_deploy_group", fnDeployGroupEvent));
90123

91124
const avgTime = totalTime / (totalSuccesses + totalErrors);
92-
93125
logger.debug(`Total Function Deployment time: ${summary.totalTime}`);
94126
logger.debug(`${totalErrors + totalSuccesses + totalAborts} Functions Deployed`);
95127
logger.debug(`${totalErrors} Functions Errored`);
96128
logger.debug(`${totalAborts} Function Deployments Aborted`);
97129
logger.debug(`Average Function Deployment time: ${avgTime}`);
98-
if (totalErrors + totalSuccesses > 0) {
99-
if (totalErrors === 0) {
100-
reports.push(track("functions_deploy_result", "success", totalSuccesses));
101-
} else if (totalSuccesses > 0) {
102-
reports.push(track("functions_deploy_result", "partial_success", totalSuccesses));
103-
reports.push(track("functions_deploy_result", "partial_failure", totalErrors));
104-
reports.push(
105-
track(
106-
"functions_deploy_result",
107-
"partial_error_ratio",
108-
totalErrors / (totalSuccesses + totalErrors)
109-
)
110-
);
111-
} else {
112-
reports.push(track("functions_deploy_result", "failure", totalErrors));
113-
}
114-
}
115130

116131
await utils.allSettled(reports);
117132
}

src/deploy/functions/runtimes/node/parseRuntimeAndValidateSDK.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as path from "path";
22
import * as clc from "colorette";
33

44
import { FirebaseError } from "../../../../error";
5-
import { track } from "../../../../track";
65
import * as runtimes from "../../runtimes";
76

87
// have to require this because no @types/cjson available
@@ -80,15 +79,13 @@ export function getRuntimeChoice(sourceDir: string, runtimeFromConfig?: string):
8079
: UNSUPPORTED_NODE_VERSION_PACKAGE_JSON_MSG) + DEPRECATED_NODE_VERSION_INFO;
8180

8281
if (!runtime || !ENGINE_RUNTIMES_NAMES.includes(runtime)) {
83-
void track("functions_runtime_notices", "package_missing_runtime");
8482
throw new FirebaseError(errorMessage, { exit: 1 });
8583
}
8684

8785
// Note: the runtimes.isValidRuntime should always be true because we've verified
8886
// it's in ENGINE_RUNTIME_NAMES and not in DEPRECATED_RUNTIMES. This is still a
8987
// good defense in depth and also lets us upcast the response to Runtime safely.
9088
if (runtimes.isDeprecatedRuntime(runtime) || !runtimes.isValidRuntime(runtime)) {
91-
void track("functions_runtime_notices", `${runtime}_deploy_prohibited`);
9289
throw new FirebaseError(errorMessage, { exit: 1 });
9390
}
9491

src/deploy/functions/runtimes/node/versioning.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import * as spawn from "cross-spawn";
66
import * as semver from "semver";
77

88
import { logger } from "../../../../logger";
9-
import { track } from "../../../../track";
109
import * as utils from "../../../../utils";
1110

1211
interface NpmShowResult {
@@ -113,7 +112,6 @@ export function getLatestSDKVersion(): string | undefined {
113112
export function checkFunctionsSDKVersion(currentVersion: string): void {
114113
try {
115114
if (semver.lt(currentVersion, MIN_SDK_VERSION)) {
116-
void track("functions_runtime_notices", "functions_sdk_too_old");
117115
utils.logWarning(FUNCTIONS_SDK_VERSION_TOO_OLD_WARNING);
118116
}
119117

0 commit comments

Comments
 (0)