mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-18 01:25:37 -05:00
[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:
@@ -13,6 +13,7 @@ import * as styles from '../constants/styles';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { LoginMigrationStatusCodes } from '../constants/helper';
|
||||
import { MultiStepStatusDialog } from '../dialog/generic/multiStepStatusDialog';
|
||||
import { getTelemetryProps, logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemetry';
|
||||
|
||||
export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -92,10 +93,13 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
};
|
||||
|
||||
this._progressLoader.loading = false;
|
||||
logError(TelemetryViews.LoginMigrationWizard, 'LoginMigrationFailed', 'Login Migrations Internal Server Error');
|
||||
}
|
||||
|
||||
this._logMigrationResult();
|
||||
await this._loadMigratingLoginsList(this.migrationStateModel);
|
||||
await this._filterTableList('');
|
||||
this.wizard.doneButton.enabled = true;
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
@@ -307,6 +311,7 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
status = LoginMigrationStatusCodes.Succeeded;
|
||||
title = constants.LOGIN_MIGRATION_STATUS_SUCCEEDED;
|
||||
var didLoginFail = Object.keys(stateMachine._loginMigrationModel.loginMigrationsResult.exceptionMap).some(key => key.toLocaleLowerCase() === loginName.toLocaleLowerCase());
|
||||
|
||||
if (didLoginFail) {
|
||||
status = LoginMigrationStatusCodes.Failed;
|
||||
title = constants.LOGIN_MIGRATION_STATUS_FAILED;
|
||||
@@ -339,60 +344,32 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
return this._migratingLoginsTable?.data?.length || 0;
|
||||
}
|
||||
|
||||
private async _runLoginMigrations(): Promise<Boolean> {
|
||||
this._progressLoader.loading = true;
|
||||
private async _updateMigrationProgressDetails(message: string) {
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': message
|
||||
});
|
||||
}
|
||||
|
||||
private async _hideMigrationProgressDetails() {
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'CSSStyles': { 'display': 'none' },
|
||||
});
|
||||
}
|
||||
|
||||
private async _markMigrationStart() {
|
||||
if (this.migrationStateModel._targetServerInstance) {
|
||||
await this._migrationProgress.updateProperties({
|
||||
'text': constants.LOGIN_MIGRATIONS_STATUS_PAGE_DESCRIPTION(this._getTotalNumberOfLogins(), this.migrationStateModel.GetTargetType(), this.migrationStateModel._targetServerInstance.name)
|
||||
});
|
||||
}
|
||||
|
||||
await this._migrationProgressDetails.updateProperties({
|
||||
'value': constants.STARTING_LOGIN_MIGRATION
|
||||
});
|
||||
this._progressLoader.loading = true;
|
||||
await this._updateMigrationProgressDetails(constants.STARTING_LOGIN_MIGRATION);
|
||||
this._logMigrationStart();
|
||||
}
|
||||
|
||||
var result = await this.migrationStateModel._loginMigrationModel.MigrateLogins(this.migrationStateModel);
|
||||
|
||||
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' },
|
||||
});
|
||||
private async _markMigrationSuccess() {
|
||||
await this._hideMigrationProgressDetails();
|
||||
|
||||
if (this.migrationStateModel._targetServerInstance) {
|
||||
await this._migrationProgress.updateProperties({
|
||||
@@ -407,9 +384,35 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
}
|
||||
|
||||
this._progressLoader.loading = false;
|
||||
}
|
||||
|
||||
this.wizard.doneButton.enabled = true;
|
||||
return result;
|
||||
private async _runLoginMigrations(): Promise<Boolean> {
|
||||
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> {
|
||||
@@ -417,9 +420,39 @@ export class LoginMigrationStatusPage extends MigrationWizardPage {
|
||||
const dialog = new MultiStepStatusDialog(
|
||||
() => { });
|
||||
|
||||
const loginResults = this.migrationStateModel._loginMigrationModel.GetLoginMigrationResults(loginName);
|
||||
const loginResults = this.migrationStateModel._loginMigrationModel.GetDisplayResults(loginName);
|
||||
const isMigrationComplete = this.migrationStateModel._loginMigrationModel.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
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { azureResource } from 'azurecore';
|
||||
import { AzureSqlDatabaseServer, getVMInstanceView, SqlVMServer } from '../api/azure';
|
||||
import { collectSourceLogins, collectTargetLogins, getSourceConnectionId, getSourceConnectionProfile, isSourceConnectionSysAdmin, LoginTableInfo } from '../api/sqlUtils';
|
||||
import { NetworkInterfaceModel } from '../api/dataModels/azure/networkInterfaceModel';
|
||||
import { getTelemetryProps, logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemetry';
|
||||
|
||||
export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -606,6 +607,7 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
const userName = this.migrationStateModel._targetUserName;
|
||||
const password = this.migrationStateModel._targetPassword;
|
||||
const loginsOnTarget: string[] = [];
|
||||
let connectionSuccessful = false;
|
||||
if (targetDatabaseServer && userName && password) {
|
||||
try {
|
||||
connectionButtonLoadingContainer.loading = true;
|
||||
@@ -622,6 +624,7 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
this.migrationStateModel._loginMigrationModel.loginsOnTarget = loginsOnTarget;
|
||||
|
||||
await this._showConnectionResults(loginsOnTarget);
|
||||
connectionSuccessful = true;
|
||||
} catch (error) {
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
@@ -631,9 +634,20 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage {
|
||||
await this._showConnectionResults(
|
||||
loginsOnTarget,
|
||||
constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE);
|
||||
|
||||
logError(TelemetryViews.LoginMigrationTargetSelectionPage, 'ConnectingToTargetFailed', error);
|
||||
connectionSuccessful = false;
|
||||
}
|
||||
finally {
|
||||
connectionButtonLoadingContainer.loading = false;
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.LoginMigrationTargetSelectionPage,
|
||||
TelemetryAction.ConnectToTarget,
|
||||
{
|
||||
...getTelemetryProps(this.migrationStateModel),
|
||||
'connectionSuccessful': JSON.stringify(connectionSuccessful)
|
||||
},
|
||||
{});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -13,7 +13,8 @@ import * as styles from '../constants/styles';
|
||||
import { collectSourceLogins, collectTargetLogins, getSourceConnectionId, LoginTableInfo } from '../api/sqlUtils';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import * as utils from '../api/utils';
|
||||
import { LoginType } from '../models/loginMigrationModel';
|
||||
import { logError, TelemetryViews } from '../telemetry';
|
||||
|
||||
|
||||
export class LoginSelectorPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -74,7 +75,7 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.selectedWindowsLogins() && !this.migrationStateModel._aadDomainName) {
|
||||
if (this.migrationStateModel._loginMigrationModel.selectedWindowsLogins && !this.migrationStateModel._aadDomainName) {
|
||||
this.wizard.message = {
|
||||
text: constants.ENTER_AAD_DOMAIN_NAME,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
@@ -365,6 +366,8 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
text: constants.LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE('source'),
|
||||
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'),
|
||||
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 {
|
||||
return this.selectedLogins().some(logins => logins.loginType.toLocaleLowerCase() === LoginType.Windows_Login);
|
||||
private async refreshAADInputBox() {
|
||||
// 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() {
|
||||
@@ -483,12 +491,9 @@ export class LoginSelectorPage extends MigrationWizardPage {
|
||||
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;
|
||||
await this.refreshAADInputBox();
|
||||
this.updateNextButton();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { SummaryPage } from './summaryPage';
|
||||
import { LoginMigrationStatusPage } from './loginMigrationStatusPage';
|
||||
import { DatabaseSelectorPage } from './databaseSelectorPage';
|
||||
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 { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
|
||||
import { azureResource } from 'azurecore';
|
||||
@@ -131,7 +131,7 @@ export class WizardController {
|
||||
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
|
||||
const newPage = pageChangeInfo.newPage;
|
||||
const lastPage = pageChangeInfo.lastPage;
|
||||
this.sendPageButtonClickEvent(pageChangeInfo)
|
||||
this.sendPageButtonClickEvent(TelemetryViews.SqlMigrationWizard, pageChangeInfo)
|
||||
.catch(e => logError(
|
||||
TelemetryViews.MigrationWizardController,
|
||||
'ErrorSendingPageButtonClick', e));
|
||||
@@ -163,7 +163,7 @@ export class WizardController {
|
||||
TelemetryViews.SqlMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
...getTelemetryProps(this._model),
|
||||
'buttonPressed': TelemetryAction.Cancel,
|
||||
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
|
||||
},
|
||||
@@ -182,7 +182,7 @@ export class WizardController {
|
||||
TelemetryViews.SqlMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
...getTelemetryProps(this._model),
|
||||
'buttonPressed': TelemetryAction.Done,
|
||||
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
|
||||
},
|
||||
@@ -216,12 +216,16 @@ export class WizardController {
|
||||
wizardSetupPromises.push(...pages.map(p => p.registerWizardContent()));
|
||||
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._wizardObject.onPageChanged(
|
||||
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
|
||||
const newPage = pageChangeInfo.newPage;
|
||||
const lastPage = pageChangeInfo.lastPage;
|
||||
this.sendPageButtonClickEvent(pageChangeInfo)
|
||||
this.sendPageButtonClickEvent(TelemetryViews.LoginMigrationWizard, pageChangeInfo)
|
||||
.catch(e => logError(
|
||||
TelemetryViews.LoginMigrationWizardController,
|
||||
'ErrorSendingPageButtonClick', e));
|
||||
@@ -243,12 +247,25 @@ export class WizardController {
|
||||
TelemetryViews.LoginMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
...getTelemetryProps(this._model),
|
||||
'buttonPressed': TelemetryAction.Cancel,
|
||||
'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(
|
||||
@@ -308,31 +325,15 @@ export class WizardController {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) {
|
||||
private async sendPageButtonClickEvent(telemetryView: TelemetryViews, pageChangeInfo: azdata.window.WizardPageChangeInfo) {
|
||||
const buttonPressed = pageChangeInfo.newPage > pageChangeInfo.lastPage
|
||||
? TelemetryAction.Next
|
||||
: TelemetryAction.Prev;
|
||||
const pageTitle = this._wizardObject.pages[pageChangeInfo.lastPage]?.title;
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SqlMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
'buttonPressed': buttonPressed,
|
||||
'pageTitle': pageTitle
|
||||
},
|
||||
{});
|
||||
const newPageTitle = this._wizardObject.pages[pageChangeInfo.newPage]?.title;
|
||||
sendButtonClickEvent(this._model, telemetryView, buttonPressed, pageTitle, newPageTitle);
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user