Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Telemetry API #160902

Merged
merged 14 commits into from Oct 11, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion src/vs/platform/environment/common/environment.ts
Expand Up @@ -87,7 +87,6 @@ export interface IEnvironmentService {
// --- telemetry
disableTelemetry: boolean;
telemetryLogResource: URI;
extensionTelemetryLogResource: URI;
serviceMachineIdResource: URI;

// --- Policy
Expand Down
1 change: 0 additions & 1 deletion src/vs/platform/environment/common/environmentService.ts
Expand Up @@ -226,7 +226,6 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron

@memoize
get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); }
get extensionTelemetryLogResource(): URI { return URI.file(join(this.logsPath, 'extensionTelemetry.log')); }
get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; }

@memoize
Expand Down
93 changes: 3 additions & 90 deletions src/vs/platform/telemetry/common/telemetryService.ts
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { DisposableStore } from 'vs/base/common/lifecycle';
import { cloneAndChange, mixin } from 'vs/base/common/objects';
import { mixin } from 'vs/base/common/objects';
import { MutableObservableValue } from 'vs/base/common/observableValue';
import { isWeb } from 'vs/base/common/platform';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
Expand All @@ -16,7 +16,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { Registry } from 'vs/platform/registry/common/platform';
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings';
import { ITelemetryData, ITelemetryInfo, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry';
import { getTelemetryLevel, ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { cleanData, getTelemetryLevel, ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';

export interface ITelemetryServiceConfig {
appenders: ITelemetryAppender[];
Expand Down Expand Up @@ -117,12 +117,7 @@ export class TelemetryService implements ITelemetryService {
data = mixin(data, this._experimentProperties);

// (last) remove all PII from data
data = cloneAndChange(data, value => {
if (typeof value === 'string') {
return this._cleanupInfo(value, anonymizeFilePaths);
}
return undefined;
});
data = cleanData(data as Record<string, any>, this._cleanupPatterns);

// Log to the appenders of sufficient level
this._appenders.forEach(a => a.log(eventName, data));
Expand Down Expand Up @@ -153,88 +148,6 @@ export class TelemetryService implements ITelemetryService {
publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): Promise<any> {
return this.publicLogError(eventName, data as ITelemetryData);
}

private _anonymizeFilePaths(stack: string): string {
let updatedStack = stack;

const cleanUpIndexes: [number, number][] = [];
for (const regexp of this._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;
}
// Anoynimize user file paths that do not need to be retained or cleaned up.
if (!nodeModulesRegex.test(result[0]) && cleanUpIndexes.every(([x, y]) => result.index < x || result.index >= y)) {
updatedStack += stack.substring(lastIndex, result.index) + '<REDACTED: user-file-path>';
lastIndex = fileRegex.lastIndex;
}
}
if (lastIndex < stack.length) {
updatedStack += stack.substr(lastIndex);
}

return updatedStack;
}

private _removePropertiesWithPossibleUserInfo(property: string): string {
// If for some reason it is undefined we skip it (this shouldn't be possible);
if (!property) {
return property;
}

const value = property.toLowerCase();

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]/ },
{ 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(value)) {
return `<REDACTED: ${secretRegex.label}>`;
}
}

return property;
}


private _cleanupInfo(property: string, anonymizeFilePaths?: boolean): string {
let updatedProperty = property;

// anonymize file paths
if (anonymizeFilePaths) {
updatedProperty = this._anonymizeFilePaths(updatedProperty);
}

// sanitize with configured cleanup patterns
for (const regexp of this._cleanupPatterns) {
updatedProperty = updatedProperty.replace(regexp, '');
}

// remove possible user info
updatedProperty = this._removePropertiesWithPossibleUserInfo(updatedProperty);

return updatedProperty;
}
}

function getTelemetryLevelSettingDescription(): string {
Expand Down
110 changes: 108 additions & 2 deletions src/vs/platform/telemetry/common/telemetryUtils.ts
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { IDisposable } from 'vs/base/common/lifecycle';
import { safeStringify } from 'vs/base/common/objects';
import { cloneAndChange, safeStringify } from 'vs/base/common/objects';
import { staticObservableValue } from 'vs/base/common/observableValue';
import { isObject } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
Expand Down Expand Up @@ -191,7 +191,7 @@ export function validateTelemetryData(data?: any): { properties: Properties; mea
};
}

const telemetryAllowedAuthorities: readonly string[] = ['ssh-remote', 'dev-container', 'attached-container', 'wsl', 'tunneling'];
const telemetryAllowedAuthorities: readonly string[] = ['ssh-remote', 'dev-container', 'attached-container', 'wsl', 'tunneling', 'codespaces'];

