Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
340 changes: 318 additions & 22 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,8 @@
"classnames": "^2.2.6",
"debug": "^4.1.1",
"dotenv": "^8.2.0",
"encoding": "^0.1.12",
"mongodb-cloud-info": "^1.1.2",
"mongodb-connection-model": "^16.0.0",
"mongodb-data-service": "^16.6.5",
"mongodb-ns": "^2.2.0",
Expand Down Expand Up @@ -660,6 +662,7 @@
"ora": "^4.0.3",
"postcss-loader": "^3.0.0",
"sinon": "^9.0.0",
"sinon-chai": "^3.5.0",
"style-loader": "^1.1.3",
"ts-loader": "^6.2.2",
"ts-node": "^8.6.2",
Expand Down
166 changes: 132 additions & 34 deletions src/connectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import * as vscode from 'vscode';
import Connection = require('mongodb-connection-model/lib/model');
import DataService = require('mongodb-data-service');
import * as keytarType from 'keytar';

const { name, version } = require('../package.json');

import { ConnectionModelType } from './connectionModelType';
import { DataServiceType } from './dataServiceType';
import { createLogger } from './logging';
Expand All @@ -14,16 +11,29 @@ import { EventEmitter } from 'events';
import { StorageController, StorageVariables } from './storage';
import { SavedConnection, StorageScope } from './storage/storageController';
import { getNodeModule } from './utils/getNodeModule';
import TelemetryController, {
TelemetryEventTypes
} from './telemetry/telemetryController';
import { getCloudInfo } from 'mongodb-cloud-info';

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

type KeyTar = typeof keytarType;

export enum DataServiceEventTypes {
CONNECTIONS_DID_CHANGE = 'CONNECTIONS_DID_CHANGE',
ACTIVE_CONNECTION_CHANGED = 'ACTIVE_CONNECTION_CHANGED',
ACTIVE_CONNECTION_CHANGING = 'ACTIVE_CONNECTION_CHANGING',
ACTIVE_CONNECTION_CHANGING = 'ACTIVE_CONNECTION_CHANGING'
}

export enum ConnectionTypes {
CONNECTION_FORM = 'CONNECTION_FORM',
CONNECTION_STRING = 'CONNECTION_STRING',
CONNECTION_ID = 'CONNECTION_ID'
}

export type SavedConnectionInformation = {
Expand Down Expand Up @@ -55,13 +65,19 @@ export default class ConnectionController {

private _statusView: StatusView;
private _storageController: StorageController;
public _telemetryController?: TelemetryController;

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

constructor(_statusView: StatusView, storageController: StorageController) {
constructor(
_statusView: StatusView,
storageController: StorageController,
telemetryController?: TelemetryController
) {
this._statusView = _statusView;
this._storageController = storageController;
this._telemetryController = telemetryController;

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

return this.saveNewConnectionAndConnect(newConnectionModel).then(
resolve,
reject
);
return this.saveNewConnectionAndConnect(
newConnectionModel,
ConnectionTypes.CONNECTION_STRING
).then(resolve, reject);
}
);
});
};

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

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

return this.saveNewConnectionAndConnect(connectionModel);
return this.saveNewConnectionAndConnect(
connectionModel,
ConnectionTypes.CONNECTION_FORM
);
};

