Skip to content

Commit ec33367

Browse files
feat: add new connection event (#85)
1 parent ccded73 commit ec33367

14 files changed

+653
-142
lines changed

package-lock.json

Lines changed: 318 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,8 @@
616616
"classnames": "^2.2.6",
617617
"debug": "^4.1.1",
618618
"dotenv": "^8.2.0",
619+
"encoding": "^0.1.12",
620+
"mongodb-cloud-info": "^1.1.2",
619621
"mongodb-connection-model": "^16.0.0",
620622
"mongodb-data-service": "^16.6.5",
621623
"mongodb-ns": "^2.2.0",
@@ -672,6 +674,7 @@
672674
"ora": "^4.0.3",
673675
"postcss-loader": "^3.0.0",
674676
"sinon": "^9.0.0",
677+
"sinon-chai": "^3.5.0",
675678
"style-loader": "^1.1.3",
676679
"ts-loader": "^6.2.2",
677680
"ts-node": "^8.6.2",

src/connectionController.ts

Lines changed: 132 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import * as vscode from 'vscode';
33
import Connection = require('mongodb-connection-model/lib/model');
44
import DataService = require('mongodb-data-service');
55
import * as keytarType from 'keytar';
6-
7-
const { name, version } = require('../package.json');
8-
96
import { ConnectionModelType } from './connectionModelType';
107
import { DataServiceType } from './dataServiceType';
118
import { createLogger } from './logging';
@@ -14,16 +11,29 @@ import { EventEmitter } from 'events';
1411
import { StorageController, StorageVariables } from './storage';
1512
import { SavedConnection, StorageScope } from './storage/storageController';
1613
import { getNodeModule } from './utils/getNodeModule';
14+
import TelemetryController, {
15+
TelemetryEventTypes
16+
} from './telemetry/telemetryController';
17+
import { getCloudInfo } from 'mongodb-cloud-info';
1718

19+
const { name, version } = require('../package.json');
1820
const log = createLogger('connection controller');
1921
const MAX_CONNECTION_NAME_LENGTH = 512;
22+
const ATLAS_REGEX = /mongodb.net[:/]/i;
23+
const LOCALHOST_REGEX = /(localhost|127\.0\.0\.1)/i;
2024

2125
type KeyTar = typeof keytarType;
2226

2327
export enum DataServiceEventTypes {
2428
CONNECTIONS_DID_CHANGE = 'CONNECTIONS_DID_CHANGE',
2529
ACTIVE_CONNECTION_CHANGED = 'ACTIVE_CONNECTION_CHANGED',
26-
ACTIVE_CONNECTION_CHANGING = 'ACTIVE_CONNECTION_CHANGING',
30+
ACTIVE_CONNECTION_CHANGING = 'ACTIVE_CONNECTION_CHANGING'
31+
}
32+
33+
export enum ConnectionTypes {
34+
CONNECTION_FORM = 'CONNECTION_FORM',
35+
CONNECTION_STRING = 'CONNECTION_STRING',
36+
CONNECTION_ID = 'CONNECTION_ID'
2737
}
2838

2939
export type SavedConnectionInformation = {
@@ -55,13 +65,19 @@ export default class ConnectionController {
5565

5666
private _statusView: StatusView;
5767
private _storageController: StorageController;
68+
public _telemetryController?: TelemetryController;
5869

5970
// Used by other parts of the extension that respond to changes in the connections.
6071
private eventEmitter: EventEmitter = new EventEmitter();
6172

62-
constructor(_statusView: StatusView, storageController: StorageController) {
73+
constructor(
74+
_statusView: StatusView,
75+
storageController: StorageController,
76+
telemetryController?: TelemetryController
77+
) {
6378
this._statusView = _statusView;
6479
this._storageController = storageController;
80+
this._telemetryController = telemetryController;
6581

6682
try {
6783
// We load keytar in two different ways. This is because when the
@@ -216,21 +232,21 @@ export default class ConnectionController {
216232
return reject(new Error(`Unable to create connection: ${error}`));
217233
}
218234

219-
return this.saveNewConnectionAndConnect(newConnectionModel).then(
220-
resolve,
221-
reject
222-
);
235+
return this.saveNewConnectionAndConnect(
236+
newConnectionModel,
237+
ConnectionTypes.CONNECTION_STRING
238+
).then(resolve, reject);
223239
}
224240
);
225241
});
226242
};
227243

228244
public parseNewConnectionAndConnect = (
229-
newConnectionModel
245+
newConnectionModel: ConnectionModelType
230246
): Promise<boolean> => {
231247
// Here we re-parse the connection, as it can be loaded from storage or
232248
// passed by the connection model without the class methods.
233-
let connectionModel;
249+
let connectionModel: ConnectionModelType;
234250

235251
try {
236252
connectionModel = new Connection(newConnectionModel);
@@ -239,11 +255,15 @@ export default class ConnectionController {
239255
return Promise.reject(new Error(`Unable to load connection: ${error}`));
240256
}
241257

242-
return this.saveNewConnectionAndConnect(connectionModel);
258+
return this.saveNewConnectionAndConnect(
259+
connectionModel,
260+
ConnectionTypes.CONNECTION_FORM
261+
);
243262
};
244263

245264
public saveNewConnectionAndConnect = async (
246-
connectionModel: ConnectionModelType
265+
connectionModel: ConnectionModelType,
266+
connectionType: ConnectionTypes
247267
): Promise<boolean> => {
248268
const { driverUrl, instanceId } = connectionModel.getAttributes({
249269
derived: true
@@ -279,19 +299,91 @@ export default class ConnectionController {
279299
}
280300

281301
return new Promise((resolve, reject) => {
282-
this.connect(connectionId, connectionModel).then((connectSuccess) => {
283-
if (!connectSuccess) {
284-
return resolve(false);
285-
}
302+
this.connect(connectionId, connectionModel, connectionType).then(
303+
(connectSuccess) => {
304+
if (!connectSuccess) {
305+
return resolve(false);
306+
}
286307

287-
resolve(true);
288-
}, reject);
308+
resolve(true);
309+
},
310+
reject
311+
);
289312
});
290313
};
291314

315+
public async getCloudInfoFromDataService(firstServerHostname) {
316+
const cloudInfo = await getCloudInfo(firstServerHostname);
317+
let isPublicCloud = false;
318+
let publicCloudName: string | null = null;
319+
320+
if (cloudInfo.isAws) {
321+
isPublicCloud = true;
322+
publicCloudName = 'aws';
323+
} else if (cloudInfo.isGcp) {
324+
isPublicCloud = true;
325+
publicCloudName = 'gcp';
326+
} else if (cloudInfo.isAzure) {
327+
isPublicCloud = true;
328+
publicCloudName = 'azure';
329+
}
330+
331+
return { isPublicCloud, publicCloudName };
332+
}
333+
334+
private async sendTelemetry(
335+
dataService: DataServiceType,
336+
connectionType: ConnectionTypes
337+
): Promise<void> {
338+
dataService.instance({}, async (error: any, data: any) => {
339+
if (error) {
340+
log.error('TELEMETRY data service error', error);
341+
}
342+
if (data) {
343+
try {
344+
const firstServerHostname = dataService.client.model.hosts[0].host;
345+
const cloudInfo = await this.getCloudInfoFromDataService(
346+
firstServerHostname
347+
);
348+
const nonGenuineServerName = data.genuineMongoDB.isGenuine
349+
? null
350+
: data.genuineMongoDB.dbType;
351+
const telemetryData = {
352+
isAtlas: !!data.client.s.url.match(ATLAS_REGEX),
353+
isLocalhost: !!data.client.s.url.match(LOCALHOST_REGEX),
354+
isDataLake: data.dataLake.isDataLake,
355+
isEnterprise: data.build.enterprise_module,
356+
isPublicCloud: cloudInfo.isPublicCloud,
357+
publicCloudName: cloudInfo.publicCloudName,
358+
isGenuine: data.genuineMongoDB.isGenuine,
359+
nonGenuineServerName,
360+
serverVersion: data.build.version,
361+
serverArch: data.build.raw.buildEnvironment.target_arch,
362+
serverOS: data.build.raw.buildEnvironment.target_os,
363+
isUsedConnectScreen:
364+
connectionType === ConnectionTypes.CONNECTION_FORM,
365+
isUsedCommandPalette:
366+
connectionType === ConnectionTypes.CONNECTION_STRING,
367+
isUsedSavedConnection:
368+
connectionType === ConnectionTypes.CONNECTION_ID
369+
};
370+
371+
// Send metrics to Segment
372+
this._telemetryController?.track(
373+
TelemetryEventTypes.NEW_CONNECTION,
374+
telemetryData
375+
);
376+
} catch (error) {
377+
log.error('TELEMETRY cloud info error', error);
378+
}
379+
}
380+
});
381+
}
382+
292383
public connect = async (
293384
connectionId: string,
294-
connectionModel: ConnectionModelType
385+
connectionModel: ConnectionModelType,
386+
connectionType: ConnectionTypes
295387
): Promise<boolean> => {
296388
log.info(
297389
'Connect called to connect to instance:',
@@ -327,6 +419,7 @@ export default class ConnectionController {
327419
connectionModel.appname = `${name} ${version}`;
328420

329421
const newDataService: DataServiceType = new DataService(connectionModel);
422+
330423
newDataService.connect((err: Error | undefined) => {
331424
this._statusView.hideMessage();
332425

@@ -348,6 +441,10 @@ export default class ConnectionController {
348441
this.eventEmitter.emit(DataServiceEventTypes.CONNECTIONS_DID_CHANGE);
349442
this.eventEmitter.emit(DataServiceEventTypes.ACTIVE_CONNECTION_CHANGED);
350443

444+
if (this._telemetryController) {
445+
this.sendTelemetry(newDataService, connectionType);
446+
}
447+
351448
return resolve(true);
352449
});
353450
});
@@ -372,13 +469,14 @@ export default class ConnectionController {
372469
return Promise.resolve(false);
373470
}
374471
return new Promise((resolve) => {
375-
this.connect(connectionId, connectionModel).then(
376-
resolve,
377-
(err: Error) => {
378-
vscode.window.showErrorMessage(err.message);
379-
return resolve(false);
380-
}
381-
);
472+
this.connect(
473+
connectionId,
474+
connectionModel,
475+
ConnectionTypes.CONNECTION_ID
476+
).then(resolve, (err: Error) => {
477+
vscode.window.showErrorMessage(err.message);
478+
return resolve(false);
479+
});
382480
});
383481
}
384482

@@ -536,13 +634,13 @@ export default class ConnectionController {
536634
const connectionNameToRemove:
537635
| string
538636
| undefined = await vscode.window.showQuickPick(
539-
connectionIds.map(
540-
(id, index) => `${index + 1}: ${this._connections[id].name}`
541-
),
542-
{
543-
placeHolder: 'Choose a connection to remove...'
544-
}
545-
);
637+
connectionIds.map(
638+
(id, index) => `${index + 1}: ${this._connections[id].name}`
639+
),
640+
{
641+
placeHolder: 'Choose a connection to remove...'
642+
}
643+
);
546644

547645
if (!connectionNameToRemove) {
548646
return Promise.resolve(false);

src/dataServiceType.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ export type DataServiceType = {
1919
callback: (error: Error | undefined, documents: object[]) => void
2020
): void;
2121

22+
instance(opts: any, callback: any): any;
23+
2224
client: any;
2325
};

src/mdbExtensionController.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export default class MDBExtensionController implements vscode.Disposable {
5555
} else {
5656
this._connectionController = new ConnectionController(
5757
this._statusView,
58-
this._storageController
58+
this._storageController,
59+
this._telemetryController
5960
);
6061
}
6162

src/telemetry/telemetryController.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as path from 'path';
55
import { config } from 'dotenv';
66
import { StorageController } from '../storage';
77

8-
const log = createLogger('analytics');
8+
const log = createLogger('telemetry');
99
const fs = require('fs');
1010

1111
type PlaygroundTelemetryEventProperties = {
@@ -21,15 +21,34 @@ type ExtensionCommandRunTelemetryEventProperties = {
2121
command: string;
2222
};
2323

24+
type NewConnectionTelemetryEventProperties = {
25+
isAtlas: boolean;
26+
isLocalhost: boolean;
27+
isDataLake: boolean;
28+
isEnterprise: boolean;
29+
isPublicCloud: boolean;
30+
publicCloudName: string | null;
31+
isGenuine: boolean;
32+
nonGenuineServerName: string | null;
33+
serverVersion: string;
34+
serverArch: string;
35+
serverOS: string;
36+
isUsedConnectScreen: boolean;
37+
isUsedCommandPalette: boolean;
38+
isUsedSavedConnection: boolean;
39+
};
40+
2441
export type TelemetryEventProperties =
2542
| PlaygroundTelemetryEventProperties
2643
| LinkClickedTelemetryEventProperties
27-
| ExtensionCommandRunTelemetryEventProperties;
44+
| ExtensionCommandRunTelemetryEventProperties
45+
| NewConnectionTelemetryEventProperties;
2846

2947
export enum TelemetryEventTypes {
3048
PLAYGROUND_CODE_EXECUTED = 'playground code executed',
3149
EXTENSION_LINK_CLICKED = 'link clicked',
32-
EXTENSION_COMMAND_RUN = 'command run'
50+
EXTENSION_COMMAND_RUN = 'command run',
51+
NEW_CONNECTION = 'new connection'
3352
}
3453

3554
/**

0 commit comments

Comments
 (0)