export function cleanRemoteAuthority(remoteAuthority?: string): string {
if (!remoteAuthority) {
Expand Down Expand Up @@ -276,3 +276,109 @@ interface IPathEnvironment {
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 {
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;
}
// Anoynimize user file paths that do not need to be retained or cleaned up.
if (!nodeModulesRegex.test(result[0]) && cleanUpIndexes.every(([x, y]) => result.index < x || result.index >= y)) {
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 value = property.toLowerCase();

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]/ },
{ 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(value)) {
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 => {
// We only know how to clean strings
if (typeof value === 'string') {
let updatedProperty = value;
// 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
18 changes: 0 additions & 18 deletions src/vs/workbench/api/browser/mainThreadTelemetry.ts
Expand Up @@ -5,7 +5,6 @@

import { Disposable } from 'vs/base/common/lifecycle';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILogger, ILoggerService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings';
import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
Expand All @@ -19,26 +18,14 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet

private static readonly _name = 'pluginHostTelemetry';

private readonly _extensionTelemetryLog: ILogger;

constructor(
extHostContext: IExtHostContext,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@IProductService private readonly _productService: IProductService,
@ILoggerService loggerService: ILoggerService,
) {
super();

const logger = loggerService.getLogger(this._environmentService.extensionTelemetryLogResource);
if (logger) {
this._extensionTelemetryLog = this._register(logger);
} else {
this._extensionTelemetryLog = this._register(loggerService.createLogger(this._environmentService.extensionTelemetryLogResource));
this._extensionTelemetryLog.info('Below are logs for extension telemetry events sent to the telemetry output channel API once the log level is set to trace.');
this._extensionTelemetryLog.info('===========================================================');
}

this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTelemetry);

if (supportsTelemetry(this._productService, this._environmentService)) {
Expand Down Expand Up @@ -67,11 +54,6 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet
$publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void {
this.$publicLog(eventName, data as any);
}

$logTelemetryToOutputChannel(eventName: string, data: Record<string, any>) {
this._extensionTelemetryLog.trace(eventName, data);
this._extensionTelemetryLog.flush();
}
}


15 changes: 6 additions & 9 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Expand Up @@ -80,7 +80,7 @@ import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting';
import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener';
import { IExtHostSecretState } from 'vs/workbench/api/common/extHostSecretState';
import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs';
import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry';
import { IExtHostTelemetry, isNewAppInstall } from 'vs/workbench/api/common/extHostTelemetry';
import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels';
import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes';
import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers';
Expand All @@ -92,7 +92,6 @@ import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive';
import { combinedDisposable } from 'vs/base/common/lifecycle';
import { checkProposedApiEnabled, ExtensionIdentifierSet, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug';
import { IExtHostTelemetryLogService } from 'vs/workbench/api/common/extHostTelemetryLogService';
import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';

export interface IExtensionRegistries {
Expand Down Expand Up @@ -124,7 +123,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostLoggerService = accessor.get(ILoggerService);
const extHostLogService = accessor.get(ILogService);
const extHostTunnelService = accessor.get(IExtHostTunnelService);
const extHostTelemetryLogService = accessor.get(IExtHostTelemetryLogService);
const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService);
const extHostWindow = accessor.get(IExtHostWindow);
const extHostSecretState = accessor.get(IExtHostSecretState);
Expand Down Expand Up @@ -330,8 +328,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
return extHostTelemetry.onDidChangeTelemetryConfiguration;
},
get isNewAppInstall() {
const installAge = Date.now() - new Date(initData.telemetryInfo.firstSessionDate).getTime();
return isNaN(installAge) ? false : installAge < 1000 * 60 * 60 * 24; // install age is less than a day
return isNewAppInstall(initData.telemetryInfo.firstSessionDate);
},
createTelemetryLogger(appender: vscode.TelemetryAppender): vscode.TelemetryLogger {
checkProposedApiEnabled(extension, 'telemetry');
return extHostTelemetry.instantiateLogger(extension, appender);
},
openExternal(uri: URI, options?: { allowContributedOpeners?: boolean | string }) {
return extHostWindow.openUri(uri, {
Expand Down Expand Up @@ -796,10 +797,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
},
get tabGroups(): vscode.TabGroups {
return extHostEditorTabs.tabGroups;
},
logTelemetryToOutputChannel(eventName: string, data: Record<string, any>): void {
checkProposedApiEnabled(extension, 'telemetryLog');
extHostTelemetryLogService.logToTelemetryOutputChannel(extension, eventName, data);
}
};

Expand Down
2 changes: 0 additions & 2 deletions src/vs/workbench/api/common/extHost.common.services.ts
Expand Up @@ -27,7 +27,6 @@ import { ExtHostLoggerService } from 'vs/workbench/api/common/extHostLoggerServi
import { ILoggerService, ILogService } from 'vs/platform/log/common/log';
import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService';
import { ExtHostVariableResolverProviderService, IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService';
import { ExtHostTelemetryLogService, IExtHostTelemetryLogService } from 'vs/workbench/api/common/extHostTelemetryLogService';
import { ExtHostLocalizationService, IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';

registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed);
Expand All @@ -51,6 +50,5 @@ registerSingleton(IExtHostWindow, ExtHostWindow, false);
registerSingleton(IExtHostWorkspace, ExtHostWorkspace, false);
registerSingleton(IExtHostSecretState, ExtHostSecretState, false);
registerSingleton(IExtHostTelemetry, ExtHostTelemetry, false);
registerSingleton(IExtHostTelemetryLogService, ExtHostTelemetryLogService, false);
registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs, false);
registerSingleton(IExtHostVariableResolverProvider, ExtHostVariableResolverProviderService, false);
1 change: 0 additions & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Expand Up @@ -598,7 +598,6 @@ export interface MainThreadStorageShape extends IDisposable {
export interface MainThreadTelemetryShape extends IDisposable {
$publicLog(eventName: string, data?: any): void;
$publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void;
$logTelemetryToOutputChannel(eventName: string, data: Record<string, any>): void;
}

export interface MainThreadEditorInsetsShape extends IDisposable {
Expand Down