[SQL-Migration] Login migrations telemetry (#22038)

This PR enhances telemetry for login migrations (and in the following ways:

Add details for starting migration (number of logins migrating, type of logins)
Log Migration result (number of errors per step, duration of each step, type of logins, if system error occurred)
Add sql-migration extension to our telemetry
Adds details when trying to connect to target
Tracks clicking "done" from the wizard
Fixes bucketizing for navigating telemetry in the login migration wizard
Sample usage of kusto query for new telemetry:
RawEventsADS
| where EventName contains 'sql-migration'
| extend view = tostring(Properties['view'])
| extend action = tostring(Properties['action'])
| extend buttonPressed = tostring(Properties['buttonpressed'])
| extend pageTitle = tostring(Properties['pagetitle'])
| extend adsVersion = tostring(Properties['common.adsversion'])
| extend targetType = tostring(Properties['targettype'])
| extend tenantId = tostring(Properties['tenantid'])
| extend subscriptionId = tostring(Properties['subscriptionid'])
| where view contains "login"
//| where adsVersion contains "1.42.0-insider"
| where ClientTimestamp >= ago(18h)
| project EventName, ClientTimestamp, SessionId, view, pageTitle, action, buttonPressed, targetType
, tenantId, subscriptionId
, adsVersion, OSVersion, Properties
This commit is contained in:
AkshayMata
2023-03-01 10:55:01 -08:00
committed by GitHub
parent d6358b3e29
commit bf05ea69ef
9 changed files with 223 additions and 95 deletions

View File

@@ -2,7 +2,7 @@
"name": "sql-migration", "name": "sql-migration",
"displayName": "%displayName%", "displayName": "%displayName%",
"description": "%description%", "description": "%description%",
"version": "1.4.1", "version": "1.4.2",
"publisher": "Microsoft", "publisher": "Microsoft",
"preview": false, "preview": false,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",

View File

@@ -9,6 +9,7 @@ import { AzureSqlDatabase, AzureSqlDatabaseServer } from './azure';
import { generateGuid } from './utils'; import { generateGuid } from './utils';
import * as utils from '../api/utils'; import * as utils from '../api/utils';
import { TelemetryAction, TelemetryViews, logError } from '../telemetry'; import { TelemetryAction, TelemetryViews, logError } from '../telemetry';
import * as constants from '../constants/strings';
const query_database_tables_sql = ` const query_database_tables_sql = `
SELECT SELECT
@@ -489,7 +490,10 @@ export async function collectTargetLogins(
return results.rows.map(row => getSqlString(row[0])) ?? []; return results.rows.map(row => getSqlString(row[0])) ?? [];
} }
throw new Error(result.errorMessage); const errorMessage = constants.COLLECTING_TARGET_LOGINS_FAILED(result.errorCode ?? 0);
const error = new Error(result.errorMessage);
logError(TelemetryViews.LoginMigrationWizard, errorMessage, error);
throw error;
} }
export async function isSourceConnectionSysAdmin(): Promise<boolean> { export async function isSourceConnectionSysAdmin(): Promise<boolean> {

View File

@@ -389,7 +389,9 @@ export const MIGRATE_LOGINS = localize('sql.login.migration.steps.migrate.logins
export const ESTABLISH_USER_MAPPINGS = localize('sql.login.migration.steps.migrate.logins', "Establish user mappings"); export const ESTABLISH_USER_MAPPINGS = localize('sql.login.migration.steps.migrate.logins', "Establish user mappings");
export const MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS = localize('sql.login.migration.steps.migrate.logins', "Migrate server roles, set login and server permissions"); export const MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS = localize('sql.login.migration.steps.migrate.logins', "Migrate server roles, set login and server permissions");
export const LOGIN_MIGRATION_COMPLETED = localize('sql.login.migration.steps.migrate.logins', "Login migration completed"); export const LOGIN_MIGRATION_COMPLETED = localize('sql.login.migration.steps.migrate.logins', "Login migration completed");
export function COLLECTING_TARGET_LOGINS_FAILED(errorCode: number): string {
return localize('sql.login.migration.collecting.target.logins.failed', "Collecting target login failed with error code {0}", errorCode);
}
// Azure SQL Target // Azure SQL Target
export const AZURE_SQL_TARGET_PAGE_TITLE = localize('sql.migration.wizard.target.title', "Azure SQL target"); export const AZURE_SQL_TARGET_PAGE_TITLE = localize('sql.migration.wizard.target.title', "Azure SQL target");

View File

@@ -15,6 +15,7 @@ type ExceptionMap = { [login: string]: any }
export enum LoginType { export enum LoginType {
Windows_Login = 'windows_login', Windows_Login = 'windows_login',
SQL_Login = 'sql_login', SQL_Login = 'sql_login',
Mixed_Mode = 'mixed_mode',
} }
export enum LoginMigrationStep { export enum LoginMigrationStep {
@@ -64,6 +65,8 @@ export class LoginMigrationModel {
public loginMigrationsResult!: contracts.StartLoginMigrationResult; public loginMigrationsResult!: contracts.StartLoginMigrationResult;
public loginMigrationsError: any; public loginMigrationsError: any;
public loginsForMigration!: LoginTableInfo[]; public loginsForMigration!: LoginTableInfo[];
public errorCountMap: Map<string, any> = new Map<string, any>();
public durationPerStep: Map<string, string> = new Map<string, string>();
private _currentStepIdx: number = 0; private _currentStepIdx: number = 0;
private _logins: Map<string, Login>; private _logins: Map<string, Login>;
private _loginMigrationSteps: LoginMigrationStep[] = []; private _loginMigrationSteps: LoginMigrationStep[] = [];
@@ -82,6 +85,32 @@ export class LoginMigrationModel {
return this._currentStepIdx === this._loginMigrationSteps.length; return this._currentStepIdx === this._loginMigrationSteps.length;
} }
public get selectedWindowsLogins(): boolean {
return this.loginsForMigration.some(logins => logins.loginType.toLocaleLowerCase() === LoginType.Windows_Login);
}
public get selectedAllWindowsLogins(): boolean {
return this.loginsForMigration.every(logins => logins.loginType.toLocaleLowerCase() === LoginType.Windows_Login);
}
public get selectedAllSQLLogins(): boolean {
return this.loginsForMigration.every(logins => logins.loginType.toLocaleLowerCase() === LoginType.SQL_Login);
}
public get loginsAuthType(): LoginType {
if (this.selectedAllWindowsLogins) {
return LoginType.Windows_Login;
} else if (this.selectedAllSQLLogins) {
return LoginType.SQL_Login;
}
return LoginType.Mixed_Mode;
}
public get hasSystemError(): boolean {
return this.loginMigrationsError ? true : false;
}
public async MigrateLogins(stateMachine: MigrationStateModel): Promise<boolean> { public async MigrateLogins(stateMachine: MigrationStateModel): Promise<boolean> {
this.addNewLogins(stateMachine._loginMigrationModel.loginsForMigration.map(row => row.loginName)); this.addNewLogins(stateMachine._loginMigrationModel.loginsForMigration.map(row => row.loginName));
return await this.runLoginMigrationStep(LoginMigrationStep.MigrateLogins, stateMachine); return await this.runLoginMigrationStep(LoginMigrationStep.MigrateLogins, stateMachine);
@@ -102,7 +131,7 @@ export class LoginMigrationModel {
} }
public GetLoginMigrationResults(loginName: string): MultiStepResult[] { public GetDisplayResults(loginName: string): MultiStepResult[] {
let loginResults: MultiStepResult[] = []; let loginResults: MultiStepResult[] = [];
let login = this.getLogin(loginName); let login = this.getLogin(loginName);
@@ -129,6 +158,15 @@ export class LoginMigrationModel {
return loginResults; return loginResults;
} }
private setErrorCountMapPerStep(step: LoginMigrationStep, result: contracts.StartLoginMigrationResult) {
const errorCount = result.exceptionMap ? Object.keys(result.exceptionMap).length : 0;
this.errorCountMap.set(LoginMigrationStep[step], errorCount);
}
private setDurationPerStep(step: LoginMigrationStep, result: contracts.StartLoginMigrationResult) {
this.durationPerStep.set(LoginMigrationStep[step], result.elapsedTime);
}
private setLoginMigrationSteps(steps: LoginMigrationStep[] = []) { private setLoginMigrationSteps(steps: LoginMigrationStep[] = []) {
this._loginMigrationSteps = []; this._loginMigrationSteps = [];
@@ -159,6 +197,10 @@ export class LoginMigrationModel {
this.markLoginStatus(loginName, loginStatus); this.markLoginStatus(loginName, loginStatus);
} }
} }
this.updateLoginMigrationResults(newResult);
this.setErrorCountMapPerStep(step, newResult);
this.setDurationPerStep(step, newResult);
} }
private updateLoginMigrationResults(newResult: contracts.StartLoginMigrationResult): void { private updateLoginMigrationResults(newResult: contracts.StartLoginMigrationResult): void {
@@ -194,7 +236,6 @@ export class LoginMigrationModel {
stateMachine._aadDomainName stateMachine._aadDomainName
))!; ))!;
this.updateLoginMigrationResults(response);
this.addLoginMigrationResults(LoginMigrationStep.MigrateLogins, response); this.addLoginMigrationResults(LoginMigrationStep.MigrateLogins, response);
return true; return true;
@@ -215,9 +256,8 @@ export class LoginMigrationModel {
stateMachine._aadDomainName stateMachine._aadDomainName
))!; ))!;
this.updateLoginMigrationResults(response);
this.addLoginMigrationResults(LoginMigrationStep.EstablishUserMapping, response); this.addLoginMigrationResults(LoginMigrationStep.EstablishUserMapping, response);
return false; return true;
} catch (error) { } catch (error) {
logError(TelemetryViews.LoginMigrationWizard, 'EstablishingUserMappingFailed', error); logError(TelemetryViews.LoginMigrationWizard, 'EstablishingUserMappingFailed', error);
@@ -236,16 +276,14 @@ export class LoginMigrationModel {
stateMachine._aadDomainName stateMachine._aadDomainName
))!; ))!;
this.updateLoginMigrationResults(response);
this.addLoginMigrationResults(LoginMigrationStep.MigrateServerRolesAndSetPermissions, response); this.addLoginMigrationResults(LoginMigrationStep.MigrateServerRolesAndSetPermissions, response);
return false; return true;
} catch (error) { } catch (error) {
logError(TelemetryViews.LoginMigrationWizard, 'MigratingServerRolesAndSettingPermissionsFailed', error); logError(TelemetryViews.LoginMigrationWizard, 'MigratingServerRolesAndSettingPermissionsFailed', error);
this.reportException(LoginMigrationStep.MigrateServerRolesAndSetPermissions, error); this.reportException(LoginMigrationStep.MigrateServerRolesAndSetPermissions, error);
this.loginMigrationsError = error; this.loginMigrationsError = error;
return true; return false;
} }
} }

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import AdsTelemetryReporter, { TelemetryEventMeasures, TelemetryEventProperties } from '@microsoft/ads-extension-telemetry'; import AdsTelemetryReporter, { TelemetryEventMeasures, TelemetryEventProperties } from '@microsoft/ads-extension-telemetry';
import { MigrationStateModel } from './models/stateMachine';
const packageJson = require('../package.json'); const packageJson = require('../package.json');
let packageInfo = { let packageInfo = {
name: packageJson.name, name: packageJson.name,
@@ -38,6 +39,9 @@ export enum TelemetryViews {
Utils = 'Utils', Utils = 'Utils',
LoginMigrationWizardController = 'LoginMigrationWizardController', LoginMigrationWizardController = 'LoginMigrationWizardController',
LoginMigrationWizard = 'LoginMigrationWizard', LoginMigrationWizard = 'LoginMigrationWizard',
LoginMigrationTargetSelectionPage = 'LoginMigrationTargetSelectionPage',
LoginMigrationSelectorPage = 'LoginMigrationSelectorPage',
LoginMigrationStatusPage = 'LoginMigrationStatusPage',
TdeConfigurationDialog = 'TdeConfigurationDialog', TdeConfigurationDialog = 'TdeConfigurationDialog',
} }
@@ -64,7 +68,11 @@ export enum TelemetryAction {
GetInstanceRequirements = 'GetInstanceRequirements', GetInstanceRequirements = 'GetInstanceRequirements',
StartDataCollection = 'StartDataCollection', StartDataCollection = 'StartDataCollection',
StopDataCollection = 'StopDataCollection', StopDataCollection = 'StopDataCollection',
GetDatabasesListFailed = 'GetDatabasesListFailed' GetDatabasesListFailed = 'GetDatabasesListFailed',
ConnectToTarget = 'ConnectToTarget',
OpenLoginMigrationWizard = 'OpenLoginMigrationWizard',
LoginMigrationStarted = 'LoginMigrationStarted',
LoginMigrationCompleted = 'LoginMigrationCompleted',
} }
export enum TelemetryErrorName { export enum TelemetryErrorName {
@@ -82,3 +90,26 @@ export function sendSqlMigrationActionEvent(telemetryView: TelemetryViews, telem
.withAdditionalMeasurements(additionalMeasurements) .withAdditionalMeasurements(additionalMeasurements)
.send(); .send();
} }
export function getTelemetryProps(migrationStateModel: MigrationStateModel): TelemetryEventProperties {
return {
'sessionId': migrationStateModel._sessionId,
'subscriptionId': migrationStateModel._targetSubscription?.id,
'resourceGroup': migrationStateModel._resourceGroup?.name,
'targetType': migrationStateModel._targetType,
'tenantId': migrationStateModel._azureAccount?.properties?.tenants[0]?.id,
};
}
export function sendButtonClickEvent(migrationStateModel: MigrationStateModel, telemetryView: TelemetryViews, buttonPressed: TelemetryAction, pageTitle: string, newPageTitle: string): void {
sendSqlMigrationActionEvent(
telemetryView,
TelemetryAction.PageButtonClick,
{
...getTelemetryProps(migrationStateModel),
'buttonPressed': buttonPressed,
'pageTitle': pageTitle,
'newPageTitle': newPageTitle
},
{});
}

View File

@@ -13,6 +13,7 @@ import * as styles from '../constants/styles';
import { IconPathHelper } from '../constants/iconPathHelper'; import { IconPathHelper } from '../constants/iconPathHelper';
import { LoginMigrationStatusCodes } from '../constants/helper'; import { LoginMigrationStatusCodes } from '../constants/helper';
import { MultiStepStatusDialog } from '../dialog/generic/multiStepStatusDialog'; import { MultiStepStatusDialog } from '../dialog/generic/multiStepStatusDialog';
import { getTelemetryProps, logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemetry';
export class LoginMigrationStatusPage extends MigrationWizardPage { export class LoginMigrationStatusPage extends MigrationWizardPage {
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
@@ -92,10 +93,13 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
}; };
this._progressLoader.loading = false; this._progressLoader.loading = false;
logError(TelemetryViews.LoginMigrationWizard, 'LoginMigrationFailed', 'Login Migrations Internal Server Error');
} }
this._logMigrationResult();
await this._loadMigratingLoginsList(this.migrationStateModel); await this._loadMigratingLoginsList(this.migrationStateModel);
await this._filterTableList(''); await this._filterTableList('');
this.wizard.doneButton.enabled = true;
} }
public async onPageLeave(): Promise<void> { public async onPageLeave(): Promise<void> {
@@ -307,6 +311,7 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
status = LoginMigrationStatusCodes.Succeeded; status = LoginMigrationStatusCodes.Succeeded;
title = constants.LOGIN_MIGRATION_STATUS_SUCCEEDED; title = constants.LOGIN_MIGRATION_STATUS_SUCCEEDED;
var didLoginFail = Object.keys(stateMachine._loginMigrationModel.loginMigrationsResult.exceptionMap).some(key => key.toLocaleLowerCase() === loginName.toLocaleLowerCase()); var didLoginFail = Object.keys(stateMachine._loginMigrationModel.loginMigrationsResult.exceptionMap).some(key => key.toLocaleLowerCase() === loginName.toLocaleLowerCase());
if (didLoginFail) { if (didLoginFail) {
status = LoginMigrationStatusCodes.Failed; status = LoginMigrationStatusCodes.Failed;
title = constants.LOGIN_MIGRATION_STATUS_FAILED; title = constants.LOGIN_MIGRATION_STATUS_FAILED;
@@ -339,60 +344,32 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
return this._migratingLoginsTable?.data?.length || 0; return this._migratingLoginsTable?.data?.length || 0;
} }
private async _runLoginMigrations(): Promise<Boolean> { private async _updateMigrationProgressDetails(message: string) {
this._progressLoader.loading = true; await this._migrationProgressDetails.updateProperties({
'value': message
});
}
private async _hideMigrationProgressDetails() {
await this._migrationProgressDetails.updateProperties({
'CSSStyles': { 'display': 'none' },
});
}
private async _markMigrationStart() {
if (this.migrationStateModel._targetServerInstance) { if (this.migrationStateModel._targetServerInstance) {
await this._migrationProgress.updateProperties({ await this._migrationProgress.updateProperties({
'text': constants.LOGIN_MIGRATIONS_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name) 'text': constants.LOGIN_MIGRATIONS_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name)
}); });
} }
await this._migrationProgressDetails.updateProperties({ this._progressLoader.loading = true;
'value': constants.STARTING_LOGIN_MIGRATION await this._updateMigrationProgressDetails(constants.STARTING_LOGIN_MIGRATION);
}); this._logMigrationStart();
}
var result = await this.migrationStateModel._loginMigrationModel.MigrateLogins(this.migrationStateModel); private async _markMigrationSuccess() {
await this._hideMigrationProgressDetails();
if (!result) {
await this._migrationProgressDetails.updateProperties({
'value': constants.STARTING_LOGIN_MIGRATION_FAILED
});
return false;
}
await this._migrationProgressDetails.updateProperties({
'value': constants.ESTABLISHING_USER_MAPPINGS
});
result = await this.migrationStateModel._loginMigrationModel.EstablishUserMappings(this.migrationStateModel);
if (!result) {
await this._migrationProgressDetails.updateProperties({
'value': constants.ESTABLISHING_USER_MAPPINGS_FAILED
});
return false;
}
await this._migrationProgressDetails.updateProperties({
'value': constants.MIGRATING_SERVER_ROLES_AND_SET_PERMISSIONS
});
result = await this.migrationStateModel._loginMigrationModel.MigrateServerRolesAndSetPermissions(this.migrationStateModel);
if (!result) {
await this._migrationProgressDetails.updateProperties({
'value': constants.MIGRATING_SERVER_ROLES_AND_SET_PERMISSIONS_FAILED
});
return false;
}
await this._migrationProgressDetails.updateProperties({
'CSSStyles': { 'display': 'none' },
});
if (this.migrationStateModel._targetServerInstance) { if (this.migrationStateModel._targetServerInstance) {
await this._migrationProgress.updateProperties({ await this._migrationProgress.updateProperties({
@@ -407,9 +384,35 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
} }
this._progressLoader.loading = false; this._progressLoader.loading = false;
}
this.wizard.doneButton.enabled = true; private async _runLoginMigrations(): Promise<Boolean> {
return result; await this._markMigrationStart();
var result = await this.migrationStateModel._loginMigrationModel.MigrateLogins(this.migrationStateModel);
if (!result) {
await this._updateMigrationProgressDetails(constants.STARTING_LOGIN_MIGRATION_FAILED);
return false;
}
await this._updateMigrationProgressDetails(constants.ESTABLISHING_USER_MAPPINGS);
result = await this.migrationStateModel._loginMigrationModel.EstablishUserMappings(this.migrationStateModel);
if (!result) {
await this._updateMigrationProgressDetails(constants.ESTABLISHING_USER_MAPPINGS_FAILED);
return false;
}
await this._updateMigrationProgressDetails(constants.MIGRATING_SERVER_ROLES_AND_SET_PERMISSIONS);
result = await this.migrationStateModel._loginMigrationModel.MigrateServerRolesAndSetPermissions(this.migrationStateModel);
if (!result) {
await this._updateMigrationProgressDetails(constants.MIGRATING_SERVER_ROLES_AND_SET_PERMISSIONS_FAILED);
return false;
}
await this._markMigrationSuccess();
return true;
} }
private async _showLoginDetailsDialog(loginName: string): Promise<void> { private async _showLoginDetailsDialog(loginName: string): Promise<void> {
@@ -417,9 +420,39 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
const dialog = new MultiStepStatusDialog( const dialog = new MultiStepStatusDialog(
() => { }); () => { });
const loginResults = this.migrationStateModel._loginMigrationModel.GetLoginMigrationResults(loginName); const loginResults = this.migrationStateModel._loginMigrationModel.GetDisplayResults(loginName);
const isMigrationComplete = this.migrationStateModel._loginMigrationModel.isMigrationComplete; const isMigrationComplete = this.migrationStateModel._loginMigrationModel.isMigrationComplete;
await dialog.openDialog(constants.LOGIN_MIGRATIONS_LOGIN_STATUS_DETAILS_TITLE(loginName), loginResults, isMigrationComplete); await dialog.openDialog(constants.LOGIN_MIGRATIONS_LOGIN_STATUS_DETAILS_TITLE(loginName), loginResults, isMigrationComplete);
} }
private _logMigrationStart(): void {
sendSqlMigrationActionEvent(
TelemetryViews.LoginMigrationStatusPage,
TelemetryAction.LoginMigrationStarted,
{
...getTelemetryProps(this.migrationStateModel),
'loginsAuthType': this.migrationStateModel._loginMigrationModel.loginsAuthType,
},
{
'numberLogins': this.migrationStateModel._loginMigrationModel.loginsForMigration.length,
}
);
}
private _logMigrationResult(): void {
sendSqlMigrationActionEvent(
TelemetryViews.LoginMigrationStatusPage,
TelemetryAction.LoginMigrationCompleted,
{
...getTelemetryProps(this.migrationStateModel),
'loginsAuthType': this.migrationStateModel._loginMigrationModel.loginsAuthType,
'numberLoginsFailingPerStep': JSON.stringify(Array.from(this.migrationStateModel._loginMigrationModel.errorCountMap)),
'durationPerStepTimestamp': JSON.stringify(Array.from(this.migrationStateModel._loginMigrationModel.durationPerStep)),
'hasSystemError': JSON.stringify(this.migrationStateModel._loginMigrationModel.hasSystemError),
// AKMA TODO: add error code string count map
},
{}
);
}
} }

View File

@@ -16,6 +16,7 @@ import { azureResource } from 'azurecore';
import { AzureSqlDatabaseServer, getVMInstanceView, SqlVMServer } from '../api/azure'; import { AzureSqlDatabaseServer, getVMInstanceView, SqlVMServer } from '../api/azure';
import { collectSourceLogins, collectTargetLogins, getSourceConnectionId, getSourceConnectionProfile, isSourceConnectionSysAdmin, LoginTableInfo } from '../api/sqlUtils'; import { collectSourceLogins, collectTargetLogins, getSourceConnectionId, getSourceConnectionProfile, isSourceConnectionSysAdmin, LoginTableInfo } from '../api/sqlUtils';
import { NetworkInterfaceModel } from '../api/dataModels/azure/networkInterfaceModel'; import { NetworkInterfaceModel } from '../api/dataModels/azure/networkInterfaceModel';
import { getTelemetryProps, logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemetry';
export class LoginMigrationTargetSelectionPage extends MigrationWizardPage { export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
@@ -606,6 +607,7 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
const userName = this.migrationStateModel._targetUserName; const userName = this.migrationStateModel._targetUserName;
const password = this.migrationStateModel._targetPassword; const password = this.migrationStateModel._targetPassword;
const loginsOnTarget: string[] = []; const loginsOnTarget: string[] = [];
let connectionSuccessful = false;
if (targetDatabaseServer && userName && password) { if (targetDatabaseServer && userName && password) {
try { try {
connectionButtonLoadingContainer.loading = true; connectionButtonLoadingContainer.loading = true;
@@ -622,6 +624,7 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
this.migrationStateModel._loginMigrationModel.loginsOnTarget = loginsOnTarget; this.migrationStateModel._loginMigrationModel.loginsOnTarget = loginsOnTarget;
await this._showConnectionResults(loginsOnTarget); await this._showConnectionResults(loginsOnTarget);
connectionSuccessful = true;
} catch (error) { } catch (error) {
this.wizard.message = { this.wizard.message = {
level: azdata.window.MessageLevel.Error, level: azdata.window.MessageLevel.Error,
@@ -631,9 +634,20 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
await this._showConnectionResults( await this._showConnectionResults(
loginsOnTarget, loginsOnTarget,
constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE); constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE);
logError(TelemetryViews.LoginMigrationTargetSelectionPage, 'ConnectingToTargetFailed', error);
connectionSuccessful = false;
} }
finally { finally {
connectionButtonLoadingContainer.loading = false; connectionButtonLoadingContainer.loading = false;
sendSqlMigrationActionEvent(
TelemetryViews.LoginMigrationTargetSelectionPage,
TelemetryAction.ConnectToTarget,
{
...getTelemetryProps(this.migrationStateModel),
'connectionSuccessful': JSON.stringify(connectionSuccessful)
},
{});
} }
} }
})); }));

