Skip to content

feature: SWA CLI Telemetry Setup #907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
3,802 changes: 2,425 additions & 1,377 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,27 @@
"@azure/identity": "^4.3.0",
"@azure/msal-common": "^14.13.0",
"adm-zip": "^0.5.14",
"applicationinsights": "^3.4.0",
"chalk": "^4.1.2",
"cli-progress": "^3.12.0",
"commander": "^9.5.0",
"concurrently": "^7.6.0",
"cookie": "^0.5.0",
"devcert": "^1.2.0",
"dotenv": "^16.4.5",
"finalhandler": "^1.2.0",
"get-port": "^5.1.1",
"globrex": "^0.1.2",
"http-proxy": "^1.18.1",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"internal-ip": "^6.2.0",
"json-schema-library": "^9.3.5",
"json-source-map": "^0.6.1",
"jwt-decode": "^4.0.0",
"keytar": "^7.9.0",
"node-fetch": "^2.7.0",
"open": "^8.4.2",
"ora": "^5.4.1",
"pem": "^1.14.8",
"prompts": "^2.4.2",
"rimraf": "^5.0.7",
"selfsigned": "^2.4.1",
"serve-static": "^1.15.0",
"update-notifier": "^7.0.0",
"wait-on": "^7.2.0",
Expand Down
33 changes: 33 additions & 0 deletions src/cli/commands/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { logger } from "../../../core/utils/logger.js";
import { readWorkflowFile } from "../../../core/utils/workflow-config.js";
import { runCommand } from "../../../core/utils/command.js";
import { swaCliConfigFilename } from "../../../core/utils/cli-config.js";
import { collectTelemetryEvent } from "../../../core/telemetry/utils.js";
import { TELEMETRY_EVENTS, TELEMETRY_RESPONSE_TYPES } from "../../../core/constants.js";

export async function build(options: SWACLIConfig) {
const startTime = new Date().getTime();
const workflowConfig = readWorkflowFile();

logger.silly({
Expand All @@ -31,6 +34,12 @@ export async function build(options: SWACLIConfig) {
if (options.auto && hasBuildOptionsDefined(options)) {
logger.error(`You can't use the --auto option when you have defined appBuildCommand or apiBuildCommand in ${swaCliConfigFilename}`);
logger.error(`or with the --app-build-command and --api-build-command options.`, true);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Build, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "Auto option cannot be used with appBuildCommand or apiBuildCommand",
});
return;
}

Expand All @@ -40,9 +49,21 @@ export async function build(options: SWACLIConfig) {

if (detectedFolders.app.length === 0 && detectedFolders.api.length === 0) {
logger.error(`Your app configuration could not be detected.`);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Build, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "App configuration could not be detected",
});
return showAutoErrorMessageAndExit();
} else if (detectedFolders.app.length > 1 || detectedFolders.api.length > 1) {
logger.error(`Multiple apps found in your project folder.`);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Build, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "Multiple apps found in project folder",
});
return showAutoErrorMessageAndExit();
}

Expand All @@ -57,6 +78,12 @@ export async function build(options: SWACLIConfig) {
} catch (error) {
logger.error(`Cannot generate your build configuration:`);
logger.error(error as Error, true);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Build, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "Cannot generate build configuration",
});
return;
}
}
Expand Down Expand Up @@ -101,6 +128,12 @@ export async function build(options: SWACLIConfig) {
logger.log(`Building api with ${chalk.green(apiBuildCommand)} in ${chalk.dim(apiLocation)} ...`);
runCommand(apiBuildCommand, apiLocation!);
}

const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Build, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Success,
});
}

