Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unified and simplified vCore telemetry #2476

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/AzureDBExperiences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum API {
Core = 'Core', // Now called NoSQL
PostgresSingle = 'PostgresSingle',
PostgresFlexible = 'PostgresFlexible',
Common = 'Common', // In case we're reporting a common event and still need to provide the value of the API
}

export enum DBAccountKind {
Expand Down
26 changes: 20 additions & 6 deletions src/commands/importDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
*--------------------------------------------------------------------------------------------*/

import { type ItemDefinition } from '@azure/cosmos';
import { parseError, type IActionContext } from '@microsoft/vscode-azext-utils';
import { callWithTelemetryAndErrorHandling, parseError, type IActionContext } from '@microsoft/vscode-azext-utils';
import { EJSON } from 'bson';
import * as fse from 'fs-extra';
import { type InsertManyResult } from 'mongodb';
import * as vscode from 'vscode';
import { cosmosMongoFilter, sqlFilter } from '../constants';
import { DocDBCollectionTreeItem } from '../docdb/tree/DocDBCollectionTreeItem';
import { ext } from '../extensionVariables';
import { MongoCollectionTreeItem } from '../mongo/tree/MongoCollectionTreeItem';
import { type InsertDocumentsResult } from '../mongoClusters/MongoClustersClient';
import { CollectionItem } from '../mongoClusters/tree/CollectionItem';
import { nonNullProp, nonNullValue } from '../utils/nonNull';
import { getRootPath } from '../utils/workspacUtils';
Expand Down Expand Up @@ -191,9 +193,16 @@ async function insertDocumentsIntoDocdb(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function insertDocumentsIntoMongo(node: MongoCollectionTreeItem, documents: any[]): Promise<string> {
let output = '';
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const parsed = await node.collection.insertMany(documents);
if (parsed.acknowledged) {

let parsed: InsertManyResult<Document> | undefined;
await callWithTelemetryAndErrorHandling('cosmosDB.mongo.importDocumets', async (actionContext) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
parsed = await node.collection.insertMany(documents);

actionContext.telemetry.measurements.documentCount = parsed?.insertedCount;
});

if (parsed?.acknowledged) {
output = `Import into mongo successful. Inserted ${parsed.insertedCount} document(s). See output for more details.`;
for (const inserted of Object.values(parsed.insertedIds)) {
ext.outputChannel.appendLog(`Inserted document: ${inserted}`);
Expand All @@ -207,10 +216,15 @@ async function insertDocumentsIntoMongoCluster(
node: CollectionItem,
documents: unknown[],
): Promise<string> {
const result = await node.insertDocuments(context, documents as Document[]);
let result: InsertDocumentsResult | undefined;
await callWithTelemetryAndErrorHandling('cosmosDB.mongoClusters.importDocumets', async (actionContext) => {
result = await node.insertDocuments(context, documents as Document[]);

actionContext.telemetry.measurements.documentCount = result?.insertedCount;
});

let message: string;
if (result.acknowledged) {
if (result?.acknowledged) {
message = `Import successful. Inserted ${result.insertedCount} document(s).`;
} else {
message = `Import failed. The operation was not acknowledged by the database.`;
Expand Down
2 changes: 1 addition & 1 deletion src/docdb/tree/DocDBAccountTreeItemBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export abstract class DocDBAccountTreeItemBase extends DocDBTreeItemBase<Databas
const result = await callWithTelemetryAndErrorHandling(
'getChildren',
async (context: IActionContext): Promise<AzExtTreeItem[]> => {
context.telemetry.properties.parentContext = this.contextValue;
context.telemetry.properties.parentNodeContext = 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')) {
Expand Down
2 changes: 1 addition & 1 deletion src/mongo/connectToMongoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function connectToMongoClient(connectionString: string, appName: st
// appname appears to be the correct equivalent to user-agent for mongo
const options: MongoClientOptions = <MongoClientOptions>{
// appName should be wrapped in '@'s when trying to connect to a Mongo account, this doesn't effect the appendUserAgent string
appName: `@${appName}@`,
appName: `${appName}[RU]`,
// https://github.com/lmammino/mongo-uri-builder/issues/2
useNewUrlParser: true,
useUnifiedTopology: true,
Expand Down
2 changes: 1 addition & 1 deletion src/mongo/tree/MongoAccountTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class MongoAccountTreeItem extends AzExtParentTreeItem {
'getChildren',
async (context: IActionContext): Promise<AzExtTreeItem[]> => {
context.telemetry.properties.experience = API.MongoDB;
context.telemetry.properties.parentContext = this.contextValue;
context.telemetry.properties.parentNodeContext = this.contextValue;

let mongoClient: MongoClient | undefined;
try {
Expand Down
27 changes: 26 additions & 1 deletion src/mongoClusters/MongoClustersClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* singletone on a client with a getter from a connection pool..
*/

import { appendExtensionUserAgent, callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
import { EJSON } from 'bson';
import {
MongoClient,
Expand All @@ -22,6 +23,8 @@ import {
type WithoutId,
} from 'mongodb';
import { CredentialCache } from './CredentialCache';
import { areMongoDBAzure, getHostsFromConnectionString } from './utils/connectionStringHelpers';
import { getMongoClusterMetadata, type MongoClusterMetadata } from './utils/getMongoClusterMetadata';
import { toFilterQueryObj } from './utils/toFilterQuery';

export interface DatabaseItemModel {
Expand Down Expand Up @@ -73,9 +76,31 @@ export class MongoClustersClient {
}

this._credentialId = credentialId;

// check if it's an azure connection, and do some special handling
let userAgentString: string | undefined = undefined;
{
const cString = CredentialCache.getCredentials(credentialId)?.connectionString as string;
const hosts = getHostsFromConnectionString(cString);
if (areMongoDBAzure(hosts)) {
userAgentString = appendExtensionUserAgent();
}
}

const cStringPassword = CredentialCache.getConnectionStringWithPassword(credentialId);

this._mongoClient = await MongoClient.connect(cStringPassword as string);
this._mongoClient = await MongoClient.connect(cStringPassword as string, {
appName: userAgentString,
});

void callWithTelemetryAndErrorHandling('cosmosDB.mongoClusters.connect.getmetadata', async (context) => {
const metadata: MongoClusterMetadata = await getMongoClusterMetadata(this._mongoClient);

context.telemetry.properties = {
...context.telemetry.properties,
...metadata,
};
});
}

public static async getClient(credentialId: string): Promise<MongoClustersClient> {
Expand Down
15 changes: 12 additions & 3 deletions src/mongoClusters/MongoClustersExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ export class MongoClustersExtension implements vscode.Disposable {
registerCommand('command.internal.mongoClusters.containerView.open', openCollectionView);
registerCommand('command.internal.mongoClusters.documentView.open', openDocumentView);

registerCommand('command.internal.mongoClusters.importDocuments', mongoClustersImportDocuments);
registerCommand('command.internal.mongoClusters.exportDocuments', mongoClustersExportQueryResults);

registerCommandWithTreeNodeUnwrapping('command.mongoClusters.launchShell', launchShell);

registerCommandWithTreeNodeUnwrapping('command.mongoClusters.dropCollection', dropCollection);
Expand All @@ -101,6 +98,18 @@ export class MongoClustersExtension implements vscode.Disposable {
'command.mongoClusters.importDocuments',
mongoClustersImportDocuments,
);

/**
* Here, exporting documents is done in two ways: one is accessible from the tree view
* via a context menu, and the other is accessible programmatically. Both of them
* use the same underlying function to export documents.
*
* mongoClustersExportEntireCollection calls mongoClustersExportQueryResults with no queryText.
*
* It was possible to merge the two commands into one, but it would result in code that is
* harder to understand and maintain.
*/
registerCommand('command.internal.mongoClusters.exportDocuments', mongoClustersExportQueryResults);
registerCommandWithTreeNodeUnwrapping(
'command.mongoClusters.exportDocuments',
mongoClustersExportEntireCollection,
Expand Down
21 changes: 3 additions & 18 deletions src/mongoClusters/commands/addWorkspaceConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { WorkspaceResourceType } from '../../tree/workspace/sharedWorkspaceResou
import { SharedWorkspaceStorage } from '../../tree/workspace/sharedWorkspaceStorage';
import { showConfirmationAsInSettings } from '../../utils/dialogs/showConfirmation';
import { localize } from '../../utils/localize';
import { areMongoDBRU } from '../utils/connectionStringHelpers';
import { type AddWorkspaceConnectionContext } from '../wizards/addWorkspaceConnection/AddWorkspaceConnectionContext';
import { ConnectionStringStep } from '../wizards/addWorkspaceConnection/ConnectionStringStep';
import { PasswordStep } from '../wizards/addWorkspaceConnection/PasswordStep';
Expand Down Expand Up @@ -55,12 +56,8 @@ export async function addWorkspaceConnection(context: IActionContext): Promise<v
wizardContext.valuesToMask.push(connectionStringWithCredentials);

// discover whether it's a MongoDB RU connection string and abort here.
let isRU: boolean = false;
connectionString.hosts.forEach((host) => {
if (isMongoDBRU(host)) {
isRU = true;
}
});
const isRU = areMongoDBRU(connectionString.hosts);

if (isRU) {
try {
await vscode.window.showInformationMessage(
Expand Down Expand Up @@ -104,15 +101,3 @@ export async function addWorkspaceConnection(context: IActionContext): Promise<v
localize('showConfirmation.addedWorkspaceConnecdtion', 'New connection has been added to your workspace.'),
);
}

function isMongoDBRU(host: string): boolean {
const knownSuffixes = ['mongo.cosmos.azure.com'];
const hostWithoutPort = host.split(':')[0];

for (const suffix of knownSuffixes) {
if (hostWithoutPort.toLowerCase().endsWith(suffix)) {
return true;
}
}
return false;
}
38 changes: 23 additions & 15 deletions src/mongoClusters/commands/exportDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type IActionContext } from '@microsoft/vscode-azext-utils';
import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils';
import { EJSON } from 'bson';
import * as vscode from 'vscode';
import { ext } from '../../extensionVariables';
Expand All @@ -12,21 +12,23 @@ import { getRootPath } from '../../utils/workspacUtils';
import { MongoClustersClient } from '../MongoClustersClient';
import { type CollectionItem } from '../tree/CollectionItem';

export async function mongoClustersExportEntireCollection(_context: IActionContext, node?: CollectionItem) {
return mongoClustersExportQueryResults(_context, node);
export async function mongoClustersExportEntireCollection(context: IActionContext, node?: CollectionItem) {
return mongoClustersExportQueryResults(context, node);
}

export async function mongoClustersExportQueryResults(
_context: IActionContext,
context: IActionContext,
node?: CollectionItem,
queryText?: string,
props?: { queryText?: string; source?: string },
): Promise<void> {
// node ??= ... pick a node if not provided
if (!node) {
throw new Error('No collection selected.');
}

const targetUri = await askForTargetFile(_context);
context.telemetry.properties.calledFrom = props?.source ?? 'contextMenu';

const targetUri = await askForTargetFile(context);

if (!targetUri) {
return;
Expand All @@ -39,7 +41,7 @@ export async function mongoClustersExportQueryResults(
node.databaseInfo.name,
node.collectionInfo.name,
docStreamAbortController.signal,
queryText,
props?.queryText,
);

const filePath = targetUri.fsPath; // Convert `vscode.Uri` to a regular file path
Expand All @@ -48,14 +50,20 @@ export async function mongoClustersExportQueryResults(
let documentCount = 0;

// Wrap the export process inside a progress reporting function
await runExportWithProgressAndDescription(node.id, async (progress, cancellationToken) => {
documentCount = await exportDocumentsToFile(
docStream,
filePath,
progress,
cancellationToken,
docStreamAbortController,
);
await callWithTelemetryAndErrorHandling('cosmosDB.mongoClusters.exportDocuments', async (actionContext) => {
await runExportWithProgressAndDescription(node.id, async (progress, cancellationToken) => {
documentCount = await exportDocumentsToFile(
docStream,
filePath,
progress,
cancellationToken,
docStreamAbortController,
);
});

actionContext.telemetry.properties.source = props?.source;
actionContext.telemetry.measurements.queryLength = props?.queryText?.length;
actionContext.telemetry.measurements.documentCount = documentCount;
});

ext.outputChannel.appendLog(`MongoDB Clusters: Exported document count: ${documentCount}`);
Expand Down
5 changes: 5 additions & 0 deletions src/mongoClusters/commands/importDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { type CollectionItem } from '../tree/CollectionItem';
export async function mongoClustersImportDocuments(
context: IActionContext,
collectionNode?: CollectionItem,
_collectionNodes?: CollectionItem[], // required by the TreeNodeCommandCallback, but not used
...args: unknown[]
): Promise<void> {
const source = (args[0] as { source?: string })?.source ?? 'contextMenu';
context.telemetry.properties.calledFrom = source;

return importDocuments(context, undefined, collectionNode);
}
2 changes: 1 addition & 1 deletion src/mongoClusters/tree/MongoClusterResourceItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class MongoClusterResourceItem extends MongoClusterItemBase {
*/
protected async authenticateAndConnect(): Promise<MongoClustersClient | null> {
const result = await callWithTelemetryAndErrorHandling(
'cosmosDB.mongoClusters.authenticate',
'cosmosDB.mongoClusters.connect',
async (context: IActionContext) => {
ext.outputChannel.appendLine(
`MongoDB Clusters: Attempting to authenticate with "${this.mongoCluster.name}"...`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class MongoClustersBranchDataProvider
*/
return await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => {
context.telemetry.properties.experience = API.MongoClusters;
context.telemetry.properties.parentContext = (await element.getTreeItem()).contextValue ?? 'unknown';
context.telemetry.properties.parentNodeContext = (await element.getTreeItem()).contextValue ?? 'unknown';

return (await element.getChildren?.())?.map((child) => {
if (child.id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase {
*/
protected async authenticateAndConnect(): Promise<MongoClustersClient | null> {
const result = await callWithTelemetryAndErrorHandling(
'cosmosDB.mongoClusters.authenticate',
'cosmosDB.mongoClusters.connect',
async (context: IActionContext) => {
context.telemetry.properties.view = 'workspace';

Expand Down Expand Up @@ -127,7 +127,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase {

// Prompt the user for credentials
await callWithTelemetryAndErrorHandling(
'cosmosDB.mongoClusters.authenticate.promptForCredentials',
'cosmosDB.mongoClusters.connect.promptForCredentials',
async (context: IActionContext) => {
context.telemetry.properties.view = 'workspace';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class MongoClustersWorkspaceBranchDataProvider
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';
context.telemetry.properties.parentNodeContext = (await element.getTreeItem()).contextValue ?? 'unknown';

return (await element.getChildren?.())?.map((child) => {
if (child.id) {
Expand Down
33 changes: 33 additions & 0 deletions src/mongoClusters/utils/connectionStringHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,41 @@ export const getPasswordFromConnectionString = (connectionString: string): strin
return new ConnectionString(connectionString).password;
};

export const getHostsFromConnectionString = (connectionString: string): string[] => {
return new ConnectionString(connectionString).hosts;
};

export const addDatabasePathToConnectionString = (connectionString: string, databaseName: string): string => {
const connectionStringOb = new ConnectionString(connectionString);
connectionStringOb.pathname = databaseName;
return connectionStringOb.toString();
};

/**
* Checks if any of the given hosts end with any of the provided suffixes.
*
* @param hosts - An array of host strings to check.
* @param suffixes - An array of suffixes to check against the hosts.
* @returns True if any host ends with any of the suffixes, false otherwise.
*/
function hostsEndWithAny(hosts: string[], suffixes: string[]): boolean {
return hosts.some((host) => {
const hostWithoutPort = host.split(':')[0].toLowerCase();
return suffixes.some((suffix) => hostWithoutPort.endsWith(suffix));
});
}

export function areMongoDBRU(hosts: string[]): boolean {
const knownSuffixes = ['mongo.cosmos.azure.com'];
return hostsEndWithAny(hosts, knownSuffixes);
}

export function areMongoDBvCore(hosts: string[]): boolean {
const knownSuffixes = ['mongocluster.cosmos.azure.com'];
return hostsEndWithAny(hosts, knownSuffixes);
}

export function areMongoDBAzure(hosts: string[]): boolean {
const knownSuffixes = ['azure.com'];
return hostsEndWithAny(hosts, knownSuffixes);
}
Loading