diff --git a/src/sql/platform/telemetry/common/telemetryKeys.ts b/src/sql/platform/telemetry/common/telemetryKeys.ts index fccdfffa07..46e62eb57d 100644 --- a/src/sql/platform/telemetry/common/telemetryKeys.ts +++ b/src/sql/platform/telemetry/common/telemetryKeys.ts @@ -43,6 +43,7 @@ export const enum TelemetryView { ExecutionPlan = 'ExecutionPlan', ExtensionHost = 'ExtensionHost', ExtensionRecommendationDialog = 'ExtensionRecommendationDialog', + LinkedAccounts = 'LinkedAccounts', Notebook = 'Notebook', NotifyEncryptionDialog = 'NotifyEncryptionDialog', ResultsPanel = 'ResultsPanel', @@ -53,13 +54,18 @@ export const enum TelemetryView { export const enum TelemetryError { DatabaseConnectionError = 'DatabaseConnectionError', - ObjectExplorerExpandError = 'ObjectExplorerExpandError' + ObjectExplorerExpandError = 'ObjectExplorerExpandError', + AddAzureAccountError = 'AddAzureAccountError', + AddAzureAccountErrorNoResult = 'AddAzureAccountErrorNoResult', + RefreshAzureAccountError = 'RefreshAzureAccountError', + RefreshAzureAccountErrorNoResult = 'RefreshAzureAccountErrorNoResult', } export const enum TelemetryAction { adsCommandExecuted = 'adsCommandExecuted', AddExecutionPlan = 'AddExecutionPlan', AddServerGroup = 'AddServerGroup', + AddAzureAccount = 'AddAzureAccount', BackupCreated = 'BackupCreated', ConnectToServer = 'ConnectToServer', CustomZoom = 'CustomZoom', @@ -95,6 +101,7 @@ export const enum TelemetryAction { OpenQuery = 'OpenQuery', OpenExecutionPlanProperties = 'OpenExecutionPlanProperties', PublishChanges = 'PublishChanges', + RefreshAzureAccount = 'RefreshAzureAccount', RestoreRequested = 'RestoreRequested', RunAgentJob = 'RunAgentJob', RunQuery = 'RunQuery', @@ -133,6 +140,7 @@ export const enum NbTelemetryAction { export const enum TelemetryPropertyName { ChartMaxRowCountExceeded = 'chartMaxRowCountExceeded', - ConnectionSource = 'connectionSource' + ConnectionSource = 'connectionSource', + AuthLibrary = 'AuthLibrary' } diff --git a/src/sql/workbench/services/accountManagement/browser/accountManagementService.ts b/src/sql/workbench/services/accountManagement/browser/accountManagementService.ts index 136df0f505..b33afc03dc 100644 --- a/src/sql/workbench/services/accountManagement/browser/accountManagementService.ts +++ b/src/sql/workbench/services/accountManagement/browser/accountManagementService.ts @@ -28,6 +28,8 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { filterAccounts } from 'sql/workbench/services/accountManagement/browser/accountDialog'; import { ADAL_AUTH_LIBRARY, MSAL_AUTH_LIBRARY, AuthLibrary, AZURE_AUTH_LIBRARY_CONFIG, getAuthLibrary } from 'sql/workbench/services/accountManagement/utils'; +import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import { TelemetryAction, TelemetryError, TelemetryPropertyName, TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys'; export class AccountManagementService implements IAccountManagementService { // CONSTANTS /////////////////////////////////////////////////////////// @@ -61,7 +63,8 @@ export class AccountManagementService implements IAccountManagementService { @IOpenerService private _openerService: IOpenerService, @ILogService private readonly _logService: ILogService, @INotificationService private readonly _notificationService: INotificationService, - @IConfigurationService private _configurationService: IConfigurationService + @IConfigurationService private _configurationService: IConfigurationService, + @IAdsTelemetryService private _telemetryService: IAdsTelemetryService ) { this._mementoContext = new Memento(AccountManagementService.ACCOUNT_MEMENTO, this._storageService); const mementoObj = this._mementoContext.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); @@ -147,18 +150,28 @@ export class AccountManagementService implements IAccountManagementService { if (accountResult.canceled === true) { return; } else { + this._telemetryService.createErrorEvent(TelemetryView.LinkedAccounts, TelemetryError.AddAzureAccountError, accountResult.errorCode, + this.getErrorType(accountResult.errorMessage)) + .withAdditionalProperties({ + [TelemetryPropertyName.AuthLibrary]: this._authLibrary + }) + .send(); if (accountResult.errorCode && accountResult.errorMessage) { throw new Error(localize('addAccountFailedCodeMessage', `{0} \nError Message: {1}`, accountResult.errorCode, accountResult.errorMessage)); - } else if (accountResult.errorMessage) { - throw new Error(localize('addAccountFailedMessage', `{0}`, accountResult.errorMessage)); } else { - throw new Error(genericAccountErrorMessage); + throw new Error(accountResult.errorMessage ?? genericAccountErrorMessage); } } } let result = await this._accountStore.addOrUpdate(accountResult); if (!result) { - this._logService.error('adding account failed'); + this._logService.error('Adding account failed, no result received.'); + this._telemetryService.createErrorEvent(TelemetryView.LinkedAccounts, TelemetryError.AddAzureAccountErrorNoResult, '-1', + this.getErrorType()) + .withAdditionalProperties({ + [TelemetryPropertyName.AuthLibrary]: this._authLibrary + }) + .send(); throw new Error(genericAccountErrorMessage); } if (result.accountAdded) { @@ -168,6 +181,11 @@ export class AccountManagementService implements IAccountManagementService { if (result.accountModified) { this.spliceModifiedAccount(provider, result.changedAccount); } + this._telemetryService.createActionEvent(TelemetryView.LinkedAccounts, TelemetryAction.AddAzureAccount) + .withAdditionalProperties({ + [TelemetryPropertyName.AuthLibrary]: this._authLibrary + }) + .send(); this.fireAccountListUpdate(provider, result.accountAdded); } finally { notificationHandler.close(); @@ -207,6 +225,7 @@ export class AccountManagementService implements IAccountManagementService { * @return Promise to return an account */ public refreshAccount(account: azdata.Account): Promise { + const genericAccountErrorMessage = localize('refreshAccountFailedGenericMessage', 'Refreshing account failed, check Azure Accounts log for more info.') return this.doWithProvider(account.key.providerId, async (provider) => { let refreshedAccount = await provider.provider.refresh(account); if (!this.isAccountResult(refreshedAccount)) { @@ -214,13 +233,33 @@ export class AccountManagementService implements IAccountManagementService { // Pattern here is to throw if this fails. Handled upstream. throw new Error(localize('refreshCanceled', "Refresh account was canceled by the user")); } else { - throw new Error(localize('refreshFailed', `${0} \nError Message: ${1}`, refreshedAccount.errorCode, refreshedAccount.errorMessage)); + this._telemetryService.createErrorEvent(TelemetryView.LinkedAccounts, TelemetryError.RefreshAzureAccountError, refreshedAccount.errorCode, + this.getErrorType(refreshedAccount.errorMessage)) + .withAdditionalProperties({ + [TelemetryPropertyName.AuthLibrary]: this._authLibrary + }) + .send(); + if (refreshedAccount.errorCode && refreshedAccount.errorMessage) { + throw new Error(localize('refreshFailed', `{0} \nError Message: {1}`, refreshedAccount.errorCode, refreshedAccount.errorMessage)); + } else { + throw new Error(refreshedAccount.errorMessage ?? genericAccountErrorMessage); + } } } else { account = refreshedAccount; } let result = await this._accountStore.addOrUpdate(account); + if (!result) { + this._logService.error('Refreshing account failed, no result received.'); + this._telemetryService.createErrorEvent(TelemetryView.LinkedAccounts, TelemetryError.RefreshAzureAccountErrorNoResult, '-1', + this.getErrorType()) + .withAdditionalProperties({ + [TelemetryPropertyName.AuthLibrary]: this._authLibrary + }) + .send(); + throw new Error(genericAccountErrorMessage); + } if (result.accountAdded) { // Double check that there isn't a matching account let indexToRemove = this.findAccountIndex(provider.accounts, result.changedAccount); @@ -241,11 +280,30 @@ export class AccountManagementService implements IAccountManagementService { } } + this._telemetryService.createActionEvent(TelemetryView.LinkedAccounts, TelemetryAction.RefreshAzureAccount) + .withAdditionalProperties({ + [TelemetryPropertyName.AuthLibrary]: this._authLibrary + }) + .send(); this.fireAccountListUpdate(provider, result.accountAdded); return result.changedAccount!; }); } + private getErrorType(errorMessage?: string | undefined): string { + let errorType: string = 'Unknown'; + if (errorMessage) { + if (errorMessage.toLocaleLowerCase().includes('token')) { + errorType = 'AccessToken'; + } else if (errorMessage.toLocaleLowerCase().includes('timeout')) { + errorType = 'Timeout'; + } else if (errorMessage.toLocaleLowerCase().includes('cache')) { + errorType = 'TokenCache' + } + } + return errorType; + } + /** * Retrieves metadata of all providers that have been registered * @returns Registered account providers diff --git a/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts b/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts index 8e75f17e3a..bfcc16310e 100644 --- a/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts +++ b/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts @@ -19,6 +19,7 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te import { AccountDialog } from 'sql/workbench/services/accountManagement/browser/accountDialog'; import { Emitter } from 'vs/base/common/event'; import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; +import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService'; // SUITE CONSTANTS ///////////////////////////////////////////////////////// const hasAccountProvider: azdata.AccountProviderMetadata = { @@ -538,10 +539,11 @@ function getTestState(): AccountManagementState { const testNotificationService = new TestNotificationService(); const testConfigurationService = new TestConfigurationService(); + const mockTelemetryService = new NullAdsTelemetryService(); // Create the account management service let ams = new AccountManagementService(mockInstantiationService.object, new TestStorageService(), - undefined, undefined, undefined, testNotificationService, testConfigurationService); + undefined, undefined, undefined, testNotificationService, testConfigurationService, mockTelemetryService); // Wire up event handlers let evUpdate = new EventVerifierSingle(); @@ -567,7 +569,6 @@ function getMockAccountProvider(): TypeMoq.Mock { mockProvider.setup(x => x.clear(TypeMoq.It.isAny())).returns(() => Promise.resolve()); mockProvider.setup(x => x.initialize(TypeMoq.It.isAny())).returns(param => Promise.resolve(param)); mockProvider.setup(x => x.prompt()).returns(() => Promise.resolve(account)); - return mockProvider; }