Clean up extension telemetry (#5596)

This commit is contained in:
Charles Gagnon
2019-05-23 15:29:28 -07:00
committed by GitHub
parent 776e2cf6e7
commit fbbf767700
3 changed files with 53 additions and 32 deletions

View File

@@ -8,7 +8,7 @@ import * as path from 'path';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { Telemetry } from './telemetry'; import { Telemetry } from './telemetry';
import { doubleEscapeSingleQuotes, backEscapeDoubleQuotes } from './utils'; import { doubleEscapeSingleQuotes, backEscapeDoubleQuotes, getTelemetryErrorType } from './utils';
import { ChildProcess, exec } from 'child_process'; import { ChildProcess, exec } from 'child_process';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
const ssmsMinVer = JSON.parse(JSON.stringify(require('./config.json'))).version; 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<void> { export async function activate(context: vscode.ExtensionContext): Promise<void> {
// This is for Windows-specific support so do nothing on other platforms // This is for Windows-specific support so do nothing on other platforms
if (process.platform === 'win32') { if (process.platform === 'win32') {
Telemetry.sendTelemetryEvent('startup/ExtensionActivated');
exePath = path.join(context.extensionPath, 'ssmsmin', 'Windows', ssmsMinVer, 'ssmsmin.exe'); exePath = path.join(context.extensionPath, 'ssmsmin', 'Windows', ssmsMinVer, 'ssmsmin.exe');
registerCommands(context); registerCommands(context);
} }
@@ -76,7 +75,7 @@ export async function deactivate(): Promise<void> {
// If the extension is being deactivated we want to kill all processes that are still currently // 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 // running otherwise they will continue to run as orphan processes. We use taskkill here in case
// they started off child processes of their own // 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<void> { async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise<void> {
if (!connectionContext) { if (!connectionContext) {
Telemetry.sendTelemetryEventForError('NoConnectionContext', { action: 'Properties' });
vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionContextForProp', 'No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand')); vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionContextForProp', 'No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand'));
return; return;
} }
@@ -106,9 +106,9 @@ async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: az
} }
else if (connectionContext.nodeInfo) { else if (connectionContext.nodeInfo) {
nodeType = connectionContext.nodeInfo.nodeType; nodeType = connectionContext.nodeInfo.nodeType;
} } else {
else { Telemetry.sendTelemetryEventForError('NoOENode', { action: 'Properties' });
vscode.window.showErrorMessage(localize('adminToolExtWin.noOeNode', 'Could not determine NodeType for handleLaunchSsmsMinPropertiesDialogCommand with context {0}', JSON.stringify(connectionContext))); vscode.window.showErrorMessage(localize('adminToolExtWin.noOENode', 'Could not determine Object Explorer node from connectionContext : {0}', JSON.stringify(connectionContext)));
return; return;
} }
@@ -122,12 +122,14 @@ async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: az
* @param connectionId The connection context from the command * @param connectionId The connection context from the command
*/ */
async function handleLaunchSsmsMinGswDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise<void> { async function handleLaunchSsmsMinGswDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise<void> {
const action = 'GenerateScripts';
if (!connectionContext) { if (!connectionContext) {
Telemetry.sendTelemetryEventForError('NoConnectionContext', { action: action });
vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionContextForGsw', 'No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand')); vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionContextForGsw', 'No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand'));
} }
launchSsmsDialog( launchSsmsDialog(
'GenerateScripts', action,
connectionContext); connectionContext);
} }
@@ -139,6 +141,7 @@ async function handleLaunchSsmsMinGswDialogCommand(connectionContext?: azdata.Ob
*/ */
async function launchSsmsDialog(action: string, connectionContext: azdata.ObjectExplorerContext): Promise<void> { async function launchSsmsDialog(action: string, connectionContext: azdata.ObjectExplorerContext): Promise<void> {
if (!connectionContext.connectionProfile) { if (!connectionContext.connectionProfile) {
Telemetry.sendTelemetryEventForError('NoConnectionProfile', { action: action });
vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionProfile', 'No connectionProfile provided from connectionContext : {0}', JSON.stringify(connectionContext))); vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionProfile', 'No connectionProfile provided from connectionContext : {0}', JSON.stringify(connectionContext)));
return; return;
} }
@@ -152,6 +155,7 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object
oeNode = await azdata.objectexplorer.getNode(connectionContext.connectionProfile.id, connectionContext.nodeInfo.nodePath); oeNode = await azdata.objectexplorer.getNode(connectionContext.connectionProfile.id, connectionContext.nodeInfo.nodePath);
} }
else { else {
Telemetry.sendTelemetryEventForError('NoOENode', { action: action });
vscode.window.showErrorMessage(localize('adminToolExtWin.noOENode', 'Could not determine Object Explorer node from connectionContext : {0}', JSON.stringify(connectionContext))); vscode.window.showErrorMessage(localize('adminToolExtWin.noOENode', 'Could not determine Object Explorer node from connectionContext : {0}', JSON.stringify(connectionContext)));
return; return;
} }
@@ -175,7 +179,12 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object
const args = buildSsmsMinCommandArgs(params); 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); vscode.window.setStatusBarMessage(localize('adminToolExtWin.launchingDialogStatus', 'Launching dialog...'), 3000);
@@ -186,11 +195,12 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object
(execException, stdout, stderr) => { (execException, stdout, stderr) => {
// Process has exited so remove from map of running processes // Process has exited so remove from map of running processes
runningProcesses.delete(proc.pid); runningProcesses.delete(proc.pid);
const err = stderr.toString();
Telemetry.sendTelemetryEvent('LaunchSsmsDialogResult', { Telemetry.sendTelemetryEvent('LaunchSsmsDialogResult', {
'action': params.action, action: params.action,
'returnCode': execException && execException.code ? execException.code.toString() : '0' returnCode: execException && execException.code ? execException.code.toString() : '0',
errorType: getTelemetryErrorType(err)
}); });
let err = stderr.toString();
if (err !== '') { if (err !== '') {
vscode.window.showErrorMessage(localize( vscode.window.showErrorMessage(localize(
'adminToolExtWin.ssmsMinError', 'adminToolExtWin.ssmsMinError',

View File

@@ -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( public static sendTelemetryEventForError(err: string, properties?: ITelemetryEventProperties): void {
err: any, methodName: string, extensionConfigName: string): void { this.sendTelemetryEvent('Error', { error: err, ...properties });
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);
}
} }
/** /**
@@ -106,7 +90,12 @@ export class Telemetry {
properties = {}; 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);
}
} }
} }

View File

@@ -36,4 +36,26 @@ export function doubleEscapeSingleQuotes(value: string): string {
*/ */
export function backEscapeDoubleQuotes(value: string): string { export function backEscapeDoubleQuotes(value: string): string {
return value.replace(/"/g, '\\"'); 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';
}
} }