Telemetry points for SQL Database Projects extension (#14088)

* Added publish and schema compare telemetry
* Adding telemetry points for add/exclude/delete
* telemetry for validation
* Adding telemetry from project roundtrip updates, editing sqlproj, and db refs
* Changed method for obtaining extension ID during registration
* Fixing test failures, updating ads telemetry package dependency
* replacing action strings with enums
* change database project string actions to enums
* Changed action name to better match dialog
* PR feedback
This commit is contained in:
Benjin Dubishar
2021-01-31 16:38:48 -08:00
committed by GitHub
parent 15fa03876d
commit 1c0259f4c5
19 changed files with 239 additions and 71 deletions

View File

@@ -14,7 +14,7 @@ export interface IProjectProviderRegistry {
* Registers a new project provider * Registers a new project provider
* @param provider The project provider * @param provider The project provider
*/ */
registerProvider(provider: IProjectProvider): vscode.Disposable; registerProvider(provider: IProjectProvider, providerId: string): vscode.Disposable;
/** /**
* Clear the providers * Clear the providers

View File

@@ -6,14 +6,14 @@
import { IProjectProvider } from 'dataworkspace'; import { IProjectProvider } from 'dataworkspace';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { IProjectProviderRegistry } from './interfaces'; import { IProjectProviderRegistry } from './interfaces';
import { TelemetryReporter, TelemetryViews } from './telemetry'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from './telemetry';
export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry { export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry {
private _providers = new Array<IProjectProvider>(); private _providers = new Array<IProjectProvider>();
private _providerFileExtensionMapping: { [key: string]: IProjectProvider } = {}; private _providerFileExtensionMapping: { [key: string]: IProjectProvider } = {};
private _providerProjectTypeMapping: { [key: string]: IProjectProvider } = {}; private _providerProjectTypeMapping: { [key: string]: IProjectProvider } = {};
registerProvider(provider: IProjectProvider): vscode.Disposable { registerProvider(provider: IProjectProvider, providerId: string): vscode.Disposable {
this.validateProvider(provider); this.validateProvider(provider);
this._providers.push(provider); this._providers.push(provider);
provider.supportedProjectTypes.forEach(projectType => { provider.supportedProjectTypes.forEach(projectType => {
@@ -21,9 +21,9 @@ export const ProjectProviderRegistry: IProjectProviderRegistry = new class imple
this._providerProjectTypeMapping[projectType.id.toUpperCase()] = provider; this._providerProjectTypeMapping[projectType.id.toUpperCase()] = provider;
}); });
TelemetryReporter.createActionEvent(TelemetryViews.ProviderRegistration, 'ProviderRegistered') TelemetryReporter.createActionEvent(TelemetryViews.ProviderRegistration, TelemetryActions.ProviderRegistered)
.withAdditionalProperties({ .withAdditionalProperties({
providerId: provider.providerExtensionId, providerId: providerId,
extensions: provider.supportedProjectTypes.map(p => p.projectFileExtension).sort().join(', ') extensions: provider.supportedProjectTypes.map(p => p.projectFileExtension).sort().join(', ')
}) })
.send(); .send();

View File

@@ -14,13 +14,6 @@ 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 {
WorkspaceTreePane = 'WorkspaceTreePane',
OpenExistingDialog = 'OpenExistingDialog',
NewProjectDialog = 'NewProjectDialog',
ProviderRegistration = 'ProviderRegistration'
}
export function calculateRelativity(projectPath: string, workspacePath?: string): string { export function calculateRelativity(projectPath: string, workspacePath?: string): string {
workspacePath = workspacePath ?? vscode.workspace.workspaceFile?.fsPath; workspacePath = workspacePath ?? vscode.workspace.workspaceFile?.fsPath;
@@ -42,3 +35,22 @@ export function calculateRelativity(projectPath: string, workspacePath?: string)
return 'other'; // sibling, cousin, descendant, etc. return 'other'; // sibling, cousin, descendant, etc.
} }
export enum TelemetryViews {
WorkspaceTreePane = 'WorkspaceTreePane',
OpenExistingDialog = 'OpenExistingDialog',
NewProjectDialog = 'NewProjectDialog',
ProviderRegistration = 'ProviderRegistration'
}
export enum TelemetryActions {
ProviderRegistered = 'ProviderRegistered',
ProjectAddedToWorkspace = 'ProjectAddedToWorkspace',
ProjectRemovedFromWorkspace = 'ProjectRemovedFromWorkspace',
OpeningProject = 'OpeningProject',
NewProjectDialogLaunched = 'NewProjectDialogLaunched',
OpeningWorkspace = 'OpeningWorkspace',
OpenExistingDialogLaunched = 'OpenExistingDialogLaunched',
NewProjectDialogCompleted = 'NewProjectDialogCompleted'
}

View File

@@ -69,11 +69,6 @@ declare module 'dataworkspace' {
* Gets the supported project types * Gets the supported project types
*/ */
readonly supportedProjectTypes: IProjectType[]; readonly supportedProjectTypes: IProjectType[];
/**
* Gets the extension ID for the project provider
*/
readonly providerExtensionId: string;
} }
/** /**

View File

@@ -13,7 +13,7 @@ import { IProjectType } from 'dataworkspace';
import { directoryExist } from '../common/utils'; import { directoryExist } from '../common/utils';
import { IconPathHelper } from '../common/iconHelper'; import { IconPathHelper } from '../common/iconHelper';
import { defaultProjectSaveLocation } from '../common/projectLocationHelper'; import { defaultProjectSaveLocation } from '../common/projectLocationHelper';
import { TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
class NewProjectDialogModel { class NewProjectDialogModel {
projectTypeId: string = ''; projectTypeId: string = '';
@@ -28,7 +28,7 @@ export class NewProjectDialog extends DialogBase {
super(constants.NewProjectDialogTitle, 'NewProject'); super(constants.NewProjectDialogTitle, 'NewProject');
// dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button // dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button
TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, 'NewProjectDialogLaunched') TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, TelemetryActions.NewProjectDialogLaunched)
.withAdditionalProperties({ isWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }) .withAdditionalProperties({ isWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() })
.send(); .send();
} }
@@ -66,7 +66,7 @@ export class NewProjectDialog extends DialogBase {
try { try {
const validateWorkspace = await this.workspaceService.validateWorkspace(); const validateWorkspace = await this.workspaceService.validateWorkspace();
TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, 'NewProjectDialogCompleted') TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, TelemetryActions.NewProjectDialogCompleted)
.withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, workspaceValidationPassed: validateWorkspace.toString() }) .withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, workspaceValidationPassed: validateWorkspace.toString() })
.send(); .send();
@@ -76,7 +76,7 @@ export class NewProjectDialog extends DialogBase {
} }
catch (err) { catch (err) {
TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, 'NewProjectDialogErrorThrown') TelemetryReporter.createErrorEvent(TelemetryViews.NewProjectDialog, TelemetryActions.NewProjectDialogCompleted)
.withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, error: err?.message ? err.message : err }) .withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, error: err?.message ? err.message : err })
.send(); .send();

View File

@@ -11,7 +11,7 @@ import * as constants from '../common/constants';
import { IWorkspaceService } from '../common/interfaces'; import { IWorkspaceService } from '../common/interfaces';
import { fileExist } from '../common/utils'; import { fileExist } from '../common/utils';
import { IconPathHelper } from '../common/iconHelper'; import { IconPathHelper } from '../common/iconHelper';
import { calculateRelativity, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { calculateRelativity, TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
export class OpenExistingDialog extends DialogBase { export class OpenExistingDialog extends DialogBase {
public _targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined; public _targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined;
@@ -32,7 +32,7 @@ export class OpenExistingDialog extends DialogBase {
super(constants.OpenExistingDialogTitle, 'OpenProject'); super(constants.OpenExistingDialogTitle, 'OpenProject');
// dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button // dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, 'OpenWorkspaceProjectDialogLaunched') TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpenExistingDialogLaunched)
.withAdditionalProperties({ isWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }) .withAdditionalProperties({ isWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() })
.send(); .send();
} }
@@ -69,7 +69,7 @@ export class OpenExistingDialog extends DialogBase {
try { try {
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) { if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
// capture that workspace was selected, also if there's already an open workspace that's being replaced // capture that workspace was selected, also if there's already an open workspace that's being replaced
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, 'OpeningWorkspace') TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningWorkspace)
.withAdditionalProperties({ hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() }) .withAdditionalProperties({ hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() })
.send(); .send();
@@ -91,7 +91,7 @@ export class OpenExistingDialog extends DialogBase {
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!)); addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
} }
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, 'OpeningProject') TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningProject)
.withAdditionalProperties(telemetryProps) .withAdditionalProperties(telemetryProps)
.send(); .send();

View File

@@ -12,7 +12,7 @@ import * as glob from 'fast-glob';
import { IWorkspaceService } from '../common/interfaces'; import { IWorkspaceService } from '../common/interfaces';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import Logger from '../common/logger'; import Logger from '../common/logger';
import { TelemetryReporter, TelemetryViews, calculateRelativity } from '../common/telemetry'; import { TelemetryReporter, TelemetryViews, calculateRelativity, TelemetryActions } from '../common/telemetry';
const WorkspaceConfigurationName = 'dataworkspace'; const WorkspaceConfigurationName = 'dataworkspace';
const ProjectsConfigurationName = 'projects'; const ProjectsConfigurationName = 'projects';
@@ -117,7 +117,7 @@ export class WorkspaceService implements IWorkspaceService {
currentProjects.push(projectFile); currentProjects.push(projectFile);
newProjectFileAdded = true; newProjectFileAdded = true;
TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, 'ProjectAddedToWorkspace') TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, TelemetryActions.ProjectAddedToWorkspace)
.withAdditionalProperties({ .withAdditionalProperties({
workspaceProjectRelativity: calculateRelativity(projectFile.fsPath), workspaceProjectRelativity: calculateRelativity(projectFile.fsPath),
projectType: path.extname(projectFile.fsPath) projectType: path.extname(projectFile.fsPath)
@@ -234,7 +234,7 @@ export class WorkspaceService implements IWorkspaceService {
if (projectIdx !== -1) { if (projectIdx !== -1) {
currentProjects.splice(projectIdx, 1); currentProjects.splice(projectIdx, 1);
TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, 'ProjectRemovedFromWorkspace') TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, TelemetryActions.ProjectRemovedFromWorkspace)
.withAdditionalProperties({ .withAdditionalProperties({
projectType: path.extname(projectFile.fsPath) projectType: path.extname(projectFile.fsPath)
}).send(); }).send();
@@ -290,7 +290,7 @@ export class WorkspaceService implements IWorkspaceService {
} }
if (extension.isActive && extension.exports && !ProjectProviderRegistry.providers.includes(extension.exports)) { if (extension.isActive && extension.exports && !ProjectProviderRegistry.providers.includes(extension.exports)) {
ProjectProviderRegistry.registerProvider(extension.exports); ProjectProviderRegistry.registerProvider(extension.exports, extension.id);
} }
} }

View File

@@ -23,7 +23,6 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro
const treeDataProvider = new MockTreeDataProvider(); const treeDataProvider = new MockTreeDataProvider();
const projectProvider: IProjectProvider = { const projectProvider: IProjectProvider = {
supportedProjectTypes: projectTypes, supportedProjectTypes: projectTypes,
providerExtensionId: 'testProvider',
RemoveProject: (projectFile: vscode.Uri): Promise<void> => { RemoveProject: (projectFile: vscode.Uri): Promise<void> => {
return Promise.resolve(); return Promise.resolve();
}, },
@@ -64,7 +63,7 @@ suite('ProjectProviderRegistry Tests', function (): void {
} }
]); ]);
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test'); should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');
const disposable1 = ProjectProviderRegistry.registerProvider(provider1); const disposable1 = ProjectProviderRegistry.registerProvider(provider1, 'test.testProvider');
let providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj'); let providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj');
should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type'); should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type');
// make sure the project type is case-insensitive for getProviderByProjectType method // make sure the project type is case-insensitive for getProviderByProjectType method
@@ -73,7 +72,7 @@ suite('ProjectProviderRegistry Tests', function (): void {
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj1'); providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj1');
should.equal(providerResult, provider1, 'provider1 should be returned for testproj1 project type'); should.equal(providerResult, provider1, 'provider1 should be returned for testproj1 project type');
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time'); should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time');
const disposable2 = ProjectProviderRegistry.registerProvider(provider2); const disposable2 = ProjectProviderRegistry.registerProvider(provider2, 'test.testProvider2');
providerResult = ProjectProviderRegistry.getProviderByProjectExtension('sqlproj'); providerResult = ProjectProviderRegistry.getProviderByProjectExtension('sqlproj');
should.equal(providerResult, provider2, 'provider2 should be returned for sqlproj project type'); should.equal(providerResult, provider2, 'provider2 should be returned for sqlproj project type');
should.strictEqual(ProjectProviderRegistry.providers.length, 2, 'there should be 2 project providers at this time'); should.strictEqual(ProjectProviderRegistry.providers.length, 2, 'there should be 2 project providers at this time');
@@ -107,7 +106,7 @@ suite('ProjectProviderRegistry Tests', function (): void {
} }
]); ]);
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test'); should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');
ProjectProviderRegistry.registerProvider(provider); ProjectProviderRegistry.registerProvider(provider, 'test.testProvider');
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time'); should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time');
ProjectProviderRegistry.clear(); ProjectProviderRegistry.clear();
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after clearing the registry'); should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after clearing the registry');

View File

@@ -78,7 +78,6 @@ suite('workspaceTreeDataProvider Tests', function (): void {
displayName: 'sql project', displayName: 'sql project',
description: '' description: ''
}], }],
providerExtensionId: 'testProvider',
RemoveProject: (projectFile: vscode.Uri): Promise<void> => { RemoveProject: (projectFile: vscode.Uri): Promise<void> => {
return Promise.resolve(); return Promise.resolve();
}, },

View File

@@ -5,11 +5,32 @@
import AdsTelemetryReporter from '@microsoft/ads-extension-telemetry'; import AdsTelemetryReporter from '@microsoft/ads-extension-telemetry';
import { GetPackageInfo } from './utils'; import { getPackageInfo } from './utils';
const packageInfo = GetPackageInfo()!; const packageInfo = getPackageInfo()!;
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 {
ProjectController = 'ProjectController',
SqlProjectPublishDialog = 'SqlProjectPublishDialog',
ProjectTree = 'ProjectTree'
}
export enum TelemetryActions {
createNewProject = 'createNewProject',
addDatabaseReference = 'addDatabaseReference',
runStreamingJobValidation = 'runStreamingJobValidation',
generateScriptClicked = 'generateScriptClicked',
deleteObjectFromProject = 'deleteObjectFromProject',
editProjectFile = 'editProjectFile',
addItemFromTree = 'addItemFromTree',
excludeFromProject = 'excludeFromProject',
projectSchemaCompareCommandInvoked = 'projectSchemaCompareCommandInvoked',
publishProject = 'publishProject',
build = 'build',
updateProjectForRoundtrip = 'updateProjectForRoundtrip',
changePlatformType = 'changePlatformType',
updateSystemDatabaseReferencesInProjFile = 'updateSystemDatabaseReferencesInProjFile'
} }

View File

@@ -261,7 +261,7 @@ export interface IPackageInfo {
aiKey: string; aiKey: string;
} }
export function GetPackageInfo(packageJson?: any): IPackageInfo | undefined { export function getPackageInfo(packageJson?: any): IPackageInfo | undefined {
if (!packageJson) { if (!packageJson) {
packageJson = require('../../package.json'); packageJson = require('../../package.json');
} }

View File

@@ -30,6 +30,7 @@ import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialo
import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings';
import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem'; import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem';
import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDatabaseDialog'; import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDatabaseDialog';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
/** /**
* Controller for managing lifecycle of projects * Controller for managing lifecycle of projects
@@ -38,7 +39,6 @@ export class ProjectsController {
private netCoreTool: NetCoreTool; private netCoreTool: NetCoreTool;
private buildHelper: BuildHelper; private buildHelper: BuildHelper;
projects: Project[] = [];
projFileWatchers = new Map<string, vscode.FileSystemWatcher>(); projFileWatchers = new Map<string, vscode.FileSystemWatcher>();
constructor() { constructor() {
@@ -57,6 +57,10 @@ export class ProjectsController {
* @param projectGuid * @param projectGuid
*/ */
public async createNewProject(creationParams: NewProjectParams): Promise<string> { public async createNewProject(creationParams: NewProjectParams): Promise<string> {
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.createNewProject)
.withAdditionalProperties({ template: creationParams.projectTypeId })
.send();
if (creationParams.projectGuid && !UUID.isUUID(creationParams.projectGuid)) { if (creationParams.projectGuid && !UUID.isUUID(creationParams.projectGuid)) {
throw new Error(`Specified GUID is invalid: '${creationParams.projectGuid}'`); throw new Error(`Specified GUID is invalid: '${creationParams.projectGuid}'`);
} }
@@ -103,6 +107,8 @@ export class ProjectsController {
public async buildProject(context: Project | dataworkspace.WorkspaceTreeItem): Promise<string> { public async buildProject(context: Project | dataworkspace.WorkspaceTreeItem): Promise<string> {
const project: Project = this.getProjectFromContext(context); const project: Project = this.getProjectFromContext(context);
const startTime = new Date();
// Check mssql extension for project dlls (tracking issue #10273) // Check mssql extension for project dlls (tracking issue #10273)
await this.buildHelper.createBuildDirFolder(); await this.buildHelper.createBuildDirFolder();
@@ -111,12 +117,21 @@ export class ProjectsController {
workingDirectory: project.projectFolderPath, workingDirectory: project.projectFolderPath,
argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath) argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath)
}; };
try { try {
await this.netCoreTool.runDotnetCommand(options); await this.netCoreTool.runDotnetCommand(options);
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.build)
.withAdditionalMeasurements({ duration: new Date().getMilliseconds() - startTime.getMilliseconds() })
.send();
return project.dacpacOutputPath; return project.dacpacOutputPath;
} } catch (err) {
catch (err) { TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.build)
.withAdditionalProperties({ error: utils.getErrorMessage(err) })
.withAdditionalMeasurements({ duration: new Date().getMilliseconds() - startTime.getMilliseconds() })
.send();
vscode.window.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err))); vscode.window.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err)));
return ''; return '';
} }
@@ -146,9 +161,22 @@ export class ProjectsController {
} }
public async publishProjectCallback(project: Project, settings: IPublishSettings | IGenerateScriptSettings): Promise<mssql.DacFxResult | undefined> { public async publishProjectCallback(project: Project, settings: IPublishSettings | IGenerateScriptSettings): Promise<mssql.DacFxResult | undefined> {
const telemetryProps: Record<string, string> = {};
const telemetryMeasures: Record<string, number> = {};
const buildStartTime = new Date().getMilliseconds();
const dacpacPath = await this.buildProject(project); const dacpacPath = await this.buildProject(project);
const buildEndTime = new Date().getMilliseconds();
telemetryMeasures.buildDuration = buildEndTime - buildStartTime;
telemetryProps.buildSucceeded = (dacpacPath !== '').toString();
if (!dacpacPath) { if (!dacpacPath) {
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject)
.withAdditionalProperties(telemetryProps)
.withAdditionalMeasurements(telemetryMeasures)
.send();
return undefined; // buildProject() handles displaying the error return undefined; // buildProject() handles displaying the error
} }
@@ -158,12 +186,41 @@ export class ProjectsController {
const dacFxService = await this.getDaxFxService(); const dacFxService = await this.getDaxFxService();
if ((<IPublishSettings>settings).upgradeExisting) { let result: mssql.DacFxResult;
return await dacFxService.deployDacpac(tempPath, settings.databaseName, (<IPublishSettings>settings).upgradeExisting, settings.connectionUri, azdata.TaskExecutionMode.execute, settings.sqlCmdVariables, settings.deploymentOptions); telemetryProps.profileUsed = (settings.profileUsed ?? false).toString();
} const actionStartTime = new Date().getMilliseconds();
else {
return await dacFxService.generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, azdata.TaskExecutionMode.script, settings.sqlCmdVariables, settings.deploymentOptions); try {
if ((<IPublishSettings>settings).upgradeExisting) {
telemetryProps.publishAction = 'deploy';
result = await dacFxService.deployDacpac(tempPath, settings.databaseName, (<IPublishSettings>settings).upgradeExisting, settings.connectionUri, azdata.TaskExecutionMode.execute, settings.sqlCmdVariables, settings.deploymentOptions);
}
else {
telemetryProps.publishAction = 'generateScript';
result = await dacFxService.generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, azdata.TaskExecutionMode.script, settings.sqlCmdVariables, settings.deploymentOptions);
}
} catch (err) {
const actionEndTime = new Date().getMilliseconds();
telemetryProps.actionDuration = (actionEndTime - actionStartTime).toString();
telemetryProps.totalDuration = (actionEndTime - buildStartTime).toString();
telemetryProps.errorMessage = utils.getErrorMessage(err);
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject)
.withAdditionalProperties(telemetryProps)
.send();
throw err;
} }
const actionEndTime = new Date().getMilliseconds();
telemetryProps.actionDuration = (actionEndTime - actionStartTime).toString();
telemetryProps.totalDuration = (actionEndTime - buildStartTime).toString();
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject)
.withAdditionalProperties(telemetryProps)
.send();
return result;
} }
public async readPublishProfileCallback(profileUri: vscode.Uri): Promise<PublishProfile> { public async readPublishProfileCallback(profileUri: vscode.Uri): Promise<PublishProfile> {
@@ -171,27 +228,42 @@ export class ProjectsController {
const dacFxService = await this.getDaxFxService(); const dacFxService = await this.getDaxFxService();
const profile = await load(profileUri, dacFxService); const profile = await load(profileUri, dacFxService);
return profile; return profile;
} } catch (e) {
catch (e) {
vscode.window.showErrorMessage(constants.profileReadError); vscode.window.showErrorMessage(constants.profileReadError);
throw e; throw e;
} }
} }
public async schemaCompare(treeNode: dataworkspace.WorkspaceTreeItem): Promise<void> { public async schemaCompare(treeNode: dataworkspace.WorkspaceTreeItem): Promise<void> {
// check if schema compare extension is installed try {
if (vscode.extensions.getExtension(constants.schemaCompareExtensionId)) { // check if schema compare extension is installed
// build project if (vscode.extensions.getExtension(constants.schemaCompareExtensionId)) {
const dacpacPath = await this.buildProject(treeNode); // build project
const dacpacPath = await this.buildProject(treeNode);
// check that dacpac exists // check that dacpac exists
if (await utils.exists(dacpacPath)) { if (await utils.exists(dacpacPath)) {
await vscode.commands.executeCommand(constants.schemaCompareStartCommand, dacpacPath); TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.projectSchemaCompareCommandInvoked);
await vscode.commands.executeCommand(constants.schemaCompareStartCommand, dacpacPath);
} else {
throw new Error(constants.buildFailedCannotStartSchemaCompare);
}
} else { } else {
vscode.window.showErrorMessage(constants.buildFailedCannotStartSchemaCompare); throw new Error(constants.schemaCompareNotInstalled);
} }
} else { } catch (err) {
vscode.window.showErrorMessage(constants.schemaCompareNotInstalled); const props: Record<string, string> = {};
const message = utils.getErrorMessage(err);
if (message === constants.buildFailedCannotStartSchemaCompare || message === constants.schemaCompareNotInstalled) {
props.errorMessage = message;
}
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.projectSchemaCompareCommandInvoked)
.withAdditionalProperties(props)
.send();
vscode.window.showErrorMessage(utils.getErrorMessage(err));
} }
} }
@@ -264,13 +336,32 @@ export class ProjectsController {
const newFileText = templates.macroExpansion(itemType.templateScript, { 'OBJECT_NAME': itemObjectName }); const newFileText = templates.macroExpansion(itemType.templateScript, { 'OBJECT_NAME': itemObjectName });
const relativeFilePath = path.join(relativePath, itemObjectName + constants.sqlFileExtension); const relativeFilePath = path.join(relativePath, itemObjectName + constants.sqlFileExtension);
const telemetryProps: Record<string, string> = { itemType: itemType.type };
const telemetryMeasurements: Record<string, number> = {};
if (itemType.type === templates.preDeployScript) {
telemetryMeasurements.numPredeployScripts = project.preDeployScripts.length;
} else if (itemType.type === templates.postDeployScript) {
telemetryMeasurements.numPostdeployScripts = project.postDeployScripts.length;
}
try { try {
const newEntry = await project.addScriptItem(relativeFilePath, newFileText, itemType.type); const newEntry = await project.addScriptItem(relativeFilePath, newFileText, itemType.type);
TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.addItemFromTree)
.withAdditionalProperties(telemetryProps)
.withAdditionalMeasurements(telemetryMeasurements)
.send();
await vscode.commands.executeCommand(constants.vscodeOpenCommand, newEntry.fsUri); await vscode.commands.executeCommand(constants.vscodeOpenCommand, newEntry.fsUri);
treeDataProvider?.notifyTreeDataChanged(); treeDataProvider?.notifyTreeDataChanged();
} catch (err) { } catch (err) {
vscode.window.showErrorMessage(utils.getErrorMessage(err)); vscode.window.showErrorMessage(utils.getErrorMessage(err));
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectTree, TelemetryActions.addItemFromTree)
.withAdditionalProperties(telemetryProps)
.withAdditionalMeasurements(telemetryMeasurements)
.send();
} }
} }
@@ -281,8 +372,10 @@ export class ProjectsController {
const fileEntry = this.getFileProjectEntry(project, node); const fileEntry = this.getFileProjectEntry(project, node);
if (fileEntry) { if (fileEntry) {
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectTree, TelemetryActions.excludeFromProject);
await project.exclude(fileEntry); await project.exclude(fileEntry);
} else { } else {
TelemetryReporter.sendErrorEvent(TelemetryViews.ProjectTree, TelemetryActions.excludeFromProject);
vscode.window.showErrorMessage(constants.unableToPerformAction(constants.excludeAction, node.uri.path)); vscode.window.showErrorMessage(constants.unableToPerformAction(constants.excludeAction, node.uri.path));
} }
@@ -327,8 +420,16 @@ export class ProjectsController {
} }
if (success) { if (success) {
TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.deleteObjectFromProject)
.withAdditionalProperties({ objectType: node.constructor.name })
.send();
this.refreshProjectsTree(context); this.refreshProjectsTree(context);
} else { } else {
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectTree, TelemetryActions.deleteObjectFromProject)
.withAdditionalProperties({ objectType: node.constructor.name })
.send();
vscode.window.showErrorMessage(constants.unableToPerformAction(constants.deleteAction, node.uri.path)); vscode.window.showErrorMessage(constants.unableToPerformAction(constants.deleteAction, node.uri.path));
} }
} }
@@ -375,6 +476,9 @@ export class ProjectsController {
try { try {
await vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(project.projectFilePath)); await vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(project.projectFilePath));
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectTree, TelemetryActions.editProjectFile);
const projFileWatcher: vscode.FileSystemWatcher = vscode.workspace.createFileSystemWatcher(project.projectFilePath); const projFileWatcher: vscode.FileSystemWatcher = vscode.workspace.createFileSystemWatcher(project.projectFilePath);
this.projFileWatchers.set(project.projectFilePath, projFileWatcher); this.projFileWatchers.set(project.projectFilePath, projFileWatcher);
@@ -496,16 +600,25 @@ export class ProjectsController {
const project: Project = this.getProjectFromContext(node); const project: Project = this.getProjectFromContext(node);
let dacpacPath: string = project.dacpacOutputPath; let dacpacPath: string = project.dacpacOutputPath;
const preExistingDacpac = await utils.exists(dacpacPath);
if (!await utils.exists(dacpacPath)) { const telemetryProps: Record<string, string> = { preExistingDacpac: preExistingDacpac.toString() };
if (!preExistingDacpac) {
dacpacPath = await this.buildProject(project); dacpacPath = await this.buildProject(project);
} }
const streamingJobDefinition: string = (await fs.readFile(node.element.fileSystemUri.fsPath)).toString(); const streamingJobDefinition: string = (await fs.readFile(node.element.fileSystemUri.fsPath)).toString();
const dacFxService = await this.getDaxFxService(); const dacFxService = await this.getDaxFxService();
const actionStartTime = new Date().getMilliseconds();
const result: mssql.ValidateStreamingJobResult = await dacFxService.validateStreamingJob(dacpacPath, streamingJobDefinition); const result: mssql.ValidateStreamingJobResult = await dacFxService.validateStreamingJob(dacpacPath, streamingJobDefinition);
const duration = new Date().getMilliseconds() - actionStartTime;
telemetryProps.success = result.success.toString();
if (result.success) { if (result.success) {
vscode.window.showInformationMessage(constants.externalStreamingJobValidationPassed); vscode.window.showInformationMessage(constants.externalStreamingJobValidationPassed);
} }
@@ -513,6 +626,11 @@ export class ProjectsController {
vscode.window.showErrorMessage(result.errorMessage); vscode.window.showErrorMessage(result.errorMessage);
} }
TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.runStreamingJobValidation)
.withAdditionalProperties(telemetryProps)
.withAdditionalMeasurements({ duration: duration })
.send();
return result; return result;
} }
@@ -660,8 +778,7 @@ export class ProjectsController {
workspaceApi.showProjectsView(); workspaceApi.showProjectsView();
await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)], model.newWorkspaceFilePath); await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)], model.newWorkspaceFilePath);
} }
} } catch (err) {
catch (err) {
vscode.window.showErrorMessage(utils.getErrorMessage(err)); vscode.window.showErrorMessage(utils.getErrorMessage(err));
} }
} }

