From 0a43ed84e3e610fa523e5f35af18cc1bb5e59558 Mon Sep 17 00:00:00 2001 From: Vasu Bhog Date: Thu, 31 Mar 2022 16:29:15 -0700 Subject: [PATCH] Add bindings telemetry (#18851) * add bindings telemetry * add exitReasons --- .../sql-bindings/src/common/telemetry.ts | 18 ++- extensions/sql-bindings/src/common/utils.ts | 12 +- .../src/dialogs/addSqlBindingQuickpick.ts | 129 +++++++++++------- extensions/sql-bindings/src/extension.ts | 2 +- .../src/services/azureFunctionsService.ts | 57 +++++++- extensions/sql-bindings/src/sql-bindings.d.ts | 4 +- 6 files changed, 167 insertions(+), 55 deletions(-) diff --git a/extensions/sql-bindings/src/common/telemetry.ts b/extensions/sql-bindings/src/common/telemetry.ts index 92967a1f30..57993126dd 100644 --- a/extensions/sql-bindings/src/common/telemetry.ts +++ b/extensions/sql-bindings/src/common/telemetry.ts @@ -10,10 +10,24 @@ const packageInfo = getPackageInfo()!; export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); export enum TelemetryViews { - SqlBindingsQuickPick = 'SqlBindingsQuickPick' + SqlBindingsQuickPick = 'SqlBindingsQuickPick', + CreateAzureFunctionWithSqlBinding = 'CreateAzureFunctionWithSqlBinding' } export enum TelemetryActions { + // Create Azure Function with Sql Binding from Table + startCreateAzureFunctionWithSqlBinding = 'startCreateAzureFunctionWithSqlBinding', + helpCreateAzureFunctionProject = 'helpCreateAzureFunctionProject', + learnMore = 'learnMore', + finishCreateAzureFunctionWithSqlBinding = 'finishCreateAzureFunctionWithSqlBinding', + exitCreateAzureFunctionQuickpick = 'exitCreateAzureFunctionQuickpick', + + // Add SQL Binding to Azure Function startAddSqlBinding = 'startAddSqlBinding', - finishAddSqlBinding = 'finishAddSqlBinding' + getAzureFunctionProject = 'getAzureFunctionProject', + getBindingType = 'getBindingType', + getObjectName = 'getObjectName', + updateConnectionString = 'updateConnectionString', + finishAddSqlBinding = 'finishAddSqlBinding', + exitSqlBindingsQuickpick = 'exitSqlBindingsQuickpick', } diff --git a/extensions/sql-bindings/src/common/utils.ts b/extensions/sql-bindings/src/common/utils.ts index bfd6309ae4..88834d926e 100644 --- a/extensions/sql-bindings/src/common/utils.ts +++ b/extensions/sql-bindings/src/common/utils.ts @@ -23,6 +23,8 @@ export interface IPackageInfo { aiKey: string; } +export class TimeoutError extends Error { } + /** * Consolidates on the error message string */ @@ -128,7 +130,7 @@ export function generateQuotedFullName(schema: string, objectName: string): stri export function timeoutPromise(errorMessage: string, ms: number = 10000): Promise { return new Promise((_, reject) => { setTimeout(() => { - reject(new Error(errorMessage)); + reject(new TimeoutError(errorMessage)); }, ms); }); } @@ -173,3 +175,11 @@ export function getPackageInfo(): IPackageInfo { aiKey: packageJson.aiKey }; } +export function getErrorType(error: any): string | undefined { + if (error instanceof TimeoutError) { + return 'TimeoutError'; + } else { + return 'UnknownError'; + } +} + diff --git a/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts b/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts index a7b3e10df0..a3acb6595c 100644 --- a/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts +++ b/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts @@ -11,6 +11,10 @@ import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/t import { addSqlBinding, getAzureFunctions } from '../services/azureFunctionsService'; export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): Promise { + let quickPickStep: string = ''; + let exitReason: string = 'cancelled'; + let propertyBag: { [key: string]: string } = {}; + TelemetryReporter.sendActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.startAddSqlBinding); if (!uri) { // this command only shows in the command palette when the active editor is a .cs file, so we can safely assume that's the scenario @@ -45,60 +49,89 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): return; } - // 1. select Azure function from the current file - const azureFunctionName = (await vscode.window.showQuickPick(azureFunctions, { - canPickMany: false, - title: constants.selectAzureFunction, - ignoreFocusOut: true - })); - - if (!azureFunctionName) { - return; - } - - // 2. select input or output binding - const selectedBinding = await azureFunctionsUtils.promptForBindingType(); - - if (!selectedBinding) { - return; - } - - // 3. ask for object name for the binding - const objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding.type); - - if (!objectName) { - return; - } - - // 4. ask for connection string setting name - let projectUri: vscode.Uri | undefined; try { - projectUri = await azureFunctionsUtils.getAFProjectContainingFile(uri); - } catch (e) { - // continue even if there's no AF project found. The binding should still be able to be added as long as there was an azure function found in the file earlier - } + // 1. select Azure function from the current file + quickPickStep = 'getAzureFunctionProject'; + const azureFunctionName = (await vscode.window.showQuickPick(azureFunctions, { + canPickMany: false, + title: constants.selectAzureFunction, + ignoreFocusOut: true + })); - let connectionStringSettingName = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(projectUri); - if (!connectionStringSettingName) { - return; - } - - // 5. insert binding - try { - const result = await addSqlBinding(selectedBinding.type, uri.fsPath, azureFunctionName, objectName, connectionStringSettingName); - - if (!result.success) { - void vscode.window.showErrorMessage(result.errorMessage); - TelemetryReporter.sendErrorEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding); + if (!azureFunctionName) { return; } + TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.getAzureFunctionProject) + .withAdditionalProperties(propertyBag).send(); - TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding) - .withAdditionalProperties({ bindingType: selectedBinding.label }) - .send(); + // 2. select input or output binding + quickPickStep = 'getBindingType'; + const selectedBinding = await azureFunctionsUtils.promptForBindingType(); + + if (!selectedBinding) { + return; + } + propertyBag.bindingType = selectedBinding.type; + TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.getBindingType) + .withAdditionalProperties(propertyBag).send(); + + // 3. ask for object name for the binding + quickPickStep = 'getObjectName'; + const objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding.type); + + if (!objectName) { + return; + } + TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.getObjectName) + .withAdditionalProperties(propertyBag).send(); + + // 4. ask for connection string setting name + let projectUri: vscode.Uri | undefined; + try { + projectUri = await azureFunctionsUtils.getAFProjectContainingFile(uri); + } catch (e) { + // continue even if there's no AF project found. The binding should still be able to be added as long as there was an azure function found in the file earlier + } + + quickPickStep = 'updateConnectionString'; + let connectionStringSettingName = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(projectUri); + if (!connectionStringSettingName) { + return; + } + TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.updateConnectionString) + .withAdditionalProperties(propertyBag).send(); + + // 5. insert binding + try { + quickPickStep = 'insertBinding'; + const result = await addSqlBinding(selectedBinding.type, uri.fsPath, azureFunctionName, objectName, connectionStringSettingName); + + if (!result.success) { + void vscode.window.showErrorMessage(result.errorMessage); + TelemetryReporter.createErrorEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding) + .withAdditionalProperties(propertyBag).send(); + return; + } + exitReason = 'done'; + TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding) + .withAdditionalProperties(propertyBag).send(); + } catch (e) { + void vscode.window.showErrorMessage(utils.getErrorMessage(e)); + TelemetryReporter.createErrorEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding, undefined, utils.getErrorType(e)) + .withAdditionalProperties(propertyBag).send(); + return; + } } catch (e) { + propertyBag.quickPickStep = quickPickStep; + exitReason = 'error'; void vscode.window.showErrorMessage(utils.getErrorMessage(e)); - TelemetryReporter.sendErrorEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding); - return; + + TelemetryReporter.createErrorEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.exitSqlBindingsQuickpick, undefined, utils.getErrorType(e)) + .withAdditionalProperties(propertyBag).send(); + } finally { + propertyBag.quickPickStep = quickPickStep; + propertyBag.exitReason = exitReason; + TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.exitSqlBindingsQuickpick) + .withAdditionalProperties(propertyBag).send(); } } diff --git a/extensions/sql-bindings/src/extension.ts b/extensions/sql-bindings/src/extension.ts index ba5a41ddb5..e6c7fb4a4c 100644 --- a/extensions/sql-bindings/src/extension.ts +++ b/extensions/sql-bindings/src/extension.ts @@ -31,7 +31,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { diff --git a/extensions/sql-bindings/src/services/azureFunctionsService.ts b/extensions/sql-bindings/src/services/azureFunctionsService.ts index 44665933e0..88eea6b7b0 100644 --- a/extensions/sql-bindings/src/services/azureFunctionsService.ts +++ b/extensions/sql-bindings/src/services/azureFunctionsService.ts @@ -9,15 +9,28 @@ import * as utils from '../common/utils'; import * as azureFunctionsUtils from '../common/azureFunctionsUtils'; import * as constants from '../common/constants'; import * as azureFunctionsContracts from '../contracts/azureFunctions/azureFunctionsContracts'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { AddSqlBindingParams, BindingType, GetAzureFunctionsParams, GetAzureFunctionsResult, ResultStatus } from 'sql-bindings'; +import { IConnectionInfo } from 'vscode-mssql'; export const hostFileName: string = 'host.json'; +export async function createAzureFunction(connectionString: string, schema: string, table: string, connectionInfo: IConnectionInfo): Promise { + let propertyBag: { [key: string]: string } = {}; + let quickPickStep: string = ''; + let exitReason: string = 'cancelled'; -export async function createAzureFunction(connectionString: string, schema: string, table: string): Promise { + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) + .withConnectionInfo(connectionInfo).send(); + quickPickStep = 'getAzureFunctionsExtensionApi'; const azureFunctionApi = await azureFunctionsUtils.getAzureFunctionsExtensionApi(); if (!azureFunctionApi) { + propertyBag.exitReason = exitReason; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick) + .withConnectionInfo(connectionInfo) + .withAdditionalProperties(propertyBag).send(); return; + } let projectFile = await azureFunctionsUtils.getAzureFunctionProject(); let newHostProjectFile!: azureFunctionsUtils.IFileFunctionObject; @@ -27,9 +40,18 @@ export async function createAzureFunction(connectionString: string, schema: stri let projectCreate = await vscode.window.showErrorMessage(constants.azureFunctionsProjectMustBeOpened, constants.createProject, constants.learnMore); if (projectCreate === constants.learnMore) { + quickPickStep = 'learnMore'; void vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(constants.sqlBindingsDoc)); + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.learnMore) + .withConnectionInfo(connectionInfo) + .withAdditionalProperties(propertyBag).send(); return; } else if (projectCreate === constants.createProject) { + quickPickStep = 'helpCreateAzureFunctionProject'; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.helpCreateAzureFunctionProject) + .withConnectionInfo(connectionInfo) + .withAdditionalProperties(propertyBag).send(); + // start the create azure function project flow try { // because of an AF extension API issue, we have to get the newly created file by adding a watcher @@ -44,8 +66,13 @@ export async function createAzureFunction(connectionString: string, schema: stri } } catch (error) { void vscode.window.showErrorMessage(utils.formatString(constants.errorNewAzureFunction, error.message ?? error)); + let errorType = utils.getErrorType(error); + TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.helpCreateAzureFunctionProject, undefined, errorType).send(); return; } finally { + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick) + .withConnectionInfo(connectionInfo) + .withAdditionalProperties(propertyBag).send(); newHostProjectFile.watcherDisposable.dispose(); } } @@ -59,6 +86,7 @@ export async function createAzureFunction(connectionString: string, schema: stri try { // get function name from user + quickPickStep = 'getAzureFunctionName'; let uniqueFunctionName = await utils.getUniqueFileName(path.dirname(projectFile), table); functionName = await vscode.window.showInputBox({ title: constants.functionNameTitle, @@ -69,13 +97,21 @@ export async function createAzureFunction(connectionString: string, schema: stri if (!functionName) { return; } + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.getAzureFunctionProject) + .withAdditionalProperties(propertyBag) + .withConnectionInfo(connectionInfo).send(); // select input or output binding + quickPickStep = 'getBindingType'; const selectedBinding = await azureFunctionsUtils.promptForBindingType(); if (!selectedBinding) { return; } + propertyBag.bindingType = selectedBinding.type; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) + .withAdditionalProperties(propertyBag) + .withConnectionInfo(connectionInfo).send(); // set the templateId based on the selected binding type let templateId: string = selectedBinding.type === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID; @@ -103,10 +139,29 @@ export async function createAzureFunction(connectionString: string, schema: stri // check for the new function file to be created and dispose of the file system watcher const timeoutForFunctionFile = utils.timeoutPromise(constants.timeoutAzureFunctionFileError); await Promise.race([newFunctionFileObject.filePromise, timeoutForFunctionFile]); + propertyBag.quickPickStep = quickPickStep; + exitReason = 'finishCreate'; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.finishCreateAzureFunctionWithSqlBinding) + .withAdditionalProperties(propertyBag) + .withConnectionInfo(connectionInfo).send(); + } catch (e) { + propertyBag.quickPickStep = quickPickStep; + exitReason = 'error'; + void vscode.window.showErrorMessage(utils.getErrorMessage(e)); + + TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, utils.getErrorType(e)) + .withAdditionalProperties(propertyBag).send(); } finally { + propertyBag.quickPickStep = quickPickStep; + propertyBag.exitReason = exitReason; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick) + .withConnectionInfo(connectionInfo) + .withAdditionalProperties(propertyBag).send(); newFunctionFileObject.watcherDisposable.dispose(); } await azureFunctionsUtils.addConnectionStringToConfig(connectionString, projectFile); + } else { + TelemetryReporter.sendErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.finishCreateAzureFunctionWithSqlBinding); } } diff --git a/extensions/sql-bindings/src/sql-bindings.d.ts b/extensions/sql-bindings/src/sql-bindings.d.ts index d298e0f171..7be4cbbd6e 100644 --- a/extensions/sql-bindings/src/sql-bindings.d.ts +++ b/extensions/sql-bindings/src/sql-bindings.d.ts @@ -86,8 +86,8 @@ declare module 'sql-bindings' { * Azure Functions binding type */ export const enum BindingType { - input, - output + input = 'input', + output = 'output' } /**