From 2dc76517ae647acffd1c6dfcd4fa68b40b36ea44 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Fri, 6 Dec 2024 14:29:09 +0100 Subject: [PATCH 1/3] fix(copilot): wrap collection name placeholder in angle brackets VSCODE-654 (#897) --- src/participant/prompts/exportToPlayground.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/participant/prompts/exportToPlayground.ts b/src/participant/prompts/exportToPlayground.ts index 4ab441087..4465a1f02 100644 --- a/src/participant/prompts/exportToPlayground.ts +++ b/src/participant/prompts/exportToPlayground.ts @@ -5,7 +5,7 @@ export class ExportToPlaygroundPrompt extends PromptBase { return `You are a MongoDB expert. Your task is to convert user's code written in any programming language to the MongoDB mongosh shell script. If the user's code contains a database and collection name, preserve them in the transpiled code, -otherwise use '' and 'YOUR_COLLECTION_NAME' placeholders. +otherwise use '' and '' placeholders. Example: User: From fabbc8cf15c767476d07eef76fe7a3a2c4eb529f Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 6 Dec 2024 16:32:17 +0100 Subject: [PATCH 2/3] feat(tree-explorer): add buttons to ask Copilot and create playgrounds from tree view VSCODE-651 (#890) Co-authored-by: Alena Khineika --- fonts/mongodb-icons.woff | Bin 1244 -> 1908 bytes images/icons/playground.svg | 1 + images/{dark => icons}/plus-circle.svg | 0 package.json | 72 +++++++++++++----- scripts/generate-icon-font.ts | 15 +++- src/commands/index.ts | 2 + src/editors/playgroundController.ts | 32 +++++++- .../playgroundSelectionCodeActionProvider.ts | 3 +- src/mdbExtensionController.ts | 34 ++++++++- src/participant/constants.ts | 1 + src/participant/participant.ts | 33 +++++++- ...laygroundFromCollectionTreeItemTemplate.ts | 15 ++++ .../playgroundFromDatabaseTreeItemTemplate.ts | 11 +++ src/templates/templateHelpers.ts | 10 +++ .../suite/participant/participant.test.ts | 69 +++++++++++++++++ 15 files changed, 268 insertions(+), 30 deletions(-) create mode 100644 images/icons/playground.svg rename images/{dark => icons}/plus-circle.svg (100%) create mode 100644 src/templates/playgroundFromCollectionTreeItemTemplate.ts create mode 100644 src/templates/playgroundFromDatabaseTreeItemTemplate.ts create mode 100644 src/templates/templateHelpers.ts diff --git a/fonts/mongodb-icons.woff b/fonts/mongodb-icons.woff index c12f6799b9969a9997c3059adb93b3cd4cb1513a..45d6e125479765cb70c11cb4a09c3ea3c01bf018 100644 GIT binary patch delta 1580 zcmXYxeK^x=7{`AzW0;YbD61Jlaxxs`oD@2ZT1vl-L<7OsAQHg*7jD1fya3>e@-3L?it39vT;>n{MH)UPsK8U}69O$%dp>iAO0=6ab)phU+Y093gMFoTIP_d!ev4gfUajRH7e^XRu(80qb=z zhtxn008r`VwZbge(1Y2&PNLl&53%6JC?TxzFvbW|pqK{5Q(&(hzR*s%xgTY|v?hU` z1RtjI6|PBvsoRjz(7_VM^MA#ryXx)L%HkQLa~;=j?`%=pMy9+`smxNVKTg;_ij;7> ziu+!yOe#3qhz&8@CBx$iw^4@$167>|gYN=2pLp6#Q2505rd2KMp%(dA?^vFnxvZ)0 z5K=hiEPEsoWfxix2l!))^TRrQrZroWE0(KtG<0X9Z#w3$e)e&>P@44CCiAh{698Zd z$sp825sJkR#o|NVE!@+m3CiyFR>U5=zDN*^sjK<|h%lZGtgg=vhb1^UIGb9cbk>3wN`OPfg?IW$RKhCJA$*>z6XU4mrR4^ACwb!JZEOi@jHEk(DkrvdjGx}Md8>h`;+oZXrZ?qAP4g)PWWrKT z)s9wYrX{L0%s;@M8Cl4H(%q=;i3276DQ)8h1-cl=;qid9g7mQfI$io^@?N#c9g_i>JAbxezCnLlo}6qj@ttwkhb6+~f0!3+^lWkWAxL*;}un&6PFOVB@kUnz)|7 zxTvV_i^ek>H|zpObVt?YBuom*rN9PcfuwwcBWhmUjofYzwB{ z=h{9GOLnMk>yX8X?JX-G7oB$&IA%V@sVq`^r-q-@m2qC#*r!Huf6QqV2m&*xOq195 zKaaC1Q_(z7tR(m%=u6X6Me1g$0_{V}(zSVp>v$lTmY;&*V=7%SfI$!rLBryDqY58!G&dMEzwa4M zn(}n(&9*nFeNovG_<3qy_kLbymFPTSsTLQyF(kg6SuxOgvfRRmUENXaqe!ZWz$3Bu Z-Tln+8e>szVg=A* zW&@y@JP<1|N;0VBCFZ6A#R7m1vH=?GpZa|6nu7ddpd(m%fNK1JxU}~0Dwc{l$q6Y5 z2@jGC5;i0!6cv8vOnh+KQ|DyzgrtPT28IbXMzamojcq43G9P0;Rll(zlTV?K>9P+` z4o{Uskc7rzu9h60Gdyn`3I+OwJvKAFZSE*wekvi!vT>Dj=;0Fwco@7ku-ye&l9-S- z=fHte2M!!Kdw^MG<^mITfz1s&)|}yqaCDfJ&2^Zu?MJu|kCTI*a2XwyL?x_h85E zYaUNxdn2O0JYn@5B~|vhr%QJHC_0?~wXY}1dv?ZV)eq)Y%YMGo_Fhv|wy3QBiu&Gv zO<%VZuGw^LBbW8@s{7VqpC3#~DiO)%;{GPTET{1etGJWQ5eA^EWzQVsYBmsHxiB{} zsbgE$0$#Z)tt-jACb571%fCJ*IpgkSVXxD%X^|YOi|M&HMJJk5BGo z;P~}$&GpM|JR$2}JoR~}a{rEsyWNJEyY)T#w`2RhW}BOE-fd9a;JluL_mWB9g%wNw zFxF1uP=dxK!=od!JFOr8%-v^HV9a5d!2E}&$zf7+Bj*uDhIQF2wm`>$<6Iyqp<(hL zo*teR4eSi8g$zz0aRw$u4K*eP2G%bOst~>?o4yYRgZwvU&I&nbVo3xhjx+{FHHBwP zPHb)e6YQ87cw!kHHt%M&XY{K*Va?TGAi!{O?vLvc9_Jp;RA~{(ESo*yv-XV$%d-cB z)&Ez@$l5Y13mbE5OV5k#OIc-kHL2=Yx^8>FNNsP+!W6C6ZV%y|jP1cfj~N&MB(qJ) diff --git a/images/icons/playground.svg b/images/icons/playground.svg new file mode 100644 index 000000000..f38f28a70 --- /dev/null +++ b/images/icons/playground.svg @@ -0,0 +1 @@ + diff --git a/images/dark/plus-circle.svg b/images/icons/plus-circle.svg similarity index 100% rename from images/dark/plus-circle.svg rename to images/icons/plus-circle.svg diff --git a/package.json b/package.json index f147a1d2a..f4676cdbe 100644 --- a/package.json +++ b/package.json @@ -255,6 +255,11 @@ "dark": "images/dark/add.svg" } }, + { + "command": "mdb.createNewPlaygroundFromTreeItem", + "title": "Create MongoDB Playground", + "icon": "$(mdb-playground)" + }, { "command": "mdb.changeActiveConnection", "title": "MongoDB: Change Active Connection" @@ -330,10 +335,7 @@ { "command": "mdb.addDatabase", "title": "Add Database...", - "icon": { - "light": "images/light/plus-circle.svg", - "dark": "images/dark/plus-circle.svg" - } + "icon": "$(mdb-plus-circle)" }, { "command": "mdb.searchForDocuments", @@ -371,13 +373,15 @@ "command": "mdb.refreshDatabase", "title": "Refresh" }, + { + "command": "mdb.askCopilotFromTreeItem", + "title": "Ask MongoDB Copilot", + "icon": "$(copilot)" + }, { "command": "mdb.addCollection", "title": "Add Collection...", - "icon": { - "light": "images/light/plus-circle.svg", - "dark": "images/dark/plus-circle.svg" - } + "icon": "$(mdb-plus-circle)" }, { "command": "mdb.viewCollectionDocuments", @@ -422,10 +426,7 @@ { "command": "mdb.createIndexFromTreeView", "title": "Create New Index...", - "icon": { - "light": "images/light/plus-circle.svg", - "dark": "images/dark/plus-circle.svg" - } + "icon": "$(mdb-plus-circle)" }, { "command": "mdb.insertObjectIdToEditor", @@ -454,10 +455,7 @@ { "command": "mdb.addStreamProcessor", "title": "Add StreamProcessor...", - "icon": { - "light": "images/light/plus-circle.svg", - "dark": "images/dark/plus-circle.svg" - } + "icon": "$(mdb-plus-circle)" }, { "command": "mdb.startStreamProcessor", @@ -587,7 +585,7 @@ { "command": "mdb.addCollection", "when": "view == mongoDBConnectionExplorer && viewItem == databaseTreeItem", - "group": "inline" + "group": "inline@3" }, { "command": "mdb.addCollection", @@ -604,10 +602,30 @@ "when": "view == mongoDBConnectionExplorer && viewItem == databaseTreeItem", "group": "2@1" }, + { + "command": "mdb.askCopilotFromTreeItem", + "when": "mdb.isCopilotActive == true && view == mongoDBConnectionExplorer && (viewItem == databaseTreeItem || viewItem == collectionTreeItem)", + "group": "inline@1" + }, + { + "command": "mdb.askCopilotFromTreeItem", + "when": "mdb.isCopilotActive == true && view == mongoDBConnectionExplorer && (viewItem == databaseTreeItem || viewItem == collectionTreeItem)", + "group": "3@1" + }, + { + "command": "mdb.createNewPlaygroundFromTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == databaseTreeItem || viewItem == collectionTreeItem)", + "group": "inline@2" + }, + { + "command": "mdb.createNewPlaygroundFromTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == databaseTreeItem || viewItem == collectionTreeItem)", + "group": "3@2" + }, { "command": "mdb.dropDatabase", "when": "view == mongoDBConnectionExplorer && viewItem == databaseTreeItem", - "group": "3@1" + "group": "4@1" }, { "command": "mdb.viewCollectionDocuments", @@ -1157,19 +1175,33 @@ } }, "icons": { - "mdb-connection-active": { + "mdb-playground": { "description": "MongoDB Icon", "default": { "fontPath": "./fonts/mongodb-icons.woff", "fontCharacter": "\\ea01" } }, - "mdb-connection-inactive": { + "mdb-plus-circle": { "description": "MongoDB Icon", "default": { "fontPath": "./fonts/mongodb-icons.woff", "fontCharacter": "\\ea02" } + }, + "mdb-connection-active": { + "description": "MongoDB Icon", + "default": { + "fontPath": "./fonts/mongodb-icons.woff", + "fontCharacter": "\\ea03" + } + }, + "mdb-connection-inactive": { + "description": "MongoDB Icon", + "default": { + "fontPath": "./fonts/mongodb-icons.woff", + "fontCharacter": "\\ea04" + } } } }, diff --git a/scripts/generate-icon-font.ts b/scripts/generate-icon-font.ts index 089dbf8d1..79ad286c9 100644 --- a/scripts/generate-icon-font.ts +++ b/scripts/generate-icon-font.ts @@ -4,7 +4,12 @@ import { GlyphData } from 'webfont/dist/src/types'; import prettier from 'prettier'; /** Icons to include in the generated icon font */ -const INCLUDED_ICONS = ['connection-active', 'connection-inactive']; +const INCLUDED_ICONS = [ + 'light/connection-active', + 'light/connection-inactive', + 'playground', + 'plus-circle', +]; /** * Generates an icon font from the included icons and outputs package.json @@ -13,7 +18,13 @@ const INCLUDED_ICONS = ['connection-active', 'connection-inactive']; */ async function main(): Promise { const font = await webfont({ - files: INCLUDED_ICONS.map((icon) => `./images/light/${icon}.svg`), + files: INCLUDED_ICONS.map((icon) => { + // Legacy support for icons inside light and dark folders. + if (icon.startsWith('light/')) { + return `./images/${icon}.svg`; + } + return `./images/icons/${icon}.svg`; + }), fontName: 'MongoDB Icons', formats: ['woff'], normalize: true, diff --git a/src/commands/index.ts b/src/commands/index.ts index a5fe02aa5..deca325c6 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -39,6 +39,7 @@ enum EXTENSION_COMMANDS { MDB_OPEN_PLAYGROUND_FROM_TREE_VIEW = 'mdb.openPlaygroundFromTreeView', MDB_CONNECT_TO_CONNECTION_TREE_VIEW = 'mdb.connectToConnectionTreeItem', MDB_CREATE_PLAYGROUND_FROM_TREE_VIEW = 'mdb.createNewPlaygroundFromTreeView', + MDB_CREATE_PLAYGROUND_FROM_TREE_ITEM = 'mdb.createNewPlaygroundFromTreeItem', MDB_DISCONNECT_FROM_CONNECTION_TREE_VIEW = 'mdb.disconnectFromConnectionTreeItem', MDB_EDIT_CONNECTION = 'mdb.editConnection', MDB_REFRESH_CONNECTION = 'mdb.refreshConnection', @@ -75,6 +76,7 @@ enum EXTENSION_COMMANDS { OPEN_PARTICIPANT_CODE_IN_PLAYGROUND = 'mdb.openParticipantCodeInPlayground', SEND_MESSAGE_TO_PARTICIPANT = 'mdb.sendMessageToParticipant', SEND_MESSAGE_TO_PARTICIPANT_FROM_INPUT = 'mdb.sendMessageToParticipantFromInput', + ASK_COPILOT_FROM_TREE_ITEM = 'mdb.askCopilotFromTreeItem', RUN_PARTICIPANT_CODE = 'mdb.runParticipantCode', CONNECT_WITH_PARTICIPANT = 'mdb.connectWithParticipant', SELECT_DATABASE_WITH_PARTICIPANT = 'mdb.selectDatabaseWithParticipant', diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index da4632294..7f2c2969d 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -8,6 +8,7 @@ import type ConnectionController from '../connectionController'; import { DataServiceEventTypes } from '../connectionController'; import { createLogger } from '../logging'; import type { ConnectionTreeItem } from '../explorer'; +import { CollectionTreeItem } from '../explorer'; import { DatabaseTreeItem } from '../explorer'; import formatError from '../utils/formatError'; import type { LanguageServerController } from '../language'; @@ -41,6 +42,8 @@ import { getPlaygroundExtensionForTelemetry, } from '../utils/playground'; import type ExportToLanguageCodeLensProvider from './exportToLanguageCodeLensProvider'; +import { playgroundFromDatabaseTreeItemTemplate } from '../templates/playgroundFromDatabaseTreeItemTemplate'; +import { playgroundFromCollectionTreeItemTemplate } from '../templates/playgroundFromCollectionTreeItemTemplate'; const log = createLogger('playground controller'); @@ -316,13 +319,36 @@ export default class PlaygroundController { return this._createPlaygroundFileWithContent(content); } + async createPlaygroundFromTreeItem( + treeItem: DatabaseTreeItem | CollectionTreeItem + ): Promise { + let content = ''; + if (treeItem instanceof DatabaseTreeItem) { + content = playgroundFromDatabaseTreeItemTemplate(treeItem.databaseName); + this._telemetryService.trackPlaygroundCreated('fromDatabaseTreeItem'); + } else if (treeItem instanceof CollectionTreeItem) { + content = playgroundFromCollectionTreeItemTemplate( + treeItem.databaseName, + treeItem.collectionName + ); + this._telemetryService.trackPlaygroundCreated('fromCollectionTreeItem'); + } + + return this._createPlaygroundFileWithContent(content); + } + async createPlayground(): Promise { const useDefaultTemplate = !!vscode.workspace .getConfiguration('mdb') .get('useDefaultTemplateForPlayground'); - const isStreams = this._connectionController.isConnectedToAtlasStreams(); - const template = isStreams ? playgroundStreamsTemplate : playgroundTemplate; - const content = useDefaultTemplate ? template : ''; + let content = ''; + if (useDefaultTemplate) { + const isStreams = this._connectionController.isConnectedToAtlasStreams(); + const template = isStreams + ? playgroundStreamsTemplate + : playgroundTemplate; + content = template; + } this._telemetryService.trackPlaygroundCreated('crud'); return this._createPlaygroundFileWithContent(content); diff --git a/src/editors/playgroundSelectionCodeActionProvider.ts b/src/editors/playgroundSelectionCodeActionProvider.ts index 578e68ce4..d943ddaba 100644 --- a/src/editors/playgroundSelectionCodeActionProvider.ts +++ b/src/editors/playgroundSelectionCodeActionProvider.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import EXTENSION_COMMANDS from '../commands'; import { isPlayground, getSelectedText } from '../utils/playground'; +import { COPILOT_CHAT_EXTENSION_ID } from '../participant/constants'; export const EXPORT_TO_LANGUAGE_ALIASES = [ { id: 'csharp', alias: 'C#' }, @@ -42,7 +43,7 @@ export default class PlaygroundSelectionCodeActionProvider provideCodeActions(): vscode.CodeAction[] | undefined { const editor = vscode.window.activeTextEditor; - const copilot = vscode.extensions.getExtension('github.copilot-chat'); + const copilot = vscode.extensions.getExtension(COPILOT_CHAT_EXTENSION_ID); let codeActions: vscode.CodeAction[] = [ this.createCodeAction({ title: 'Run selected playground blocks', diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index 43213a662..c7c5f139e 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -51,6 +51,10 @@ import type { SendMessageToParticipantOptions, SendMessageToParticipantFromInputOptions, } from './participant/participantTypes'; +import { + COPILOT_CHAT_EXTENSION_ID, + COPILOT_EXTENSION_ID, +} from './participant/constants'; // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. @@ -177,12 +181,26 @@ export default class MDBExtensionController implements vscode.Disposable { // ------ In-app notifications ------ // void this.showCopilotIntroductionForEstablishedUsers(); - const copilot = vscode.extensions.getExtension('GitHub.copilot'); + const copilot = vscode.extensions.getExtension(COPILOT_EXTENSION_ID); void vscode.commands.executeCommand( 'setContext', 'mdb.isCopilotActive', copilot?.isActive ); + + // TODO: This is a workaround related to https://github.com/microsoft/vscode/issues/234426 + // If the extension was found but is not activated, there is a chance that the MongoDB extension + // was activated before the Copilot one, so we check again after a delay. + if (copilot && !copilot?.isActive) { + setTimeout(() => { + const copilot = vscode.extensions.getExtension(COPILOT_EXTENSION_ID); + void vscode.commands.executeCommand( + 'setContext', + 'mdb.isCopilotActive', + copilot?.isActive === true + ); + }, 3000); + } } registerCommands = (): void => { @@ -330,6 +348,13 @@ export default class MDBExtensionController implements vscode.Disposable { return true; } ); + this.registerParticipantCommand( + EXTENSION_COMMANDS.ASK_COPILOT_FROM_TREE_ITEM, + async (treeItem: DatabaseTreeItem | CollectionTreeItem) => { + await this._participantController.askCopilotFromTreeItem(treeItem); + return true; + } + ); this.registerParticipantCommand( EXTENSION_COMMANDS.RUN_PARTICIPANT_CODE, ({ runnableContent }: RunParticipantCodeCommandArgs) => { @@ -742,6 +767,11 @@ export default class MDBExtensionController implements vscode.Disposable { EXTENSION_COMMANDS.MDB_CREATE_PLAYGROUND_FROM_TREE_VIEW, () => this._playgroundController.createPlayground() ); + this.registerCommand( + EXTENSION_COMMANDS.MDB_CREATE_PLAYGROUND_FROM_TREE_ITEM, + (treeItem: DatabaseTreeItem | CollectionTreeItem) => + this._playgroundController.createPlaygroundFromTreeItem(treeItem) + ); this.registerCommand( EXTENSION_COMMANDS.MDB_REFRESH_PLAYGROUNDS_FROM_TREE_VIEW, () => this._playgroundsExplorer.refresh() @@ -972,7 +1002,7 @@ export default class MDBExtensionController implements vscode.Disposable { } ); - const copilot = vscode.extensions.getExtension('github.copilot-chat'); + const copilot = vscode.extensions.getExtension(COPILOT_CHAT_EXTENSION_ID); if (result?.title === action) { await this._participantController.sendMessageToParticipant({ message: '', diff --git a/src/participant/constants.ts b/src/participant/constants.ts index 1aeec079e..86511f823 100644 --- a/src/participant/constants.ts +++ b/src/participant/constants.ts @@ -4,6 +4,7 @@ import { ChatMetadataStore } from './chatMetadata'; export const CHAT_PARTICIPANT_ID = 'mongodb.participant'; export const CHAT_PARTICIPANT_MODEL = 'gpt-4o'; export const COPILOT_EXTENSION_ID = 'GitHub.copilot'; +export const COPILOT_CHAT_EXTENSION_ID = 'GitHub.copilot-chat'; export type ParticipantResponseType = | 'query' diff --git a/src/participant/participant.ts b/src/participant/participant.ts index f39cec2bc..5d98dd543 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -55,6 +55,7 @@ import type { } from './participantTypes'; import { DEFAULT_EXPORT_TO_LANGUAGE_DRIVER_SYNTAX } from '../editors/exportToLanguageCodeLensProvider'; import { EXPORT_TO_LANGUAGE_ALIASES } from '../editors/playgroundSelectionCodeActionProvider'; +import { CollectionTreeItem, DatabaseTreeItem } from '../explorer'; const log = createLogger('participant'); @@ -137,7 +138,12 @@ export default class ParticipantController { async sendMessageToParticipant( options: SendMessageToParticipantOptions ): Promise { - const { message, isNewChat = false, isPartialQuery = false } = options; + const { + message, + isNewChat = false, + isPartialQuery = false, + ...otherOptions + } = options; if (isNewChat) { await vscode.commands.executeCommand('workbench.action.chat.newChat'); @@ -146,7 +152,8 @@ export default class ParticipantController { ); } - return vscode.commands.executeCommand('workbench.action.chat.open', { + return await vscode.commands.executeCommand('workbench.action.chat.open', { + ...otherOptions, query: `@MongoDB ${message}`, isPartialQuery, }); @@ -182,6 +189,28 @@ export default class ParticipantController { }); } + async askCopilotFromTreeItem( + treeItem: DatabaseTreeItem | CollectionTreeItem + ): Promise { + if (treeItem instanceof DatabaseTreeItem) { + const { databaseName } = treeItem; + + await this.sendMessageToParticipant({ + message: `I want to ask questions about the \`${databaseName}\` database.`, + isNewChat: true, + }); + } else if (treeItem instanceof CollectionTreeItem) { + const { databaseName, collectionName } = treeItem; + + await this.sendMessageToParticipant({ + message: `I want to ask questions about the \`${databaseName}\` database's \`${collectionName}\` collection.`, + isNewChat: true, + }); + } else { + throw new Error('Unsupported tree item type'); + } + } + async _getChatResponse({ modelInput, token, diff --git a/src/templates/playgroundFromCollectionTreeItemTemplate.ts b/src/templates/playgroundFromCollectionTreeItemTemplate.ts new file mode 100644 index 000000000..35c335887 --- /dev/null +++ b/src/templates/playgroundFromCollectionTreeItemTemplate.ts @@ -0,0 +1,15 @@ +import { createTemplate } from './templateHelpers'; + +export const playgroundFromCollectionTreeItemTemplate = createTemplate( + (databaseName, collectionName) => `// MongoDB Playground +// Use Ctrl+Space inside a snippet or a string literal to trigger completions. + +// The current database to use. +use(${databaseName}); + +// Find a document in a collection. +db.getCollection(${collectionName}).findOne({ + +}); +` +); diff --git a/src/templates/playgroundFromDatabaseTreeItemTemplate.ts b/src/templates/playgroundFromDatabaseTreeItemTemplate.ts new file mode 100644 index 000000000..0a5c57941 --- /dev/null +++ b/src/templates/playgroundFromDatabaseTreeItemTemplate.ts @@ -0,0 +1,11 @@ +import { createTemplate } from './templateHelpers'; + +export const playgroundFromDatabaseTreeItemTemplate = createTemplate( + (currentDatabase) => `// MongoDB Playground +// Use Ctrl+Space inside a snippet or a string literal to trigger completions. + +// The current database to use. +use(${currentDatabase}); + +` +); diff --git a/src/templates/templateHelpers.ts b/src/templates/templateHelpers.ts new file mode 100644 index 000000000..185464a32 --- /dev/null +++ b/src/templates/templateHelpers.ts @@ -0,0 +1,10 @@ +/** Wraps a template function and escapes given string arguments. */ +export function createTemplate string>( + templateBuilder: T +): (...args: Parameters) => string { + return (...args: Parameters) => { + const escapedArgs = args.map((arg) => JSON.stringify(arg)); + + return templateBuilder(...escapedArgs); + }; +} diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index ce89bf105..1263f0e6e 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -42,6 +42,8 @@ import { } from './participantHelpers'; import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensProvider'; import PlaygroundResultProvider from '../../../editors/playgroundResultProvider'; +import { CollectionTreeItem, DatabaseTreeItem } from '../../../explorer'; +import type { SendMessageToParticipantOptions } from '../../../participant/participantTypes'; // The Copilot's model in not available in tests, // therefore we need to mock its methods and returning values. @@ -1802,6 +1804,73 @@ Schema: }); }); + suite('opened from tree view', function () { + let sendMessageToParticipantStub: SinonStub< + [options: SendMessageToParticipantOptions], + Promise + >; + + beforeEach(function () { + sendMessageToParticipantStub = sinon.stub( + testParticipantController, + 'sendMessageToParticipant' + ); + }); + + suite('with a database item', function () { + const mockDatabaseItem = Object.assign( + Object.create(DatabaseTreeItem.prototype), + { + databaseName: 'testDb', + } as DatabaseTreeItem + ); + + test('opens the chat and sends a message to set database context', async function () { + expect(sendMessageToParticipantStub).not.called; + + await testParticipantController.askCopilotFromTreeItem( + mockDatabaseItem + ); + + expect(sendMessageToParticipantStub).has.callCount(1); + + expect(sendMessageToParticipantStub.getCall(0).args).deep.equals([ + { + message: `I want to ask questions about the \`${mockDatabaseItem.databaseName}\` database.`, + isNewChat: true, + }, + ]); + }); + }); + + suite('with a collection item', function () { + const mockCollectionItem = Object.assign( + Object.create(CollectionTreeItem.prototype), + { + databaseName: 'testDb', + collectionName: 'testColl', + } as CollectionTreeItem + ); + + test('opens the chat and sends a message to set database and collection context', async function () { + expect(sendMessageToParticipantStub).not.called; + + await testParticipantController.askCopilotFromTreeItem( + mockCollectionItem + ); + + expect(sendMessageToParticipantStub).has.callCount(1); + + expect(sendMessageToParticipantStub.getCall(0).args).deep.equals([ + { + message: `I want to ask questions about the \`${mockCollectionItem.databaseName}\` database's \`${mockCollectionItem.collectionName}\` collection.`, + isNewChat: true, + }, + ]); + }); + }); + }); + suite('determining the namespace', function () { ['query', 'schema'].forEach(function (command) { suite(`${command} command`, function () { From 7879cf959a22f56aced40dfa2afcdceff71e85b2 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Mon, 9 Dec 2024 15:25:08 +0100 Subject: [PATCH 3/3] feat(tree-explorer): add treeview telemetry and refactor to use a more standardized format VSCODE-651 (#891) Co-authored-by: Alena Khineika --- src/commands/index.ts | 2 + src/documentSource.ts | 2 +- .../queryWithCopilotCodeLensProvider.ts | 6 +- src/mdbExtensionController.ts | 13 ++- src/participant/participant.ts | 84 +++++++++++++------ src/participant/participantTypes.ts | 34 +++++++- src/participant/prompts/promptBase.ts | 3 +- src/participant/prompts/promptHistory.ts | 3 +- src/telemetry/telemetryService.ts | 76 +++++++++++++---- .../suite/participant/participant.test.ts | 15 +++- .../suite/participant/participantHelpers.ts | 2 +- 11 files changed, 177 insertions(+), 63 deletions(-) diff --git a/src/commands/index.ts b/src/commands/index.ts index deca325c6..b1a23606c 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -85,4 +85,6 @@ enum EXTENSION_COMMANDS { SHOW_EXPORT_TO_LANGUAGE_RESULT = 'mdb.showExportToLanguageResult', } +export type ExtensionCommand = EXTENSION_COMMANDS; + export default EXTENSION_COMMANDS; diff --git a/src/documentSource.ts b/src/documentSource.ts index 4c110ca04..329032a86 100644 --- a/src/documentSource.ts +++ b/src/documentSource.ts @@ -2,5 +2,5 @@ export enum DocumentSource { DOCUMENT_SOURCE_TREEVIEW = 'treeview', DOCUMENT_SOURCE_PLAYGROUND = 'playground', DOCUMENT_SOURCE_COLLECTIONVIEW = 'collectionview', - DOCUMENT_SOURCE_QUERY_WITH_COPILOT_CODELENS = 'query with copilot codelens', + DOCUMENT_SOURCE_CODELENS = 'codelens', } diff --git a/src/editors/queryWithCopilotCodeLensProvider.ts b/src/editors/queryWithCopilotCodeLensProvider.ts index 23b3958a4..993d718a7 100644 --- a/src/editors/queryWithCopilotCodeLensProvider.ts +++ b/src/editors/queryWithCopilotCodeLensProvider.ts @@ -31,9 +31,11 @@ export class QueryWithCopilotCodeLensProvider prompt: 'Describe the query you would like to generate', placeHolder: 'e.g. Find the document in sample_mflix.users with the name of Kayden Washington', - messagePrefix: '/query', + command: 'query', isNewChat: true, - source: DocumentSource.DOCUMENT_SOURCE_QUERY_WITH_COPILOT_CODELENS, + telemetry: { + source: DocumentSource.DOCUMENT_SOURCE_CODELENS, + }, }; return [ diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index c7c5f139e..e3a7b831e 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -25,7 +25,7 @@ import { } from './explorer'; import ExportToLanguageCodeLensProvider from './editors/exportToLanguageCodeLensProvider'; import { type ExportToLanguageResult } from './types/playgroundType'; -import EXTENSION_COMMANDS from './commands'; +import type { ExtensionCommand } from './commands'; import type FieldTreeItem from './explorer/fieldTreeItem'; import type IndexListTreeItem from './explorer/indexListTreeItem'; import { LanguageServerController } from './language'; @@ -40,21 +40,20 @@ import WebviewController from './views/webviewController'; import { createIdFactory, generateId } from './utils/objectIdHelper'; import { ConnectionStorage } from './storage/connectionStorage'; import type StreamProcessorTreeItem from './explorer/streamProcessorTreeItem'; -import type { - ParticipantCommand, - RunParticipantCodeCommandArgs, -} from './participant/participant'; +import type { RunParticipantCodeCommandArgs } from './participant/participant'; import ParticipantController from './participant/participant'; import type { OpenSchemaCommandArgs } from './participant/prompts/schema'; import { QueryWithCopilotCodeLensProvider } from './editors/queryWithCopilotCodeLensProvider'; import type { SendMessageToParticipantOptions, SendMessageToParticipantFromInputOptions, + ParticipantCommand, } from './participant/participantTypes'; import { COPILOT_CHAT_EXTENSION_ID, COPILOT_EXTENSION_ID, } from './participant/constants'; +import EXTENSION_COMMANDS from './commands'; // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. @@ -402,7 +401,7 @@ export default class MDBExtensionController implements vscode.Disposable { }; registerParticipantCommand = ( - command: string, + command: ExtensionCommand, commandHandler: (...args: any[]) => Promise ): void => { const commandHandlerWithTelemetry = (args: any[]): Promise => { @@ -420,7 +419,7 @@ export default class MDBExtensionController implements vscode.Disposable { }; registerCommand = ( - command: string, + command: ExtensionCommand, commandHandler: (...args: any[]) => Promise ): void => { const commandHandlerWithTelemetry = (args: any[]): Promise => { diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 5d98dd543..4f336f1a2 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -52,10 +52,13 @@ import { PromptHistory } from './prompts/promptHistory'; import type { SendMessageToParticipantOptions, SendMessageToParticipantFromInputOptions, + ParticipantCommand, + ParticipantCommandType, } from './participantTypes'; import { DEFAULT_EXPORT_TO_LANGUAGE_DRIVER_SYNTAX } from '../editors/exportToLanguageCodeLensProvider'; import { EXPORT_TO_LANGUAGE_ALIASES } from '../editors/playgroundSelectionCodeActionProvider'; import { CollectionTreeItem, DatabaseTreeItem } from '../explorer'; +import { DocumentSource } from '../documentSource'; const log = createLogger('participant'); @@ -72,8 +75,6 @@ export type RunParticipantCodeCommandArgs = { runnableContent: string; }; -export type ParticipantCommand = '/query' | '/schema' | '/docs'; - const MAX_MARKDOWN_LIST_LENGTH = 10; export default class ParticipantController { @@ -121,6 +122,7 @@ export default class ParticipantController { participantId: this._participant?.id, }); this._participant.onDidReceiveFeedback(this.handleUserFeedback.bind(this)); + return this._participant; } @@ -142,6 +144,8 @@ export default class ParticipantController { message, isNewChat = false, isPartialQuery = false, + telemetry, + command, ...otherOptions } = options; @@ -151,10 +155,28 @@ export default class ParticipantController { 'workbench.action.chat.clearHistory' ); } + const commandPrefix = command ? `/${command} ` : ''; + const query = `@MongoDB ${commandPrefix}${message}`; + + if (telemetry) { + if (isNewChat) { + this._telemetryService.trackParticipantChatOpenedFromAction({ + ...telemetry, + command, + }); + } + if (!isPartialQuery) { + this._telemetryService.trackParticipantPromptSubmittedFromAction({ + ...telemetry, + command: command ?? 'generic', + input_length: query.length, + }); + } + } return await vscode.commands.executeCommand('workbench.action.chat.open', { ...otherOptions, - query: `@MongoDB ${message}`, + query, isPartialQuery, }); } @@ -163,27 +185,34 @@ export default class ParticipantController { options: SendMessageToParticipantFromInputOptions ): Promise { const { - messagePrefix = '', - isNewChat = false, - isPartialQuery = false, - source, + isNewChat, + isPartialQuery, + telemetry, + command, ...inputBoxOptions } = options; - this._telemetryService.trackCopilotParticipantSubmittedFromInputBox({ - source, - }); - const message = await vscode.window.showInputBox({ ...inputBoxOptions, }); + if (telemetry) { + this._telemetryService.trackParticipantInputBoxSubmitted({ + ...telemetry, + input_length: message?.length, + dismissed: message === undefined, + command, + }); + } + if (message === undefined || message.trim() === '') { return Promise.resolve(); } return this.sendMessageToParticipant({ - message: `${messagePrefix ? `${messagePrefix} ` : ''}${message}`, + message, + telemetry, + command, isNewChat, isPartialQuery, }); @@ -198,6 +227,10 @@ export default class ParticipantController { await this.sendMessageToParticipant({ message: `I want to ask questions about the \`${databaseName}\` database.`, isNewChat: true, + telemetry: { + source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + source_details: 'database', + }, }); } else if (treeItem instanceof CollectionTreeItem) { const { databaseName, collectionName } = treeItem; @@ -205,6 +238,10 @@ export default class ParticipantController { await this.sendMessageToParticipant({ message: `I want to ask questions about the \`${databaseName}\` database's \`${collectionName}\` collection.`, isNewChat: true, + telemetry: { + source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + source_details: 'collection', + }, }); } else { throw new Error('Unsupported tree item type'); @@ -233,7 +270,7 @@ export default class ParticipantController { }) ), }); - this._telemetryService.trackCopilotParticipantPrompt(modelInput.stats); + this._telemetryService.trackParticipantPrompt(modelInput.stats); const modelResponse = await model.sendRequest( modelInput.messages, @@ -413,7 +450,7 @@ export default class ParticipantController { stream, }); - this._telemetryService.trackCopilotParticipantResponse({ + this._telemetryService.trackParticipantResponse({ command: 'generic', has_cta: false, found_namespace: false, @@ -1380,7 +1417,7 @@ export default class ParticipantController { ], }); - this._telemetryService.trackCopilotParticipantResponse({ + this._telemetryService.trackParticipantResponse({ command: 'schema', has_cta: true, found_namespace: true, @@ -1491,7 +1528,7 @@ export default class ParticipantController { token, }); - this._telemetryService.trackCopilotParticipantResponse({ + this._telemetryService.trackParticipantResponse({ command: 'query', has_cta: false, found_namespace: true, @@ -1597,7 +1634,7 @@ export default class ParticipantController { this._streamGenericDocsLink(stream); - this._telemetryService.trackCopilotParticipantResponse({ + this._telemetryService.trackParticipantResponse({ command: 'docs/copilot', has_cta: true, found_namespace: false, @@ -1677,7 +1714,7 @@ export default class ParticipantController { } } - this._telemetryService.trackCopilotParticipantResponse({ + this._telemetryService.trackParticipantResponse({ command: 'docs/chatbot', has_cta: !!docsResult.responseReferences, found_namespace: false, @@ -1795,10 +1832,7 @@ export default class ParticipantController { return true; } catch (error) { const message = formatError(error).message; - this._telemetryService.trackCopilotParticipantError( - error, - 'exportToPlayground' - ); + this._telemetryService.trackParticipantError(error, 'exportToPlayground'); void vscode.window.showErrorMessage( `An error occurred exporting to a playground: ${message}` ); @@ -1905,9 +1939,9 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i return await this.handleGenericRequest(...args); } } catch (error) { - this._telemetryService.trackCopilotParticipantError( + this._telemetryService.trackParticipantError( error, - request.command || 'generic' + (request.command as ParticipantCommandType) || 'generic' ); // Re-throw other errors so they show up in the UI. throw error; @@ -1956,7 +1990,7 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i 'unhelpfulReason' in feedback ? (feedback.unhelpfulReason as string) : undefined; - this._telemetryService.trackCopilotParticipantFeedback({ + this._telemetryService.trackParticipantFeedback({ feedback: chatResultFeedbackKindToTelemetryValue(feedback.kind), reason: unhelpfulReason, response_type: (feedback.result as ChatResult)?.metadata.intent, diff --git a/src/participant/participantTypes.ts b/src/participant/participantTypes.ts index 3d271b2a1..b8fdc257f 100644 --- a/src/participant/participantTypes.ts +++ b/src/participant/participantTypes.ts @@ -1,14 +1,40 @@ import type * as vscode from 'vscode'; import type { DocumentSource } from '../documentSource'; +export type ParticipantCommandType = 'query' | 'schema' | 'docs'; +export type ParticipantCommand = `/${ParticipantCommandType}`; + +export type ParticipantRequestType = ParticipantCommandType | 'generic'; + +export type ParticipantResponseType = + | 'query' + | 'schema' + | 'docs' + | 'docs/chatbot' + | 'docs/copilot' + | 'exportToPlayground' + | 'generic' + | 'emptyRequest' + | 'cancelledRequest' + | 'askToConnect' + | 'askForNamespace'; + +type TelemetryMetadata = { + source: DocumentSource; + source_details?: 'database' | 'collection'; +}; + +/** Based on options from Copilot's chat open command IChatViewOpenOptions */ export type SendMessageToParticipantOptions = { message: string; + command?: ParticipantCommandType; isNewChat?: boolean; isPartialQuery?: boolean; + telemetry?: TelemetryMetadata; }; -export type SendMessageToParticipantFromInputOptions = { - messagePrefix?: string; - source?: DocumentSource; -} & Omit & +export type SendMessageToParticipantFromInputOptions = Pick< + SendMessageToParticipantOptions, + 'isNewChat' | 'isPartialQuery' | 'command' | 'telemetry' +> & vscode.InputBoxOptions; diff --git a/src/participant/prompts/promptBase.ts b/src/participant/prompts/promptBase.ts index 0df0d1b38..56ed32f67 100644 --- a/src/participant/prompts/promptBase.ts +++ b/src/participant/prompts/promptBase.ts @@ -5,6 +5,7 @@ import type { ParticipantPromptProperties, } from '../../telemetry/telemetryService'; import { PromptHistory } from './promptHistory'; +import type { ParticipantCommandType } from '../participantTypes'; export interface PromptArgsBase { request: { @@ -175,7 +176,7 @@ export abstract class PromptBase { ), user_input_length: request.prompt.length, has_sample_documents: hasSampleDocs, - command: request.command || 'generic', + command: (request.command as ParticipantCommandType) || 'generic', history_size: context?.history.length || 0, internal_purpose: this.internalPurposeForTelemetry, }; diff --git a/src/participant/prompts/promptHistory.ts b/src/participant/prompts/promptHistory.ts index 6f55e577a..fa042d703 100644 --- a/src/participant/prompts/promptHistory.ts +++ b/src/participant/prompts/promptHistory.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { ParticipantErrorTypes } from '../participantErrorTypes'; -import type { ChatResult, ParticipantResponseType } from '../constants'; +import type { ChatResult } from '../constants'; +import type { ParticipantResponseType } from '../participantTypes'; export class PromptHistory { private static _handleChatResponseTurn({ diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 609238a92..6044d3e8c 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -12,8 +12,13 @@ import { getConnectionTelemetryProperties } from './connectionTelemetry'; import type { NewConnectionTelemetryEventProperties } from './connectionTelemetry'; import type { ShellEvaluateResult } from '../types/playgroundType'; import type { StorageController } from '../storage'; -import type { ParticipantResponseType } from '../participant/constants'; import { ParticipantErrorTypes } from '../participant/participantErrorTypes'; +import type { ExtensionCommand } from '../commands'; +import type { + ParticipantCommandType, + ParticipantRequestType, + ParticipantResponseType, +} from '../participant/participantTypes'; const log = createLogger('telemetry'); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -37,7 +42,7 @@ type LinkClickedTelemetryEventProperties = { }; type ExtensionCommandRunTelemetryEventProperties = { - command: string; + command: ExtensionCommand; }; type DocumentUpdatedTelemetryEventProperties = { @@ -98,7 +103,7 @@ type ParticipantFeedbackProperties = { }; type ParticipantResponseFailedProperties = { - command: string; + command: ParticipantResponseType; error_code?: string; error_name: ParticipantErrorTypes; }; @@ -106,7 +111,7 @@ type ParticipantResponseFailedProperties = { export type InternalPromptPurpose = 'intent' | 'namespace' | undefined; export type ParticipantPromptProperties = { - command: string; + command: ParticipantCommandType; user_input_length: number; total_message_length: number; has_sample_documents: boolean; @@ -115,7 +120,7 @@ export type ParticipantPromptProperties = { }; export type ParticipantResponseProperties = { - command: string; + command: ParticipantResponseType; has_cta: boolean; has_runnable_content: boolean; found_namespace: boolean; @@ -126,8 +131,22 @@ export type CopilotIntroductionProperties = { is_copilot_active: boolean; }; -export type ParticipantOpenedFromInputBoxProperties = { - source?: DocumentSource; +export type ParticipantPromptSubmittedFromActionProperties = { + source: DocumentSource; + input_length: number; + command: ParticipantRequestType; +}; + +export type ParticipantChatOpenedFromActionProperties = { + source: DocumentSource; + command?: ParticipantCommandType; +}; + +export type ParticipantInputBoxSubmitted = { + source: DocumentSource; + input_length: number | undefined; + dismissed: boolean; + command?: ParticipantCommandType; }; export function chatResultFeedbackKindToTelemetryValue( @@ -160,7 +179,8 @@ type TelemetryEventProperties = | ParticipantFeedbackProperties | ParticipantResponseFailedProperties | ParticipantPromptProperties - | ParticipantOpenedFromInputBoxProperties + | ParticipantPromptSubmittedFromActionProperties + | ParticipantChatOpenedFromActionProperties | ParticipantResponseProperties | CopilotIntroductionProperties; @@ -182,9 +202,16 @@ export enum TelemetryEventTypes { PARTICIPANT_FEEDBACK = 'Participant Feedback', PARTICIPANT_WELCOME_SHOWN = 'Participant Welcome Shown', PARTICIPANT_RESPONSE_FAILED = 'Participant Response Failed', + /** Tracks all submitted prompts */ PARTICIPANT_PROMPT_SUBMITTED = 'Participant Prompt Submitted', + /** Tracks prompts that were submitted as a result of an action other than + * the user typing the message, such as clicking on an item in tree view or a codelens */ + PARTICIPANT_PROMPT_SUBMITTED_FROM_ACTION = 'Participant Prompt Submitted From Action', + /** Tracks when a new chat was opened from an action such as clicking on a tree view. */ + PARTICIPANT_CHAT_OPENED_FROM_ACTION = 'Participant Chat Opened From Action', + /** Tracks after a participant interacts with the input box we open to let the user write the prompt for participant. */ + PARTICIPANT_INPUT_BOX_SUBMITTED = 'Participant Inbox Box Submitted', PARTICIPANT_RESPONSE_GENERATED = 'Participant Response Generated', - PARTICIPANT_SUBMITTED_FROM_INPUT_BOX = 'Participant Submitted From Input Box', COPILOT_INTRODUCTION_CLICKED = 'Copilot Introduction Clicked', COPILOT_INTRODUCTION_DISMISSED = 'Copilot Introduction Dismissed', } @@ -326,7 +353,7 @@ export default class TelemetryService { ); } - trackCommandRun(command: string): void { + trackCommandRun(command: ExtensionCommand): void { this.track(TelemetryEventTypes.EXTENSION_COMMAND_RUN, { command }); } @@ -435,17 +462,30 @@ export default class TelemetryService { ); } - trackCopilotParticipantFeedback(props: ParticipantFeedbackProperties): void { + trackParticipantFeedback(props: ParticipantFeedbackProperties): void { this.track(TelemetryEventTypes.PARTICIPANT_FEEDBACK, props); } - trackCopilotParticipantSubmittedFromInputBox( - props: ParticipantOpenedFromInputBoxProperties + trackParticipantPromptSubmittedFromAction( + props: ParticipantPromptSubmittedFromActionProperties ): void { - this.track(TelemetryEventTypes.PARTICIPANT_SUBMITTED_FROM_INPUT_BOX, props); + this.track( + TelemetryEventTypes.PARTICIPANT_PROMPT_SUBMITTED_FROM_ACTION, + props + ); + } + + trackParticipantChatOpenedFromAction( + props: ParticipantChatOpenedFromActionProperties + ): void { + this.track(TelemetryEventTypes.PARTICIPANT_CHAT_OPENED_FROM_ACTION, props); } - trackCopilotParticipantError(err: any, command: string): void { + trackParticipantInputBoxSubmitted(props: ParticipantInputBoxSubmitted): void { + this.track(TelemetryEventTypes.PARTICIPANT_INPUT_BOX_SUBMITTED, props); + } + + trackParticipantError(err: any, command: ParticipantResponseType): void { let errorCode: string | undefined; let errorName: ParticipantErrorTypes; // Making the chat request might fail because @@ -477,14 +517,14 @@ export default class TelemetryService { command, error_code: errorCode, error_name: errorName, - }); + } satisfies ParticipantResponseFailedProperties); } - trackCopilotParticipantPrompt(stats: ParticipantPromptProperties): void { + trackParticipantPrompt(stats: ParticipantPromptProperties): void { this.track(TelemetryEventTypes.PARTICIPANT_PROMPT_SUBMITTED, stats); } - trackCopilotParticipantResponse(props: ParticipantResponseProperties): void { + trackParticipantResponse(props: ParticipantResponseProperties): void { this.track(TelemetryEventTypes.PARTICIPANT_RESPONSE_GENERATED, props); } diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 1263f0e6e..ba8a710ca 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -44,6 +44,7 @@ import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensP import PlaygroundResultProvider from '../../../editors/playgroundResultProvider'; import { CollectionTreeItem, DatabaseTreeItem } from '../../../explorer'; import type { SendMessageToParticipantOptions } from '../../../participant/participantTypes'; +import { DocumentSource } from '../../../documentSource'; // The Copilot's model in not available in tests, // therefore we need to mock its methods and returning values. @@ -1838,6 +1839,10 @@ Schema: { message: `I want to ask questions about the \`${mockDatabaseItem.databaseName}\` database.`, isNewChat: true, + telemetry: { + source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + source_details: 'database', + }, }, ]); }); @@ -1865,6 +1870,10 @@ Schema: { message: `I want to ask questions about the \`${mockCollectionItem.databaseName}\` database's \`${mockCollectionItem.collectionName}\` collection.`, isNewChat: true, + telemetry: { + source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + source_details: 'collection', + }, }, ]); }); @@ -2569,7 +2578,7 @@ Schema: test('reports error', function () { const err = Error('Filtered by Responsible AI Service'); - testParticipantController._telemetryService.trackCopilotParticipantError( + testParticipantController._telemetryService.trackParticipantError( err, 'query' ); @@ -2590,7 +2599,7 @@ Schema: test('reports nested error', function () { const err = new Error('Parent error'); err.cause = Error('This message is flagged as off topic: off_topic.'); - testParticipantController._telemetryService.trackCopilotParticipantError( + testParticipantController._telemetryService.trackParticipantError( err, 'docs' ); @@ -2609,7 +2618,7 @@ Schema: test('Reports error code when available', function () { // eslint-disable-next-line new-cap const err = vscode.LanguageModelError.NotFound('Model not found'); - testParticipantController._telemetryService.trackCopilotParticipantError( + testParticipantController._telemetryService.trackParticipantError( err, 'schema' ); diff --git a/src/test/suite/participant/participantHelpers.ts b/src/test/suite/participant/participantHelpers.ts index 77448c3d9..4ff909582 100644 --- a/src/test/suite/participant/participantHelpers.ts +++ b/src/test/suite/participant/participantHelpers.ts @@ -1,6 +1,6 @@ import { CHAT_PARTICIPANT_ID } from '../../../participant/constants'; import * as vscode from 'vscode'; -import type { ParticipantCommand } from '../../../participant/participant'; +import type { ParticipantCommand } from '../../../participant/participantTypes'; export function createChatRequestTurn( command: ParticipantCommand | undefined,