telemetryUtils.ts 11.0 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { IDisposable } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
7
import { IConfigurationService, ConfigurationTarget, ConfigurationTargetToString } from 'vs/platform/configuration/common/configuration';
8
import { IKeybindingService, KeybindingSource } from 'vs/platform/keybinding/common/keybinding';
9
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
10
import { ILogService } from 'vs/platform/log/common/log';
11
import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings';
12 13
import { safeStringify } from 'vs/base/common/objects';
import { isObject } from 'vs/base/common/types';
14

M
Matt Bierner 已提交
15 16
export const NullTelemetryService = new class implements ITelemetryService {
	_serviceBrand: undefined;
17
	publicLog(eventName: string, data?: ITelemetryData) {
R
Rob Lourens 已提交
18
		return Promise.resolve(undefined);
M
Matt Bierner 已提交
19
	}
20 21 22
	publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
		return this.publicLog(eventName, data as ITelemetryData);
	}
23
	setEnabled() { }
M
Matt Bierner 已提交
24
	isOptedIn: true;
25 26
	getTelemetryInfo(): Promise<ITelemetryInfo> {
		return Promise.resolve({
27 28 29 30 31 32 33 34 35
			instanceId: 'someValue.instanceId',
			sessionId: 'someValue.sessionId',
			machineId: 'someValue.machineId'
		});
	}
};

export interface ITelemetryAppender {
	log(eventName: string, data: any): void;
36
	flush(): Promise<any>;
37 38 39
}

export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetryAppender {
40 41
	return {
		log: (e, d) => appenders.forEach(a => a.log(e, d)),
42
		flush: () => Promise.all(appenders.map(a => a.flush()))
43
	};
44 45
}

46
export const NullAppender: ITelemetryAppender = { log: () => null, flush: () => Promise.resolve(null) };
47

48 49 50

export class LogAppender implements ITelemetryAppender {

51
	private commonPropertiesRegex = /^sessionID$|^version$|^timestamp$|^commitHash$|^common\./;
52 53
	constructor(@ILogService private readonly _logService: ILogService) { }

54
	flush(): Promise<any> {
55
		return Promise.resolve(undefined);
56 57 58
	}

	log(eventName: string, data: any): void {
M
Matt Bierner 已提交
59
		const strippedData: { [key: string]: any } = {};
60 61 62 63 64 65 66 67 68
		Object.keys(data).forEach(key => {
			if (!this.commonPropertiesRegex.test(key)) {
				strippedData[key] = data[key];
			}
		});
		this._logService.trace(`telemetry/${eventName}`, strippedData);
	}
}

K
kieferrm 已提交
69
/* __GDPR__FRAGMENT__
K
kieferrm 已提交
70 71
	"URIDescriptor" : {
		"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
C
Christof Marti 已提交
72
		"scheme": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
73
		"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
K
kieferrm 已提交
74
		"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
K
kieferrm 已提交
75 76
	}
*/
77 78
export interface URIDescriptor {
	mimeType?: string;
R
rebornix 已提交
79
	scheme?: string;
80
	ext?: string;
81
	path?: string;
82 83
}

C
Christof Marti 已提交
84 85 86
/**
 * Only add settings that cannot contain any personal/private information of users (PII).
 */
