diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 12bd0c683a..044c2a5fd4 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -591,8 +591,8 @@ export interface ServerAssessmentProperties { } export interface AssessmentResult { - startedOn: string; - endedOn: string; + startTime: string; + endedTime: string; assessmentResult: ServerAssessmentProperties; rawAssessmentResult: any; errors: ErrorModel[]; diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 5789294172..6aab26535f 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -7,7 +7,7 @@ "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", "icon": "images/extension.png", - "aiKey": "06ba2446-fa56-40aa-853a-26b73255b723", + "aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e", "engines": { "vscode": "*", "azdata": ">=1.29.0" @@ -151,10 +151,14 @@ }, "dependencies": { "@microsoft/ads-extension-telemetry": "^1.1.3", + "uuid": "^8.3.2", "vscode-nls": "^4.1.2" }, "__metadata": { "publisherDisplayName": "Microsoft", "publisherId": "Microsoft" + }, + "devDependencies": { + "@types/uuid": "^8.3.1" } } diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 869f7951e3..666c639158 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -403,7 +403,8 @@ export interface StartDatabaseMigrationRequest { username: string, password: string }, - scope: string + scope: string, + autoCutoverConfiguration?: AutoCutoverConfiguration } } diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index f03e5f2fe4..15cbf858f0 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -7,6 +7,7 @@ import { CategoryValue, DropDownComponent } from 'azdata'; import { DAYS, HRS, MINUTE, SEC } from '../constants/strings'; import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel'; import { MigrationContext } from '../models/migrationLocalStorage'; +import * as crypto from 'crypto'; export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { @@ -157,6 +158,10 @@ export function findDropDownItemIndex(dropDown: DropDownComponent, value: string return -1; } +export function hashString(value: string): string { + return crypto.createHash('sha512').update(value).digest('hex'); +} + export function debounce(delay: number): Function { return decorate((fn, key) => { const timerKey = `$debounce$${key}`; diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts index 1274c7b712..8df74962ac 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts @@ -5,6 +5,7 @@ import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration, getMigrationAsyncOperationDetails, AzureAsyncOperationResource } from '../../api/azure'; import { MigrationContext } from '../../models/migrationLocalStorage'; +import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery'; export enum MigrationStatus { Failed = 'Failed', @@ -34,6 +35,16 @@ export class MigrationCutoverDialogModel { this._migration.subscription, this._migration.migrationContext )); + + sendSqlMigrationActionEvent( + TelemetryViews.MigrationCutoverDialog, + TelemetryAction.MigrationStatus, + { + 'sessionId': this._migration.sessionId!, + 'migrationStatus': this.migrationStatus.properties.migrationStatus + }, + {} + ); // Logging status to help debugging. console.log(this.migrationStatus); } @@ -41,11 +52,21 @@ export class MigrationCutoverDialogModel { public async startCutover(): Promise { try { if (this.migrationStatus) { - return await startMigrationCutover( + const cutover = await startMigrationCutover( this._migration.azureAccount, this._migration.subscription, this.migrationStatus ); + sendSqlMigrationActionEvent( + TelemetryViews.MigrationCutoverDialog, + TelemetryAction.CutoverMigration, + { + 'sessionId': this._migration.sessionId!, + 'migrationEndTime': new Date().toString() + }, + {} + ); + return cutover; } } catch (error) { console.log(error); @@ -56,11 +77,21 @@ export class MigrationCutoverDialogModel { public async cancelMigration(): Promise { try { if (this.migrationStatus) { + const cutoverStartTime = new Date().toString(); await stopMigration( this._migration.azureAccount, this._migration.subscription, this.migrationStatus ); + sendSqlMigrationActionEvent( + TelemetryViews.MigrationCutoverDialog, + TelemetryAction.CancelMigration, + { + 'sessionId': this._migration.sessionId!, + 'cutoverStartTime': cutoverStartTime + }, + {} + ); } } catch (error) { console.log(error); diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index 475cb0704f..53427bf02c 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -67,7 +67,8 @@ export class MigrationLocalStorage { azureAccount: azdata.Account, subscription: azureResource.AzureResourceSubscription, controller: SqlMigrationService, - asyncURL: string): void { + asyncURL: string, + sessionId: string): void { try { let migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; migrationMementos = migrationMementos.filter(m => m.migrationContext.id !== migrationContext.id); @@ -78,7 +79,8 @@ export class MigrationLocalStorage { subscription: subscription, azureAccount: azureAccount, controller: controller, - asyncUrl: asyncURL + asyncUrl: asyncURL, + sessionId: sessionId }); this.context.globalState.update(this.mementoToken, migrationMementos); } catch (e) { @@ -99,5 +101,6 @@ export interface MigrationContext { subscription: azureResource.AzureResourceSubscription, controller: SqlMigrationService, asyncUrl: string, - asyncOperationResult?: AzureAsyncOperationResource + asyncOperationResult?: AzureAsyncOperationResource, + sessionId?: string } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 2ef7b0a03b..d9defe1998 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -13,6 +13,9 @@ import { SKURecommendations } from './externalContract'; import * as constants from '../constants/strings'; import { MigrationLocalStorage } from './migrationLocalStorage'; import * as nls from 'vscode-nls'; +import { v4 as uuidv4 } from 'uuid'; +import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemtery'; +import { hashString } from '../api/utils'; const localize = nls.loadMessageBundle(); export enum State { @@ -133,11 +136,15 @@ export class MigrationStateModel implements Model, vscode.Disposable { private _skuRecommendations: SKURecommendations | undefined; public _assessmentResults!: ServerAssessement; + private _assessmentApiResponse!: mssql.AssessmentResult; + public _vmDbs: string[] = []; public _miDbs: string[] = []; public _targetType!: MigrationTargetType; public refreshDatabaseBackupPage!: boolean; + public _sessionId: string = uuidv4(); + public excludeDbs: string[] = [ 'master', 'tempdb', @@ -177,27 +184,151 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getDatabaseAssessments(): Promise { const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId); - // stress test backend & dialog component - const assessmentResults = await this.migrationService.getAssessments( - ownerUri, - this._databaseAssessment - ); - const dbAssessments = assessmentResults?.assessmentResult.databases.filter(d => !this.excludeDbs.includes(d.name)).map(d => { - return { - name: d.name, - issues: d.items.filter(i => i.appliesToMigrationTargetPlatform === MigrationTargetType.SQLMI) ?? [] - }; - }); + this._assessmentApiResponse = (await this.migrationService.getAssessments(ownerUri, this._databaseAssessment))!; this._assessmentResults = { - issues: assessmentResults?.assessmentResult.items?.filter(i => i.appliesToMigrationTargetPlatform === MigrationTargetType.SQLMI) ?? [], - databaseAssessments: dbAssessments! ?? [] + issues: this._assessmentApiResponse.assessmentResult.items, + databaseAssessments: this._assessmentApiResponse.assessmentResult.databases.map(d => { + return { + name: d.name, + issues: d.items + }; + }) }; + // Generating all the telemetry asynchronously as we don't need to block the user for it. + this.generateAssessmentTelemetry().catch(e => console.error(e)); return this._assessmentResults; } - public findDatabaseAssessments(databaseName: string): mssql.SqlMigrationAssessmentResultItem[] | undefined { - return this._assessmentResults.databaseAssessments.find(databaseAsssessment => databaseAsssessment.name === databaseName)?.issues; + private async generateAssessmentTelemetry(): Promise { + try { + + let serverIssues = this._assessmentResults.issues.map(i => { + return { + ruleId: i.ruleId, + count: i.impactedObjects.length + }; + }); + + const serverAssessmentErrorsMap: Map = new Map(); + this._assessmentApiResponse.assessmentResult.errors.forEach(e => { + serverAssessmentErrorsMap.set(e.errorId, serverAssessmentErrorsMap.get(e.errorId) ?? 0 + 1); + }); + + let serverErrors: { errorId: number, count: number }[] = []; + serverAssessmentErrorsMap.forEach((v, k) => { + serverErrors.push( + { + errorId: k, + count: v + } + ); + }); + + const startTime = new Date(this._assessmentApiResponse.startTime); + const endTime = new Date(this._assessmentApiResponse.endedTime); + + sendSqlMigrationActionEvent( + TelemetryViews.MigrationWizardTargetSelectionPage, + TelemetryAction.ServerAssessment, + { + 'sessionId': this._sessionId, + 'tenantId': this._azureAccount.properties.tenants[0].id, + 'hashedServerName': hashString(this._assessmentApiResponse.assessmentResult.name), + 'startTime': startTime.toString(), + 'endTime': endTime.toString(), + 'serverVersion': this._assessmentApiResponse.assessmentResult.serverVersion, + 'serverEdition': this._assessmentApiResponse.assessmentResult.serverEdition, + 'platform': this._assessmentApiResponse.assessmentResult.serverHostPlatform, + 'engineEdition': this._assessmentApiResponse.assessmentResult.serverEngineEdition, + 'serverIssues': JSON.stringify(serverIssues), + 'serverErrors': JSON.stringify(serverErrors) + }, + { + 'issuesCount': this._assessmentResults.issues.length, + 'warningsCount': this._assessmentResults.databaseAssessments.reduce((count, d) => count + d.issues.length, 0), + 'durationInMilliseconds': endTime.getTime() - startTime.getTime(), + 'databaseCount': this._assessmentResults.databaseAssessments.length, + 'serverHostCpuCount': this._assessmentApiResponse.assessmentResult.cpuCoreCount, + 'serverHostPhysicalMemoryInBytes': this._assessmentApiResponse.assessmentResult.physicalServerMemory, + 'serverDatabases': this._assessmentApiResponse.assessmentResult.numberOfUserDatabases, + 'serverDatabasesReadyForMigration': this._assessmentApiResponse.assessmentResult.sqlManagedInstanceTargetReadiness.numberOfDatabasesReadyForMigration, + 'offlineDatabases': this._assessmentApiResponse.assessmentResult.sqlManagedInstanceTargetReadiness.numberOfNonOnlineDatabases + } + ); + + const databaseWarningsMap: Map = new Map(); + const databaseErrorsMap: Map = new Map(); + + this._assessmentApiResponse.assessmentResult.databases.forEach(d => { + + sendSqlMigrationActionEvent( + TelemetryViews.MigrationWizardTargetSelectionPage, + TelemetryAction.DatabaseAssessment, + { + 'sessionId': this._sessionId, + 'hashedDatabaseName': hashString(d.name), + 'compatibilityLevel': d.compatibilityLevel + }, + { + 'warningsCount': d.items.length, + 'errorsCount': d.errors.length, + 'assessmentTimeMs': d.assessmentTimeInMilliseconds, + 'numberOfBlockerIssues': d.sqlManagedInstanceTargetReadiness.numOfBlockerIssues, + 'databaseSizeInMb': d.databaseSize + } + ); + + d.items.forEach(i => { + databaseWarningsMap.set(i.ruleId, databaseWarningsMap.get(i.ruleId) ?? 0 + i.impactedObjects.length); + }); + + d.errors.forEach(e => { + databaseErrorsMap.set(e.errorId, databaseErrorsMap.get(e.errorId) ?? 0 + 1); + }); + + }); + + let databaseWarnings: { warningId: string, count: number }[] = []; + + databaseWarningsMap.forEach((v, k) => { + databaseWarnings.push({ + warningId: k, + count: v + }); + }); + + sendSqlMigrationActionEvent( + TelemetryViews.MigrationWizardTargetSelectionPage, + TelemetryAction.DatabaseAssessmentWarning, + { + 'sessionId': this._sessionId, + 'warnings': JSON.stringify(databaseWarnings) + }, + {} + ); + + let databaseErrors: { errorId: number, count: number }[] = []; + databaseErrorsMap.forEach((v, k) => { + databaseErrors.push({ + errorId: k, + count: v + }); + }); + + sendSqlMigrationActionEvent( + TelemetryViews.MigrationWizardTargetSelectionPage, + TelemetryAction.DatabaseAssessmentError, + { + 'sessionId': this._sessionId, + 'errors': JSON.stringify(databaseErrors) + }, + {} + ); + + } catch (e) { + console.log(e); + } } public get gatheringInformationError(): string | undefined { @@ -566,6 +697,15 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getBlobContainerValues(subscription: azureResource.AzureResourceSubscription, storageAccount: StorageAccount): Promise { let blobContainerValues: azdata.CategoryValue[] = []; + if (!this._azureAccount || !subscription || !storageAccount) { + blobContainerValues = [ + { + displayName: constants.NO_BLOBCONTAINERS_FOUND, + name: '' + } + ]; + return blobContainerValues; + } try { this._blobContainers = await getBlobContainers(this._azureAccount, subscription, storageAccount); this._blobContainers.forEach((blobContainer) => { @@ -704,6 +844,27 @@ export class MigrationStateModel implements Model, vscode.Disposable { response.databaseMigration.properties.sourceDatabaseName = this._migrationDbs[i]; response.databaseMigration.properties.backupConfiguration = requestBody.properties.backupConfiguration!; if (response.status === 201 || response.status === 200) { + + sendSqlMigrationActionEvent( + TelemetryViews.MigrationWizardSummaryPage, + TelemetryAction.StartMigration, + { + 'hashedServerName': hashString(this._assessmentApiResponse.assessmentResult.name), + 'hashedDatabaseName': hashString(this._migrationDbs[i]), + 'migrationMode': requestBody.properties.autoCutoverConfiguration ? 'online' : 'offline', + 'sessionId': this._sessionId, + 'migrationStartTime': new Date().toString(), + 'targetDatabaseName': this._targetDatabaseNames[i], + 'serverName': this._targetServerInstance.name, + 'tenantId': this._azureAccount.properties.tenants[0].id, + 'location': this._targetServerInstance.location, + 'sqlMigrationServiceId': this._sqlMigrationService.id, + 'irRegistered': (this._nodeNames.length > 0).toString() + }, + { + } + ); + MigrationLocalStorage.saveMigration( currentConnection!, response.databaseMigration, @@ -711,7 +872,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { this._azureAccount, this._targetSubscription, this._sqlMigrationService, - response.asyncUrl + response.asyncUrl, + this._sessionId ); vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', this._migrationDbs[i], this._targetServerInstance.name, this._targetDatabaseNames[i])); } diff --git a/extensions/sql-migration/src/telemtery.ts b/extensions/sql-migration/src/telemtery.ts index 4b72c77fac..e42ab6b158 100644 --- a/extensions/sql-migration/src/telemtery.ts +++ b/extensions/sql-migration/src/telemtery.ts @@ -6,22 +6,36 @@ import AdsTelemetryReporter, { TelemetryEventMeasures, TelemetryEventProperties } from '@microsoft/ads-extension-telemetry'; import { getPackageInfo } from './api/utils'; const packageJson = require('../package.json'); - let packageInfo = getPackageInfo(packageJson)!; export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); export enum TelemetryViews { SqlServerDashboard = 'SqlServerDashboard', - MigrationWizard = 'MigrationWizard', CreateDataMigrationServiceDialog = 'CreateDataMigrationServiceDialog', AssessmentsDialog = 'AssessmentsDialog', MigrationCutoverDialog = 'MigrationCutoverDialog', MigrationStatusDialog = 'MigrationStatusDialog', - AssessmentsPage = 'AssessmentsPage' + MigrationWizardAccountSelectionPage = 'MigrationWizardAccountSelectionPage', + MigrationWizardTargetSelectionPage = 'MigrationWizardTargetSelectionPage', + MigrationWizardSummaryPage = 'MigrationWizardSummaryPage', + StartMigrationService = 'StartMigrationSerivce' } -export function sendSqlMigrationActionEvent(telemetryView: string, telemetryAction: string, additionalProps: TelemetryEventProperties, additionalMeasurements: TelemetryEventMeasures): void { +export enum TelemetryAction { + ServerAssessment = 'ServerAssessment', + ServerAssessmentIssues = 'ServerAssessmentIssues', + ServerAssessmentError = 'ServerAssessmentError', + DatabaseAssessment = 'DatabaseAsssessment', + DatabaseAssessmentWarning = 'DatabaseAssessmentWarning', + DatabaseAssessmentError = 'DatabaseAssessmentError', + StartMigration = 'StartMigration', + CutoverMigration = 'CutoverMigration', + CancelMigration = 'CancelMigration', + MigrationStatus = 'MigrationStatus' +} + +export function sendSqlMigrationActionEvent(telemetryView: TelemetryViews, telemetryAction: TelemetryAction, additionalProps: TelemetryEventProperties, additionalMeasurements: TelemetryEventMeasures): void { TelemetryReporter.createActionEvent(telemetryView, telemetryAction) .withAdditionalProperties(additionalProps) .withAdditionalMeasurements(additionalMeasurements) diff --git a/extensions/sql-migration/yarn.lock b/extensions/sql-migration/yarn.lock index 01ce252d21..c0e12504aa 100644 --- a/extensions/sql-migration/yarn.lock +++ b/extensions/sql-migration/yarn.lock @@ -9,6 +9,11 @@ dependencies: vscode-extension-telemetry "^0.1.6" +"@types/uuid@^8.3.1": + version "8.3.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" + integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== + applicationinsights@1.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.7.4.tgz#e7d96435594d893b00cf49f70a5927105dbb8749" @@ -85,6 +90,11 @@ stack-chain@^1.3.7: resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285" integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU= +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + vscode-extension-telemetry@^0.1.6: version "0.1.7" resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.7.tgz#18389bc24127c89dade29cd2b71ba69a6ee6ad26"