diff --git a/extensions/admin-tool-ext-win/src/main.ts b/extensions/admin-tool-ext-win/src/main.ts index 60cb7f5db8..e6d451c31d 100644 --- a/extensions/admin-tool-ext-win/src/main.ts +++ b/extensions/admin-tool-ext-win/src/main.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { Telemetry } from './telemetry'; -import { doubleEscapeSingleQuotes, backEscapeDoubleQuotes } from './utils'; +import { doubleEscapeSingleQuotes, backEscapeDoubleQuotes, getTelemetryErrorType } from './utils'; import { ChildProcess, exec } from 'child_process'; const localize = nls.loadMessageBundle(); const ssmsMinVer = JSON.parse(JSON.stringify(require('./config.json'))).version; @@ -66,7 +66,6 @@ export interface LaunchSsmsDialogParams { export async function activate(context: vscode.ExtensionContext): Promise { // This is for Windows-specific support so do nothing on other platforms if (process.platform === 'win32') { - Telemetry.sendTelemetryEvent('startup/ExtensionActivated'); exePath = path.join(context.extensionPath, 'ssmsmin', 'Windows', ssmsMinVer, 'ssmsmin.exe'); registerCommands(context); } @@ -76,7 +75,7 @@ export async function deactivate(): Promise { // If the extension is being deactivated we want to kill all processes that are still currently // running otherwise they will continue to run as orphan processes. We use taskkill here in case // they started off child processes of their own - runningProcesses.forEach(p => exec('taskkill /pid ' + p.pid + ' /T /F')); + runningProcesses.forEach(p => exec(`taskkill /pid ${p.pid} /T /F`)); } /** @@ -96,6 +95,7 @@ function registerCommands(context: vscode.ExtensionContext): void { */ async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise { if (!connectionContext) { + Telemetry.sendTelemetryEventForError('NoConnectionContext', { action: 'Properties' }); vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionContextForProp', 'No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand')); return; } @@ -106,9 +106,9 @@ async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: az } else if (connectionContext.nodeInfo) { nodeType = connectionContext.nodeInfo.nodeType; - } - else { - vscode.window.showErrorMessage(localize('adminToolExtWin.noOeNode', 'Could not determine NodeType for handleLaunchSsmsMinPropertiesDialogCommand with context {0}', JSON.stringify(connectionContext))); + } else { + Telemetry.sendTelemetryEventForError('NoOENode', { action: 'Properties' }); + vscode.window.showErrorMessage(localize('adminToolExtWin.noOENode', 'Could not determine Object Explorer node from connectionContext : {0}', JSON.stringify(connectionContext))); return; } @@ -122,12 +122,14 @@ async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: az * @param connectionId The connection context from the command */ async function handleLaunchSsmsMinGswDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise { + const action = 'GenerateScripts'; if (!connectionContext) { + Telemetry.sendTelemetryEventForError('NoConnectionContext', { action: action }); vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionContextForGsw', 'No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand')); } launchSsmsDialog( - 'GenerateScripts', + action, connectionContext); } @@ -139,6 +141,7 @@ async function handleLaunchSsmsMinGswDialogCommand(connectionContext?: azdata.Ob */ async function launchSsmsDialog(action: string, connectionContext: azdata.ObjectExplorerContext): Promise { if (!connectionContext.connectionProfile) { + Telemetry.sendTelemetryEventForError('NoConnectionProfile', { action: action }); vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionProfile', 'No connectionProfile provided from connectionContext : {0}', JSON.stringify(connectionContext))); return; } @@ -152,6 +155,7 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object oeNode = await azdata.objectexplorer.getNode(connectionContext.connectionProfile.id, connectionContext.nodeInfo.nodePath); } else { + Telemetry.sendTelemetryEventForError('NoOENode', { action: action }); vscode.window.showErrorMessage(localize('adminToolExtWin.noOENode', 'Could not determine Object Explorer node from connectionContext : {0}', JSON.stringify(connectionContext))); return; } @@ -175,7 +179,12 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object const args = buildSsmsMinCommandArgs(params); - Telemetry.sendTelemetryEvent('LaunchSsmsDialog', { 'action': action }); + Telemetry.sendTelemetryEvent('LaunchSsmsDialog', + { + action: action, + nodeType: oeNode ? oeNode.nodeType : 'Server', + authType: connectionContext.connectionProfile.authenticationType + }); vscode.window.setStatusBarMessage(localize('adminToolExtWin.launchingDialogStatus', 'Launching dialog...'), 3000); @@ -186,11 +195,12 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object (execException, stdout, stderr) => { // Process has exited so remove from map of running processes runningProcesses.delete(proc.pid); + const err = stderr.toString(); Telemetry.sendTelemetryEvent('LaunchSsmsDialogResult', { - 'action': params.action, - 'returnCode': execException && execException.code ? execException.code.toString() : '0' + action: params.action, + returnCode: execException && execException.code ? execException.code.toString() : '0', + errorType: getTelemetryErrorType(err) }); - let err = stderr.toString(); if (err !== '') { vscode.window.showErrorMessage(localize( 'adminToolExtWin.ssmsMinError', diff --git a/extensions/admin-tool-ext-win/src/telemetry.ts b/extensions/admin-tool-ext-win/src/telemetry.ts index ebdb9b257d..530c8a8dd3 100644 --- a/extensions/admin-tool-ext-win/src/telemetry.ts +++ b/extensions/admin-tool-ext-win/src/telemetry.ts @@ -62,27 +62,11 @@ export class Telemetry { } /** - * Send a telemetry event for an exception + * Send a telemetry event for a general error + * @param err The error to log */ - public static sendTelemetryEventForException( - err: any, methodName: string, extensionConfigName: string): void { - try { - let stackArray: string[]; - let firstLine: string = ''; - if (err !== undefined && err.stack !== undefined) { - stackArray = err.stack.split('\n'); - if (stackArray !== undefined && stackArray.length >= 2) { - firstLine = stackArray[1]; // The first line is the error message and we don't want to send that telemetry event - firstLine = filterErrorPath(firstLine); - } - } - - // Only adding the method name and the fist line of the stack trace. We don't add the error message because it might have PII - this.sendTelemetryEvent('Exception', { methodName: methodName, errorLine: firstLine }); - } catch (telemetryErr) { - // If sending telemetry event fails ignore it so it won't break the extension - console.error('Failed to send telemetry event. error: ' + telemetryErr, extensionConfigName); - } + public static sendTelemetryEventForError(err: string, properties?: ITelemetryEventProperties): void { + this.sendTelemetryEvent('Error', { error: err, ...properties }); } /** @@ -106,7 +90,12 @@ export class Telemetry { properties = {}; } - this.reporter.sendTelemetryEvent(eventName, properties, measures); + try { + this.reporter.sendTelemetryEvent(eventName, properties, measures); + } catch (telemetryErr) { + // If sending telemetry event fails ignore it so it won't break the extension + console.error('Failed to send telemetry event. error: ' + telemetryErr); + } } } diff --git a/extensions/admin-tool-ext-win/src/utils.ts b/extensions/admin-tool-ext-win/src/utils.ts index 68364017ed..d03a9278f1 100644 --- a/extensions/admin-tool-ext-win/src/utils.ts +++ b/extensions/admin-tool-ext-win/src/utils.ts @@ -36,4 +36,26 @@ export function doubleEscapeSingleQuotes(value: string): string { */ export function backEscapeDoubleQuotes(value: string): string { return value.replace(/"/g, '\\"'); +} + +/** + * Map an error message into a GDPR-Compliant short name for the type of error. + * @param msg The error message to map + */ +export function getTelemetryErrorType(msg: string): string { + if (msg.indexOf('is not recognized as an internal or external command') !== -1) { + return 'ExeNotFound'; + } + else if (msg.indexOf('Unknown Action') !== -1) { + return 'UnknownAction'; + } + else if (msg.indexOf('No Action Provided') !== -1) { + return 'NoActionProvided'; + } + else if (msg.indexOf('Run exception') !== -1) { + return 'RunException'; + } + else { + return 'Other'; + } } \ No newline at end of file