View File

@@ -13,7 +13,8 @@ import * as styles from '../constants/styles';
import { collectSourceLogins, collectTargetLogins, getSourceConnectionId, LoginTableInfo } from '../api/sqlUtils'; import { collectSourceLogins, collectTargetLogins, getSourceConnectionId, LoginTableInfo } from '../api/sqlUtils';
import { IconPathHelper } from '../constants/iconPathHelper'; import { IconPathHelper } from '../constants/iconPathHelper';
import * as utils from '../api/utils'; import * as utils from '../api/utils';
import { LoginType } from '../models/loginMigrationModel'; import { logError, TelemetryViews } from '../telemetry';
export class LoginSelectorPage extends MigrationWizardPage { export class LoginSelectorPage extends MigrationWizardPage {
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
@@ -74,7 +75,7 @@ export class LoginSelectorPage extends MigrationWizardPage {
return false; return false;
} }
if (this.selectedWindowsLogins() && !this.migrationStateModel._aadDomainName) { if (this.migrationStateModel._loginMigrationModel.selectedWindowsLogins && !this.migrationStateModel._aadDomainName) {
this.wizard.message = { this.wizard.message = {
text: constants.ENTER_AAD_DOMAIN_NAME, text: constants.ENTER_AAD_DOMAIN_NAME,
level: azdata.window.MessageLevel.Error level: azdata.window.MessageLevel.Error
@@ -365,6 +366,8 @@ export class LoginSelectorPage extends MigrationWizardPage {
text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('source'), text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('source'),
description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message), description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message),
}; };
logError(TelemetryViews.LoginMigrationWizard, 'CollectingSourceLoginsFailed', error);
} }
} }
@@ -396,6 +399,8 @@ export class LoginSelectorPage extends MigrationWizardPage {
text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('target'), text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('target'),
description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message), description: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR(error.message),
}; };
logError(TelemetryViews.LoginMigrationWizard, 'CollectingTargetLoginsFailed', error);
} }
} }
@@ -471,8 +476,11 @@ export class LoginSelectorPage extends MigrationWizardPage {
|| []; || [];
} }
private selectedWindowsLogins(): boolean { private async refreshAADInputBox() {
return this.selectedLogins().some(logins => logins.loginType.toLocaleLowerCase() === LoginType.Windows_Login); // Display AAD Domain Name input box only if windows logins selected, else disable
const selectedWindowsLogins = this.migrationStateModel._loginMigrationModel.selectedWindowsLogins;
await utils.updateControlDisplay(this._aadDomainNameContainer, selectedWindowsLogins);
await this._loginSelectorTable.updateProperty("height", selectedWindowsLogins ? 600 : 650);
} }
private async updateValuesOnSelection() { private async updateValuesOnSelection() {
@@ -483,12 +491,9 @@ export class LoginSelectorPage extends MigrationWizardPage {
this._loginSelectorTable.data?.length || 0) this._loginSelectorTable.data?.length || 0)
}); });
// Display AAD Domain Name input box if windows logins selected, else disable
const hasSelectedWindowsLogins = this.selectedWindowsLogins()
await utils.updateControlDisplay(this._aadDomainNameContainer, hasSelectedWindowsLogins);
await this._loginSelectorTable.updateProperty("height", hasSelectedWindowsLogins ? 600 : 650);
this.migrationStateModel._loginMigrationModel.loginsForMigration = selectedLogins; this.migrationStateModel._loginMigrationModel.loginsForMigration = selectedLogins;
this.migrationStateModel._loginMigrationModel.loginsForMigration = selectedLogins;
await this.refreshAADInputBox();
this.updateNextButton(); this.updateNextButton();
} }