function hasBuildOptionsDefined(options: SWACLIConfig): boolean {
Expand Down
56 changes: 54 additions & 2 deletions src/cli/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ import { logger, logGitHubIssueMessageAndExit } from "../../../core/utils/logger
import { isUserOrConfigOption } from "../../../core/utils/options.js";
import { readWorkflowFile } from "../../../core/utils/workflow-config.js";
import { chooseOrCreateProjectDetails, getStaticSiteDeployment } from "../../../core/account.js";
import { DEFAULT_RUNTIME_LANGUAGE } from "../../../core/constants.js";
import { DEFAULT_RUNTIME_LANGUAGE, TELEMETRY_EVENTS, TELEMETRY_RESPONSE_TYPES } from "../../../core/constants.js";
import { cleanUp, getDeployClientPath } from "../../../core/deploy-client.js";
import { swaCLIEnv } from "../../../core/env.js";
import { getDefaultVersion } from "../../../core/functions-versions.js";
import { login } from "../login/login.js";
import { loadPackageJson } from "../../../core/utils/json.js";
import { collectTelemetryEvent } from "../../../core/telemetry/utils.js";

const packageInfo = loadPackageJson();

export async function deploy(options: SWACLIConfig) {
const startTime = new Date().getTime();
const { SWA_CLI_DEPLOYMENT_TOKEN, SWA_CLI_DEBUG } = swaCLIEnv();
const isVerboseEnabled = SWA_CLI_DEBUG === "silly";

Expand Down Expand Up @@ -52,6 +54,12 @@ export async function deploy(options: SWACLIConfig) {
dataApiLocation = path.resolve(dataApiLocation);
if (!fs.existsSync(dataApiLocation)) {
logger.error(`The provided Data API folder ${dataApiLocation} does not exist. Abort.`, true);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Deploy, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "Data API folder does not exist",
});
return;
} else {
logger.log(`Deploying Data API from folder:`);
Expand All @@ -67,6 +75,12 @@ export async function deploy(options: SWACLIConfig) {
if (!fs.existsSync(resolvedOutputLocation)) {
if (!fs.existsSync(outputLocation as string)) {
logger.error(`The folder "${resolvedOutputLocation}" is not found. Exit.`, true);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Deploy, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "Output location does not exist",
});
return;
}
// otherwise, build folder (outputLocation) is using the absolute location
Expand All @@ -83,6 +97,12 @@ export async function deploy(options: SWACLIConfig) {
resolvedApiLocation = path.resolve(apiLocation!);
if (!fs.existsSync(resolvedApiLocation)) {
logger.error(`The provided API folder ${resolvedApiLocation} does not exist. Abort.`, true);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Deploy, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "API folder does not exist",
});
return;
} else {
logger.log(`Deploying API from folder:`);
Expand Down Expand Up @@ -161,6 +181,12 @@ export async function deploy(options: SWACLIConfig) {

if (!deploymentToken) {
logger.error("Cannot find a deployment token. Aborting.", true);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Deploy, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "Deployment token not found",
});
} else {
logger.log(chalk.green(`✔ Successfully setup project!`));

Expand All @@ -184,6 +210,12 @@ export async function deploy(options: SWACLIConfig) {
}
} catch (error: any) {
logger.error(error.message);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Deploy, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "Deployment token not found",
});
return;
}
}
Expand Down Expand Up @@ -219,7 +251,9 @@ export async function deploy(options: SWACLIConfig) {
}

swaConfigLocation = swaConfigLocation || userWorkflowConfig?.appLocation;
const swaConfigFilePath = (await findSWAConfigFile(swaConfigLocation!))?.filepath;
const swaConfigFile = await findSWAConfigFile(swaConfigLocation!);
const swaConfigFilePath = swaConfigFile?.filepath;
const swaConfigFileContent = swaConfigFile?.content;
const resolvedSwaConfigLocation = swaConfigFilePath ? path.dirname(swaConfigFilePath) : undefined;