87 88
const configurationValueWhitelist = [
	'editor.fontFamily',
89 90 91 92 93 94 95
	'editor.fontWeight',
	'editor.fontSize',
	'editor.lineHeight',
	'editor.letterSpacing',
	'editor.lineNumbers',
	'editor.rulers',
	'editor.wordSeparators',
96
	'editor.tabSize',
D
David Lechner 已提交
97
	'editor.indentSize',
98 99 100 101 102
	'editor.insertSpaces',
	'editor.detectIndentation',
	'editor.roundedSelection',
	'editor.scrollBeyondLastLine',
	'editor.minimap.enabled',
103
	'editor.minimap.side',
104 105 106 107 108 109 110 111 112 113 114
	'editor.minimap.renderCharacters',
	'editor.minimap.maxColumn',
	'editor.find.seedSearchStringFromSelection',
	'editor.find.autoFindInSelection',
	'editor.wordWrap',
	'editor.wordWrapColumn',
	'editor.wrappingIndent',
	'editor.mouseWheelScrollSensitivity',
	'editor.multiCursorModifier',
	'editor.quickSuggestions',
	'editor.quickSuggestionsDelay',
115 116
	'editor.parameterHints.enabled',
	'editor.parameterHints.cycle',
117
	'editor.autoClosingBrackets',
J
Jackson Kearl 已提交
118
	'editor.autoClosingQuotes',
119
	'editor.autoSurround',
R
rebornix 已提交
120
	'editor.autoIndent',
121 122 123 124 125 126 127 128
	'editor.formatOnType',
	'editor.formatOnPaste',
	'editor.suggestOnTriggerCharacters',
	'editor.acceptSuggestionOnEnter',
	'editor.acceptSuggestionOnCommitCharacter',
	'editor.snippetSuggestions',
	'editor.emptySelectionClipboard',
	'editor.wordBasedSuggestions',
J
Johannes Rieken 已提交
129
	'editor.suggestSelection',
130 131
	'editor.suggestFontSize',
	'editor.suggestLineHeight',
132
	'editor.tabCompletion',
133 134 135 136 137
	'editor.selectionHighlight',
	'editor.occurrencesHighlight',
	'editor.overviewRulerLanes',
	'editor.overviewRulerBorder',
	'editor.cursorBlinking',
138
	'editor.cursorSmoothCaretAnimation',
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
	'editor.cursorStyle',
	'editor.mouseWheelZoom',
	'editor.fontLigatures',
	'editor.hideCursorInOverviewRuler',
	'editor.renderWhitespace',
	'editor.renderControlCharacters',
	'editor.renderIndentGuides',
	'editor.renderLineHighlight',
	'editor.codeLens',
	'editor.folding',
	'editor.showFoldingControls',
	'editor.matchBrackets',
	'editor.glyphMargin',
	'editor.useTabStops',
	'editor.trimAutoWhitespace',
	'editor.stablePeek',
	'editor.dragAndDrop',
	'editor.formatOnSave',
R
rebornix 已提交
157
	'editor.colorDecorators',
158

J
Johannes Rieken 已提交
159 160 161
	'breadcrumbs.enabled',
	'breadcrumbs.filePath',
	'breadcrumbs.symbolPath',
162
	'breadcrumbs.symbolSortOrder',
J
Johannes Rieken 已提交
163 164 165 166 167
	'breadcrumbs.useQuickPick',
	'explorer.openEditors.visible',
	'extensions.autoUpdate',
	'files.associations',
	'files.autoGuessEncoding',
168
	'files.autoSave',
J
Johannes Rieken 已提交
169 170 171
	'files.autoSaveDelay',
	'files.encoding',
	'files.eol',
172 173 174 175 176
	'files.hotExit',
	'files.trimTrailingWhitespace',
	'git.confirmSync',
	'git.enabled',
	'http.proxyStrictSSL',
J
Johannes Rieken 已提交
177
	'javascript.validate.enable',
178
	'php.builtInCompletions.enable',
179
	'php.validate.enable',
180
	'php.validate.run',
J
Johannes Rieken 已提交
181 182 183
	'terminal.integrated.fontFamily',
	'window.openFilesInNewWindow',
	'window.restoreWindows',
184
	'window.nativeFullScreen',
J
Johannes Rieken 已提交
185 186 187 188
	'window.zoomLevel',
	'workbench.editor.enablePreview',
	'workbench.editor.enablePreviewFromQuickOpen',
	'workbench.editor.showTabs',
A
Alexander 已提交
189
	'workbench.editor.highlightModifiedTabs',
J
Johannes Rieken 已提交
190
	'workbench.sideBar.location',
191
	'workbench.startupEditor',
J
Johannes Rieken 已提交
192 193
	'workbench.statusBar.visible',
	'workbench.welcome.enabled',
194 195 196
];

export function configurationTelemetry(telemetryService: ITelemetryService, configurationService: IConfigurationService): IDisposable {
197
	return configurationService.onDidChangeConfiguration(event => {
198
		if (event.source !== ConfigurationTarget.DEFAULT) {
199
			type UpdateConfigurationClassification = {
200 201 202
				configurationSource: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
				configurationKeys: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
			};
203
			type UpdateConfigurationEvent = {
204 205 206
				configurationSource: string;
				configurationKeys: string[];
			};
207
			telemetryService.publicLog2<UpdateConfigurationEvent, UpdateConfigurationClassification>('updateConfiguration', {
A
Alex Dima 已提交
208
				configurationSource: ConfigurationTargetToString(event.source),
209 210
				configurationKeys: flattenKeys(event.sourceConfig)
			});
211
			type UpdateConfigurationValuesClassification = {
212 213 214
				configurationSource: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
				configurationValues: { classification: 'CustomerContent', purpose: 'FeatureInsight' };
			};
215
			type UpdateConfigurationValuesEvent = {
216 217 218
				configurationSource: string;
				configurationValues: { [key: string]: any }[];
			};
219
			telemetryService.publicLog2<UpdateConfigurationValuesEvent, UpdateConfigurationValuesClassification>('updateConfigurationValues', {
A
Alex Dima 已提交
220
				configurationSource: ConfigurationTargetToString(event.source),
221 222 223 224 225 226 227 228 229
				configurationValues: flattenValues(event.sourceConfig, configurationValueWhitelist)
			});
		}
	});
}

