From 30e1ace194155f4d7ecbab1a7268be112de41613 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:15:35 +0000 Subject: [PATCH 01/22] chore: update CHANGELOG.md (#904) Co-authored-by: gagik <17454623+gagik@users.noreply.github.com> --- CHANGELOG.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 227de7db0..a9dd9dd04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## [v1.11.0](https://github.com/mongodb-js/vscode/releases/tag/v1.11.0) - 2024-12-11 + +## What's Changed +* feat(playground): add Generate Query with Copilot code lens in playgrounds VSCODE-650 by @gagik in https://github.com/mongodb-js/vscode/pull/881 +* feat(copilot): optimize namespace for export to playground VSCODE-654 by @alenakhineika in https://github.com/mongodb-js/vscode/pull/887 +* feat(tree-explorer): add buttons to ask Copilot and create playgrounds from tree view VSCODE-651 by @gagik in https://github.com/mongodb-js/vscode/pull/890 +* feat(participant): filter message history when it goes over maxInputTokens VSCODE-653 by @gagik in https://github.com/mongodb-js/vscode/pull/894 +* feat(participant): remove in-app notification for participant VSCODE-662 by @gagik in https://github.com/mongodb-js/vscode/pull/899 +* fix(connect): can not select files on the connection form VSCODE-658 by @alenakhineika in https://github.com/mongodb-js/vscode/pull/898 + + +**Full Changelog**: https://github.com/mongodb-js/vscode/compare/v1.10.0...v1.11.0 + + ## [v1.10.0](https://github.com/mongodb-js/vscode/releases/tag/v1.10.0) - 2024-11-25 ## What's Changed @@ -402,15 +416,3 @@ To dig deeper please feel free to follow the links mentioned below: - Fixed image locations in the published VSCode marketplace README resolving to an incorrect branch (VSCODE-281, #331) -## [v0.6.8](https://github.com/mongodb-js/vscode/releases/tag/v0.6.8) - 2021-07-27 - -### Added - -- Added support for showing databases a user has permissions to when they cannot `listDatabases` (VSCODE-157, #317) - -### Changed - -- Updated code lenses in playgrounds to now appear at the end of a selection for partially running (#324) -- Update our CI release pipeline - this is the first automated release ✨ - - From 172bc535b49e98312f7b3f4d531e9a70fd39a205 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 20 Dec 2024 19:02:57 +0100 Subject: [PATCH 02/22] fix(playground): add better handling and tracking for export to playground errors VSCODE-666 (#906) --- src/participant/participant.ts | 110 ++++++++++++------ src/participant/participantErrorTypes.ts | 5 + src/telemetry/telemetryService.ts | 15 +++ .../suite/participant/participant.test.ts | 31 +++++ 4 files changed, 125 insertions(+), 36 deletions(-) diff --git a/src/participant/participant.ts b/src/participant/participant.ts index 3c1260f23..f54f5ff83 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -45,7 +45,10 @@ import { processStreamWithIdentifiers } from './streamParsing'; import type { PromptIntent } from './prompts/intent'; import { isPlayground, getSelectedText, getAllText } from '../utils/playground'; import type { DataService } from 'mongodb-data-service'; -import { ParticipantErrorTypes } from './participantErrorTypes'; +import { + ParticipantErrorTypes, + type ExportToPlaygroundError, +} from './participantErrorTypes'; import type PlaygroundResultProvider from '../editors/playgroundResultProvider'; import { isExportToLanguageResult } from '../types/playgroundType'; import { PromptHistory } from './prompts/promptHistory'; @@ -345,26 +348,35 @@ export default class ParticipantController { token: vscode.CancellationToken; language?: string; }): Promise { - const chatResponse = await this._getChatResponse({ - modelInput, - token, - }); + try { + const chatResponse = await this._getChatResponse({ + modelInput, + token, + }); - const languageCodeBlockIdentifier = { - start: `\`\`\`${language ? language : 'javascript'}`, - end: '```', - }; + const languageCodeBlockIdentifier = { + start: `\`\`\`${language ? language : 'javascript'}`, + end: '```', + }; - const runnableContent: string[] = []; - await processStreamWithIdentifiers({ - processStreamFragment: () => {}, - onStreamIdentifier: (content: string) => { - runnableContent.push(content.trim()); - }, - inputIterable: chatResponse.text, - identifier: languageCodeBlockIdentifier, - }); - return runnableContent.length ? runnableContent.join('') : null; + const runnableContent: string[] = []; + await processStreamWithIdentifiers({ + processStreamFragment: () => {}, + onStreamIdentifier: (content: string) => { + runnableContent.push(content.trim()); + }, + inputIterable: chatResponse.text, + identifier: languageCodeBlockIdentifier, + }); + return runnableContent.length ? runnableContent.join('') : null; + } catch (error) { + /** If anything goes wrong with the response or the stream, return null instead of throwing. */ + log.error( + 'Error while streaming chat response with export to language', + error + ); + return null; + } } async streamChatResponseContentWithCodeActions({ @@ -1784,49 +1796,75 @@ export default class ParticipantController { } async exportCodeToPlayground(): Promise { - const selectedText = getSelectedText(); - const codeToExport = selectedText || getAllText(); + const codeToExport = getSelectedText() || getAllText(); try { - const content = await vscode.window.withProgress( + const contentOrError = await vscode.window.withProgress< + { value: string } | { error: ExportToPlaygroundError } + >( { location: vscode.ProgressLocation.Notification, title: 'Exporting code to a playground...', cancellable: true, }, - async (progress, token): Promise => { - const modelInput = await Prompts.exportToPlayground.buildMessages({ - request: { prompt: codeToExport }, - }); + async ( + progress, + token + ): Promise<{ value: string } | { error: ExportToPlaygroundError }> => { + let modelInput: ModelInput | undefined; + try { + modelInput = await Prompts.exportToPlayground.buildMessages({ + request: { prompt: codeToExport }, + }); + } catch (error) { + return { error: 'modelInput' }; + } const result = await Promise.race([ this.streamChatResponseWithExportToLanguage({ modelInput, token, }), - new Promise((resolve) => + new Promise((resolve) => token.onCancellationRequested(() => { log.info('The export to a playground operation was canceled.'); - resolve(null); + resolve('cancelled'); }) ), ]); - if (result?.includes("Sorry, I can't assist with that.")) { - void vscode.window.showErrorMessage( - 'Sorry, we were unable to generate the playground, please try again. If the error persists, try changing your selected code.' - ); - return null; + if (result === 'cancelled') { + return { error: 'cancelled' }; } - return result; + if (!result || result?.includes("Sorry, I can't assist with that.")) { + return { error: 'streamChatResponseWithExportToLanguage' }; + } + + return { value: result }; } ); - if (!content) { - return true; + if ('error' in contentOrError) { + const { error } = contentOrError; + if (error === 'cancelled') { + return true; + } + + void vscode.window.showErrorMessage( + 'Failed to generate a MongoDB Playground. Please ensure your code block contains a MongoDB query.' + ); + + // Content in this case is already equal to the failureType; this is just to make it explicit + // and avoid accidentally sending actual contents of the message. + this._telemetryService.trackExportToPlaygroundFailed({ + input_length: codeToExport?.length, + error_name: error, + }); + return false; } + const content = contentOrError.value; await vscode.commands.executeCommand( EXTENSION_COMMANDS.OPEN_PARTICIPANT_CODE_IN_PLAYGROUND, { diff --git a/src/participant/participantErrorTypes.ts b/src/participant/participantErrorTypes.ts index 1b2a6ce83..d83ce8598 100644 --- a/src/participant/participantErrorTypes.ts +++ b/src/participant/participantErrorTypes.ts @@ -5,3 +5,8 @@ export enum ParticipantErrorTypes { OTHER = 'Other', DOCS_CHATBOT_API = 'Docs Chatbot API Issue', } + +export type ExportToPlaygroundError = + | 'cancelled' + | 'modelInput' + | 'streamChatResponseWithExportToLanguage'; diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index bccfc1b7e..232031994 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -12,6 +12,7 @@ import { getConnectionTelemetryProperties } from './connectionTelemetry'; import type { NewConnectionTelemetryEventProperties } from './connectionTelemetry'; import type { ShellEvaluateResult } from '../types/playgroundType'; import type { StorageController } from '../storage'; +import type { ExportToPlaygroundError } from '../participant/participantErrorTypes'; import { ParticipantErrorTypes } from '../participant/participantErrorTypes'; import type { ExtensionCommand } from '../commands'; import type { @@ -54,6 +55,11 @@ type DocumentEditedTelemetryEventProperties = { source: DocumentSource; }; +type ExportToPlaygroundFailedEventProperties = { + input_length: number | undefined; + error_name?: ExportToPlaygroundError; +}; + type PlaygroundExportedToLanguageTelemetryEventProperties = { language?: string; exported_code_length: number; @@ -106,6 +112,7 @@ type ParticipantResponseFailedProperties = { command: ParticipantResponseType; error_code?: string; error_name: ParticipantErrorTypes; + error_details?: string; }; export type InternalPromptPurpose = 'intent' | 'namespace' | undefined; @@ -171,6 +178,7 @@ type TelemetryEventProperties = | PlaygroundSavedTelemetryEventProperties | PlaygroundLoadedTelemetryEventProperties | KeytarSecretsMigrationFailedProperties + | ExportToPlaygroundFailedEventProperties | SavedConnectionsLoadedProperties | ParticipantFeedbackProperties | ParticipantResponseFailedProperties @@ -193,6 +201,7 @@ export enum TelemetryEventTypes { PLAYGROUND_EXPORTED_TO_LANGUAGE = 'Playground Exported To Language', PLAYGROUND_CREATED = 'Playground Created', KEYTAR_SECRETS_MIGRATION_FAILED = 'Keytar Secrets Migration Failed', + EXPORT_TO_PLAYGROUND_FAILED = 'Export To Playground Failed', SAVED_CONNECTIONS_LOADED = 'Saved Connections Loaded', PARTICIPANT_FEEDBACK = 'Participant Feedback', PARTICIPANT_WELCOME_SHOWN = 'Participant Welcome Shown', @@ -346,6 +355,12 @@ export default class TelemetryService { ); } + trackExportToPlaygroundFailed( + props: ExportToPlaygroundFailedEventProperties + ): void { + this.track(TelemetryEventTypes.EXPORT_TO_PLAYGROUND_FAILED, props); + } + trackCommandRun(command: ExtensionCommand): void { this.track(TelemetryEventTypes.EXTENSION_COMMAND_RUN, { command }); } diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 78572f75b..5d8565793 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -1818,6 +1818,37 @@ Schema: ); }); + test('tracks failures with export to playground and not as a failed prompt', async function () { + const editor = vscode.window.activeTextEditor; + if (!editor) { + throw new Error('Window active text editor is undefined'); + } + + const testDocumentUri = editor.document.uri; + const edit = new vscode.WorkspaceEdit(); + const code = ` + THIS IS SOME ERROR CAUSING CODE. +`; + edit.replace(testDocumentUri, getFullRange(editor.document), code); + await vscode.workspace.applyEdit(edit); + + await testParticipantController.exportCodeToPlayground(); + sendRequestStub.rejects(); + const messages = sendRequestStub.firstCall.args[0]; + expect(getMessageContent(messages[1])).to.equal(code.trim()); + expect(telemetryTrackStub).calledWith( + TelemetryEventTypes.EXPORT_TO_PLAYGROUND_FAILED, + { + input_length: code.trim().length, + error_name: 'streamChatResponseWithExportToLanguage', + } + ); + + expect(telemetryTrackStub).not.calledWith( + TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED + ); + }); + test('exports selected lines of code to a playground', async function () { const editor = vscode.window.activeTextEditor; if (!editor) { From c69e31492f45c3d19f80db28c24ff81a6fa8a1c0 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 17 Jan 2025 09:49:41 +0100 Subject: [PATCH 03/22] fix(deps): bump MongoCluster to fix failing OIDC tests VSCODE-670 (#910) ubuntu-latest was bumped to 24.04, but 7.0.x only supports up to ubuntu 22, so we have to use a newer server. Co-authored-by: Nikola Irinchev --- package-lock.json | 173 +++++++++++++++++++----------------- package.json | 8 +- src/test/suite/oidc.test.ts | 2 +- 3 files changed, 94 insertions(+), 89 deletions(-) diff --git a/package-lock.json b/package-lock.json index bdd2b5b41..3194a3e8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,8 +47,8 @@ "devDependencies": { "@babel/preset-typescript": "^7.25.7", "@babel/types": "^7.25.8", - "@mongodb-js/oidc-mock-provider": "^0.10.1", - "@mongodb-js/oidc-plugin": "^0.4.0", + "@mongodb-js/oidc-mock-provider": "^0.10.2", + "@mongodb-js/oidc-plugin": "^1.1.5", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/sbom-tools": "^0.7.2", "@mongodb-js/signing-utils": "^0.3.6", @@ -92,9 +92,9 @@ "mocha": "^10.7.3", "mocha-junit-reporter": "^2.2.1", "mocha-multi": "^1.1.7", - "mongodb-client-encryption": "^6.1.0", + "mongodb-client-encryption": "^6.1.1", "mongodb-rag-core": "^0.4.1", - "mongodb-runner": "^5.7.0", + "mongodb-runner": "^5.7.1", "node-fetch": "^2.7.0", "node-loader": "^0.6.0", "npm-run-all": "^4.1.5", @@ -3858,14 +3858,15 @@ } }, "node_modules/@mongodb-js/mongodb-downloader": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.6.tgz", - "integrity": "sha512-Cu82TRmAP/OIRizx9o+fReQf8FfovI28rjY0pu8wHyCoUlG7q3Zkxb/lppB7a9/kzQfONeHAoXIOQkvSasKYrw==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.7.tgz", + "integrity": "sha512-zX18NlZNvN9Yao94VOxubeVOU0BaYX7X1giew1o0fWoIbUIiGzDDVQFRwRNybN+wD7dMFJYvLc6EOlmB9shd5w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "debug": "^4.3.4", "decompress": "^4.2.1", - "mongodb-download-url": "^1.5.1", + "mongodb-download-url": "^1.5.5", "node-fetch": "^2.6.11", "tar": "^6.1.15" } @@ -3876,10 +3877,11 @@ "integrity": "sha512-I704bSQRu/SusTcCV8qqtdFVvAJf1aKZtgGM2VnYjPn2njZd5j7864k/CF9TeeR8+r0At5qqNa3N4MX9YxPnEg==" }, "node_modules/@mongodb-js/oidc-mock-provider": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-mock-provider/-/oidc-mock-provider-0.10.1.tgz", - "integrity": "sha512-RIIkEo0gsjlMO6zY11JsKH4UhX8/EAaxD3DxhhY/m7ncCo6pIp4bGT8rWvqTHnf+rheyxeyEilpU1jQaUzqENQ==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-mock-provider/-/oidc-mock-provider-0.10.2.tgz", + "integrity": "sha512-mH9tpgqYvF2ZRBbFKta+ziN48V+t/+NPLQoe7nZ8bYbWsGfXY79QKMIElaXlU8HnemnqUbOqBSYuizgs62OxfQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "yargs": "17.7.2" }, @@ -3929,18 +3931,17 @@ } }, "node_modules/@mongodb-js/oidc-plugin": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-0.4.0.tgz", - "integrity": "sha512-tinXSz6O2AmgMAgorXUcCJtDhayghkmsXVVTd5UiXhzSA/NNVtlleZXSVkG6tr46WXGzLISgVX+lUzzcEIiwJQ==", - "dev": true, + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.5.tgz", + "integrity": "sha512-K76ADgrDpL+lg6L/QsEBIGbSjTEUljYDGDX75Tq4+zIkx3JQgeQhS5J3qZNzKwJa4nj+EwhihaADLRgsMpAtrA==", + "license": "Apache-2.0", "dependencies": { - "abort-controller": "^3.0.0", "express": "^4.18.2", "open": "^9.1.0", "openid-client": "^5.6.4" }, "engines": { - "node": ">= 14.18.0" + "node": ">= 16.20.1" } }, "node_modules/@mongodb-js/prettier-config-devtools": { @@ -4192,19 +4193,6 @@ "mongodb-log-writer": "^1.4.2" } }, - "node_modules/@mongosh/service-provider-node-driver/node_modules/@mongodb-js/oidc-plugin": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", - "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", - "dependencies": { - "express": "^4.18.2", - "open": "^9.1.0", - "openid-client": "^5.6.4" - }, - "engines": { - "node": ">= 16.20.1" - } - }, "node_modules/@mongosh/shell-api": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@mongosh/shell-api/-/shell-api-2.3.3.tgz", @@ -4272,21 +4260,6 @@ "mongodb-log-writer": "^1.4.2" } }, - "node_modules/@mongosh/types/node_modules/@mongodb-js/oidc-plugin": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", - "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "express": "^4.18.2", - "open": "^9.1.0", - "openid-client": "^5.6.4" - }, - "engines": { - "node": ">= 16.20.1" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7256,7 +7229,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "devOptional": true, + "optional": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -7266,6 +7239,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -7275,13 +7249,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bl/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -7296,13 +7272,15 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bl/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -7579,6 +7557,7 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "dev": true, + "license": "MIT", "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" @@ -7588,7 +7567,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/buffer-crc32": { "version": "0.2.13", @@ -7608,7 +7588,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/buffer-from": { "version": "0.1.2", @@ -8672,6 +8653,7 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -8716,6 +8698,7 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -8730,6 +8713,7 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -8746,6 +8730,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -8755,6 +8740,7 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, + "license": "MIT", "dependencies": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -8769,6 +8755,7 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", "dev": true, + "license": "MIT", "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -8784,6 +8771,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8793,6 +8781,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", "dev": true, + "license": "MIT", "dependencies": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -11175,6 +11164,7 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -11183,7 +11173,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "devOptional": true + "optional": true }, "node_modules/fill-range": { "version": "7.1.1", @@ -11580,6 +11570,7 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -11592,6 +11583,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -11603,7 +11595,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fs-monkey": { "version": "1.0.5", @@ -13129,7 +13122,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-negative-zero": { "version": "2.0.2", @@ -13241,6 +13235,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -14081,6 +14076,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^3.0.0" }, @@ -14093,6 +14089,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -14477,6 +14474,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=8" } @@ -14486,6 +14484,7 @@ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, + "license": "MIT", "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -14499,6 +14498,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -14510,7 +14510,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/mkdirp": { "version": "1.0.4", @@ -14805,14 +14806,13 @@ } }, "node_modules/mongodb-client-encryption": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.1.0.tgz", - "integrity": "sha512-Y3Hakre82nXD/pNDUzBjxfgwWSj5E1ar9ZLkqyXDfvirv4huHMbg8Q2qVO/TXlNJuf1B2bzrEDXsTqHKQSQLtw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.1.1.tgz", + "integrity": "sha512-hqy68WWNZmkoqldFlpw8TQNaJUOor4YCM/FuxETZTeTZpGEbQwkTwdrQlgiV14qyOizPw8abbAxGi3zX+raA9w==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "bindings": "^1.5.0", "node-addon-api": "^4.3.0", "prebuild-install": "^7.1.2" }, @@ -14931,25 +14931,12 @@ "mongodb-log-writer": "^1.4.2" } }, - "node_modules/mongodb-data-service/node_modules/@mongodb-js/oidc-plugin": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", - "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", - "peer": true, - "dependencies": { - "express": "^4.18.2", - "open": "^9.1.0", - "openid-client": "^5.6.4" - }, - "engines": { - "node": ">= 16.20.1" - } - }, "node_modules/mongodb-download-url": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/mongodb-download-url/-/mongodb-download-url-1.5.4.tgz", - "integrity": "sha512-+2pnlKikAo6U4PQX4GXPNYoBxASzWQdivVkb8RuoToCeSM1acZJ1fnMLtY4JtNq93OAd5CScIs9WAnlC5bC0YQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/mongodb-download-url/-/mongodb-download-url-1.5.5.tgz", + "integrity": "sha512-8HLqKVVuKQBinKRZbDu0YSzwLfD/Wb//vOIm3CMk0/2AzZzp0pg+8E+DAkx7VLEdoyuPVWLU5v/doODjXlPYSA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "debug": "^4.1.1", "minimist": "^1.2.3", @@ -15057,12 +15044,13 @@ } }, "node_modules/mongodb-runner": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.7.0.tgz", - "integrity": "sha512-G7GmVj5SzoSL/GY7lMnhNSSrPUZLkWuV1/CtSwdadsrA7aGuxB0KDviTmyecfI2/OWsT39t8X3ISE5R30EV5Xg==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.7.1.tgz", + "integrity": "sha512-/MBEP2DcMpNbpSsXqG+lgFqYehCd2qasdWIfKuv4jGKwLoDPv/mWoQYAQDFAC2xaxjb576Y2LwUAeYeB1KPZdg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@mongodb-js/mongodb-downloader": "^0.3.6", + "@mongodb-js/mongodb-downloader": "^0.3.7", "@mongodb-js/saslprep": "^1.1.9", "debug": "^4.3.4", "mongodb": "^6.9.0", @@ -16615,6 +16603,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -16624,6 +16613,7 @@ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -16633,6 +16623,7 @@ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, + "license": "MIT", "dependencies": { "pinkie": "^2.0.0" }, @@ -17841,6 +17832,7 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, + "license": "MIT", "dependencies": { "commander": "^2.8.1" }, @@ -17853,7 +17845,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/select": { "version": "1.1.2", @@ -18804,6 +18797,7 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, + "license": "MIT", "dependencies": { "is-natural-number": "^4.0.1" } @@ -19094,6 +19088,7 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, + "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -19197,6 +19192,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -19214,13 +19210,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tar-stream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -19235,13 +19233,15 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tar-stream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -19251,6 +19251,7 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -19259,7 +19260,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/terser": { "version": "5.31.1", @@ -19437,7 +19439,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/to-fast-properties": { "version": "2.0.0", @@ -19861,6 +19864,7 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -19885,6 +19889,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" diff --git a/package.json b/package.json index 4e0fac3db..d796f2189 100644 --- a/package.json +++ b/package.json @@ -1244,8 +1244,8 @@ "devDependencies": { "@babel/preset-typescript": "^7.25.7", "@babel/types": "^7.25.8", - "@mongodb-js/oidc-mock-provider": "^0.10.1", - "@mongodb-js/oidc-plugin": "^0.4.0", + "@mongodb-js/oidc-mock-provider": "^0.10.2", + "@mongodb-js/oidc-plugin": "^1.1.5", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/sbom-tools": "^0.7.2", "@mongodb-js/signing-utils": "^0.3.6", @@ -1289,9 +1289,9 @@ "mocha": "^10.7.3", "mocha-junit-reporter": "^2.2.1", "mocha-multi": "^1.1.7", - "mongodb-client-encryption": "^6.1.0", + "mongodb-client-encryption": "^6.1.1", "mongodb-rag-core": "^0.4.1", - "mongodb-runner": "^5.7.0", + "mongodb-runner": "^5.7.1", "node-fetch": "^2.7.0", "node-loader": "^0.6.0", "npm-run-all": "^4.1.5", diff --git a/src/test/suite/oidc.test.ts b/src/test/suite/oidc.test.ts index 054aa5b39..41aa63099 100644 --- a/src/test/suite/oidc.test.ts +++ b/src/test/suite/oidc.test.ts @@ -121,7 +121,7 @@ suite('OIDC Tests', function () { cluster = await MongoCluster.start({ ...defaultClusterOptions, - version: '7.0.x', + version: '8.0.x', downloadOptions: { enterprise: true }, args: [ '--setParameter', From b6e36e83218bd5ba37d04188a45d47fe6de31638 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Mon, 20 Jan 2025 17:09:46 +0100 Subject: [PATCH 04/22] feat(tree-explorer): add ability to set preset connections in settings.json VSCODE-665 (#909) --- .vscode/settings.json | 8 +- package.json | 81 ++++++++-- src/commands/index.ts | 1 + src/connectionController.ts | 55 ++++++- src/explorer/connectionTreeItem.ts | 33 ++-- src/explorer/explorerTreeController.ts | 86 ++++++----- src/mdbExtensionController.ts | 17 +++ src/storage/connectionStorage.ts | 57 ++++++- src/telemetry/telemetryService.ts | 14 ++ src/test/suite/connectionController.test.ts | 25 ++++ .../suite/explorer/connectionTreeItem.test.ts | 11 +- src/test/suite/mdbExtensionController.test.ts | 1 + .../suite/storage/connectionStorage.test.ts | 141 +++++++++++++++++- .../suite/telemetry/telemetryService.test.ts | 2 + 14 files changed, 456 insertions(+), 76 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 32baf6163..8dc745b7f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,11 @@ "out": true }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + "mdb.presetConnections": [ + { + "name": "Preset Connection", + "connectionString": "mongodb://localhost:27017" + } + ] } diff --git a/package.json b/package.json index d796f2189..d67dd7c3a 100644 --- a/package.json +++ b/package.json @@ -320,6 +320,10 @@ "command": "mdb.copyConnectionString", "title": "Copy Connection String" }, + { + "command": "mdb.editPresetConnections", + "title": "Edit Preset Connections..." + }, { "command": "mdb.renameConnection", "title": "Rename Connection..." @@ -489,42 +493,49 @@ }, { "command": "mdb.addConnection", - "when": "view == mongoDBConnectionExplorer" + "when": "view == mongoDBConnectionExplorer", + "group": "1@1" }, { "command": "mdb.addConnectionWithURI", - "when": "view == mongoDBConnectionExplorer" + "when": "view == mongoDBConnectionExplorer", + "group": "1@2" + }, + { + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer", + "group": "2@1" } ], "view/item/context": [ { "command": "mdb.addDatabase", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == false", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == false", "group": "inline" }, { "command": "mdb.addDatabase", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == false", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == false", "group": "1@1" }, { "command": "mdb.addStreamProcessor", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == true", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == true", "group": "inline" }, { "command": "mdb.addStreamProcessor", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem && mdb.isAtlasStreams == true", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem) && mdb.isAtlasStreams == true", "group": "1@1" }, { "command": "mdb.refreshConnection", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "1@2" }, { "command": "mdb.treeViewOpenMongoDBShell", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "2@1" }, { @@ -537,14 +548,19 @@ "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", "group": "3@2" }, + { + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer && viewItem == connectedPresetConnectionTreeItem", + "group": "3@2" + }, { "command": "mdb.copyConnectionString", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "4@1" }, { "command": "mdb.disconnectFromConnectionTreeItem", - "when": "view == mongoDBConnectionExplorer && viewItem == connectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == connectedConnectionTreeItem || viewItem == connectedPresetConnectionTreeItem)", "group": "5@1" }, { @@ -559,7 +575,7 @@ }, { "command": "mdb.connectToConnectionTreeItem", - "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedPresetConnectionTreeItem)", "group": "1@1" }, { @@ -572,9 +588,14 @@ "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", "group": "2@2" }, + { + "command": "mdb.editPresetConnections", + "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedPresetConnectionTreeItem", + "group": "2@2" + }, { "command": "mdb.copyConnectionString", - "when": "view == mongoDBConnectionExplorer && viewItem == disconnectedConnectionTreeItem", + "when": "view == mongoDBConnectionExplorer && (viewItem == disconnectedConnectionTreeItem || viewItem == disconnectedPresetConnectionTreeItem)", "group": "3@1" }, { @@ -1171,6 +1192,42 @@ "type": "string", "default": "", "description": "Specify a shell command that is run to start the browser for authenticating with the OIDC identity provider for the server connection. Leave this empty for default browser." + }, + "mdb.presetConnections": { + "scope": "window", + "type": "array", + "description": "Defines preset connections. Can be used to share connection configurations in a workspace or global scope. Do not store sensitive credentials here.", + "examples": [ + [ + { + "name": "Preset Connection", + "connectionString": "mongodb://localhost:27017" + } + ] + ], + "items": { + "type": "object", + "examples": [ + { + "name": "Preset Connection", + "connectionString": "mongodb://localhost:27017" + } + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the connection." + }, + "connectionString": { + "type": "string", + "description": "Connection string. Do not store sensitive credentials here." + } + }, + "required": [ + "name", + "connectionString" + ] + } } } }, diff --git a/src/commands/index.ts b/src/commands/index.ts index b1a23606c..348189649 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -44,6 +44,7 @@ enum EXTENSION_COMMANDS { MDB_EDIT_CONNECTION = 'mdb.editConnection', MDB_REFRESH_CONNECTION = 'mdb.refreshConnection', MDB_COPY_CONNECTION_STRING = 'mdb.copyConnectionString', + MDB_EDIT_PRESET_CONNECTIONS = 'mdb.editPresetConnections', MDB_REMOVE_CONNECTION_TREE_VIEW = 'mdb.treeItemRemoveConnection', MDB_RENAME_CONNECTION = 'mdb.renameConnection', MDB_ADD_DATABASE = 'mdb.addDatabase', diff --git a/src/connectionController.ts b/src/connectionController.ts index 4bffaa094..159ecea8e 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -21,10 +21,15 @@ import type { StorageController } from './storage'; import type { StatusView } from './views'; import type TelemetryService from './telemetry/telemetryService'; import { openLink } from './utils/linkHelper'; -import type { LoadedConnection } from './storage/connectionStorage'; +import type { + ConnectionSource, + LoadedConnection, +} from './storage/connectionStorage'; import { ConnectionStorage } from './storage/connectionStorage'; import LINKS from './utils/links'; import { isAtlasStream } from 'mongodb-build-info'; +import { DocumentSource } from './documentSource'; +import type { ConnectionTreeItem } from './explorer'; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJSON = require('../package.json'); @@ -161,7 +166,55 @@ export default class ConnectionController { }); } + async openPresetConnectionsSettings( + originTreeItem: ConnectionTreeItem | undefined + ): Promise { + this._telemetryService.trackPresetConnectionEdited({ + source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + source_details: originTreeItem ? 'tree_item' : 'header', + }); + let source: ConnectionSource | undefined = originTreeItem?.source; + if (!source) { + const mdbConfiguration = vscode.workspace.getConfiguration('mdb'); + + const presetConnections = mdbConfiguration?.inspect('presetConnections'); + + if (presetConnections?.workspaceValue) { + source = 'workspaceSettings'; + } else if (presetConnections?.globalValue) { + source = 'globalSettings'; + } else { + // If no preset settings exist in workspace and global scope, + // set a default one inside the workspace and open it. + source = 'workspaceSettings'; + await mdbConfiguration.update('presetConnections', [ + { + name: 'Preset Connection', + connectionString: 'mongodb://localhost:27017', + }, + ]); + } + } + switch (source) { + case 'globalSettings': + await vscode.commands.executeCommand( + 'workbench.action.openSettingsJson' + ); + break; + case 'workspaceSettings': + case 'user': + await vscode.commands.executeCommand( + 'workbench.action.openWorkspaceSettingsFile' + ); + break; + default: + throw new Error('Unknown preset connection source'); + } + } + async loadSavedConnections(): Promise { + this._connections = Object.create(null); + const loadedConnections = await this._connectionStorage.loadConnections(); for (const connection of loadedConnections) { diff --git a/src/explorer/connectionTreeItem.ts b/src/explorer/connectionTreeItem.ts index 9c47423a4..53f83da70 100644 --- a/src/explorer/connectionTreeItem.ts +++ b/src/explorer/connectionTreeItem.ts @@ -11,11 +11,11 @@ import formatError from '../utils/formatError'; import { getImagesPath } from '../extensionConstants'; import type TreeItemParent from './treeItemParentInterface'; import StreamProcessorTreeItem from './streamProcessorTreeItem'; +import type { ConnectionSource } from '../storage/connectionStorage'; -export enum ConnectionItemContextValues { - disconnected = 'disconnectedConnectionTreeItem', - connected = 'connectedConnectionTreeItem', -} +export type ConnectionItemContextValue = `${'disconnected' | 'connected'}${ + | '' + | 'Preset'}ConnectionTreeItem`; function getIconPath(isActiveConnection: boolean): { light: string; @@ -39,7 +39,7 @@ export default class ConnectionTreeItem extends vscode.TreeItem implements TreeItemParent, vscode.TreeDataProvider { - contextValue = ConnectionItemContextValues.disconnected; + contextValue: ConnectionItemContextValue = 'disconnectedConnectionTreeItem'; private _childrenCache: { [key: string]: DatabaseTreeItem | StreamProcessorTreeItem; @@ -50,6 +50,7 @@ export default class ConnectionTreeItem connectionId: string; isExpanded: boolean; + source: ConnectionSource; constructor({ connectionId, @@ -58,6 +59,7 @@ export default class ConnectionTreeItem connectionController, cacheIsUpToDate, childrenCache, + source, }: { connectionId: string; collapsibleState: vscode.TreeItemCollapsibleState; @@ -67,21 +69,24 @@ export default class ConnectionTreeItem childrenCache: { [key: string]: DatabaseTreeItem | StreamProcessorTreeItem; }; // Existing cache. + source: ConnectionSource; }) { super( connectionController.getSavedConnectionName(connectionId), collapsibleState ); - if ( + const isConnected = connectionController.getActiveConnectionId() === connectionId && !connectionController.isDisconnecting() && - !connectionController.isConnecting() - ) { - this.contextValue = ConnectionItemContextValues.connected; - } + !connectionController.isConnecting(); + + this.contextValue = `${isConnected ? 'connected' : 'disconnected'}${ + source === 'user' ? '' : 'Preset' + }ConnectionTreeItem`; this.connectionId = connectionId; + this.source = source; this._connectionController = connectionController; this.isExpanded = isExpanded; this._childrenCache = childrenCache; @@ -204,7 +209,9 @@ export default class ConnectionTreeItem return Object.values(this._childrenCache); } - private async _buildChildrenCacheForDatabases(dataService: DataService) { + private async _buildChildrenCacheForDatabases( + dataService: DataService + ): Promise> { const databases = await this.listDatabases(); databases.sort((a: string, b: string) => a.localeCompare(b)); @@ -226,7 +233,9 @@ export default class ConnectionTreeItem return newChildrenCache; } - private async _buildChildrenCacheForStreams(dataService: DataService) { + private async _buildChildrenCacheForStreams( + dataService: DataService + ): Promise> { const processors = await this.listStreamProcessors(); processors.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/src/explorer/explorerTreeController.ts b/src/explorer/explorerTreeController.ts index 7c569cfb2..851fc3ac3 100644 --- a/src/explorer/explorerTreeController.ts +++ b/src/explorer/explorerTreeController.ts @@ -8,6 +8,7 @@ import { DOCUMENT_ITEM } from './documentTreeItem'; import { DOCUMENT_LIST_ITEM, CollectionTypes } from './documentListTreeItem'; import EXTENSION_COMMANDS from '../commands'; import { sortTreeItemsByLabel } from './treeItemUtils'; +import type { LoadedConnection } from '../storage/connectionStorage'; const log = createLogger('explorer tree controller'); @@ -130,6 +131,50 @@ export default class ExplorerTreeController return element; } + private _getConnectionExpandedState( + connection: LoadedConnection, + pastConnectionTreeItems: { + [key: string]: ConnectionTreeItem; + } + ): { + collapsibleState: vscode.TreeItemCollapsibleState; + isExpanded: boolean; + } { + const isActiveConnection = + connection.id === this._connectionController.getActiveConnectionId(); + const isBeingConnectedTo = + this._connectionController.isConnecting() && + connection.id === this._connectionController.getConnectingConnectionId(); + + let collapsibleState = isActiveConnection + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.Collapsed; + + if ( + pastConnectionTreeItems[connection.id] && + !pastConnectionTreeItems[connection.id].isExpanded + ) { + // Connection was manually collapsed while being active. + collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + } + if (isActiveConnection && this._connectionController.isDisconnecting()) { + // Don't show a collapsable state when the connection is being disconnected from. + collapsibleState = vscode.TreeItemCollapsibleState.None; + } + if (isBeingConnectedTo) { + // Don't show a collapsable state when the connection is being connected to. + collapsibleState = vscode.TreeItemCollapsibleState.None; + } + return { + collapsibleState, + // Set expanded when we're connecting to a connection so that it + // expands when it's connected. + isExpanded: + isBeingConnectedTo || + collapsibleState === vscode.TreeItemCollapsibleState.Expanded, + }; + } + getChildren(element?: any): Thenable { // When no element is present we are at the root. if (!element) { @@ -139,45 +184,14 @@ export default class ExplorerTreeController // Create new connection tree items, using cached children wherever possible. connections.forEach((connection) => { - const isActiveConnection = - connection.id === this._connectionController.getActiveConnectionId(); - const isBeingConnectedTo = - this._connectionController.isConnecting() && - connection.id === - this._connectionController.getConnectingConnectionId(); - - let connectionExpandedState = isActiveConnection - ? vscode.TreeItemCollapsibleState.Expanded - : vscode.TreeItemCollapsibleState.Collapsed; - - if ( - pastConnectionTreeItems[connection.id] && - !pastConnectionTreeItems[connection.id].isExpanded - ) { - // Connection was manually collapsed while being active. - connectionExpandedState = vscode.TreeItemCollapsibleState.Collapsed; - } - if ( - isActiveConnection && - this._connectionController.isDisconnecting() - ) { - // Don't show a collapsable state when the connection is being disconnected from. - connectionExpandedState = vscode.TreeItemCollapsibleState.None; - } - if (isBeingConnectedTo) { - // Don't show a collapsable state when the connection is being connected to. - connectionExpandedState = vscode.TreeItemCollapsibleState.None; - } + const { collapsibleState, isExpanded } = + this._getConnectionExpandedState(connection, pastConnectionTreeItems); this._connectionTreeItems[connection.id] = new ConnectionTreeItem({ connectionId: connection.id, - collapsibleState: connectionExpandedState, - // Set expanded when we're connecting to a connection so that it - // expands when it's connected. - isExpanded: - isBeingConnectedTo || - connectionExpandedState === - vscode.TreeItemCollapsibleState.Expanded, + collapsibleState, + isExpanded, + source: connection.source ?? 'user', connectionController: this._connectionController, cacheIsUpToDate: pastConnectionTreeItems[connection.id] ? pastConnectionTreeItems[connection.id].cacheIsUpToDate diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index b9a2de1c3..b6f0b9505 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -160,6 +160,15 @@ export default class MDBExtensionController implements vscode.Disposable { this._editorsController.registerProviders(); } + subscribeToConfigurationChanges(): void { + const subscription = vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('mdb.presetConnections')) { + void this._connectionController.loadSavedConnections(); + } + }); + this._context.subscriptions.push(subscription); + } + async activate(): Promise { this._explorerController.activateConnectionsTreeView(); this._helpExplorer.activateHelpTreeView(this._telemetryService); @@ -172,6 +181,7 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommands(); this.showOverviewPageIfRecentlyInstalled(); + this.subscribeToConfigurationChanges(); const copilot = vscode.extensions.getExtension(COPILOT_EXTENSION_ID); void vscode.commands.executeCommand( @@ -480,6 +490,13 @@ export default class MDBExtensionController implements vscode.Disposable { return true; } ); + this.registerCommand( + EXTENSION_COMMANDS.MDB_EDIT_PRESET_CONNECTIONS, + async (element: ConnectionTreeItem | undefined) => { + await this._connectionController.openPresetConnectionsSettings(element); + return true; + } + ); this.registerCommand( EXTENSION_COMMANDS.MDB_COPY_CONNECTION_STRING, async (element: ConnectionTreeItem): Promise => { diff --git a/src/storage/connectionStorage.ts b/src/storage/connectionStorage.ts index e233ad4d5..853ef12e2 100644 --- a/src/storage/connectionStorage.ts +++ b/src/storage/connectionStorage.ts @@ -11,21 +11,34 @@ import type StorageController from './storageController'; import type { SecretStorageLocationType } from './storageController'; import { DefaultSavingLocations, + SecretStorageLocation, StorageLocation, StorageVariables, } from './storageController'; +import { v4 as uuidv4 } from 'uuid'; const log = createLogger('connection storage'); +export type ConnectionSource = 'globalSettings' | 'workspaceSettings' | 'user'; export interface StoreConnectionInfo { id: string; // Connection model id or a new uuid. name: string; // Possibly user given name, not unique. storageLocation: StorageLocation; secretStorageLocation?: SecretStorageLocationType; connectionOptions?: ConnectionOptions; + source?: ConnectionSource; lastUsed?: Date; // Date and time when the connection was last used, i.e. connected with. } +export type PresetSavedConnection = { + name: string; + connectionString: string; +}; + +export type PresetSavedConnectionWithSource = PresetSavedConnection & { + source: ConnectionSource; +}; + type StoreConnectionInfoWithConnectionOptions = StoreConnectionInfo & Required>; @@ -56,6 +69,7 @@ export class ConnectionStorage { return { id: connectionId, name, + source: 'user', storageLocation: this.getPreferredStorageLocationFromConfiguration(), secretStorageLocation: 'vscode.SecretStorage', connectionOptions: connectionOptions, @@ -166,7 +180,42 @@ export class ConnectionStorage { ); } - async loadConnections() { + _loadPresetConnections(): LoadedConnection[] { + const configuration = vscode.workspace.getConfiguration('mdb'); + const presetConnectionsInfo = + configuration.inspect('presetConnections'); + + if (!presetConnectionsInfo) { + return []; + } + + const combinedPresetConnections: PresetSavedConnectionWithSource[] = [ + ...(presetConnectionsInfo?.globalValue ?? []).map((preset) => ({ + ...preset, + source: 'globalSettings' as const, + })), + ...(presetConnectionsInfo?.workspaceValue ?? []).map((preset) => ({ + ...preset, + source: 'workspaceSettings' as const, + })), + ]; + + return combinedPresetConnections.map( + (presetConnection) => + ({ + id: uuidv4(), + name: presetConnection.name, + connectionOptions: { + connectionString: presetConnection.connectionString, + }, + source: presetConnection.source, + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + } satisfies LoadedConnection) + ); + } + + async loadConnections(): Promise { const globalAndWorkspaceConnections = Object.values({ ...this._storageController.get( StorageVariables.GLOBAL_SAVED_CONNECTIONS, @@ -203,10 +252,12 @@ export class ConnectionStorage { }) ); - return loadedConnections; + const presetConnections = this._loadPresetConnections(); + + return [...loadedConnections, ...presetConnections]; } - async removeConnection(connectionId: string) { + async removeConnection(connectionId: string): Promise { await this._storageController.deleteSecret(connectionId); // See if the connection exists in the saved global or workspace connections diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 232031994..82ccd523b 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -91,6 +91,8 @@ type ConnectionEditedTelemetryEventProperties = { type SavedConnectionsLoadedProperties = { // Total number of connections saved on disk saved_connections: number; + // Total number of connections from preset settings + preset_connections: number; // Total number of connections that extension was able to load, it might // differ from saved_connections since there might be failures in loading // secrets for a connection in which case we don't list the connections in the @@ -145,6 +147,11 @@ export type ParticipantChatOpenedFromActionProperties = { command?: ParticipantCommandType; }; +export type PresetSavedConnectionEditedProperties = { + source: DocumentSource; + source_details: 'tree_item' | 'header'; +}; + export type ParticipantInputBoxSubmitted = { source: DocumentSource; input_length: number | undefined; @@ -216,6 +223,7 @@ export enum TelemetryEventTypes { /** 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', + PRESET_CONNECTION_EDITED = 'Preset Connection Edited', } /** @@ -446,6 +454,12 @@ export default class TelemetryService { ); } + trackPresetConnectionEdited( + props: PresetSavedConnectionEditedProperties + ): void { + this.track(TelemetryEventTypes.PRESET_CONNECTION_EDITED, props); + } + trackPlaygroundCreated(playgroundType: string): void { this.track(TelemetryEventTypes.PLAYGROUND_CREATED, { playground_type: playgroundType, diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index 78f7d649d..0f54d5820 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -240,6 +240,31 @@ suite('Connection Controller Test Suite', function () { expect(testConnectionController.getSavedConnections().length).to.equal(0); }); + test('clears connections when loading saved connections', async () => { + // This might happen if i.e. one defines a preset connection and then deletes it. + // In that case we'd have defined this connection but there was never a follow up + // delete event to clear it. So on reload we need to start from a clean slate. + testConnectionController._connections['1234'] = { + id: '1234', + name: 'orphan', + connectionOptions: { + connectionString: 'localhost:3000', + }, + storageLocation: StorageLocation.NONE, + secretStorageLocation: SecretStorageLocation.SecretStorage, + }; + + // Should persist as this is a saved connection. + await testConnectionController.addNewConnectionStringAndConnect( + TEST_DATABASE_URI + ); + + await testConnectionController.loadSavedConnections(); + + expect(testConnectionController.getSavedConnections().length).to.equal(1); + expect(testConnectionController._connections['1234']).is.undefined; + }); + test('the connection model loads both global and workspace stored connection models', async () => { const expectedDriverUrl = `mongodb://localhost:27088/?appname=mongodb-vscode+${version}`; diff --git a/src/test/suite/explorer/connectionTreeItem.test.ts b/src/test/suite/explorer/connectionTreeItem.test.ts index 13f7bd157..4e65a0042 100644 --- a/src/test/suite/explorer/connectionTreeItem.test.ts +++ b/src/test/suite/explorer/connectionTreeItem.test.ts @@ -4,9 +4,7 @@ import { beforeEach, afterEach } from 'mocha'; import sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; -import ConnectionTreeItem, { - ConnectionItemContextValues, -} from '../../../explorer/connectionTreeItem'; +import ConnectionTreeItem from '../../../explorer/connectionTreeItem'; import { DataServiceStub } from '../stubs'; import formatError from '../../../utils/formatError'; import { mdbTestExtension } from '../stubbableMdbExtension'; @@ -14,7 +12,7 @@ import { mdbTestExtension } from '../stubbableMdbExtension'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { contributes } = require('../../../../package.json'); -function getTestConnectionTreeItem() { +function getTestConnectionTreeItem(): ConnectionTreeItem { return new ConnectionTreeItem({ connectionId: 'test', collapsibleState: vscode.TreeItemCollapsibleState.Expanded, @@ -23,6 +21,7 @@ function getTestConnectionTreeItem() { mdbTestExtension.testExtensionController._connectionController, cacheIsUpToDate: false, childrenCache: {}, + source: 'user', }); } @@ -32,10 +31,10 @@ suite('ConnectionTreeItem Test Suite', () => { let disconnectedRegisteredCommandInPackageJson = false; contributes.menus['view/item/context'].forEach((contextItem) => { - if (contextItem.when.includes(ConnectionItemContextValues.connected)) { + if (contextItem.when.includes('connected')) { connectedRegisteredCommandInPackageJson = true; } - if (contextItem.when.includes(ConnectionItemContextValues.disconnected)) { + if (contextItem.when.includes('disconnected')) { disconnectedRegisteredCommandInPackageJson = true; } }); diff --git a/src/test/suite/mdbExtensionController.test.ts b/src/test/suite/mdbExtensionController.test.ts index 440a83262..518b917bc 100644 --- a/src/test/suite/mdbExtensionController.test.ts +++ b/src/test/suite/mdbExtensionController.test.ts @@ -42,6 +42,7 @@ function getTestConnectionTreeItem( mdbTestExtension.testExtensionController._connectionController, cacheIsUpToDate: false, childrenCache: {}, + source: 'user', ...options, }); } diff --git a/src/test/suite/storage/connectionStorage.test.ts b/src/test/suite/storage/connectionStorage.test.ts index 30ae15719..2f336547d 100644 --- a/src/test/suite/storage/connectionStorage.test.ts +++ b/src/test/suite/storage/connectionStorage.test.ts @@ -15,12 +15,15 @@ import { TEST_DATABASE_URI_USER, TEST_USER_PASSWORD, } from '../dbTestHelper'; -import type { StoreConnectionInfo } from '../../../storage/connectionStorage'; +import type { LoadedConnection } from '../../../storage/connectionStorage'; import { ConnectionStorage } from '../../../storage/connectionStorage'; const testDatabaseConnectionName = 'localhost:27088'; -const newTestConnection = (connectionStorage: ConnectionStorage, id: string) => +const newTestConnection = ( + connectionStorage: ConnectionStorage, + id: string +): LoadedConnection => connectionStorage.createNewConnection({ connectionId: id, connectionOptions: { @@ -305,9 +308,7 @@ suite('Connection Storage Test Suite', function () { expect(connections.length).to.equal(1); const newSavedConnectionInfoWithSecrets = - await testConnectionStorage._getConnectionInfoWithSecrets( - connections[0] as StoreConnectionInfo - ); + await testConnectionStorage._getConnectionInfoWithSecrets(connections[0]); expect(newSavedConnectionInfoWithSecrets).to.deep.equal(connectionInfo); }); @@ -321,6 +322,134 @@ suite('Connection Storage Test Suite', function () { extensionSandbox.restore(); }); + suite('when there are preset connections', () => { + const presetConnections = { + globalValue: [ + { + name: 'Global Connection 1', + connectionString: + 'mongodb://localhost:27017/?readPreference=primary&ssl=false', + }, + ], + workspaceValue: [ + { + name: 'Preset Connection 1', + connectionString: 'mongodb://localhost:27017', + }, + { + name: 'Preset Connection 2', + connectionString: 'mongodb://localhost:27018', + }, + ], + }; + + let getConfigurationStub: sinon.SinonStub< + [ + section?: string | undefined, + scope?: vscode.ConfigurationScope | null | undefined + ], + vscode.WorkspaceConfiguration + >; + let inspectPresetConnectionsStub: sinon.SinonStub; + + beforeEach(() => { + testSandbox.restore(); + inspectPresetConnectionsStub = testSandbox.stub(); + }); + + test('loads the preset connections', async () => { + getConfigurationStub = testSandbox.stub( + vscode.workspace, + 'getConfiguration' + ); + getConfigurationStub.returns({ + inspect: inspectPresetConnectionsStub, + get: () => undefined, + } as any); + + inspectPresetConnectionsStub + .withArgs('presetConnections') + .returns(presetConnections); + + const loadedConnections = await testConnectionStorage.loadConnections(); + + const expectedConnectionValues = [ + ...presetConnections.globalValue.map((connection) => ({ + ...connection, + source: 'globalSettings', + })), + ...presetConnections.workspaceValue.map((connection) => ({ + ...connection, + source: 'workspaceSettings', + })), + ]; + + expect(loadedConnections.length).equals( + expectedConnectionValues.length + ); + + for (let i = 0; i < expectedConnectionValues.length; i++) { + const connection = loadedConnections[i]; + const expected = expectedConnectionValues[i]; + expect(connection.name).equals(expected.name); + expect(connection.connectionOptions.connectionString).equals( + expected.connectionString + ); + expect(connection.source).equals(expected.source); + } + }); + + test('loads both preset and other saved connections', async () => { + const savedConnection = newTestConnection(testConnectionStorage, '1'); + await testConnectionStorage.saveConnection(savedConnection); + + getConfigurationStub = testSandbox.stub( + vscode.workspace, + 'getConfiguration' + ); + getConfigurationStub.returns({ + inspect: inspectPresetConnectionsStub, + get: () => undefined, + } as any); + + inspectPresetConnectionsStub + .withArgs('presetConnections') + .returns(presetConnections); + + const loadedConnections = await testConnectionStorage.loadConnections(); + + const expectedConnectionValues = [ + { + name: savedConnection.name, + source: 'user', + connectionString: `${savedConnection.connectionOptions.connectionString}/`, + }, + ...presetConnections.globalValue.map((connection) => ({ + ...connection, + source: 'globalSettings', + })), + ...presetConnections.workspaceValue.map((connection) => ({ + ...connection, + source: 'workspaceSettings', + })), + ]; + + expect(loadedConnections.length).equals( + expectedConnectionValues.length + ); + + for (let i = 0; i < expectedConnectionValues.length; i++) { + const connection = loadedConnections[i]; + const expected = expectedConnectionValues[i]; + expect(connection.name).equals(expected.name); + expect(connection.connectionOptions.connectionString).equals( + expected.connectionString + ); + expect(connection.source).equals(expected.source); + } + }); + }); + suite('when connection secrets are already in SecretStorage', () => { afterEach(() => { testSandbox.restore(); @@ -341,6 +470,8 @@ suite('Connection Storage Test Suite', function () { // By default the connection secrets are already stored in SecretStorage const savedConnections = await testConnectionStorage.loadConnections(); + + expect(savedConnections.length).equals(2); expect( savedConnections.every( ({ secretStorageLocation }) => diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index cfdcf8be1..f9830ca9e 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -672,6 +672,7 @@ suite('Telemetry Controller Test Suite', () => { testTelemetryService.trackSavedConnectionsLoaded({ saved_connections: 3, loaded_connections: 3, + preset_connections: 3, connections_with_secrets_in_keytar: 0, connections_with_secrets_in_secret_storage: 3, }); @@ -684,6 +685,7 @@ suite('Telemetry Controller Test Suite', () => { properties: { saved_connections: 3, loaded_connections: 3, + preset_connections: 3, connections_with_secrets_in_keytar: 0, connections_with_secrets_in_secret_storage: 3, }, From 0816d1c0cf3bc7f0b0c325e549ce0c260b1d5e8b Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 23 Jan 2025 13:11:42 +0100 Subject: [PATCH 05/22] chore(telemetry): refactor telemetry types VSCODE-672 (#912) --- src/connectionController.ts | 37 +- src/documentSource.ts | 2 + src/editors/editorsController.ts | 2 +- src/editors/mongoDBDocumentService.ts | 15 +- src/editors/playgroundController.ts | 68 +- .../queryWithCopilotCodeLensProvider.ts | 1 + src/explorer/helpTree.ts | 5 +- src/mdbExtensionController.ts | 14 +- src/participant/participant.ts | 182 +++-- src/participant/participantTypes.ts | 9 +- src/participant/prompts/docs.ts | 2 +- src/participant/prompts/intent.ts | 2 +- src/participant/prompts/namespace.ts | 2 +- src/participant/prompts/promptBase.ts | 12 +- src/telemetry/connectionTelemetry.ts | 6 +- src/telemetry/index.ts | 5 +- src/telemetry/telemetryEvents.ts | 652 ++++++++++++++++++ src/telemetry/telemetryService.ts | 390 +---------- src/test/suite/connectionController.test.ts | 9 +- .../activeConnectionCodeLensProvider.test.ts | 2 +- .../collectionDocumentsProvider.test.ts | 5 +- .../editDocumentCodeLensProvider.test.ts | 2 +- .../exportToLanguageCodeLensProvider.test.ts | 2 +- .../editors/mongoDBDocumentService.test.ts | 2 +- .../editors/playgroundController.test.ts | 2 +- .../editors/playgroundResultProvider.test.ts | 2 +- src/test/suite/explorer/helpExplorer.test.ts | 17 +- .../language/languageServerController.test.ts | 2 +- .../suite/participant/participant.test.ts | 207 +++--- .../suite/telemetry/telemetryService.test.ts | 118 ++-- .../suite/views/webviewController.test.ts | 2 +- src/utils/playground.ts | 14 - src/views/webviewController.ts | 16 +- 33 files changed, 1085 insertions(+), 723 deletions(-) create mode 100644 src/telemetry/telemetryEvents.ts diff --git a/src/connectionController.ts b/src/connectionController.ts index 159ecea8e..f7f396326 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -19,7 +19,7 @@ import { createLogger } from './logging'; import formatError from './utils/formatError'; import type { StorageController } from './storage'; import type { StatusView } from './views'; -import type TelemetryService from './telemetry/telemetryService'; +import type { TelemetryService } from './telemetry'; import { openLink } from './utils/linkHelper'; import type { ConnectionSource, @@ -28,8 +28,8 @@ import type { import { ConnectionStorage } from './storage/connectionStorage'; import LINKS from './utils/links'; import { isAtlasStream } from 'mongodb-build-info'; -import { DocumentSource } from './documentSource'; import type { ConnectionTreeItem } from './explorer'; +import { PresetConnectionEditedTelemetryEvent } from './telemetry'; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJSON = require('../package.json'); @@ -129,10 +129,12 @@ export default class ConnectionController { // have a setting on the system for storing credentials. // When the setting is on this `connectionMergeInfos` would have the session // credential information and merge it before connecting. - connectionMergeInfos: Record> = - Object.create(null); + private _connectionMergeInfos: Record< + string, + RecursivePartial + > = Object.create(null); - _activeDataService: DataService | null = null; + private _activeDataService: DataService | null = null; _connectionStorage: ConnectionStorage; _telemetryService: TelemetryService; @@ -140,7 +142,7 @@ export default class ConnectionController { private _currentConnectionId: null | string = null; _connectionAttempt: null | ConnectionAttempt = null; - _connectionStringInputCancellationToken: null | vscode.CancellationTokenSource = + private _connectionStringInputCancellationToken: null | vscode.CancellationTokenSource = null; private _connectingConnectionId: null | string = null; private _disconnecting = false; @@ -169,10 +171,11 @@ export default class ConnectionController { async openPresetConnectionsSettings( originTreeItem: ConnectionTreeItem | undefined ): Promise { - this._telemetryService.trackPresetConnectionEdited({ - source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, - source_details: originTreeItem ? 'tree_item' : 'header', - }); + this._telemetryService.track( + new PresetConnectionEditedTelemetryEvent( + originTreeItem ? 'tree_item' : 'header' + ) + ); let source: ConnectionSource | undefined = originTreeItem?.source; if (!source) { const mdbConfiguration = vscode.workspace.getConfiguration('mdb'); @@ -227,7 +230,7 @@ export default class ConnectionController { // TODO: re-enable with fewer 'Saved Connections Loaded' events // https://jira.mongodb.org/browse/VSCODE-462 - /* this._telemetryService.trackSavedConnectionsLoaded({ + /* this._telemetryService.track(new SavedConnectionsLoadedTelemetryEvent({ saved_connections: globalAndWorkspaceConnections.length, loaded_connections: loadedConnections.length, ).length, @@ -236,7 +239,7 @@ export default class ConnectionController { connection.secretStorageLocation === SecretStorageLocation.SecretStorage ).length, - }); */ + })); */ } async connectWithURI(): Promise { @@ -402,7 +405,7 @@ export default class ConnectionController { const connectionInfo: LoadedConnection = merge( cloneDeep(this._connections[connectionId]), - this.connectionMergeInfos[connectionId] ?? {} + this._connectionMergeInfos[connectionId] ?? {} ); if (!connectionInfo.connectionOptions) { @@ -557,8 +560,8 @@ export default class ConnectionController { mergeConnectionInfo = { connectionOptions: await dataService.getUpdatedSecrets(), }; - this.connectionMergeInfos[connectionInfo.id] = merge( - cloneDeep(this.connectionMergeInfos[connectionInfo.id]), + this._connectionMergeInfos[connectionInfo.id] = merge( + cloneDeep(this._connectionMergeInfos[connectionInfo.id]), mergeConnectionInfo ); } @@ -585,8 +588,8 @@ export default class ConnectionController { connectionOptions: await dataService.getUpdatedSecrets(), }; if (!mergeConnectionInfo) return; - this.connectionMergeInfos[connectionInfo.id] = merge( - cloneDeep(this.connectionMergeInfos[connectionInfo.id]), + this._connectionMergeInfos[connectionInfo.id] = merge( + cloneDeep(this._connectionMergeInfos[connectionInfo.id]), mergeConnectionInfo ); diff --git a/src/documentSource.ts b/src/documentSource.ts index 329032a86..066c96cb6 100644 --- a/src/documentSource.ts +++ b/src/documentSource.ts @@ -4,3 +4,5 @@ export enum DocumentSource { DOCUMENT_SOURCE_COLLECTIONVIEW = 'collectionview', DOCUMENT_SOURCE_CODELENS = 'codelens', } + +export type DocumentSourceDetails = 'database' | 'collection' | undefined; diff --git a/src/editors/editorsController.ts b/src/editors/editorsController.ts index 66f1024c5..67285595e 100644 --- a/src/editors/editorsController.ts +++ b/src/editors/editorsController.ts @@ -31,7 +31,7 @@ import type PlaygroundController from './playgroundController'; import type PlaygroundResultProvider from './playgroundResultProvider'; import { PLAYGROUND_RESULT_SCHEME } from './playgroundResultProvider'; import { StatusView } from '../views'; -import type TelemetryService from '../telemetry/telemetryService'; +import type { TelemetryService } from '../telemetry'; import type { QueryWithCopilotCodeLensProvider } from './queryWithCopilotCodeLensProvider'; const log = createLogger('editors controller'); diff --git a/src/editors/mongoDBDocumentService.ts b/src/editors/mongoDBDocumentService.ts index 1806cddda..a67a9b446 100644 --- a/src/editors/mongoDBDocumentService.ts +++ b/src/editors/mongoDBDocumentService.ts @@ -7,8 +7,9 @@ import { DocumentSource } from '../documentSource'; import type { EditDocumentInfo } from '../types/editDocumentInfoType'; import formatError from '../utils/formatError'; import type { StatusView } from '../views'; -import type TelemetryService from '../telemetry/telemetryService'; +import type { TelemetryService } from '../telemetry'; import { getEJSON } from '../utils/ejson'; +import { DocumentUpdatedTelemetryEvent } from '../telemetry'; const log = createLogger('document controller'); @@ -50,9 +51,11 @@ export default class MongoDBDocumentService { _saveDocumentFailed(message: string): void { const errorMessage = `Unable to save document: ${message}`; - this._telemetryService.trackDocumentUpdated( - DocumentSource.DOCUMENT_SOURCE_TREEVIEW, - false + this._telemetryService.track( + new DocumentUpdatedTelemetryEvent( + DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + false + ) ); throw new Error(errorMessage); @@ -98,7 +101,9 @@ export default class MongoDBDocumentService { returnDocument: 'after', } ); - this._telemetryService.trackDocumentUpdated(source, true); + this._telemetryService.track( + new DocumentUpdatedTelemetryEvent(source, true) + ); } catch (error) { return this._saveDocumentFailed(formatError(error).message); } finally { diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index 7f2c2969d..4268c82b2 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -34,16 +34,16 @@ import { import playgroundSearchTemplate from '../templates/playgroundSearchTemplate'; import playgroundTemplate from '../templates/playgroundTemplate'; import type { StatusView } from '../views'; -import type TelemetryService from '../telemetry/telemetryService'; -import { - isPlayground, - getSelectedText, - getAllText, - getPlaygroundExtensionForTelemetry, -} from '../utils/playground'; +import type { TelemetryService } from '../telemetry'; +import { isPlayground, getSelectedText, getAllText } from '../utils/playground'; import type ExportToLanguageCodeLensProvider from './exportToLanguageCodeLensProvider'; import { playgroundFromDatabaseTreeItemTemplate } from '../templates/playgroundFromDatabaseTreeItemTemplate'; import { playgroundFromCollectionTreeItemTemplate } from '../templates/playgroundFromCollectionTreeItemTemplate'; +import { + PlaygroundCreatedTelemetryEvent, + PlaygroundExecutedTelemetryEvent, + PlaygroundSavedTelemetryEvent, +} from '../telemetry'; const log = createLogger('playground controller'); @@ -135,17 +135,15 @@ export default class PlaygroundController { if (isPlayground(document.uri)) { // TODO: re-enable with fewer 'Playground Loaded' events // https://jira.mongodb.org/browse/VSCODE-432 - /* this._telemetryService.trackPlaygroundLoaded( - getPlaygroundExtensionForTelemetry(document.uri) - ); */ + // this._telemetryService.track(new PlaygroundLoadedTelemetryEvent(document.uri)); await vscode.languages.setTextDocumentLanguage(document, 'javascript'); } }); vscode.workspace.onDidSaveTextDocument((document) => { if (isPlayground(document.uri)) { - this._telemetryService.trackPlaygroundSaved( - getPlaygroundExtensionForTelemetry(document.uri) + this._telemetryService.track( + new PlaygroundSavedTelemetryEvent(document.uri) ); } }); @@ -231,7 +229,7 @@ export default class PlaygroundController { .replace('CURRENT_DATABASE', databaseName) .replace('CURRENT_COLLECTION', collectionName); - this._telemetryService.trackPlaygroundCreated('search'); + this._telemetryService.track(new PlaygroundCreatedTelemetryEvent('search')); return this._createPlaygroundFileWithContent(content); } @@ -246,9 +244,13 @@ export default class PlaygroundController { content = content .replace('NEW_DATABASE_NAME', element.databaseName) .replace('Create a new database', 'The current database to use'); - this._telemetryService.trackPlaygroundCreated('createCollection'); + this._telemetryService.track( + new PlaygroundCreatedTelemetryEvent('createCollection') + ); } else { - this._telemetryService.trackPlaygroundCreated('createDatabase'); + this._telemetryService.track( + new PlaygroundCreatedTelemetryEvent('createDatabase') + ); } return this._createPlaygroundFileWithContent(content); @@ -262,7 +264,7 @@ export default class PlaygroundController { .replace('CURRENT_DATABASE', databaseName) .replace('CURRENT_COLLECTION', collectionName); - this._telemetryService.trackPlaygroundCreated('index'); + this._telemetryService.track(new PlaygroundCreatedTelemetryEvent('index')); return this._createPlaygroundFileWithContent(content); } @@ -277,7 +279,7 @@ export default class PlaygroundController { const content = useDefaultTemplate ? playgroundBasicTextTemplate.replace('PLAYGROUND_CONTENT', text) : text; - this._telemetryService.trackPlaygroundCreated('agent'); + this._telemetryService.track(new PlaygroundCreatedTelemetryEvent('agent')); return this._createPlaygroundFileWithContent(content); } @@ -291,7 +293,9 @@ export default class PlaygroundController { .replace('CURRENT_COLLECTION', collectionName) .replace('DOCUMENT_CONTENTS', documentContents); - this._telemetryService.trackPlaygroundCreated('cloneDocument'); + this._telemetryService.track( + new PlaygroundCreatedTelemetryEvent('cloneDocument') + ); return this._createPlaygroundFileWithContent(content); } @@ -303,7 +307,9 @@ export default class PlaygroundController { .replace('CURRENT_DATABASE', databaseName) .replace('CURRENT_COLLECTION', collectionName); - this._telemetryService.trackPlaygroundCreated('insertDocument'); + this._telemetryService.track( + new PlaygroundCreatedTelemetryEvent('insertDocument') + ); return this._createPlaygroundFileWithContent(content); } @@ -314,7 +320,9 @@ export default class PlaygroundController { element.cacheIsUpToDate = false; - this._telemetryService.trackPlaygroundCreated('createStreamProcessor'); + this._telemetryService.track( + new PlaygroundCreatedTelemetryEvent('createStreamProcessor') + ); return this._createPlaygroundFileWithContent(content); } @@ -325,13 +333,17 @@ export default class PlaygroundController { let content = ''; if (treeItem instanceof DatabaseTreeItem) { content = playgroundFromDatabaseTreeItemTemplate(treeItem.databaseName); - this._telemetryService.trackPlaygroundCreated('fromDatabaseTreeItem'); + this._telemetryService.track( + new PlaygroundCreatedTelemetryEvent('fromDatabaseTreeItem') + ); } else if (treeItem instanceof CollectionTreeItem) { content = playgroundFromCollectionTreeItemTemplate( treeItem.databaseName, treeItem.collectionName ); - this._telemetryService.trackPlaygroundCreated('fromCollectionTreeItem'); + this._telemetryService.track( + new PlaygroundCreatedTelemetryEvent('fromCollectionTreeItem') + ); } return this._createPlaygroundFileWithContent(content); @@ -350,7 +362,7 @@ export default class PlaygroundController { content = template; } - this._telemetryService.trackPlaygroundCreated('crud'); + this._telemetryService.track(new PlaygroundCreatedTelemetryEvent('crud')); return this._createPlaygroundFileWithContent(content); } @@ -391,10 +403,12 @@ export default class PlaygroundController { } this._statusView.hideMessage(); - this._telemetryService.trackPlaygroundCodeExecuted( - result, - this._isPartialRun, - result ? false : true + this._telemetryService.track( + new PlaygroundExecutedTelemetryEvent( + result, + this._isPartialRun, + result ? false : true + ) ); return result; diff --git a/src/editors/queryWithCopilotCodeLensProvider.ts b/src/editors/queryWithCopilotCodeLensProvider.ts index 993d718a7..d1a95c7f7 100644 --- a/src/editors/queryWithCopilotCodeLensProvider.ts +++ b/src/editors/queryWithCopilotCodeLensProvider.ts @@ -35,6 +35,7 @@ export class QueryWithCopilotCodeLensProvider isNewChat: true, telemetry: { source: DocumentSource.DOCUMENT_SOURCE_CODELENS, + source_details: undefined, }, }; diff --git a/src/explorer/helpTree.ts b/src/explorer/helpTree.ts index 5d54c97e8..ca0f5bc94 100644 --- a/src/explorer/helpTree.ts +++ b/src/explorer/helpTree.ts @@ -4,6 +4,7 @@ import { getImagesPath } from '../extensionConstants'; import type { TelemetryService } from '../telemetry'; import { openLink } from '../utils/linkHelper'; import LINKS from '../utils/links'; +import { LinkClickedTelemetryEvent } from '../telemetry'; const HELP_LINK_CONTEXT_VALUE = 'HELP_LINK'; @@ -144,7 +145,9 @@ export default class HelpTree telemetryService: TelemetryService ): Promise { if (helpItem.contextValue === HELP_LINK_CONTEXT_VALUE) { - telemetryService.trackLinkClicked('helpPanel', helpItem.linkId); + telemetryService.track( + new LinkClickedTelemetryEvent('helpPanel', helpItem.linkId) + ); if (helpItem.useRedirect) { try { diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index b6f0b9505..dad644070 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -33,7 +33,7 @@ import launchMongoShell from './commands/launchMongoShell'; import type SchemaTreeItem from './explorer/schemaTreeItem'; import { StatusView } from './views'; import { StorageController, StorageVariables } from './storage'; -import TelemetryService from './telemetry/telemetryService'; +import { TelemetryService } from './telemetry'; import type PlaygroundsTreeItem from './explorer/playgroundsTreeItem'; import PlaygroundResultProvider from './editors/playgroundResultProvider'; import WebviewController from './views/webviewController'; @@ -51,6 +51,10 @@ import type { } from './participant/participantTypes'; import EXTENSION_COMMANDS from './commands'; import { COPILOT_EXTENSION_ID } from './participant/constants'; +import { + CommandRunTelemetryEvent, + DocumentEditedTelemetryEvent, +} from './telemetry'; // This class is the top-level controller for our extension. // Commands which the extensions handles are defined in the function `activate`. @@ -313,7 +317,9 @@ export default class MDBExtensionController implements vscode.Disposable { this.registerCommand( EXTENSION_COMMANDS.MDB_OPEN_MONGODB_DOCUMENT_FROM_CODE_LENS, (data: EditDocumentInfo) => { - this._telemetryService.trackDocumentOpenedInEditor(data.source); + this._telemetryService.track( + new DocumentEditedTelemetryEvent(data.source) + ); return this._editorsController.openMongoDBDocument(data); } @@ -408,7 +414,7 @@ export default class MDBExtensionController implements vscode.Disposable { commandHandler: (...args: any[]) => Promise ): void => { const commandHandlerWithTelemetry = (args: any[]): Promise => { - this._telemetryService.trackCommandRun(command); + this._telemetryService.track(new CommandRunTelemetryEvent(command)); return commandHandler(args); }; @@ -426,7 +432,7 @@ export default class MDBExtensionController implements vscode.Disposable { commandHandler: (...args: any[]) => Promise ): void => { const commandHandlerWithTelemetry = (args: any[]): Promise => { - this._telemetryService.trackCommandRun(command); + this._telemetryService.track(new CommandRunTelemetryEvent(command)); return commandHandler(args); }; diff --git a/src/participant/participant.ts b/src/participant/participant.ts index f54f5ff83..fd36fa7d5 100644 --- a/src/participant/participant.ts +++ b/src/participant/participant.ts @@ -34,11 +34,19 @@ import { type OpenSchemaCommandArgs, } from './prompts/schema'; import { - chatResultFeedbackKindToTelemetryValue, - TelemetryEventTypes, -} from '../telemetry/telemetryService'; + ExportToPlaygroundFailedTelemetryEvent, + ParticipantChatOpenedFromActionTelemetryEvent, + ParticipantFeedbackTelemetryEvent, + ParticipantInputBoxSubmittedTelemetryEvent, + ParticipantPromptSubmittedFromActionTelemetryEvent, + ParticipantPromptSubmittedTelemetryEvent, + ParticipantResponseFailedTelemetryEvent, + ParticipantResponseGeneratedTelemetryEvent, + ParticipantWelcomeShownTelemetryEvent, + PlaygroundExportedToLanguageTelemetryEvent, +} from '../telemetry'; import { DocsChatbotAIService } from './docsChatbotAIService'; -import type TelemetryService from '../telemetry/telemetryService'; +import type { TelemetryService } from '../telemetry'; import formatError from '../utils/formatError'; import { getContent, type ModelInput } from './prompts/promptBase'; import { processStreamWithIdentifiers } from './streamParsing'; @@ -163,17 +171,18 @@ export default class ParticipantController { if (telemetry) { if (isNewChat) { - this._telemetryService.trackParticipantChatOpenedFromAction({ - ...telemetry, - command, - }); + this._telemetryService.track( + new ParticipantChatOpenedFromActionTelemetryEvent(telemetry, command) + ); } if (!isPartialQuery) { - this._telemetryService.trackParticipantPromptSubmittedFromAction({ - ...telemetry, - command: command ?? 'generic', - input_length: query.length, - }); + this._telemetryService.track( + new ParticipantPromptSubmittedFromActionTelemetryEvent( + telemetry, + command ?? 'generic', + query.length + ) + ); } } @@ -200,12 +209,13 @@ export default class ParticipantController { }); if (telemetry) { - this._telemetryService.trackParticipantInputBoxSubmitted({ - ...telemetry, - input_length: message?.length, - dismissed: message === undefined, - command, - }); + this._telemetryService.track( + new ParticipantInputBoxSubmittedTelemetryEvent( + telemetry, + message, + command + ) + ); } if (message === undefined || message.trim() === '') { @@ -273,7 +283,9 @@ export default class ParticipantController { }) ), }); - this._telemetryService.trackParticipantPrompt(modelInput.stats); + this._telemetryService.track( + new ParticipantPromptSubmittedTelemetryEvent(modelInput.stats) + ); const modelResponse = await model.sendRequest( modelInput.messages, @@ -462,13 +474,15 @@ export default class ParticipantController { stream, }); - this._telemetryService.trackParticipantResponse({ - command: 'generic', - has_cta: false, - found_namespace: false, - has_runnable_content: hasCodeBlock, - output_length: outputLength, - }); + this._telemetryService.track( + new ParticipantResponseGeneratedTelemetryEvent({ + command: 'generic', + hasCta: false, + foundNamespace: false, + hasRunnableContent: hasCodeBlock, + outputLength: outputLength, + }) + ); return genericRequestChatResult(context.history); } @@ -1429,13 +1443,15 @@ export default class ParticipantController { ], }); - this._telemetryService.trackParticipantResponse({ - command: 'schema', - has_cta: true, - found_namespace: true, - has_runnable_content: false, - output_length: response.outputLength, - }); + this._telemetryService.track( + new ParticipantResponseGeneratedTelemetryEvent({ + command: 'schema', + hasCta: true, + foundNamespace: true, + hasRunnableContent: false, + outputLength: response.outputLength, + }) + ); return schemaRequestChatResult(context.history); } @@ -1540,13 +1556,15 @@ export default class ParticipantController { token, }); - this._telemetryService.trackParticipantResponse({ - command: 'query', - has_cta: false, - found_namespace: true, - has_runnable_content: hasCodeBlock, - output_length: outputLength, - }); + this._telemetryService.track( + new ParticipantResponseGeneratedTelemetryEvent({ + command: 'query', + hasCta: false, + foundNamespace: true, + hasRunnableContent: hasCodeBlock, + outputLength: outputLength, + }) + ); return queryRequestChatResult(context.history); } @@ -1612,7 +1630,9 @@ export default class ParticipantController { const stats = Prompts.docs.getStats(history, { request, context }); - this._telemetryService.trackParticipantPrompt(stats); + this._telemetryService.track( + new ParticipantPromptSubmittedTelemetryEvent(stats) + ); log.info('Docs chatbot message sent', { chatId, @@ -1651,13 +1671,15 @@ export default class ParticipantController { this._streamGenericDocsLink(stream); - this._telemetryService.trackParticipantResponse({ - command: 'docs/copilot', - has_cta: true, - found_namespace: false, - has_runnable_content: hasCodeBlock, - output_length: outputLength, - }); + this._telemetryService.track( + new ParticipantResponseGeneratedTelemetryEvent({ + command: 'docs/copilot', + hasCta: true, + foundNamespace: false, + hasRunnableContent: hasCodeBlock, + outputLength: outputLength, + }) + ); } _streamResponseReference({ @@ -1731,13 +1753,15 @@ export default class ParticipantController { } } - this._telemetryService.trackParticipantResponse({ - command: 'docs/chatbot', - has_cta: !!docsResult.responseReferences, - found_namespace: false, - has_runnable_content: false, - output_length: docsResult.responseContent?.length ?? 0, - }); + this._telemetryService.track( + new ParticipantResponseGeneratedTelemetryEvent({ + command: 'docs/chatbot', + hasCta: !!docsResult.responseReferences, + foundNamespace: false, + hasRunnableContent: false, + outputLength: docsResult.responseContent?.length ?? 0, + }) + ); } catch (error) { // If the docs chatbot API is not available, fall back to Copilot’s LLM and include // the MongoDB documentation link for users to go to our documentation site directly. @@ -1751,11 +1775,10 @@ export default class ParticipantController { } this._telemetryService.track( - TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED, - { - command: 'docs', - error_name: ParticipantErrorTypes.DOCS_CHATBOT_API, - } + new ParticipantResponseFailedTelemetryEvent( + 'docs', + ParticipantErrorTypes.DOCS_CHATBOT_API + ) ); await this._handleDocsRequestWithCopilot(...args); @@ -1857,10 +1880,12 @@ export default class ParticipantController { // Content in this case is already equal to the failureType; this is just to make it explicit // and avoid accidentally sending actual contents of the message. - this._telemetryService.trackExportToPlaygroundFailed({ - input_length: codeToExport?.length, - error_name: error, - }); + this._telemetryService.track( + new ExportToPlaygroundFailedTelemetryEvent( + codeToExport?.length, + error + ) + ); return false; } @@ -1957,7 +1982,7 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i ); this._telemetryService.track( - TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN + new ParticipantWelcomeShownTelemetryEvent() ); await this._storageController.update( @@ -2033,11 +2058,13 @@ 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.trackParticipantFeedback({ - feedback: chatResultFeedbackKindToTelemetryValue(feedback.kind), - reason: unhelpfulReason, - response_type: (feedback.result as ChatResult)?.metadata.intent, - }); + this._telemetryService.track( + new ParticipantFeedbackTelemetryEvent( + feedback.kind, + (feedback.result as ChatResult)?.metadata.intent, + unhelpfulReason + ) + ); } _getConnectionNames(): string[] { @@ -2119,11 +2146,14 @@ Please see our [FAQ](https://www.mongodb.com/docs/generative-ai-faq/) for more i language, includeDriverSyntax, }); - this._telemetryService.trackPlaygroundExportedToLanguageExported({ - language, - exported_code_length: transpiledContent?.length || 0, - with_driver_syntax: includeDriverSyntax, - }); + + this._telemetryService.track( + new PlaygroundExportedToLanguageTelemetryEvent( + language, + transpiledContent?.length, + includeDriverSyntax + ) + ); await vscode.commands.executeCommand( EXTENSION_COMMANDS.SHOW_EXPORT_TO_LANGUAGE_RESULT, diff --git a/src/participant/participantTypes.ts b/src/participant/participantTypes.ts index b8fdc257f..3b65ceef0 100644 --- a/src/participant/participantTypes.ts +++ b/src/participant/participantTypes.ts @@ -1,5 +1,5 @@ import type * as vscode from 'vscode'; -import type { DocumentSource } from '../documentSource'; +import type { ParticipantTelemetryMetadata } from '../telemetry'; export type ParticipantCommandType = 'query' | 'schema' | 'docs'; export type ParticipantCommand = `/${ParticipantCommandType}`; @@ -19,18 +19,13 @@ export type ParticipantResponseType = | '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; + telemetry?: ParticipantTelemetryMetadata; }; export type SendMessageToParticipantFromInputOptions = Pick< diff --git a/src/participant/prompts/docs.ts b/src/participant/prompts/docs.ts index 316e12809..41b1cfe59 100644 --- a/src/participant/prompts/docs.ts +++ b/src/participant/prompts/docs.ts @@ -1,4 +1,4 @@ -import type { ParticipantPromptProperties } from '../../telemetry/telemetryService'; +import type { ParticipantPromptProperties } from '../../telemetry'; import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; import type * as vscode from 'vscode'; diff --git a/src/participant/prompts/intent.ts b/src/participant/prompts/intent.ts index 8a1266f69..02096fbd4 100644 --- a/src/participant/prompts/intent.ts +++ b/src/participant/prompts/intent.ts @@ -1,4 +1,4 @@ -import type { InternalPromptPurpose } from '../../telemetry/telemetryService'; +import type { InternalPromptPurpose } from '../../telemetry'; import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; diff --git a/src/participant/prompts/namespace.ts b/src/participant/prompts/namespace.ts index c5428f191..2cbd5bdbb 100644 --- a/src/participant/prompts/namespace.ts +++ b/src/participant/prompts/namespace.ts @@ -1,4 +1,4 @@ -import type { InternalPromptPurpose } from '../../telemetry/telemetryService'; +import type { InternalPromptPurpose } from '../../telemetry'; import type { PromptArgsBase } from './promptBase'; import { PromptBase } from './promptBase'; diff --git a/src/participant/prompts/promptBase.ts b/src/participant/prompts/promptBase.ts index 4bde40d67..e49bd6775 100644 --- a/src/participant/prompts/promptBase.ts +++ b/src/participant/prompts/promptBase.ts @@ -3,7 +3,7 @@ import type { ChatResult } from '../constants'; import type { InternalPromptPurpose, ParticipantPromptProperties, -} from '../../telemetry/telemetryService'; +} from '../../telemetry'; import { PromptHistory } from './promptHistory'; import type { ParticipantCommandType } from '../participantTypes'; import { getCopilotModel } from '../model'; @@ -208,15 +208,15 @@ export abstract class PromptBase { hasSampleDocs: boolean ): ParticipantPromptProperties { return { - total_message_length: messages.reduce( + totalMessageLength: messages.reduce( (acc, message) => acc + getContentLength(message), 0 ), - user_input_length: request.prompt.length, - has_sample_documents: hasSampleDocs, + userInputLength: request.prompt.length, + hasSampleDocuments: hasSampleDocs, command: (request.command as ParticipantCommandType) || 'generic', - history_size: context?.history.length || 0, - internal_purpose: this.internalPurposeForTelemetry, + historySize: context?.history.length || 0, + internalPurpose: this.internalPurposeForTelemetry, }; } } diff --git a/src/telemetry/connectionTelemetry.ts b/src/telemetry/connectionTelemetry.ts index c92f675ac..ecdbf37ed 100644 --- a/src/telemetry/connectionTelemetry.ts +++ b/src/telemetry/connectionTelemetry.ts @@ -39,13 +39,13 @@ export type HostInformation = { function getHostnameForConnection(dataService: DataService): string | null { const lastSeenTopology = dataService.getLastSeenTopology(); - const resolvedHost = lastSeenTopology?.servers.values().next().value.address; + const resolvedHost = lastSeenTopology?.servers.values().next().value?.address; - if (resolvedHost.startsWith('[')) { + if (resolvedHost?.startsWith('[')) { return resolvedHost.slice(1).split(']')[0]; // IPv6 } - return resolvedHost.split(':')[0]; + return resolvedHost?.split(':')[0] || null; } async function getPublicCloudInfo(host: string): Promise<{ diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts index 7fc815c66..b06c36a30 100644 --- a/src/telemetry/index.ts +++ b/src/telemetry/index.ts @@ -1,3 +1,2 @@ -import TelemetryService from './telemetryService'; - -export { TelemetryService }; +export * from './telemetryEvents'; +export { TelemetryService } from './telemetryService'; diff --git a/src/telemetry/telemetryEvents.ts b/src/telemetry/telemetryEvents.ts new file mode 100644 index 000000000..4c4554fea --- /dev/null +++ b/src/telemetry/telemetryEvents.ts @@ -0,0 +1,652 @@ +import type { ExtensionCommand } from '../commands'; +import type { DocumentSourceDetails } from '../documentSource'; +import { DocumentSource } from '../documentSource'; +import type { + ExportToPlaygroundError, + ParticipantErrorTypes, +} from '../participant/participantErrorTypes'; +import type { + ParticipantCommandType, + ParticipantRequestType, + ParticipantResponseType, +} from '../participant/participantTypes'; +import type { ShellEvaluateResult } from '../types/playgroundType'; +import type { NewConnectionTelemetryEventProperties } from './connectionTelemetry'; +import * as vscode from 'vscode'; + +type PlaygroundFileType = 'other' | 'mongodbjs' | 'mongodb'; + +type TelemetryFeedbackKind = 'positive' | 'negative' | undefined; + +/** + * The purpose of the internal prompt - e.g. 'intent', 'namespace' + */ +export type InternalPromptPurpose = 'intent' | 'namespace' | undefined; + +export type ParticipantTelemetryMetadata = { + /** The source of the participant prompt - e.g. 'codelens', 'treeview', etc. */ + source: DocumentSource; + + /** Additional details about the source - e.g. if it's 'treeview', the detail can be 'database' or 'collection'. */ + source_details: DocumentSourceDetails; +}; + +export type ParticipantPromptProperties = { + command: ParticipantCommandType; + userInputLength: number; + totalMessageLength: number; + hasSampleDocuments: boolean; + historySize: number; + internalPurpose: InternalPromptPurpose; +}; + +function getPlaygroundFileTypeFromUri( + fileUri?: vscode.Uri +): PlaygroundFileType { + let fileType: PlaygroundFileType = 'other'; + + if (fileUri?.fsPath.match(/\.(mongodb\.js)$/gi)) { + fileType = 'mongodbjs'; + } else if (fileUri?.fsPath.match(/\.(mongodb)$/gi)) { + fileType = 'mongodb'; + } + + return fileType; +} + +type PlaygroundType = + | 'search' + | 'createCollection' + | 'createDatabase' + | 'index' + | 'agent' + | 'cloneDocument' + | 'insertDocument' + | 'createStreamProcessor' + | 'fromDatabaseTreeItem' + | 'fromCollectionTreeItem' + | 'crud'; + +abstract class TelemetryEventBase { + abstract type: string; + abstract properties: Record; +} + +/** Reported when a playground file is run */ +export class PlaygroundExecutedTelemetryEvent implements TelemetryEventBase { + type = 'Playground Code Executed'; + properties: { + /** The type of the executed operation, e.g. 'insert', 'update', 'delete', 'query', 'aggregation', 'other' */ + type: string | null; + + /** Whether the entire script was run or just a part of it */ + partial: boolean; + + /** Whether an error occurred during execution */ + error: boolean; + }; + + constructor(result: ShellEvaluateResult, partial: boolean, error: boolean) { + this.properties = { + type: result ? this.getPlaygroundResultType(result) : null, + partial, + error, + }; + } + + private getPlaygroundResultType(res: ShellEvaluateResult): string { + if (!res || !res.result || !res.result.type) { + return 'other'; + } + + const shellApiType = res.result.type.toLocaleLowerCase(); + + // See: https://github.com/mongodb-js/mongosh/blob/main/packages/shell-api/src/shell-api.ts + if (shellApiType.includes('insert')) { + return 'insert'; + } + if (shellApiType.includes('update')) { + return 'update'; + } + if (shellApiType.includes('delete')) { + return 'delete'; + } + if (shellApiType.includes('aggregation')) { + return 'aggregation'; + } + if (shellApiType.includes('cursor')) { + return 'query'; + } + + return 'other'; + } +} + +/** Reported when a user clicks a hyperlink - e.g. from the Help pane */ +export class LinkClickedTelemetryEvent implements TelemetryEventBase { + type = 'Link Clicked'; + properties: { + /** The screen where the link was clicked */ + screen: string; + + /** The ID of the clicked link - e.g. `whatsNew`, `extensionDocumentation`, etc. */ + link_id: string; + }; + + constructor(screen: string, linkId: string) { + this.properties = { screen, link_id: linkId }; + } +} + +/** + * Reported when any command is run by the user. Commands are the building blocks + * of the extension and can be executed either by clicking a UI element or by opening + * the command pallette (CMD+Shift+P). This event is likely to duplicate other events + * as it's fired automatically, regardless of other more-specific events. + */ +export class CommandRunTelemetryEvent implements TelemetryEventBase { + type = 'Command Run'; + properties: { + /** The command that was executed - e.g. `mdb.connect`, `mdb.openMongoDBIssueReporter`, etc. */ + command: ExtensionCommand; + }; + + constructor(command: ExtensionCommand) { + this.properties = { command }; + } +} + +/** Reported every time we connect to a cluster/db */ +export class NewConnectionTelemetryEvent implements TelemetryEventBase { + type = 'New Connection'; + properties: NewConnectionTelemetryEventProperties; + + constructor(properties: NewConnectionTelemetryEventProperties) { + this.properties = properties; + } +} + +/** Reported when a connection is edited */ +export class ConnectionEditedTelemetryEvent implements TelemetryEventBase { + type = 'Connection Edited'; + properties = {}; +} + +/** Reported when the user opens the connection editor */ +export class OpenEditConnectionTelemetryEvent implements TelemetryEventBase { + type = 'Open Edit Connection'; + properties = {}; +} + +/** Reported when a playground file is saved */ +export class PlaygroundSavedTelemetryEvent implements TelemetryEventBase { + type = 'Playground Saved'; + properties: { + /** The type of the file, e.g. 'mongodbjs' for .mongodb.js or 'mongodb' for .mongodb */ + file_type: PlaygroundFileType; + }; + + constructor(fileUri?: vscode.Uri) { + this.properties = { file_type: getPlaygroundFileTypeFromUri(fileUri) }; + } +} + +/** Reported when a playground file is opened */ +export class PlaygroundLoadedTelemetryEvent implements TelemetryEventBase { + type = 'Playground Loaded'; + properties: { + /** The type of the file, e.g. 'mongodbjs' for .mongodb.js or 'mongodb' for .mongodb */ + file_type: PlaygroundFileType; + }; + + constructor(fileUri?: vscode.Uri) { + this.properties = { file_type: getPlaygroundFileTypeFromUri(fileUri) }; + } +} + +/** Reported when a document is saved (e.g. when the user edits a document from a collection) */ +export class DocumentUpdatedTelemetryEvent implements TelemetryEventBase { + type = 'Document Updated'; + properties: { + /** The source of the document update, e.g. 'editor', 'tree_view', etc. */ + source: DocumentSource; + + /** Whether the operation was successful */ + success: boolean; + }; + + constructor(source: DocumentSource, success: boolean) { + this.properties = { source, success }; + } +} + +/** Reported when a document is opened in the editor, e.g. from a query results view */ +export class DocumentEditedTelemetryEvent implements TelemetryEventBase { + type = 'Document Edited'; + properties: { + /** The source of the document - e.g. codelens, treeview, etc. */ + source: DocumentSource; + }; + + constructor(source: DocumentSource) { + this.properties = { source }; + } +} + +/** Reported when a playground file is exported to a language */ +export class PlaygroundExportedToLanguageTelemetryEvent + implements TelemetryEventBase +{ + type = 'Playground Exported To Language'; + properties: { + /** The target language of the export */ + language: string; + + /** The length of the exported code */ + exported_code_length: number; + + /** Whether the user opted to include driver syntax (e.g. import statements) */ + with_driver_syntax: boolean; + }; + + constructor( + language: string, + exportedCodeLength: number | undefined, + withDriverSyntax: boolean + ) { + this.properties = { + language, + exported_code_length: exportedCodeLength || 0, + with_driver_syntax: withDriverSyntax, + }; + } +} + +/** Reported when a new playground is created */ +export class PlaygroundCreatedTelemetryEvent implements TelemetryEventBase { + type = 'Playground Created'; + properties: { + /** + * The playground type - e.g. 'search', 'createCollection', 'createDatabase', etc. This is typically + * indicative of the element the user clicked to create the playground. + */ + playground_type: PlaygroundType; + }; + + constructor(playgroundType: PlaygroundType) { + this.properties = { playground_type: playgroundType }; + } +} +/** Reported when exporting to playground fails */ +export class ExportToPlaygroundFailedTelemetryEvent + implements TelemetryEventBase +{ + type = 'Export To Playground Failed'; + properties: { + /** The length of the playground code */ + input_length: number | undefined; + + /** The name of the error that occurred */ + error_name?: ExportToPlaygroundError; + }; + + constructor( + inputLength: number | undefined, + errorName: ExportToPlaygroundError + ) { + this.properties = { input_length: inputLength, error_name: errorName }; + } +} + +/** + * Reported when saved connections are loaded from disk. This is currently disabled + * due to the large volume of events. + */ +export class SavedConnectionsLoadedTelemetryEvent + implements TelemetryEventBase +{ + type = 'Saved Connections Loaded'; + properties: { + /** Total number of connections saved on disk */ + saved_connections: number; + + /** Total number of connections from preset settings */ + preset_connections: number; + + /** + * Total number of connections that extension was able to load, it might + * differ from saved_connections since there might be failures in loading + * secrets for a connection in which case we don't list the connections in the + * list of loaded connections. + * */ + loaded_connections: number; + + /** Total number of connections that have secrets stored in keytar */ + connections_with_secrets_in_keytar: number; + + /** Total number of connections that have secrets stored in secret storage */ + connections_with_secrets_in_secret_storage: number; + }; + + constructor({ + savedConnections, + presetConnections, + loadedConnections, + connectionsWithSecretsInKeytar, + connectionsWithSecretsInSecretStorage, + }: { + savedConnections: number; + presetConnections: number; + loadedConnections: number; + connectionsWithSecretsInKeytar: number; + connectionsWithSecretsInSecretStorage: number; + }) { + this.properties = { + saved_connections: savedConnections, + preset_connections: presetConnections, + loaded_connections: loadedConnections, + connections_with_secrets_in_keytar: connectionsWithSecretsInKeytar, + connections_with_secrets_in_secret_storage: + connectionsWithSecretsInSecretStorage, + }; + } +} + +/** Reported when the user provides feedback to the chatbot on a response */ +export class ParticipantFeedbackTelemetryEvent implements TelemetryEventBase { + type = 'Participant Feedback'; + properties: { + /** The type of feedback provided - e.g. 'positive', 'negative' */ + feedback: TelemetryFeedbackKind; + + /** The response type that the feedback was provided for - e.g. 'query', 'schema', 'docs' */ + response_type: ParticipantResponseType; + + /** If the feedback was negative, the reason for the negative feedback. It's picked from + * a set of predefined options and not a free-form text field. + * */ + reason?: String; + }; + + constructor( + feedback: vscode.ChatResultFeedbackKind, + responseType: ParticipantResponseType, + reason?: String + ) { + this.properties = { + feedback: this.chatResultFeedbackKindToTelemetryValue(feedback), + response_type: responseType, + reason, + }; + } + + private chatResultFeedbackKindToTelemetryValue( + kind: vscode.ChatResultFeedbackKind + ): TelemetryFeedbackKind { + switch (kind) { + case vscode.ChatResultFeedbackKind.Helpful: + return 'positive'; + case vscode.ChatResultFeedbackKind.Unhelpful: + return 'negative'; + default: + return undefined; + } + } +} + +/** Reported when the participant welcome message is shown */ +export class ParticipantWelcomeShownTelemetryEvent + implements TelemetryEventBase +{ + type = 'Participant Welcome Shown'; + properties = {}; +} + +/** Reported when a participant response fails */ +export class ParticipantResponseFailedTelemetryEvent + implements TelemetryEventBase +{ + type = 'Participant Response Failed'; + properties: { + /** The type of the command that failed - e.g. 'query', 'schema', 'docs' */ + command: ParticipantResponseType; + + /** The error code that caused the failure */ + error_code?: string; + + /** The name of the error that caused the failure */ + error_name: ParticipantErrorTypes; + + /** Additional details about the error if any. */ + error_details?: string; + }; + + constructor( + command: ParticipantResponseType, + errorName: ParticipantErrorTypes, + errorCode?: string, + errorDetails?: string + ) { + this.properties = { + command, + error_code: errorCode, + error_name: errorName, + error_details: errorDetails, + }; + } +} + +/** Reported when a participant prompt is submitted */ +export class ParticipantPromptSubmittedTelemetryEvent + implements TelemetryEventBase +{ + type = 'Participant Prompt Submitted'; + properties: { + /** The type of the command that was submitted - e.g. 'query', 'schema', 'docs' */ + command: ParticipantCommandType; + + /** The length of the user input */ + user_input_length: number; + + /** The total length of the message - i.e. user input + participant prompt */ + total_message_length: number; + + /** Whether the prompt has sample documents */ + has_sample_documents: boolean; + + /** The size of the history */ + history_size: number; + + /** For internal prompts - e.g. trying to extract the 'intent', 'namespace' or the + * namespace from the chat history. + */ + internal_purpose: InternalPromptPurpose; + }; + + constructor({ + command, + userInputLength, + totalMessageLength, + hasSampleDocuments, + historySize, + internalPurpose, + }: ParticipantPromptProperties) { + this.properties = { + command: command, + user_input_length: userInputLength, + total_message_length: totalMessageLength, + has_sample_documents: hasSampleDocuments, + history_size: historySize, + internal_purpose: internalPurpose, + }; + } +} + +/** + * Reported when a participant prompt is submitted from an action other than typing directly. + * This is typically one of the activation points - e.g. clicking on the tree view, a codelens, etc. + */ +export class ParticipantPromptSubmittedFromActionTelemetryEvent + implements TelemetryEventBase +{ + type = 'Participant Prompt Submitted From Action'; + properties: ParticipantTelemetryMetadata & { + /** The length of the input */ + input_length: number; + + /** The command we're requesting - e.g. 'query', 'schema', 'docs' */ + command: ParticipantRequestType; + }; + + constructor( + sourceMetadata: ParticipantTelemetryMetadata, + requestType: ParticipantRequestType, + inputLength: number + ) { + this.properties = { + ...sourceMetadata, + input_length: inputLength, + command: requestType, + }; + } +} + +/** Reported when a new chat is initiated from an activation point in the extension (e.g. the database tree view) */ +export class ParticipantChatOpenedFromActionTelemetryEvent + implements TelemetryEventBase +{ + type = 'Participant Chat Opened From Action'; + properties: ParticipantTelemetryMetadata & { + /** The command - if any - we're opening a chat for - e.g. 'query', 'schema', 'docs' */ + command?: ParticipantCommandType; + }; + + constructor( + sourceMetadata: ParticipantTelemetryMetadata, + command?: ParticipantCommandType + ) { + this.properties = { ...sourceMetadata, command }; + } +} + +/** Reported when we open an input box to ask the user for a message that we'll send to copilot */ +export class ParticipantInputBoxSubmittedTelemetryEvent + implements TelemetryEventBase +{ + type = 'Participant Inbox Box Submitted'; + properties: ParticipantTelemetryMetadata & { + /** The supplied input length */ + input_length: number; + + /** Whether the input was dismissed */ + dismissed: boolean; + + /** The command we're requesting - e.g. 'query', 'schema', 'docs' */ + command?: ParticipantCommandType; + }; + + constructor( + sourceMetadata: ParticipantTelemetryMetadata, + message: string | undefined, + command?: ParticipantCommandType + ) { + this.properties = { + ...sourceMetadata, + input_length: message?.length || 0, + dismissed: message === undefined, + command, + }; + } +} + +/** Reported when a participant response is generated */ +export class ParticipantResponseGeneratedTelemetryEvent + implements TelemetryEventBase +{ + type = 'Participant Response Generated'; + properties: { + /** The type of the command that was requested - e.g. 'query', 'schema', 'docs' */ + command: ParticipantResponseType; + + /** Whether the response has a call to action (e.g. 'Open in playground' button) */ + has_cta: boolean; + + /** Whether the response has runnable content (e.g. a code block) */ + has_runnable_content: boolean; + + /** Whether the response contains namespace information */ + found_namespace: boolean; + + /** The length of the output */ + output_length: number; + }; + + constructor({ + command, + hasCta, + hasRunnableContent, + foundNamespace, + outputLength, + }: { + command: ParticipantResponseType; + hasCta: boolean; + hasRunnableContent: boolean; + foundNamespace: boolean; + outputLength: number; + }) { + this.properties = { + command, + has_cta: hasCta, + has_runnable_content: hasRunnableContent, + found_namespace: foundNamespace, + output_length: outputLength, + }; + } +} + +/** Reported when a preset connection is edited */ +export class PresetConnectionEditedTelemetryEvent + implements TelemetryEventBase +{ + type = 'Preset Connection Edited'; + properties: { + /** The source of the interaction - currently, only treeview */ + source: Extract; + + /** Additional details about the source - e.g. if it's a specific connection element, + * it'll be 'tree_item', otherwise it'll be 'header'. + */ + source_details: 'tree_item' | 'header'; + }; + + constructor(sourceDetails: 'tree_item' | 'header') { + this.properties = { + source: DocumentSource.DOCUMENT_SOURCE_TREEVIEW, + source_details: sourceDetails, + }; + } +} + +export type TelemetryEvent = + | PlaygroundExecutedTelemetryEvent + | LinkClickedTelemetryEvent + | CommandRunTelemetryEvent + | NewConnectionTelemetryEvent + | ConnectionEditedTelemetryEvent + | OpenEditConnectionTelemetryEvent + | PlaygroundSavedTelemetryEvent + | PlaygroundLoadedTelemetryEvent + | DocumentUpdatedTelemetryEvent + | DocumentEditedTelemetryEvent + | PlaygroundExportedToLanguageTelemetryEvent + | PlaygroundCreatedTelemetryEvent + | ExportToPlaygroundFailedTelemetryEvent + | SavedConnectionsLoadedTelemetryEvent + | ParticipantFeedbackTelemetryEvent + | ParticipantWelcomeShownTelemetryEvent + | ParticipantPromptSubmittedTelemetryEvent + | ParticipantPromptSubmittedFromActionTelemetryEvent + | ParticipantChatOpenedFromActionTelemetryEvent + | ParticipantInputBoxSubmittedTelemetryEvent + | ParticipantResponseGeneratedTelemetryEvent + | PresetConnectionEditedTelemetryEvent; diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 82ccd523b..fd6d0b730 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -7,229 +7,30 @@ import { Analytics as SegmentAnalytics } from '@segment/analytics-node'; import type { ConnectionTypes } from '../connectionController'; import { createLogger } from '../logging'; -import type { DocumentSource } from '../documentSource'; import { getConnectionTelemetryProperties } from './connectionTelemetry'; -import type { NewConnectionTelemetryEventProperties } from './connectionTelemetry'; -import type { ShellEvaluateResult } from '../types/playgroundType'; import type { StorageController } from '../storage'; -import type { ExportToPlaygroundError } from '../participant/participantErrorTypes'; import { ParticipantErrorTypes } from '../participant/participantErrorTypes'; -import type { ExtensionCommand } from '../commands'; -import type { - ParticipantCommandType, - ParticipantRequestType, - ParticipantResponseType, -} from '../participant/participantTypes'; +import type { ParticipantResponseType } from '../participant/participantTypes'; +import type { TelemetryEvent } from './telemetryEvents'; +import { + NewConnectionTelemetryEvent, + ParticipantResponseFailedTelemetryEvent, +} from './telemetryEvents'; const log = createLogger('telemetry'); // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require('../../package.json'); -type PlaygroundTelemetryEventProperties = { - type: string | null; - partial: boolean; - error: boolean; -}; - export type SegmentProperties = { event: string; anonymousId: string; properties: Record; }; -type LinkClickedTelemetryEventProperties = { - screen: string; - link_id: string; -}; - -type ExtensionCommandRunTelemetryEventProperties = { - command: ExtensionCommand; -}; - -type DocumentUpdatedTelemetryEventProperties = { - source: DocumentSource; - success: boolean; -}; - -type DocumentEditedTelemetryEventProperties = { - source: DocumentSource; -}; - -type ExportToPlaygroundFailedEventProperties = { - input_length: number | undefined; - error_name?: ExportToPlaygroundError; -}; - -type PlaygroundExportedToLanguageTelemetryEventProperties = { - language?: string; - exported_code_length: number; - with_driver_syntax?: boolean; -}; - -type PlaygroundCreatedTelemetryEventProperties = { - playground_type: string; -}; - -type PlaygroundSavedTelemetryEventProperties = { - file_type?: string; -}; - -type PlaygroundLoadedTelemetryEventProperties = { - file_type?: string; -}; - -type KeytarSecretsMigrationFailedProperties = { - saved_connections: number; - loaded_connections: number; - connections_with_failed_keytar_migration: number; -}; - -type ConnectionEditedTelemetryEventProperties = { - success: boolean; -}; - -type SavedConnectionsLoadedProperties = { - // Total number of connections saved on disk - saved_connections: number; - // Total number of connections from preset settings - preset_connections: number; - // Total number of connections that extension was able to load, it might - // differ from saved_connections since there might be failures in loading - // secrets for a connection in which case we don't list the connections in the - // list of loaded connections. - loaded_connections: number; - connections_with_secrets_in_keytar: number; - connections_with_secrets_in_secret_storage: number; -}; - -type TelemetryFeedbackKind = 'positive' | 'negative' | undefined; - -type ParticipantFeedbackProperties = { - feedback: TelemetryFeedbackKind; - response_type: ParticipantResponseType; - reason?: String; -}; - -type ParticipantResponseFailedProperties = { - command: ParticipantResponseType; - error_code?: string; - error_name: ParticipantErrorTypes; - error_details?: string; -}; - -export type InternalPromptPurpose = 'intent' | 'namespace' | undefined; - -export type ParticipantPromptProperties = { - command: ParticipantCommandType; - user_input_length: number; - total_message_length: number; - has_sample_documents: boolean; - history_size: number; - internal_purpose: InternalPromptPurpose; -}; - -export type ParticipantResponseProperties = { - command: ParticipantResponseType; - has_cta: boolean; - has_runnable_content: boolean; - found_namespace: boolean; - output_length: number; -}; - -export type ParticipantPromptSubmittedFromActionProperties = { - source: DocumentSource; - input_length: number; - command: ParticipantRequestType; -}; - -export type ParticipantChatOpenedFromActionProperties = { - source: DocumentSource; - command?: ParticipantCommandType; -}; - -export type PresetSavedConnectionEditedProperties = { - source: DocumentSource; - source_details: 'tree_item' | 'header'; -}; - -export type ParticipantInputBoxSubmitted = { - source: DocumentSource; - input_length: number | undefined; - dismissed: boolean; - command?: ParticipantCommandType; -}; - -export function chatResultFeedbackKindToTelemetryValue( - kind: vscode.ChatResultFeedbackKind -): TelemetryFeedbackKind { - switch (kind) { - case vscode.ChatResultFeedbackKind.Helpful: - return 'positive'; - case vscode.ChatResultFeedbackKind.Unhelpful: - return 'negative'; - default: - return undefined; - } -} - -type TelemetryEventProperties = - | PlaygroundTelemetryEventProperties - | LinkClickedTelemetryEventProperties - | ExtensionCommandRunTelemetryEventProperties - | NewConnectionTelemetryEventProperties - | DocumentUpdatedTelemetryEventProperties - | ConnectionEditedTelemetryEventProperties - | DocumentEditedTelemetryEventProperties - | PlaygroundExportedToLanguageTelemetryEventProperties - | PlaygroundCreatedTelemetryEventProperties - | PlaygroundSavedTelemetryEventProperties - | PlaygroundLoadedTelemetryEventProperties - | KeytarSecretsMigrationFailedProperties - | ExportToPlaygroundFailedEventProperties - | SavedConnectionsLoadedProperties - | ParticipantFeedbackProperties - | ParticipantResponseFailedProperties - | ParticipantPromptProperties - | ParticipantPromptSubmittedFromActionProperties - | ParticipantChatOpenedFromActionProperties - | ParticipantResponseProperties; - -export enum TelemetryEventTypes { - PLAYGROUND_CODE_EXECUTED = 'Playground Code Executed', - EXTENSION_LINK_CLICKED = 'Link Clicked', - EXTENSION_COMMAND_RUN = 'Command Run', - NEW_CONNECTION = 'New Connection', - CONNECTION_EDITED = 'Connection Edited', - OPEN_EDIT_CONNECTION = 'Open Edit Connection', - PLAYGROUND_SAVED = 'Playground Saved', - PLAYGROUND_LOADED = 'Playground Loaded', - DOCUMENT_UPDATED = 'Document Updated', - DOCUMENT_EDITED = 'Document Edited', - PLAYGROUND_EXPORTED_TO_LANGUAGE = 'Playground Exported To Language', - PLAYGROUND_CREATED = 'Playground Created', - KEYTAR_SECRETS_MIGRATION_FAILED = 'Keytar Secrets Migration Failed', - EXPORT_TO_PLAYGROUND_FAILED = 'Export To Playground Failed', - SAVED_CONNECTIONS_LOADED = 'Saved Connections Loaded', - 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', - PRESET_CONNECTION_EDITED = 'Preset Connection Edited', -} - /** * This controller manages telemetry. */ -export default class TelemetryService { +export class TelemetryService { _segmentAnalytics?: SegmentAnalytics; _segmentAnonymousId: string; _segmentKey?: string; // The segment API write key. @@ -325,16 +126,13 @@ export default class TelemetryService { }); } - track( - eventType: TelemetryEventTypes, - properties?: TelemetryEventProperties - ): void { + track(event: TelemetryEvent): void { try { this._segmentAnalyticsTrack({ ...this.getTelemetryUserIdentity(), - event: eventType, + event: event.type, properties: { - ...properties, + ...event.properties, extension_version: `${version}`, }, }); @@ -343,61 +141,14 @@ export default class TelemetryService { } } - async _getConnectionTelemetryProperties( - dataService: DataService, - connectionType: ConnectionTypes - ): Promise { - return await getConnectionTelemetryProperties(dataService, connectionType); - } - async trackNewConnection( dataService: DataService, connectionType: ConnectionTypes ): Promise { const connectionTelemetryProperties = - await this._getConnectionTelemetryProperties(dataService, connectionType); - - this.track( - TelemetryEventTypes.NEW_CONNECTION, - connectionTelemetryProperties - ); - } - - trackExportToPlaygroundFailed( - props: ExportToPlaygroundFailedEventProperties - ): void { - this.track(TelemetryEventTypes.EXPORT_TO_PLAYGROUND_FAILED, props); - } + await getConnectionTelemetryProperties(dataService, connectionType); - trackCommandRun(command: ExtensionCommand): void { - this.track(TelemetryEventTypes.EXTENSION_COMMAND_RUN, { command }); - } - - getPlaygroundResultType(res: ShellEvaluateResult): string { - if (!res || !res.result || !res.result.type) { - return 'other'; - } - - const shellApiType = res.result.type.toLocaleLowerCase(); - - // See: https://github.com/mongodb-js/mongosh/blob/main/packages/shell-api/src/shell-api.js - if (shellApiType.includes('insert')) { - return 'insert'; - } - if (shellApiType.includes('update')) { - return 'update'; - } - if (shellApiType.includes('delete')) { - return 'delete'; - } - if (shellApiType.includes('aggregation')) { - return 'aggregation'; - } - if (shellApiType.includes('cursor')) { - return 'query'; - } - - return 'other'; + this.track(new NewConnectionTelemetryEvent(connectionTelemetryProperties)); } getTelemetryUserIdentity(): { anonymousId: string } { @@ -406,107 +157,6 @@ export default class TelemetryService { }; } - trackPlaygroundCodeExecuted( - result: ShellEvaluateResult, - partial: boolean, - error: boolean - ): void { - this.track(TelemetryEventTypes.PLAYGROUND_CODE_EXECUTED, { - type: result ? this.getPlaygroundResultType(result) : null, - partial, - error, - }); - } - - trackLinkClicked(screen: string, linkId: string): void { - this.track(TelemetryEventTypes.EXTENSION_LINK_CLICKED, { - screen, - link_id: linkId, - }); - } - - trackPlaygroundLoaded(fileType?: string): void { - this.track(TelemetryEventTypes.PLAYGROUND_LOADED, { - file_type: fileType, - }); - } - - trackPlaygroundSaved(fileType?: string): void { - this.track(TelemetryEventTypes.PLAYGROUND_SAVED, { - file_type: fileType, - }); - } - - trackDocumentUpdated(source: DocumentSource, success: boolean): void { - this.track(TelemetryEventTypes.DOCUMENT_UPDATED, { source, success }); - } - - trackDocumentOpenedInEditor(source: DocumentSource): void { - this.track(TelemetryEventTypes.DOCUMENT_EDITED, { source }); - } - - trackPlaygroundExportedToLanguageExported( - playgroundExportedProps: PlaygroundExportedToLanguageTelemetryEventProperties - ): void { - this.track( - TelemetryEventTypes.PLAYGROUND_EXPORTED_TO_LANGUAGE, - playgroundExportedProps - ); - } - - trackPresetConnectionEdited( - props: PresetSavedConnectionEditedProperties - ): void { - this.track(TelemetryEventTypes.PRESET_CONNECTION_EDITED, props); - } - - trackPlaygroundCreated(playgroundType: string): void { - this.track(TelemetryEventTypes.PLAYGROUND_CREATED, { - playground_type: playgroundType, - }); - } - - trackSavedConnectionsLoaded( - savedConnectionsLoadedProps: SavedConnectionsLoadedProperties - ): void { - this.track( - TelemetryEventTypes.SAVED_CONNECTIONS_LOADED, - savedConnectionsLoadedProps - ); - } - - trackKeytarSecretsMigrationFailed( - keytarSecretsMigrationFailedProps: KeytarSecretsMigrationFailedProperties - ): void { - this.track( - TelemetryEventTypes.KEYTAR_SECRETS_MIGRATION_FAILED, - keytarSecretsMigrationFailedProps - ); - } - - trackParticipantFeedback(props: ParticipantFeedbackProperties): void { - this.track(TelemetryEventTypes.PARTICIPANT_FEEDBACK, props); - } - - trackParticipantPromptSubmittedFromAction( - props: ParticipantPromptSubmittedFromActionProperties - ): void { - this.track( - TelemetryEventTypes.PARTICIPANT_PROMPT_SUBMITTED_FROM_ACTION, - props - ); - } - - trackParticipantChatOpenedFromAction( - props: ParticipantChatOpenedFromActionProperties - ): void { - this.track(TelemetryEventTypes.PARTICIPANT_CHAT_OPENED_FROM_ACTION, props); - } - - 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; @@ -535,18 +185,8 @@ export default class TelemetryService { errorName = ParticipantErrorTypes.OTHER; } - this.track(TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED, { - command, - error_code: errorCode, - error_name: errorName, - } satisfies ParticipantResponseFailedProperties); - } - - trackParticipantPrompt(stats: ParticipantPromptProperties): void { - this.track(TelemetryEventTypes.PARTICIPANT_PROMPT_SUBMITTED, stats); - } - - trackParticipantResponse(props: ParticipantResponseProperties): void { - this.track(TelemetryEventTypes.PARTICIPANT_RESPONSE_GENERATED, props); + this.track( + new ParticipantResponseFailedTelemetryEvent(command, errorName, errorCode) + ); } } diff --git a/src/test/suite/connectionController.test.ts b/src/test/suite/connectionController.test.ts index 0f54d5820..92ad2a1ac 100644 --- a/src/test/suite/connectionController.test.ts +++ b/src/test/suite/connectionController.test.ts @@ -19,7 +19,7 @@ import { SecretStorageLocation, } from '../../storage/storageController'; import { StatusView } from '../../views'; -import TelemetryService from '../../telemetry/telemetryService'; +import { TelemetryService } from '../../telemetry'; import { ExtensionContextStub } from './stubs'; import { TEST_DATABASE_URI, @@ -818,7 +818,7 @@ suite('Connection Controller Test Suite', function () { test('two disconnects on one connection at once complete without erroring', (done) => { let disconnectsCompleted = 0; - async function disconnect() { + async function disconnect(): Promise { try { await testConnectionController.disconnect(); @@ -1213,10 +1213,7 @@ suite('Connection Controller Test Suite', function () { '_getConnectionInfoWithSecrets', (connectionInfo) => Promise.resolve(connectionInfo as LoadedConnection) ); - const trackStub = testSandbox.stub( - testTelemetryService, - 'trackSavedConnectionsLoaded' - ); + const trackStub = testSandbox.stub(testTelemetryService, 'track'); // Clear any connections and load so we get our stubbed connections from above. testConnectionController.clearAllConnections(); diff --git a/src/test/suite/editors/activeConnectionCodeLensProvider.test.ts b/src/test/suite/editors/activeConnectionCodeLensProvider.test.ts index 05a65ca53..58dc93f2c 100644 --- a/src/test/suite/editors/activeConnectionCodeLensProvider.test.ts +++ b/src/test/suite/editors/activeConnectionCodeLensProvider.test.ts @@ -10,7 +10,7 @@ import ConnectionController from '../../../connectionController'; import { StatusView } from '../../../views'; import { StorageController } from '../../../storage'; import { ExtensionContextStub } from '../stubs'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import { TEST_DATABASE_URI } from '../dbTestHelper'; suite('Active Connection CodeLens Provider Test Suite', () => { diff --git a/src/test/suite/editors/collectionDocumentsProvider.test.ts b/src/test/suite/editors/collectionDocumentsProvider.test.ts index eb844baa7..95b78a05e 100644 --- a/src/test/suite/editors/collectionDocumentsProvider.test.ts +++ b/src/test/suite/editors/collectionDocumentsProvider.test.ts @@ -17,7 +17,7 @@ import { SecretStorageLocation, StorageLocation, } from '../../../storage/storageController'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { ExtensionContextStub, mockTextEditor } from '../stubs'; @@ -172,6 +172,7 @@ suite('Collection Documents Provider Test Suite', () => { sandbox.stub(testCollectionViewProvider._statusView, 'hideMessage'); await testCollectionViewProvider.provideTextDocumentContent(uri); + assert( testQueryStore.operations[operationId].hasMoreDocumentsToShow === false, 'Expected not to have more documents to show.' @@ -202,7 +203,7 @@ suite('Collection Documents Provider Test Suite', () => { const showMessageStub = sandbox.stub(testStatusView, 'showMessage'); const hideMessageStub = sandbox.stub(testStatusView, 'hideMessage'); - mockActiveDataService.find = () => { + mockActiveDataService.find = (): Promise<{ field: string }[]> => { assert(showMessageStub.called); assert(!hideMessageStub.called); assert(showMessageStub.firstCall.args[0] === 'Fetching documents...'); diff --git a/src/test/suite/editors/editDocumentCodeLensProvider.test.ts b/src/test/suite/editors/editDocumentCodeLensProvider.test.ts index b553fa85a..3643aa61e 100644 --- a/src/test/suite/editors/editDocumentCodeLensProvider.test.ts +++ b/src/test/suite/editors/editDocumentCodeLensProvider.test.ts @@ -11,7 +11,7 @@ import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensP import { mockTextEditor } from '../stubs'; import { StatusView } from '../../../views'; import { StorageController } from '../../../storage'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import { ExtensionContextStub } from '../stubs'; suite('Edit Document Code Lens Provider Test Suite', () => { diff --git a/src/test/suite/editors/exportToLanguageCodeLensProvider.test.ts b/src/test/suite/editors/exportToLanguageCodeLensProvider.test.ts index b0eb03545..25b7f5662 100644 --- a/src/test/suite/editors/exportToLanguageCodeLensProvider.test.ts +++ b/src/test/suite/editors/exportToLanguageCodeLensProvider.test.ts @@ -7,7 +7,7 @@ import ExportToLanguageCodeLensProvider, { import PlaygroundResultProvider from '../../../editors/playgroundResultProvider'; import StorageController from '../../../storage/storageController'; import { ExtensionContextStub } from '../stubs'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import StatusView from '../../../views/statusView'; import ConnectionController from '../../../connectionController'; import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensProvider'; diff --git a/src/test/suite/editors/mongoDBDocumentService.test.ts b/src/test/suite/editors/mongoDBDocumentService.test.ts index dafbd5829..5c0530b7b 100644 --- a/src/test/suite/editors/mongoDBDocumentService.test.ts +++ b/src/test/suite/editors/mongoDBDocumentService.test.ts @@ -11,7 +11,7 @@ import MongoDBDocumentService from '../../../editors/mongoDBDocumentService'; import { StorageController } from '../../../storage'; import { StatusView } from '../../../views'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import { ExtensionContextStub } from '../stubs'; const expect = chai.expect; diff --git a/src/test/suite/editors/playgroundController.test.ts b/src/test/suite/editors/playgroundController.test.ts index 0bb8cf545..f13a79c53 100644 --- a/src/test/suite/editors/playgroundController.test.ts +++ b/src/test/suite/editors/playgroundController.test.ts @@ -14,7 +14,7 @@ import { PlaygroundController } from '../../../editors'; import PlaygroundResultProvider from '../../../editors/playgroundResultProvider'; import { StatusView } from '../../../views'; import { StorageController } from '../../../storage'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import { ExtensionContextStub, LanguageServerControllerStub } from '../stubs'; import { mockTextEditor } from '../stubs'; diff --git a/src/test/suite/editors/playgroundResultProvider.test.ts b/src/test/suite/editors/playgroundResultProvider.test.ts index b7d972d0f..6b7efc5ae 100644 --- a/src/test/suite/editors/playgroundResultProvider.test.ts +++ b/src/test/suite/editors/playgroundResultProvider.test.ts @@ -14,7 +14,7 @@ import EditDocumentCodeLensProvider from '../../../editors/editDocumentCodeLensP import PlaygroundResultProvider from '../../../editors/playgroundResultProvider'; import { StatusView } from '../../../views'; import { StorageController } from '../../../storage'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import { ExtensionContextStub } from '../stubs'; const expect = chai.expect; diff --git a/src/test/suite/explorer/helpExplorer.test.ts b/src/test/suite/explorer/helpExplorer.test.ts index 9bb9df134..c86eae3c2 100644 --- a/src/test/suite/explorer/helpExplorer.test.ts +++ b/src/test/suite/explorer/helpExplorer.test.ts @@ -110,11 +110,11 @@ suite('Help Explorer Test Suite', function () { const testHelpExplorer = mdbTestExtension.testExtensionController._helpExplorer; - const stubLinkClickedTelemetry = sandbox.fake(); + const stubTrackTelemetry = sandbox.fake(); sandbox.replace( mdbTestExtension.testExtensionController._telemetryService, - 'trackLinkClicked', - stubLinkClickedTelemetry + 'track', + stubTrackTelemetry ); testHelpExplorer.activateHelpTreeView( mdbTestExtension.testExtensionController._telemetryService @@ -127,8 +127,13 @@ suite('Help Explorer Test Suite', function () { atlasHelpItem, mdbTestExtension.testExtensionController._telemetryService ); - assert(stubLinkClickedTelemetry.called); - assert(stubLinkClickedTelemetry.firstCall.args[0] === 'helpPanel'); - assert(stubLinkClickedTelemetry.firstCall.args[1] === 'freeClusterCTA'); + assert(stubTrackTelemetry.called); + assert( + stubTrackTelemetry.firstCall.args[0].properties.screen === 'helpPanel' + ); + assert( + stubTrackTelemetry.firstCall.args[0].properties.link_id === + 'freeClusterCTA' + ); }); }); diff --git a/src/test/suite/language/languageServerController.test.ts b/src/test/suite/language/languageServerController.test.ts index 686785729..00876ae62 100644 --- a/src/test/suite/language/languageServerController.test.ts +++ b/src/test/suite/language/languageServerController.test.ts @@ -18,7 +18,7 @@ import PlaygroundResultProvider from '../../../editors/playgroundResultProvider' import { StatusView } from '../../../views'; import { StorageController } from '../../../storage'; import { TEST_DATABASE_URI } from '../dbTestHelper'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import { ExtensionContextStub } from '../stubs'; import ExportToLanguageCodeLensProvider from '../../../editors/exportToLanguageCodeLensProvider'; diff --git a/src/test/suite/participant/participant.test.ts b/src/test/suite/participant/participant.test.ts index 5d8565793..4b3c99c78 100644 --- a/src/test/suite/participant/participant.test.ts +++ b/src/test/suite/participant/participant.test.ts @@ -12,13 +12,13 @@ import { StorageController } from '../../../storage'; import { StatusView } from '../../../views'; import { ExtensionContextStub } from '../stubs'; import type { + ExportToPlaygroundFailedTelemetryEvent, InternalPromptPurpose, - ParticipantPromptProperties, - ParticipantResponseProperties, -} from '../../../telemetry/telemetryService'; -import TelemetryService, { - TelemetryEventTypes, -} from '../../../telemetry/telemetryService'; + ParticipantFeedbackTelemetryEvent, + ParticipantPromptSubmittedTelemetryEvent, + ParticipantResponseFailedTelemetryEvent, + ParticipantResponseGeneratedTelemetryEvent, +} from '../../../telemetry'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import type { ChatResult } from '../../../participant/constants'; import { CHAT_PARTICIPANT_ID } from '../../../participant/constants'; @@ -48,6 +48,7 @@ import type { SendMessageToParticipantOptions, } from '../../../participant/participantTypes'; import { DocumentSource } from '../../../documentSource'; +import { TelemetryService } from '../../../telemetry'; // The Copilot's model in not available in tests, // therefore we need to mock its methods and returning values. @@ -143,28 +144,29 @@ suite('Participant Controller Test Suite', function () { expect(telemetryTrackStub.callCount).to.be.greaterThan(callIndex); const call = telemetryTrackStub.getCalls()[callIndex]; - expect(call.args[0]).to.equal('Participant Prompt Submitted'); + const arg = call.args[0] as ParticipantPromptSubmittedTelemetryEvent; + expect(arg.type).to.equal('Participant Prompt Submitted'); - const properties = call.args[1] as ParticipantPromptProperties; - - expect(properties.command).to.equal(command); - expect(properties.has_sample_documents).to.equal(expectSampleDocs); - expect(properties.history_size).to.equal(chatContextStub.history.length); + expect(arg.properties.command).to.equal(command); + expect(arg.properties.has_sample_documents).to.equal(expectSampleDocs); + expect(arg.properties.history_size).to.equal( + chatContextStub.history.length + ); /** For docs chatbot requests, the length of the prompt would be longer as it gets the prompt history prepended.*/ if (command !== 'docs') { // Total message length includes participant as well as user prompt - expect(properties.total_message_length).to.be.greaterThan( - properties.user_input_length + expect(arg.properties.total_message_length).to.be.greaterThan( + arg.properties.user_input_length ); } // User prompt length should be at least equal to the supplied user prompt, but my occasionally // be greater - e.g. when we enhance the context. - expect(properties.user_input_length).to.be.greaterThanOrEqual( + expect(arg.properties.user_input_length).to.be.greaterThanOrEqual( chatRequest.prompt.length ); - expect(properties.internal_purpose).to.equal(expectedInternalPurpose); + expect(arg.properties.internal_purpose).to.equal(expectedInternalPurpose); }; const assertResponseTelemetry = ( @@ -183,15 +185,14 @@ suite('Participant Controller Test Suite', function () { ): void => { expect(telemetryTrackStub.callCount).to.be.greaterThan(callIndex); const call = telemetryTrackStub.getCalls()[callIndex]; - expect(call.args[0]).to.equal('Participant Response Generated'); - - const properties = call.args[1] as ParticipantResponseProperties; - - expect(properties.command).to.equal(command); - expect(properties.found_namespace).to.equal(foundNamespace); - expect(properties.has_cta).to.equal(hasCTA); - expect(properties.has_runnable_content).to.equal(hasRunnableContent); - expect(properties.output_length).to.be.greaterThan(0); + const arg = call.args[0] as ParticipantResponseGeneratedTelemetryEvent; + expect(arg.type).to.equal('Participant Response Generated'); + + expect(arg.properties.command).to.equal(command); + expect(arg.properties.found_namespace).to.equal(foundNamespace); + expect(arg.properties.has_cta).to.equal(hasCTA); + expect(arg.properties.has_runnable_content).to.equal(hasRunnableContent); + expect(arg.properties.output_length).to.be.greaterThan(0); }; beforeEach(function () { @@ -515,10 +516,9 @@ suite('Participant Controller Test Suite', function () { // Once to report welcome screen shown, second time to track the user prompt expect(telemetryTrackStub).to.have.been.calledTwice; - expect(telemetryTrackStub.firstCall.args[0]).to.equal( - TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN + expect(telemetryTrackStub.firstCall.args[0].type).to.equal( + 'Participant Welcome Shown' ); - expect(telemetryTrackStub.firstCall.args[1]).to.be.undefined; assertCommandTelemetry('query', chatRequestMock, { callIndex: 1, expectedInternalPurpose: 'namespace', @@ -551,9 +551,7 @@ suite('Participant Controller Test Suite', function () { const telemetryEvents = telemetryTrackStub .getCalls() .map((call) => call.args[0]) - .filter( - (arg) => arg === TelemetryEventTypes.PARTICIPANT_WELCOME_SHOWN - ); + .filter((arg) => arg === 'Participant Welcome Shown'); expect(telemetryEvents).to.be.empty; }); @@ -1746,13 +1744,17 @@ Schema: expect( telemetryTrackStub.getCalls() ).to.have.length.greaterThanOrEqual(2); - expect(telemetryTrackStub.firstCall.args[0]).to.equal( - TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED + + const firstTelemetryEvent = telemetryTrackStub.firstCall + .args[0] as ParticipantResponseFailedTelemetryEvent; + expect(firstTelemetryEvent.type).to.equal( + 'Participant Response Failed' ); - const properties = telemetryTrackStub.firstCall.args[1]; - expect(properties.command).to.equal('docs'); - expect(properties.error_name).to.equal('Docs Chatbot API Issue'); + expect(firstTelemetryEvent.properties.command).to.equal('docs'); + expect(firstTelemetryEvent.properties.error_name).to.equal( + 'Docs Chatbot API Issue' + ); assertResponseTelemetry('docs/copilot', { callIndex: 2, @@ -1836,17 +1838,25 @@ Schema: sendRequestStub.rejects(); const messages = sendRequestStub.firstCall.args[0]; expect(getMessageContent(messages[1])).to.equal(code.trim()); - expect(telemetryTrackStub).calledWith( - TelemetryEventTypes.EXPORT_TO_PLAYGROUND_FAILED, - { - input_length: code.trim().length, - error_name: 'streamChatResponseWithExportToLanguage', - } - ); - expect(telemetryTrackStub).not.calledWith( - TelemetryEventTypes.PARTICIPANT_RESPONSE_FAILED + const playgroundFailedTelemetryEvent = telemetryTrackStub + .getCalls() + .find((c) => c.args[0].type === 'Export To Playground Failed') + ?.args[0] as ExportToPlaygroundFailedTelemetryEvent; + expect(playgroundFailedTelemetryEvent.type).to.equal( + 'Export To Playground Failed' + ); + expect(playgroundFailedTelemetryEvent.properties.error_name).to.equal( + 'streamChatResponseWithExportToLanguage' ); + expect( + playgroundFailedTelemetryEvent.properties.input_length + ).to.equal(code.trim().length); + + const participantResponseFailedTelemetryEvent = telemetryTrackStub + .getCalls() + .find((c) => c.args[0].type === 'Participant Response Failed'); + expect(participantResponseFailedTelemetryEvent).to.be.undefined; }); test('exports selected lines of code to a playground', async function () { @@ -2268,9 +2278,9 @@ Schema: ); expect(stats.command).to.equal('generic'); - expect(stats.has_sample_documents).to.be.false; - expect(stats.user_input_length).to.equal(chatRequestMock.prompt.length); - expect(stats.total_message_length).to.equal( + expect(stats.hasSampleDocuments).to.be.false; + expect(stats.userInputLength).to.equal(chatRequestMock.prompt.length); + expect(stats.totalMessageLength).to.equal( getContentLength(messages[0]) + getContentLength(messages[1]) ); }); @@ -2335,9 +2345,9 @@ Schema: ); expect(stats.command).to.equal('query'); - expect(stats.has_sample_documents).to.be.true; - expect(stats.user_input_length).to.equal(chatRequestMock.prompt.length); - expect(stats.total_message_length).to.equal( + expect(stats.hasSampleDocuments).to.be.true; + expect(stats.userInputLength).to.equal(chatRequestMock.prompt.length); + expect(stats.totalMessageLength).to.equal( getContentLength(messages[0]) + getContentLength(messages[1]) + getContentLength(messages[2]) @@ -2345,7 +2355,7 @@ Schema: // The length of the user prompt length should be taken from the prompt supplied // by the user, even if we enhance it with sample docs and schema. - expect(stats.user_input_length).to.be.lessThan( + expect(stats.userInputLength).to.be.lessThan( getContentLength(messages[2]) ); }); @@ -2390,9 +2400,9 @@ Schema: expect(getMessageContent(messages[1])).to.include(schema); expect(stats.command).to.equal('schema'); - expect(stats.has_sample_documents).to.be.false; - expect(stats.user_input_length).to.equal(chatRequestMock.prompt.length); - expect(stats.total_message_length).to.equal( + expect(stats.hasSampleDocuments).to.be.false; + expect(stats.userInputLength).to.equal(chatRequestMock.prompt.length); + expect(stats.totalMessageLength).to.equal( getContentLength(messages[0]) + getContentLength(messages[1]) ); }); @@ -2417,9 +2427,9 @@ Schema: ); expect(stats.command).to.equal('query'); - expect(stats.has_sample_documents).to.be.false; - expect(stats.user_input_length).to.equal(chatRequestMock.prompt.length); - expect(stats.total_message_length).to.equal( + expect(stats.hasSampleDocuments).to.be.false; + expect(stats.userInputLength).to.equal(chatRequestMock.prompt.length); + expect(stats.totalMessageLength).to.equal( getContentLength(messages[0]) + getContentLength(messages[1]) ); }); @@ -2592,14 +2602,14 @@ Schema: expect(getMessageContent(messages[1])).to.contain(expectedPrompt); expect(stats.command).to.equal('query'); - expect(stats.has_sample_documents).to.be.false; - expect(stats.user_input_length).to.equal(expectedPrompt.length); - expect(stats.total_message_length).to.equal( + expect(stats.hasSampleDocuments).to.be.false; + expect(stats.userInputLength).to.equal(expectedPrompt.length); + expect(stats.totalMessageLength).to.equal( getContentLength(messages[0]) + getContentLength(messages[1]) ); // The prompt builder may add extra info, but we're only reporting the actual user input - expect(stats.user_input_length).to.be.lessThan( + expect(stats.userInputLength).to.be.lessThan( getContentLength(messages[1]) ); }); @@ -2668,17 +2678,18 @@ Schema: }); sinon.assert.calledOnce(telemetryTrackStub); - expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( - 'Participant Feedback' + const telemetryEvent = telemetryTrackStub.lastCall + .args[0] as ParticipantFeedbackTelemetryEvent; + expect(telemetryEvent.type).to.be.equal('Participant Feedback'); + + expect(telemetryEvent.properties.feedback).to.be.equal('positive'); + expect(telemetryEvent.properties.reason).to.be.undefined; + expect(telemetryEvent.properties.response_type).to.be.equal( + 'askToConnect' ); - const properties = telemetryTrackStub.lastCall.args[1]; - expect(properties.feedback).to.be.equal('positive'); - expect(properties.reason).to.be.undefined; - expect(properties.response_type).to.be.equal('askToConnect'); - // Ensure we're not leaking the response content into the telemetry payload - expect(JSON.stringify(properties)) + expect(JSON.stringify(telemetryEvent.properties)) .to.not.include('creditCardNumber') .and.not.include('1234-5678-9012-3456'); }); @@ -2696,17 +2707,16 @@ Schema: } as vscode.ChatResultFeedback); sinon.assert.calledOnce(telemetryTrackStub); - expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( - 'Participant Feedback' - ); + const telemetryEvent = telemetryTrackStub.lastCall + .args[0] as ParticipantFeedbackTelemetryEvent; + expect(telemetryEvent.type).to.be.equal('Participant Feedback'); - const properties = telemetryTrackStub.lastCall.args[1]; - expect(properties.feedback).to.be.equal('negative'); - expect(properties.reason).to.be.equal('incompleteCode'); - expect(properties.response_type).to.be.equal('query'); + expect(telemetryEvent.properties.feedback).to.be.equal('negative'); + expect(telemetryEvent.properties.reason).to.be.equal('incompleteCode'); + expect(telemetryEvent.properties.response_type).to.be.equal('query'); // Ensure we're not leaking the response content into the telemetry payload - expect(JSON.stringify(properties)) + expect(JSON.stringify(telemetryEvent.properties)) .to.not.include('SSN') .and.not.include('123456789'); }); @@ -2719,14 +2729,13 @@ Schema: ); sinon.assert.calledOnce(telemetryTrackStub); - expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( - 'Participant Response Failed' - ); + const telemetryEvent = telemetryTrackStub.lastCall + .args[0] as ParticipantResponseFailedTelemetryEvent; + expect(telemetryEvent.type).to.be.equal('Participant Response Failed'); - const properties = telemetryTrackStub.lastCall.args[1]; - expect(properties.command).to.be.equal('query'); - expect(properties.error_code).to.be.undefined; - expect(properties.error_name).to.be.equal( + expect(telemetryEvent.properties.command).to.be.equal('query'); + expect(telemetryEvent.properties.error_code).to.be.undefined; + expect(telemetryEvent.properties.error_name).to.be.equal( 'Filtered by Responsible AI Service' ); }); @@ -2740,14 +2749,15 @@ Schema: ); sinon.assert.calledOnce(telemetryTrackStub); - expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( - 'Participant Response Failed' - ); + const telemetryEvent = telemetryTrackStub.lastCall + .args[0] as ParticipantResponseFailedTelemetryEvent; + expect(telemetryEvent.type).to.be.equal('Participant Response Failed'); - const properties = telemetryTrackStub.lastCall.args[1]; - expect(properties.command).to.be.equal('docs'); - expect(properties.error_code).to.be.undefined; - expect(properties.error_name).to.be.equal('Chat Model Off Topic'); + expect(telemetryEvent.properties.command).to.be.equal('docs'); + expect(telemetryEvent.properties.error_code).to.be.undefined; + expect(telemetryEvent.properties.error_name).to.be.equal( + 'Chat Model Off Topic' + ); }); test('Reports error code when available', function () { @@ -2759,14 +2769,13 @@ Schema: ); sinon.assert.calledOnce(telemetryTrackStub); - expect(telemetryTrackStub.lastCall.args[0]).to.be.equal( - 'Participant Response Failed' - ); + const telemetryEvent = telemetryTrackStub.lastCall + .args[0] as ParticipantResponseFailedTelemetryEvent; + expect(telemetryEvent.type).to.be.equal('Participant Response Failed'); - const properties = telemetryTrackStub.lastCall.args[1]; - expect(properties.command).to.be.equal('schema'); - expect(properties.error_code).to.be.equal('NotFound'); - expect(properties.error_name).to.be.equal('Other'); + expect(telemetryEvent.properties.command).to.be.equal('schema'); + expect(telemetryEvent.properties.error_code).to.be.equal('NotFound'); + expect(telemetryEvent.properties.error_name).to.be.equal('Other'); }); }); }); diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index f9830ca9e..55e9fe6d7 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -14,7 +14,16 @@ import { DocumentSource } from '../../../documentSource'; import { mdbTestExtension } from '../stubbableMdbExtension'; import { DatabaseTreeItem, DocumentTreeItem } from '../../../explorer'; import { DataServiceStub } from '../stubs'; -import { chatResultFeedbackKindToTelemetryValue } from '../../../telemetry/telemetryService'; +import { + DocumentEditedTelemetryEvent, + DocumentUpdatedTelemetryEvent, + LinkClickedTelemetryEvent, + ParticipantFeedbackTelemetryEvent, + PlaygroundExecutedTelemetryEvent, + PlaygroundExportedToLanguageTelemetryEvent, + PlaygroundSavedTelemetryEvent, + SavedConnectionsLoadedTelemetryEvent, +} from '../../../telemetry'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require('../../../../package.json'); @@ -93,7 +102,7 @@ suite('Telemetry Controller Test Suite', () => { }); test('get segment key', () => { - let segmentKey; + let segmentKey: string | undefined; try { const segmentKeyFileLocation = '../../../../constants'; @@ -187,7 +196,7 @@ suite('Telemetry Controller Test Suite', () => { test('track document saved form a tree-view event', () => { const source = DocumentSource.DOCUMENT_SOURCE_TREEVIEW; - testTelemetryService.trackDocumentUpdated(source, true); + testTelemetryService.track(new DocumentUpdatedTelemetryEvent(source, true)); sandbox.assert.calledWith( fakeSegmentAnalyticsTrack, sinon.match({ @@ -204,7 +213,7 @@ suite('Telemetry Controller Test Suite', () => { test('track document opened form playground results', () => { const source = DocumentSource.DOCUMENT_SOURCE_PLAYGROUND; - testTelemetryService.trackDocumentOpenedInEditor(source); + testTelemetryService.track(new DocumentEditedTelemetryEvent(source)); sandbox.assert.calledWith( fakeSegmentAnalyticsTrack, sinon.match({ @@ -280,7 +289,11 @@ suite('Telemetry Controller Test Suite', () => { }); test('track playground saved event', () => { - testTelemetryService.trackPlaygroundSaved('mongodbjs'); + testTelemetryService.track( + new PlaygroundSavedTelemetryEvent( + vscode.Uri.file('/users/peter/projects/test/myplayground.mongodb.js') + ) + ); sandbox.assert.calledWith( fakeSegmentAnalyticsTrack, sinon.match({ @@ -295,7 +308,9 @@ suite('Telemetry Controller Test Suite', () => { }); test('track link clicked event', () => { - testTelemetryService.trackLinkClicked('helpPanel', 'linkId'); + testTelemetryService.track( + new LinkClickedTelemetryEvent('helpPanel', 'linkId') + ); sandbox.assert.calledWith( fakeSegmentAnalyticsTrack, sinon.match({ @@ -311,11 +326,9 @@ suite('Telemetry Controller Test Suite', () => { }); test('track playground exported to language', () => { - testTelemetryService.trackPlaygroundExportedToLanguageExported({ - language: 'java', - exported_code_length: 3, - with_driver_syntax: false, - }); + testTelemetryService.track( + new PlaygroundExportedToLanguageTelemetryEvent('java', 3, false) + ); sandbox.assert.calledWith( fakeSegmentAnalyticsTrack, @@ -341,7 +354,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('aggregation'); }); @@ -354,7 +368,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('other'); }); @@ -367,7 +382,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('other'); }); @@ -380,7 +396,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('query'); }); @@ -393,7 +410,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('other'); }); @@ -406,7 +424,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('delete'); }); @@ -419,7 +438,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('insert'); }); @@ -432,7 +452,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('insert'); }); @@ -445,7 +466,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('other'); }); @@ -458,7 +480,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('other'); }); @@ -471,7 +494,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('other'); }); @@ -484,7 +508,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('update'); }); @@ -497,7 +522,8 @@ suite('Telemetry Controller Test Suite', () => { language: 'plaintext', }, }; - const type = testTelemetryService.getPlaygroundResultType(res); + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; expect(type).to.deep.equal('other'); }); }); @@ -669,13 +695,15 @@ suite('Telemetry Controller Test Suite', () => { }); test.skip('track saved connections loaded', () => { - testTelemetryService.trackSavedConnectionsLoaded({ - saved_connections: 3, - loaded_connections: 3, - preset_connections: 3, - connections_with_secrets_in_keytar: 0, - connections_with_secrets_in_secret_storage: 3, - }); + testTelemetryService.track( + new SavedConnectionsLoadedTelemetryEvent({ + savedConnections: 3, + loadedConnections: 3, + presetConnections: 3, + connectionsWithSecretsInKeytar: 0, + connectionsWithSecretsInSecretStorage: 3, + }) + ); sandbox.assert.calledWith( fakeSegmentAnalyticsTrack, @@ -693,27 +721,6 @@ suite('Telemetry Controller Test Suite', () => { ); }); - test('track failed keytar secrets migrations', () => { - testTelemetryService.trackKeytarSecretsMigrationFailed({ - saved_connections: 3, - loaded_connections: 3, - connections_with_failed_keytar_migration: 1, - }); - - sandbox.assert.calledWith( - fakeSegmentAnalyticsTrack, - sinon.match({ - anonymousId, - event: 'Keytar Secrets Migration Failed', - properties: { - saved_connections: 3, - loaded_connections: 3, - connections_with_failed_keytar_migration: 1, - }, - }) - ); - }); - function enumKeys< TEnum extends object, TKey extends keyof TEnum = keyof TEnum @@ -724,9 +731,10 @@ suite('Telemetry Controller Test Suite', () => { test('ChatResultFeedbackKind to TelemetryFeedbackKind maps all values', () => { for (const kind of enumKeys(vscode.ChatResultFeedbackKind)) { expect( - chatResultFeedbackKindToTelemetryValue( - vscode.ChatResultFeedbackKind[kind] - ), + new ParticipantFeedbackTelemetryEvent( + vscode.ChatResultFeedbackKind[kind], + 'generic' + ).properties.feedback, `Expect ${kind} to produce a concrete telemetry value` ).to.not.be.undefined; } diff --git a/src/test/suite/views/webviewController.test.ts b/src/test/suite/views/webviewController.test.ts index f0cb964dd..ed8c33ae1 100644 --- a/src/test/suite/views/webviewController.test.ts +++ b/src/test/suite/views/webviewController.test.ts @@ -10,7 +10,7 @@ import { mdbTestExtension } from '../stubbableMdbExtension'; import { MESSAGE_TYPES } from '../../../views/webview-app/extension-app-message-constants'; import { StatusView } from '../../../views'; import { StorageController } from '../../../storage'; -import TelemetryService from '../../../telemetry/telemetryService'; +import { TelemetryService } from '../../../telemetry'; import { ExtensionContextStub } from '../stubs'; import { TEST_DATABASE_URI } from '../dbTestHelper'; import WebviewController, { diff --git a/src/utils/playground.ts b/src/utils/playground.ts index 392fbf47f..e568b0800 100644 --- a/src/utils/playground.ts +++ b/src/utils/playground.ts @@ -103,20 +103,6 @@ export const getAllText = (): string => { return vscode.window.activeTextEditor?.document.getText().trim() || ''; }; -export const getPlaygroundExtensionForTelemetry = ( - fileUri?: vscode.Uri -): string => { - let fileType = 'other'; - - if (fileUri?.fsPath.match(/\.(mongodb\.js)$/gi)) { - fileType = 'mongodbjs'; - } else if (fileUri?.fsPath.match(/\.(mongodb)$/gi)) { - fileType = 'mongodb'; - } - - return fileType; -}; - export const getPlaygrounds = async ({ fsPath, excludeFromPlaygroundsSearch, diff --git a/src/views/webviewController.ts b/src/views/webviewController.ts index fbf0c81cb..927b7648d 100644 --- a/src/views/webviewController.ts +++ b/src/views/webviewController.ts @@ -15,9 +15,13 @@ import { } from './webview-app/extension-app-message-constants'; import { openLink } from '../utils/linkHelper'; import type { StorageController } from '../storage'; -import type TelemetryService from '../telemetry/telemetryService'; +import type { TelemetryService } from '../telemetry'; import { getFeatureFlagsScript } from '../featureFlags'; -import { TelemetryEventTypes } from '../telemetry/telemetryService'; +import { + ConnectionEditedTelemetryEvent, + LinkClickedTelemetryEvent, + OpenEditConnectionTelemetryEvent, +} from '../telemetry'; import type { FileChooserOptions } from './webview-app/use-connection-form'; const log = createLogger('webview controller'); @@ -248,7 +252,7 @@ export default class WebviewController { connection: message.connectionInfo, isEditingConnection: true, }); - this._telemetryService.track(TelemetryEventTypes.CONNECTION_EDITED); + this._telemetryService.track(new ConnectionEditedTelemetryEvent()); return; case MESSAGE_TYPES.OPEN_FILE_CHOOSER: await this.handleWebviewOpenFileChooserAttempt({ @@ -292,7 +296,9 @@ export default class WebviewController { } return; case MESSAGE_TYPES.EXTENSION_LINK_CLICKED: - this._telemetryService.trackLinkClicked(message.screen, message.linkId); + this._telemetryService.track( + new LinkClickedTelemetryEvent(message.screen, message.linkId) + ); return; case MESSAGE_TYPES.RENAME_ACTIVE_CONNECTION: if (this._connectionController.isCurrentlyConnected()) { @@ -360,7 +366,7 @@ export default class WebviewController { // Wait for the panel to open. await new Promise((resolve) => setTimeout(resolve, 200)); - this._telemetryService.track(TelemetryEventTypes.OPEN_EDIT_CONNECTION); + this._telemetryService.track(new OpenEditConnectionTelemetryEvent()); void webviewPanel.webview.postMessage({ command: MESSAGE_TYPES.OPEN_EDIT_CONNECTION, From cd7a82bf840dc4585ba8c11fbaa7cf92ea0a8b28 Mon Sep 17 00:00:00 2001 From: Gaurab Aryal Date: Thu, 23 Jan 2025 14:05:12 +0000 Subject: [PATCH 06/22] feat(participant): add disambiguation examples to answer MongoDB queries in Copilot (#911) --- package.json | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/package.json b/package.json index d67dd7c3a..625963f9d 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,54 @@ "isSticky": true, "description": "Analyze a collection's schema." } + ], + "disambiguation": [ + { + "category": "mongodb", + "description": "The user is asking for assistance or information related to MongoDB.", + "examples": [ + "Explain this MongoDB query.", + "How do I write this MongoDB query in Python?", + "How do I use $lookup in MongoDB?", + "Can you help me debug a MongoDB query?", + "Rewrite this MongoDB query", + "find all documents in my collection", + "How to convert this MongoDB query to Python?", + "How to convert this MongoDB query to Java?", + "How to convert this MongoDB query to Node.js?", + "How to convert this MongoDB query to Go?", + "How to convert this MongoDB query to php?", + "How to convert this MongoDB query to C#?", + "How to convert this MongoDB query to Rust?", + "How to convert this MongoDB query to Ruby?", + "How do I update documents in MongoDB?", + "How do I insert documents in MongoDB?", + "How do I create documents in MongoDB?", + "How do I delete documents in MongoDB?", + "Explain how to use MongoDB transactions.", + "How to model one-to-one relationships in MongoDB?", + "How to model one-to-many relationships in MongoDB?", + "Help me write this MongoDB query:", + "Explain MongoDB query syntax.", + "Help me write a MongoDB aggregation pipeline.", + "Help me debug this aggregation pipeline.", + "How do I use $lookup in a MongoDB aggregation pipeline?", + "How do I optimize this MongoDB query?", + "How do I optimize indexes in MongoDB?", + "How do I optimize this data model in MongoDB?", + "How do I create a compound index for this query?", + "How do I model relationships in MongoDB?", + "Review my MongoDB schema", + "MongoDB best practices for indexing", + "How do I use MongoDB Atlas search?", + "How do I perform semantic search in MongoDB?", + "How do I stream data using MongoDB?", + "How do I monitor change streams in MongoDB?", + "How do I set up a MongoDB replica set for local development?", + "How do I add schema validation in MongoDB?", + "How to version schema in MongoDB?" + ] + } ] } ], From 6ada4e9507c5faabfce13cd9b48fa6b79a3c948f Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 23 Jan 2025 15:27:27 +0100 Subject: [PATCH 07/22] chore(ci): enable codeql scanning for actions (#913) --- .github/workflows/check-pr-title.yml | 6 +- .github/workflows/codeql.yml | 58 +++++-------------- .github/workflows/draft-release.yaml | 3 + .github/workflows/publish-release.yaml | 3 + .github/workflows/rebuild-changelog.yaml | 6 +- .../report-nightly-build-failures.yaml | 3 + .github/workflows/test-and-build.yaml | 3 + 7 files changed, 35 insertions(+), 47 deletions(-) diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml index 2a5dcc59b..b96d04e30 100644 --- a/.github/workflows/check-pr-title.yml +++ b/.github/workflows/check-pr-title.yml @@ -2,6 +2,8 @@ name: "Check PR Title" on: pull_request: types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled, converted_to_draft, edited] +permissions: + pull-requests: read jobs: check-pr-title: @@ -9,13 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Enforce conventional commit style - uses: realm/ci-actions/title-checker@main + uses: realm/ci-actions/title-checker@d6cc8f067474759d38e6d24e272027b4c88bc0a9 with: regex: '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|ops){1}(\([\w\-\.]+\))?(!)?: .*' error-hint: 'Invalid PR title. Make sure it follows the conventional commit specification (i.e. "(): ") or add the no-title-validation label' ignore-labels: 'no-title-validation' - name: Enforce JIRA ticket in title - uses: realm/ci-actions/title-checker@main + uses: realm/ci-actions/title-checker@d6cc8f067474759d38e6d24e272027b4c88bc0a9 # Skip the JIRA ticket check for PRs opened by bots if: ${{ !contains(github.event.pull_request.user.login, '[bot]') }} with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a7954643e..559f6046d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,30 +1,24 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" +name: CodeQL on: push: - branches: [ "main" ] + branches: + - main pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: + - main schedule: - cron: '32 18 * * 6' +permissions: + contents: read + security-events: write + jobs: analyze: name: Analyze - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + runs-on: ubuntu-latest + timeout-minutes: 360 permissions: actions: read contents: read @@ -33,27 +27,18 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + language: + - javascript + - actions steps: - name: Checkout repository uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality config: | paths-ignore: - '**/*.test.js' @@ -63,21 +48,6 @@ jobs: - '**/*.test.tsx' - '**/*.spec.tsx' - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml index 2246bc3ac..2a5ac954b 100644 --- a/.github/workflows/draft-release.yaml +++ b/.github/workflows/draft-release.yaml @@ -18,6 +18,9 @@ on: description: 'Exact version: (Only effective selecting "exact-version" as version bump)' required: false +permissions: + contents: write + jobs: prepare-release: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index c9e8f1462..323300232 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: publish-release: name: Publish Release diff --git a/.github/workflows/rebuild-changelog.yaml b/.github/workflows/rebuild-changelog.yaml index 6e6e11834..c818e0203 100644 --- a/.github/workflows/rebuild-changelog.yaml +++ b/.github/workflows/rebuild-changelog.yaml @@ -12,6 +12,10 @@ on: schedule: - cron: "0 3 * * *" +permissions: + contents: write + pull-requests: write + jobs: rebuild-changelog: name: Rebuild changelog @@ -63,7 +67,7 @@ jobs: - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # 7.0.6 with: commit-message: Update changelog base: main diff --git a/.github/workflows/report-nightly-build-failures.yaml b/.github/workflows/report-nightly-build-failures.yaml index 71bac7844..018ecb943 100644 --- a/.github/workflows/report-nightly-build-failures.yaml +++ b/.github/workflows/report-nightly-build-failures.yaml @@ -10,6 +10,9 @@ on: types: - completed +permissions: + contents: read + jobs: on-failure: runs-on: ubuntu-latest diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index ceaaa04f3..c57a2d215 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -10,6 +10,9 @@ on: schedule: - cron: "0 0 * * *" +permissions: + contents: read + jobs: test-and-build: name: Test and Build From 10814f01d4af8a18da40f6cbacecee7b79da15eb Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 24 Jan 2025 17:41:41 +0100 Subject: [PATCH 08/22] chore(ci): add a separate PR workflow for forks (#915) --- .../workflows/test-and-build-from-fork.yaml | 52 +++++++++++++++++++ .github/workflows/test-and-build.yaml | 1 + src/commands/launchMongoShell.ts | 10 ++-- src/editors/documentIdStore.ts | 2 +- src/explorer/databaseTreeItem.ts | 3 +- src/featureFlags.ts | 4 +- src/logging.ts | 2 +- src/storage/storageController.ts | 2 +- src/test/runTest.ts | 4 +- src/test/setup-webview.ts | 4 +- .../suite/editors/editorsController.test.ts | 2 +- .../suite/explorer/collectionTreeItem.test.ts | 2 +- .../suite/explorer/databaseTreeItem.test.ts | 2 +- .../explorer/documentListTreeItem.test.ts | 2 +- .../suite/explorer/documentTreeItem.test.ts | 2 +- src/test/suite/explorer/fieldTreeItem.test.ts | 4 +- .../suite/explorer/indexListTreeItem.test.ts | 2 +- .../suite/explorer/schemaTreeItem.test.ts | 2 +- .../explorer/streamProcessorTreeItem.test.ts | 2 +- src/test/suite/oidc.test.ts | 35 +++++++++---- src/test/suite/playground.test.ts | 2 +- src/test/suite/suggestTestHelpers.ts | 14 ++--- src/test/suite/utils/linkHelper.test.ts | 2 +- .../views/webview-app/connect-helper.test.tsx | 4 +- .../views/webview-app/overview-page.test.tsx | 6 ++- .../webview-app/resources-panel.test.tsx | 4 +- src/test/suite/waitFor.ts | 2 +- src/utils/ejson.ts | 4 +- src/utils/links.ts | 14 ++--- src/views/webview-app/atlas-cta.tsx | 4 +- .../webview-app/resources-panel/footer.tsx | 4 +- .../webview-app/resources-panel/links.tsx | 2 +- src/views/webview-app/use-connection-form.ts | 20 +++++-- .../webview-app/use-connection-status.ts | 2 +- .../use-detect-vscode-dark-mode.tsx | 2 +- 35 files changed, 156 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/test-and-build-from-fork.yaml diff --git a/.github/workflows/test-and-build-from-fork.yaml b/.github/workflows/test-and-build-from-fork.yaml new file mode 100644 index 000000000..c5d11a048 --- /dev/null +++ b/.github/workflows/test-and-build-from-fork.yaml @@ -0,0 +1,52 @@ +name: Test and Build (from fork) +on: + pull_request_target: + branches: + - main +permissions: + contents: write + pull-requests: write + +jobs: + test-and-build: + name: Test and Build + if: github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.head.repo.full_name != github.repository + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + fail-fast: false + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js Environment + uses: actions/setup-node@v4 + with: + node-version: 20.16.0 + + - name: Install Dependencies + run: npm ci --omit=optional + + - name: Run Checks + run: npm run check + # the glob here just fails + if: ${{ runner.os != 'Windows' }} + + - name: Run Tests + env: + NODE_OPTIONS: "--max_old_space_size=4096" + SEGMENT_KEY: ${{ secrets.SEGMENT_KEY_DEV }} + run: npm run test + + - name: Enable auto-merge for Dependabot PRs + if: github.event.pull_request.user.login == 'dependabot[bot]' + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index c57a2d215..922ee71a0 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -16,6 +16,7 @@ permissions: jobs: test-and-build: name: Test and Build + if: github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository strategy: matrix: diff --git a/src/commands/launchMongoShell.ts b/src/commands/launchMongoShell.ts index 2da599c7e..039044031 100644 --- a/src/commands/launchMongoShell.ts +++ b/src/commands/launchMongoShell.ts @@ -12,7 +12,7 @@ const launchMongoDBShellWithEnv = ({ mdbConnectionString: string; envVariableString: string; parentHandle?: string; -}) => { +}): void => { const mongoDBShell = vscode.window.createTerminal({ name: 'MongoDB Shell', env: { @@ -29,19 +29,19 @@ const launchMongoDBShellWithEnv = ({ mongoDBShell.show(); }; -const getPowershellEnvString = () => { +const getPowershellEnvString = (): string => { return '$Env:MDB_CONNECTION_STRING'; }; -const getCmdEnvString = () => { +const getCmdEnvString = (): string => { return '%MDB_CONNECTION_STRING%'; }; -const getGitBashEnvString = () => { +const getGitBashEnvString = (): string => { return '$MDB_CONNECTION_STRING'; }; -const getBashEnvString = () => { +const getBashEnvString = (): string => { return '$MDB_CONNECTION_STRING'; }; diff --git a/src/editors/documentIdStore.ts b/src/editors/documentIdStore.ts index ce3c963f7..6169dbf6e 100644 --- a/src/editors/documentIdStore.ts +++ b/src/editors/documentIdStore.ts @@ -32,7 +32,7 @@ export default class DocumentIdStore { return newDocument.documentIdReference; } - get(documentIdReference: string) { + get(documentIdReference: string): any { const existingDocument = this._documents.find( (item) => item.documentIdReference === documentIdReference ); diff --git a/src/explorer/databaseTreeItem.ts b/src/explorer/databaseTreeItem.ts index 24f96f64f..ee542dab9 100644 --- a/src/explorer/databaseTreeItem.ts +++ b/src/explorer/databaseTreeItem.ts @@ -129,7 +129,8 @@ export default class DatabaseTreeItem const sortFunction = ( collectionA: CollectionDetailsType, collectionB: CollectionDetailsType - ) => (collectionA.name || '').localeCompare(collectionB.name || ''); + ): number => + (collectionA.name || '').localeCompare(collectionB.name || ''); const collectionTreeEntries = [ ...otherCollections.sort(sortFunction), diff --git a/src/featureFlags.ts b/src/featureFlags.ts index 23de293f9..8c42412ea 100644 --- a/src/featureFlags.ts +++ b/src/featureFlags.ts @@ -4,14 +4,14 @@ const FEATURE_FLAGS = { export type FeatureFlag = keyof typeof FEATURE_FLAGS; -export const getFeatureFlag = (flag: FeatureFlag) => { +export const getFeatureFlag = (flag: FeatureFlag): boolean => { if (typeof window === 'object') { return (window as any).MDB_FEATURE_FLAGS?.[flag]; } return FEATURE_FLAGS[flag]; }; -export const getFeatureFlagsScript = (nonce: string) => { +export const getFeatureFlagsScript = (nonce: string): string => { return `