const cliEnv: SWACLIEnv = {
Expand Down Expand Up @@ -317,6 +351,12 @@ export async function deploy(options: SWACLIConfig) {

child.on("error", (error) => {
logger.error(error.toString());
const endTime = new Date().getTime();
collectTelemetryEvent(TELEMETRY_EVENTS.Deploy, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: error.toString(),
});
});

child.on("close", (code) => {
Expand All @@ -325,6 +365,12 @@ export async function deploy(options: SWACLIConfig) {
if (code === 0) {
spinner.succeed(chalk.green(`Project deployed to ${chalk.underline(projectUrl)} 🚀`));
logger.log(``);
const endTime = new Date().getTime();
collectTelemetryEvent(TELEMETRY_EVENTS.Deploy, {
ApiRuntime: swaConfigFileContent?.platform?.apiRuntime!,
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Success,
});
}
});
}
Expand All @@ -335,6 +381,12 @@ export async function deploy(options: SWACLIConfig) {
logger.error(
`For further information, please visit the Azure Static Web Apps documentation at https://docs.microsoft.com/azure/static-web-apps/`,
);
const endTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Deploy, {
Duration: (endTime - startTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: (error as any).message,
});
logGitHubIssueMessageAndExit();
} finally {
cleanUp();
Expand Down
20 changes: 20 additions & 0 deletions src/cli/commands/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { hasConfigurationNameInConfigFile, swaCliConfigFileExists, swaCliConfigF
import { logger } from "../../../core/utils/logger.js";
import { detectDbConfigFiles, detectProjectFolders, generateConfiguration, isDescendantPath } from "../../../core/frameworks/detect.js";
import { getChoicesForApiLanguage } from "../../../core/functions-versions.js";
import { collectTelemetryEvent } from "../../../core/telemetry/utils.js";
import { TELEMETRY_EVENTS, TELEMETRY_RESPONSE_TYPES } from "../../../core/constants.js";

export async function init(options: SWACLIConfig, showHints: boolean = true) {
const cmdStartTime = new Date().getTime();
const configFilePath = options.config!;
const disablePrompts = options.yes ?? false;
const outputFolder = process.cwd();
Expand Down Expand Up @@ -99,9 +102,26 @@ export async function init(options: SWACLIConfig, showHints: boolean = true) {
} catch (error) {
logger.error(`Cannot generate your project configuration:`);
logger.error(error as Error, true);

const failureEndTime = new Date().getTime();
await collectTelemetryEvent(TELEMETRY_EVENTS.Init, {
Duration: (failureEndTime - cmdStartTime).toString(),
AppFramework: projectConfig?.name?.split(", with")[0]!,
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: error as string,
});

return;
}

const cmdEndTime = new Date().getTime();

await collectTelemetryEvent(TELEMETRY_EVENTS.Init, {
Duration: (cmdEndTime - cmdStartTime).toString(),
AppFramework: projectConfig?.name?.split(", with")[0]!,
ResponseType: TELEMETRY_RESPONSE_TYPES.Success,
});

printFrameworkConfig(projectConfig);

const { confirmSettings } = await promptOrUseDefault(disablePrompts, {
Expand Down
20 changes: 19 additions & 1 deletion src/cli/commands/login/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,45 @@ import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { logger, logGitHubIssueMessageAndExit } from "../../../core/utils/logger.js";
import { authenticateWithAzureIdentity, listSubscriptions, listTenants } from "../../../core/account.js";
import { AZURE_LOGIN_CONFIG, ENV_FILENAME } from "../../../core/constants.js";
import { AZURE_LOGIN_CONFIG, ENV_FILENAME, TELEMETRY_EVENTS, TELEMETRY_RESPONSE_TYPES } from "../../../core/constants.js";
import { pathExists, safeReadJson } from "../../../core/utils/file.js";
import { updateGitIgnore } from "../../../core/git.js";
import { chooseSubscription, chooseTenant } from "../../../core/prompts.js";
import { Environment } from "../../../core/swa-cli-persistence-plugin/impl/azure-environment.js";
import { collectTelemetryEvent } from "../../../core/telemetry/utils.js";

const defaultScope = `${Environment.AzureCloud.resourceManagerEndpointUrl}/.default`;

export async function loginCommand(options: SWACLIConfig) {
const cmdStartTime = new Date().getTime();

try {
const { credentialChain, subscriptionId } = await login(options);

const cmdEndTime = new Date().getTime();
if (credentialChain && subscriptionId) {
logger.log(chalk.green(`✔ Successfully setup project!`));
await collectTelemetryEvent(TELEMETRY_EVENTS.Login, {
Duration: (cmdEndTime - cmdStartTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Success,
});
} else {
logger.log(chalk.red(`✘ Failed to setup project!`));
logGitHubIssueMessageAndExit();
await collectTelemetryEvent(TELEMETRY_EVENTS.Login, {
Duration: (cmdEndTime - cmdStartTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: "Login failed",
});
}
} catch (error) {
logger.error(`Failed to setup project: ${(error as any).message}`);
logGitHubIssueMessageAndExit();
await collectTelemetryEvent(TELEMETRY_EVENTS.Login, {
Duration: (new Date().getTime() - cmdStartTime).toString(),
ResponseType: TELEMETRY_RESPONSE_TYPES.Failure,
ErrorMessage: (error as any).message,
});
}
}

Expand Down
Loading
Loading