From 3d494dcd73b0bb57d688cf13cac8d5cc058643b9 Mon Sep 17 00:00:00 2001 From: kisantia <31145923+kisantia@users.noreply.github.com> Date: Fri, 24 May 2019 15:32:23 -0700 Subject: [PATCH] Add initial telemetry for schema compare (#5595) * add initial telemetry to schema compare * addressing comments --- .../src/dialogs/schemaCompareDialog.ts | 5 + .../schema-compare/src/schemaCompareResult.ts | 44 +++++++- extensions/schema-compare/src/telemetry.ts | 102 ++++++++++++++++++ extensions/schema-compare/src/utils.ts | 34 ++++++ 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 extensions/schema-compare/src/telemetry.ts create mode 100644 extensions/schema-compare/src/utils.ts diff --git a/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts b/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts index 3236ab5426..038c9ef288 100644 --- a/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts +++ b/extensions/schema-compare/src/dialogs/schemaCompareDialog.ts @@ -11,6 +11,7 @@ import * as os from 'os'; import { SchemaCompareResult } from '../schemaCompareResult'; import { isNullOrUndefined } from 'util'; import { existsSync } from 'fs'; +import { Telemetry } from '../telemetry'; const localize = nls.loadMessageBundle(); const OkButtonText: string = localize('schemaCompareDialog.ok', 'Ok'); @@ -137,6 +138,10 @@ export class SchemaCompareDialog { }; } + Telemetry.sendTelemetryEvent('SchemaCompareStart', { + 'sourceIsDacpac': this.sourceIsDacpac.toString(), + 'targetIsDacpac': this.targetIsDacpac.toString() + }); let schemaCompareResult = new SchemaCompareResult(sourceName, targetName, sourceEndpointInfo, targetEndpointInfo); schemaCompareResult.start(); } diff --git a/extensions/schema-compare/src/schemaCompareResult.ts b/extensions/schema-compare/src/schemaCompareResult.ts index da5c51a7b2..48fde78d44 100644 --- a/extensions/schema-compare/src/schemaCompareResult.ts +++ b/extensions/schema-compare/src/schemaCompareResult.ts @@ -7,9 +7,10 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as os from 'os'; -import { SchemaCompareOptionsDialog } from './dialogs/schemaCompareOptionsDialog'; import * as path from 'path'; - +import { SchemaCompareOptionsDialog } from './dialogs/schemaCompareOptionsDialog'; +import { Telemetry } from './telemetry'; +import { getTelemetryErrorType } from './utils'; const localize = nls.loadMessageBundle(); const diffEditorTitle = localize('schemaCompare.ObjectDefinitionsTitle', 'Object Definitions'); @@ -180,13 +181,21 @@ export class SchemaCompareResult { // take updates if any this.deploymentOptions = this.schemaCompareOptionDialog.deploymentOptions; } - + Telemetry.sendTelemetryEvent('SchemaComparisonStarted'); let service = await SchemaCompareResult.getService('MSSQL'); this.comparisonResult = await service.schemaCompare(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions); if (!this.comparisonResult || !this.comparisonResult.success) { + Telemetry.sendTelemetryEventForError('SchemaComparisonFailed', { + 'errorType': getTelemetryErrorType(this.comparisonResult.errorMessage), + 'operationId': this.comparisonResult.operationId + }); vscode.window.showErrorMessage(localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", this.comparisonResult.errorMessage ? this.comparisonResult.errorMessage : 'Unknown')); return; } + Telemetry.sendTelemetryEvent('SchemaComparisonFinished', { + 'endTime': Date.now().toString(), + 'operationId': this.comparisonResult.operationId + }); let data = this.getAllDifferences(this.comparisonResult.differences); @@ -418,12 +427,24 @@ export class SchemaCompareResult { }).component(); this.generateScriptButton.onDidClick(async (click) => { + Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptStarted', { + 'startTime:': Date.now().toString(), + 'operationId': this.comparisonResult.operationId + }); let service = await SchemaCompareResult.getService('MSSQL'); let result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.script); if (!result || !result.success) { + Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptFailed', { + 'errorType': getTelemetryErrorType(result.errorMessage), + 'operationId': this.comparisonResult.operationId + }); vscode.window.showErrorMessage( localize('schemaCompare.generateScriptErrorMessage', "Generate script failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown')); } + Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptEnded', { + 'endTime:': Date.now().toString(), + 'operationId': this.comparisonResult.operationId + }); }); } @@ -438,6 +459,9 @@ export class SchemaCompareResult { }).component(); this.optionsButton.onDidClick(async (click) => { + Telemetry.sendTelemetryEvent('SchemaCompareOptionsOpened', { + 'operationId': this.comparisonResult.operationId + }); //restore options from last time if (this.schemaCompareOptionDialog && this.schemaCompareOptionDialog.deploymentOptions) { this.deploymentOptions = this.schemaCompareOptionDialog.deploymentOptions; @@ -459,12 +483,24 @@ export class SchemaCompareResult { }).component(); this.applyButton.onDidClick(async (click) => { + Telemetry.sendTelemetryEvent('SchemaCompareApplyStarted', { + 'startTime': Date.now().toString(), + 'operationId': this.comparisonResult.operationId + }); let service = await SchemaCompareResult.getService('MSSQL'); let result = await service.schemaComparePublishChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute); if (!result || !result.success) { + Telemetry.sendTelemetryEvent('SchemaCompareApplyFailed', { + 'errorType': getTelemetryErrorType(result.errorMessage), + 'operationId': this.comparisonResult.operationId + }); vscode.window.showErrorMessage( localize('schemaCompare.updateErrorMessage', "Schema Compare Apply failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown')); } + Telemetry.sendTelemetryEvent('SchemaCompareApplyEnded', { + 'endTime': Date.now().toString(), + 'operationId': this.comparisonResult.operationId + }); }); } @@ -496,6 +532,8 @@ export class SchemaCompareResult { }).component(); this.switchButton.onDidClick(async (click) => { + Telemetry.sendTelemetryEvent('SchemaCompareSwitch'); + // switch source and target [this.sourceEndpointInfo, this.targetEndpointInfo] = [this.targetEndpointInfo, this.sourceEndpointInfo]; [this.sourceName, this.targetName] = [this.targetName, this.sourceName]; diff --git a/extensions/schema-compare/src/telemetry.ts b/extensions/schema-compare/src/telemetry.ts new file mode 100644 index 0000000000..2f392f703d --- /dev/null +++ b/extensions/schema-compare/src/telemetry.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import * as vscode from 'vscode'; +import TelemetryReporter from 'vscode-extension-telemetry'; + +import * as Utils from './utils'; + +const packageJson = require('../package.json'); + +export interface ITelemetryEventProperties { + [key: string]: string; +} + +export interface ITelemetryEventMeasures { + [key: string]: number; +} + +/** + * Filters error paths to only include source files. Exported to support testing + */ +export function filterErrorPath(line: string): string { + if (line) { + let values: string[] = line.split('/out/'); + if (values.length <= 1) { + // Didn't match expected format + return line; + } else { + return values[1]; + } + } +} + +export class Telemetry { + private static reporter: TelemetryReporter; + private static disabled: boolean; + + /** + * Disable telemetry reporting + */ + public static disable(): void { + this.disabled = true; + } + + /** + * Initialize the telemetry reporter for use. + */ + public static initialize(): void { + if (typeof this.reporter === 'undefined') { + // Check if the user has opted out of telemetry + if (!vscode.workspace.getConfiguration('telemetry').get('enableTelemetry', true)) { + this.disable(); + return; + } + + let packageInfo = Utils.getPackageInfo(packageJson); + this.reporter = new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); + } + } + + /** + * Send a telemetry event for a general error + * @param err The error to log + */ + public static sendTelemetryEventForError(err: string, properties?: ITelemetryEventProperties): void { + this.sendTelemetryEvent('Error', { error: err, ...properties }); + } + + /** + * Send a telemetry event using application insights + */ + public static sendTelemetryEvent( + eventName: string, + properties?: ITelemetryEventProperties, + measures?: ITelemetryEventMeasures): void { + + if (typeof this.disabled === 'undefined') { + this.disabled = false; + } + + if (this.disabled || typeof (this.reporter) === 'undefined') { + // Don't do anything if telemetry is disabled + return; + } + + if (!properties || typeof properties === 'undefined') { + properties = {}; + } + + 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); + } + } +} + +Telemetry.initialize(); diff --git a/extensions/schema-compare/src/utils.ts b/extensions/schema-compare/src/utils.ts new file mode 100644 index 0000000000..770a4f4060 --- /dev/null +++ b/extensions/schema-compare/src/utils.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export interface IPackageInfo { + name: string; + version: string; + aiKey: string; +} + +export function getPackageInfo(packageJson: any): IPackageInfo { + if (packageJson) { + return { + name: packageJson.name, + version: packageJson.version, + aiKey: packageJson.aiKey + }; + } +} + +/** + * Map an error message into a short name for the type of error. + * @param msg The error message to map + */ +export function getTelemetryErrorType(msg: string): string { + if (msg.indexOf('Object reference not set to an instance of an object') !== -1) { + return 'ObjectReferenceNotSet'; + } + else { + return 'Other'; + } +} \ No newline at end of file