diff --git a/extensions/data-workspace/src/common/interfaces.ts b/extensions/data-workspace/src/common/interfaces.ts index b16eb09f64..3eea7c4ad6 100644 --- a/extensions/data-workspace/src/common/interfaces.ts +++ b/extensions/data-workspace/src/common/interfaces.ts @@ -14,7 +14,7 @@ export interface IProjectProviderRegistry { * Registers a new project provider * @param provider The project provider */ - registerProvider(provider: IProjectProvider): vscode.Disposable; + registerProvider(provider: IProjectProvider, providerId: string): vscode.Disposable; /** * Clear the providers diff --git a/extensions/data-workspace/src/common/projectProviderRegistry.ts b/extensions/data-workspace/src/common/projectProviderRegistry.ts index b1f333ea54..074b6c7af6 100644 --- a/extensions/data-workspace/src/common/projectProviderRegistry.ts +++ b/extensions/data-workspace/src/common/projectProviderRegistry.ts @@ -6,14 +6,14 @@ import { IProjectProvider } from 'dataworkspace'; import * as vscode from 'vscode'; import { IProjectProviderRegistry } from './interfaces'; -import { TelemetryReporter, TelemetryViews } from './telemetry'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from './telemetry'; export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry { private _providers = new Array(); private _providerFileExtensionMapping: { [key: string]: IProjectProvider } = {}; private _providerProjectTypeMapping: { [key: string]: IProjectProvider } = {}; - registerProvider(provider: IProjectProvider): vscode.Disposable { + registerProvider(provider: IProjectProvider, providerId: string): vscode.Disposable { this.validateProvider(provider); this._providers.push(provider); provider.supportedProjectTypes.forEach(projectType => { @@ -21,9 +21,9 @@ export const ProjectProviderRegistry: IProjectProviderRegistry = new class imple this._providerProjectTypeMapping[projectType.id.toUpperCase()] = provider; }); - TelemetryReporter.createActionEvent(TelemetryViews.ProviderRegistration, 'ProviderRegistered') + TelemetryReporter.createActionEvent(TelemetryViews.ProviderRegistration, TelemetryActions.ProviderRegistered) .withAdditionalProperties({ - providerId: provider.providerExtensionId, + providerId: providerId, extensions: provider.supportedProjectTypes.map(p => p.projectFileExtension).sort().join(', ') }) .send(); diff --git a/extensions/data-workspace/src/common/telemetry.ts b/extensions/data-workspace/src/common/telemetry.ts index ccf8cc8839..2837d5a516 100644 --- a/extensions/data-workspace/src/common/telemetry.ts +++ b/extensions/data-workspace/src/common/telemetry.ts @@ -14,13 +14,6 @@ let packageInfo = utils.getPackageInfo(packageJson)!; 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 { workspacePath = workspacePath ?? vscode.workspace.workspaceFile?.fsPath; @@ -42,3 +35,22 @@ export function calculateRelativity(projectPath: string, workspacePath?: string) 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' +} diff --git a/extensions/data-workspace/src/dataworkspace.d.ts b/extensions/data-workspace/src/dataworkspace.d.ts index 86ea0f89cb..32d92e420d 100644 --- a/extensions/data-workspace/src/dataworkspace.d.ts +++ b/extensions/data-workspace/src/dataworkspace.d.ts @@ -69,11 +69,6 @@ declare module 'dataworkspace' { * Gets the supported project types */ readonly supportedProjectTypes: IProjectType[]; - - /** - * Gets the extension ID for the project provider - */ - readonly providerExtensionId: string; } /** diff --git a/extensions/data-workspace/src/dialogs/newProjectDialog.ts b/extensions/data-workspace/src/dialogs/newProjectDialog.ts index 810a53888c..9c597a41f8 100644 --- a/extensions/data-workspace/src/dialogs/newProjectDialog.ts +++ b/extensions/data-workspace/src/dialogs/newProjectDialog.ts @@ -13,7 +13,7 @@ import { IProjectType } from 'dataworkspace'; import { directoryExist } from '../common/utils'; import { IconPathHelper } from '../common/iconHelper'; import { defaultProjectSaveLocation } from '../common/projectLocationHelper'; -import { TelemetryReporter, TelemetryViews } from '../common/telemetry'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; class NewProjectDialogModel { projectTypeId: string = ''; @@ -28,7 +28,7 @@ export class NewProjectDialog extends DialogBase { super(constants.NewProjectDialogTitle, 'NewProject'); // 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() }) .send(); } @@ -66,7 +66,7 @@ export class NewProjectDialog extends DialogBase { try { 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() }) .send(); @@ -76,7 +76,7 @@ export class NewProjectDialog extends DialogBase { } 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 }) .send(); diff --git a/extensions/data-workspace/src/dialogs/openExistingDialog.ts b/extensions/data-workspace/src/dialogs/openExistingDialog.ts index d61414b1a3..eab062d1c6 100644 --- a/extensions/data-workspace/src/dialogs/openExistingDialog.ts +++ b/extensions/data-workspace/src/dialogs/openExistingDialog.ts @@ -11,7 +11,7 @@ import * as constants from '../common/constants'; import { IWorkspaceService } from '../common/interfaces'; import { fileExist } from '../common/utils'; 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 { public _targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined; @@ -32,7 +32,7 @@ export class OpenExistingDialog extends DialogBase { super(constants.OpenExistingDialogTitle, 'OpenProject'); // 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() }) .send(); } @@ -69,7 +69,7 @@ export class OpenExistingDialog extends DialogBase { try { if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) { // 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() }) .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!)); } - TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, 'OpeningProject') + TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, TelemetryActions.OpeningProject) .withAdditionalProperties(telemetryProps) .send(); diff --git a/extensions/data-workspace/src/services/workspaceService.ts b/extensions/data-workspace/src/services/workspaceService.ts index 1d1b0146fb..77016638ea 100644 --- a/extensions/data-workspace/src/services/workspaceService.ts +++ b/extensions/data-workspace/src/services/workspaceService.ts @@ -12,7 +12,7 @@ import * as glob from 'fast-glob'; import { IWorkspaceService } from '../common/interfaces'; import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; import Logger from '../common/logger'; -import { TelemetryReporter, TelemetryViews, calculateRelativity } from '../common/telemetry'; +import { TelemetryReporter, TelemetryViews, calculateRelativity, TelemetryActions } from '../common/telemetry'; const WorkspaceConfigurationName = 'dataworkspace'; const ProjectsConfigurationName = 'projects'; @@ -117,7 +117,7 @@ export class WorkspaceService implements IWorkspaceService { currentProjects.push(projectFile); newProjectFileAdded = true; - TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, 'ProjectAddedToWorkspace') + TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, TelemetryActions.ProjectAddedToWorkspace) .withAdditionalProperties({ workspaceProjectRelativity: calculateRelativity(projectFile.fsPath), projectType: path.extname(projectFile.fsPath) @@ -234,7 +234,7 @@ export class WorkspaceService implements IWorkspaceService { if (projectIdx !== -1) { currentProjects.splice(projectIdx, 1); - TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, 'ProjectRemovedFromWorkspace') + TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, TelemetryActions.ProjectRemovedFromWorkspace) .withAdditionalProperties({ projectType: path.extname(projectFile.fsPath) }).send(); @@ -290,7 +290,7 @@ export class WorkspaceService implements IWorkspaceService { } if (extension.isActive && extension.exports && !ProjectProviderRegistry.providers.includes(extension.exports)) { - ProjectProviderRegistry.registerProvider(extension.exports); + ProjectProviderRegistry.registerProvider(extension.exports, extension.id); } } diff --git a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts index 091033dc8c..b77216c7f9 100644 --- a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts +++ b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts @@ -23,7 +23,6 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro const treeDataProvider = new MockTreeDataProvider(); const projectProvider: IProjectProvider = { supportedProjectTypes: projectTypes, - providerExtensionId: 'testProvider', RemoveProject: (projectFile: vscode.Uri): Promise => { 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'); - const disposable1 = ProjectProviderRegistry.registerProvider(provider1); + const disposable1 = ProjectProviderRegistry.registerProvider(provider1, 'test.testProvider'); let providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj'); should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type'); // make sure the project type is case-insensitive for getProviderByProjectType method @@ -73,7 +72,7 @@ suite('ProjectProviderRegistry Tests', function (): void { providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj1'); 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'); - const disposable2 = ProjectProviderRegistry.registerProvider(provider2); + const disposable2 = ProjectProviderRegistry.registerProvider(provider2, 'test.testProvider2'); providerResult = ProjectProviderRegistry.getProviderByProjectExtension('sqlproj'); 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'); @@ -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'); - ProjectProviderRegistry.registerProvider(provider); + ProjectProviderRegistry.registerProvider(provider, 'test.testProvider'); should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time'); ProjectProviderRegistry.clear(); should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after clearing the registry'); diff --git a/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts b/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts index da63c20ca1..81b6b6b0c3 100644 --- a/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts +++ b/extensions/data-workspace/src/test/workspaceTreeDataProvider.test.ts @@ -78,7 +78,6 @@ suite('workspaceTreeDataProvider Tests', function (): void { displayName: 'sql project', description: '' }], - providerExtensionId: 'testProvider', RemoveProject: (projectFile: vscode.Uri): Promise => { return Promise.resolve(); }, diff --git a/extensions/sql-database-projects/src/common/telemetry.ts b/extensions/sql-database-projects/src/common/telemetry.ts index d3f5aba312..72710c5d77 100644 --- a/extensions/sql-database-projects/src/common/telemetry.ts +++ b/extensions/sql-database-projects/src/common/telemetry.ts @@ -5,11 +5,32 @@ 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 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' } diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index c724434a5d..1287c2bed1 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -261,7 +261,7 @@ export interface IPackageInfo { aiKey: string; } -export function GetPackageInfo(packageJson?: any): IPackageInfo | undefined { +export function getPackageInfo(packageJson?: any): IPackageInfo | undefined { if (!packageJson) { packageJson = require('../../package.json'); } diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 46c8e4a7c1..d9d7aebe55 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -30,6 +30,7 @@ import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialo import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem'; import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDatabaseDialog'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; /** * Controller for managing lifecycle of projects @@ -38,7 +39,6 @@ export class ProjectsController { private netCoreTool: NetCoreTool; private buildHelper: BuildHelper; - projects: Project[] = []; projFileWatchers = new Map(); constructor() { @@ -57,6 +57,10 @@ export class ProjectsController { * @param projectGuid */ public async createNewProject(creationParams: NewProjectParams): Promise { + TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.createNewProject) + .withAdditionalProperties({ template: creationParams.projectTypeId }) + .send(); + if (creationParams.projectGuid && !UUID.isUUID(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 { const project: Project = this.getProjectFromContext(context); + const startTime = new Date(); + // Check mssql extension for project dlls (tracking issue #10273) await this.buildHelper.createBuildDirFolder(); @@ -111,12 +117,21 @@ export class ProjectsController { workingDirectory: project.projectFolderPath, argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath) }; + try { await this.netCoreTool.runDotnetCommand(options); + TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.build) + .withAdditionalMeasurements({ duration: new Date().getMilliseconds() - startTime.getMilliseconds() }) + .send(); + 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))); return ''; } @@ -146,9 +161,22 @@ export class ProjectsController { } public async publishProjectCallback(project: Project, settings: IPublishSettings | IGenerateScriptSettings): Promise { + const telemetryProps: Record = {}; + const telemetryMeasures: Record = {}; + const buildStartTime = new Date().getMilliseconds(); + const dacpacPath = await this.buildProject(project); + const buildEndTime = new Date().getMilliseconds(); + telemetryMeasures.buildDuration = buildEndTime - buildStartTime; + telemetryProps.buildSucceeded = (dacpacPath !== '').toString(); + if (!dacpacPath) { + TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject) + .withAdditionalProperties(telemetryProps) + .withAdditionalMeasurements(telemetryMeasures) + .send(); + return undefined; // buildProject() handles displaying the error } @@ -158,12 +186,41 @@ export class ProjectsController { const dacFxService = await this.getDaxFxService(); - if ((settings).upgradeExisting) { - return await dacFxService.deployDacpac(tempPath, settings.databaseName, (settings).upgradeExisting, settings.connectionUri, azdata.TaskExecutionMode.execute, settings.sqlCmdVariables, settings.deploymentOptions); - } - else { - return await dacFxService.generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, azdata.TaskExecutionMode.script, settings.sqlCmdVariables, settings.deploymentOptions); + let result: mssql.DacFxResult; + telemetryProps.profileUsed = (settings.profileUsed ?? false).toString(); + const actionStartTime = new Date().getMilliseconds(); + + try { + if ((settings).upgradeExisting) { + telemetryProps.publishAction = 'deploy'; + result = await dacFxService.deployDacpac(tempPath, settings.databaseName, (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 { @@ -171,27 +228,42 @@ export class ProjectsController { const dacFxService = await this.getDaxFxService(); const profile = await load(profileUri, dacFxService); return profile; - } - catch (e) { + } catch (e) { vscode.window.showErrorMessage(constants.profileReadError); throw e; } } public async schemaCompare(treeNode: dataworkspace.WorkspaceTreeItem): Promise { - // check if schema compare extension is installed - if (vscode.extensions.getExtension(constants.schemaCompareExtensionId)) { - // build project - const dacpacPath = await this.buildProject(treeNode); + try { + // check if schema compare extension is installed + if (vscode.extensions.getExtension(constants.schemaCompareExtensionId)) { + // build project + const dacpacPath = await this.buildProject(treeNode); - // check that dacpac exists - if (await utils.exists(dacpacPath)) { - await vscode.commands.executeCommand(constants.schemaCompareStartCommand, dacpacPath); + // check that dacpac exists + if (await utils.exists(dacpacPath)) { + TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.projectSchemaCompareCommandInvoked); + await vscode.commands.executeCommand(constants.schemaCompareStartCommand, dacpacPath); + } else { + throw new Error(constants.buildFailedCannotStartSchemaCompare); + } } else { - vscode.window.showErrorMessage(constants.buildFailedCannotStartSchemaCompare); + throw new Error(constants.schemaCompareNotInstalled); } - } else { - vscode.window.showErrorMessage(constants.schemaCompareNotInstalled); + } catch (err) { + const props: Record = {}; + 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 relativeFilePath = path.join(relativePath, itemObjectName + constants.sqlFileExtension); + const telemetryProps: Record = { itemType: itemType.type }; + const telemetryMeasurements: Record = {}; + + if (itemType.type === templates.preDeployScript) { + telemetryMeasurements.numPredeployScripts = project.preDeployScripts.length; + } else if (itemType.type === templates.postDeployScript) { + telemetryMeasurements.numPostdeployScripts = project.postDeployScripts.length; + } + try { 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); treeDataProvider?.notifyTreeDataChanged(); } catch (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); if (fileEntry) { + TelemetryReporter.sendActionEvent(TelemetryViews.ProjectTree, TelemetryActions.excludeFromProject); await project.exclude(fileEntry); } else { + TelemetryReporter.sendErrorEvent(TelemetryViews.ProjectTree, TelemetryActions.excludeFromProject); vscode.window.showErrorMessage(constants.unableToPerformAction(constants.excludeAction, node.uri.path)); } @@ -327,8 +420,16 @@ export class ProjectsController { } if (success) { + TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.deleteObjectFromProject) + .withAdditionalProperties({ objectType: node.constructor.name }) + .send(); + this.refreshProjectsTree(context); } else { + TelemetryReporter.createErrorEvent(TelemetryViews.ProjectTree, TelemetryActions.deleteObjectFromProject) + .withAdditionalProperties({ objectType: node.constructor.name }) + .send(); + vscode.window.showErrorMessage(constants.unableToPerformAction(constants.deleteAction, node.uri.path)); } } @@ -375,6 +476,9 @@ export class ProjectsController { try { 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); this.projFileWatchers.set(project.projectFilePath, projFileWatcher); @@ -496,16 +600,25 @@ export class ProjectsController { const project: Project = this.getProjectFromContext(node); let dacpacPath: string = project.dacpacOutputPath; + const preExistingDacpac = await utils.exists(dacpacPath); - if (!await utils.exists(dacpacPath)) { + const telemetryProps: Record = { preExistingDacpac: preExistingDacpac.toString() }; + + + if (!preExistingDacpac) { dacpacPath = await this.buildProject(project); } const streamingJobDefinition: string = (await fs.readFile(node.element.fileSystemUri.fsPath)).toString(); const dacFxService = await this.getDaxFxService(); + const actionStartTime = new Date().getMilliseconds(); + const result: mssql.ValidateStreamingJobResult = await dacFxService.validateStreamingJob(dacpacPath, streamingJobDefinition); + const duration = new Date().getMilliseconds() - actionStartTime; + telemetryProps.success = result.success.toString(); + if (result.success) { vscode.window.showInformationMessage(constants.externalStreamingJobValidationPassed); } @@ -513,6 +626,11 @@ export class ProjectsController { vscode.window.showErrorMessage(result.errorMessage); } + TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.runStreamingJobValidation) + .withAdditionalProperties(telemetryProps) + .withAdditionalMeasurements({ duration: duration }) + .send(); + return result; } @@ -660,8 +778,7 @@ export class ProjectsController { workspaceApi.showProjectsView(); await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)], model.newWorkspaceFilePath); } - } - catch (err) { + } catch (err) { vscode.window.showErrorMessage(utils.getErrorMessage(err)); } } diff --git a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts index 7a0c4e4ce4..044ef27925 100644 --- a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts @@ -14,6 +14,7 @@ import { cssStyles } from '../common/uiConstants'; import { IconPathHelper } from '../common/iconHelper'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { Deferred } from '../common/promise'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; export enum ReferenceType { project, @@ -51,7 +52,7 @@ export class AddDatabaseReferenceDialog { public addReference: ((proj: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings) => any) | undefined; 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); } @@ -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); this.dispose(); diff --git a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts index 0eeca9696a..3182261988 100644 --- a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts @@ -36,7 +36,7 @@ export class CreateProjectFromDatabaseDialog { public createProjectFromDatabaseCallback: ((model: ImportDataModel) => any) | 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.dialog.registerCloseValidator(async () => { return this.validate(); diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index 220aa4b73d..417010585b 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -15,6 +15,7 @@ import { DeploymentOptions, SchemaObjectType } from '../../../mssql/src/mssql'; import { IconPathHelper } from '../common/iconHelper'; import { cssStyles } from '../common/uiConstants'; import { getConnectionName } from './utils'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; interface DataSourceDropdownValue extends azdata.CategoryValue { dataSource: SqlConnectionDataSource; @@ -40,6 +41,7 @@ export class PublishDatabaseDialog { private connectionIsDataSource: boolean | undefined; private sqlCmdVars: Record | undefined; private deploymentOptions: DeploymentOptions | undefined; + private profileUsed: boolean = false; private toDispose: vscode.Disposable[] = []; @@ -48,7 +50,7 @@ export class PublishDatabaseDialog { public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined; 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); } @@ -184,7 +186,8 @@ export class PublishDatabaseDialog { upgradeExisting: true, connectionUri: await this.getConnectionUri(), sqlCmdVariables: this.getSqlCmdVariablesForPublish(), - deploymentOptions: await this.getDeploymentOptions() + deploymentOptions: await this.getDeploymentOptions(), + profileUsed: this.profileUsed }; azdata.window.closeDialog(this.dialog); @@ -194,12 +197,15 @@ export class PublishDatabaseDialog { } public async generateScriptClick(): Promise { + TelemetryReporter.sendActionEvent(TelemetryViews.SqlProjectPublishDialog, TelemetryActions.generateScriptClicked); + const sqlCmdVars = this.getSqlCmdVariablesForPublish(); const settings: IGenerateScriptSettings = { databaseName: this.getTargetDatabaseName(), connectionUri: await this.getConnectionUri(), sqlCmdVariables: sqlCmdVars, - deploymentOptions: await this.getDeploymentOptions() + deploymentOptions: await this.getDeploymentOptions(), + profileUsed: this.profileUsed }; azdata.window.closeDialog(this.dialog); diff --git a/extensions/sql-database-projects/src/models/IPublishSettings.ts b/extensions/sql-database-projects/src/models/IPublishSettings.ts index 865620a55f..ec2a62cb0e 100644 --- a/extensions/sql-database-projects/src/models/IPublishSettings.ts +++ b/extensions/sql-database-projects/src/models/IPublishSettings.ts @@ -11,6 +11,7 @@ export interface IPublishSettings { upgradeExisting: boolean; sqlCmdVariables?: Record; deploymentOptions?: DeploymentOptions; + profileUsed?: boolean; } export interface IGenerateScriptSettings { @@ -18,4 +19,5 @@ export interface IGenerateScriptSettings { connectionUri: string; sqlCmdVariables?: Record; deploymentOptions?: DeploymentOptions; + profileUsed?: boolean; } diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 250355f463..7f173db7f8 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -15,6 +15,7 @@ import { Uri, window } from 'vscode'; import { promises as fs } from 'fs'; import { DataSource } from './dataSources/dataSources'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from './IDatabaseReferenceSettings'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; /** * Class representing a Project, and providing functions for operating on it @@ -195,6 +196,8 @@ export class Project { return; } + TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.updateProjectForRoundtrip); + if (!this.importedTargets.includes(constants.NetCoreTargets)) { const result = await window.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString); if (result === constants.yesString) { @@ -387,6 +390,13 @@ export class Project { } 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 }); } } + + 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): Promise { diff --git a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts index f8135ecfa7..2b44f5a914 100644 --- a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts +++ b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts @@ -11,10 +11,8 @@ import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProje import { ProjectsController } from '../controllers/projectController'; import { Project } from '../models/project'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; -import { GetPackageInfo } from '../common/utils'; export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider { - constructor(private projectController: ProjectsController) { } @@ -76,6 +74,4 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide return vscode.Uri.file(projectFile); } - - get providerExtensionId(): string { return GetPackageInfo()!.fullName; } } diff --git a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts index 50c4427fbc..43544739c8 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts @@ -79,7 +79,8 @@ describe('Publish Database Dialog', () => { 'ProdDatabaseName': 'MyProdDatabase', 'BackupDatabaseName': 'MyBackupDatabase' }, - deploymentOptions: mockDacFxOptionsResult.deploymentOptions + deploymentOptions: mockDacFxOptionsResult.deploymentOptions, + profileUsed: false }; dialog.object.publish = (_, prof) => { profile = prof; }; @@ -94,7 +95,8 @@ describe('Publish Database Dialog', () => { 'ProdDatabaseName': 'MyProdDatabase', 'BackupDatabaseName': 'MyBackupDatabase' }, - deploymentOptions: mockDacFxOptionsResult.deploymentOptions + deploymentOptions: mockDacFxOptionsResult.deploymentOptions, + profileUsed: false }; dialog.object.generateScript = (_, prof) => { profile = prof; };