diff --git a/src/vs/platform/telemetry/browser/mainTelemetryService.ts b/src/vs/platform/telemetry/browser/mainTelemetryService.ts index 61d1c153f111324c30dd7530cb143c36f4c5195a..f1f0a18b98f806dacbea63033ff0424fb3171732 100644 --- a/src/vs/platform/telemetry/browser/mainTelemetryService.ts +++ b/src/vs/platform/telemetry/browser/mainTelemetryService.ts @@ -5,28 +5,12 @@ 'use strict'; -import Platform = require('vs/base/common/platform'); -import Objects = require('vs/base/common/objects'); -import uuid = require('vs/base/common/uuid'); +import * as Platform from 'vs/base/common/platform'; +import * as uuid from 'vs/base/common/uuid'; import {AbstractTelemetryService} from 'vs/platform/telemetry/common/abstractTelemetryService'; -import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; +import {ITelemetryService, ITelemetryServiceConfig} from 'vs/platform/telemetry/common/telemetry'; import {IdleMonitor, UserStatus} from 'vs/base/browser/idleMonitor'; -export interface TelemetryServiceConfig { - enableTelemetry?: boolean; - - enableHardIdle?: boolean; - enableSoftIdle?: boolean; - sessionID?: string; - commitHash?: string; - version?: string; -} - -const DefaultTelemetryServiceConfig: TelemetryServiceConfig = { - enableTelemetry: true, - enableHardIdle: true, - enableSoftIdle: true -}; export class MainTelemetryService extends AbstractTelemetryService implements ITelemetryService { // how long of inactivity before a user is considered 'inactive' - 2 minutes @@ -34,17 +18,15 @@ export class MainTelemetryService extends AbstractTelemetryService implements IT public static IDLE_START_EVENT_NAME = 'UserIdleStart'; public static IDLE_STOP_EVENT_NAME = 'UserIdleStop'; - protected config: TelemetryServiceConfig; - private hardIdleMonitor: IdleMonitor; private softIdleMonitor: IdleMonitor; private eventCount: number; private userIdHash: string; private startTime: Date; + private optInFriendly: string[]; - constructor(config?: TelemetryServiceConfig) { - this.config = Objects.withDefaults(config, DefaultTelemetryServiceConfig); - super(); + constructor(config?: ITelemetryServiceConfig) { + super(config); this.sessionId = this.config.sessionID || (uuid.generateUuid() + Date.now()); @@ -59,6 +41,9 @@ export class MainTelemetryService extends AbstractTelemetryService implements IT this.eventCount = 0; this.startTime = new Date(); + + //holds a cache of predefined events that can be sent regardress of user optin status + this.optInFriendly = ['optInStatus']; } private onUserIdle(): void { @@ -86,11 +71,16 @@ export class MainTelemetryService extends AbstractTelemetryService implements IT return; } - // don't send telemetry when not enabled + // don't send telemetry when channel is not enabled if (!this.config.enableTelemetry) { return; } + // don't send events when the user is optout unless the event is flaged as optin friendly + if(!this.config.userOptIn && this.optInFriendly.indexOf(eventName) === -1) { + return; + } + this.eventCount++; data = this.addCommonProperties(data); diff --git a/src/vs/platform/telemetry/common/abstractTelemetryService.ts b/src/vs/platform/telemetry/common/abstractTelemetryService.ts index 2545c4bf2ce82101d32a16ef59e6a910bb9a158f..ddfbb10a7621ebe4e3ef86bd5fe9c33dbc3fd7fb 100644 --- a/src/vs/platform/telemetry/common/abstractTelemetryService.ts +++ b/src/vs/platform/telemetry/common/abstractTelemetryService.ts @@ -8,12 +8,19 @@ import Errors = require('vs/base/common/errors'); import Types = require('vs/base/common/types'); import Platform = require('vs/base/common/platform'); import {TimeKeeper, IEventsListener, ITimerEvent} from 'vs/base/common/timer'; -import {safeStringify} from 'vs/base/common/objects'; +import {safeStringify, withDefaults} from 'vs/base/common/objects'; import {Registry} from 'vs/platform/platform'; -import {ITelemetryService, ITelemetryAppender, ITelemetryInfo} from 'vs/platform/telemetry/common/telemetry'; +import {ITelemetryService, ITelemetryAppender, ITelemetryInfo, ITelemetryServiceConfig} from 'vs/platform/telemetry/common/telemetry'; import {SyncDescriptor0} from 'vs/platform/instantiation/common/descriptors'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; +const DefaultTelemetryServiceConfig: ITelemetryServiceConfig = { + enableTelemetry: true, + enableHardIdle: true, + enableSoftIdle: true, + userOptIn: true +}; + /** * Base class for main process telemetry services */ @@ -22,7 +29,6 @@ export abstract class AbstractTelemetryService implements ITelemetryService { public serviceId = ITelemetryService; - private toUnbind: any[]; private timeKeeper: TimeKeeper; private appenders: ITelemetryAppender[]; private oldOnError: any; @@ -34,8 +40,11 @@ export abstract class AbstractTelemetryService implements ITelemetryService { protected sessionId: string; protected instanceId: string; protected machineId: string; + protected toUnbind: any[]; - constructor() { + protected config: ITelemetryServiceConfig; + + constructor(config?: ITelemetryServiceConfig) { this.sessionId = 'SESSION_ID_NOT_SET'; this.timeKeeper = new TimeKeeper(); this.toUnbind = []; @@ -49,6 +58,8 @@ export abstract class AbstractTelemetryService implements ITelemetryService { this.enableGlobalErrorHandler(); this.errorFlushTimeout = -1; + + this.config = withDefaults(config, DefaultTelemetryServiceConfig); } private _safeStringify(data: any): string { diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 80eb26b5e62523d3800429c276b1253fea3119c3..8a71728c9df1245d6398a92925e43ffc3ecce610 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -63,6 +63,17 @@ export interface ITelemetryAppender extends Lifecycle.IDisposable { log(eventName: string, data?: any): void; } +export interface ITelemetryServiceConfig { + enableTelemetry?: boolean; + userOptIn?: boolean; + + enableHardIdle?: boolean; + enableSoftIdle?: boolean; + sessionID?: string; + commitHash?: string; + version?: string; +} + export function anonymize(input: string): string { if (!input) { return input; diff --git a/src/vs/platform/telemetry/electron-browser/electronTelemetryService.ts b/src/vs/platform/telemetry/electron-browser/electronTelemetryService.ts index a79a5c1cd992f4c91cbc76607bb8d44aab83cad1..b317d8eff7638450e0c5b356df075ed6802ef9ec 100644 --- a/src/vs/platform/telemetry/electron-browser/electronTelemetryService.ts +++ b/src/vs/platform/telemetry/electron-browser/electronTelemetryService.ts @@ -6,24 +6,44 @@ import getmac = require('getmac'); import crypto = require('crypto'); -import {MainTelemetryService, TelemetryServiceConfig} from 'vs/platform/telemetry/browser/mainTelemetryService'; -import {ITelemetryService, ITelemetryInfo} from 'vs/platform/telemetry/common/telemetry'; +import * as nls from 'vs/nls'; +import * as errors from 'vs/base/common/errors'; +import * as uuid from 'vs/base/common/uuid'; +import {MainTelemetryService} from 'vs/platform/telemetry/browser/mainTelemetryService'; +import {ITelemetryService, ITelemetryInfo, ITelemetryServiceConfig} from 'vs/platform/telemetry/common/telemetry'; import {IStorageService} from 'vs/platform/storage/common/storage'; -import errors = require('vs/base/common/errors'); -import uuid = require('vs/base/common/uuid'); +import {IConfigurationRegistry, Extensions} from 'vs/platform/configuration/common/configurationRegistry'; +import {IConfigurationService, IConfigurationServiceEvent, ConfigurationServiceEventTypes} from 'vs/platform/configuration/common/configuration'; +import {Registry} from 'vs/platform/platform'; + + +const TELEMETRY_SECTION_ID = 'telemetry'; class StorageKeys { public static MachineId: string = 'telemetry.machineId'; public static InstanceId: string = 'telemetry.instanceId'; } +interface ITelemetryEvent { + eventName: string; + data?: any; +} + export class ElectronTelemetryService extends MainTelemetryService implements ITelemetryService { + private static MAX_BUFFER_SIZE = 100; + private _setupIds: Promise; + private _buffer: ITelemetryEvent[]; + private _optInStatusLoaded: boolean; - constructor( @IStorageService private storageService: IStorageService, config?: TelemetryServiceConfig) { + constructor(@IConfigurationService private configurationService: IConfigurationService, @IStorageService private storageService: IStorageService, config?: ITelemetryServiceConfig) { super(config); + this._buffer = []; + this._optInStatusLoaded = false; + + this.loadOptinSettings(); this._setupIds = this.setupIds(); } @@ -33,6 +53,40 @@ export class ElectronTelemetryService extends MainTelemetryService implements IT public getTelemetryInfo(): Promise { return this._setupIds; } + /** + * override the base publicLog to prevent reporting any events before the optIn status is read from configuration + */ + public publicLog(eventName:string, data?: any): void { + if (this._optInStatusLoaded) { + super.publicLog(eventName, data); + } else { + // in case loading configuration is delayed, make sure the buffer does not grow beyond MAX_BUFFER_SIZE + if (this._buffer.length > ElectronTelemetryService.MAX_BUFFER_SIZE) { + this._buffer = []; + } + this._buffer.push({eventName: eventName, data: data}); + } + } + + private flushBuffer(): void { + let event: ITelemetryEvent = null; + while(event = this._buffer.pop()) { + super.publicLog(event.eventName, event.data); + } + } + + private loadOptinSettings(): void { + this.configurationService.loadConfiguration(TELEMETRY_SECTION_ID).done(config => { + this.config.userOptIn = config ? config.enableTelemetry : this.config.userOptIn; + this._optInStatusLoaded = true; + this.publicLog('optInStatus', {optIn: this.config.userOptIn}); + this.flushBuffer(); + }); + + this.toUnbind.push(this.configurationService.addListener(ConfigurationServiceEventTypes.UPDATED, (e: IConfigurationServiceEvent) => { + this.config.userOptIn = e.config && e.config[TELEMETRY_SECTION_ID] ? e.config[TELEMETRY_SECTION_ID].enableTelemetry : this.config.userOptIn; + })); + } private setupIds(): Promise { return Promise.all([this.setupInstanceId(), this.setupMachineId()]).then(() => { @@ -89,4 +143,19 @@ export class ElectronTelemetryService extends MainTelemetryService implements IT } } -} \ No newline at end of file +} + +const configurationRegistry = Registry.as(Extensions.Configuration); +configurationRegistry.registerConfiguration({ + 'id': TELEMETRY_SECTION_ID, + 'order': 20, + 'type': 'object', + 'title': nls.localize('telemetryConfigurationTitle', "Telemetry configuration"), + 'properties': { + 'telemetry.enableTelemetry': { + 'type': 'boolean', + 'description': nls.localize('telemetry.enableTelemetry', "Enable usage data and errors to be sent to Microsoft."), + 'default': true + } + } +}); \ No newline at end of file diff --git a/src/vs/platform/telemetry/test/node/telemetryService.test.ts b/src/vs/platform/telemetry/test/node/telemetryService.test.ts index 0fb4b8d03d3339093309e317860187f53494c00a..afee024c30bd598709ff5984cd50ee2364641661 100644 --- a/src/vs/platform/telemetry/test/node/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/node/telemetryService.test.ts @@ -16,6 +16,8 @@ import Platform = require('vs/platform/platform'); import * as sinon from 'sinon'; import {createSyncDescriptor} from 'vs/platform/instantiation/common/descriptors'; +const optInStatusEventName: string = 'optInStatus'; + class TestTelemetryAppender implements TelemetryService.ITelemetryAppender { public events: any[]; @@ -745,4 +747,52 @@ suite('TelemetryService', () => { assert.equal(service.getSessionId(), testSessionId); service.dispose(); })); + + test('Telemetry Service respects user opt-in settings', sinon.test(function() { + let service = new MainTelemetryService.MainTelemetryService({userOptIn: false, enableTelemetry: true}); + let testAppender = new TestTelemetryAppender(); + service.addTelemetryAppender(testAppender); + + service.publicLog('testEvent'); + assert.equal(testAppender.getEventsCount(), 0); + + service.dispose(); + })); + + test('Telemetry Service dont send events when enableTelemetry is off even if user is optin', sinon.test(function() { + let service = new MainTelemetryService.MainTelemetryService({userOptIn: true, enableTelemetry: false}); + let testAppender = new TestTelemetryAppender(); + service.addTelemetryAppender(testAppender); + + service.publicLog('testEvent'); + assert.equal(testAppender.getEventsCount(), 0); + + service.dispose(); + })); + + test('Telemetry Service sends events when enableTelemetry is on even user optin is on', sinon.test(function() { + let service = new MainTelemetryService.MainTelemetryService({userOptIn: true, enableTelemetry: true}); + let testAppender = new TestTelemetryAppender(); + service.addTelemetryAppender(testAppender); + + service.publicLog('testEvent'); + assert.equal(testAppender.getEventsCount(), 1); + + service.dispose(); + })); + + test('Telemetry Service allows optin friendly events', sinon.test(function() { + let service = new MainTelemetryService.MainTelemetryService({userOptIn: false, enableTelemetry: true}); + let testAppender = new TestTelemetryAppender(); + service.addTelemetryAppender(testAppender); + + service.publicLog('testEvent'); + assert.equal(testAppender.getEventsCount(), 0); + + service.publicLog(optInStatusEventName, {userOptIn: false}); + assert.equal(testAppender.getEventsCount(), 1); + assert.equal(testAppender.events[0].eventName, optInStatusEventName); + assert.equal(testAppender.events[0].data.userOptIn, false); + service.dispose(); + })); }); \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 953f5cfdd797b168ea2530c454956150473f0efa..35ede2f3aa5ae99f720fedec12fbf80a9501b52d 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -234,20 +234,20 @@ export class WorkbenchShell { let disableWorkspaceStorage = this.configuration.env.pluginTestsPath || (!this.workspace && !this.configuration.env.pluginDevelopmentPath); // without workspace or in any plugin test, we use inMemory storage unless we develop a plugin where we want to preserve state this.storageService = new Storage(this.contextService, window.localStorage, disableWorkspaceStorage ? inMemoryLocalStorageInstance : window.localStorage); + let configService = new ConfigurationService( + this.contextService, + eventService + ); + // no telemetry in a window for plugin development! let enableTelemetry = this.configuration.env.isBuilt && !this.configuration.env.pluginDevelopmentPath ? !!this.configuration.env.enableTelemetry : false; - this.telemetryService = new ElectronTelemetryService(this.storageService, { enableTelemetry: enableTelemetry, version: this.configuration.env.version, commitHash: this.configuration.env.commitHash }); + this.telemetryService = new ElectronTelemetryService(configService, this.storageService, { enableTelemetry: enableTelemetry, version: this.configuration.env.version, commitHash: this.configuration.env.commitHash }); this.keybindingService = new WorkbenchKeybindingService(this.contextService, eventService, this.telemetryService, window); this.messageService = new MessageService(this.contextService, this.windowService, this.telemetryService, this.keybindingService); this.keybindingService.setMessageService(this.messageService); - let configService = new ConfigurationService( - this.contextService, - eventService - ); - let fileService = new FileService( configService, eventService,