diff --git a/package-lock.json b/package-lock.json index 39a7ad3f..e6375d71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "@types/chai": "^4.3.20", "@types/debug": "^4.1.12", "@types/glob": "^7.2.0", + "@types/lodash": "^4.17.14", "@types/micromatch": "^4.0.9", "@types/mkdirp": "^2.0.0", "@types/mocha": "^8.2.3", @@ -5408,6 +5409,13 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/micromatch": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz", diff --git a/package.json b/package.json index ea468cdb..7cd01b7e 100644 --- a/package.json +++ b/package.json @@ -1361,6 +1361,7 @@ "@types/chai": "^4.3.20", "@types/debug": "^4.1.12", "@types/glob": "^7.2.0", + "@types/lodash": "^4.17.14", "@types/micromatch": "^4.0.9", "@types/mkdirp": "^2.0.0", "@types/mocha": "^8.2.3", diff --git a/src/connectionController.ts b/src/connectionController.ts index f7f39632..6fd4531d 100644 --- a/src/connectionController.ts +++ b/src/connectionController.ts @@ -331,7 +331,7 @@ export default class ConnectionController { } } - public sendTelemetry( + private sendTelemetry( newDataService: DataService, connectionType: ConnectionTypes ): void { @@ -436,7 +436,7 @@ export default class ConnectionController { }); const browserAuthCommand = vscode.workspace .getConfiguration('mdb') - .get('browserCommandForOIDCAuth'); + .get('browserCommandForOIDCAuth'); dataService = await connectionAttempt.connect({ ...connectionOptions, oidc: { diff --git a/src/explorer/explorerController.ts b/src/explorer/explorerController.ts index 61fdf394..dde15e98 100644 --- a/src/explorer/explorerController.ts +++ b/src/explorer/explorerController.ts @@ -1,20 +1,26 @@ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import type ConnectionController from '../connectionController'; import { DataServiceEventTypes } from '../connectionController'; import ExplorerTreeController from './explorerTreeController'; +import { createTrackedTreeView } from '../utils/treeViewHelper'; +import type { TelemetryService } from '../telemetry'; export default class ExplorerController { - private _connectionController: ConnectionController; private _treeController: ExplorerTreeController; private _treeView?: vscode.TreeView; - constructor(connectionController: ConnectionController) { - this._connectionController = connectionController; - this._treeController = new ExplorerTreeController(connectionController); + constructor( + private _connectionController: ConnectionController, + private _telemetryService: TelemetryService + ) { + this._treeController = new ExplorerTreeController( + this._connectionController, + this._telemetryService + ); } - createTreeView = (): void => { + private createTreeView = (): void => { // Remove the listener that called this function. this._connectionController.removeEventListener( DataServiceEventTypes.CONNECTIONS_DID_CHANGE, @@ -22,17 +28,16 @@ export default class ExplorerController { ); if (!this._treeView) { - this._treeView = vscode.window.createTreeView( + this._treeView = createTrackedTreeView( 'mongoDBConnectionExplorer', - { - treeDataProvider: this._treeController, - } + this._treeController, + this._telemetryService ); this._treeController.activateTreeViewEventHandlers(this._treeView); } }; - activateConnectionsTreeView(): void { + public activateConnectionsTreeView(): void { // Listen for a change in connections to occur before we create the tree // so that we show the `viewsWelcome` before any connections are added. this._connectionController.addEventListener( @@ -41,7 +46,7 @@ export default class ExplorerController { ); } - deactivate(): void { + public deactivate(): void { if (this._treeController) { this._treeController.removeListeners(); } @@ -52,7 +57,7 @@ export default class ExplorerController { } } - refresh(): boolean { + public refresh(): boolean { if (this._treeController) { return this._treeController.refresh(); } diff --git a/src/explorer/explorerTreeController.ts b/src/explorer/explorerTreeController.ts index 851fc3ac..abe20022 100644 --- a/src/explorer/explorerTreeController.ts +++ b/src/explorer/explorerTreeController.ts @@ -9,19 +9,25 @@ import { DOCUMENT_LIST_ITEM, CollectionTypes } from './documentListTreeItem'; import EXTENSION_COMMANDS from '../commands'; import { sortTreeItemsByLabel } from './treeItemUtils'; import type { LoadedConnection } from '../storage/connectionStorage'; +import { + TreeItemExpandedTelemetryEvent, + type TelemetryService, +} from '../telemetry'; +import type TreeItemParentInterface from './treeItemParentInterface'; const log = createLogger('explorer tree controller'); export default class ExplorerTreeController implements vscode.TreeDataProvider { - private _connectionController: ConnectionController; private _connectionTreeItems: { [key: string]: ConnectionTreeItem }; - constructor(connectionController: ConnectionController) { + constructor( + private _connectionController: ConnectionController, + private _telemetryService: TelemetryService + ) { this._onDidChangeTreeData = new vscode.EventEmitter(); this.onDidChangeTreeData = this._onDidChangeTreeData.event; - this._connectionController = connectionController; // Subscribe to changes in the connections. this._connectionController.addEventListener( @@ -46,14 +52,19 @@ export default class ExplorerTreeController activateTreeViewEventHandlers = ( treeView: vscode.TreeView ): void => { - treeView.onDidCollapseElement((event: any) => { + treeView.onDidCollapseElement((event) => { log.info('Tree item was collapsed', event.element.label); - if (event.element.onDidCollapse) { - event.element.onDidCollapse(); + const treeItem = event.element as vscode.TreeItem & + TreeItemParentInterface; + if (treeItem.onDidCollapse) { + treeItem.onDidCollapse(); } - if (event.element.doesNotRequireTreeUpdate) { + if ( + 'doesNotRequireTreeUpdate' in treeItem && + treeItem.doesNotRequireTreeUpdate + ) { // When the element is already loaded (synchronous), we do not need to // fully refresh the tree. return; @@ -62,20 +73,29 @@ export default class ExplorerTreeController this._onTreeItemUpdate(); }); - treeView.onDidExpandElement(async (event: any): Promise => { - log.info('Connection tree item was expanded', { - connectionId: event.element.connectionId, - connectionName: event.element.label, - isExpanded: event.element.isExpanded, + treeView.onDidExpandElement(async (event): Promise => { + const treeItem = event.element as vscode.TreeItem & + TreeItemParentInterface; + this._telemetryService.track( + new TreeItemExpandedTelemetryEvent(treeItem) + ); + + log.info('Explorer tree item was expanded', { + type: treeItem.contextValue, + connectionName: treeItem.label, + isExpanded: treeItem.isExpanded, }); - if (!event.element.onDidExpand) { + if (!treeItem.onDidExpand) { return; } - await event.element.onDidExpand(); + await treeItem.onDidExpand(); - if (event.element.doesNotRequireTreeUpdate) { + if ( + 'doesNotRequireTreeUpdate' in treeItem && + treeItem.doesNotRequireTreeUpdate + ) { // When the element is already loaded (synchronous), we do not // need to fully refresh the tree. return; diff --git a/src/explorer/helpExplorer.ts b/src/explorer/helpExplorer.ts index 2e6cdf2f..03d6c3c8 100644 --- a/src/explorer/helpExplorer.ts +++ b/src/explorer/helpExplorer.ts @@ -1,23 +1,26 @@ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import HelpTree from './helpTree'; import type { TelemetryService } from '../telemetry'; +import { createTrackedTreeView } from '../utils/treeViewHelper'; export default class HelpExplorer { _treeController: HelpTree; _treeView?: vscode.TreeView; - constructor() { + constructor(private _telemetryService: TelemetryService) { this._treeController = new HelpTree(); } - activateHelpTreeView(telemetryService: TelemetryService): void { + activateHelpTreeView(): void { if (!this._treeView) { - this._treeView = vscode.window.createTreeView('mongoDBHelpExplorer', { - treeDataProvider: this._treeController, - }); + this._treeView = createTrackedTreeView( + 'mongoDBHelpExplorer', + this._treeController, + this._telemetryService + ); this._treeController.activateTreeViewEventHandlers( this._treeView, - telemetryService + this._telemetryService ); } } diff --git a/src/explorer/playgroundsExplorer.ts b/src/explorer/playgroundsExplorer.ts index 99e51ff3..bc3ff5c1 100644 --- a/src/explorer/playgroundsExplorer.ts +++ b/src/explorer/playgroundsExplorer.ts @@ -1,21 +1,22 @@ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import PlaygroundsTree from './playgroundsTree'; +import type { TelemetryService } from '../telemetry'; +import { createTrackedTreeView } from '../utils/treeViewHelper'; export default class PlaygroundsExplorer { private _treeController: PlaygroundsTree; private _treeView?: vscode.TreeView; - constructor() { + constructor(private _telemetryService: TelemetryService) { this._treeController = new PlaygroundsTree(); } private createPlaygroundsTreeView = (): void => { if (!this._treeView) { - this._treeView = vscode.window.createTreeView( + this._treeView = createTrackedTreeView( 'mongoDBPlaygroundsExplorer', - { - treeDataProvider: this._treeController, - } + this._treeController, + this._telemetryService ); this._treeController.activateTreeViewEventHandlers(this._treeView); } diff --git a/src/mdbExtensionController.ts b/src/mdbExtensionController.ts index dad64407..a671ada2 100644 --- a/src/mdbExtensionController.ts +++ b/src/mdbExtensionController.ts @@ -103,10 +103,11 @@ export default class MDBExtensionController implements vscode.Disposable { }); this._languageServerController = new LanguageServerController(context); this._explorerController = new ExplorerController( - this._connectionController + this._connectionController, + this._telemetryService ); - this._helpExplorer = new HelpExplorer(); - this._playgroundsExplorer = new PlaygroundsExplorer(); + this._helpExplorer = new HelpExplorer(this._telemetryService); + this._playgroundsExplorer = new PlaygroundsExplorer(this._telemetryService); this._editDocumentCodeLensProvider = new EditDocumentCodeLensProvider( this._connectionController ); @@ -175,7 +176,7 @@ export default class MDBExtensionController implements vscode.Disposable { async activate(): Promise { this._explorerController.activateConnectionsTreeView(); - this._helpExplorer.activateHelpTreeView(this._telemetryService); + this._helpExplorer.activateHelpTreeView(); this._playgroundsExplorer.activatePlaygroundsTreeView(); this._telemetryService.activateSegmentAnalytics(); this._participantController.createParticipant(this._context); diff --git a/src/telemetry/telemetryEvents.ts b/src/telemetry/telemetryEvents.ts index 4c4554fe..a454f02f 100644 --- a/src/telemetry/telemetryEvents.ts +++ b/src/telemetry/telemetryEvents.ts @@ -76,7 +76,14 @@ abstract class TelemetryEventBase { export class PlaygroundExecutedTelemetryEvent implements TelemetryEventBase { type = 'Playground Code Executed'; properties: { - /** The type of the executed operation, e.g. 'insert', 'update', 'delete', 'query', 'aggregation', 'other' */ + /** + * The type of the executed operation. Common CRUD operations are mapped to + * 'insert', 'update', 'delete', 'query', 'aggregation'. Other operations return + * the type of the result returned by the shell API - e.g. 'collection', 'database', + * 'help', etc. for known shell types and 'string', 'number', 'undefined', etc. for + * plain JS types. In the unlikely case the shell evaluator was unable to determine + * a type, 'other' is returned. + */ type: string | null; /** Whether the entire script was run or just a part of it */ @@ -118,7 +125,7 @@ export class PlaygroundExecutedTelemetryEvent implements TelemetryEventBase { return 'query'; } - return 'other'; + return shellApiType; } } @@ -627,6 +634,41 @@ export class PresetConnectionEditedTelemetryEvent } } +/** Reported when the extension side panel is opened. VSCode doesn't expose + * a subscribable event for this, so we're inferring it by subscribing to + * treeView.onDidChangeVisibility for all the extension treeviews and throttling + * the events. + */ +export class SidePanelOpenedTelemetryEvent implements TelemetryEventBase { + type = 'Side Panel Opened'; + properties: {}; + + constructor() { + this.properties = {}; + } +} + +/** + * Reported when a tree item from the collection explorer is expanded. + */ +export class TreeItemExpandedTelemetryEvent implements TelemetryEventBase { + type = 'Section Expanded'; + properties: { + /** + * The name of the section - e.g. database, collection, etc. This is obtained from the + * `contextValue` field of the tree item. + * */ + section_name?: string; + }; + + constructor(item: vscode.TreeItem) { + // We suffix all tree item context values with 'TreeItem', which is redundant when sending to analytics. + this.properties = { + section_name: item.contextValue?.replace('TreeItem', ''), + }; + } +} + export type TelemetryEvent = | PlaygroundExecutedTelemetryEvent | LinkClickedTelemetryEvent @@ -649,4 +691,6 @@ export type TelemetryEvent = | ParticipantChatOpenedFromActionTelemetryEvent | ParticipantInputBoxSubmittedTelemetryEvent | ParticipantResponseGeneratedTelemetryEvent - | PresetConnectionEditedTelemetryEvent; + | PresetConnectionEditedTelemetryEvent + | SidePanelOpenedTelemetryEvent + | TreeItemExpandedTelemetryEvent; diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts index fd6d0b73..aa228dbf 100644 --- a/src/telemetry/telemetryService.ts +++ b/src/telemetry/telemetryService.ts @@ -4,6 +4,7 @@ import { config } from 'dotenv'; import type { DataService } from 'mongodb-data-service'; import fs from 'fs'; import { Analytics as SegmentAnalytics } from '@segment/analytics-node'; +import { throttle } from 'lodash'; import type { ConnectionTypes } from '../connectionController'; import { createLogger } from '../logging'; @@ -14,6 +15,7 @@ import type { ParticipantResponseType } from '../participant/participantTypes'; import type { TelemetryEvent } from './telemetryEvents'; import { NewConnectionTelemetryEvent, + SidePanelOpenedTelemetryEvent, ParticipantResponseFailedTelemetryEvent, } from './telemetryEvents'; @@ -189,4 +191,12 @@ export class TelemetryService { new ParticipantResponseFailedTelemetryEvent(command, errorName, errorCode) ); } + + trackTreeViewActivated: () => void = throttle( + () => { + this.track(new SidePanelOpenedTelemetryEvent()); + }, + 5000, + { leading: true, trailing: false } + ); } diff --git a/src/test/suite/explorer/helpExplorer.test.ts b/src/test/suite/explorer/helpExplorer.test.ts index c86eae3c..4a58cd6f 100644 --- a/src/test/suite/explorer/helpExplorer.test.ts +++ b/src/test/suite/explorer/helpExplorer.test.ts @@ -16,20 +16,18 @@ suite('Help Explorer Test Suite', function () { }); test('tree view should be not created until it is activated', () => { - const testHelpExplorer = new HelpExplorer(); - assert(testHelpExplorer._treeView === undefined); - testHelpExplorer.activateHelpTreeView( + const testHelpExplorer = new HelpExplorer( mdbTestExtension.testExtensionController._telemetryService ); + assert(testHelpExplorer._treeView === undefined); + testHelpExplorer.activateHelpTreeView(); assert(testHelpExplorer._treeView !== undefined); }); test('the tree should have 5 help tree items', async () => { const testHelpExplorer = mdbTestExtension.testExtensionController._helpExplorer; - testHelpExplorer.activateHelpTreeView( - mdbTestExtension.testExtensionController._telemetryService - ); + testHelpExplorer.activateHelpTreeView(); const helpTreeItems = await testHelpExplorer._treeController.getChildren(); assert(helpTreeItems.length === 6); }); @@ -37,9 +35,7 @@ suite('Help Explorer Test Suite', function () { test('the tree should have an atlas item with a url and atlas icon', async () => { const testHelpExplorer = mdbTestExtension.testExtensionController._helpExplorer; - testHelpExplorer.activateHelpTreeView( - mdbTestExtension.testExtensionController._telemetryService - ); + testHelpExplorer.activateHelpTreeView(); const helpTreeItems = await testHelpExplorer._treeController.getChildren(); const atlasHelpItem = helpTreeItems[5]; @@ -62,9 +58,7 @@ suite('Help Explorer Test Suite', function () { const testHelpExplorer = mdbTestExtension.testExtensionController._helpExplorer; - testHelpExplorer.activateHelpTreeView( - mdbTestExtension.testExtensionController._telemetryService - ); + testHelpExplorer.activateHelpTreeView(); const stubExecuteCommand = sandbox.fake(); sandbox.replace(vscode.commands, 'executeCommand', stubExecuteCommand); @@ -90,9 +84,7 @@ suite('Help Explorer Test Suite', function () { const testHelpExplorer = mdbTestExtension.testExtensionController._helpExplorer; - testHelpExplorer.activateHelpTreeView( - mdbTestExtension.testExtensionController._telemetryService - ); + testHelpExplorer.activateHelpTreeView(); const stubExecuteCommand = sandbox.fake(); sandbox.replace(linkHelper, 'openLink', stubExecuteCommand); @@ -116,9 +108,7 @@ suite('Help Explorer Test Suite', function () { 'track', stubTrackTelemetry ); - testHelpExplorer.activateHelpTreeView( - mdbTestExtension.testExtensionController._telemetryService - ); + testHelpExplorer.activateHelpTreeView(); sandbox.replace(vscode.commands, 'executeCommand', sandbox.fake()); const helpTreeItems = await testHelpExplorer._treeController.getChildren(); diff --git a/src/test/suite/telemetry/telemetryService.test.ts b/src/test/suite/telemetry/telemetryService.test.ts index 55e9fe6d..408fd245 100644 --- a/src/test/suite/telemetry/telemetryService.test.ts +++ b/src/test/suite/telemetry/telemetryService.test.ts @@ -24,6 +24,7 @@ import { PlaygroundSavedTelemetryEvent, SavedConnectionsLoadedTelemetryEvent, } from '../../../telemetry'; +import type { SegmentProperties } from '../../../telemetry/telemetryService'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require('../../../../package.json'); @@ -345,137 +346,66 @@ suite('Telemetry Controller Test Suite', () => { }); suite('prepare playground result types', () => { - test('convert AggregationCursor shellApiType to aggregation telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'AggregationCursor', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('aggregation'); - }); - - test('convert BulkWriteResult shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'BulkWriteResult', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('other'); - }); - - test('convert Collection shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'Collection', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('other'); - }); - - test('convert Cursor shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'Cursor', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('query'); - }); - - test('convert Database shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'Database', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('other'); - }); - - test('convert DeleteResult shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'DeleteResult', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('delete'); - }); - - test('convert InsertManyResult shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'InsertManyResult', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('insert'); - }); - - test('convert InsertOneResult shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'InsertOneResult', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('insert'); - }); + const unmappedTypes = [ + 'BulkWriteResult', + 'Collection', + 'Database', + 'ReplicaSet', + 'Shard', + 'ShellApi', + 'string', + 'number', + 'undefined', + ]; + for (const type of unmappedTypes) { + test(`reports original type if not remapped: ${type}`, () => { + const res = { + result: { + namespace: undefined, + type, + content: '', + language: 'plaintext', + }, + }; + + const reportedType = new PlaygroundExecutedTelemetryEvent( + res, + false, + false + ).properties.type; + expect(reportedType).to.deep.equal(type?.toLocaleLowerCase()); + }); + } - test('convert ReplicaSet shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'ReplicaSet', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('other'); - }); + const mappedTypes: Record = { + Cursor: 'query', + DeleteResult: 'delete', + InsertManyResult: 'insert', + InsertOneResult: 'insert', + UpdateResult: 'update', + AggregationCursor: 'aggregation', + }; + + for (const [shellApiType, telemetryType] of Object.entries(mappedTypes)) { + test(`convert ${shellApiType} shellApiType to ${telemetryType} telemetry type`, () => { + const res = { + result: { + namespace: undefined, + type: shellApiType, + content: '', + language: 'plaintext', + }, + }; + const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + .properties.type; + expect(type).to.deep.equal(telemetryType); + }); + } - test('convert Shard shellApiType to other telemetry type', () => { + test('convert result with missing type to "other"', () => { const res = { result: { namespace: undefined, - type: 'Shard', content: '', language: 'plaintext', }, @@ -485,46 +415,19 @@ suite('Telemetry Controller Test Suite', () => { expect(type).to.deep.equal('other'); }); - test('convert ShellApi shellApiType to other telemetry type', () => { + test('convert shell api result with undefined result field "other"', () => { const res = { - result: { - namespace: undefined, - type: 'ShellApi', - content: '', - language: 'plaintext', - }, + result: undefined, }; const type = new PlaygroundExecutedTelemetryEvent(res, false, false) .properties.type; expect(type).to.deep.equal('other'); }); - test('convert UpdateResult shellApiType to other telemetry type', () => { - const res = { - result: { - namespace: undefined, - type: 'UpdateResult', - content: '', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) + test('convert null shell api result to null', () => { + const type = new PlaygroundExecutedTelemetryEvent(null, false, false) .properties.type; - expect(type).to.deep.equal('update'); - }); - - test('return other telemetry type if evaluation returns a string', () => { - const res = { - result: { - namespace: undefined, - type: undefined, - content: '2', - language: 'plaintext', - }, - }; - const type = new PlaygroundExecutedTelemetryEvent(res, false, false) - .properties.type; - expect(type).to.deep.equal('other'); + expect(type).to.be.null; }); }); @@ -739,4 +642,40 @@ suite('Telemetry Controller Test Suite', () => { ).to.not.be.undefined; } }); + + test('trackTreeViewActivated throttles invocations', async function () { + this.timeout(6000); + + const verifyEvent = (call: sinon.SinonSpyCall): void => { + const event = call.args[0] as SegmentProperties; + expect(event.event).to.equal('Side Panel Opened'); + expect(event.properties).to.have.keys(['extension_version']); + expect(Object.keys(event.properties)).to.have.length(1); + }; + + expect(fakeSegmentAnalyticsTrack.getCalls()).has.length(0); + + // First time we call track - should be reported immediately + testTelemetryService.trackTreeViewActivated(); + expect(fakeSegmentAnalyticsTrack.getCalls()).has.length(1); + verifyEvent(fakeSegmentAnalyticsTrack.getCall(0)); + + // Calling track again without waiting - call should be throttled + testTelemetryService.trackTreeViewActivated(); + expect(fakeSegmentAnalyticsTrack.getCalls()).has.length(1); + + // Wait less than the throttle time - call should still be throttled + for (let i = 0; i < 4; i++) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + testTelemetryService.trackTreeViewActivated(); + expect(fakeSegmentAnalyticsTrack.getCalls()).has.length(1); + } + + // Wait more than throttle time - 4x1000 + 1100 = 5100 ms, this time the + // call should be reported. + await new Promise((resolve) => setTimeout(resolve, 1100)); + testTelemetryService.trackTreeViewActivated(); + expect(fakeSegmentAnalyticsTrack.getCalls()).has.length(2); + verifyEvent(fakeSegmentAnalyticsTrack.getCall(1)); + }); }); diff --git a/src/utils/treeViewHelper.ts b/src/utils/treeViewHelper.ts new file mode 100644 index 00000000..4cb7b278 --- /dev/null +++ b/src/utils/treeViewHelper.ts @@ -0,0 +1,20 @@ +import * as vscode from 'vscode'; +import type { TelemetryService } from '../telemetry'; + +export function createTrackedTreeView( + viewId: string, + provider: vscode.TreeDataProvider, + telemetryService: TelemetryService +): vscode.TreeView { + const result = vscode.window.createTreeView(viewId, { + treeDataProvider: provider, + }); + + result.onDidChangeVisibility((event) => { + if (event.visible) { + telemetryService.trackTreeViewActivated(); + } + }); + + return result; +}