Skip to content

Commit

Permalink
Migrates functions metrics to GA4 (#6053)
Browse files Browse the repository at this point in the history
* track functions metrics with ga4

* remove old track() calls
  • Loading branch information
blidd-google committed Jun 28, 2023
1 parent 654e884 commit 197506a
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 147 deletions.
14 changes: 14 additions & 0 deletions src/deploy/functions/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as backend from "./backend";
import * as gcfV2 from "../../gcp/cloudfunctionsv2";
import * as projectConfig from "../../functions/projectConfig";
import * as deployHelper from "./functionsDeployHelper";
import { Runtime } from "./runtimes";

// These types should probably be in a root deploy.ts, but we can only boil the ocean one bit at a time.
interface CodebasePayload {
Expand Down Expand Up @@ -49,6 +50,19 @@ export interface Context {
gcfV1: string[];
gcfV2: string[];
};

// Tracks metrics about codebase deployments to send to GA4
codebaseDeployEvents?: Record<string, CodebaseDeployEvent>;
}

export interface CodebaseDeployEvent {
params?: "env_only" | "with_secrets" | "none";
runtime?: Runtime;
runtime_notice?: string;
fn_deploy_num_successes: number;
fn_deploy_num_failures: number;
fn_deploy_num_canceled: number;
fn_deploy_num_skipped: number;
}

export interface FirebaseConfig {
Expand Down
2 changes: 2 additions & 0 deletions src/deploy/functions/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { FirebaseError } from "../../error";
import { assertExhaustive, mapObject, nullsafeVisitor } from "../../functional";
import { UserEnvsOpts, writeUserEnvs } from "../../functions/env";
import { FirebaseConfig } from "./args";
import { Runtime } from "./runtimes";

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

/**
Expand Down
3 changes: 0 additions & 3 deletions src/deploy/functions/ensure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { logLabeledBullet, logLabeledSuccess } from "../../utils";
import { ensureServiceAgentRole } from "../../gcp/secretManager";
import { getFirebaseProject } from "../../management/projects";
import { assertExhaustive } from "../../functional";
import { track } from "../../track";
import * as backend from "./backend";

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

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

function nodePermissionError(projectId: string): FirebaseError {
void track("functions_runtime_notices", "nodejs10_permission_error");
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(
projectId
)}.
Expand Down
35 changes: 14 additions & 21 deletions src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { logLabeledBullet } from "../../utils";
import { getFunctionsConfig, prepareFunctionsUpload } from "./prepareFunctionsUpload";
import { promptForFailurePolicies, promptForMinInstances } from "./prompts";
import { needProjectId, needProjectNumber } from "../../projectUtils";
import { track } from "../../track";
import { logger } from "../../logger";
import { ensureTriggerRegions } from "./triggerRegionHelper";
import { ensureServiceAgentRoles } from "./checkIam";
Expand All @@ -38,11 +37,6 @@ import { allEndpoints, Backend } from "./backend";
import { assertExhaustive } from "../../functional";

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

/**
* Prepare functions codebases for deploy.
Expand Down Expand Up @@ -88,6 +82,8 @@ export async function prepare(
runtimeConfig = { ...runtimeConfig, ...(await getFunctionsConfig(projectId)) };
}

context.codebaseDeployEvents = {};

// ===Phase 1. Load codebases from source.
const wantBuilds = await loadCodebases(
context.config,
Expand Down Expand Up @@ -155,15 +151,23 @@ export async function prepare(
codebaseUsesEnvs.push(codebase);
}

context.codebaseDeployEvents[codebase] = {
fn_deploy_num_successes: 0,
fn_deploy_num_failures: 0,
fn_deploy_num_canceled: 0,
fn_deploy_num_skipped: 0,
};

if (wantBuild.params.length > 0) {
if (wantBuild.params.every((p) => p.type !== "secret")) {
void track("functions_params_in_build", "env_only");
context.codebaseDeployEvents[codebase].params = "env_only";
} else {
void track("functions_params_in_build", "with_secrets");
context.codebaseDeployEvents[codebase].params = "with_secrets";
}
} else {
void track("functions_params_in_build", "none");
context.codebaseDeployEvents[codebase].params = "none";
}
context.codebaseDeployEvents[codebase].runtime = wantBuild.runtime;
}

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

const tag = hasUserConfig(runtimeConfig)
? codebaseUsesEnvs.length > 0
? "mixed"
: "runtime_config"
: codebaseUsesEnvs.length > 0
? "dotenv"
: "none";
void track("functions_codebase_deploy_env_method", tag);

const codebaseCnt = Object.keys(payload.functions).length;
void track("functions_codebase_deploy_count", codebaseCnt >= 5 ? "5+" : codebaseCnt.toString());

// ===Phase 5. Enable APIs required by the deploying backends.
const wantBackend = backend.merge(...Object.values(wantBackends));
const haveBackend = backend.merge(...Object.values(haveBackends));
Expand Down Expand Up @@ -468,6 +460,7 @@ export async function loadCodebases(
// in order for .init() calls to succeed.
GOOGLE_CLOUD_QUOTA_PROJECT: projectId,
});
wantBuilds[codebase].runtime = codebaseConfig.runtime;
}
return wantBuilds;
}
2 changes: 1 addition & 1 deletion src/deploy/functions/release/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export async function release(

const summary = await fab.applyPlan(plan);

await reporter.logAndTrackDeployStats(summary);
await reporter.logAndTrackDeployStats(summary, context);
reporter.printErrors(summary);

// N.B. Fabricator::applyPlan updates the endpoints it deploys to include the
Expand Down
77 changes: 46 additions & 31 deletions src/deploy/functions/release/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as backend from "../backend";
import * as clc from "colorette";

import * as args from "../args";
import { logger } from "../../../logger";
import { track } from "../../../track";
import { trackGA4 } from "../../../track";
import * as utils from "../../../utils";
import { getFunctionLabel } from "../functionsDeployHelper";

Expand Down Expand Up @@ -56,62 +57,76 @@ export class AbortedDeploymentError extends DeploymentError {
}

/** Add debugger logs and GA metrics for deploy stats. */
export async function logAndTrackDeployStats(summary: Summary): Promise<void> {
export async function logAndTrackDeployStats(
summary: Summary,
context?: args.Context
): Promise<void> {
let totalTime = 0;
let totalErrors = 0;
let totalSuccesses = 0;
let totalAborts = 0;
const reports: Array<Promise<void>> = [];

const regions = new Set<string>();
const codebases = new Set<string>();
for (const result of summary.results) {
const tag = triggerTag(result.endpoint);
const fnDeployEvent = {
platform: result.endpoint.platform,
trigger_type: backend.endpointTriggerType(result.endpoint),
region: result.endpoint.region,
runtime: result.endpoint.runtime,
status: !result.error
? "success"
: result.error instanceof AbortedDeploymentError
? "aborted"
: "failure",
duration: result.durationMs,
};
reports.push(trackGA4("function_deploy", fnDeployEvent));

regions.add(result.endpoint.region);
codebases.add(result.endpoint.codebase || "default");
totalTime += result.durationMs;
if (!result.error) {
totalSuccesses++;
reports.push(track("function_deploy_success", tag, result.durationMs));
if (context?.codebaseDeployEvents?.[result.endpoint.codebase || "default"] !== undefined) {
context.codebaseDeployEvents[result.endpoint.codebase || "default"]
.fn_deploy_num_successes++;
}
} else if (result.error instanceof AbortedDeploymentError) {
totalAborts++;
reports.push(track("function_deploy_abort", tag, result.durationMs));
if (context?.codebaseDeployEvents?.[result.endpoint.codebase || "default"] !== undefined) {
context.codebaseDeployEvents[result.endpoint.codebase || "default"]
.fn_deploy_num_canceled++;
}
} else {
totalErrors++;
reports.push(track("function_deploy_failure", tag, result.durationMs));
if (context?.codebaseDeployEvents?.[result.endpoint.codebase || "default"] !== undefined) {
context.codebaseDeployEvents[result.endpoint.codebase || "default"]
.fn_deploy_num_failures++;
}
}
}

const regionCountTag = regions.size < 5 ? regions.size.toString() : ">=5";
reports.push(track("functions_region_count", regionCountTag, 1));

const gcfv1 = summary.results.find((r) => r.endpoint.platform === "gcfv1");
const gcfv2 = summary.results.find((r) => r.endpoint.platform === "gcfv2");
const tag = gcfv1 && gcfv2 ? "v1+v2" : gcfv1 ? "v1" : "v2";
reports.push(track("functions_codebase_deploy", tag, summary.results.length));
for (const codebase of codebases) {
if (context?.codebaseDeployEvents) {
reports.push(trackGA4("codebase_deploy", { ...context.codebaseDeployEvents[codebase] }));
}
}
const fnDeployGroupEvent = {
codebase_deploy_count: codebases.size >= 5 ? "5+" : codebases.size.toString(),
fn_deploy_num_successes: totalSuccesses,
fn_deploy_num_canceled: totalAborts,
fn_deploy_num_failures: totalErrors,
};
reports.push(trackGA4("function_deploy_group", fnDeployGroupEvent));

const avgTime = totalTime / (totalSuccesses + totalErrors);

logger.debug(`Total Function Deployment time: ${summary.totalTime}`);
logger.debug(`${totalErrors + totalSuccesses + totalAborts} Functions Deployed`);
logger.debug(`${totalErrors} Functions Errored`);
logger.debug(`${totalAborts} Function Deployments Aborted`);
logger.debug(`Average Function Deployment time: ${avgTime}`);
if (totalErrors + totalSuccesses > 0) {
if (totalErrors === 0) {
reports.push(track("functions_deploy_result", "success", totalSuccesses));
} else if (totalSuccesses > 0) {
reports.push(track("functions_deploy_result", "partial_success", totalSuccesses));
reports.push(track("functions_deploy_result", "partial_failure", totalErrors));
reports.push(
track(
"functions_deploy_result",
"partial_error_ratio",
totalErrors / (totalSuccesses + totalErrors)
)
);
} else {
reports.push(track("functions_deploy_result", "failure", totalErrors));
}
}

await utils.allSettled(reports);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as path from "path";
import * as clc from "colorette";

import { FirebaseError } from "../../../../error";
import { track } from "../../../../track";
import * as runtimes from "../../runtimes";

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

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

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

Expand Down
2 changes: 0 additions & 2 deletions src/deploy/functions/runtimes/node/versioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as spawn from "cross-spawn";
import * as semver from "semver";

import { logger } from "../../../../logger";
import { track } from "../../../../track";
import * as utils from "../../../../utils";

interface NpmShowResult {
Expand Down Expand Up @@ -113,7 +112,6 @@ export function getLatestSDKVersion(): string | undefined {
export function checkFunctionsSDKVersion(currentVersion: string): void {
try {
if (semver.lt(currentVersion, MIN_SDK_VERSION)) {
void track("functions_runtime_notices", "functions_sdk_too_old");
utils.logWarning(FUNCTIONS_SDK_VERSION_TOO_OLD_WARNING);
}

Expand Down

0 comments on commit 197506a

Please sign in to comment.