Adding Dacpac extension telemetry and core wizard/page telemetry updates(#13859)

* Dacpac telmetry code changes

* Removed added spaces

* Generate deployScript accessibility changed back to public

* code review suggessions updates

* dacpac extension tests fixes

* Updated time and filesize methods allowing general return values

* Telemetry code updates

* Dacpac Telemetry potential data loss capture and PII error excluded

* Dacpac telemetry code updates for comments

* Wizard pages navigation telemetry event capture moved to the core

* DacpacTelemetry code updates

* Extension wizard cancel telemetry for data loss

* Dacpac telemetry pagename and small code updates

* final Dacpac telemetry code updates...
This commit is contained in:
Sai Avishkar Sreerama
2021-01-21 17:00:37 -06:00
committed by GitHub
parent 07d798c949
commit 0316d9ac57
16 changed files with 282 additions and 63 deletions

View File

@@ -14,5 +14,10 @@ let packageInfo = Utils.getPackageInfo(packageJson);
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
export enum TelemetryViews { export enum TelemetryViews {
SelectOperationPage = 'SelectOperationPage' DataTierApplicationWizard = 'DataTierApplicationWizard',
DeployDacpac = 'DeployDacpac',
DeployPlanPage = 'DeployPlanPage',
ExportBacpac = 'ExportBacpac',
ExtractDacpac = 'ExtractDacpac',
ImportBacpac = 'ImportBacpac'
} }

View File

@@ -10,6 +10,7 @@ import * as should from 'should';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import * as loc from '../localizedConstants'; import * as loc from '../localizedConstants';
import * as utils from '../utils';
import { DataTierApplicationWizard, Operation } from '../wizard/dataTierApplicationWizard'; import { DataTierApplicationWizard, Operation } from '../wizard/dataTierApplicationWizard';
import { DacFxDataModel } from '../wizard/api/models'; import { DacFxDataModel } from '../wizard/api/models';
import { DacFxTestService, deployOperationId, extractOperationId, importOperationId, exportOperationId, generateDeployPlan } from './testDacFxService'; import { DacFxTestService, deployOperationId, extractOperationId, importOperationId, exportOperationId, generateDeployPlan } from './testDacFxService';
@@ -56,6 +57,11 @@ describe('Dacfx wizard with connection', function (): void {
it('Should call all service methods correctly', async () => { it('Should call all service methods correctly', async () => {
wizard.model.server = connectionProfileMock; wizard.model.server = connectionProfileMock;
wizard.model.potentialDataLoss = true;
wizard.model.upgradeExisting = true;
const fileSizeStub = sinon.stub(utils, 'tryGetFileSize');
fileSizeStub.resolves(TypeMoq.It.isAnyNumber());
await validateServiceCalls(wizard, Operation.deploy, deployOperationId); await validateServiceCalls(wizard, Operation.deploy, deployOperationId);
await validateServiceCalls(wizard, Operation.extract, extractOperationId); await validateServiceCalls(wizard, Operation.extract, extractOperationId);
@@ -74,6 +80,10 @@ describe('Dacfx wizard with connection', function (): void {
let wizard = new DataTierApplicationWizard(service.object); let wizard = new DataTierApplicationWizard(service.object);
wizard.model = <DacFxDataModel>{}; wizard.model = <DacFxDataModel>{};
wizard.model.server = connectionProfileMock; wizard.model.server = connectionProfileMock;
wizard.model.potentialDataLoss = true;
wizard.model.upgradeExisting = true;
const fileSizeStub = sinon.stub(utils, 'tryGetFileSize');
fileSizeStub.resolves(TypeMoq.It.isAnyNumber());
let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves(); let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves();
wizard.selectedOperation = Operation.deploy; wizard.selectedOperation = Operation.deploy;
await wizard.executeOperation(); await wizard.executeOperation();
@@ -92,6 +102,8 @@ describe('Dacfx wizard with connection', function (): void {
let wizard = new DataTierApplicationWizard(service.object); let wizard = new DataTierApplicationWizard(service.object);
wizard.model = <DacFxDataModel>{}; wizard.model = <DacFxDataModel>{};
wizard.model.server = connectionProfileMock; wizard.model.server = connectionProfileMock;
const fileSizeStub = sinon.stub(utils, 'tryGetFileSize');
fileSizeStub.resolves(TypeMoq.It.isAnyNumber());
let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves(); let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves();
wizard.selectedOperation = Operation.export; wizard.selectedOperation = Operation.export;
await wizard.executeOperation(); await wizard.executeOperation();
@@ -110,6 +122,8 @@ describe('Dacfx wizard with connection', function (): void {
let wizard = new DataTierApplicationWizard(service.object); let wizard = new DataTierApplicationWizard(service.object);
wizard.model = <DacFxDataModel>{}; wizard.model = <DacFxDataModel>{};
wizard.model.server = connectionProfileMock; wizard.model.server = connectionProfileMock;
const fileSizeStub = sinon.stub(utils, 'tryGetFileSize');
fileSizeStub.resolves(TypeMoq.It.isAnyNumber());
let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves(); let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves();
wizard.selectedOperation = Operation.extract; wizard.selectedOperation = Operation.extract;
await wizard.executeOperation(); await wizard.executeOperation();
@@ -128,6 +142,9 @@ describe('Dacfx wizard with connection', function (): void {
let wizard = new DataTierApplicationWizard(service.object); let wizard = new DataTierApplicationWizard(service.object);
wizard.model = <DacFxDataModel>{}; wizard.model = <DacFxDataModel>{};
wizard.model.server = connectionProfileMock; wizard.model.server = connectionProfileMock;
wizard.model.potentialDataLoss = true;
const fileSizeStub = sinon.stub(utils, 'tryGetFileSize');
fileSizeStub.resolves(TypeMoq.It.isAnyNumber());
let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves(); let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves();
await wizard.generateDeployScript(); await wizard.generateDeployScript();
should(showErrorMessageStub.calledOnce).be.true(); should(showErrorMessageStub.calledOnce).be.true();
@@ -145,6 +162,8 @@ describe('Dacfx wizard with connection', function (): void {
let wizard = new DataTierApplicationWizard(service.object); let wizard = new DataTierApplicationWizard(service.object);
wizard.model = <DacFxDataModel>{}; wizard.model = <DacFxDataModel>{};
wizard.model.server = connectionProfileMock; wizard.model.server = connectionProfileMock;
const fileSizeStub = sinon.stub(utils, 'tryGetFileSize');
fileSizeStub.resolves(TypeMoq.It.isAnyNumber());
let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves(); let showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves();
wizard.selectedOperation = Operation.import; wizard.selectedOperation = Operation.import;
await wizard.executeOperation(); await wizard.executeOperation();

View File

@@ -3,6 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
export interface IPackageInfo { export interface IPackageInfo {
name: string; name: string;
version: string; version: string;
@@ -22,14 +24,16 @@ export function getPackageInfo(packageJson: any): IPackageInfo | undefined {
} }
/** /**
* Map an error message into a short name for the type of error. * Get file size from the file stats using the file path uri
* @param msg The error message to map * If the file does not exists, purposely returning undefined instead of throwing an error for telemetry purpose.
* @param uri The file path
*/ */
export function getTelemetryErrorType(msg: string): string { export async function tryGetFileSize(uri: string): Promise<number | undefined> {
if (msg && msg.indexOf('Object reference not set to an instance of an object') !== -1) { try {
return 'ObjectReferenceNotSet'; const stats = await fs.promises.stat(uri);
return stats?.size;
} }
else { catch (e) {
return 'Other'; return undefined;
} }
} }

View File

@@ -16,4 +16,5 @@ export interface DacFxDataModel {
filePath: string; filePath: string;
version: string; version: string;
upgradeExisting: boolean; upgradeExisting: boolean;
potentialDataLoss: boolean;
} }

View File

@@ -6,6 +6,8 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as loc from '../localizedConstants'; import * as loc from '../localizedConstants';
import * as mssql from '../../../mssql';
import * as utils from '../utils';
import { SelectOperationPage } from './pages/selectOperationpage'; import { SelectOperationPage } from './pages/selectOperationpage';
import { DeployConfigPage } from './pages/deployConfigPage'; import { DeployConfigPage } from './pages/deployConfigPage';
import { DeployPlanPage } from './pages/deployPlanPage'; import { DeployPlanPage } from './pages/deployPlanPage';
@@ -15,7 +17,8 @@ import { ExtractConfigPage } from './pages/extractConfigPage';
import { ImportConfigPage } from './pages/importConfigPage'; import { ImportConfigPage } from './pages/importConfigPage';
import { DacFxDataModel } from './api/models'; import { DacFxDataModel } from './api/models';
import { BasePage } from './api/basePage'; import { BasePage } from './api/basePage';
import * as mssql from '../../../mssql'; import { TelemetryReporter, TelemetryViews } from '../telemetry';
import { TelemetryEventMeasures, TelemetryEventProperties } from 'ads-extension-telemetry';
const msSqlProvider = 'MSSQL'; const msSqlProvider = 'MSSQL';
class Page { class Page {
@@ -84,7 +87,7 @@ export class DataTierApplicationWizard {
public selectedOperation: Operation; public selectedOperation: Operation;
constructor(dacfxInputService?: mssql.IDacFxService) { constructor(dacfxInputService?: mssql.IDacFxService) {
this.wizard = azdata.window.createWizard(loc.wizardTitle); this.wizard = azdata.window.createWizard(loc.wizardTitle, 'Data Tier Application Wizard');
this.dacfxService = dacfxInputService; this.dacfxService = dacfxInputService;
} }
@@ -110,6 +113,8 @@ export class DataTierApplicationWizard {
} }
// don't open the wizard if connection dialog is cancelled // don't open the wizard if connection dialog is cancelled
if (!this.connection) { if (!this.connection) {
//Reporting Dacpac wizard cancelled event to Telemetry
TelemetryReporter.sendActionEvent(TelemetryViews.DataTierApplicationWizard, 'ConnectionDialogCancelled');
return false; return false;
} }
} }
@@ -123,13 +128,13 @@ export class DataTierApplicationWizard {
} }
public setPages(): void { public setPages(): void {
let selectOperationWizardPage = azdata.window.createWizardPage(loc.selectOperationPageName); let selectOperationWizardPage = azdata.window.createWizardPage(loc.selectOperationPageName, 'Select an Operation Page');
let deployConfigWizardPage = azdata.window.createWizardPage(loc.deployConfigPageName); let deployConfigWizardPage = azdata.window.createWizardPage(loc.deployConfigPageName, 'Deploy Config Page');
let deployPlanWizardPage = azdata.window.createWizardPage(loc.deployPlanPageName); let deployPlanWizardPage = azdata.window.createWizardPage(loc.deployPlanPageName, 'Deploy Plan Page');
let summaryWizardPage = azdata.window.createWizardPage(loc.summaryPageName); let summaryWizardPage = azdata.window.createWizardPage(loc.summaryPageName, 'Summary Page');
let extractConfigWizardPage = azdata.window.createWizardPage(loc.extractConfigPageName); let extractConfigWizardPage = azdata.window.createWizardPage(loc.extractConfigPageName, 'Extract Config Page');
let importConfigWizardPage = azdata.window.createWizardPage(loc.importConfigPageName); let importConfigWizardPage = azdata.window.createWizardPage(loc.importConfigPageName, 'Import Config Page');
let exportConfigWizardPage = azdata.window.createWizardPage(loc.exportConfigPageName); let exportConfigWizardPage = azdata.window.createWizardPage(loc.exportConfigPageName, 'Export Config Page');
this.pages.set(PageName.selectOperation, new Page(selectOperationWizardPage)); this.pages.set(PageName.selectOperation, new Page(selectOperationWizardPage));
this.pages.set(PageName.deployConfig, new Page(deployConfigWizardPage)); this.pages.set(PageName.deployConfig, new Page(deployConfigWizardPage));
@@ -206,6 +211,7 @@ export class DataTierApplicationWizard {
this.wizard.generateScriptButton.hidden = true; this.wizard.generateScriptButton.hidden = true;
this.wizard.generateScriptButton.onClick(async () => await this.generateDeployScript()); this.wizard.generateScriptButton.onClick(async () => await this.generateDeployScript());
this.wizard.doneButton.onClick(async () => await this.executeOperation()); this.wizard.doneButton.onClick(async () => await this.executeOperation());
this.wizard.cancelButton.onClick(() => this.cancelDataTierApplicationWizard());
} }
public registerNavigationValidator(validator: (pageChangeInfo: azdata.window.WizardPageChangeInfo) => boolean) { public registerNavigationValidator(validator: (pageChangeInfo: azdata.window.WizardPageChangeInfo) => boolean) {
@@ -287,48 +293,161 @@ export class DataTierApplicationWizard {
} }
} }
public async deploy(): Promise<mssql.DacFxResult> { // Cancel button on click event is using to send the data loss information to telemetry
const service = await this.getService(msSqlProvider); private cancelDataTierApplicationWizard(): void {
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId); TelemetryReporter.createActionEvent(TelemetryViews.DataTierApplicationWizard, 'WizardCanceled')
.withAdditionalProperties({
isPotentialDataLoss: this.model.potentialDataLoss.toString()
}).send();
}
return await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, azdata.TaskExecutionMode.execute); public async deploy(): Promise<mssql.DacFxResult> {
const deployStartTime = new Date().getTime();
let service: mssql.IDacFxService;
let ownerUri: string;
let result: mssql.DacFxResult;
let additionalProps: TelemetryEventProperties = {};
let additionalMeasurements: TelemetryEventMeasures = {};
try {
service = await this.getService(msSqlProvider);
ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, azdata.TaskExecutionMode.execute);
} catch (e) {
additionalProps.exceptionOccurred = 'true';
}
// If result is null which means exception occured, will be adding additional props to the Telemetry
if (!result) {
additionalProps = { ...additionalProps, ...this.getDacServiceArgsAsProps(service, this.model.database, this.model.filePath, ownerUri) };
}
additionalProps.deploymentStatus = result?.success.toString();
additionalProps.upgradeExistingDatabase = this.model.upgradeExisting.toString();
additionalProps.potentialDataLoss = this.model.potentialDataLoss.toString();
additionalMeasurements.deployDacpacFileSizeBytes = await utils.tryGetFileSize(this.model.filePath);
additionalMeasurements.totalDurationMs = (new Date().getTime() - deployStartTime);
// Deploy Dacpac: 'Deploy button' clicked in deploy summary page, Reporting the event selection to the telemetry
this.sendDacServiceTelemetryEvent(TelemetryViews.DeployDacpac, 'DeployDacpacOperation', additionalProps, additionalMeasurements);
return result;
} }
private async extract(): Promise<mssql.DacFxResult> { private async extract(): Promise<mssql.DacFxResult> {
const service = await this.getService(msSqlProvider); const extractStartTime = new Date().getTime();
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId); let service: mssql.IDacFxService;
let ownerUri: string;
let result: mssql.DacFxResult;
let additionalProps: TelemetryEventProperties = {};
let additionalMeasurements: TelemetryEventMeasures = {};
try {
service = await this.getService(msSqlProvider);
ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, azdata.TaskExecutionMode.execute);
} catch (e) {
additionalProps.exceptionOccurred = 'true';
}
return await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, azdata.TaskExecutionMode.execute); // If result is null which means exception occured, will be adding additional props to the Telemetry
if (!result) {
additionalProps = { ...additionalProps, ...this.getDacServiceArgsAsProps(service, this.model.database, this.model.filePath, ownerUri) };
}
additionalProps.extractStatus = result?.success.toString();
additionalMeasurements.extractedDacpacFileSizeBytes = await utils.tryGetFileSize(this.model.filePath);
additionalMeasurements.totalDurationMs = (new Date().getTime() - extractStartTime);
// Extract Dacpac: 'Extract button' clicked in extract summary page, Reporting the event selection to the telemetry
this.sendDacServiceTelemetryEvent(TelemetryViews.ExtractDacpac, 'ExtractDacpacOperation', additionalProps, additionalMeasurements);
return result;
} }
private async export(): Promise<mssql.DacFxResult> { private async export(): Promise<mssql.DacFxResult> {
const service = await this.getService(msSqlProvider); const exportStartTime = new Date().getTime();
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId); let service: mssql.IDacFxService;
let ownerUri: string;
let result: mssql.DacFxResult;
let additionalProps: TelemetryEventProperties = {};
let additionalMeasurements: TelemetryEventMeasures = {};
try {
service = await this.getService(msSqlProvider);
ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, azdata.TaskExecutionMode.execute);
} catch (e) {
additionalProps.exceptionOccurred = 'true';
}
return await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, azdata.TaskExecutionMode.execute); // If result is null which means exception occured, will be adding additional props to the Telemetry
if (!result) {
additionalProps = { ...additionalProps, ...this.getDacServiceArgsAsProps(service, this.model.database, this.model.filePath, ownerUri) };
}
additionalProps.exportStatus = result?.success.toString();
additionalMeasurements.exportedBacpacFileSizeBytes = await utils.tryGetFileSize(this.model.filePath);
additionalMeasurements.totalDurationMs = (new Date().getTime() - exportStartTime);
// Export Bacpac: 'Export button' clicked in Export summary page, Reporting the event selection to the telemetry
this.sendDacServiceTelemetryEvent(TelemetryViews.ExportBacpac, 'ExportBacpacOperation', additionalProps, additionalMeasurements);
return result;
} }
private async import(): Promise<mssql.DacFxResult> { private async import(): Promise<mssql.DacFxResult> {
const service = await this.getService(msSqlProvider); const importStartTime = new Date().getTime();
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId); let service: mssql.IDacFxService;
let ownerUri: string;
let result: mssql.DacFxResult;
let additionalProps: TelemetryEventProperties = {};
let additionalMeasurements: TelemetryEventMeasures = {};
try {
service = await this.getService(msSqlProvider);
ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute);
} catch (e) {
additionalProps.exceptionOccurred = 'true';
}
return await service.importBacpac(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute); // If result is null which means exception occured, will be adding additional props to the Telemetry
if (!result) {
additionalProps = { ...additionalProps, ...this.getDacServiceArgsAsProps(service, this.model.database, this.model.filePath, ownerUri) };
}
additionalProps.importStatus = result?.success.toString();
additionalMeasurements.importedBacpacFileSizeBytes = await utils.tryGetFileSize(this.model.filePath);
additionalMeasurements.totalDurationMs = (new Date().getTime() - importStartTime);
// Import Bacpac: 'Import button' clicked in Import summary page, Reporting the event selection to the telemetry
this.sendDacServiceTelemetryEvent(TelemetryViews.ImportBacpac, 'ImportBacpacOperation', additionalProps, additionalMeasurements);
return result;
} }
public async generateDeployScript(): Promise<mssql.DacFxResult> { public async generateDeployScript(): Promise<mssql.DacFxResult> {
const service = await this.getService(msSqlProvider); const genScriptStartTime = new Date().getTime();
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId); let service: mssql.IDacFxService;
let ownerUri: string;
let result: mssql.DacFxResult;
let additionalProps: TelemetryEventProperties = {};
let additionalMeasurements: TelemetryEventMeasures = {};
try {
this.wizard.message = { this.wizard.message = {
text: loc.generatingScriptMessage, text: loc.generatingScriptMessage,
level: azdata.window.MessageLevel.Information, level: azdata.window.MessageLevel.Information,
description: '' description: ''
}; };
let result = await service.generateDeployScript(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.script); service = await this.getService(msSqlProvider);
ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
result = await service.generateDeployScript(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.script);
} catch (e) {
additionalProps.exceptionOccurred = 'true';
}
if (!result || !result.success) { if (!result || !result.success) {
vscode.window.showErrorMessage(loc.generateDeployErrorMessage(result?.errorMessage)); vscode.window.showErrorMessage(loc.generateDeployErrorMessage(result?.errorMessage));
} }
// If result is null which means exception occured, will be adding additional props to the Telemetry
if (!result) {
additionalProps = { ...additionalProps, ...this.getDacServiceArgsAsProps(service, this.model.database, this.model.filePath, ownerUri) };
}
additionalProps.isScriptGenerated = result?.success.toString();
additionalProps.potentialDataLoss = this.model.potentialDataLoss.toString();
additionalMeasurements.deployDacpacFileSizeBytes = await utils.tryGetFileSize(this.model.filePath);
additionalMeasurements.totalDurationMs = (new Date().getTime() - genScriptStartTime);
// Deploy Dacpac 'generate script' button clicked in DeployPlanPage, Reporting the event selection to the telemetry with fail/sucess status
this.sendDacServiceTelemetryEvent(TelemetryViews.DeployDacpac, 'GenerateDeployScriptOperation', additionalProps, additionalMeasurements);
return result; return result;
} }
@@ -372,15 +491,32 @@ export class DataTierApplicationWizard {
} }
public async generateDeployPlan(): Promise<string> { public async generateDeployPlan(): Promise<string> {
const service = await this.getService(msSqlProvider); const deployPlanStartTime = new Date().getTime();
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId); let service: mssql.IDacFxService;
let ownerUri: string;
const result = await service.generateDeployPlan(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute); let result: mssql.GenerateDeployPlanResult;
let additionalProps: TelemetryEventProperties = {};
let additionalMeasurements: TelemetryEventMeasures = {};
try {
service = await this.getService(msSqlProvider);
ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
result = await service.generateDeployPlan(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute);
} catch (e) {
additionalProps.exceptionOccurred = 'true';
}
if (!result || !result.success) { if (!result || !result.success) {
vscode.window.showErrorMessage(loc.deployPlanErrorMessage(result?.errorMessage)); vscode.window.showErrorMessage(loc.deployPlanErrorMessage(result?.errorMessage));
} }
// If result is null which means exception occured, will be adding additional props to the Telemetry
if (!result) {
additionalProps = { ...additionalProps, ...this.getDacServiceArgsAsProps(service, this.model.database, this.model.filePath, ownerUri) };
}
additionalProps.isPlanGenerated = result?.success.toString();
additionalMeasurements.totalDurationMs = (new Date().getTime() - deployPlanStartTime);
// send Generate deploy plan error/succes telemetry event
this.sendDacServiceTelemetryEvent(TelemetryViews.DeployPlanPage, 'GenerateDeployPlanOperation', additionalProps, additionalMeasurements);
return result.report; return result.report;
} }
@@ -390,4 +526,20 @@ export class DataTierApplicationWizard {
} }
return this.dacfxService; return this.dacfxService;
} }
public getDacServiceArgsAsProps(service: mssql.IDacFxService, database: string, filePath: string, ownerUri: string): { [k: string]: string } {
return {
isServiceExist: (!!service).toString(),
isDatabaseExists: (!!database).toString(),
isFilePathExist: (!!filePath).toString(),
isOwnerUriExist: (!!ownerUri).toString()
};
}
private sendDacServiceTelemetryEvent(telemetryView: string, telemetryAction: string, additionalProps: TelemetryEventProperties, additionalMeasurements: TelemetryEventMeasures): void {
TelemetryReporter.createActionEvent(telemetryView, telemetryAction)
.withAdditionalProperties(additionalProps)
.withAdditionalMeasurements(additionalMeasurements)
.send();
}
} }

