-
Notifications
You must be signed in to change notification settings - Fork 31k
/
Copy pathtelemetryUtils.ts
408 lines (342 loc) · 14.6 KB
/
telemetryUtils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
/*---------------------------------------------------------------------------------------------
* 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';
import { cloneAndChange, safeStringify } from 'vs/base/common/objects';
import { isObject } from 'vs/base/common/types';
import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IProductService } from 'vs/platform/product/common/productService';
import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
import { verifyMicrosoftInternalDomain } from 'vs/platform/telemetry/common/commonProperties';
import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry';
/**
* A special class used to denoting a telemetry value which should not be clean.
* This is because that value is "Trusted" not to contain identifiable information such as paths.
* NOTE: This is used as an API type as well, and should not be changed.
*/
export class TelemetryTrustedValue<T> {
// This is merely used as an identifier as the instance will be lost during serialization over the exthost
public readonly isTrustedTelemetryValue = true;
constructor(public readonly value: T) { }
}
export class NullTelemetryServiceShape implements ITelemetryService {
declare readonly _serviceBrand: undefined;
readonly telemetryLevel = TelemetryLevel.NONE;
readonly sessionId = 'someValue.sessionId';
readonly machineId = 'someValue.machineId';
readonly firstSessionDate = 'someValue.firstSessionDate';
readonly sendErrorTelemetry = false;
publicLog() { }
publicLog2() { }
publicLogError() { }
publicLogError2() { }
setExperimentProperty() { }
}
export const NullTelemetryService = new NullTelemetryServiceShape();
export class NullEndpointTelemetryService implements ICustomEndpointTelemetryService {
_serviceBrand: undefined;
async publicLog(_endpoint: ITelemetryEndpoint, _eventName: string, _data?: ITelemetryData): Promise<void> {
// noop
}
async publicLogError(_endpoint: ITelemetryEndpoint, _errorEventName: string, _data?: ITelemetryData): Promise<void> {
// noop
}
}
export const telemetryLogId = 'telemetry';
export const extensionTelemetryLogChannelId = 'extensionTelemetryLog';
export interface ITelemetryAppender {
log(eventName: string, data: any): void;
flush(): Promise<any>;
}
export const NullAppender: ITelemetryAppender = { log: () => null, flush: () => Promise.resolve(null) };
/* __GDPR__FRAGMENT__
"URIDescriptor" : {
"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"scheme": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface URIDescriptor {
mimeType?: string;
scheme?: string;
ext?: string;
path?: string;
}
export function configurationTelemetry(telemetryService: ITelemetryService, configurationService: IConfigurationService): IDisposable {
// Debounce the event by 1000 ms and merge all affected keys into one event
const debouncedConfigService = Event.debounce(configurationService.onDidChangeConfiguration, (last, cur) => {
const newAffectedKeys: ReadonlySet<string> = last ? new Set([...last.affectedKeys, ...cur.affectedKeys]) : cur.affectedKeys;
return { ...cur, affectedKeys: newAffectedKeys };
}, 1000, true);
return debouncedConfigService(event => {
if (event.source !== ConfigurationTarget.DEFAULT) {
type UpdateConfigurationClassification = {
owner: 'lramos15, sbatten';
comment: 'Event which fires when user updates settings';
configurationSource: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What configuration file was updated i.e user or workspace' };
configurationKeys: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What configuration keys were updated' };
};
type UpdateConfigurationEvent = {
configurationSource: string;
configurationKeys: string[];
};
telemetryService.publicLog2<UpdateConfigurationEvent, UpdateConfigurationClassification>('updateConfiguration', {
configurationSource: ConfigurationTargetToString(event.source),
configurationKeys: Array.from(event.affectedKeys)
});
}
});
}
/**
* Determines whether or not we support logging telemetry.
* This checks if the product is capable of collecting telemetry but not whether or not it can send it
* For checking the user setting and what telemetry you can send please check `getTelemetryLevel`.
* This returns true if `--disable-telemetry` wasn't used, the product.json allows for telemetry, and we're not testing an extension
* If false telemetry is disabled throughout the product
* @param productService
* @param environmentService
* @returns false - telemetry is completely disabled, true - telemetry is logged locally, but may not be sent
*/
export function supportsTelemetry(productService: IProductService, environmentService: IEnvironmentService): boolean {
// If it's OSS and telemetry isn't disabled via the CLI we will allow it for logging only purposes
if (!environmentService.isBuilt && !environmentService.disableTelemetry) {
return true;
}
return !(environmentService.disableTelemetry || !productService.enableTelemetry || environmentService.extensionTestsLocationURI);
}
/**
* Checks to see if we're in logging only mode to debug telemetry.
* This is if telemetry is enabled and we're in OSS, but no telemetry key is provided so it's not being sent just logged.
* @param productService
* @param environmentService
* @returns True if telemetry is actually disabled and we're only logging for debug purposes
*/
export function isLoggingOnly(productService: IProductService, environmentService: IEnvironmentService): boolean {
// Logging only mode is only for OSS
if (environmentService.isBuilt) {
return false;
}
if (environmentService.disableTelemetry) {
return false;
}
if (productService.enableTelemetry && productService.aiConfig?.ariaKey) {
return false;
}
return true;
}
/**
* Determines how telemetry is handled based on the user's configuration.
*
* @param configurationService
* @returns OFF, ERROR, ON
*/
export function getTelemetryLevel(configurationService: IConfigurationService): TelemetryLevel {
const newConfig = configurationService.getValue<TelemetryConfiguration>(TELEMETRY_SETTING_ID);
const crashReporterConfig = configurationService.getValue<boolean | undefined>(TELEMETRY_CRASH_REPORTER_SETTING_ID);
const oldConfig = configurationService.getValue<boolean | undefined>(TELEMETRY_OLD_SETTING_ID);
// If `telemetry.enableCrashReporter` is false or `telemetry.enableTelemetry' is false, disable telemetry
if (oldConfig === false || crashReporterConfig === false) {
return TelemetryLevel.NONE;
}
// Maps new telemetry setting to a telemetry level
switch (newConfig ?? TelemetryConfiguration.ON) {
case TelemetryConfiguration.ON:
return TelemetryLevel.USAGE;
case TelemetryConfiguration.ERROR:
return TelemetryLevel.ERROR;
case TelemetryConfiguration.CRASH:
return TelemetryLevel.CRASH;
case TelemetryConfiguration.OFF:
return TelemetryLevel.NONE;
}
}
export interface Properties {
[key: string]: string;
}
export interface Measurements {
[key: string]: number;
}
export function validateTelemetryData(data?: any): { properties: Properties; measurements: Measurements } {
const properties: Properties = {};
const measurements: Measurements = {};
const flat: Record<string, any> = {};
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') {
if (value.length > 8192) {
console.warn(`Telemetry property: ${prop} has been trimmed to 8192, the original length is ${value.length}`);
}
//enforce property value to be less than 8192 char, take the first 8192 char
// https://docs.microsoft.com/en-us/azure/azure-monitor/app/api-custom-events-metrics#limits
properties[prop] = value.substring(0, 8191);
} else if (typeof value !== 'undefined' && value !== null) {
properties[prop] = value;
}
}
return {
properties,
measurements
};
}
const telemetryAllowedAuthorities = new Set(['ssh-remote', 'dev-container', 'attached-container', 'wsl', 'tunnel', 'codespaces', 'amlext']);
export function cleanRemoteAuthority(remoteAuthority?: string): string {
if (!remoteAuthority) {
return 'none';
}
const remoteName = getRemoteName(remoteAuthority);
return telemetryAllowedAuthorities.has(remoteName) ? remoteName : 'other';
}
function flatten(obj: any, result: { [key: string]: any }, order: number = 0, prefix?: string): void {
if (!obj) {
return;
}
for (const 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;
}
}
}
/**
* Whether or not this is an internal user
* @param productService The product service
* @param configService The config servivce
* @returns true if internal, false otherwise
*/
export function isInternalTelemetry(productService: IProductService, configService: IConfigurationService) {
const msftInternalDomains = productService.msftInternalDomains || [];
const internalTesting = configService.getValue<boolean>('telemetry.internalTesting');
return verifyMicrosoftInternalDomain(msftInternalDomains) || internalTesting;
}
interface IPathEnvironment {
appRoot: string;
extensionsPath: string;
userDataPath: string;
userHome: URI;
tmpDir: URI;
}
export function getPiiPathsFromEnvironment(paths: IPathEnvironment): string[] {
return [paths.appRoot, paths.extensionsPath, paths.userHome.fsPath, paths.tmpDir.fsPath, paths.userDataPath];
}
//#region Telemetry Cleaning
/**
* Cleans a given stack of possible paths
* @param stack The stack to sanitize
* @param cleanupPatterns Cleanup patterns to remove from the stack
* @returns The cleaned stack
*/
function anonymizeFilePaths(stack: string, cleanupPatterns: RegExp[]): string {
// Fast check to see if it is a file path to avoid doing unnecessary heavy regex work
if (!stack || (!stack.includes('/') && !stack.includes('\\'))) {
return stack;
}
let updatedStack = stack;
const cleanUpIndexes: [number, number][] = [];
for (const regexp of cleanupPatterns) {
while (true) {
const result = regexp.exec(stack);
if (!result) {
break;
}
cleanUpIndexes.push([result.index, regexp.lastIndex]);
}
}
const nodeModulesRegex = /^[\\\/]?(node_modules|node_modules\.asar)[\\\/]/;
const fileRegex = /(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w-\._]+(\\\\|\\|\/))+[\w-\._]*/g;
let lastIndex = 0;
updatedStack = '';
while (true) {
const result = fileRegex.exec(stack);
if (!result) {
break;
}
// Check to see if the any cleanupIndexes partially overlap with this match
const overlappingRange = cleanUpIndexes.some(([start, end]) => result.index < end && start < fileRegex.lastIndex);
// anoynimize user file paths that do not need to be retained or cleaned up.
if (!nodeModulesRegex.test(result[0]) && !overlappingRange) {
updatedStack += stack.substring(lastIndex, result.index) + '<REDACTED: user-file-path>';
lastIndex = fileRegex.lastIndex;
}
}
if (lastIndex < stack.length) {
updatedStack += stack.substr(lastIndex);
}
return updatedStack;
}
/**
* Attempts to remove commonly leaked PII
* @param property The property which will be removed if it contains user data
* @returns The new value for the property
*/
function removePropertiesWithPossibleUserInfo(property: string): string {
// If for some reason it is undefined we skip it (this shouldn't be possible);
if (!property) {
return property;
}
const userDataRegexes = [
{ label: 'Google API Key', regex: /AIza[A-Za-z0-9_\\\-]{35}/ },
{ label: 'Slack Token', regex: /xox[pbar]\-[A-Za-z0-9]/ },
{ label: 'Generic Secret', regex: /(key|token|sig|secret|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]/i },
{ label: 'Email', regex: /@[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+/ } // Regex which matches @*.site
];
// Check for common user data in the telemetry events
for (const secretRegex of userDataRegexes) {
if (secretRegex.regex.test(property)) {
return `<REDACTED: ${secretRegex.label}>`;
}
}
return property;
}
/**
* Does a best possible effort to clean a data object from any possible PII.
* @param data The data object to clean
* @param paths Any additional patterns that should be removed from the data set
* @returns A new object with the PII removed
*/
export function cleanData(data: Record<string, any>, cleanUpPatterns: RegExp[]): Record<string, any> {
return cloneAndChange(data, value => {
// If it's a trusted value it means it's okay to skip cleaning so we don't clean it
if (value instanceof TelemetryTrustedValue || Object.hasOwnProperty.call(value, 'isTrustedTelemetryValue')) {
return value.value;
}
// We only know how to clean strings
if (typeof value === 'string') {
let updatedProperty = value.replaceAll('%20', ' ');
// First we anonymize any possible file paths
updatedProperty = anonymizeFilePaths(updatedProperty, cleanUpPatterns);
// Then we do a simple regex replace with the defined patterns
for (const regexp of cleanUpPatterns) {
updatedProperty = updatedProperty.replace(regexp, '');
}
// Lastly, remove commonly leaked PII
updatedProperty = removePropertiesWithPossibleUserInfo(updatedProperty);
return updatedProperty;
}
return undefined;
});
}
//#endregion