public saveNewConnectionAndConnect = async (
connectionModel: ConnectionModelType
connectionModel: ConnectionModelType,
connectionType: ConnectionTypes
): Promise<boolean> => {
const { driverUrl, instanceId } = connectionModel.getAttributes({
derived: true
Expand Down Expand Up @@ -279,19 +299,91 @@ export default class ConnectionController {
}

return new Promise((resolve, reject) => {
this.connect(connectionId, connectionModel).then((connectSuccess) => {
if (!connectSuccess) {
return resolve(false);
}
this.connect(connectionId, connectionModel, connectionType).then(
(connectSuccess) => {
if (!connectSuccess) {
return resolve(false);
}

resolve(true);
}, reject);
resolve(true);
},
reject
);
});
};

public async getCloudInfoFromDataService(firstServerHostname) {
const cloudInfo = await getCloudInfo(firstServerHostname);
let isPublicCloud = false;
let publicCloudName: string | null = null;

if (cloudInfo.isAws) {
isPublicCloud = true;
publicCloudName = 'aws';
} else if (cloudInfo.isGcp) {
isPublicCloud = true;
publicCloudName = 'gcp';
} else if (cloudInfo.isAzure) {
isPublicCloud = true;
publicCloudName = 'azure';
}

return { isPublicCloud, publicCloudName };
}

private async sendTelemetry(
dataService: DataServiceType,
connectionType: ConnectionTypes
): Promise<void> {
dataService.instance({}, async (error: any, data: any) => {
if (error) {
log.error('TELEMETRY data service error', error);
}
if (data) {
try {
const firstServerHostname = dataService.client.model.hosts[0].host;
const cloudInfo = await this.getCloudInfoFromDataService(
firstServerHostname
);
const nonGenuineServerName = data.genuineMongoDB.isGenuine
? null
: data.genuineMongoDB.dbType;
const telemetryData = {
isAtlas: !!data.client.s.url.match(ATLAS_REGEX),
isLocalhost: !!data.client.s.url.match(LOCALHOST_REGEX),
isDataLake: data.dataLake.isDataLake,
isEnterprise: data.build.enterprise_module,
isPublicCloud: cloudInfo.isPublicCloud,
publicCloudName: cloudInfo.publicCloudName,
isGenuine: data.genuineMongoDB.isGenuine,
nonGenuineServerName,
serverVersion: data.build.version,
serverArch: data.build.raw.buildEnvironment.target_arch,
serverOS: data.build.raw.buildEnvironment.target_os,
isUsedConnectScreen:
connectionType === ConnectionTypes.CONNECTION_FORM,
isUsedCommandPalette:
connectionType === ConnectionTypes.CONNECTION_STRING,
isUsedSavedConnection:
connectionType === ConnectionTypes.CONNECTION_ID
};

// Send metrics to Segment
this._telemetryController?.track(
TelemetryEventTypes.NEW_CONNECTION,
telemetryData
);
} catch (error) {
log.error('TELEMETRY cloud info error', error);
}
}
});
}

public connect = async (
connectionId: string,
connectionModel: ConnectionModelType
connectionModel: ConnectionModelType,
connectionType: ConnectionTypes
): Promise<boolean> => {
log.info(
'Connect called to connect to instance:',
Expand Down Expand Up @@ -327,6 +419,7 @@ export default class ConnectionController {
connectionModel.appname = `${name} ${version}`;

const newDataService: DataServiceType = new DataService(connectionModel);

newDataService.connect((err: Error | undefined) => {
this._statusView.hideMessage();

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

if (this._telemetryController) {
this.sendTelemetry(newDataService, connectionType);
}

return resolve(true);
});
});
Expand All @@ -372,13 +469,14 @@ export default class ConnectionController {
return Promise.resolve(false);
}
return new Promise((resolve) => {
this.connect(connectionId, connectionModel).then(
resolve,
(err: Error) => {
vscode.window.showErrorMessage(err.message);
return resolve(false);
}
);
this.connect(
connectionId,
connectionModel,
ConnectionTypes.CONNECTION_ID
).then(resolve, (err: Error) => {
vscode.window.showErrorMessage(err.message);
return resolve(false);
});
});
}

Expand Down Expand Up @@ -536,13 +634,13 @@ export default class ConnectionController {
const connectionNameToRemove:
| string
| undefined = await vscode.window.showQuickPick(
connectionIds.map(
(id, index) => `${index + 1}: ${this._connections[id].name}`
),
{
placeHolder: 'Choose a connection to remove...'
}
);
connectionIds.map(
(id, index) => `${index + 1}: ${this._connections[id].name}`
),
{
placeHolder: 'Choose a connection to remove...'
}
);

if (!connectionNameToRemove) {
return Promise.resolve(false);
Expand Down
2 changes: 2 additions & 0 deletions src/dataServiceType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ export type DataServiceType = {
callback: (error: Error | undefined, documents: object[]) => void
): void;

instance(opts: any, callback: any): any;

client: any;
};
3 changes: 2 additions & 1 deletion src/mdbExtensionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export default class MDBExtensionController implements vscode.Disposable {
} else {
this._connectionController = new ConnectionController(
this._statusView,
this._storageController
this._storageController,
this._telemetryController
);
}

Expand Down
25 changes: 22 additions & 3 deletions src/telemetry/telemetryController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from 'path';
import { config } from 'dotenv';
import { StorageController } from '../storage';

const log = createLogger('analytics');
const log = createLogger('telemetry');
const fs = require('fs');

type PlaygroundTelemetryEventProperties = {
Expand All @@ -21,15 +21,34 @@ type ExtensionCommandRunTelemetryEventProperties = {
command: string;
};

type NewConnectionTelemetryEventProperties = {
isAtlas: boolean;
isLocalhost: boolean;
isDataLake: boolean;
isEnterprise: boolean;
isPublicCloud: boolean;
publicCloudName: string | null;
isGenuine: boolean;
nonGenuineServerName: string | null;
serverVersion: string;
serverArch: string;
serverOS: string;
isUsedConnectScreen: boolean;
isUsedCommandPalette: boolean;
isUsedSavedConnection: boolean;
};

export type TelemetryEventProperties =
| PlaygroundTelemetryEventProperties
| LinkClickedTelemetryEventProperties
| ExtensionCommandRunTelemetryEventProperties;
| ExtensionCommandRunTelemetryEventProperties
| NewConnectionTelemetryEventProperties;

export enum TelemetryEventTypes {
PLAYGROUND_CODE_EXECUTED = 'playground code executed',
EXTENSION_LINK_CLICKED = 'link clicked',
EXTENSION_COMMAND_RUN = 'command run'
EXTENSION_COMMAND_RUN = 'command run',
NEW_CONNECTION = 'new connection'
}

/**
Expand Down
Loading