View File

@@ -77,12 +77,14 @@ export class DeployPlanPage extends DacFxConfigPage {
this.formBuilder.addFormItem(this.dataLossComponentGroup, { horizontal: true, componentWidth: 400 }); this.formBuilder.addFormItem(this.dataLossComponentGroup, { horizontal: true, componentWidth: 400 });
this.dataLossCheckbox.checked = false; this.dataLossCheckbox.checked = false;
this.dataLossCheckbox.enabled = false; this.dataLossCheckbox.enabled = false;
this.model.potentialDataLoss = false;
this.formBuilder.removeFormItem(this.noDataLossTextComponent); this.formBuilder.removeFormItem(this.noDataLossTextComponent);
this.loader.loading = true; this.loader.loading = true;
this.table.data = []; this.table.data = [];
await this.populateTable(); await this.populateTable();
this.loader.loading = false; this.loader.loading = false;
return true; return true;
} }
@@ -103,6 +105,7 @@ export class DeployPlanPage extends DacFxConfigPage {
value: loc.dataLossTextWithCount(result.dataLossAlerts.size) value: loc.dataLossTextWithCount(result.dataLossAlerts.size)
}); });
this.dataLossCheckbox.enabled = true; this.dataLossCheckbox.enabled = true;
this.model.potentialDataLoss = true;
} else { } else {
// check checkbox to enable Next button and remove checkbox because there won't be any possible data loss // check checkbox to enable Next button and remove checkbox because there won't be any possible data loss
this.dataLossCheckbox.checked = true; this.dataLossCheckbox.checked = true;

View File

@@ -644,6 +644,13 @@ declare module 'azdata' {
width?: DialogWidth; width?: DialogWidth;
} }
export interface WizardPage extends ModelViewPanel {
/**
* An optional name for the page. If provided it will be used for telemetry
*/
pageName?: string;
}
export type DialogWidth = 'narrow' | 'medium' | 'wide' | number; export type DialogWidth = 'narrow' | 'medium' | 'wide' | number;
/** /**
@@ -661,6 +668,13 @@ declare module 'azdata' {
* @param width The width of the wizard, default value is 'narrow' * @param width The width of the wizard, default value is 'narrow'
*/ */
export function createWizard(title: string, name?: string, width?: DialogWidth): Wizard; export function createWizard(title: string, name?: string, width?: DialogWidth): Wizard;
/**
* Create a wizard page with the given title, for inclusion in a wizard
* @param title The title of the page
* @param pageName The optional page name parameter will be used for telemetry
*/
export function createWizardPage(title: string, pageName?: string): WizardPage;
} }
export namespace workspace { export namespace workspace {

View File

@@ -27,6 +27,7 @@ export const FirewallRuleRequested = 'FirewallRuleCreated';
export const DashboardNavigated = 'DashboardNavigated'; export const DashboardNavigated = 'DashboardNavigated';
export const GetDataGridItems = 'GetDataGridItems'; export const GetDataGridItems = 'GetDataGridItems';
export const GetDataGridColumns = 'GetDataGridColumns'; export const GetDataGridColumns = 'GetDataGridColumns';
export const WizardPagesNavigation = 'WizardPagesNavigation';
// Telemetry Properties // Telemetry Properties

View File

@@ -151,7 +151,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
public $setWizardPageDetails(handle: number, details: IModelViewWizardPageDetails): Thenable<void> { public $setWizardPageDetails(handle: number, details: IModelViewWizardPageDetails): Thenable<void> {
let page = this._wizardPages.get(handle); let page = this._wizardPages.get(handle);
if (!page) { if (!page) {
page = new WizardPage(details.title, details.content); page = new WizardPage(details.title, details.content, details.pageName);
page.onValidityChanged(valid => this._proxy.$onPanelValidityChanged(handle, valid)); page.onValidityChanged(valid => this._proxy.$onPanelValidityChanged(handle, valid));
this._wizardPages.set(handle, page); this._wizardPages.set(handle, page);
this._wizardPageHandles.set(page, handle); this._wizardPageHandles.set(page, handle);
@@ -161,6 +161,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
page.content = details.content; page.content = details.content;
page.enabled = details.enabled; page.enabled = details.enabled;
page.description = details.description; page.description = details.description;
page.pageName = details.pageName;
if (details.customButtons !== undefined) { if (details.customButtons !== undefined) {
page.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle)); page.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle));
} }

View File

@@ -324,7 +324,8 @@ class WizardPageImpl extends ModelViewPanelImpl implements azdata.window.WizardP
constructor(public title: string, constructor(public title: string,
extHostModelViewDialog: ExtHostModelViewDialog, extHostModelViewDialog: ExtHostModelViewDialog,
extHostModelView: ExtHostModelViewShape, extHostModelView: ExtHostModelViewShape,
extension: IExtensionDescription) { extension: IExtensionDescription,
public pageName?: string) {
super('modelViewWizardPage', extHostModelViewDialog, extHostModelView, extension); super('modelViewWizardPage', extHostModelViewDialog, extHostModelView, extension);
} }
@@ -758,8 +759,8 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
this._pageInfoChangedCallbacks.set(handle, callback); this._pageInfoChangedCallbacks.set(handle, callback);
} }
public createWizardPage(title: string, extension?: IExtensionDescription): azdata.window.WizardPage { public createWizardPage(title: string, extension?: IExtensionDescription, pageName?: string): azdata.window.WizardPage {
let page = new WizardPageImpl(title, this, this._extHostModelView, extension); let page = new WizardPageImpl(title, this, this._extHostModelView, extension, pageName);
page.handle = this.getHandle(page); page.handle = this.getHandle(page);
return page; return page;
} }
@@ -781,7 +782,8 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
customButtons: page.customButtons ? page.customButtons.map(button => this.getHandle(button)) : undefined, customButtons: page.customButtons ? page.customButtons.map(button => this.getHandle(button)) : undefined,
enabled: page.enabled, enabled: page.enabled,
title: page.title, title: page.title,
description: page.description description: page.description,
pageName: page.pageName
}); });
} }