View File

@@ -14,6 +14,7 @@ import { cssStyles } from '../common/uiConstants';
import { IconPathHelper } from '../common/iconHelper'; import { IconPathHelper } from '../common/iconHelper';
import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings';
import { Deferred } from '../common/promise'; import { Deferred } from '../common/promise';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
export enum ReferenceType { export enum ReferenceType {
project, project,
@@ -51,7 +52,7 @@ export class AddDatabaseReferenceDialog {
public addReference: ((proj: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings) => any) | undefined; public addReference: ((proj: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings) => any) | undefined;
constructor(private project: Project) { constructor(private project: Project) {
this.dialog = azdata.window.createModelViewDialog(constants.addDatabaseReferenceDialogName); this.dialog = azdata.window.createModelViewDialog(constants.addDatabaseReferenceDialogName, 'addDatabaseReferencesDialog');
this.addDatabaseReferenceTab = azdata.window.createTab(constants.addDatabaseReferenceDialogName); this.addDatabaseReferenceTab = azdata.window.createTab(constants.addDatabaseReferenceDialogName);
} }
@@ -157,6 +158,10 @@ export class AddDatabaseReferenceDialog {
}; };
} }
TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.addDatabaseReference)
.withAdditionalProperties({ referenceType: this.currentReferenceType!.toString() })
.send();
await this.addReference!(this.project, referenceSettings); await this.addReference!(this.project, referenceSettings);
this.dispose(); this.dispose();

