vCore: unified telemetry event names, removed obsolete telemetry calls (#2409)

vCore:
- Resource Tracking: Introduced tracking for resource and workspace trees, with further updates and improvements.
- Command Tracking: Enhanced command tracking.
- Telemetry Cleanup: Removed outdated telemetry data.
Shared:
- Telemetry Unification: Consolidated telemetry for 'getChildren' events at the Cosmos DB account level.
This commit is contained in:
Tomasz Naumowicz 2024-11-13 17:44:33 +01:00 коммит произвёл GitHub
Родитель 6b7f7d86ce
Коммит 77681e4e9b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
18 изменённых файлов: 450 добавлений и 421 удалений

Просмотреть файл

@ -589,52 +589,42 @@
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.hello",
"title": "Dev: Say Hello!"
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.webview",
"title": "Dev: Show WebView"
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.dropCollection",
"command": "command.mongoClusters.dropCollection",
"title": "Drop Collection..."
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.dropDatabase",
"command": "command.mongoClusters.dropDatabase",
"title": "Drop Database..."
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.createCollection",
"command": "command.mongoClusters.createCollection",
"title": "Create Collection..."
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.createDatabase",
"command": "command.mongoClusters.createDatabase",
"title": "Create Database..."
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.importDocuments",
"command": "command.mongoClusters.importDocuments",
"title": "Import Documents into Collection..."
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.exportDocuments",
"command": "command.mongoClusters.exportDocuments",
"title": "Export Documents from Collection..."
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.launchShell",
"command": "command.mongoClusters.launchShell",
"title": "Launch Shell"
},
{
"category": "MongoDB (vCore)",
"command": "mongoClusters.cmd.removeWorkspaceConnection",
"command": "command.mongoClusters.removeWorkspaceConnection",
"title": "Remove Connection..."
}
],
@ -1135,38 +1125,38 @@
"group": "1@1"
},
{
"command": "mongoClusters.cmd.dropCollection",
"command": "command.mongoClusters.dropCollection",
"when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.collection"
},
{
"command": "mongoClusters.cmd.dropDatabase",
"command": "command.mongoClusters.dropDatabase",
"when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.database"
},
{
"command": "mongoClusters.cmd.removeWorkspaceConnection",
"command": "command.mongoClusters.removeWorkspaceConnection",
"when": "vscodeDatabases.mongoClustersSupportEnabled && view == azureWorkspace && viewItem == mongoClusters.item.mongoCluster"
},
{
"command": "mongoClusters.cmd.createCollection",
"command": "command.mongoClusters.createCollection",
"when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.database"
},
{
"command": "mongoClusters.cmd.createDatabase",
"command": "command.mongoClusters.createDatabase",
"when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /mongoClusters.item.mongoCluster/i",
"group": "1@1"
},
{
"command": "mongoClusters.cmd.importDocuments",
"command": "command.mongoClusters.importDocuments",
"when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.collection",
"group": "1@1"
},
{
"command": "mongoClusters.cmd.exportDocuments",
"command": "command.mongoClusters.exportDocuments",
"when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem == mongoClusters.item.collection",
"group": "1@2"
},
{
"command": "mongoClusters.cmd.launchShell",
"command": "command.mongoClusters.launchShell",
"when": "vscodeDatabases.mongoClustersSupportEnabled && viewItem =~ /mongoClusters.item.(mongoCluster|database|collection)/i",
"group": "2@1"
}
@ -1213,14 +1203,6 @@
{
"command": "postgreSQL.executeQuery",
"when": "editorLangId == 'postgres'"
},
{
"command": "mongoClusters.cmd.hello",
"when": "vscodeDatabases.mongoClustersSupportEnabled"
},
{
"command": "mongoClusters.cmd.webview",
"when": "vscodeDatabases.mongoClustersSupportEnabled"
}
]
},

Просмотреть файл