View File

@@ -435,8 +435,8 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp
closeDialog(dialog: azdata.window.Dialog) { closeDialog(dialog: azdata.window.Dialog) {
return extHostModelViewDialog.closeDialog(dialog); return extHostModelViewDialog.closeDialog(dialog);
}, },
createWizardPage(title: string): azdata.window.WizardPage { createWizardPage(title: string, pageName?: string): azdata.window.WizardPage {
return extHostModelViewDialog.createWizardPage(title, extension); return extHostModelViewDialog.createWizardPage(title, extension, pageName);
}, },
createWizard(title: string, name?: string, width?: azdata.window.DialogWidth): azdata.window.Wizard { createWizard(title: string, name?: string, width?: azdata.window.DialogWidth): azdata.window.Wizard {
return extHostModelViewDialog.createWizard(title, name, width); return extHostModelViewDialog.createWizard(title, name, width);

View File

@@ -278,6 +278,7 @@ export interface IModelViewWizardPageDetails {
enabled: boolean; enabled: boolean;
customButtons: number[]; customButtons: number[];
description: string; description: string;
pageName?: string;
} }
export interface IModelViewWizardDetails { export interface IModelViewWizardDetails {

View File

@@ -391,14 +391,15 @@ export abstract class Modal extends Disposable implements IThemable {
/** /**
* Hides the modal and removes key listeners * Hides the modal and removes key listeners
*/ */
protected hide(reason?: string) { protected hide(reason?: string, currentPageName?: string): void {
this._modalShowingContext.get()!.pop(); this._modalShowingContext.get()!.pop();
this._bodyContainer!.remove(); this._bodyContainer!.remove();
this.disposableStore.clear(); this.disposableStore.clear();
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Shell, TelemetryKeys.ModalDialogClosed) this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Shell, TelemetryKeys.ModalDialogClosed)
.withAdditionalProperties({ .withAdditionalProperties({
name: this._name, name: this._name,
reason: reason reason: reason,
currentPageName: currentPageName
}) })
.send(); .send();
this.restoreKeyboardFocus(); this.restoreKeyboardFocus();

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import 'vs/css!./media/dialogModal'; import 'vs/css!./media/dialogModal';
import { Modal, IModalOptions } from 'sql/workbench/browser/modal/modal'; import { Modal, IModalOptions } from 'sql/workbench/browser/modal/modal';
import { Wizard, DialogButton, WizardPage } from 'sql/workbench/services/dialog/common/dialogTypes'; import { Wizard, DialogButton, WizardPage } from 'sql/workbench/services/dialog/common/dialogTypes';
@@ -49,14 +50,14 @@ export class WizardModal extends Modal {
options: IModalOptions, options: IModalOptions,
@ILayoutService layoutService: ILayoutService, @ILayoutService layoutService: ILayoutService,
@IThemeService themeService: IThemeService, @IThemeService themeService: IThemeService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService, @IAdsTelemetryService private _telemetryEventService: IAdsTelemetryService,
@IContextKeyService contextKeyService: IContextKeyService, @IContextKeyService contextKeyService: IContextKeyService,
@IInstantiationService private _instantiationService: IInstantiationService, @IInstantiationService private _instantiationService: IInstantiationService,
@IClipboardService clipboardService: IClipboardService, @IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService, @ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
) { ) {
super(_wizard.title, _wizard.name, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, options); super(_wizard.title, _wizard.name, _telemetryEventService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, options);
this._useDefaultMessageBoxLocation = false; this._useDefaultMessageBoxLocation = false;
} }
@@ -175,6 +176,7 @@ export class WizardModal extends Modal {
public async showPage(index: number, validate: boolean = true, focus: boolean = false, readHeader: boolean = true): Promise<void> { public async showPage(index: number, validate: boolean = true, focus: boolean = false, readHeader: boolean = true): Promise<void> {
let pageToShow = this._wizard.pages[index]; let pageToShow = this._wizard.pages[index];
const prevPageIndex = this._wizard.currentPage;
if (!pageToShow) { if (!pageToShow) {
this.done(validate).catch(err => onUnexpectedError(err)); this.done(validate).catch(err => onUnexpectedError(err));
return; return;
@@ -209,6 +211,15 @@ export class WizardModal extends Modal {
this._doneButton.enabled = this._wizard.doneButton.enabled && pageToShow.valid; this._doneButton.enabled = this._wizard.doneButton.enabled && pageToShow.valid;
} }
}); });
if (index !== prevPageIndex) {
this._telemetryEventService.createActionEvent(TelemetryKeys.TelemetryView.Shell, TelemetryKeys.WizardPagesNavigation)
.withAdditionalProperties({
wizardName: this._wizard.name,
pageNavigationFrom: this._wizard.pages[prevPageIndex].pageName ?? prevPageIndex,
pageNavigationTo: this._wizard.pages[index].pageName ?? index
}).send();
}
} }
private setButtonsForPage(index: number) { private setButtonsForPage(index: number) {
@@ -268,9 +279,10 @@ export class WizardModal extends Modal {
} }
public cancel(): void { public cancel(): void {
const currentPage = this._wizard.pages[this._wizard.currentPage];
this._onCancel.fire(); this._onCancel.fire();
this.dispose(); this.dispose();
this.hide('cancel'); this.hide('cancel', currentPage.pageName ?? this._wizard.currentPage.toString());
} }
private async validateNavigation(newPage: number): Promise<boolean> { private async validateNavigation(newPage: number): Promise<boolean> {

View File

@@ -157,7 +157,7 @@ export class WizardPage extends DialogTab {
private _onUpdate: Emitter<void> = new Emitter<void>(); private _onUpdate: Emitter<void> = new Emitter<void>();
public readonly onUpdate: Event<void> = this._onUpdate.event; public readonly onUpdate: Event<void> = this._onUpdate.event;
constructor(public title: string, content?: string) { constructor(public title: string, content?: string, public pageName?: string) {
super(title, content); super(title, content);
} }

View File

@@ -140,14 +140,16 @@ suite('MainThreadModelViewDialog Tests', () => {
content: 'content1', content: 'content1',
enabled: true, enabled: true,
customButtons: [], customButtons: [],
description: 'description1' description: 'description1',
pageName: 'pageName1'
}; };
page2Details = { page2Details = {
title: 'page2', title: 'page2',
content: 'content2', content: 'content2',
enabled: true, enabled: true,
customButtons: [button1Handle, button2Handle], customButtons: [button1Handle, button2Handle],
description: 'description2' description: 'description2',
pageName: undefined
}; };
wizardDetails = { wizardDetails = {
backButton: backButtonHandle, backButton: backButtonHandle,
@@ -302,7 +304,8 @@ suite('MainThreadModelViewDialog Tests', () => {
content: 'content_3', content: 'content_3',
customButtons: [], customButtons: [],
enabled: true, enabled: true,
description: undefined description: undefined,
pageName: undefined
}; };
// If I open the wizard and then add a page // If I open the wizard and then add a page