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
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
Loading