export function keybindingsTelemetry(telemetryService: ITelemetryService, keybindingService: IKeybindingService): IDisposable {
	return keybindingService.onDidUpdateKeybindings(event => {
		if (event.source === KeybindingSource.User && event.keybindings) {
230
			type UpdateKeybindingsClassification = {
231 232
				bindings: { classification: 'CustomerContent', purpose: 'FeatureInsight' };
			};
233
			type UpdateKeybindingsEvents = {
234 235
				bindings: { key: string, command: string, when: string | undefined, args: boolean | undefined }[];
			};
236
			telemetryService.publicLog2<UpdateKeybindingsEvents, UpdateKeybindingsClassification>('updateKeybindings', {
237 238 239 240 241 242 243 244 245 246 247
				bindings: event.keybindings.map(binding => ({
					key: binding.key,
					command: binding.command,
					when: binding.when,
					args: binding.args ? true : undefined
				}))
			});
		}
	});
}

248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318

export interface Properties {
	[key: string]: string;
}

export interface Measurements {
	[key: string]: number;
}

export function validateTelemetryData(data?: any): { properties: Properties, measurements: Measurements } {

	const properties: Properties = Object.create(null);
	const measurements: Measurements = Object.create(null);

	const flat = Object.create(null);
	flatten(data, flat);

	for (let prop in flat) {
		// enforce property names less than 150 char, take the last 150 char
		prop = prop.length > 150 ? prop.substr(prop.length - 149) : prop;
		const value = flat[prop];

		if (typeof value === 'number') {
			measurements[prop] = value;

		} else if (typeof value === 'boolean') {
			measurements[prop] = value ? 1 : 0;

		} else if (typeof value === 'string') {
			//enforce property value to be less than 1024 char, take the first 1024 char
			properties[prop] = value.substring(0, 1023);

		} else if (typeof value !== 'undefined' && value !== null) {
			properties[prop] = value;
		}
	}

	return {
		properties,
		measurements
	};
}

function flatten(obj: any, result: { [key: string]: any }, order: number = 0, prefix?: string): void {
	if (!obj) {
		return;
	}

	for (let item of Object.getOwnPropertyNames(obj)) {
		const value = obj[item];
		const index = prefix ? prefix + item : item;

		if (Array.isArray(value)) {
			result[index] = safeStringify(value);

		} else if (value instanceof Date) {
			// TODO unsure why this is here and not in _getData
			result[index] = value.toISOString();

		} else if (isObject(value)) {
			if (order < 2) {
				flatten(value, result, order + 1, index + '.');
			} else {
				result[index] = safeStringify(value);
			}
		} else {
			result[index] = value;
		}
	}
}

M
Matt Bierner 已提交
319
function flattenKeys(value: Object | undefined): string[] {
320 321 322 323 324 325 326 327
	if (!value) {
		return [];
	}
	const result: string[] = [];
	flatKeys(result, '', value);
	return result;
}

M
Matt Bierner 已提交
328
function flatKeys(result: string[], prefix: string, value: { [key: string]: any } | undefined): void {
329 330 331 332 333 334 335 336
	if (value && typeof value === 'object' && !Array.isArray(value)) {
		Object.keys(value)
			.forEach(key => flatKeys(result, prefix ? `${prefix}.${key}` : key, value[key]));
	} else {
		result.push(prefix);
	}
}

M
Matt Bierner 已提交
337
function flattenValues(value: { [key: string]: any } | undefined, keys: string[]): { [key: string]: any }[] {
338 339 340 341 342 343 344 345 346 347 348
	if (!value) {
		return [];
	}

	return keys.reduce((array, key) => {
		const v = key.split('.')
			.reduce((tmp, k) => tmp && typeof tmp === 'object' ? tmp[k] : undefined, value);
		if (typeof v !== 'undefined') {
			array.push({ [key]: v });
		}
		return array;
A
Alex Dima 已提交
349
	}, <{ [key: string]: any }[]>[]);
350
}