src/vs/platform/telemetry/common/telemetryUtils.ts

478 LOC · 306 covered · 172 uncovered · 50 ranges · 5171 concepts · 16 introducers · 2777 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- telemetryUtils.ts ×15
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { cloneAndChange, safeStringify } from '../../../base/common/objects.js';
7 > import { isObject } from '../../../base/common/types.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { localize } from '../../../nls.js';
10 > import { IConfigurationService } from '../../configuration/common/configuration.js';
11 > import { IEnvironmentService } from '../../environment/common/environment.js';
12 > import { LoggerGroup } from '../../log/common/log.js';
13 > import { IProductService } from '../../product/common/productService.js';
14 > import { getRemoteName } from '../../remote/common/remoteHosts.js';
15 > import { verifyMicrosoftInternalDomain } from './commonProperties.js';
16 > import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from './telemetry.js';
17 >
18 > /**
19 > * A special class used to denoting a telemetry value which should not be clean.
20 > * This is because that value is "Trusted" not to contain identifiable information such as paths.
21 > * NOTE: This is used as an API type as well, and should not be changed.
22 > */
23 > export class TelemetryTrustedValue<T> {
24 > // This is merely used as an identifier as the instance will be lost during serialization over the exthost
25 > public readonly isTrustedTelemetryValue = true;
26 > constructor(public readonly value: T) { }
27 > }
28 >
29 > export class NullTelemetryServiceShape implements ITelemetryService {
30 > declare readonly _serviceBrand: undefined;
31 > readonly telemetryLevel = TelemetryLevel.NONE;
32 > readonly sessionId = 'someValue.sessionId';
33 > readonly machineId = 'someValue.machineId';
34 > readonly sqmId = 'someValue.sqmId';
35 > readonly devDeviceId = 'someValue.devDeviceId';
36 > readonly firstSessionDate = 'someValue.firstSessionDate';
37 > readonly sendErrorTelemetry = false;
38 > publicLog() { }
39 > publicLog2() { }
40 > publicLogError() { }
41 > publicLogError2() { }
42 > setExperimentProperty() { }
43 > setCommonProperty() { }
44 > }
45 >
46 > export const NullTelemetryService = new NullTelemetryServiceShape();
47 >
48 > export class NullEndpointTelemetryService implements ICustomEndpointTelemetryService {
49 > _serviceBrand: undefined;
50 >
51 > async publicLog(_endpoint: ITelemetryEndpoint, _eventName: string, _data?: ITelemetryData): Promise<void> {
52 // noop
53 }
55 > async publicLogError(_endpoint: ITelemetryEndpoint, _errorEventName: string, _data?: ITelemetryData): Promise<void> {
56 // noop
57 }
59 >
60 > export const telemetryLogId = 'telemetry';
61 > export const TelemetryLogGroup: LoggerGroup = { id: telemetryLogId, name: localize('telemetryLogName', "Telemetry") };
62 >
63 > export interface ITelemetryAppender {
64 > log(eventName: string, data: ITelemetryData): void;
65 > flush(): Promise<void>;
66 > }
67 >
68 > export const NullAppender: ITelemetryAppender = { log: () => null, flush: () => Promise.resolve(undefined) };
69 >
70 >
71 > /* __GDPR__FRAGMENT__
72 > "URIDescriptor" : {
73 > "mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
74 > "scheme": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
75 > "ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
76 > "path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
77 > }
78 > */
79 > export interface URIDescriptor {
80 > mimeType?: string;
81 > scheme?: string;
82 > ext?: string;
83 > path?: string;
84 > }
85 >
86 > /**
87 > * Determines whether or not we support logging telemetry.
88 > * This checks if the product is capable of collecting telemetry but not whether or not it can send it
89 > * For checking the user setting and what telemetry you can send please check `getTelemetryLevel`.
90 > * This returns true if `--disable-telemetry` wasn't used, the product.json allows for telemetry, and we're not testing an extension
91 > * If false telemetry is disabled throughout the product
92 > * @param productService
93 > * @param environmentService
94 > * @returns false - telemetry is completely disabled, true - telemetry is logged locally, but may not be sent
95 > */
96 > export function supportsTelemetry(productService: IProductService, environmentService: IEnvironmentService): boolean {
97 > // If it's OSS and telemetry isn't disabled via the CLI we will allow it for logging only purposes telemetryUtils.ts ×7
98 > if (!environmentService.isBuilt && !environmentService.disableTelemetry) {
99 > return true; telemetryUtils.ts ×1
100 > }
101 > return !(environmentService.disableTelemetry || !productService.enableTelemetry); telemetryUtils.ts ×1
104 > /**
105 > * Checks to see if we're in logging only mode to debug telemetry.
106 > * 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.
107 > * @param productService
108 > * @param environmentService
109 > * @returns True if telemetry is actually disabled and we're only logging for debug purposes
110 > */
111 > export function isLoggingOnly(productService: IProductService, environmentService: IEnvironmentService): boolean {
112 > // If we're testing an extension, log telemetry for debug purposes telemetryUtils.ts ×3
113 > if (environmentService.extensionTestsLocationURI) {
114 return true;
115 }
116 > // Logging only mode is only for OSS telemetryUtils.ts ×3
117 > if (environmentService.isBuilt) {
118 > return false; 1dsAppender.ts ×11
119 > }
121 > if (environmentService.disableTelemetry) {
122 return false;
123 }
125 > if (productService.enableTelemetry && productService.aiConfig?.ariaKey) { telemetryUtils.ts ×3
126 return false;
127 }
129 > return true;
130 > }
132 > /**
133 > * Determines how telemetry is handled based on the user's configuration.
134 > *
135 > * @param configurationService
136 > * @returns OFF, ERROR, ON
137 > */
138 > export function getTelemetryLevel(configurationService: IConfigurationService): TelemetryLevel {
139 > const newConfig = configurationService.getValue<TelemetryConfiguration>(TELEMETRY_SETTING_ID); telemetryUtils.ts ×7
140 > const crashReporterConfig = configurationService.getValue<boolean | undefined>(TELEMETRY_CRASH_REPORTER_SETTING_ID);
141 > const oldConfig = configurationService.getValue<boolean | undefined>(TELEMETRY_OLD_SETTING_ID);
142 >
143 > // If `telemetry.enableCrashReporter` is false or `telemetry.enableTelemetry' is false, disable telemetry
144 > if (oldConfig === false || crashReporterConfig === false) {
145 return TelemetryLevel.NONE;
146 }
148 > // Maps new telemetry setting to a telemetry level
149 > switch (newConfig ?? TelemetryConfiguration.ON) {
150 > case TelemetryConfiguration.ON:
151 > return TelemetryLevel.USAGE;
152 > case TelemetryConfiguration.ERROR:
153 return TelemetryLevel.ERROR;
154 > case TelemetryConfiguration.CRASH: telemetryUtils.ts ×7
155 return TelemetryLevel.CRASH;
156 > case TelemetryConfiguration.OFF: telemetryUtils.ts ×7
157 return TelemetryLevel.NONE;
159 > }
161 > export interface Properties {
162 > [key: string]: string;
163 > }
164 >
165 > export interface Measurements {
166 > [key: string]: number;
167 > }
168 >
169 > export function validateTelemetryData(data?: unknown): { properties: Properties; measurements: Measurements } {
171 > const properties: Properties = {};
172 > const measurements: Measurements = {};
173 >
174 > const flat: Record<string, unknown> = {};
175 > flatten(data, flat);
176 >
177 > for (let prop in flat) {
178 > // enforce property names less than 150 char, take the last 150 char
179 > prop = prop.length > 150 ? prop.substr(prop.length - 149) : prop;
180 > const value = flat[prop];
181 >
182 > if (typeof value === 'number') {
183 > measurements[prop] = value;
184 >
185 > } else if (typeof value === 'boolean') {
186 > measurements[prop] = value ? 1 : 0;
187 >
188 > } else if (typeof value === 'string') {
189 > if (value.length > 8192) {
190 console.warn(`Telemetry property: ${prop} has been trimmed to 8192, the original length is ${value.length}`);
191 }
192 > //enforce property value to be less than 8192 char, take the first 8192 char telemetryUtils.ts ×8
193 > // https://docs.microsoft.com/en-us/azure/azure-monitor/app/api-custom-events-metrics#limits
194 > properties[prop] = value.substring(0, 8191);
195 >
196 > } else if (typeof value !== 'undefined' && value !== null) {
197 properties[prop] = String(value);
198 }
200 >
201 > return {
202 > properties,
203 > measurements
204 > };
205 > }
207 > interface IRemoteAuthoringConfig {
208 > remoteExtensionTips?: { readonly [remoteName: string]: unknown };
209 > virtualWorkspaceExtensionTips?: { readonly [remoteName: string]: unknown };
210 > }
211 >
212 > export function cleanRemoteAuthority(remoteAuthority: string | undefined, config: IRemoteAuthoringConfig): string {
213 > if (!remoteAuthority) { telemetryUtils.ts ×3
214 > return 'none'; telemetryUtils.ts ×1
215 > }
217 > const remoteName = getRemoteName(remoteAuthority);
218 >
219 > const set1 = config?.remoteExtensionTips;
220 > if (set1 && Object.prototype.hasOwnProperty.call(set1, remoteName)) { telemetryUtils.ts ×3
221 > return remoteName; telemetryUtils.ts ×1
222 > }
224 > const set2 = config?.virtualWorkspaceExtensionTips;
225 > if (set2 && Object.prototype.hasOwnProperty.call(set2, remoteName)) { telemetryUtils.ts ×3
226 > return remoteName; telemetryUtils.ts ×1
227 > }
229 > return 'other';
230 > }
232 > function flatten(obj: unknown, result: Record<string, unknown>, order: number = 0, prefix?: string): void { telemetryUtils.ts ×8
233 > if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
234 return;
235 }
237 > const source = obj as Record<string, unknown>;
238 > for (const item of Object.getOwnPropertyNames(source)) {
239 > const value = source[item];
240 > const index = prefix ? prefix + item : item;
241 >
242 > if (Array.isArray(value)) {
243 result[index] = safeStringify(value);
244
245 > } else if (value instanceof Date) { telemetryUtils.ts ×8
246 // TODO unsure why this is here and not in _getData
247 result[index] = value.toISOString();
248
249 > } else if (isObject(value)) { telemetryUtils.ts ×8
250 if (order < 2) {
251 flatten(value, result, order + 1, index + '.');
252 } else {
253 result[index] = safeStringify(value);
254 }
255 > } else { telemetryUtils.ts ×8
256 > result[index] = value;
257 > }
258 > }
259 > }
261 > /**
262 > * Whether or not this is an internal user
263 > * @param productService The product service
264 > * @param configService The config servivce
265 > * @returns true if internal, false otherwise
266 > */
267 > export function isInternalTelemetry(productService: IProductService, configService: IConfigurationService) {
268 > const msftInternalDomains = productService.msftInternalDomains || []; telemetryService.ts ×9
269 > const internalTesting = configService.getValue<boolean>('telemetry.internalTesting');
270 > return verifyMicrosoftInternalDomain(msftInternalDomains) || internalTesting;
271 > }
273 > interface IPathEnvironment {
274 > appRoot: string;
275 > extensionsPath: string;
276 > userDataPath: string;
277 > userHome: URI;
278 > tmpDir: URI;
279 > }
280 >
281 > export function getPiiPathsFromEnvironment(paths: IPathEnvironment): string[] {
282 > return [paths.appRoot, paths.extensionsPath, paths.userHome.fsPath, paths.tmpDir.fsPath, paths.userDataPath]; telemetryService.ts ×9
283 > }
285 > //#region Telemetry Cleaning
286 >
287 > /**
288 > * Cleans a given stack of possible paths
289 > * @param stack The stack to sanitize
290 > * @param cleanupPatterns Cleanup patterns to remove from the stack
291 > * @returns The cleaned stack
292 > */
293 function anonymizeFilePaths(stack: string, cleanupPatterns: RegExp[]): string {
294
295 // Fast check to see if it is a file path to avoid doing unnecessary heavy regex work
296 if (!stack || (!stack.includes('/') && !stack.includes('\\'))) {
297 return stack;
298 }
299
300 let updatedStack = stack;
301
302 const cleanUpIndexes: [number, number][] = [];
303 for (const regexp of cleanupPatterns) {
304 while (true) {
305 const result = regexp.exec(stack);
306 if (!result) {
307 break;
308 }
309 cleanUpIndexes.push([result.index, regexp.lastIndex]);
310 }
311 }
312
313 // Match node_modules or node_modules.asar at any position in the path, capturing the node_modules/... suffix
314 const nodeModulesRegex = /(?:^|[\\\/])((node_modules|node_modules\.asar)[\\\/].*)$/;
315 // Match VS Code extension paths:
316 // 1. User extensions: .vscode/extensions/, .vscode-insiders/extensions/, .vscode-server/extensions/, .vscode-server-insiders/extensions/, etc.
317 // 2. Built-in extensions: resources/app/extensions/
318 // Capture everything from the vscode folder or resources/app/extensions onwards
319 const vscodeExtensionsPathRegex = /^(.*?)((?:\.vscode(?:-[a-z]+)*|resources[\\\/]app)[\\\/]extensions[\\\/].*)$/i;
320 const fileRegex = /(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w\-\._@]+(\\\\|\\|\/))+[\w\-\._@]*/g;
321 let lastIndex = 0;
322 updatedStack = '';
323
324 while (true) {
325 const result = fileRegex.exec(stack);
326 if (!result) {
327 break;
328 }
329
330 // Check to see if the any cleanupIndexes partially overlap with this match
331 const overlappingRange = cleanUpIndexes.some(([start, end]) => result.index < end && start < fileRegex.lastIndex);
332
333 // anoynimize user file paths that do not need to be retained or cleaned up.
334 if (!overlappingRange) {
335 // Check if this is a VS Code extension path - if so, preserve the .vscode*/extensions/... portion
336 const vscodeExtMatch = vscodeExtensionsPathRegex.exec(result[0]);
337 if (vscodeExtMatch) {
338 // Keep ".vscode[-variant]/extensions/extension-name/..." but redact the parent folder
339 updatedStack += stack.substring(lastIndex, result.index) + '<REDACTED: user-file-path>/' + vscodeExtMatch[2];
340 } else {
341 // Check if node_modules appears in the path — preserve node_modules/... suffix
342 const nodeModulesMatch = nodeModulesRegex.exec(result[0]);
343 if (nodeModulesMatch) {
344 updatedStack += stack.substring(lastIndex, result.index) + '<REDACTED: user-file-path>/' + nodeModulesMatch[1];
345 } else {
346 updatedStack += stack.substring(lastIndex, result.index) + '<REDACTED: user-file-path>';
347 }
348 }
349 lastIndex = fileRegex.lastIndex;
350 }
351 }
352 if (lastIndex < stack.length) {
353 updatedStack += stack.substr(lastIndex);
354 }
355
356 return updatedStack;
357 }
359 > const userDataRegexes = [
360 > { label: 'URL', regex: /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]*/ },
361 > { label: 'Google API Key', regex: /AIza[A-Za-z0-9_\\\-]{35}/ },
362 > { label: 'JWT', regex: /eyJ[0eXAiOiJKV1Qi|hbGci|a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+/ },
363 > { label: 'Slack Token', regex: /xox[pbar]\-[A-Za-z0-9]/ },
364 > { label: 'GitHub Token', regex: /(gh[psuro]_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})/ },
365 > { label: 'Generic Secret', regex: /(key|token|sig|secret|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]/i },
366 > { label: 'CLI Credentials', regex: /((login|psexec|(certutil|psexec)\.exe).{1,50}(\s-u(ser(name)?)?\s+.{3,100})?\s-(admin|user|vm|root)?p(ass(word)?)?\s+["']?[^$\-\/\s]|(^|[\s\r\n\\])net(\.exe)?.{1,5}(user\s+|share\s+\/user:| user -? secrets ? set) \s + [^ $\s \/])/ },
367 > { label: 'Microsoft Entra ID', regex: /eyJ(?:0eXAiOiJKV1Qi|hbGci|[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.)/ },
368 > { label: 'Email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/ }
369 > ];
370 >
371 > /**
372 > * Redacts a value if it contains commonly leaked PII.
373 > * @param value The value returned (as-is) when no PII is detected
374 > * @param probe The string actually matched against the PII heuristics. Defaults
375 > * to `value`; callers may pass a value that includes a trailing delimiter (e.g. a
376 > * newline) so that heuristics relying on a non-alphanumeric boundary match the
377 > * same way they would against the original whole string.
378 > * @returns A `<REDACTED: ...>` marker if the probe matched, otherwise `value`
379 > */
380 function redactIfPossibleUserInfo(value: string, probe: string = value): string {
381 for (const secretRegex of userDataRegexes) {
382 if (secretRegex.regex.test(probe)) {
383 return `<REDACTED: ${secretRegex.label}>`;
384 }
385 }
386 return value;
387 }
389 > /**
390 > * Attempts to remove commonly leaked PII.
391 > *
392 > * When a match is found the check is applied per line so that a single suspicious
393 > * frame (e.g. a stack frame containing a function name such as `getStorageKey`
394 > * which matches the broad `Generic Secret` heuristic) only redacts that line —
395 > * replacing it with a `<REDACTED: ...>` marker — instead of wiping the entire
396 > * multi-line value such as a whole callstack.
397 > * @param property The property whose offending lines will be replaced with a redaction marker if they contain user data
398 > * @returns The new value for the property
399 > */
400 function removePropertiesWithPossibleUserInfo(property: string): string {
401 // If for some reason it is undefined we skip it (this shouldn't be possible);
402 if (!property) {
403 return property;
404 }
405
406 // Fast path: if nothing matches we return the value untouched without
407 // allocating. This keeps the common (no-PII) case as cheap as the previous
408 // implementation and avoids splitting potentially large callstacks.
409 let hasMatch = false;
410 for (const secretRegex of userDataRegexes) {
411 if (secretRegex.regex.test(property)) {
412 hasMatch = true;
413 break;
414 }
415 }
416 if (!hasMatch) {
417 return property;
418 }
419
420 // Single line values keep the original behavior of redacting the whole value.
421 if (!property.includes('\n')) {
422 return redactIfPossibleUserInfo(property);
423 }
424
425 // Multi-line values (e.g. callstacks) are redacted line-by-line so we only
426 // drop the offending lines and preserve the rest of the information. The
427 // newline delimiter stripped by `split` is re-appended (for every line but
428 // the last) when matching so heuristics that rely on a trailing
429 // non-alphanumeric boundary behave identically to the previous whole-string
430 // check and don't under-redact the last token of a line.
431 const lines = property.split('\n');
432 for (let i = 0; i < lines.length; i++) {
433 const probe = i < lines.length - 1 ? lines[i] + '\n' : lines[i];
434 lines[i] = redactIfPossibleUserInfo(lines[i], probe);
435 }
436 return lines.join('\n');
437 }
439 >
440 > /**
441 > * Does a best possible effort to clean a data object from any possible PII.
442 > * @param data The data object to clean
443 > * @param paths Any additional patterns that should be removed from the data set
444 > * @returns A new object with the PII removed
445 > */
446 > export function cleanData(data: ITelemetryData | undefined, cleanUpPatterns: RegExp[]): Record<string, unknown> {
447 if (!data) {
448 return {};
449 }
450 return cloneAndChange(data, value => {
451
452 // If it's a trusted value it means it's okay to skip cleaning so we don't clean it
453 if (value instanceof TelemetryTrustedValue || Object.hasOwnProperty.call(value, 'isTrustedTelemetryValue')) {
454 return value.value;
455 }
456
457 // We only know how to clean strings
458 if (typeof value === 'string') {
459 let updatedProperty = value.replaceAll('%20', ' ');
460
461 // First we anonymize any possible file paths
462 updatedProperty = anonymizeFilePaths(updatedProperty, cleanUpPatterns);
463
464 // Then we do a simple regex replace with the defined patterns
465 for (const regexp of cleanUpPatterns) {
466 updatedProperty = updatedProperty.replace(regexp, '');
467 }
468
469 // Lastly, remove commonly leaked PII
470 updatedProperty = removePropertiesWithPossibleUserInfo(updatedProperty);
471
472 return updatedProperty;
473 }
474 return undefined;
475 });
476 }
478 > //#endregion