@ -9,6 +9,7 @@ import { nonNullProp } from './utils/nonNull';
export enum API {
MongoDB = 'MongoDB',
MongoClusters = 'MongoClusters',
Graph = 'Graph',
Table = 'Table',
Core = 'Core', // Now called NoSQL

Просмотреть файл

@ -13,11 +13,14 @@ import {
type Resource,
} from '@azure/cosmos';
import {
callWithTelemetryAndErrorHandling,
type AzExtParentTreeItem,
type AzExtTreeItem,
type IActionContext,
type ICreateChildImplContext,
} from '@microsoft/vscode-azext-utils';
import type * as vscode from 'vscode';
import { API } from '../../AzureDBExperiences';
import { type IDeleteWizardContext } from '../../commands/deleteDatabaseAccount/IDeleteWizardContext';
import { deleteCosmosDBAccount } from '../../commands/deleteDatabaseAccount/deleteCosmosDBAccount';
import { getThemeAgnosticIconPath, SERVERLESS_CAPABILITY_NAME } from '../../constants';
@ -98,31 +101,55 @@ export abstract class DocDBAccountTreeItemBase extends DocDBTreeItemBase<Databas
}
public async loadMoreChildrenImpl(clearCache: boolean): Promise<AzExtTreeItem[]> {
if (this.root.isEmulator) {
const unableToReachEmulatorMessage: string =
"Unable to reach emulator. Please ensure it is started and connected to the port specified by the 'cosmosDB.emulator.port' setting, then try again.";
return await rejectOnTimeout(
2000,
() => super.loadMoreChildrenImpl(clearCache),
unableToReachEmulatorMessage,
);
} else {
try {
return await super.loadMoreChildrenImpl(clearCache);
} catch (e) {
if (e instanceof Error && isRbacException(e) && !this.hasShownRbacNotification) {
this.hasShownRbacNotification = true;
const principalId = (await getSignedInPrincipalIdForAccountEndpoint(this.root.endpoint)) ?? '';
// chedck if the principal ID matches the one that is signed in, otherwise this might be a security problem, hence show the error message
if (e.message.includes(`[${principalId}]`) && (await ensureRbacPermission(this, principalId))) {
const result = await callWithTelemetryAndErrorHandling(
'getChildren',
async (context: IActionContext): Promise<AzExtTreeItem[]> => {
context.telemetry.properties.parentContext = this.contextValue;
// move this to a shared file, currently it's defined in DocDBAccountTreeItem so I can't reference it here
if (this.contextValue.includes('cosmosDBDocumentServer')) {
context.telemetry.properties.experience = API.Core;
// same issue as above
} else if (this.contextValue.includes('cosmosDBGraphAccount')) {
context.telemetry.properties.experience = API.Graph;
// same issue as above
} else if (this.contextValue.includes('cosmosDBTableAccount')) {
context.telemetry.properties.experience = API.Table;
}
if (this.root.isEmulator) {
const unableToReachEmulatorMessage: string =
"Unable to reach emulator. Please ensure it is started and connected to the port specified by the 'cosmosDB.emulator.port' setting, then try again.";
return await rejectOnTimeout(
2000,
() => super.loadMoreChildrenImpl(clearCache),
unableToReachEmulatorMessage,
);
} else {
try {
return await super.loadMoreChildrenImpl(clearCache);
} else {
void showRbacPermissionError(this.fullId, principalId);
} catch (e) {
if (e instanceof Error && isRbacException(e) && !this.hasShownRbacNotification) {
this.hasShownRbacNotification = true;
const principalId =
(await getSignedInPrincipalIdForAccountEndpoint(this.root.endpoint)) ?? '';
// chedck if the principal ID matches the one that is signed in, otherwise this might be a security problem, hence show the error message
if (
e.message.includes(`[${principalId}]`) &&
(await ensureRbacPermission(this, principalId))
) {
return await super.loadMoreChildrenImpl(clearCache);
} else {
void showRbacPermissionError(this.fullId, principalId);
}
}
throw e; // rethrowing tells the resources extension to show the exception message in the tree
}
}
throw e; // rethrowing tells the resources extension to show the exception message in the tree
}
}
},
);
return result ?? [];
}
public async deleteTreeItemImpl(context: IDeleteWizardContext): Promise<void> {

Просмотреть файл

@ -7,12 +7,15 @@ import { type DatabaseAccountGetResults } from '@azure/arm-cosmosdb/src/models';
import {
appendExtensionUserAgent,
AzExtParentTreeItem,
callWithTelemetryAndErrorHandling,
parseError,
type AzExtTreeItem,
type IActionContext,
type ICreateChildImplContext,
} from '@microsoft/vscode-azext-utils';
import { type MongoClient } from 'mongodb';
import type * as vscode from 'vscode';
import { API } from '../../AzureDBExperiences';
import { deleteCosmosDBAccount } from '../../commands/deleteDatabaseAccount/deleteCosmosDBAccount';
import { type IDeleteWizardContext } from '../../commands/deleteDatabaseAccount/IDeleteWizardContext';
import { getThemeAgnosticIconPath, Links, testDb } from '../../constants';
@ -63,56 +66,70 @@ export class MongoAccountTreeItem extends AzExtParentTreeItem {
}
public async loadMoreChildrenImpl(_clearCache: boolean): Promise<AzExtTreeItem[]> {
let mongoClient: MongoClient | undefined;
try {
let databases: IDatabaseInfo[];
const result = await callWithTelemetryAndErrorHandling(
'getChildren',
async (context: IActionContext): Promise<AzExtTreeItem[]> => {
context.telemetry.properties.experience = API.MongoDB;
context.telemetry.properties.parentContext = this.contextValue;
if (!this.connectionString) {
throw new Error('Missing connection string');
}
let mongoClient: MongoClient | undefined;
try {
let databases: IDatabaseInfo[];
// Azure MongoDB accounts need to have the name passed in for private endpoints
mongoClient = await connectToMongoClient(
this.connectionString,
this.databaseAccount ? nonNullProp(this.databaseAccount, 'name') : appendExtensionUserAgent(),
);
if (!this.connectionString) {
throw new Error('Missing connection string');
}
const databaseInConnectionString = getDatabaseNameFromConnectionString(this.connectionString);
if (databaseInConnectionString && !this.root.isEmulator) {
// emulator violates the connection string format
// If the database is in the connection string, that's all we connect to (we might not even have permissions to list databases)
databases = [
{
name: databaseInConnectionString,
empty: false,
},
];
} else {
// https://mongodb.github.io/node-mongodb-native/3.1/api/index.html
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result: { databases: IDatabaseInfo[] } = await mongoClient.db(testDb).admin().listDatabases();
databases = result.databases;
}
return databases
.filter(
(database: IDatabaseInfo) =>
!(database.name && database.name.toLowerCase() === 'admin' && database.empty),
) // Filter out the 'admin' database if it's empty
.map(
(database) => new MongoDatabaseTreeItem(this, nonNullProp(database, 'name'), this.connectionString),
);
} catch (error) {
const message = parseError(error).message;
if (this.root?.isEmulator && message.includes('ECONNREFUSED')) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.message = `Unable to reach emulator. See ${Links.LocalConnectionDebuggingTips} for debugging tips.\n${message}`;
}
throw error;
} finally {
if (mongoClient) {
void mongoClient.close();
}
}
// Azure MongoDB accounts need to have the name passed in for private endpoints
mongoClient = await connectToMongoClient(
this.connectionString,
this.databaseAccount ? nonNullProp(this.databaseAccount, 'name') : appendExtensionUserAgent(),
);
const databaseInConnectionString = getDatabaseNameFromConnectionString(this.connectionString);
if (databaseInConnectionString && !this.root.isEmulator) {
// emulator violates the connection string format
// If the database is in the connection string, that's all we connect to (we might not even have permissions to list databases)
databases = [
{
name: databaseInConnectionString,
empty: false,
},
];
} else {
// https://mongodb.github.io/node-mongodb-native/3.1/api/index.html
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result: { databases: IDatabaseInfo[] } = await mongoClient
.db(testDb)
.admin()
.listDatabases();
databases = result.databases;
}
return databases
.filter(
(database: IDatabaseInfo) =>
!(database.name && database.name.toLowerCase() === 'admin' && database.empty),
) // Filter out the 'admin' database if it's empty
.map(
(database) =>
new MongoDatabaseTreeItem(this, nonNullProp(database, 'name'), this.connectionString),
);
} catch (error) {
const message = parseError(error).message;
if (this.root?.isEmulator && message.includes('ECONNREFUSED')) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.message = `Unable to reach emulator. See ${Links.LocalConnectionDebuggingTips} for debugging tips.\n${message}`;
}
throw error;
} finally {
if (mongoClient) {
void mongoClient.close();
}
}
},
);
return result ?? [];
}
public async createChildImpl(context: ICreateChildImplContext): Promise<MongoDatabaseTreeItem> {

Просмотреть файл

@ -43,85 +43,77 @@ export class MongoClustersExtension implements vscode.Disposable {
}
async activate(): Promise<void> {
await callWithTelemetryAndErrorHandling('mongoClusters.activate', async (activateContext: IActionContext) => {
activateContext.telemetry.properties.isActivationEvent = 'true';
await callWithTelemetryAndErrorHandling(
'cosmosDB.mongoClusters.activate',
async (activateContext: IActionContext) => {
activateContext.telemetry.properties.isActivationEvent = 'true';
const isMongoClustersEnabled: boolean = isMongoClustersSupportenabled() ?? false;
const isMongoClustersEnabled: boolean = isMongoClustersSupportenabled() ?? false;
activateContext.telemetry.properties.mongoClustersEnabled = isMongoClustersEnabled.toString();
activateContext.telemetry.properties.mongoClustersEnabled = isMongoClustersEnabled.toString();
// allows to show/hide commands in the package.json file
vscode.commands.executeCommand(
'setContext',
'vscodeDatabases.mongoClustersSupportEnabled',
isMongoClustersEnabled,
);
// allows to show/hide commands in the package.json file
vscode.commands.executeCommand(
'setContext',
'vscodeDatabases.mongoClustersSupportEnabled',
isMongoClustersEnabled,
);
if (!isMongoClustersEnabled) {
return;
}
if (!isMongoClustersEnabled) {
return;
}
// // // MongoClusters / MongoDB (vCore) support is enabled // // //
// // // MongoClusters / MongoDB (vCore) support is enabled // // //
ext.mongoClustersBranchDataProvider = new MongoClustersBranchDataProvider();
ext.rgApiV2.resources.registerAzureResourceBranchDataProvider(
AzExtResourceType.MongoClusters,
ext.mongoClustersBranchDataProvider,
);
ext.mongoClustersBranchDataProvider = new MongoClustersBranchDataProvider();
ext.rgApiV2.resources.registerAzureResourceBranchDataProvider(
AzExtResourceType.MongoClusters,
ext.mongoClustersBranchDataProvider,
);
ext.workspaceDataProvider = new SharedWorkspaceResourceProvider();
ext.rgApiV2.resources.registerWorkspaceResourceProvider(ext.workspaceDataProvider);
ext.workspaceDataProvider = new SharedWorkspaceResourceProvider();
ext.rgApiV2.resources.registerWorkspaceResourceProvider(ext.workspaceDataProvider);
ext.mongoClustersWorkspaceBranchDataProvider = new MongoClustersWorkspaceBranchDataProvider();
ext.rgApiV2.resources.registerWorkspaceResourceBranchDataProvider(
WorkspaceResourceType.MongoClusters,
ext.mongoClustersWorkspaceBranchDataProvider,
);
ext.mongoClustersWorkspaceBranchDataProvider = new MongoClustersWorkspaceBranchDataProvider();
ext.rgApiV2.resources.registerWorkspaceResourceBranchDataProvider(
WorkspaceResourceType.MongoClusters,
ext.mongoClustersWorkspaceBranchDataProvider,
);
// using registerCommand instead of vscode.commands.registerCommand for better telemetry:
// https://github.com/microsoft/vscode-azuretools/tree/main/utils#telemetry-and-error-handling
registerCommand('mongoClusters.cmd.hello', this.commandSayHello);
// using registerCommand instead of vscode.commands.registerCommand for better telemetry:
// https://github.com/microsoft/vscode-azuretools/tree/main/utils#telemetry-and-error-handling
registerCommand('mongoClusters.internal.containerView.open', openCollectionView);
registerCommand('mongoClusters.internal.documentView.open', openDocumentView);
registerCommand('command.internal.mongoClusters.containerView.open', openCollectionView);
registerCommand('command.internal.mongoClusters.documentView.open', openDocumentView);
registerCommandWithTreeNodeUnwrapping('mongoClusters.cmd.launchShell', launchShell);
registerCommand('command.internal.mongoClusters.importDocuments', mongoClustersImportDocuments);
registerCommand('command.internal.mongoClusters.exportDocuments', mongoClustersExportQueryResults);
registerCommandWithTreeNodeUnwrapping('mongoClusters.cmd.dropCollection', dropCollection);
registerCommandWithTreeNodeUnwrapping('mongoClusters.cmd.dropDatabase', dropDatabase);
registerCommandWithTreeNodeUnwrapping('command.mongoClusters.launchShell', launchShell);
registerCommandWithTreeNodeUnwrapping('mongoClusters.cmd.createCollection', createCollection);
registerCommandWithTreeNodeUnwrapping('mongoClusters.cmd.createDatabase', createDatabase);
registerCommandWithTreeNodeUnwrapping('command.mongoClusters.dropCollection', dropCollection);
registerCommandWithTreeNodeUnwrapping('command.mongoClusters.dropDatabase', dropDatabase);
registerCommandWithTreeNodeUnwrapping('mongoClusters.cmd.importDocuments', mongoClustersImportDocuments);
registerCommandWithTreeNodeUnwrapping(
'mongoClusters.cmd.exportDocuments',
mongoClustersExportEntireCollection,
);
registerCommandWithTreeNodeUnwrapping('command.mongoClusters.createCollection', createCollection);
registerCommandWithTreeNodeUnwrapping('command.mongoClusters.createDatabase', createDatabase);
registerCommand('mongoClusters.internal.importDocuments', mongoClustersImportDocuments);
registerCommand('mongoClusters.internal.exportDocuments', mongoClustersExportQueryResults);
registerCommandWithTreeNodeUnwrapping(
'command.mongoClusters.importDocuments',
mongoClustersImportDocuments,
);
registerCommandWithTreeNodeUnwrapping(
'command.mongoClusters.exportDocuments',
mongoClustersExportEntireCollection,
);
registerCommand('mongoClusters.cmd.addWorkspaceConnection', addWorkspaceConnection);
registerCommandWithTreeNodeUnwrapping(
'mongoClusters.cmd.removeWorkspaceConnection',
removeWorkspaceConnection,
);
registerCommand('command.mongoClusters.addWorkspaceConnection', addWorkspaceConnection);
registerCommandWithTreeNodeUnwrapping(
'command.mongoClusters.removeWorkspaceConnection',
removeWorkspaceConnection,
);
ext.outputChannel.appendLine(`mongoClusters: activated.`);
});
}
// commands
commandSayHello = (): void => {
console.log(`Hello there here!!!`);
void vscode.window.showInformationMessage('Saying hello here!');
void vscode.window.showWarningMessage(
`Are you sure?`,
{ modal: true, detail: "You are about to:\n\ndelete 5 documents.\n\nThis action can't be undone." },
'Delete',
ext.outputChannel.appendLine(`MongoDB Clusters: activated.`);
},
);
};
}
}

Просмотреть файл

@ -3,12 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
AzureWizard,
callWithTelemetryAndErrorHandling,
UserCancelledError,
type IActionContext,
} from '@microsoft/vscode-azext-utils';
import { AzureWizard, UserCancelledError, type IActionContext } from '@microsoft/vscode-azext-utils';
import ConnectionString from 'mongodb-connection-string-url';
import * as vscode from 'vscode';
import { API } from '../../AzureDBExperiences';
@ -29,25 +24,20 @@ export async function addWorkspaceConnection(context: IActionContext): Promise<v
promptSteps: [new ConnectionStringStep(), new UsernameStep(), new PasswordStep()],
});
// Prompt the user for credentials
await callWithTelemetryAndErrorHandling(
'mongoClusters.addWorkspaceConnection.promptForCredentials',
async (context: IActionContext) => {
context.errorHandling.rethrow = true;
context.errorHandling.suppressDisplay = true;
try {
await wizard.prompt();
} catch (error) {
if (error instanceof UserCancelledError) {
// The user cancelled the wizard
wizardContext.aborted = true;
return;
} else {
throw error;
}
}
},
);
context.errorHandling.rethrow = true;
context.errorHandling.suppressDisplay = true;
try {
await wizard.prompt();
} catch (error) {
if (error instanceof UserCancelledError) {
// The user cancelled the wizard
wizardContext.aborted = true;
return;
} else {
throw error;
}
}
if (wizardContext.aborted) {
return;

Просмотреть файл

@ -33,7 +33,7 @@ export class CollectionItem {
contextValue: 'mongoClusters.item.documents',
id: `${this.id}/documents`,
label: 'Documents',
commandId: 'mongoClusters.internal.containerView.open',
commandId: 'command.internal.mongoClusters.containerView.open',
commandArgs: [
{
id: this.id,

Просмотреть файл

@ -34,7 +34,7 @@ export class DatabaseItem {
id: `${this.id}/no-databases`,
label: 'Create collection...',
iconPath: new vscode.ThemeIcon('plus'),
commandId: 'mongoClusters.cmd.createCollection',
commandId: 'command.mongoClusters.createCollection',
commandArgs: [this],
}),
];

Просмотреть файл

@ -38,6 +38,7 @@ export class IndexItem {
getTreeItem(): TreeItem {
return {
id: this.id,
contextValue: 'mongoClusters.item.index',
label: this.indexInfo.name,
iconPath: new ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change
collapsibleState: TreeItemCollapsibleState.Collapsed,

Просмотреть файл

@ -31,6 +31,7 @@ export class IndexesItem {
getTreeItem(): TreeItem {
return {
id: this.id,
contextValue: 'mongoClusters.item.indexes',
label: 'Indexes',
iconPath: new ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change
collapsibleState: TreeItemCollapsibleState.Collapsed,

Просмотреть файл

@ -3,12 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
callWithTelemetryAndErrorHandling,
createGenericElement,
type IActionContext,
type TreeElementBase,
} from '@microsoft/vscode-azext-utils';
import { createGenericElement, type IActionContext, type TreeElementBase } from '@microsoft/vscode-azext-utils';
import { type TreeItem } from 'vscode';
import * as vscode from 'vscode';
@ -38,7 +33,7 @@ export abstract class MongoClusterItemBase implements TreeElementBase {
* @param context The action context.
* @returns An instance of MongoClustersClient if successful; otherwise, null.
*/
protected abstract authenticateAndConnect(context: IActionContext): Promise<MongoClustersClient | null>;
protected abstract authenticateAndConnect(): Promise<MongoClustersClient | null>;
/**
* Authenticates and connects to the cluster to list all available databases.
@ -54,66 +49,52 @@ export abstract class MongoClusterItemBase implements TreeElementBase {
* @returns A list of databases in the cluster or a single element to create a new database.
*/
async getChildren(): Promise<TreeElementBase[]> {
const result = await callWithTelemetryAndErrorHandling(
'mongoClusterItem.getChildren',
async (context: IActionContext) => {
// Error handling setup
context.errorHandling.suppressDisplay = false;
context.errorHandling.rethrow = true;
context.valuesToMask.push(this.id, this.mongoCluster.name);
ext.outputChannel.appendLine(`MongoDB Clusters: Loading cluster details for "${this.mongoCluster.name}"`);
ext.outputChannel.appendLine(
`MongoDB Clusters: Loading cluster details for "${this.mongoCluster.name}"`,
);
let mongoClustersClient: MongoClustersClient | null;
let mongoClustersClient: MongoClustersClient | null;
// Check if credentials are cached, and return the cached client if available
if (CredentialCache.hasCredentials(this.id)) {
ext.outputChannel.appendLine(
`MongoDB Clusters: Reusing active connection for "${this.mongoCluster.name}".`,
);
mongoClustersClient = await MongoClustersClient.getClient(this.id);
} else {
// Call to the abstract method to authenticate and connect to the cluster
mongoClustersClient = await this.authenticateAndConnect();
}
// Check if credentials are cached, and return the cached client if available
if (CredentialCache.hasCredentials(this.id)) {
ext.outputChannel.appendLine(
`MongoDB Clusters: Reusing active connection for "${this.mongoCluster.name}".`,
);
mongoClustersClient = await MongoClustersClient.getClient(this.id);
} else {
// Call to the abstract method to authenticate and connect to the cluster
mongoClustersClient = await this.authenticateAndConnect(context);
}
// If authentication failed, return the error element
if (!mongoClustersClient) {
return [
createGenericElement({
contextValue: 'error',
id: `${this.id}/error`,
label: 'Failed to authenticate (click to retry)',
iconPath: new vscode.ThemeIcon('error'),
commandId: 'azureResourceGroups.refreshTree',
}),
];
}
// If authentication failed, return the error element
if (!mongoClustersClient) {
return [
createGenericElement({
contextValue: 'error',
id: `${this.id}/error`,
label: 'Failed to authenticate (click to retry)',
iconPath: new vscode.ThemeIcon('error'),
commandId: 'azureResourceGroups.refreshTree',
}),
];
}
// List the databases
return mongoClustersClient.listDatabases().then((databases: DatabaseItemModel[]) => {
if (databases.length === 0) {
return [
createGenericElement({
contextValue: 'mongoClusters.item.no-databases',
id: `${this.id}/no-databases`,
label: 'Create database...',
iconPath: new vscode.ThemeIcon('plus'),
commandId: 'command.mongoClusters.createDatabase',
commandArgs: [this],
}),
];
}
// List the databases
return mongoClustersClient.listDatabases().then((databases: DatabaseItemModel[]) => {
if (databases.length === 0) {
return [
createGenericElement({
contextValue: 'mongoClusters.item.no-databases',
id: `${this.id}/no-databases`,
label: 'Create database...',
iconPath: new vscode.ThemeIcon('plus'),
commandId: 'mongoClusters.cmd.createDatabase',
commandArgs: [this],
}),
];
}
// Map the databases to DatabaseItem elements
return databases.map((database) => new DatabaseItem(this.mongoCluster, database));
});
},
);
return result ?? [];
// Map the databases to DatabaseItem elements
return databases.map((database) => new DatabaseItem(this.mongoCluster, database));
});
}
/**

Просмотреть файл

@ -40,80 +40,89 @@ export class MongoClusterResourceItem extends MongoClusterItemBase {
* @param context The action context.
* @returns An instance of MongoClustersClient if successful; otherwise, null.
*/
protected async authenticateAndConnect(context: IActionContext): Promise<MongoClustersClient | null> {
ext.outputChannel.appendLine(
`MongoDB Clusters: Attempting to authenticate with "${this.mongoCluster.name}"...`,
protected async authenticateAndConnect(): Promise<MongoClustersClient | null> {
const result = await callWithTelemetryAndErrorHandling(
'cosmosDB.mongoClusters.authenticate',
async (context: IActionContext) => {
ext.outputChannel.appendLine(
`MongoDB Clusters: Attempting to authenticate with "${this.mongoCluster.name}"...`,
);
// Create a client to interact with the MongoDB vCore management API and read the cluster details
const managementClient = await createMongoClustersManagementClient(context, this.subscription);
const clusterInformation = await managementClient.mongoClusters.get(
this.mongoCluster.resourceGroup as string,
this.mongoCluster.name,
);
const clusterConnectionString = nonNullValue(clusterInformation.connectionString);
context.valuesToMask.push(clusterConnectionString);
// Fetch non-admin users using the extracted method
const clusterNonAdminUsers = await this.fetchNonAdminUsersFromAzure(
managementClient,
clusterInformation,
);
const wizardContext: AuthenticateWizardContext = {
...context,
adminUserName: clusterInformation.administratorLogin,
otherUserNames: clusterNonAdminUsers,
resourceName: this.mongoCluster.name,
};
// Prompt the user for credentials
const credentialsProvided = await this.promptForCredentials(wizardContext);
// If the wizard was aborted or failed, return null
if (!credentialsProvided) {
return null;
}
context.valuesToMask.push(nonNullProp(wizardContext, 'password'));
// Cache the credentials
CredentialCache.setCredentials(
this.id,
nonNullValue(clusterConnectionString),
nonNullProp(wizardContext, 'selectedUserName'),
nonNullProp(wizardContext, 'password'),
);
ext.outputChannel.append(
`MongoDB Clusters: Connecting to the cluster as "${wizardContext.selectedUserName}"... `,
);
// Attempt to create the client with the provided credentials
let mongoClustersClient: MongoClustersClient;
try {
mongoClustersClient = await MongoClustersClient.getClient(this.id).catch((error: Error) => {
ext.outputChannel.appendLine(`Error: ${error.message}`);
void vscode.window.showErrorMessage(`Failed to connect: ${error.message}`);
throw error;
});
} catch (error) {
console.log(error);
// If connection fails, remove cached credentials
await MongoClustersClient.deleteClient(this.id);
CredentialCache.deleteCredentials(this.id);
// Return null to indicate failure
return null;
}
ext.outputChannel.appendLine(
`MongoDB Clusters: Connected to "${this.mongoCluster.name}" as "${wizardContext.selectedUserName}".`,
);
return mongoClustersClient;
},
);
// Create a client to interact with the MongoDB vCore management API and read the cluster details
const managementClient = await createMongoClustersManagementClient(context, this.subscription);
const clusterInformation = await managementClient.mongoClusters.get(
this.mongoCluster.resourceGroup as string,
this.mongoCluster.name,
);
const clusterConnectionString = nonNullValue(clusterInformation.connectionString);
context.valuesToMask.push(clusterConnectionString);
// Fetch non-admin users using the extracted method
const clusterNonAdminUsers = await this.fetchNonAdminUsersFromAzure(managementClient, clusterInformation);
const wizardContext: AuthenticateWizardContext = {
...context,
adminUserName: clusterInformation.administratorLogin,
otherUserNames: clusterNonAdminUsers,
resourceName: this.mongoCluster.name,
};
// Prompt the user for credentials
const credentialsProvided = await this.promptForCredentials(wizardContext);
// If the wizard was aborted or failed, return null
if (!credentialsProvided) {
return null;
}
context.valuesToMask.push(nonNullProp(wizardContext, 'password'));
// Cache the credentials
CredentialCache.setCredentials(
this.id,
nonNullValue(clusterConnectionString),
nonNullProp(wizardContext, 'selectedUserName'),
nonNullProp(wizardContext, 'password'),
);
ext.outputChannel.append(
`MongoDB Clusters: Connecting to the cluster as "${wizardContext.selectedUserName}"... `,
);
// Attempt to create the client with the provided credentials
let mongoClustersClient: MongoClustersClient;
try {
mongoClustersClient = await MongoClustersClient.getClient(this.id).catch((error: Error) => {
ext.outputChannel.appendLine('failed.');
ext.outputChannel.appendLine(`Error: ${error.message}`);
void vscode.window.showErrorMessage(`Failed to connect: ${error.message}`);
throw error;
});
} catch (error) {
console.log(error);
// If connection fails, remove cached credentials
await MongoClustersClient.deleteClient(this.id);
CredentialCache.deleteCredentials(this.id);
// Return null to indicate failure
return null;
}
ext.outputChannel.appendLine(
`MongoDB Clusters: Connected to "${this.mongoCluster.name}" as "${wizardContext.selectedUserName}".`,
);
return mongoClustersClient;
return result ?? null;
}
/**
@ -130,20 +139,14 @@ export class MongoClusterResourceItem extends MongoClusterItemBase {
});
// Prompt the user for credentials
await callWithTelemetryAndErrorHandling(
'mongoClusterItem.authenticate.promptForCredentials',
async (_context: IActionContext) => {
_context.errorHandling.rethrow = true;
_context.errorHandling.suppressDisplay = false;
try {
await wizard.prompt(); // This will prompt the user; results are stored in wizardContext
} catch (error) {
if (error instanceof UserCancelledError) {
wizardContext.aborted = true;
}
}
},
);
try {
await wizard.prompt(); // This will prompt the user; results are stored in wizardContext
} catch (error) {
if (error instanceof UserCancelledError) {
wizardContext.aborted = true;
}
}
// Return true if the wizard completed successfully; false otherwise
return !wizardContext.aborted;

Просмотреть файл

@ -13,6 +13,7 @@ import {
type ResourceModelBase,
} from '@microsoft/vscode-azureresources-api';
import * as vscode from 'vscode';
import { API } from '../../AzureDBExperiences';
import { ext } from '../../extensionVariables';
import { createMongoClustersManagementClient } from '../../utils/azureClients';
import { type MongoClusterModel } from './MongoClusterModel';
@ -49,13 +50,18 @@ export class MongoClustersBranchDataProvider
/**
* getChildren is called for every element in the tree when expanding, the element being expanded is being passed as an argument
*/
return (await element.getChildren?.())?.map((child) => {
if (child.id) {
return ext.state.wrapItemInStateHandling(child as TreeElementBase & { id: string }, () =>
this.refresh(child),
);
}
return child;
return await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => {
context.telemetry.properties.experience = API.MongoClusters;
context.telemetry.properties.parentContext = (await element.getTreeItem()).contextValue ?? 'unknown';
return (await element.getChildren?.())?.map((child) => {
if (child.id) {
return ext.state.wrapItemInStateHandling(child as TreeElementBase & { id: string }, () =>
this.refresh(child),
);
}
return child;
});
});
}
@ -65,12 +71,14 @@ export class MongoClustersBranchDataProvider
*/
const resourceItem = await callWithTelemetryAndErrorHandling(
'mongoCluster.getResourceItem',
'resolveResource',
// disabling require-await, the async aspect is in there, but uses the .then pattern
// eslint-disable-next-line @typescript-eslint/require-await
async (_context: IActionContext) => {
async (context: IActionContext) => {
context.telemetry.properties.experience = API.MongoClusters;
if (this.detailsCacheUpdateRequested) {
void this.updateResourceCache(_context, element.subscription, 1000 * 60 * 5).then(() => {
void this.updateResourceCache(context, element.subscription, 1000 * 60 * 5).then(() => {
/**
* Instances of MongoClusterItem were stored in the itemsToUpdateInfo map,
* so that when the cache is updated, the items can be refreshed.
@ -118,9 +126,11 @@ export class MongoClustersBranchDataProvider
cacheDuration: number,
): Promise<void> {
return callWithTelemetryAndErrorHandling(
'mongoClusters.getResourceItem.cacheUpdate',
'resolveResource.updatingResourceCache',
async (context: IActionContext) => {
try {
context.telemetry.properties.experience = API.MongoClusters;
this.detailsCacheUpdateRequested = false;
setTimeout(() => {

Просмотреть файл

@ -35,70 +35,82 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase {
* @param context The action context.
* @returns An instance of MongoClustersClient if successful; otherwise, null.
*/
protected async authenticateAndConnect(context: IActionContext): Promise<MongoClustersClient | null> {
ext.outputChannel.appendLine(`MongoDB Clusters: Attempting to authenticate with ${this.mongoCluster.name}`);
protected async authenticateAndConnect(): Promise<MongoClustersClient | null> {
const result = await callWithTelemetryAndErrorHandling(
'cosmosDB.mongoClusters.authenticate',
async (context: IActionContext) => {
context.telemetry.properties.view = 'workspace';
let mongoClustersClient: MongoClustersClient;
ext.outputChannel.appendLine(
`MongoDB Clusters: Attempting to authenticate with ${this.mongoCluster.name}`,
);
const connectionString = new ConnectionString(nonNullValue(this.mongoCluster.connectionString));
let mongoClustersClient: MongoClustersClient;
let username: string | undefined = connectionString.username;
let password: string | undefined = connectionString.password;
const connectionString = new ConnectionString(nonNullValue(this.mongoCluster.connectionString));
if (!username || username.length === 0 || !password || password.length === 0) {
const wizardContext: AuthenticateWizardContext = {
...context,
adminUserName: undefined,
otherUserNames: [],
resourceName: this.mongoCluster.name,
let username: string | undefined = connectionString.username;
let password: string | undefined = connectionString.password;
// preconfigure the username in case it's provided connection string
selectedUserName: username,
// we'll always ask for the password
};
if (!username || username.length === 0 || !password || password.length === 0) {
const wizardContext: AuthenticateWizardContext = {
...context,
adminUserName: undefined,
otherUserNames: [],
resourceName: this.mongoCluster.name,
// Prompt the user for credentials using the extracted method
const credentialsProvided = await this.promptForCredentials(wizardContext);
// preconfigure the username in case it's provided connection string
selectedUserName: username,
// we'll always ask for the password
};
// If the wizard was aborted or failed, return null
if (!credentialsProvided) {
return null;
}
// Prompt the user for credentials using the extracted method
const credentialsProvided = await this.promptForCredentials(wizardContext);
context.valuesToMask.push(nonNullProp(wizardContext, 'password'));
// If the wizard was aborted or failed, return null
if (!credentialsProvided) {
return null;
}
username = nonNullProp(wizardContext, 'selectedUserName');
password = nonNullProp(wizardContext, 'password');
}
context.valuesToMask.push(nonNullProp(wizardContext, 'password'));
ext.outputChannel.append(`MongoDB Clusters: Connecting to the cluster as "${username}"... `);
username = nonNullProp(wizardContext, 'selectedUserName');
password = nonNullProp(wizardContext, 'password');
}
// Cache the credentials
CredentialCache.setCredentials(this.id, connectionString.toString(), username, password);
ext.outputChannel.append(`MongoDB Clusters: Connecting to the cluster as "${username}"... `);
// Attempt to create the client with the provided credentials
try {
mongoClustersClient = await MongoClustersClient.getClient(this.id).catch((error: Error) => {
ext.outputChannel.appendLine('failed.');
ext.outputChannel.appendLine(`Error: ${error.message}`);
// Cache the credentials
CredentialCache.setCredentials(this.id, connectionString.toString(), username, password);
void vscode.window.showErrorMessage(`Failed to connect: ${error.message}`);
// Attempt to create the client with the provided credentials
try {
mongoClustersClient = await MongoClustersClient.getClient(this.id).catch((error: Error) => {
ext.outputChannel.appendLine('failed.');
ext.outputChannel.appendLine(`Error: ${error.message}`);
throw error;
});
} catch (error) {
console.log(error);
// If connection fails, remove cached credentials
await MongoClustersClient.deleteClient(this.id);
CredentialCache.deleteCredentials(this.id);
void vscode.window.showErrorMessage(`Failed to connect: ${error.message}`);
// Return null to indicate failure
return null;
}
throw error;
});
} catch (error) {
console.log(error);
// If connection fails, remove cached credentials
await MongoClustersClient.deleteClient(this.id);
CredentialCache.deleteCredentials(this.id);
ext.outputChannel.appendLine(`MongoDB Clusters: Connected to "${this.mongoCluster.name}" as "${username}"`);
// Return null to indicate failure
return null;
}
return mongoClustersClient;
ext.outputChannel.appendLine(
`MongoDB Clusters: Connected to "${this.mongoCluster.name}" as "${username}"`,
);
return mongoClustersClient;
},
);
return result ?? null;
}
/**
@ -115,10 +127,12 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase {
// Prompt the user for credentials
await callWithTelemetryAndErrorHandling(
'mongoClusterItem.authenticate.promptForCredentials',
async (_context: IActionContext) => {
_context.errorHandling.rethrow = true;
_context.errorHandling.suppressDisplay = false;
'cosmosDB.mongoClusters.authenticate.promptForCredentials',
async (context: IActionContext) => {
context.telemetry.properties.view = 'workspace';
context.errorHandling.rethrow = true;
context.errorHandling.suppressDisplay = false;
try {
await wizard.prompt(); // This will prompt the user; results are stored in wizardContext
} catch (error) {

Просмотреть файл

@ -3,10 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { type TreeElementBase } from '@microsoft/vscode-azext-utils';
import {
callWithTelemetryAndErrorHandling,
type IActionContext,
type TreeElementBase,
} from '@microsoft/vscode-azext-utils';
import { type WorkspaceResourceBranchDataProvider } from '@microsoft/vscode-azureresources-api';
import * as vscode from 'vscode';
import { type TreeItem } from 'vscode';
import { API } from '../../../AzureDBExperiences';
import { ext } from '../../../extensionVariables';
import { MongoDBAccountsWorkspaceItem } from './MongoDBAccountsWorkspaceItem';
@ -27,13 +32,19 @@ export class MongoClustersWorkspaceBranchDataProvider
}
async getChildren(element: TreeElementBase): Promise<TreeElementBase[] | null | undefined> {
return (await element.getChildren?.())?.map((child) => {
if (child.id) {
return ext.state.wrapItemInStateHandling(child as TreeElementBase & { id: string }, () =>
this.refresh(child),
);
}
return child;
return await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => {
context.telemetry.properties.experience = API.MongoClusters;
context.telemetry.properties.view = 'workspace';
context.telemetry.properties.parentContext = (await element.getTreeItem()).contextValue ?? 'unknown';
return (await element.getChildren?.())?.map((child) => {
if (child.id) {
return ext.state.wrapItemInStateHandling(child as TreeElementBase & { id: string }, () =>
this.refresh(child),
);
}
return child;
});
});
}

Просмотреть файл

@ -14,7 +14,7 @@ export class MongoDBAccountsWorkspaceItem implements TreeElementBase {
id: string;
constructor() {
this.id = `vscode.cosmosdb.workspace.mongoclusters.mongodbaccounts`;
this.id = `vscode.cosmosdb.workspace.mongoclusters.accounts`;
}
async getChildren(): Promise<TreeElementBase[]> {
@ -34,7 +34,7 @@ export class MongoDBAccountsWorkspaceItem implements TreeElementBase {
id: this.id + '/newConnection',
label: 'New Connection...',
iconPath: new ThemeIcon('plus'),
commandId: 'mongoClusters.cmd.addWorkspaceConnection',
commandId: 'command.mongoClusters.addWorkspaceConnection',
}),
];
}
@ -42,7 +42,7 @@ export class MongoDBAccountsWorkspaceItem implements TreeElementBase {
getTreeItem(): TreeItem {
return {
id: this.id,
contextValue: 'vscode.cosmosdb.workspace.mongoclusters.mongodbaccounts',
contextValue: 'vscode.cosmosdb.workspace.mongoclusters.accounts',
label: 'MongoDB Cluster Accounts',
iconPath: new ThemeIcon('link'),
collapsibleState: TreeItemCollapsibleState.Collapsed,

Просмотреть файл

@ -35,8 +35,7 @@ export class ConnectionStringStep extends AzureWizardPromptStep<AddWorkspaceConn
// eslint-disable-next-line @typescript-eslint/require-await
private async validateConnectionString(connectionString: string): Promise<string | null | undefined> {
try {
const parsedCS = new ConnectionString(connectionString);
console.log(parsedCS);
new ConnectionString(connectionString);
} catch (error) {
if (error instanceof Error && error.name === 'MongoParseError') {
return error.message;

Просмотреть файл

@ -113,7 +113,7 @@ export const collectionsViewRouter = router({
.mutation(({ ctx }) => {
const myCtx = ctx as RouterContext;
vscode.commands.executeCommand('mongoClusters.internal.documentView.open', {
vscode.commands.executeCommand('command.internal.mongoClusters.documentView.open', {
sessionId: myCtx.sessionId,
databaseName: myCtx.databaseName,
collectionName: myCtx.collectionName,
@ -127,7 +127,7 @@ export const collectionsViewRouter = router({
.mutation(({ input, ctx }) => {
const myCtx = ctx as RouterContext;
vscode.commands.executeCommand('mongoClusters.internal.documentView.open', {
vscode.commands.executeCommand('command.internal.mongoClusters.documentView.open', {
sessionId: myCtx.sessionId,
databaseName: myCtx.databaseName,
collectionName: myCtx.collectionName,
@ -142,7 +142,7 @@ export const collectionsViewRouter = router({
.mutation(({ input, ctx }) => {
const myCtx = ctx as RouterContext;
vscode.commands.executeCommand('mongoClusters.internal.documentView.open', {
vscode.commands.executeCommand('command.internal.mongoClusters.documentView.open', {
sessionId: myCtx.sessionId,
databaseName: myCtx.databaseName,
collectionName: myCtx.collectionName,
@ -187,7 +187,7 @@ export const collectionsViewRouter = router({
const myCtx = ctx as RouterContext;
vscode.commands.executeCommand(
'mongoClusters.internal.exportDocuments',
'command.internal.mongoClusters.exportDocuments',
myCtx.collectionTreeItem,
input.query,
);
@ -195,6 +195,6 @@ export const collectionsViewRouter = router({
importDocuments: publicProcedure.query(async ({ ctx }) => {
const myCtx = ctx as RouterContext;
vscode.commands.executeCommand('mongoClusters.internal.importDocuments', myCtx.collectionTreeItem);
vscode.commands.executeCommand('command.internal.mongoClusters.importDocuments', myCtx.collectionTreeItem);
}),
});