View File

@@ -36,7 +36,7 @@ export class CreateProjectFromDatabaseDialog {
public createProjectFromDatabaseCallback: ((model: ImportDataModel) => any) | undefined; public createProjectFromDatabaseCallback: ((model: ImportDataModel) => any) | undefined;
constructor(private profile: azdata.IConnectionProfile | undefined) { constructor(private profile: azdata.IConnectionProfile | undefined) {
this.dialog = azdata.window.createModelViewDialog(constants.createProjectFromDatabaseDialogName); this.dialog = azdata.window.createModelViewDialog(constants.createProjectFromDatabaseDialogName, 'createProjectFromDatabaseDialog');
this.createProjectFromDatabaseTab = azdata.window.createTab(constants.createProjectFromDatabaseDialogName); this.createProjectFromDatabaseTab = azdata.window.createTab(constants.createProjectFromDatabaseDialogName);
this.dialog.registerCloseValidator(async () => { this.dialog.registerCloseValidator(async () => {
return this.validate(); return this.validate();

View File

@@ -15,6 +15,7 @@ import { DeploymentOptions, SchemaObjectType } from '../../../mssql/src/mssql';
import { IconPathHelper } from '../common/iconHelper'; import { IconPathHelper } from '../common/iconHelper';
import { cssStyles } from '../common/uiConstants'; import { cssStyles } from '../common/uiConstants';
import { getConnectionName } from './utils'; import { getConnectionName } from './utils';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
interface DataSourceDropdownValue extends azdata.CategoryValue { interface DataSourceDropdownValue extends azdata.CategoryValue {
dataSource: SqlConnectionDataSource; dataSource: SqlConnectionDataSource;
@@ -40,6 +41,7 @@ export class PublishDatabaseDialog {
private connectionIsDataSource: boolean | undefined; private connectionIsDataSource: boolean | undefined;
private sqlCmdVars: Record<string, string> | undefined; private sqlCmdVars: Record<string, string> | undefined;
private deploymentOptions: DeploymentOptions | undefined; private deploymentOptions: DeploymentOptions | undefined;
private profileUsed: boolean = false;
private toDispose: vscode.Disposable[] = []; private toDispose: vscode.Disposable[] = [];
@@ -48,7 +50,7 @@ export class PublishDatabaseDialog {
public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined; public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined;
constructor(private project: Project) { constructor(private project: Project) {
this.dialog = azdata.window.createModelViewDialog(constants.publishDialogName); this.dialog = azdata.window.createModelViewDialog(constants.publishDialogName, 'sqlProjectPublishDialog');
this.publishTab = azdata.window.createTab(constants.publishDialogName); this.publishTab = azdata.window.createTab(constants.publishDialogName);
} }
@@ -184,7 +186,8 @@ export class PublishDatabaseDialog {
upgradeExisting: true, upgradeExisting: true,
connectionUri: await this.getConnectionUri(), connectionUri: await this.getConnectionUri(),
sqlCmdVariables: this.getSqlCmdVariablesForPublish(), sqlCmdVariables: this.getSqlCmdVariablesForPublish(),
deploymentOptions: await this.getDeploymentOptions() deploymentOptions: await this.getDeploymentOptions(),
profileUsed: this.profileUsed
}; };
azdata.window.closeDialog(this.dialog); azdata.window.closeDialog(this.dialog);
@@ -194,12 +197,15 @@ export class PublishDatabaseDialog {
} }
public async generateScriptClick(): Promise<void> { public async generateScriptClick(): Promise<void> {
TelemetryReporter.sendActionEvent(TelemetryViews.SqlProjectPublishDialog, TelemetryActions.generateScriptClicked);
const sqlCmdVars = this.getSqlCmdVariablesForPublish(); const sqlCmdVars = this.getSqlCmdVariablesForPublish();
const settings: IGenerateScriptSettings = { const settings: IGenerateScriptSettings = {
databaseName: this.getTargetDatabaseName(), databaseName: this.getTargetDatabaseName(),
connectionUri: await this.getConnectionUri(), connectionUri: await this.getConnectionUri(),
sqlCmdVariables: sqlCmdVars, sqlCmdVariables: sqlCmdVars,
deploymentOptions: await this.getDeploymentOptions() deploymentOptions: await this.getDeploymentOptions(),
profileUsed: this.profileUsed
}; };
azdata.window.closeDialog(this.dialog); azdata.window.closeDialog(this.dialog);

View File

@@ -11,6 +11,7 @@ export interface IPublishSettings {
upgradeExisting: boolean; upgradeExisting: boolean;
sqlCmdVariables?: Record<string, string>; sqlCmdVariables?: Record<string, string>;
deploymentOptions?: DeploymentOptions; deploymentOptions?: DeploymentOptions;
profileUsed?: boolean;
} }
export interface IGenerateScriptSettings { export interface IGenerateScriptSettings {
@@ -18,4 +19,5 @@ export interface IGenerateScriptSettings {
connectionUri: string; connectionUri: string;
sqlCmdVariables?: Record<string, string>; sqlCmdVariables?: Record<string, string>;
deploymentOptions?: DeploymentOptions; deploymentOptions?: DeploymentOptions;
profileUsed?: boolean;
} }

View File

@@ -15,6 +15,7 @@ import { Uri, window } from 'vscode';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { DataSource } from './dataSources/dataSources'; import { DataSource } from './dataSources/dataSources';
import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from './IDatabaseReferenceSettings'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from './IDatabaseReferenceSettings';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
/** /**
* Class representing a Project, and providing functions for operating on it * Class representing a Project, and providing functions for operating on it
@@ -195,6 +196,8 @@ export class Project {
return; return;
} }
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.updateProjectForRoundtrip);
if (!this.importedTargets.includes(constants.NetCoreTargets)) { if (!this.importedTargets.includes(constants.NetCoreTargets)) {
const result = await window.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString); const result = await window.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString);
if (result === constants.yesString) { if (result === constants.yesString) {
@@ -387,6 +390,13 @@ export class Project {
} }
await this.serializeToProjFile(this.projFileXmlDoc); await this.serializeToProjFile(this.projFileXmlDoc);
TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.changePlatformType)
.withAdditionalProperties({
from: this.getProjectTargetVersion(),
to: compatLevel
})
.send();
} }
} }
@@ -854,6 +864,10 @@ export class Project {
await this.addSystemDatabaseReference({ databaseName: databaseVariableName, systemDb: systemDb, suppressMissingDependenciesErrors: suppressMissingDependences }); await this.addSystemDatabaseReference({ databaseName: databaseVariableName, systemDb: systemDb, suppressMissingDependenciesErrors: suppressMissingDependences });
} }
} }
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.updateSystemDatabaseReferencesInProjFile)
.withAdditionalMeasurements({ referencesCount: this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference).length })
.send();
} }
private async addToProjFile(entry: ProjectEntry, xmlTag?: string, attributes?: Map<string, string>): Promise<void> { private async addToProjFile(entry: ProjectEntry, xmlTag?: string, attributes?: Map<string, string>): Promise<void> {

View File

@@ -11,10 +11,8 @@ import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProje
import { ProjectsController } from '../controllers/projectController'; import { ProjectsController } from '../controllers/projectController';
import { Project } from '../models/project'; import { Project } from '../models/project';
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
import { GetPackageInfo } from '../common/utils';
export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider { export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider {
constructor(private projectController: ProjectsController) { constructor(private projectController: ProjectsController) {
} }
@@ -76,6 +74,4 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
return vscode.Uri.file(projectFile); return vscode.Uri.file(projectFile);
} }
get providerExtensionId(): string { return GetPackageInfo()!.fullName; }
} }

View File

@@ -79,7 +79,8 @@ describe('Publish Database Dialog', () => {
'ProdDatabaseName': 'MyProdDatabase', 'ProdDatabaseName': 'MyProdDatabase',
'BackupDatabaseName': 'MyBackupDatabase' 'BackupDatabaseName': 'MyBackupDatabase'
}, },
deploymentOptions: mockDacFxOptionsResult.deploymentOptions deploymentOptions: mockDacFxOptionsResult.deploymentOptions,
profileUsed: false
}; };
dialog.object.publish = (_, prof) => { profile = prof; }; dialog.object.publish = (_, prof) => { profile = prof; };
@@ -94,7 +95,8 @@ describe('Publish Database Dialog', () => {
'ProdDatabaseName': 'MyProdDatabase', 'ProdDatabaseName': 'MyProdDatabase',
'BackupDatabaseName': 'MyBackupDatabase' 'BackupDatabaseName': 'MyBackupDatabase'
}, },
deploymentOptions: mockDacFxOptionsResult.deploymentOptions deploymentOptions: mockDacFxOptionsResult.deploymentOptions,
profileUsed: false
}; };
dialog.object.generateScript = (_, prof) => { profile = prof; }; dialog.object.generateScript = (_, prof) => { profile = prof; };