From c31a9c536f980b7f850f236dfdd40a1d90bf19dd Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Fri, 17 Mar 2023 19:31:00 +0100 Subject: [PATCH] feat: add diagnostics feature to the language server VSCODE-375 (#493) * feat: add diagnostics feature to the language server VSCODE-375 * test: add diagnostic suite and hopefully fix telemetry suit * test: try to replace property with sandbox * test: simplify the agg exported telemetry test * refactor: provide fix for new dbs and address other pr comments * refactor: remove unused function * fix: do not highlight use in the middle of the string * refactor: remove trim * fix: remove extra space * fix: do not find use diagnostic issue when use in the middle of other command * refactor: checking for multiple conditions with startsWith --- .github/workflows/test-and-build.yaml | 2 +- src/commands/index.ts | 3 + src/editors/editorsController.ts | 20 +- src/editors/playgroundController.ts | 40 +- ...playgroundDiagnosticsCodeActionProvider.ts | 89 ++++ src/language/diagnosticCodes.ts | 5 + src/language/mongoDBService.ts | 114 ++++- src/language/server.ts | 13 +- src/mdbExtensionController.ts | 15 + src/telemetry/telemetryService.ts | 51 ++- src/templates/playgroundTemplate.ts | 2 +- .../suite/language/mongoDBService.test.ts | 380 ++++++++++++++++- .../suite/telemetry/telemetryService.test.ts | 392 ++++++++---------- src/types/playgroundType.ts | 14 + 14 files changed, 888 insertions(+), 252 deletions(-) create mode 100644 src/editors/playgroundDiagnosticsCodeActionProvider.ts create mode 100644 src/language/diagnosticCodes.ts diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index 55a572232..f993d6293 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -43,7 +43,7 @@ jobs: uses: actions/setup-node@v2.1.2 with: # Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0 - node-version: ^14.17.5 + node-version: ^16.16.0 - name: Run node-gyp bug workaround script run: | diff --git a/src/commands/index.ts b/src/commands/index.ts index 41788dd97..b6eccf45a 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -14,6 +14,9 @@ enum EXTENSION_COMMANDS { MDB_RUN_ALL_PLAYGROUND_BLOCKS = 'mdb.runAllPlaygroundBlocks', MDB_RUN_ALL_OR_SELECTED_PLAYGROUND_BLOCKS = 'mdb.runPlayground', + MDB_FIX_THIS_INVALID_INTERACTIVE_SYNTAX = 'mdb.fixThisInvalidInteractiveSyntax', + MDB_FIX_ALL_INVALID_INTERACTIVE_SYNTAX = 'mdb.fixAllInvalidInteractiveSyntax', + MDB_EXPORT_TO_PYTHON = 'mdb.exportToPython', MDB_EXPORT_TO_JAVA = 'mdb.exportToJava', MDB_EXPORT_TO_CSHARP = 'mdb.exportToCsharp', diff --git a/src/editors/editorsController.ts b/src/editors/editorsController.ts index f02307392..d2362e60d 100644 --- a/src/editors/editorsController.ts +++ b/src/editors/editorsController.ts @@ -4,6 +4,7 @@ import { EJSON } from 'bson'; import ActiveConnectionCodeLensProvider from './activeConnectionCodeLensProvider'; import ExportToLanguageCodeLensProvider from './exportToLanguageCodeLensProvider'; import PlaygroundSelectedCodeActionProvider from './playgroundSelectedCodeActionProvider'; +import PlaygroundDiagnosticsCodeActionProvider from './playgroundDiagnosticsCodeActionProvider'; import ConnectionController from '../connectionController'; import CollectionDocumentsCodeLensProvider from './collectionDocumentsCodeLensProvider'; import CollectionDocumentsOperationsStore from './collectionDocumentsOperationsStore'; @@ -84,6 +85,7 @@ export function getViewCollectionDocumentsUri( */ export default class EditorsController { _playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; + _playgroundDiagnosticsCodeActionProvider: PlaygroundDiagnosticsCodeActionProvider; _connectionController: ConnectionController; _playgroundController: PlaygroundController; _collectionDocumentsOperationsStore = @@ -110,7 +112,8 @@ export default class EditorsController { playgroundResultViewProvider: PlaygroundResultProvider, activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider, exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider, - codeActionProvider: PlaygroundSelectedCodeActionProvider, + playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider, + playgroundDiagnosticsCodeActionProvider: PlaygroundDiagnosticsCodeActionProvider, editDocumentCodeLensProvider: EditDocumentCodeLensProvider ) { this._connectionController = connectionController; @@ -141,7 +144,10 @@ export default class EditorsController { new CollectionDocumentsCodeLensProvider( this._collectionDocumentsOperationsStore ); - this._playgroundSelectedCodeActionProvider = codeActionProvider; + this._playgroundSelectedCodeActionProvider = + playgroundSelectedCodeActionProvider; + this._playgroundDiagnosticsCodeActionProvider = + playgroundDiagnosticsCodeActionProvider; vscode.workspace.onDidCloseTextDocument((e) => { const uriParams = new URLSearchParams(e.uri.query); @@ -436,6 +442,16 @@ export default class EditorsController { } ) ); + this._context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + 'javascript', + this._playgroundDiagnosticsCodeActionProvider, + { + providedCodeActionKinds: + PlaygroundDiagnosticsCodeActionProvider.providedCodeActionKinds, + } + ) + ); } deactivate(): void { diff --git a/src/editors/playgroundController.ts b/src/editors/playgroundController.ts index e2f7892e4..d0c2773bc 100644 --- a/src/editors/playgroundController.ts +++ b/src/editors/playgroundController.ts @@ -28,6 +28,8 @@ import { ExportToLanguageAddons, ExportToLanguageNamespace, ExportToLanguageMode, + ThisDiagnosticFix, + AllDiagnosticFixes, } from '../types/playgroundType'; import PlaygroundResultProvider, { PLAYGROUND_RESULT_SCHEME, @@ -125,7 +127,7 @@ export default class PlaygroundController { playgroundResultViewProvider: PlaygroundResultProvider, activeConnectionCodeLensProvider: ActiveConnectionCodeLensProvider, exportToLanguageCodeLensProvider: ExportToLanguageCodeLensProvider, - codeActionProvider: PlaygroundSelectedCodeActionProvider, + playgroundSelectedCodeActionProvide: PlaygroundSelectedCodeActionProvider, explorerController: ExplorerController ) { this._connectionController = connectionController; @@ -138,7 +140,8 @@ export default class PlaygroundController { vscode.window.createOutputChannel('Playground output'); this._activeConnectionCodeLensProvider = activeConnectionCodeLensProvider; this._exportToLanguageCodeLensProvider = exportToLanguageCodeLensProvider; - this._playgroundSelectedCodeActionProvider = codeActionProvider; + this._playgroundSelectedCodeActionProvider = + playgroundSelectedCodeActionProvide; this._explorerController = explorerController; this._connectionController.addEventListener( @@ -272,9 +275,15 @@ export default class PlaygroundController { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const filePath = workspaceFolder?.uri.fsPath || os.homedir(); + // We count open untitled playground files to use this number as part of a new playground path. const numberUntitledPlaygrounds = vscode.workspace.textDocuments.filter( (doc) => isPlayground(doc.uri) ).length; + + // We need a secondary `mongodb` extension otherwise VSCode will + // suggest playground-1.js name when saving playground to the disk. + // Users can open playgrounds from the disk + // and we need a way to distinguish this files from regular JS files. const fileName = path.join( filePath, `playground-${numberUntitledPlaygrounds + 1}.mongodb.js` @@ -293,6 +302,8 @@ export default class PlaygroundController { await vscode.workspace.applyEdit(edit); // Actually show the editor. + // We open playgrounds by URI to use the secondary `mongodb` extension + // as an identifier that distinguishes them from regular JS files. const document = await vscode.workspace.openTextDocument(documentUri); // Focus new text document. @@ -639,6 +650,31 @@ export default class PlaygroundController { return this._evaluatePlayground(); } + async fixThisInvalidInteractiveSyntax({ + documentUri, + range, + fix, + }: ThisDiagnosticFix) { + const edit = new vscode.WorkspaceEdit(); + edit.replace(documentUri, range, fix); + await vscode.workspace.applyEdit(edit); + return true; + } + + async fixAllInvalidInteractiveSyntax({ + documentUri, + diagnostics, + }: AllDiagnosticFixes) { + const edit = new vscode.WorkspaceEdit(); + + for (const { range, fix } of diagnostics) { + edit.replace(documentUri, range, fix); + } + + await vscode.workspace.applyEdit(edit); + return true; + } + async openPlayground(filePath: string): Promise { try { const document = await vscode.workspace.openTextDocument(filePath); diff --git a/src/editors/playgroundDiagnosticsCodeActionProvider.ts b/src/editors/playgroundDiagnosticsCodeActionProvider.ts new file mode 100644 index 000000000..20072b3b4 --- /dev/null +++ b/src/editors/playgroundDiagnosticsCodeActionProvider.ts @@ -0,0 +1,89 @@ +import * as vscode from 'vscode'; + +import type { Diagnostic } from 'vscode-languageserver/node'; + +import EXTENSION_COMMANDS from '../commands'; +import DIAGNOSTIC_CODES from './../language/diagnosticCodes'; + +export default class PlaygroundDiagnosticsCodeActionProvider + implements vscode.CodeActionProvider +{ + _onDidChangeCodeCodeAction: vscode.EventEmitter = + new vscode.EventEmitter(); + + static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix]; + + constructor() { + vscode.workspace.onDidChangeConfiguration(() => { + this._onDidChangeCodeCodeAction.fire(); + }); + } + + readonly onDidChangeCodeLenses: vscode.Event = + this._onDidChangeCodeCodeAction.event; + + provideCodeActions( + document: vscode.TextDocument, + _range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext + ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + const fixCodeActions: vscode.CodeAction[] = []; + const diagnostics = context.diagnostics as unknown as Diagnostic[]; + + for (const diagnostic of diagnostics) { + switch (diagnostic.code) { + case DIAGNOSTIC_CODES.invalidInteractiveSyntaxes: + { + const fix = new vscode.CodeAction( + 'Fix this interactive syntax problem', + vscode.CodeActionKind.QuickFix + ); + fix.command = { + command: + EXTENSION_COMMANDS.MDB_FIX_THIS_INVALID_INTERACTIVE_SYNTAX, + title: 'Fix invalid interactive syntax', + arguments: [ + { + documentUri: document.uri, + range: diagnostic.range, + fix: diagnostic.data?.fix, + }, + ], + }; + fixCodeActions.push(fix); + } + break; + default: + break; + } + } + + const allDiagnostics = vscode.languages + .getDiagnostics(document.uri) + .filter((d) => d.code === DIAGNOSTIC_CODES.invalidInteractiveSyntaxes); + + if (allDiagnostics.length > 1) { + const fix = new vscode.CodeAction( + 'Fix all interactive syntax problems', + vscode.CodeActionKind.QuickFix + ); + + fix.command = { + command: EXTENSION_COMMANDS.MDB_FIX_ALL_INVALID_INTERACTIVE_SYNTAX, + title: 'Fix invalid interactive syntax', + arguments: [ + { + documentUri: document.uri, + diagnostics: allDiagnostics.map((d) => ({ + range: d.range, + fix: (d as Diagnostic).data?.fix, + })), + }, + ], + }; + fixCodeActions.push(fix); + } + + return fixCodeActions; + } +} diff --git a/src/language/diagnosticCodes.ts b/src/language/diagnosticCodes.ts new file mode 100644 index 000000000..872310790 --- /dev/null +++ b/src/language/diagnosticCodes.ts @@ -0,0 +1,5 @@ +enum DIAGNOSTIC_CODES { + invalidInteractiveSyntaxes = 'playground.invalidInteractiveSyntaxes', +} + +export default DIAGNOSTIC_CODES; diff --git a/src/language/mongoDBService.ts b/src/language/mongoDBService.ts index e02ca7961..f95b730a7 100644 --- a/src/language/mongoDBService.ts +++ b/src/language/mongoDBService.ts @@ -1,10 +1,15 @@ import * as util from 'util'; -import { CompletionItemKind, MarkupKind } from 'vscode-languageserver/node'; +import { + CompletionItemKind, + MarkupKind, + DiagnosticSeverity, +} from 'vscode-languageserver/node'; import type { CancellationToken, Connection, CompletionItem, MarkupContent, + Diagnostic, } from 'vscode-languageserver/node'; import path from 'path'; import { signatures } from '@mongosh/shell-api'; @@ -25,6 +30,7 @@ import type { MongoClientOptions, } from '../types/playgroundType'; import { Visitor } from './visitor'; +import DIAGNOSTIC_CODES from './diagnosticCodes'; export const languageServerWorkerFileName = 'languageServerWorker.js'; @@ -86,7 +92,7 @@ export default class MongoDBService { connectionId, connectionString, connectionOptions, - }: ServiceProviderParams): Promise { + }: ServiceProviderParams): Promise { // If already connected close the previous connection. await this.disconnectFromServiceProvider(); @@ -101,15 +107,12 @@ export default class MongoDBService { try { // Get database names for the current connection. const databases = await this._getDatabases(); - // Create and cache database completion items. this._cacheDatabaseCompletionItems(databases); - return Promise.resolve(true); } catch (error) { this._connection.console.error( `LS get databases error: ${util.inspect(error)}` ); - return Promise.resolve(false); } } @@ -557,6 +560,103 @@ export default class MongoDBService { return []; } + // Highlight the usage of commands that only works inside interactive session. + // eslint-disable-next-line complexity + provideDiagnostics(textFromEditor: string) { + const lines = textFromEditor.split(/\r?\n/g); + const diagnostics: Diagnostic[] = []; + const invalidInteractiveSyntaxes = [ + { issue: 'use', fix: "use('VALUE_TO_REPLACE')", default: 'database' }, + { issue: 'show databases', fix: 'db.getMongo().getDBs()' }, + { issue: 'show dbs', fix: 'db.getMongo().getDBs()' }, + { issue: 'show collections', fix: 'db.getCollectionNames()' }, + { issue: 'show tables', fix: 'db.getCollectionNames()' }, + { + issue: 'show profile', + fix: "db.getCollection('system.profile').find()", + }, + { issue: 'show users', fix: 'db.getUsers()' }, + { issue: 'show roles', fix: 'db.getRoles({ showBuiltinRoles: true })' }, + { issue: 'show logs', fix: "db.adminCommand({ getLog: '*' })" }, + { + issue: 'show log', + fix: "db.adminCommand({ getLog: 'VALUE_TO_REPLACE' })", + default: 'global', + }, + ]; + + for (const [i, line] of lines.entries()) { + for (const item of invalidInteractiveSyntaxes) { + const issue = item.issue; // E.g. 'use'. + const startCharacter = line.indexOf(issue); // The start index where the issue was found in the string. + + // In case of `show logs` exclude `show log` diagnostic issue. + if ( + issue === 'show log' && + line.substring(startCharacter).startsWith('show logs') + ) { + continue; + } + + // In case of `user.authenticate()` do not rise a diagnostic issue. + if ( + issue === 'use' && + !line.substring(startCharacter).startsWith('use ') + ) { + continue; + } + + if (!line.trim().startsWith(issue)) { + continue; + } + + let endCharacter = startCharacter + issue.length; + let valueToReplaceWith = item.default; + let fix = item.fix; + + // If there is a default value, it should be used + // instead of VALUE_TO_REPLACE placeholder of the `fix` string. + if (valueToReplaceWith) { + const words = line.substring(startCharacter).split(' '); + + // The index of the value for `use ` and `show log `. + const valueIndex = issue.split(' ').length; + + if (words[valueIndex]) { + // The `use ('database')`, `use []`, `use .` are valid JS strings. + if ( + ['(', '[', '.'].some((word) => words[valueIndex].startsWith(word)) + ) { + continue; + } + + // Get the value from a string by removing quotes if any. + valueToReplaceWith = words[valueIndex].replace(/['";]+/g, ''); + + // Add the replacement value and the space between to the total command length. + endCharacter += words[valueIndex].length + 1; + } + + fix = fix.replace('VALUE_TO_REPLACE', valueToReplaceWith); + } + + diagnostics.push({ + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: i, character: startCharacter }, + end: { line: i, character: endCharacter }, + }, + message: `Did you mean \`${fix}\`?`, + data: { fix }, + }); + } + } + + return diagnostics; + } + /** * Convert schema field names to Completion Items and cache them. */ @@ -576,8 +676,8 @@ export default class MongoDBService { * Convert database names to Completion Items and cache them. */ _cacheDatabaseCompletionItems(databases: Document[]): void { - this._cachedDatabases = databases.map((item) => ({ - label: (item as { name: string }).name, + this._cachedDatabases = databases.map((db) => ({ + label: (db as { name: string }).name, kind: CompletionItemKind.Field, preselect: true, })); diff --git a/src/language/server.ts b/src/language/server.ts index 34b8098b4..d4437cf6a 100644 --- a/src/language/server.ts +++ b/src/language/server.ts @@ -125,10 +125,15 @@ documents.onDidClose((e) => { // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. -documents.onDidChangeContent((/* change */) => { - // connection.console.log( - // `documents.onDidChangeContent: ${JSON.stringify(change)}` - // ); +documents.onDidChangeContent(async (change) => { + const textFromEditor = change.document.getText(); + + const diagnostics = mongoDBService.provideDiagnostics( + textFromEditor ? textFromEditor : '' + ); + + // Send the computed diagnostics to VSCode. + await connection.sendDiagnostics({ uri: change.document.uri, diagnostics }); }); connection.onRequest(new RequestType('textDocument/codeLens'), (/* event*/) => { diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index f4f623f2c..5a45874b3 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import ActiveConnectionCodeLensProvider from './editors/activeConnectionCodeLensProvider'; import PlaygroundSelectedCodeActionProvider from './editors/playgroundSelectedCodeActionProvider'; +import PlaygroundDiagnosticsCodeActionProvider from './editors/playgroundDiagnosticsCodeActionProvider'; import ConnectionController from './connectionController'; import ConnectionTreeItem from './explorer/connectionTreeItem'; import { createLogger } from './logging'; @@ -45,6 +46,7 @@ const log = createLogger('commands'); // Commands which the extensions handles are defined in the function `activate`. export default class MDBExtensionController implements vscode.Disposable { _playgroundSelectedCodeActionProvider: PlaygroundSelectedCodeActionProvider; + _playgroundDiagnosticsCodeActionProvider: PlaygroundDiagnosticsCodeActionProvider; _connectionController: ConnectionController; _context: vscode.ExtensionContext; _editorsController: EditorsController; @@ -98,6 +100,8 @@ export default class MDBExtensionController implements vscode.Disposable { new ExportToLanguageCodeLensProvider(); this._playgroundSelectedCodeActionProvider = new PlaygroundSelectedCodeActionProvider(); + this._playgroundDiagnosticsCodeActionProvider = + new PlaygroundDiagnosticsCodeActionProvider(); this._playgroundController = new PlaygroundController( this._connectionController, this._languageServerController, @@ -119,6 +123,7 @@ export default class MDBExtensionController implements vscode.Disposable { this._activeConnectionCodeLensProvider, this._exportToLanguageCodeLensProvider, this._playgroundSelectedCodeActionProvider, + this._playgroundDiagnosticsCodeActionProvider, this._editDocumentCodeLensProvider ); this._webviewController = new WebviewController( @@ -196,6 +201,16 @@ export default class MDBExtensionController implements vscode.Disposable { () => this._playgroundController.runAllOrSelectedPlaygroundBlocks() ); + this.registerCommand( + EXTENSION_COMMANDS.MDB_FIX_THIS_INVALID_INTERACTIVE_SYNTAX, + (data) => this._playgroundController.fixThisInvalidInteractiveSyntax(data) + ); + + this.registerCommand( + EXTENSION_COMMANDS.MDB_FIX_ALL_INVALID_INTERACTIVE_SYNTAX, + (data) => this._playgroundController.fixAllInvalidInteractiveSyntax(data) + ); + // ------ EXPORT TO LANGUAGE ------ // this.registerCommand(EXTENSION_COMMANDS.MDB_EXPORT_TO_PYTHON, () => this._playgroundController.exportToLanguage(ExportToLanguages.PYTHON) diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index 8c00045b1..353c6764f 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -174,40 +174,47 @@ export default class TelemetryService { return true; } + _segmentAnalyticsTrack(segmentProperties: SegmentProperties) { + if (!this._isTelemetryFeatureEnabled()) { + return; + } + + this._segmentAnalytics?.track(segmentProperties, (error?: Error) => { + if (error) { + log.error('Failed to track telemetry', error); + } else { + log.info('Telemetry sent', error); + } + }); + } + track( eventType: TelemetryEventTypes, properties?: TelemetryEventProperties ): void { - if (this._isTelemetryFeatureEnabled()) { - const segmentProperties: SegmentProperties = { - ...this.getTelemetryUserIdentity(), - event: eventType, - properties: { - ...properties, - extension_version: `${version}`, - }, - }; - - log.info('Telemetry track properties', segmentProperties); + this._segmentAnalyticsTrack({ + ...this.getTelemetryUserIdentity(), + event: eventType, + properties: { + ...properties, + extension_version: `${version}`, + }, + }); + } - this._segmentAnalytics?.track(segmentProperties, (error?: Error) => { - if (error) { - log.error('Failed to track telemetry', error); - } - }); - } + async _getConnectionTelemetryProperties( + dataService: DataService, + connectionType: ConnectionTypes + ) { + return await getConnectionTelemetryProperties(dataService, connectionType); } async trackNewConnection( dataService: DataService, connectionType: ConnectionTypes ): Promise { - if (!this._isTelemetryFeatureEnabled()) { - return; - } - const connectionTelemetryProperties = - await getConnectionTelemetryProperties(dataService, connectionType); + await this._getConnectionTelemetryProperties(dataService, connectionType); this.track( TelemetryEventTypes.NEW_CONNECTION, diff --git a/src/templates/playgroundTemplate.ts b/src/templates/playgroundTemplate.ts index c65696d4c..ca7e99288 100644 --- a/src/templates/playgroundTemplate.ts +++ b/src/templates/playgroundTemplate.ts @@ -17,7 +17,7 @@ db.getCollection('sales').insertMany([ { 'item': 'abc', 'price': 10, 'quantity': 2, 'date': new Date('2014-03-01T08:00:00Z') }, { 'item': 'jkl', 'price': 20, 'quantity': 1, 'date': new Date('2014-03-01T09:00:00Z') }, { 'item': 'xyz', 'price': 5, 'quantity': 10, 'date': new Date('2014-03-15T09:00:00Z') }, - { 'item': 'xyz', 'price': 5, 'quantity': 20, 'date': new Date('2014-04-04T11:21:39.736Z') }, + { 'item': 'xyz', 'price': 5, 'quantity': 20, 'date': new Date('2014-04-04T11:21:39.736Z') }, { 'item': 'abc', 'price': 10, 'quantity': 10, 'date': new Date('2014-04-04T21:23:13.331Z') }, { 'item': 'def', 'price': 7.5, 'quantity': 5, 'date': new Date('2015-06-04T05:08:13Z') }, { 'item': 'def', 'price': 7.5, 'quantity': 10, 'date': new Date('2015-09-10T08:43:00Z') }, diff --git a/src/test/suite/language/mongoDBService.test.ts b/src/test/suite/language/mongoDBService.test.ts index f98e4f488..c50afe661 100644 --- a/src/test/suite/language/mongoDBService.test.ts +++ b/src/test/suite/language/mongoDBService.test.ts @@ -3,8 +3,9 @@ import { before } from 'mocha'; import { CancellationTokenSource, CompletionItemKind, - CompletionItem, + DiagnosticSeverity, } from 'vscode-languageclient/node'; +import type { CompletionItem } from 'vscode-languageclient/node'; import chai from 'chai'; import { createConnection } from 'vscode-languageserver/node'; import fs from 'fs'; @@ -16,6 +17,7 @@ import MongoDBService, { import { mdbTestExtension } from '../stubbableMdbExtension'; import { StreamStub } from '../stubs'; import READ_PREFERENCES from '../../../views/webview-app/connection-model/constants/read-preferences'; +import DIAGNOSTIC_CODES from '../../../language/diagnosticCodes'; const expect = chai.expect; const INCREASED_TEST_TIMEOUT = 5000; @@ -1381,7 +1383,7 @@ suite('MongoDBService Test Suite', () => { }); }); - suite('getExportToLanguageMode', function () { + suite('Export to language mode', function () { this.timeout(INCREASED_TEST_TIMEOUT); const up = new StreamStub(); @@ -1540,4 +1542,378 @@ suite('MongoDBService Test Suite', () => { expect(mode).to.be.equal('AGGREGATION'); }); }); + + suite('Diagnostic', function () { + const up = new StreamStub(); + const down = new StreamStub(); + const connection = createConnection(up, down); + + connection.listen(); + + const testMongoDBService = new MongoDBService(connection); + + before(() => { + testMongoDBService._cacheDatabaseCompletionItems([{ name: 'test' }]); + }); + + test('does not find use diagnostic issue when a line does not start with use', () => { + const textFromEditor = + "You can use '.hasNext()/.next()' to iterate through the cursor page by page"; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([]); + }); + + test('does not find use diagnostic issue when use in the middle of other command', () => { + const textFromEditor = 'user.authenticate()'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([]); + }); + + test('does not find use diagnostic issue when use is followed by a space and curly bracket', () => { + const textFromEditor = 'use ('; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([]); + }); + + test('does not find use diagnostic issue when use is followed by a space and point', () => { + const textFromEditor = 'use .'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([]); + }); + + test('does not find use diagnostic issue when use is followed by a space and bracket', () => { + const textFromEditor = 'use ['; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([]); + }); + + test('finds use without database diagnostic issue', () => { + const textFromEditor = 'use '; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 3 }, + }, + message: "Did you mean `use('database')`?", + data: { fix: "use('database')" }, + }, + ]); + }); + + test('finds use with an existing database without quotes diagnostic issue', () => { + const textFromEditor = 'use test'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 8 }, + }, + message: "Did you mean `use('test')`?", + data: { fix: "use('test')" }, + }, + ]); + }); + + test('finds use with a new database without quotes diagnostic issue', () => { + const textFromEditor = 'use lena'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 8 }, + }, + message: "Did you mean `use('lena')`?", + data: { fix: "use('lena')" }, + }, + ]); + }); + + test('finds use with database and single quotes diagnostic issue', () => { + const textFromEditor = "use 'test'"; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 }, + }, + message: "Did you mean `use('test')`?", + data: { fix: "use('test')" }, + }, + ]); + }); + + test('finds use with database and double quotes diagnostic issue', () => { + const textFromEditor = 'use "test"'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 }, + }, + message: "Did you mean `use('test')`?", + data: { fix: "use('test')" }, + }, + ]); + }); + + test('finds show databases diagnostic issue', () => { + const textFromEditor = 'show databases'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 14 }, + }, + message: 'Did you mean `db.getMongo().getDBs()`?', + data: { fix: 'db.getMongo().getDBs()' }, + }, + ]); + }); + + test('finds show dbs diagnostic issue', () => { + const textFromEditor = 'show dbs'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 8 }, + }, + message: 'Did you mean `db.getMongo().getDBs()`?', + data: { fix: 'db.getMongo().getDBs()' }, + }, + ]); + }); + + test('finds show collections diagnostic issue', () => { + const textFromEditor = 'show collections'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 16 }, + }, + message: 'Did you mean `db.getCollectionNames()`?', + data: { fix: 'db.getCollectionNames()' }, + }, + ]); + }); + + test('finds show tables diagnostic issue', () => { + const textFromEditor = 'show tables'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 11 }, + }, + message: 'Did you mean `db.getCollectionNames()`?', + data: { fix: 'db.getCollectionNames()' }, + }, + ]); + }); + + test('finds show profile diagnostic issue', () => { + const textFromEditor = 'show profile'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 12 }, + }, + message: "Did you mean `db.getCollection('system.profile').find()`?", + data: { fix: "db.getCollection('system.profile').find()" }, + }, + ]); + }); + + test('finds show users diagnostic issue', () => { + const textFromEditor = 'show users'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 }, + }, + message: 'Did you mean `db.getUsers()`?', + data: { fix: 'db.getUsers()' }, + }, + ]); + }); + + test('finds show roles diagnostic issue', () => { + const textFromEditor = 'show roles'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 }, + }, + message: 'Did you mean `db.getRoles({ showBuiltinRoles: true })`?', + data: { fix: 'db.getRoles({ showBuiltinRoles: true })' }, + }, + ]); + }); + + test('finds show logs diagnostic issue', () => { + const textFromEditor = 'show logs'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 9 }, + }, + message: "Did you mean `db.adminCommand({ getLog: '*' })`?", + data: { fix: "db.adminCommand({ getLog: '*' })" }, + }, + ]); + }); + + test('finds show log diagnostic issue', () => { + const textFromEditor = 'show log'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 8 }, + }, + message: "Did you mean `db.adminCommand({ getLog: 'global' })`?", + data: { fix: "db.adminCommand({ getLog: 'global' })" }, + }, + ]); + }); + + test('finds show log without type diagnostic issue', () => { + const textFromEditor = 'show log '; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 8 }, + }, + message: "Did you mean `db.adminCommand({ getLog: 'global' })`?", + data: { fix: "db.adminCommand({ getLog: 'global' })" }, + }, + ]); + }); + + test('finds show log with type and single quotes diagnostic issue', () => { + const textFromEditor = "show log 'global'"; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 17 }, + }, + message: "Did you mean `db.adminCommand({ getLog: 'global' })`?", + data: { fix: "db.adminCommand({ getLog: 'global' })" }, + }, + ]); + }); + + test('finds show log with type and double quotes diagnostic issue', () => { + const textFromEditor = 'show log "startupWarnings"'; + const diagnostics = testMongoDBService.provideDiagnostics(textFromEditor); + + expect(diagnostics).to.be.deep.equal([ + { + severity: DiagnosticSeverity.Error, + source: 'mongodb', + code: DIAGNOSTIC_CODES.invalidInteractiveSyntaxes, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 26 }, + }, + message: + "Did you mean `db.adminCommand({ getLog: 'startupWarnings' })`?", + data: { fix: "db.adminCommand({ getLog: 'startupWarnings' })" }, + }, + ]); + }); + }); }); diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index 6ef935f55..c99c1463c 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -8,19 +8,13 @@ import { resolve } from 'path'; import sinon from 'sinon'; import type { SinonSpy } from 'sinon'; import sinonChai from 'sinon-chai'; -import SegmentAnalytics from 'analytics-node'; import { ConnectionTypes } from '../../../connectionController'; import { DocumentSource } from '../../../documentSource'; import { ExportToLanguageMode } from '../../../types/playgroundType'; import { mdbTestExtension } from '../stubbableMdbExtension'; -import { - SegmentProperties, - TelemetryEventTypes, -} from '../../../telemetry/telemetryService'; const expect = chai.expect; -const { version } = require('../../../../package.json'); chai.use(sinonChai); @@ -30,21 +24,13 @@ suite('Telemetry Controller Test Suite', () => { const testTelemetryService = mdbTestExtension.testExtensionController._telemetryService; let dataServiceStub: DataService; + const { anonymousId } = testTelemetryService.getTelemetryUserIdentity(); + + let fakeSegmentAnalyticsTrack: SinonSpy; - let fakeTrackNewConnection: SinonSpy; - let fakeTrackCommandRun: SinonSpy; - let fakeTrackPlaygroundCodeExecuted: SinonSpy; - let fakeTrackPlaygroundLoadedMethod: SinonSpy; - let fakeTrack: SinonSpy; const sandbox = sinon.createSandbox(); beforeEach(() => { - fakeTrackNewConnection = sandbox.fake.resolves(true); - fakeTrackCommandRun = sandbox.fake(); - fakeTrackPlaygroundCodeExecuted = sandbox.fake(); - fakeTrackPlaygroundLoadedMethod = sandbox.fake(); - fakeTrack = sandbox.fake(); - const instanceStub = sandbox.stub(); instanceStub.resolves({ dataLake: {}, @@ -56,27 +42,24 @@ suite('Telemetry Controller Test Suite', () => { instance: instanceStub, } as Pick as unknown as DataService; + fakeSegmentAnalyticsTrack = sandbox.fake(); sandbox.replace( mdbTestExtension.testExtensionController._telemetryService, - 'trackCommandRun', - fakeTrackCommandRun - ); - sandbox.replace( - mdbTestExtension.testExtensionController._playgroundController - ._telemetryService, - 'trackPlaygroundCodeExecuted', - fakeTrackPlaygroundCodeExecuted + '_segmentAnalyticsTrack', + fakeSegmentAnalyticsTrack ); sandbox.replace( mdbTestExtension.testExtensionController._playgroundController - ._telemetryService, - 'trackPlaygroundLoaded', - fakeTrackPlaygroundLoadedMethod - ); - sandbox.replace( - mdbTestExtension.testExtensionController._languageServerController, + ._languageServerController, 'evaluate', - sandbox.fake.resolves([{ type: 'TEST', content: 'Result' }]) + sandbox.fake.resolves({ + result: { + namespace: 'db.coll', + type: 'other', + content: 'dbs', + language: 'plaintext', + }, + }) ); sandbox.replace( mdbTestExtension.testExtensionController._playgroundController @@ -109,6 +92,11 @@ suite('Telemetry Controller Test Suite', () => { }) ); sandbox.stub(vscode.window, 'showInformationMessage'); + sandbox.replace( + testTelemetryService, + '_isTelemetryFeatureEnabled', + sandbox.fake.returns(true) + ); }); afterEach(() => { @@ -117,7 +105,7 @@ suite('Telemetry Controller Test Suite', () => { }); test('get segment key', () => { - let segmentKey: string | undefined; + let segmentKey; try { const segmentKeyFileLocation = '../../../../constants'; @@ -132,110 +120,109 @@ suite('Telemetry Controller Test Suite', () => { test('track command run event', async () => { await vscode.commands.executeCommand('mdb.addConnection'); - sandbox.assert.calledWith(fakeTrackCommandRun, 'mdb.addConnection'); - }); - - test('track new connection event when connecting via connection string', () => { - const testConnectionController = - mdbTestExtension.testExtensionController._connectionController; - - sandbox.replace( - mdbTestExtension.testExtensionController._telemetryService, - 'trackNewConnection', - fakeTrackNewConnection + sandbox.assert.calledWith( + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Command Run', + properties: { + command: 'mdb.addConnection', + extension_version: '0.0.0-dev.0', + }, + }) ); + }); - testConnectionController.sendTelemetry( + test('track new connection event when connecting via connection string', async () => { + await testTelemetryService.trackNewConnection( dataServiceStub, ConnectionTypes.CONNECTION_STRING ); - sandbox.assert.calledWith( - fakeTrackNewConnection, - sandbox.match.any, - sandbox.match(ConnectionTypes.CONNECTION_STRING) + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'New Connection', + properties: { + is_used_connect_screen: false, + is_used_command_palette: true, + is_used_saved_connection: false, + vscode_mdb_extension_version: '0.0.0-dev.0', + extension_version: '0.0.0-dev.0', + }, + }) ); }); - test('track new connection event when connecting via connection form', () => { - const testConnectionController = - mdbTestExtension.testExtensionController._connectionController; - - sandbox.replace( - mdbTestExtension.testExtensionController._telemetryService, - 'trackNewConnection', - fakeTrackNewConnection - ); - - testConnectionController.sendTelemetry( + test('track new connection event when connecting via connection form', async () => { + await testTelemetryService.trackNewConnection( dataServiceStub, ConnectionTypes.CONNECTION_FORM ); - sandbox.assert.calledWith( - fakeTrackNewConnection, - sandbox.match.any, - sandbox.match(ConnectionTypes.CONNECTION_FORM) + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'New Connection', + properties: { + is_used_connect_screen: true, + is_used_command_palette: false, + is_used_saved_connection: false, + vscode_mdb_extension_version: '0.0.0-dev.0', + extension_version: '0.0.0-dev.0', + }, + }) ); }); - test('track new connection event when connecting via saved connection', () => { - const testConnectionController = - mdbTestExtension.testExtensionController._connectionController; - - sandbox.replace( - mdbTestExtension.testExtensionController._telemetryService, - 'trackNewConnection', - fakeTrackNewConnection - ); - - testConnectionController.sendTelemetry( + test('track new connection event when connecting via saved connection', async () => { + await testTelemetryService.trackNewConnection( dataServiceStub, ConnectionTypes.CONNECTION_ID ); - sandbox.assert.calledWith( - fakeTrackNewConnection, - sandbox.match.any, - sandbox.match(ConnectionTypes.CONNECTION_ID) + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'New Connection', + properties: { + is_used_connect_screen: false, + is_used_command_palette: false, + is_used_saved_connection: true, + vscode_mdb_extension_version: '0.0.0-dev.0', + extension_version: '0.0.0-dev.0', + }, + }) ); }); test('track document saved form a tree-view event', () => { const source = DocumentSource.DOCUMENT_SOURCE_TREEVIEW; - - sandbox.replace(testTelemetryService, 'track', fakeTrack); - testTelemetryService.trackDocumentUpdated(source, true); - sandbox.assert.calledWith( - fakeTrack, - sandbox.match('Document Updated'), - sandbox.match({ source, success: true }) + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Document Updated', + properties: { + source: 'treeview', + success: true, + extension_version: '0.0.0-dev.0', + }, + }) ); }); - test('track document opened form playground results', async () => { - const fakeTrackDocumentOpenedInEditor = sandbox.fake(); - sandbox.replace( - mdbTestExtension.testExtensionController._telemetryService, - 'trackDocumentOpenedInEditor', - fakeTrackDocumentOpenedInEditor - ); - - await vscode.commands.executeCommand( - 'mdb.openMongoDBDocumentFromCodeLens', - { - source: 'playground', - line: 1, - documentId: '93333a0d-83f6-4e6f-a575-af7ea6187a4a', - namespace: 'db.coll', - connectionId: null, - } - ); - - expect(fakeTrackDocumentOpenedInEditor.firstCall.firstArg).to.be.equal( - 'playground' + test('track document opened form playground results', () => { + const source = DocumentSource.DOCUMENT_SOURCE_PLAYGROUND; + testTelemetryService.trackDocumentOpenedInEditor(source); + sandbox.assert.calledWith( + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Document Edited', + properties: { source: 'playground', extension_version: '0.0.0-dev.0' }, + }) ); }); @@ -243,7 +230,19 @@ suite('Telemetry Controller Test Suite', () => { const testPlaygroundController = mdbTestExtension.testExtensionController._playgroundController; await testPlaygroundController._evaluate('show dbs'); - sandbox.assert.called(fakeTrackPlaygroundCodeExecuted); + sandbox.assert.calledWith( + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Playground Code Executed', + properties: { + type: 'other', + partial: false, + error: false, + extension_version: '0.0.0-dev.0', + }, + }) + ); }); test('track playground loaded event', async () => { @@ -252,72 +251,47 @@ suite('Telemetry Controller Test Suite', () => { '../../../../src/test/fixture/testSaving.mongodb' ); await vscode.workspace.openTextDocument(vscode.Uri.file(docPath)); - sandbox.assert.called(fakeTrackPlaygroundLoadedMethod); + sandbox.assert.calledWith( + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Playground Loaded', + properties: { extension_version: '0.0.0-dev.0' }, + }) + ); }); test('track playground saved event', () => { - sandbox.replace(testTelemetryService, 'track', fakeTrack); testTelemetryService.trackPlaygroundSaved(); - sandbox.assert.calledWith(fakeTrack); - }); - - test('track adds extension version to event properties when there are no event properties', () => { - sandbox.replace( - testTelemetryService, - '_isTelemetryFeatureEnabled', - sandbox.fake.returns(true) + sandbox.assert.calledWith( + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Playground Saved', + properties: { extension_version: '0.0.0-dev.0' }, + }) ); - const fakeSegmentTrack = sandbox.fake.yields(null); - sandbox.replace(testTelemetryService, '_segmentAnalytics', { - track: fakeSegmentTrack, - } as unknown as SegmentAnalytics); - - testTelemetryService.track(TelemetryEventTypes.EXTENSION_LINK_CLICKED); - - const telemetryEvent: SegmentProperties = - fakeSegmentTrack.firstCall.args[0]; - - expect(telemetryEvent.properties).to.deep.equal({ - extension_version: version, - }); - expect(telemetryEvent.event).to.equal('Link Clicked'); }); - test('track adds extension version to existing event properties', () => { - sandbox.replace( - testTelemetryService, - '_isTelemetryFeatureEnabled', - sandbox.fake.returns(true) + test('track link clicked event', () => { + testTelemetryService.trackLinkClicked('helpPanel', 'linkId'); + sandbox.assert.calledWith( + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Link Clicked', + properties: { + screen: 'helpPanel', + link_id: 'linkId', + extension_version: '0.0.0-dev.0', + }, + }) ); - - const fakeSegmentTrack = sandbox.fake.yields(null); - sandbox.replace(testTelemetryService, '_segmentAnalytics', { - track: fakeSegmentTrack, - } as unknown as SegmentAnalytics); - - testTelemetryService.track(TelemetryEventTypes.PLAYGROUND_LOADED, { - source: DocumentSource.DOCUMENT_SOURCE_PLAYGROUND, - }); - - const telemetryEvent: SegmentProperties = - fakeSegmentTrack.firstCall.args[0]; - expect(telemetryEvent.properties).to.deep.equal({ - extension_version: version, - source: DocumentSource.DOCUMENT_SOURCE_PLAYGROUND, - }); - expect(telemetryEvent.event).to.equal('Playground Loaded'); }); test('track query exported to language', async function () { this.timeout(5000); - const fakeSegmentTrack = sandbox.fake(); - sandbox.replace( - mdbTestExtension.testExtensionController._telemetryService, - 'trackQueryExported', - fakeSegmentTrack - ); - const textFromEditor = "{ '_id': 1, 'item': 'abc', 'price': 10 }"; const selection = { start: { line: 0, character: 0 }, @@ -326,9 +300,16 @@ suite('Telemetry Controller Test Suite', () => { const mode = ExportToLanguageMode.QUERY; const language = 'python'; - mdbTestExtension.testExtensionController._playgroundController._playgroundSelectedCodeActionProvider.mode = - mode; - mdbTestExtension.testExtensionController._playgroundController._exportToLanguageCodeLensProvider._exportToLanguageAddons = + sandbox.replace( + mdbTestExtension.testExtensionController._playgroundController + ._playgroundSelectedCodeActionProvider, + 'mode', + mode + ); + sandbox.replace( + mdbTestExtension.testExtensionController._playgroundController + ._exportToLanguageCodeLensProvider, + '_exportToLanguageAddons', { textFromEditor, selectedText: textFromEditor, @@ -337,62 +318,51 @@ suite('Telemetry Controller Test Suite', () => { driverSyntax: false, builders: false, language, - }; + } + ); await mdbTestExtension.testExtensionController._playgroundController._transpile(); - const telemetryArgs = fakeSegmentTrack.getCall(0).args[0]; - expect(telemetryArgs).to.deep.equal({ - language, - with_import_statements: false, - with_builders: false, - with_driver_syntax: false, - }); - }); - - test('track aggregation exported to language', async function () { - this.timeout(5000); - - const fakeSegmentTrack = sandbox.fake.yields(null); - sandbox.replace( - mdbTestExtension.testExtensionController._telemetryService, - 'trackAggregationExported', - fakeSegmentTrack + sandbox.assert.calledWith( + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Query Exported', + properties: { + language: 'python', + with_import_statements: false, + with_builders: false, + with_driver_syntax: false, + extension_version: '0.0.0-dev.0', + }, + }) ); + }); - const textFromEditor = "[{ '_id': 1, 'item': 'abc', 'price': 10 }]"; - const selection = { - start: { line: 0, character: 0 }, - end: { line: 0, character: 42 }, - } as vscode.Selection; - const mode = ExportToLanguageMode.AGGREGATION; - const language = 'java'; - - mdbTestExtension.testExtensionController._playgroundController._playgroundSelectedCodeActionProvider.mode = - mode; - mdbTestExtension.testExtensionController._playgroundController._exportToLanguageCodeLensProvider._exportToLanguageAddons = - { - textFromEditor, - // Use undefined instead of the selected text to skip countAggregationStagesInString - // that might make the test flaky. - selectedText: undefined, - selection, - importStatements: false, - driverSyntax: false, - builders: false, - language, - }; - - await mdbTestExtension.testExtensionController._playgroundController._transpile(); - - const telemetryArgs = fakeSegmentTrack.getCall(0).args[0]; - expect(telemetryArgs).to.deep.equal({ - language, - num_stages: null, + test('track aggregation exported to language', () => { + testTelemetryService.trackAggregationExported({ + language: 'java', + num_stages: 1, with_import_statements: false, with_builders: false, with_driver_syntax: false, }); + + sandbox.assert.calledWith( + fakeSegmentAnalyticsTrack, + sinon.match({ + anonymousId, + event: 'Aggregation Exported', + properties: { + language: 'java', + num_stages: 1, + with_import_statements: false, + with_builders: false, + with_driver_syntax: false, + extension_version: '0.0.0-dev.0', + }, + }) + ); }); suite('prepare playground result types', () => { diff --git a/src/types/playgroundType.ts b/src/types/playgroundType.ts index acc4597e4..2dc6824d8 100644 --- a/src/types/playgroundType.ts +++ b/src/types/playgroundType.ts @@ -69,3 +69,17 @@ export interface WorkerEvaluate { connectionString: string; connectionOptions: MongoClientOptions; } + +export interface ThisDiagnosticFix { + documentUri: vscode.Uri; + range: any; + fix: string; +} + +export interface AllDiagnosticFixes { + documentUri: vscode.Uri; + diagnostics: { + range: any; + fix: string; + }[]; +}