From db20b2d27b2bb3d37c4ccf9645f80307c2f81c8f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 5 Dec 2024 13:51:49 +0100 Subject: [PATCH] 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. --- src/AzureDBExperiences.ts | 1 + src/commands/importDocuments.ts | 29 +++- src/docdb/tree/DocDBAccountTreeItemBase.ts | 2 +- src/mongo/connectToMongoClient.ts | 2 +- src/mongo/tree/MongoAccountTreeItem.ts | 2 +- src/mongoClusters/MongoClustersClient.ts | 28 +++- src/mongoClusters/MongoClustersExtension.ts | 15 +- .../commands/addWorkspaceConnection.ts | 21 +-- src/mongoClusters/commands/exportDocuments.ts | 38 +++-- src/mongoClusters/commands/importDocuments.ts | 5 + .../tree/MongoClusterResourceItem.ts | 2 +- .../tree/MongoClustersBranchDataProvider.ts | 4 +- .../workspace/MongoClusterWorkspaceItem.ts | 6 +- ...ongoClustersWorkbenchBranchDataProvider.ts | 2 +- .../utils/connectionStringHelpers.ts | 33 +++++ .../utils/getMongoClusterMetadata.ts | 107 ++++++++++++++ .../create/PromptCollectionNameStep.ts | 2 +- .../wizards/create/PromptDatabaseNameStep.ts | 2 +- src/table/tree/TableAccountTreeItem.ts | 2 +- src/webviews/api/configuration/appRouter.ts | 66 ++++++++- .../api/extension-server/WebviewController.ts | 12 +- src/webviews/api/extension-server/trpc.ts | 29 ++++ .../collectionView/CollectionView.tsx | 71 +++++++-- .../collectionViewController.ts | 5 +- .../collectionView/collectionViewRouter.ts | 34 +++-- .../components/DataViewPanelTable.tsx | 2 +- .../components/toolbar/ToolbarMainView.tsx | 37 +++++ .../toolbar/ToolbarTableNavigation.tsx | 43 +++++- .../toolbar/ToolbarViewNavigation.tsx | 83 ++++++++++- .../components/toolbar/toolbarPaging.tsx | 138 ------------------ .../documentView/documentView.tsx | 33 ++++- .../documentView/documentsViewController.ts | 5 +- .../documentView/documentsViewRouter.ts | 7 +- 33 files changed, 628 insertions(+), 240 deletions(-) create mode 100644 src/mongoClusters/utils/getMongoClusterMetadata.ts delete mode 100644 src/webviews/mongoClusters/collectionView/components/toolbar/toolbarPaging.tsx diff --git a/src/AzureDBExperiences.ts b/src/AzureDBExperiences.ts index 454e2bdf..eed43a81 100644 --- a/src/AzureDBExperiences.ts +++ b/src/AzureDBExperiences.ts @@ -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 { diff --git a/src/commands/importDocuments.ts b/src/commands/importDocuments.ts index baa18f8a..9ee006e9 100644 --- a/src/commands/importDocuments.ts +++ b/src/commands/importDocuments.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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 * as vscode from 'vscode'; @@ -191,9 +191,17 @@ async function insertDocumentsIntoDocdb( // eslint-disable-next-line @typescript-eslint/no-explicit-any async function insertDocumentsIntoMongo(node: MongoCollectionTreeItem, documents: any[]): Promise { let output = ''; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const parsed = await node.collection.insertMany(documents); - if (parsed.acknowledged) { + + const parsed = await callWithTelemetryAndErrorHandling('cosmosDB.mongo.importDocumets', async (actionContext) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const parsed = await node.collection.insertMany(documents); + + actionContext.telemetry.measurements.documentCount = parsed?.insertedCount; + + return parsed; + }); + + 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}`); @@ -207,10 +215,19 @@ async function insertDocumentsIntoMongoCluster( node: CollectionItem, documents: unknown[], ): Promise { - const result = await node.insertDocuments(context, documents as Document[]); + const result = await callWithTelemetryAndErrorHandling( + 'cosmosDB.mongoClusters.importDocumets', + async (actionContext) => { + const result = await node.insertDocuments(context, documents as Document[]); + + actionContext.telemetry.measurements.documentCount = result?.insertedCount; + + return result; + }, + ); 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.`; diff --git a/src/docdb/tree/DocDBAccountTreeItemBase.ts b/src/docdb/tree/DocDBAccountTreeItemBase.ts index 3c80426c..716da8e4 100644 --- a/src/docdb/tree/DocDBAccountTreeItemBase.ts +++ b/src/docdb/tree/DocDBAccountTreeItemBase.ts @@ -104,7 +104,7 @@ export abstract class DocDBAccountTreeItemBase extends DocDBTreeItemBase => { - 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')) { diff --git a/src/mongo/connectToMongoClient.ts b/src/mongo/connectToMongoClient.ts index b33d266d..70227f97 100644 --- a/src/mongo/connectToMongoClient.ts +++ b/src/mongo/connectToMongoClient.ts @@ -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 = { // 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, diff --git a/src/mongo/tree/MongoAccountTreeItem.ts b/src/mongo/tree/MongoAccountTreeItem.ts index ba45b9a3..3834b37d 100644 --- a/src/mongo/tree/MongoAccountTreeItem.ts +++ b/src/mongo/tree/MongoAccountTreeItem.ts @@ -70,7 +70,7 @@ export class MongoAccountTreeItem extends AzExtParentTreeItem { 'getChildren', async (context: IActionContext): Promise => { context.telemetry.properties.experience = API.MongoDB; - context.telemetry.properties.parentContext = this.contextValue; + context.telemetry.properties.parentNodeContext = this.contextValue; let mongoClient: MongoClient | undefined; try { diff --git a/src/mongoClusters/MongoClustersClient.ts b/src/mongoClusters/MongoClustersClient.ts index 8c0d9d85..3df82ca9 100644 --- a/src/mongoClusters/MongoClustersClient.ts +++ b/src/mongoClusters/MongoClustersClient.ts @@ -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, @@ -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 { @@ -73,9 +76,26 @@ export class MongoClustersClient { } this._credentialId = credentialId; + + // check if it's an azure connection, and do some special handling + const cString = CredentialCache.getCredentials(credentialId)?.connectionString as string; + const hosts = getHostsFromConnectionString(cString); + const userAgentString = areMongoDBAzure(hosts) ? appendExtensionUserAgent() : undefined; + 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 { @@ -197,7 +217,7 @@ export class MongoClustersClient { try { while (await cursor.hasNext()) { if (abortSignal.aborted) { - console.log('streamDocuments: Aborted by an abort signal.'); + console.debug('streamDocuments: Aborted by an abort signal.'); return; } @@ -324,7 +344,7 @@ export class MongoClustersClient { try { newCollection = await this._mongoClient.db(databaseName).createCollection(collectionName); } catch (_e) { - console.log(_e); //todo: add to telemetry + console.error(_e); //todo: add to telemetry return false; } @@ -338,7 +358,7 @@ export class MongoClustersClient { .createCollection('_dummy_collection_creation_forces_db_creation'); await newCollection.drop(); } catch (_e) { - console.log(_e); //todo: add to telemetry + console.error(_e); //todo: add to telemetry return false; } diff --git a/src/mongoClusters/MongoClustersExtension.ts b/src/mongoClusters/MongoClustersExtension.ts index d67cb186..3156e909 100644 --- a/src/mongoClusters/MongoClustersExtension.ts +++ b/src/mongoClusters/MongoClustersExtension.ts @@ -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); @@ -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, diff --git a/src/mongoClusters/commands/addWorkspaceConnection.ts b/src/mongoClusters/commands/addWorkspaceConnection.ts index 31ac2ce5..a6ff01ad 100644 --- a/src/mongoClusters/commands/addWorkspaceConnection.ts +++ b/src/mongoClusters/commands/addWorkspaceConnection.ts @@ -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'; @@ -55,12 +56,8 @@ export async function addWorkspaceConnection(context: IActionContext): Promise { - if (isMongoDBRU(host)) { - isRU = true; - } - }); + const isRU = areMongoDBRU(connectionString.hosts); + if (isRU) { try { await vscode.window.showInformationMessage( @@ -104,15 +101,3 @@ export async function addWorkspaceConnection(context: IActionContext): Promise { // 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; @@ -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 @@ -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}`); diff --git a/src/mongoClusters/commands/importDocuments.ts b/src/mongoClusters/commands/importDocuments.ts index c6fc33a3..5b425086 100644 --- a/src/mongoClusters/commands/importDocuments.ts +++ b/src/mongoClusters/commands/importDocuments.ts @@ -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 { + const source = (args[0] as { source?: string })?.source || 'contextMenu'; + context.telemetry.properties.calledFrom = source; + return importDocuments(context, undefined, collectionNode); } diff --git a/src/mongoClusters/tree/MongoClusterResourceItem.ts b/src/mongoClusters/tree/MongoClusterResourceItem.ts index 986756ac..eeb7c6b4 100644 --- a/src/mongoClusters/tree/MongoClusterResourceItem.ts +++ b/src/mongoClusters/tree/MongoClusterResourceItem.ts @@ -40,7 +40,7 @@ export class MongoClusterResourceItem extends MongoClusterItemBase { */ protected async authenticateAndConnect(): Promise { 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}"...`, diff --git a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts index 2d91a768..7a981060 100644 --- a/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts +++ b/src/mongoClusters/tree/MongoClustersBranchDataProvider.ts @@ -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) { @@ -174,7 +174,7 @@ export class MongoClustersBranchDataProvider }); }); } catch (e) { - console.error({ ...context, ...subscription }); + console.debug({ ...context, ...subscription }); throw e; } }, diff --git a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts index f7afc50a..b64f7844 100644 --- a/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts +++ b/src/mongoClusters/tree/workspace/MongoClusterWorkspaceItem.ts @@ -37,7 +37,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase { */ protected async authenticateAndConnect(): Promise { const result = await callWithTelemetryAndErrorHandling( - 'cosmosDB.mongoClusters.authenticate', + 'cosmosDB.mongoClusters.connect', async (context: IActionContext) => { context.telemetry.properties.view = 'workspace'; @@ -93,7 +93,7 @@ export class MongoClusterWorkspaceItem extends MongoClusterItemBase { throw error; }); } catch (error) { - console.log(error); + console.error(error); // If connection fails, remove cached credentials await MongoClustersClient.deleteClient(this.id); CredentialCache.deleteCredentials(this.id); @@ -126,7 +126,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'; diff --git a/src/mongoClusters/tree/workspace/MongoClustersWorkbenchBranchDataProvider.ts b/src/mongoClusters/tree/workspace/MongoClustersWorkbenchBranchDataProvider.ts index 39634e06..35fa6509 100644 --- a/src/mongoClusters/tree/workspace/MongoClustersWorkbenchBranchDataProvider.ts +++ b/src/mongoClusters/tree/workspace/MongoClustersWorkbenchBranchDataProvider.ts @@ -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) { diff --git a/src/mongoClusters/utils/connectionStringHelpers.ts b/src/mongoClusters/utils/connectionStringHelpers.ts index 68ccecff..8083d0f0 100644 --- a/src/mongoClusters/utils/connectionStringHelpers.ts +++ b/src/mongoClusters/utils/connectionStringHelpers.ts @@ -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); +} diff --git a/src/mongoClusters/utils/getMongoClusterMetadata.ts b/src/mongoClusters/utils/getMongoClusterMetadata.ts new file mode 100644 index 00000000..6d7251ab --- /dev/null +++ b/src/mongoClusters/utils/getMongoClusterMetadata.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { type MongoClient } from 'mongodb'; + +/** + * Interface to define the structure of MongoDB cluster metadata. + * The data structure is flat with dot notation in field names to meet requirements in the telemetry section. + * + * The fields are optional to allow for partial data collection in case of errors. + */ +export interface MongoClusterMetadata { + 'serverInfo.version'?: string; // MongoDB server version (non-sensitive) + 'serverInfo.gitVersion'?: string; // Git version of the MongoDB server (non-sensitive) + 'serverInfo.opensslVersion'?: string; // OpenSSL version used by the server (non-sensitive) + 'serverInfo.platform'?: string; // Server platform information (non-sensitive) + 'serverInfo.storageEngines'?: string; // Storage engine used by the server (non-sensitive) + 'serverInfo.modules'?: string; // List of modules loaded by the server (non-sensitive) + 'serverInfo.error'?: string; // Error message if fetching server info fails + + 'topology.type'?: string; // Type of topology (e.g., replica set, sharded cluster) + 'topology.numberOfServers'?: string; // Number of servers + 'topology.minWireVersion'?: string; // Minimum wire protocol version supported + 'topology.maxWireVersion'?: string; // Maximum wire protocol version supported + 'topology.error'?: string; // Error message if fetching topology info fails + + 'serverStatus.uptime'?: string; // Server uptime in seconds (non-sensitive) + 'serverStatus.connections.current'?: string; // Current number of connections (non-sensitive) + 'serverStatus.connections.available'?: string; // Available connections (non-sensitive) + 'serverStatus.memory.resident'?: string; // Resident memory usage in MB (non-sensitive) + 'serverStatus.memory.virtual'?: string; // Virtual memory usage in MB (non-sensitive) + 'serverStatus.error'?: string; // Error message if fetching server status fails + + 'hostInfo.json'?: string; // JSON stringified host information + 'hostInfo.error'?: string; // Error message if fetching host info fails +} + +/** + * Retrieves metadata information about a MongoDB cluster. + * This data helps improve diagnostics and user experience. + * No internal server addresses or sensitive information are read. + * + * @param client - The MongoClient instance connected to the MongoDB cluster. + * @returns A promise that resolves to an object containing various metadata about the MongoDB cluster. + * + */ +export async function getMongoClusterMetadata(client: MongoClient): Promise { + const result: MongoClusterMetadata = {}; + + const adminDb = client.db().admin(); + + // Fetch build info (server version, git version, etc.) + // This information is non-sensitive and aids in diagnostics. + try { + const buildInfo = await adminDb.command({ buildInfo: 1 }); + result['serverInfo.version'] = buildInfo.version; + result['serverInfo.gitVersion'] = buildInfo.gitVersion; + result['serverInfo.opensslVersion'] = buildInfo.opensslVersion; + result['serverInfo.platform'] = buildInfo.platform; + result['serverInfo.storageEngines'] = (buildInfo.storageEngines as string[])?.join(';'); + result['serverInfo.modules'] = (buildInfo.modules as string[])?.join(';'); + } catch (error) { + result['serverInfo.error'] = error instanceof Error ? error.message : String(error); + } + + // Fetch server status information. + // Includes non-sensitive data like uptime and connection metrics. + try { + const serverStatus = await adminDb.command({ serverStatus: 1 }); + result['serverStatus.uptime'] = serverStatus.uptime.toString(); + result['serverStatus.connections.current'] = serverStatus.connections?.current.toString(); + result['serverStatus.connections.available'] = serverStatus.connections?.available.toString(); + result['serverStatus.memory.resident'] = serverStatus.mem?.resident.toString(); + result['serverStatus.memory.virtual'] = serverStatus.mem?.virtual.toString(); + } catch (error) { + result['serverStatus.error'] = error instanceof Error ? error.message : String(error); + } + + // Fetch topology information using the 'hello' command. + // Internal server addresses are not collected to ensure privacy. + try { + const helloInfo = await adminDb.command({ hello: 1 }); + result['topology.type'] = helloInfo.msg || 'unknown'; + result['topology.numberOfServers'] = (helloInfo.hosts?.length || 0).toString(); + result['topology.minWireVersion'] = helloInfo.minWireVersion.toString(); + result['topology.maxWireVersion'] = helloInfo.maxWireVersion.toString(); + } catch (error) { + result['topology.error'] = error instanceof Error ? error.message : String(error); + } + + // Fetch host information + try { + const hostInfo = await adminDb.command({ hostInfo: 1 }); + result['hostInfo.json'] = JSON.stringify(hostInfo); + } catch (error) { + result['hostInfo.error'] = error instanceof Error ? error.message : String(error); + } + + // Return the collected metadata. + return result; +} diff --git a/src/mongoClusters/wizards/create/PromptCollectionNameStep.ts b/src/mongoClusters/wizards/create/PromptCollectionNameStep.ts index b24fe04f..75101a2f 100644 --- a/src/mongoClusters/wizards/create/PromptCollectionNameStep.ts +++ b/src/mongoClusters/wizards/create/PromptCollectionNameStep.ts @@ -91,7 +91,7 @@ export class CollectionNameStep extends AzureWizardPromptStep { context.telemetry.properties.experience = API.Table; - context.telemetry.properties.parentContext = this.contextValue; + context.telemetry.properties.parentNodeContext = this.contextValue; const tableNotFoundTreeItem: AzExtTreeItem = new GenericTreeItem(this, { contextValue: 'tableNotSupported', diff --git a/src/webviews/api/configuration/appRouter.ts b/src/webviews/api/configuration/appRouter.ts index b0c1846d..1a904845 100644 --- a/src/webviews/api/configuration/appRouter.ts +++ b/src/webviews/api/configuration/appRouter.ts @@ -6,7 +6,9 @@ /** * This a minimal tRPC server */ +import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; import { z } from 'zod'; +import { type API } from '../../../AzureDBExperiences'; import { collectionsViewRouter as collectionViewRouter } from '../../mongoClusters/collectionView/collectionViewRouter'; import { documentsViewRouter as documentViewRouter } from '../../mongoClusters/documentView/documentsViewRouter'; import { publicProcedure, router } from '../extension-server/trpc'; @@ -23,7 +25,69 @@ import { publicProcedure, router } from '../extension-server/trpc'; * There is one router called 'commonRouter'. It has procedures that are shared across all webviews. */ +export type BaseRouterContext = { + dbExperience: API; + webviewName: string; +}; + +/** + * eventName: string, + properties?: Record, + measurements?: Record + */ const commonRouter = router({ + reportEvent: publicProcedure + // This is the input schema of your procedure, two parameters, both strings + .input( + z.object({ + eventName: z.string(), + properties: z.optional(z.record(z.string())), //By default, the keys of a JavaScript object are always strings (or symbols). Even if you use a number as an object key, JavaScript will convert it to a string internally. + measurements: z.optional(z.record(z.number())), //By default, the keys of a JavaScript object are always strings (or symbols). Even if you use a number as an object key, JavaScript will convert it to a string internally. + }), + ) + // Here the procedure (query or mutation) + .mutation(({ input, ctx }) => { + const myCtx = ctx as BaseRouterContext; + + void callWithTelemetryAndErrorHandling( + `cosmosDB.${myCtx.dbExperience}.webview.event.${myCtx.webviewName}.${input.eventName}`, + (context) => { + context.errorHandling.suppressDisplay = true; + context.telemetry.properties.experience = myCtx.dbExperience; + Object.assign(context.telemetry.properties, input.properties ?? {}); + Object.assign(context.telemetry.measurements, input.measurements ?? {}); + }, + ); + }), + reportError: publicProcedure + // This is the input schema of your procedure, two parameters, both strings + .input( + z.object({ + message: z.string(), + stack: z.string(), + componentStack: z.optional(z.string()), + properties: z.optional(z.record(z.string())), //By default, the keys of a JavaScript object are always strings (or symbols). Even if you use a number as an object key, JavaScript will convert it to a string internally. + }), + ) + // Here the procedure (query or mutation) + .mutation(({ input, ctx }) => { + const myCtx = ctx as BaseRouterContext; + + void callWithTelemetryAndErrorHandling( + `cosmosDB.${myCtx.dbExperience}.webview.error.${myCtx.webviewName}`, + (context) => { + context.errorHandling.suppressDisplay = true; + context.telemetry.properties.experience = myCtx.dbExperience; + + Object.assign(context.telemetry.properties, input.properties ?? {}); + + const newError = new Error(input.message); + // If it's a rendering error in the webview, swap the stack with the componentStack which is more helpful + newError.stack = input.componentStack ?? input.stack; + throw newError; + }, + ); + }), hello: publicProcedure // This is the input schema of your procedure, no parameters .query(async () => { @@ -37,8 +101,6 @@ const commonRouter = router({ .input(z.string()) // Here the procedure (query or mutation) .query(async ({ input }) => { - await new Promise((resolve) => setTimeout(resolve, 3000)); - // This is what you're returning to your client return { text: `Hello ${input}!` }; }), diff --git a/src/webviews/api/extension-server/WebviewController.ts b/src/webviews/api/extension-server/WebviewController.ts index 3c244a3a..38fac622 100644 --- a/src/webviews/api/extension-server/WebviewController.ts +++ b/src/webviews/api/extension-server/WebviewController.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { appRouter } from '../configuration/appRouter'; +import { type API } from '../../../AzureDBExperiences'; +import { appRouter, type BaseRouterContext } from '../configuration/appRouter'; import { type VsCodeLinkNotification, type VsCodeLinkRequestMessage } from '../webview-client/vscodeLink'; import { WebviewBaseController } from './WebviewBaseController'; import { createCallerFactory } from './trpc'; @@ -21,7 +22,7 @@ export class WebviewController extends WebviewBaseController extends WebviewBaseController extends WebviewBaseController extends WebviewBaseController { + const result = await callWithTelemetryAndErrorHandling>( + `cosmosDB.rpc.${type}.${path}`, + async (context) => { + context.errorHandling.suppressDisplay = true; + + const result = await next(); + + if (!result.ok) { + context.telemetry.properties.result = 'Failed'; + context.telemetry.properties.error = result.error.message; + + /** + * we're not any error here as we just want to log it here and let the + * caller of the RPC call handle the error there. + */ + } + + return result; + }, + ); + + return result as MiddlewareResult; +}); diff --git a/src/webviews/mongoClusters/collectionView/CollectionView.tsx b/src/webviews/mongoClusters/collectionView/CollectionView.tsx index 9d4a155e..0dc07fe7 100644 --- a/src/webviews/mongoClusters/collectionView/CollectionView.tsx +++ b/src/webviews/mongoClusters/collectionView/CollectionView.tsx @@ -138,6 +138,17 @@ export const CollectionView = (): JSX.Element => { break; } + trpcClient.common.reportEvent + .mutate({ + eventName: 'viewChanged', + properties: { + view: selection, + }, + }) + .catch((error) => { + console.debug('Failed to report an event:', error); + }); + setCurrentContext((prev) => ({ ...prev, currentView: selection })); getDataForView(selection); }; @@ -169,8 +180,8 @@ export const CollectionView = (): JSX.Element => { tableData: (result.data as TableDataEntry[]) ?? [], })); }) - .catch((_error) => { - console.log('error'); + .catch((error) => { + console.debug('Failed to perform an action:', error); }); break; } @@ -183,8 +194,8 @@ export const CollectionView = (): JSX.Element => { treeData: result, })); }) - .catch((_error) => { - console.log('error'); + .catch((error) => { + console.debug('Failed to perform an action:', error); }); break; case Views.JSON: @@ -196,8 +207,8 @@ export const CollectionView = (): JSX.Element => { jsonDocuments: result, })); }) - .catch((_error) => { - console.log('error'); + .catch((error) => { + console.debug('Failed to perform an action:', error); }); break; default: @@ -211,8 +222,8 @@ export const CollectionView = (): JSX.Element => { .then(async (schema) => { void (await currentContextRef.current.queryEditor?.setJsonSchema(schema)); }) - .catch((_error) => { - console.log('error'); + .catch((error) => { + console.debug('Failed to perform an action:', error); }); } @@ -297,24 +308,40 @@ export const CollectionView = (): JSX.Element => { const activeCell = activeDocument[activeColumn] as { value?: string; type?: string }; - console.log('Step-in requested on cell', activeCell, 'in row', row, 'column', cell); + console.debug('Step-in requested on cell', activeCell, 'in row', row, 'column', cell); if (activeColumn === '_id') { - console.log('Cell is an _id, skipping step-in'); + console.debug('Cell is an _id, skipping step-in'); return; } if (activeCell.type !== 'object') { - console.log('Cell is not an object, skipping step-in'); + console.debug('Cell is not an object, skipping step-in'); return; } + const newPath = [...(currentContext.currentViewState?.currentPath ?? []), activeColumn]; + setCurrentContext((prev) => ({ ...prev, currentViewState: { - currentPath: [...(currentContext.currentViewState?.currentPath ?? []), activeColumn], + currentPath: newPath, }, })); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'stepIn', + properties: { + source: 'step-in-button', + }, + measurements: { + depth: newPath.length ?? 0, + }, + }) + .catch((error) => { + console.debug('Failed to report query event:', error); + }); } return ( @@ -327,12 +354,26 @@ export const CollectionView = (): JSX.Element => { + onExecuteRequest={(q: string) => { setCurrentContext((prev) => ({ ...prev, currrentQueryDefinition: { ...prev.currrentQueryDefinition, queryText: q, pageNumber: 1 }, - })) - } + })); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'executeQuery', + properties: { + ui: 'shortcut', + }, + measurements: { + queryLenth: q.length, + }, + }) + .catch((error) => { + console.debug('Failed to report query event:', error); + }); + }} /> diff --git a/src/webviews/mongoClusters/collectionView/collectionViewController.ts b/src/webviews/mongoClusters/collectionView/collectionViewController.ts index a3f63ba6..ea7324e8 100644 --- a/src/webviews/mongoClusters/collectionView/collectionViewController.ts +++ b/src/webviews/mongoClusters/collectionView/collectionViewController.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { API } from '../../../AzureDBExperiences'; import { ext } from '../../../extensionVariables'; import { type CollectionItem } from '../../../mongoClusters/tree/CollectionItem'; import { WebviewController } from '../../api/extension-server/WebviewController'; @@ -24,9 +25,11 @@ export class CollectionViewController extends WebviewController { + getInfo: publicProcedure.use(trpcToTelemetry).query(({ ctx }) => { const myCtx = ctx as RouterContext; return 'Info from the webview: ' + JSON.stringify(myCtx); }), runQuery: publicProcedure + .use(trpcToTelemetry) // parameters .input( z.object({ @@ -57,6 +59,7 @@ export const collectionsViewRouter = router({ return { documentCount: size }; }), getAutocompletionSchema: publicProcedure + .use(trpcToTelemetry) // procedure type .query(({ ctx }) => { const myCtx = ctx as RouterContext; @@ -78,6 +81,7 @@ export const collectionsViewRouter = router({ return querySchema; }), getCurrentPageAsTable: publicProcedure + .use(trpcToTelemetry) //parameters .input(z.array(z.string())) // procedure type @@ -90,6 +94,7 @@ export const collectionsViewRouter = router({ return tableData; }), getCurrentPageAsTree: publicProcedure + .use(trpcToTelemetry) // procedure type .query(({ ctx }) => { const myCtx = ctx as RouterContext; @@ -100,6 +105,7 @@ export const collectionsViewRouter = router({ return treeData; }), getCurrentPageAsJson: publicProcedure + .use(trpcToTelemetry) // procedure type .query(({ ctx }) => { const myCtx = ctx as RouterContext; @@ -110,6 +116,7 @@ export const collectionsViewRouter = router({ return jsonData; }), addDocument: publicProcedure + .use(trpcToTelemetry) // procedure type .mutation(({ ctx }) => { const myCtx = ctx as RouterContext; @@ -122,6 +129,7 @@ export const collectionsViewRouter = router({ }); }), viewDocumentById: publicProcedure + .use(trpcToTelemetry) // parameters .input(z.string()) // procedure type @@ -137,6 +145,7 @@ export const collectionsViewRouter = router({ }); }), editDocumentById: publicProcedure + .use(trpcToTelemetry) // parameters .input(z.string()) // procedure type @@ -152,6 +161,7 @@ export const collectionsViewRouter = router({ }); }), deleteDocumentsById: publicProcedure + .use(trpcToTelemetry) // parameteres .input(z.array(z.string())) // stands for string[] // procedure type @@ -194,21 +204,23 @@ export const collectionsViewRouter = router({ return acknowledged; }), exportDocuments: publicProcedure + .use(trpcToTelemetry) // parameters .input(z.object({ query: z.string() })) //procedure type - .query(async ({ input, ctx }) => { + .query(({ input, ctx }) => { const myCtx = ctx as RouterContext; - vscode.commands.executeCommand( - 'command.internal.mongoClusters.exportDocuments', - myCtx.collectionTreeItem, - input.query, - ); + vscode.commands.executeCommand('command.internal.mongoClusters.exportDocuments', myCtx.collectionTreeItem, { + queryText: input.query, + source: 'webview;collectionView', + }); }), - importDocuments: publicProcedure.query(async ({ ctx }) => { + importDocuments: publicProcedure.use(trpcToTelemetry).query(({ ctx }) => { const myCtx = ctx as RouterContext; - vscode.commands.executeCommand('command.internal.mongoClusters.importDocuments', myCtx.collectionTreeItem); + vscode.commands.executeCommand('command.mongoClusters.importDocuments', myCtx.collectionTreeItem, null, { + source: 'webview;collectionView', + }); }), }); diff --git a/src/webviews/mongoClusters/collectionView/components/DataViewPanelTable.tsx b/src/webviews/mongoClusters/collectionView/components/DataViewPanelTable.tsx index 6855c831..6e45bc22 100644 --- a/src/webviews/mongoClusters/collectionView/components/DataViewPanelTable.tsx +++ b/src/webviews/mongoClusters/collectionView/components/DataViewPanelTable.tsx @@ -74,7 +74,7 @@ export function DataViewPanelTable({ liveHeaders, liveData }: Props): React.JSX. columnDefinitions={gridColumns} // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment dataset={liveData} - onReactGridCreated={() => console.log('Grid created')} + onReactGridCreated={() => console.debug('Grid created')} /> ); } diff --git a/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarMainView.tsx b/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarMainView.tsx index f9221a53..4a3c546c 100644 --- a/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarMainView.tsx +++ b/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarMainView.tsx @@ -20,6 +20,12 @@ export const ToolbarMainView = (): JSX.Element => { }; const ToolbarQueryOperations = (): JSX.Element => { + /** + * Use the `useTrpcClient` hook to get the tRPC client and an event target + * for handling notifications from the extension. + */ + const { trpcClient /** , vscodeEventTarget */ } = useTrpcClient(); + const [currentContext, setCurrentContext] = useContext(CollectionViewContext); const handleExecuteQuery = () => { @@ -38,6 +44,20 @@ const ToolbarQueryOperations = (): JSX.Element => { ...prev, currrentQueryDefinition: { ...prev.currrentQueryDefinition, queryText: queryContent, pageNumber: 1 }, })); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'executeQuery', + properties: { + ui: 'button', + }, + measurements: { + queryLenth: queryContent.length, + }, + }) + .catch((error) => { + console.debug('Failed to report query event:', error); + }); }; const handleRefreshResults = () => { @@ -46,6 +66,23 @@ const ToolbarQueryOperations = (): JSX.Element => { ...prev, currrentQueryDefinition: { ...prev.currrentQueryDefinition }, })); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'refreshResults', + properties: { + ui: 'button', + view: currentContext.currentView, + }, + measurements: { + page: currentContext.currrentQueryDefinition.pageNumber, + pageSize: currentContext.currrentQueryDefinition.pageSize, + queryLength: currentContext.currrentQueryDefinition.queryText.length, + }, + }) + .catch((error) => { + console.debug('Failed to report query event:', error); + }); }; return ( diff --git a/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarTableNavigation.tsx b/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarTableNavigation.tsx index 93a6eb6d..96d2906f 100644 --- a/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarTableNavigation.tsx +++ b/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarTableNavigation.tsx @@ -16,29 +16,68 @@ import { } from '@fluentui/react-components'; import { ArrowUp16Filled } from '@fluentui/react-icons'; import { useContext } from 'react'; +import { useTrpcClient } from '../../../../api/webview-client/useTrpcClient'; import { CollectionViewContext, Views } from '../../collectionViewContext'; export const ToolbarTableNavigation = (): JSX.Element => { + /** + * Use the `useTrpcClient` hook to get the tRPC client and an event target + * for handling notifications from the extension. + */ + const { trpcClient /** , vscodeEventTarget */ } = useTrpcClient(); + const [currentContext, setCurrentContext] = useContext(CollectionViewContext); function levelUp() { + const newPath = currentContext.currentViewState?.currentPath.slice(0, -1) ?? []; + setCurrentContext({ ...currentContext, currentViewState: { ...currentContext.currentViewState, - currentPath: currentContext.currentViewState?.currentPath.slice(0, -1) ?? [], + currentPath: newPath, }, }); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'stepIn', + properties: { + source: 'step-out-button', + }, + measurements: { + depth: newPath.length ?? 0, + }, + }) + .catch((_error) => { + console.debug(_error); + }); } function jumpToLevel(level: number) { + const newPath = currentContext.currentViewState?.currentPath.slice(0, level) ?? []; + setCurrentContext({ ...currentContext, currentViewState: { ...currentContext.currentViewState, - currentPath: currentContext.currentViewState?.currentPath.slice(0, level) ?? [], + currentPath: newPath, }, }); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'stepIn', + properties: { + source: 'breadcrumb', + }, + measurements: { + depth: newPath.length ?? 0, + }, + }) + .catch((_error) => { + console.debug(_error); + }); } type Item = { diff --git a/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarViewNavigation.tsx b/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarViewNavigation.tsx index 5b9dc836..1630e244 100644 --- a/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarViewNavigation.tsx +++ b/src/webviews/mongoClusters/collectionView/components/toolbar/ToolbarViewNavigation.tsx @@ -6,30 +6,75 @@ import { Dropdown, Label, Option, Toolbar, ToolbarButton, Tooltip } from '@fluentui/react-components'; import { ArrowLeftFilled, ArrowPreviousFilled, ArrowRightFilled } from '@fluentui/react-icons'; import { useContext } from 'react'; +import { useTrpcClient } from '../../../../api/webview-client/useTrpcClient'; import { CollectionViewContext } from '../../collectionViewContext'; import { ToolbarDividerTransparent } from './ToolbarDividerTransparent'; export const ToolbarViewNavigation = (): JSX.Element => { + /** + * Use the `useTrpcClient` hook to get the tRPC client and an event target + * for handling notifications from the extension. + */ + const { trpcClient /** , vscodeEventTarget */ } = useTrpcClient(); + const [currentContext, setCurrentContext] = useContext(CollectionViewContext); function goToNextPage() { + const newPage = currentContext.currrentQueryDefinition.pageNumber + 1; + setCurrentContext({ ...currentContext, currrentQueryDefinition: { ...currentContext.currrentQueryDefinition, - pageNumber: currentContext.currrentQueryDefinition.pageNumber + 1, + pageNumber: newPage, }, }); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'pagination', + properties: { + source: 'next-page', + ui: 'button', + view: currentContext.currentView, + }, + measurements: { + page: newPage, + pageSize: currentContext.currrentQueryDefinition.pageSize, + }, + }) + .catch((error) => { + console.debug('Failed to report pagination event:', error); + }); } function goToPreviousPage() { + const newPage = Math.max(1, currentContext.currrentQueryDefinition.pageNumber - 1); + setCurrentContext({ ...currentContext, currrentQueryDefinition: { ...currentContext.currrentQueryDefinition, - pageNumber: Math.max(1, currentContext.currrentQueryDefinition.pageNumber - 1), + pageNumber: newPage, }, }); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'pagination', + properties: { + source: 'prev-page', + ui: 'button', + view: currentContext.currentView, + }, + measurements: { + page: newPage, + pageSize: currentContext.currrentQueryDefinition.pageSize, + }, + }) + .catch((error) => { + console.debug('Failed to report pagination event:', error); + }); } function goToFirstPage() { @@ -37,6 +82,23 @@ export const ToolbarViewNavigation = (): JSX.Element => { ...currentContext, currrentQueryDefinition: { ...currentContext.currrentQueryDefinition, pageNumber: 1 }, }); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'pagination', + properties: { + source: 'first-page', + ui: 'button', + view: currentContext.currentView, + }, + measurements: { + page: 1, + pageSize: currentContext.currrentQueryDefinition.pageSize, + }, + }) + .catch((error) => { + console.debug('Failed to report pagination event:', error); + }); } function setPageSize(pageSize: number) { @@ -48,6 +110,23 @@ export const ToolbarViewNavigation = (): JSX.Element => { pageNumber: 1, }, }); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'pagination', + properties: { + source: 'page-size', + ui: 'button', + view: currentContext.currentView, + }, + measurements: { + page: currentContext.currrentQueryDefinition.pageNumber, + pageSize: pageSize, + }, + }) + .catch((error) => { + console.debug('Failed to report pagination event:', error); + }); } return ( diff --git a/src/webviews/mongoClusters/collectionView/components/toolbar/toolbarPaging.tsx b/src/webviews/mongoClusters/collectionView/components/toolbar/toolbarPaging.tsx deleted file mode 100644 index 16f55633..00000000 --- a/src/webviews/mongoClusters/collectionView/components/toolbar/toolbarPaging.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dropdown, Label, Option, Toolbar, ToolbarButton, ToolbarDivider, Tooltip } from '@fluentui/react-components'; -import { ArrowLeftFilled, ArrowPreviousFilled, ArrowRightFilled, ArrowUp16Filled } from '@fluentui/react-icons'; -import { useContext } from 'react'; -import { CollectionViewContext, Views } from '../../collectionViewContext'; -import { ToolbarDividerTransparent } from './ToolbarDividerTransparent'; - -export const ToolbarPaging = (): JSX.Element => { - const [currentContext, setCurrentContext] = useContext(CollectionViewContext); - - function goToNextPage() { - setCurrentContext({ - ...currentContext, - currrentQueryDefinition: { - ...currentContext.currrentQueryDefinition, - pageNumber: currentContext.currrentQueryDefinition.pageNumber + 1, - }, - }); - } - - function goToPreviousPage() { - setCurrentContext({ - ...currentContext, - currrentQueryDefinition: { - ...currentContext.currrentQueryDefinition, - pageNumber: Math.max(1, currentContext.currrentQueryDefinition.pageNumber - 1), - }, - }); - } - - function goToFirstPage() { - setCurrentContext({ - ...currentContext, - currrentQueryDefinition: { ...currentContext.currrentQueryDefinition, pageNumber: 1 }, - }); - } - - function setPageSize(pageSize: number) { - setCurrentContext({ - ...currentContext, - currrentQueryDefinition: { - ...currentContext.currrentQueryDefinition, - pageSize: pageSize, - pageNumber: 1, - }, - }); - } - - function levelUp() { - setCurrentContext({ - ...currentContext, - currentViewState: { - ...currentContext.currentViewState, - currentPath: currentContext.currentViewState?.currentPath.slice(0, -1) ?? [], - }, - }); - } - - // function refresh() { - // setCurrentContext({ - // ...currentContext - // }); - // } - - return ( - - - } - disabled={ - currentContext.currentView !== Views.TABLE || - currentContext.currentViewState?.currentPath.length === 0 - } - /> - - - - - - } - disabled={currentContext.isLoading} - /> - - - - } - disabled={currentContext.isLoading} - /> - - - - } - disabled={currentContext.isLoading} - /> - - - - - - { - setPageSize(parseInt(data.optionText ?? '10')); - }} - style={{ minWidth: '100px', maxWidth: '100px' }} - defaultValue="10" - defaultSelectedOptions={['10']} - > - - - - - - - - - - - - ); -}; diff --git a/src/webviews/mongoClusters/documentView/documentView.tsx b/src/webviews/mongoClusters/documentView/documentView.tsx index f6832327..e7473e61 100644 --- a/src/webviews/mongoClusters/documentView/documentView.tsx +++ b/src/webviews/mongoClusters/documentView/documentView.tsx @@ -172,14 +172,31 @@ export const DocumentView = (): JSX.Element => { setIsLoading(true); + let documentLength = 0; + void trpcClient.mongoClusters.documentView.getDocumentById .query(configuration.documentId) .then((response) => { + documentLength = response.length ?? 0; setContent(response); }) .finally(() => { setIsLoading(false); }); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'refreshDocument', + properties: { + ui: 'button', + }, + measurements: { + documentLength: documentLength, + }, + }) + .catch((error) => { + console.debug('Failed to report event:', error); + }); } function handleOnSaveRequest(): void { @@ -204,11 +221,25 @@ export const DocumentView = (): JSX.Element => { setIsDirty(false); }) .catch((error) => { - console.error('Error saving document:', error); + console.debug('Error saving document:', error); }) .finally(() => { setIsLoading(false); }); + + trpcClient.common.reportEvent + .mutate({ + eventName: 'saveDocument', + properties: { + ui: 'button', + }, + measurements: { + documentLength: editorContent.length, + }, + }) + .catch((error) => { + console.debug('Failed to report event:', error); + }); } function handleOnValidateRequest(): void {} diff --git a/src/webviews/mongoClusters/documentView/documentsViewController.ts b/src/webviews/mongoClusters/documentView/documentsViewController.ts index 7b1b4b5a..74f1d1e6 100644 --- a/src/webviews/mongoClusters/documentView/documentsViewController.ts +++ b/src/webviews/mongoClusters/documentView/documentsViewController.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ViewColumn } from 'vscode'; +import { API } from '../../../AzureDBExperiences'; import { ext } from '../../../extensionVariables'; import { WebviewController } from '../../api/extension-server/WebviewController'; import { type RouterContext } from './documentsViewRouter'; @@ -33,9 +34,11 @@ export class DocumentsViewController extends WebviewController