Skip to content

Commit db20b2d

Browse files
authored
Unified and simplified vCore telemetry (#2476)
### PR Merge Message Summary - **Telemetry Updates**: - Renamed telemetry properties: `parentContext` -> `parentNodeContext`. - Added `calledFrom` to commands shared between webview and extension. - Added telemetry calls for tRPC webview-extension communication (path only, no data). - Introduced telemetry events, errors, and RPC call logging. - Added support for paging, view changes, query, and refresh events. - Extended telemetry to include `documentView` webview and MongoCluster server information. - Added telemetry for import and export actions. - **Code Improvements**: - Removed obsolete code and unused files. - Removed outdated duration measurement (replaced by `callWithTelemetryAndErrorHandling`). - Improved logging: replaced `console.log` with `console.error`. - **VCore Enhancements**: - Exposed `appName` to `azure.com` servers. - **PR Review Feedback**: - Incorporated improvements based on review comments. This update focuses on enhancing telemetry coverage and cleaning up the codebase for better maintainability and reliability.
1 parent 9741015 commit db20b2d

33 files changed

+628
-240
lines changed

src/AzureDBExperiences.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export enum API {
1515
Core = 'Core', // Now called NoSQL
1616
PostgresSingle = 'PostgresSingle',
1717
PostgresFlexible = 'PostgresFlexible',
18+
Common = 'Common', // In case we're reporting a common event and still need to provide the value of the API
1819
}
1920

2021
export enum DBAccountKind {

src/commands/importDocuments.ts

+23-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { type ItemDefinition } from '@azure/cosmos';
7-
import { parseError, type IActionContext } from '@microsoft/vscode-azext-utils';
7+
import { callWithTelemetryAndErrorHandling, parseError, type IActionContext } from '@microsoft/vscode-azext-utils';
88
import { EJSON } from 'bson';
99
import * as fse from 'fs-extra';
1010
import * as vscode from 'vscode';
@@ -191,9 +191,17 @@ async function insertDocumentsIntoDocdb(
191191
// eslint-disable-next-line @typescript-eslint/no-explicit-any
192192
async function insertDocumentsIntoMongo(node: MongoCollectionTreeItem, documents: any[]): Promise<string> {
193193
let output = '';
194-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
195-
const parsed = await node.collection.insertMany(documents);
196-
if (parsed.acknowledged) {
194+
195+
const parsed = await callWithTelemetryAndErrorHandling('cosmosDB.mongo.importDocumets', async (actionContext) => {
196+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
197+
const parsed = await node.collection.insertMany(documents);
198+
199+
actionContext.telemetry.measurements.documentCount = parsed?.insertedCount;
200+
201+
return parsed;
202+
});
203+
204+
if (parsed?.acknowledged) {
197205
output = `Import into mongo successful. Inserted ${parsed.insertedCount} document(s). See output for more details.`;
198206
for (const inserted of Object.values(parsed.insertedIds)) {
199207
ext.outputChannel.appendLog(`Inserted document: ${inserted}`);
@@ -207,10 +215,19 @@ async function insertDocumentsIntoMongoCluster(
207215
node: CollectionItem,
208216
documents: unknown[],
209217
): Promise<string> {
210-
const result = await node.insertDocuments(context, documents as Document[]);
218+
const result = await callWithTelemetryAndErrorHandling(
219+
'cosmosDB.mongoClusters.importDocumets',
220+
async (actionContext) => {
221+
const result = await node.insertDocuments(context, documents as Document[]);
222+
223+
actionContext.telemetry.measurements.documentCount = result?.insertedCount;
224+
225+
return result;
226+
},
227+
);
211228

212229
let message: string;
213-
if (result.acknowledged) {
230+
if (result?.acknowledged) {
214231
message = `Import successful. Inserted ${result.insertedCount} document(s).`;
215232
} else {
216233
message = `Import failed. The operation was not acknowledged by the database.`;

src/docdb/tree/DocDBAccountTreeItemBase.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export abstract class DocDBAccountTreeItemBase extends DocDBTreeItemBase<Databas
104104
const result = await callWithTelemetryAndErrorHandling(
105105
'getChildren',
106106
async (context: IActionContext): Promise<AzExtTreeItem[]> => {
107-
context.telemetry.properties.parentContext = this.contextValue;
107+
context.telemetry.properties.parentNodeContext = this.contextValue;
108108

109109
// move this to a shared file, currently it's defined in DocDBAccountTreeItem so I can't reference it here
110110
if (this.contextValue.includes('cosmosDBDocumentServer')) {

src/mongo/connectToMongoClient.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export async function connectToMongoClient(connectionString: string, appName: st
1010
// appname appears to be the correct equivalent to user-agent for mongo
1111
const options: MongoClientOptions = <MongoClientOptions>{
1212
// appName should be wrapped in '@'s when trying to connect to a Mongo account, this doesn't effect the appendUserAgent string
13-
appName: `@${appName}@`,
13+
appName: `${appName}[RU]`,
1414
// https://github.com/lmammino/mongo-uri-builder/issues/2
1515
useNewUrlParser: true,
1616
useUnifiedTopology: true,

src/mongo/tree/MongoAccountTreeItem.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class MongoAccountTreeItem extends AzExtParentTreeItem {
7070
'getChildren',
7171
async (context: IActionContext): Promise<AzExtTreeItem[]> => {
7272
context.telemetry.properties.experience = API.MongoDB;
73-
context.telemetry.properties.parentContext = this.contextValue;
73+
context.telemetry.properties.parentNodeContext = this.contextValue;
7474

7575
let mongoClient: MongoClient | undefined;
7676
try {

src/mongoClusters/MongoClustersClient.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* singletone on a client with a getter from a connection pool..
1010
*/
1111

12+
import { appendExtensionUserAgent, callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
1213
import { EJSON } from 'bson';
1314
import {
1415
MongoClient,
@@ -22,6 +23,8 @@ import {
2223
type WithoutId,
2324
} from 'mongodb';
2425
import { CredentialCache } from './CredentialCache';
26+
import { areMongoDBAzure, getHostsFromConnectionString } from './utils/connectionStringHelpers';
27+
import { getMongoClusterMetadata, type MongoClusterMetadata } from './utils/getMongoClusterMetadata';
2528
import { toFilterQueryObj } from './utils/toFilterQuery';
2629

2730
export interface DatabaseItemModel {
@@ -73,9 +76,26 @@ export class MongoClustersClient {
7376
}
7477

7578
this._credentialId = credentialId;
79+
80+
// check if it's an azure connection, and do some special handling
81+
const cString = CredentialCache.getCredentials(credentialId)?.connectionString as string;
82+
const hosts = getHostsFromConnectionString(cString);
83+
const userAgentString = areMongoDBAzure(hosts) ? appendExtensionUserAgent() : undefined;
84+
7685
const cStringPassword = CredentialCache.getConnectionStringWithPassword(credentialId);
7786

78-
this._mongoClient = await MongoClient.connect(cStringPassword as string);
87+
this._mongoClient = await MongoClient.connect(cStringPassword as string, {
88+
appName: userAgentString,
89+
});
90+
91+
void callWithTelemetryAndErrorHandling('cosmosDB.mongoClusters.connect.getmetadata', async (context) => {
92+
const metadata: MongoClusterMetadata = await getMongoClusterMetadata(this._mongoClient);
93+
94+
context.telemetry.properties = {
95+
...context.telemetry.properties,
96+
...metadata,
97+
};
98+
});
7999
}
80100

81101
public static async getClient(credentialId: string): Promise<MongoClustersClient> {
@@ -197,7 +217,7 @@ export class MongoClustersClient {
197217
try {
198218
while (await cursor.hasNext()) {
199219
if (abortSignal.aborted) {
200-
console.log('streamDocuments: Aborted by an abort signal.');
220+
console.debug('streamDocuments: Aborted by an abort signal.');
201221
return;
202222
}
203223

@@ -324,7 +344,7 @@ export class MongoClustersClient {
324344
try {
325345
newCollection = await this._mongoClient.db(databaseName).createCollection(collectionName);
326346
} catch (_e) {
327-
console.log(_e); //todo: add to telemetry
347+
console.error(_e); //todo: add to telemetry
328348
return false;
329349
}
330350

@@ -338,7 +358,7 @@ export class MongoClustersClient {
338358
.createCollection('_dummy_collection_creation_forces_db_creation');
339359
await newCollection.drop();
340360
} catch (_e) {
341-
console.log(_e); //todo: add to telemetry
361+
console.error(_e); //todo: add to telemetry
342362
return false;
343363
}
344364

src/mongoClusters/MongoClustersExtension.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,6 @@ export class MongoClustersExtension implements vscode.Disposable {
8686
registerCommand('command.internal.mongoClusters.containerView.open', openCollectionView);
8787
registerCommand('command.internal.mongoClusters.documentView.open', openDocumentView);
8888

89-
registerCommand('command.internal.mongoClusters.importDocuments', mongoClustersImportDocuments);
90-
registerCommand('command.internal.mongoClusters.exportDocuments', mongoClustersExportQueryResults);
91-
9289
registerCommandWithTreeNodeUnwrapping('command.mongoClusters.launchShell', launchShell);
9390

9491
registerCommandWithTreeNodeUnwrapping('command.mongoClusters.dropCollection', dropCollection);
@@ -101,6 +98,18 @@ export class MongoClustersExtension implements vscode.Disposable {
10198
'command.mongoClusters.importDocuments',
10299
mongoClustersImportDocuments,
103100
);
101+
102+
/**
103+
* Here, exporting documents is done in two ways: one is accessible from the tree view
104+
* via a context menu, and the other is accessible programmatically. Both of them
105+
* use the same underlying function to export documents.
106+
*
107+
* mongoClustersExportEntireCollection calls mongoClustersExportQueryResults with no queryText.
108+
*
109+
* It was possible to merge the two commands into one, but it would result in code that is
110+
* harder to understand and maintain.
111+
*/
112+
registerCommand('command.internal.mongoClusters.exportDocuments', mongoClustersExportQueryResults);
104113
registerCommandWithTreeNodeUnwrapping(
105114
'command.mongoClusters.exportDocuments',
106115
mongoClustersExportEntireCollection,

src/mongoClusters/commands/addWorkspaceConnection.ts

+3-18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { WorkspaceResourceType } from '../../tree/workspace/sharedWorkspaceResou
1212
import { SharedWorkspaceStorage } from '../../tree/workspace/sharedWorkspaceStorage';
1313
import { showConfirmationAsInSettings } from '../../utils/dialogs/showConfirmation';
1414
import { localize } from '../../utils/localize';
15+
import { areMongoDBRU } from '../utils/connectionStringHelpers';
1516
import { type AddWorkspaceConnectionContext } from '../wizards/addWorkspaceConnection/AddWorkspaceConnectionContext';
1617
import { ConnectionStringStep } from '../wizards/addWorkspaceConnection/ConnectionStringStep';
1718
import { PasswordStep } from '../wizards/addWorkspaceConnection/PasswordStep';
@@ -55,12 +56,8 @@ export async function addWorkspaceConnection(context: IActionContext): Promise<v
5556
wizardContext.valuesToMask.push(connectionStringWithCredentials);
5657

5758
// discover whether it's a MongoDB RU connection string and abort here.
58-
let isRU: boolean = false;
59-
connectionString.hosts.forEach((host) => {
60-
if (isMongoDBRU(host)) {
61-
isRU = true;
62-
}
63-
});
59+
const isRU = areMongoDBRU(connectionString.hosts);
60+
6461
if (isRU) {
6562
try {
6663
await vscode.window.showInformationMessage(
@@ -104,15 +101,3 @@ export async function addWorkspaceConnection(context: IActionContext): Promise<v
104101
localize('showConfirmation.addedWorkspaceConnecdtion', 'New connection has been added to your workspace.'),
105102
);
106103
}
107-
108-
function isMongoDBRU(host: string): boolean {
109-
const knownSuffixes = ['mongo.cosmos.azure.com'];
110-
const hostWithoutPort = host.split(':')[0];
111-
112-
for (const suffix of knownSuffixes) {
113-
if (hostWithoutPort.toLowerCase().endsWith(suffix)) {
114-
return true;
115-
}
116-
}
117-
return false;
118-
}

src/mongoClusters/commands/exportDocuments.ts

+23-15
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { type IActionContext } from '@microsoft/vscode-azext-utils';
6+
import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils';
77
import { EJSON } from 'bson';
88
import * as vscode from 'vscode';
99
import { ext } from '../../extensionVariables';
@@ -12,21 +12,23 @@ import { getRootPath } from '../../utils/workspacUtils';
1212
import { MongoClustersClient } from '../MongoClustersClient';
1313
import { type CollectionItem } from '../tree/CollectionItem';
1414

15-
export async function mongoClustersExportEntireCollection(_context: IActionContext, node?: CollectionItem) {
16-
return mongoClustersExportQueryResults(_context, node);
15+
export async function mongoClustersExportEntireCollection(context: IActionContext, node?: CollectionItem) {
16+
return mongoClustersExportQueryResults(context, node);
1717
}
1818

1919
export async function mongoClustersExportQueryResults(
20-
_context: IActionContext,
20+
context: IActionContext,
2121
node?: CollectionItem,
22-
queryText?: string,
22+
props?: { queryText?: string; source?: string },
2323
): Promise<void> {
2424
// node ??= ... pick a node if not provided
2525
if (!node) {
2626
throw new Error('No collection selected.');
2727
}
2828

29-
const targetUri = await askForTargetFile(_context);
29+
context.telemetry.properties.calledFrom = props?.source || 'contextMenu';
30+
31+
const targetUri = await askForTargetFile(context);
3032

3133
if (!targetUri) {
3234
return;
@@ -39,7 +41,7 @@ export async function mongoClustersExportQueryResults(
3941
node.databaseInfo.name,
4042
node.collectionInfo.name,
4143
docStreamAbortController.signal,
42-
queryText,
44+
props?.queryText,
4345
);
4446

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

5052
// Wrap the export process inside a progress reporting function
51-
await runExportWithProgressAndDescription(node.id, async (progress, cancellationToken) => {
52-
documentCount = await exportDocumentsToFile(
53-
docStream,
54-
filePath,
55-
progress,
56-
cancellationToken,
57-
docStreamAbortController,
58-
);
53+
await callWithTelemetryAndErrorHandling('cosmosDB.mongoClusters.exportDocuments', async (actionContext) => {
54+
await runExportWithProgressAndDescription(node.id, async (progress, cancellationToken) => {
55+
documentCount = await exportDocumentsToFile(
56+
docStream,
57+
filePath,
58+
progress,
59+
cancellationToken,
60+
docStreamAbortController,
61+
);
62+
});
63+
64+
actionContext.telemetry.properties.source = props?.source;
65+
actionContext.telemetry.measurements.queryLength = props?.queryText?.length;
66+
actionContext.telemetry.measurements.documentCount = documentCount;
5967
});
6068

6169
ext.outputChannel.appendLog(`MongoDB Clusters: Exported document count: ${documentCount}`);

src/mongoClusters/commands/importDocuments.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import { type CollectionItem } from '../tree/CollectionItem';
1010
export async function mongoClustersImportDocuments(
1111
context: IActionContext,
1212
collectionNode?: CollectionItem,
13+
_collectionNodes?: CollectionItem[], // required by the TreeNodeCommandCallback, but not used
14+
...args: unknown[]
1315
): Promise<void> {
16+
const source = (args[0] as { source?: string })?.source || 'contextMenu';
17+
context.telemetry.properties.calledFrom = source;
18+
1419
return importDocuments(context, undefined, collectionNode);
1520
}

src/mongoClusters/tree/MongoClusterResourceItem.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class MongoClusterResourceItem extends MongoClusterItemBase {
4040
*/
4141
protected async authenticateAndConnect(): Promise<MongoClustersClient | null> {
4242
const result = await callWithTelemetryAndErrorHandling(
43-
'cosmosDB.mongoClusters.authenticate',
43+
'cosmosDB.mongoClusters.connect',
4444
async (context: IActionContext) => {
4545
ext.outputChannel.appendLine(
4646
`MongoDB Clusters: Attempting to authenticate with "${this.mongoCluster.name}"...`,

src/mongoClusters/tree/MongoClustersBranchDataProvider.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class MongoClustersBranchDataProvider
5252
*/
5353
return await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => {
5454
context.telemetry.properties.experience = API.MongoClusters;
55-
context.telemetry.properties.parentContext = (await element.getTreeItem()).contextValue ?? 'unknown';
55+
context.telemetry.properties.parentNodeContext = (await element.getTreeItem()).contextValue || 'unknown';
5656

5757
return (await element.getChildren?.())?.map((child) => {
5858
if (child.id) {
@@ -174,7 +174,7 @@ export class MongoClustersBranchDataProvider
174174
});
175175
});
176176
} catch (e) {
177-
console.error({ ...context, ...subscription });
177+
console.debug({ ...context, ...subscription });
178178
throw e;
179179
}
180180
},

src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase {
3737
*/
3838
protected async authenticateAndConnect(): Promise<MongoClustersClient | null> {
3939
const result = await callWithTelemetryAndErrorHandling(
40-
'cosmosDB.mongoClusters.authenticate',
40+
'cosmosDB.mongoClusters.connect',
4141
async (context: IActionContext) => {
4242
context.telemetry.properties.view = 'workspace';
4343

@@ -93,7 +93,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase {
9393
throw error;
9494
});
9595
} catch (error) {
96-
console.log(error);
96+
console.error(error);
9797
// If connection fails, remove cached credentials
9898
await MongoClustersClient.deleteClient(this.id);
9999
CredentialCache.deleteCredentials(this.id);
@@ -126,7 +126,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase {
126126

127127
// Prompt the user for credentials
128128
await callWithTelemetryAndErrorHandling(
129-
'cosmosDB.mongoClusters.authenticate.promptForCredentials',
129+
'cosmosDB.mongoClusters.connect.promptForCredentials',
130130
async (context: IActionContext) => {
131131
context.telemetry.properties.view = 'workspace';
132132

src/mongoClusters/tree/workspace/MongoClustersWorkbenchBranchDataProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class MongoClustersWorkspaceBranchDataProvider
3535
return await callWithTelemetryAndErrorHandling('getChildren', async (context: IActionContext) => {
3636
context.telemetry.properties.experience = API.MongoClusters;
3737
context.telemetry.properties.view = 'workspace';
38-
context.telemetry.properties.parentContext = (await element.getTreeItem()).contextValue ?? 'unknown';
38+
context.telemetry.properties.parentNodeContext = (await element.getTreeItem()).contextValue ?? 'unknown';
3939

4040
return (await element.getChildren?.())?.map((child) => {
4141
if (child.id) {

0 commit comments

Comments
 (0)