Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(telemetry): telemetry improvements VSCODE-673 #914

Merged
merged 6 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,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",
Expand Down
4 changes: 2 additions & 2 deletions src/connectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ export default class ConnectionController {
}
}

public sendTelemetry(
private sendTelemetry(
newDataService: DataService,
connectionType: ConnectionTypes
): void {
Expand Down Expand Up @@ -442,7 +442,7 @@ export default class ConnectionController {
oidc: {
...cloneDeep(connectionOptions.oidc),
openBrowser: browserAuthCommand
? { command: browserAuthCommand }
? { command: browserAuthCommand as string }
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
: async ({ signal, url }): Promise<void> => {
try {
await openLink(url);
Expand Down
30 changes: 17 additions & 13 deletions src/explorer/explorerController.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
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<vscode.TreeItem>;

constructor(connectionController: ConnectionController) {
this._connectionController = connectionController;
this._treeController = new ExplorerTreeController(connectionController);
constructor(
private _connectionController: ConnectionController,
private _telemetryService: TelemetryService
) {
this._treeController = new ExplorerTreeController(
this._connectionController
);
}

createTreeView = (): void => {
private createTreeView = (): void => {
// Remove the listener that called this function.
this._connectionController.removeEventListener(
DataServiceEventTypes.CONNECTIONS_DID_CHANGE,
this.createTreeView
);

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(
Expand All @@ -41,7 +45,7 @@ export default class ExplorerController {
);
}

deactivate(): void {
public deactivate(): void {
if (this._treeController) {
this._treeController.removeListeners();
}
Expand All @@ -52,7 +56,7 @@ export default class ExplorerController {
}
}

refresh(): boolean {
public refresh(): boolean {
if (this._treeController) {
return this._treeController.refresh();
}
Expand Down
17 changes: 10 additions & 7 deletions src/explorer/helpExplorer.ts
Original file line number Diff line number Diff line change
@@ -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<vscode.TreeItem>;

constructor() {
constructor(private _telemetryService: TelemetryService) {
this._treeController = new HelpTree();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need _telemetryService in the constructor?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is meant to be a shortcut for passing it as an argument in the constructor and defining/setting it as a property of the class object which is then used in its method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to inject the telemetry service and I think it's better to do it via the ctor rather than as an argument to activateHelpTreeView like it was previously. I think that way is cleaner architecturally as I see it as a dependency we set once for the lifetime of the controller. Obviously, there's not a huge difference with the HelpExplorer because it's a fairly minimalistic class, but if you consider a more complex controller that has multiple methods which need to log some telemetry events, it becomes much easier to reason about how the dependencies are satisfied.

}

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
);
}
}
Expand Down
13 changes: 7 additions & 6 deletions src/explorer/playgroundsExplorer.ts
Original file line number Diff line number Diff line change
@@ -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<vscode.TreeItem>;

constructor() {
constructor(private _telemetryService: TelemetryService) {
this._treeController = new PlaygroundsTree();
Copy link
Contributor

@alenakhineika alenakhineika Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Doesn't seem we use _telemetryService in the constructor.

}

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);
}
Expand Down
9 changes: 5 additions & 4 deletions src/mdbExtensionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down Expand Up @@ -175,7 +176,7 @@ export default class MDBExtensionController implements vscode.Disposable {

async activate(): Promise<void> {
this._explorerController.activateConnectionsTreeView();
this._helpExplorer.activateHelpTreeView(this._telemetryService);
this._helpExplorer.activateHelpTreeView();
this._playgroundsExplorer.activatePlaygroundsTreeView();
this._telemetryService.activateSegmentAnalytics();
this._participantController.createParticipant(this._context);
Expand Down
14 changes: 14 additions & 0 deletions src/telemetry/telemetryEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,20 @@ 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 PanelOpenedTelemetryEvent implements TelemetryEventBase {
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
type = 'Side Panel Opened';
properties: {};

constructor() {
this.properties = {};
}
}

export type TelemetryEvent =
| PlaygroundExecutedTelemetryEvent
| LinkClickedTelemetryEvent
Expand Down
10 changes: 10 additions & 0 deletions src/telemetry/telemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,6 +15,7 @@ import type { ParticipantResponseType } from '../participant/participantTypes';
import type { TelemetryEvent } from './telemetryEvents';
import {
NewConnectionTelemetryEvent,
PanelOpenedTelemetryEvent,
ParticipantResponseFailedTelemetryEvent,
} from './telemetryEvents';

Expand Down Expand Up @@ -189,4 +191,12 @@ export class TelemetryService {
new ParticipantResponseFailedTelemetryEvent(command, errorName, errorCode)
);
}

trackTreeViewActivated: () => void = throttle(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why to we want to throttle here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the way the extension works, there's no integration point we can use to be notified that the side panel is opened, which is why we're piggy-backing on the tree visibility event. However, since we have 3 trees in the side panel (connections, playgrounds, help), that event would get fired 3 times every time we open the sidepanel. Rather than deal with this in post-processing, I figured throttling would be a reasonable way to reduce the noise.

() => {
this.track(new PanelOpenedTelemetryEvent());
},
5000,
{ leading: true, trailing: false }
);
}
26 changes: 8 additions & 18 deletions src/test/suite/explorer/helpExplorer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,26 @@ 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);
});

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];

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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();
Expand Down
37 changes: 37 additions & 0 deletions src/test/suite/telemetry/telemetryService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -739,4 +740,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));
});
});
20 changes: 20 additions & 0 deletions src/utils/treeViewHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as vscode from 'vscode';
import type { TelemetryService } from '../telemetry';

export function createTrackedTreeView(
viewId: string,
provider: vscode.TreeDataProvider<vscode.TreeItem>,
telemetryService: TelemetryService
): vscode.TreeView<vscode.TreeItem> {
const result = vscode.window.createTreeView(viewId, {
treeDataProvider: provider,
});

result.onDidChangeVisibility((event) => {
if (event.visible) {
telemetryService.trackTreeViewActivated();
}
});

return result;
}
Loading