View File

@@ -16,7 +16,7 @@ import { SummaryPage } from './summaryPage';
import { LoginMigrationStatusPage } from './loginMigrationStatusPage'; import { LoginMigrationStatusPage } from './loginMigrationStatusPage';
import { DatabaseSelectorPage } from './databaseSelectorPage'; import { DatabaseSelectorPage } from './databaseSelectorPage';
import { LoginSelectorPage } from './loginSelectorPage'; import { LoginSelectorPage } from './loginSelectorPage';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemetry'; import { sendSqlMigrationActionEvent, sendButtonClickEvent, TelemetryAction, TelemetryViews, logError, getTelemetryProps } from '../telemetry';
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage'; import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
import { azureResource } from 'azurecore'; import { azureResource } from 'azurecore';
@@ -131,7 +131,7 @@ export class WizardController {
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => { async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
const newPage = pageChangeInfo.newPage; const newPage = pageChangeInfo.newPage;
const lastPage = pageChangeInfo.lastPage; const lastPage = pageChangeInfo.lastPage;
this.sendPageButtonClickEvent(pageChangeInfo) this.sendPageButtonClickEvent(TelemetryViews.SqlMigrationWizard, pageChangeInfo)
.catch(e => logError( .catch(e => logError(
TelemetryViews.MigrationWizardController, TelemetryViews.MigrationWizardController,
'ErrorSendingPageButtonClick', e)); 'ErrorSendingPageButtonClick', e));
@@ -163,7 +163,7 @@ export class WizardController {
TelemetryViews.SqlMigrationWizard, TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick, TelemetryAction.PageButtonClick,
{ {
...this.getTelemetryProps(), ...getTelemetryProps(this._model),
'buttonPressed': TelemetryAction.Cancel, 'buttonPressed': TelemetryAction.Cancel,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
}, },
@@ -182,7 +182,7 @@ export class WizardController {
TelemetryViews.SqlMigrationWizard, TelemetryViews.SqlMigrationWizard,
TelemetryAction.PageButtonClick, TelemetryAction.PageButtonClick,
{ {
...this.getTelemetryProps(), ...getTelemetryProps(this._model),
'buttonPressed': TelemetryAction.Done, 'buttonPressed': TelemetryAction.Done,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
}, },
@@ -216,12 +216,16 @@ export class WizardController {
wizardSetupPromises.push(...pages.map(p => p.registerWizardContent())); wizardSetupPromises.push(...pages.map(p => p.registerWizardContent()));
wizardSetupPromises.push(this._wizardObject.open()); wizardSetupPromises.push(this._wizardObject.open());
// Emit telemetry for starting login migration wizard
const firstPageTitle = this._wizardObject.pages.length > 0 ? this._wizardObject.pages[0].title : "";
sendButtonClickEvent(this._model, TelemetryViews.LoginMigrationWizard, TelemetryAction.OpenLoginMigrationWizard, "", firstPageTitle);
this._model.extensionContext.subscriptions.push( this._model.extensionContext.subscriptions.push(
this._wizardObject.onPageChanged( this._wizardObject.onPageChanged(
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => { async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
const newPage = pageChangeInfo.newPage; const newPage = pageChangeInfo.newPage;
const lastPage = pageChangeInfo.lastPage; const lastPage = pageChangeInfo.lastPage;
this.sendPageButtonClickEvent(pageChangeInfo) this.sendPageButtonClickEvent(TelemetryViews.LoginMigrationWizard, pageChangeInfo)
.catch(e => logError( .catch(e => logError(
TelemetryViews.LoginMigrationWizardController, TelemetryViews.LoginMigrationWizardController,
'ErrorSendingPageButtonClick', e)); 'ErrorSendingPageButtonClick', e));
@@ -243,12 +247,25 @@ export class WizardController {
TelemetryViews.LoginMigrationWizard, TelemetryViews.LoginMigrationWizard,
TelemetryAction.PageButtonClick, TelemetryAction.PageButtonClick,
{ {
...this.getTelemetryProps(), ...getTelemetryProps(this._model),
'buttonPressed': TelemetryAction.Cancel, 'buttonPressed': TelemetryAction.Cancel,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
}, },
{}); {});
})); }));
this._disposables.push(
this._wizardObject.doneButton.onClick(async (e) => {
sendSqlMigrationActionEvent(
TelemetryViews.LoginMigrationWizard,
TelemetryAction.PageButtonClick,
{
...getTelemetryProps(this._model),
'buttonPressed': TelemetryAction.Done,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
},
{});
}));
} }
private async updateServiceContext( private async updateServiceContext(
@@ -308,31 +325,15 @@ export class WizardController {
return undefined; return undefined;
} }
private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) { private async sendPageButtonClickEvent(telemetryView: TelemetryViews, pageChangeInfo: azdata.window.WizardPageChangeInfo) {
const buttonPressed = pageChangeInfo.newPage > pageChangeInfo.lastPage const buttonPressed = pageChangeInfo.newPage > pageChangeInfo.lastPage
? TelemetryAction.Next ? TelemetryAction.Next
: TelemetryAction.Prev; : TelemetryAction.Prev;
const pageTitle = this._wizardObject.pages[pageChangeInfo.lastPage]?.title; const pageTitle = this._wizardObject.pages[pageChangeInfo.lastPage]?.title;
sendSqlMigrationActionEvent( const newPageTitle = this._wizardObject.pages[pageChangeInfo.newPage]?.title;
TelemetryViews.SqlMigrationWizard, sendButtonClickEvent(this._model, telemetryView, buttonPressed, pageTitle, newPageTitle);
TelemetryAction.PageButtonClick,
{
...this.getTelemetryProps(),
'buttonPressed': buttonPressed,
'pageTitle': pageTitle
},
{});
} }
private getTelemetryProps() {
return {
'sessionId': this._model._sessionId,
'subscriptionId': this._model._targetSubscription?.id,
'resourceGroup': this._model._resourceGroup?.name,
'targetType': this._model._targetType,
'tenantId': this._model?._azureAccount?.properties?.tenants[0]?.id
};
}
} }
export function createInformationRow( export function createInformationRow(