diff --git a/packages/core/shared/reflect-metadata/index.d.ts b/packages/core/shared/reflect-metadata/index.d.ts new file mode 100644 index 0000000000000..7b873b4cc900a --- /dev/null +++ b/packages/core/shared/reflect-metadata/index.d.ts @@ -0,0 +1 @@ +export * from 'reflect-metadata'; diff --git a/packages/core/shared/reflect-metadata/index.js b/packages/core/shared/reflect-metadata/index.js new file mode 100644 index 0000000000000..a866c850ab048 --- /dev/null +++ b/packages/core/shared/reflect-metadata/index.js @@ -0,0 +1,2 @@ +module.exports = require('reflect-metadata'); + diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index cedfd254d0009..e50aba9e37784 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -43,6 +43,9 @@ export * from './contribution-filter'; export * from './nls'; export * from './numbers'; export * from './performance'; +export * from './types'; +export { default as URI } from './uri'; +export * from './telemetry'; import { environment } from '@theia/application-package/lib/environment'; export { environment }; diff --git a/packages/core/src/common/objects.ts b/packages/core/src/common/objects.ts index 183fc81bdfef8..6c0aba149c5e5 100644 --- a/packages/core/src/common/objects.ts +++ b/packages/core/src/common/objects.ts @@ -14,6 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +import { isObject, isUndefined } from './types'; + export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { return obj; @@ -69,3 +71,50 @@ export function notEmpty(arg: T | undefined | null): arg is T { export function isEmpty(arg: Object): boolean { return Object.keys(arg).length === 0 && arg.constructor === Object; } + +// copied and modified from https://github.com/microsoft/vscode/blob/1.76.0/src/vs/base/common/objects.ts#L45-L83 +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function cloneAndChange(obj: any, changer: (orig: any) => any, seen: Set): any { + // impossible to clone an undefined or null object + // eslint-disable-next-line no-null/no-null + if (isUndefined(obj) || obj === null) { + return obj; + } + + const changed = changer(obj); + if (!isUndefined(changed)) { + return changed; + } + + if (Array.isArray(obj)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const r1: any[] = []; + for (const e of obj) { + r1.push(cloneAndChange(e, changer, seen)); + } + return r1; + } + + if (isObject(obj)) { + if (seen.has(obj)) { + throw new Error('Cannot clone recursive data-structure'); + } + seen.add(obj); + const r2 = {}; + for (const i2 in obj) { + if (_hasOwnProperty.call(obj, i2)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (r2 as any)[i2] = cloneAndChange(obj[i2], changer, seen); + } + } + seen.delete(obj); + return r2; + } + + return obj; +} diff --git a/packages/core/src/common/telemetry.ts b/packages/core/src/common/telemetry.ts new file mode 100644 index 0000000000000..3b143957fc280 --- /dev/null +++ b/packages/core/src/common/telemetry.ts @@ -0,0 +1,45 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +export class TelemetryTrustedValue { + readonly value: T; + + constructor(value: T) { + this.value = value; + } +} + +export interface TelemetryLogger { + readonly sender: TelemetrySender; + readonly options: TelemetryLoggerOptions | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logUsage(eventName: string, data?: Record>): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logError(eventNameOrException: string | Error, data?: Record>): void; + + dispose(): void; +} + +interface TelemetrySender { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendEventData(eventName: string, data?: Record): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendErrorData(error: Error, data?: Record): void; + flush?(): void | Thenable; +} + +interface TelemetryLoggerOptions { +} diff --git a/packages/core/src/common/types.ts b/packages/core/src/common/types.ts index a01d1c4bef275..0a147c33d59d7 100644 --- a/packages/core/src/common/types.ts +++ b/packages/core/src/common/types.ts @@ -14,6 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +type UnknownObject = Record & { [K in keyof T]: unknown }; + export type Mutable = { -readonly [P in keyof T]: T[P] }; export type MaybeNull = { [P in keyof T]: T[P] | null }; export type MaybeUndefined = { [P in keyof T]: T[P] | undefined }; @@ -177,3 +179,12 @@ export namespace ArrayUtils { export function unreachable(_never: never, message: string = 'unhandled case'): never { throw new Error(message); } + +export function isUndefined(value: unknown): value is undefined { + return typeof value === 'undefined'; +} + +export function isObject(value: unknown): value is UnknownObject { + // eslint-disable-next-line no-null/no-null + return typeof value === 'object' && value !== null; +} diff --git a/packages/monaco/package.json b/packages/monaco/package.json index 87c362acb2a9c..ef032c65afa52 100644 --- a/packages/monaco/package.json +++ b/packages/monaco/package.json @@ -13,7 +13,7 @@ "idb": "^4.0.5", "jsonc-parser": "^2.2.0", "vscode-oniguruma": "1.6.1", - "vscode-textmate": "7.0.1" + "vscode-textmate": "7.0.4" }, "publishConfig": { "access": "public" diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 23ffae0440948..498df4fcca280 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1903,6 +1903,13 @@ export interface CommentsMain { $onDidCommentThreadsChange(handle: number, event: CommentThreadChangedEvent): void; } +export interface TelemetryMain { +} + +export interface TelemetryExt { +} + +// endregion export const PLUGIN_RPC_CONTEXT = { AUTHENTICATION_MAIN: >createProxyIdentifier('AuthenticationMain'), COMMAND_REGISTRY_MAIN: >createProxyIdentifier('CommandRegistryMain'), @@ -1937,7 +1944,8 @@ export const PLUGIN_RPC_CONTEXT = { LABEL_SERVICE_MAIN: >createProxyIdentifier('LabelServiceMain'), TIMELINE_MAIN: >createProxyIdentifier('TimelineMain'), THEMING_MAIN: >createProxyIdentifier('ThemingMain'), - COMMENTS_MAIN: >createProxyIdentifier('CommentsMain') + COMMENTS_MAIN: >createProxyIdentifier('CommentsMain'), + TELEMETRY_MAIN: >createProxyIdentifier('TelemetryMain'), }; export const MAIN_RPC_CONTEXT = { @@ -1972,7 +1980,8 @@ export const MAIN_RPC_CONTEXT = { LABEL_SERVICE_EXT: createProxyIdentifier('LabelServiceExt'), TIMELINE_EXT: createProxyIdentifier('TimeLineExt'), THEMING_EXT: createProxyIdentifier('ThemingExt'), - COMMENTS_EXT: createProxyIdentifier('CommentsExt') + COMMENTS_EXT: createProxyIdentifier('CommentsExt'), + TELEMETRY_EXT: createProxyIdentifier('TelemetryExt)') }; export interface TasksExt { diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index 4176d07b499c0..5907fb5c74fcc 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -13,7 +13,7 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** - +import 'reflect-metadata'; import { Emitter } from '@theia/core/lib/common/event'; import { RPCProtocolImpl } from '../../../common/rpc-protocol'; import { PluginManagerExtImpl } from '../../../plugin/plugin-manager'; @@ -49,9 +49,9 @@ const rpc = new RPCProtocolImpl({ ctx.postMessage(m); }, }, -{ - reviver: reviver -}); + { + reviver: reviver + }); // eslint-disable-next-line @typescript-eslint/no-explicit-any addEventListener('message', (message: any) => { diff --git a/packages/plugin-ext/src/hosted/node/plugin-host.ts b/packages/plugin-ext/src/hosted/node/plugin-host.ts index fa54f21beb537..abb2f773dd7d0 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host.ts @@ -13,7 +13,7 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** - +import '@theia/core/shared/reflect-metadata'; import { Emitter } from '@theia/core/lib/common/event'; import { RPCProtocolImpl, MessageType, ConnectionClosedError } from '../../common/rpc-protocol'; import { PluginHostRPC } from './plugin-host-rpc'; @@ -83,9 +83,9 @@ const rpc = new RPCProtocolImpl({ } } }, -{ - reviver: reviver -}); + { + reviver: reviver + }); process.on('message', async (message: string) => { if (terminating) { diff --git a/packages/plugin-ext/src/plugin/env.ts b/packages/plugin-ext/src/plugin/env.ts index 8033ca7c12b3b..f289c11da92d2 100644 --- a/packages/plugin-ext/src/plugin/env.ts +++ b/packages/plugin-ext/src/plugin/env.ts @@ -14,7 +14,6 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { Emitter, Event } from '@theia/core/lib/common/event'; import * as theia from '@theia/plugin'; import { RPCProtocol } from '../common/rpc-protocol'; import { EnvMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; @@ -31,16 +30,12 @@ export abstract class EnvExtImpl { private envMachineId: string; private envSessionId: string; private host: string; - private _isTelemetryEnabled: boolean; private _remoteName: string | undefined; - private onDidChangeTelemetryEnabledEmitter = new Emitter(); constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.ENV_MAIN); this.envSessionId = v4(); this.envMachineId = v4(); - // we don't support telemetry at the moment - this._isTelemetryEnabled = false; this._remoteName = undefined; } @@ -101,14 +96,6 @@ export abstract class EnvExtImpl { return this.host; } - get isTelemetryEnabled(): boolean { - return this._isTelemetryEnabled; - } - - get onDidChangeTelemetryEnabled(): Event { - return this.onDidChangeTelemetryEnabledEmitter.event; - } - get remoteName(): string | undefined { return this._remoteName; } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 104773f855f98..0e46f479da30d 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -155,6 +155,7 @@ import { InlayHint, InlayHintKind, InlayHintLabelPart, + TelemetryTrustedValue, TestRunProfileKind, TestTag, TestRunRequest, @@ -201,6 +202,7 @@ import { CustomEditorsExtImpl } from './custom-editors'; import { WebviewViewsExtImpl } from './webview-views'; import { PluginPackage } from '../common'; import { Endpoint } from '@theia/core/lib/browser/endpoint'; +import { TelemetryExtImpl } from './telemetry-ext'; export function createAPIFactory( rpc: RPCProtocol, @@ -240,6 +242,7 @@ export function createAPIFactory( const commentsExt = rpc.set(MAIN_RPC_CONTEXT.COMMENTS_EXT, new CommentsExtImpl(rpc, commandRegistry, documents)); const customEditorExt = rpc.set(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT, new CustomEditorsExtImpl(rpc, documents, webviewExt, workspaceExt)); const webviewViewsExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEW_VIEWS_EXT, new WebviewViewsExtImpl(rpc, webviewExt)); + const telemetryExt = rpc.set(MAIN_RPC_CONTEXT.TELEMETRY_EXT, new TelemetryExtImpl()); rpc.set(MAIN_RPC_CONTEXT.DEBUG_EXT, debugExt); return function (plugin: InternalPlugin): typeof theia { @@ -634,9 +637,12 @@ export function createAPIFactory( get appHost(): string { return envExt.appHost; }, get language(): string { return envExt.language; }, get isNewAppInstall(): boolean { return envExt.isNewAppInstall; }, - get isTelemetryEnabled(): boolean { return envExt.isTelemetryEnabled; }, + get isTelemetryEnabled(): boolean { return telemetryExt.isTelemetryEnabled; }, get onDidChangeTelemetryEnabled(): theia.Event { - return envExt.onDidChangeTelemetryEnabled; + return telemetryExt.onDidChangeTelemetryEnabled; + }, + createTelemetryLogger(sender: theia.TelemetrySender, options?: theia.TelemetryLoggerOptions): theia.TelemetryLogger { + return telemetryExt.createTelemetryLogger(sender, options); }, get remoteName(): string | undefined { return envExt.remoteName; }, get machineId(): string { return envExt.machineId; }, @@ -1116,6 +1122,7 @@ export function createAPIFactory( InlayHint, InlayHintKind, InlayHintLabelPart, + TelemetryTrustedValue, TestRunProfileKind, TestTag, TestRunRequest, diff --git a/packages/plugin-ext/src/plugin/telemetry-ext.ts b/packages/plugin-ext/src/plugin/telemetry-ext.ts new file mode 100644 index 0000000000000..c83f2ec487d2d --- /dev/null +++ b/packages/plugin-ext/src/plugin/telemetry-ext.ts @@ -0,0 +1,299 @@ +// ***************************************************************************** +// Copyright (C) 2023 STMicroelectronics and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { cloneAndChange } from '@theia/core'; +import { mixin } from '../common/types'; +import { TelemetryTrustedValue, TelemetryLoggerOptions } from './types-impl'; + +export class TelemetryExtImpl { + + _isTelemetryEnabled: boolean = false; // telemetry not activated by default + private readonly onDidChangeTelemetryEnabledEmitter = new Emitter(); + readonly onDidChangeTelemetryEnabled: Event = this.onDidChangeTelemetryEnabledEmitter.event; + + get isTelemetryEnabled(): boolean { + return this._isTelemetryEnabled; + } + + set isTelemetryEnabled(isTelemetryEnabled: boolean) { + if (this._isTelemetryEnabled !== isTelemetryEnabled) { + this._isTelemetryEnabled = isTelemetryEnabled; + this.onDidChangeTelemetryEnabledEmitter.fire(this._isTelemetryEnabled); + } + } + + createTelemetryLogger(sender: TelemetrySender, options?: TelemetryLoggerOptions | undefined): TelemetryLogger { + const logger = new TelemetryLogger(sender, this._isTelemetryEnabled, options); + this.onDidChangeTelemetryEnabled(isEnabled => { + logger.telemetryEnabled = isEnabled; + }); + return logger; + } +} + +export class TelemetryLogger { + private sender: TelemetrySender | undefined; + readonly options: TelemetryLoggerOptions | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly commonProperties: Record; + telemetryEnabled: boolean; + + private readonly onDidChangeEnableStatesEmitter: Emitter = new Emitter(); + readonly onDidChangeEnableStates: Event = this.onDidChangeEnableStatesEmitter.event; + private _isUsageEnabled: boolean; + private _isErrorsEnabled: boolean; + + constructor(sender: TelemetrySender, telemetryEnabled: boolean, options?: TelemetryLoggerOptions) { + this.sender = sender; + this.options = options; + this.commonProperties = this.getCommonProperties(); + this._isErrorsEnabled = true; + this._isUsageEnabled = true; + this.telemetryEnabled = telemetryEnabled; + } + + get isUsageEnabled(): boolean { + return this._isUsageEnabled; + } + + set isUsageEnabled(isUsageEnabled: boolean) { + if (this._isUsageEnabled !== isUsageEnabled) { + this._isUsageEnabled = isUsageEnabled; + this.onDidChangeEnableStatesEmitter.fire(this); + } + } + + get isErrorsEnabled(): boolean { + return this._isErrorsEnabled; + } + + set isErrorsEnabled(isErrorsEnabled: boolean) { + if (this._isErrorsEnabled !== isErrorsEnabled) { + this._isErrorsEnabled = isErrorsEnabled; + this.onDidChangeEnableStatesEmitter.fire(this); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logUsage(eventName: string, data?: Record>): void { + if (!this.telemetryEnabled || !this.isUsageEnabled) { + return; + } + this.logEvent(eventName, data); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logError(eventNameOrException: string | Error, data?: Record>): void { + if (!this.telemetryEnabled || !this.isErrorsEnabled || !this.sender) { + // no sender available or error shall not be sent + return; + } + if (typeof eventNameOrException === 'string') { + this.logEvent(eventNameOrException, data); + } else { + this.sender.sendErrorData(eventNameOrException, data); + } + } + + dispose(): void { + if (this.sender?.flush) { + let tempSender: TelemetrySender | undefined = this.sender; + this.sender = undefined; + Promise.resolve(tempSender.flush!()).then(tempSender = undefined); + } else { + this.sender = undefined; + } + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private logEvent(eventName: string, data?: Record): void { + // No sender means likely disposed of, we should no-op + if (!this.sender) { + return; + } + data = mixInCommonPropsAndCleanData(data || {}, this.options?.additionalCommonProperties, this.options?.ignoreBuiltInCommonProperties ? undefined : this.commonProperties); + this.sender?.sendEventData(eventName, data); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getCommonProperties(): Record { + return []; + } +} + +interface TelemetrySender { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendEventData(eventName: string, data?: Record): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendErrorData(error: Error, data?: Record): void; + flush?(): void | Thenable; +} + +// copied and modified from https://github.com/microsoft/vscode/blob/1.76.0/src/vs/workbench/api/common/extHostTelemetry.ts +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function mixInCommonPropsAndCleanData(data: Record, additionalProperties?: Record, commonProperties?: Record): Record { + let updatedData = data.properties ?? data; + + // We don't clean measurements since they are just numbers + updatedData = cleanData(updatedData, []); + + if (additionalProperties) { + updatedData = mixin(updatedData, additionalProperties); + } + + if (commonProperties) { + updatedData = mixin(updatedData, commonProperties); + } + + if (data.properties) { + data.properties = updatedData; + } else { + data = updatedData; + } + + return data; +} + +// copied and modified from https://github.com/microsoft/vscode/blob/1.76.0/src/vs/platform/telemetry/common/telemetryUtils.ts#L321-L442 +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * 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); + + // anonymize 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) + ''; + 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[0-9A-Za-z-_]{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 ``; + } + } + + 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 + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function cleanData(data: Record, cleanUpPatterns: RegExp[]): Record { + 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) { + return value.value; + } + + // We only know how to clean strings + if (typeof value === 'string') { + let updatedProperty = value.replace(/%20/g, ' '); + + // 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; + }, new Set()); +} + diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index cf9eea96158cb..3d3a3824b0a96 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -2996,4 +2996,50 @@ export enum InputBoxValidationSeverity { Error = 3 } -// #endregion +export class TelemetryTrustedValue { + readonly value: T; + + constructor(value: T) { + this.value = value; + } +} + +export class TelemetryLogger { + readonly onDidChangeEnableStates: theia.Event; + readonly isUsageEnabled: boolean; + readonly isErrorsEnabled: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logUsage(eventName: string, data?: Record>): void { } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logError(eventNameOrError: string | Error, data?: Record>): void { } + dispose(): void { } + constructor(readonly sender: TelemetrySender, readonly options?: TelemetryLoggerOptions) { } +} + +export interface TelemetrySender { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendEventData(eventName: string, data?: Record): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendErrorData(error: Error, data?: Record): void; + flush?(): void | Thenable; +} + +export interface TelemetryLoggerOptions { + /** + * Whether or not you want to avoid having the built-in common properties such as os, extension name, etc injected into the data object. + * Defaults to `false` if not defined. + */ + readonly ignoreBuiltInCommonProperties?: boolean; + + /** + * Whether or not unhandled errors on the extension host caused by your extension should be logged to your sender. + * Defaults to `false` if not defined. + */ + readonly ignoreUnhandledErrors?: boolean; + + /** + * Any additional common properties which should be injected into the data object. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly additionalCommonProperties?: Record; +} diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 98ac3bc8cb526..6a1dc3b55918d 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -7055,6 +7055,15 @@ export module '@theia/plugin' { */ export const onDidChangeTelemetryEnabled: Event; + /** + * Creates a new {@link TelemetryLogger telemetry logger}. + * + * @param sender The telemetry sender that is used by the telemetry logger. + * @param options Options for the telemetry logger. + * @returns A new telemetry logger + */ + export function createTelemetryLogger(sender: TelemetrySender, options?: TelemetryLoggerOptions): TelemetryLogger; + /** * The name of a remote. Defined by extensions, popular samples are `wsl` for the Windows * Subsystem for Linux or `ssh-remote` for remotes using a secure shell. @@ -12988,6 +12997,140 @@ export module '@theia/plugin' { */ export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable; } + + export class TelemetryTrustedValue { + readonly value: T; + + constructor(value: T); + } + + /** + * A telemetry logger which can be used by extensions to log usage and error telemetry. + * + * A logger wraps around a {@link TelemetrySender sender} but it guarantees that + * - user settings to disable or tweak telemetry are respected, and that + * - potential sensitive data is removed + * + * It also enables an "echo UI" that prints whatever data is send and it allows the editor + * to forward unhandled errors to the respective extensions. + * + * To get an instance of a `TelemetryLogger`, use + * {@link env.createTelemetryLogger `createTelemetryLogger`}. + */ + export interface TelemetryLogger { + + /** + * An {@link Event} which fires when the enablement state of usage or error telemetry changes. + */ + readonly onDidChangeEnableStates: Event; + + /** + * Whether or not usage telemetry is enabled for this logger. + */ + readonly isUsageEnabled: boolean; + + /** + * Whether or not error telemetry is enabled for this logger. + */ + readonly isErrorsEnabled: boolean; + + /** + * Log a usage event. + * + * After completing cleaning, telemetry setting checks, and data mix-in calls `TelemetrySender.sendEventData` to log the event. + * Automatically supports echoing to extension telemetry output channel. + * @param eventName The event name to log + * @param data The data to log + */ + logUsage(eventName: string, data?: Record): void; + + /** + * Log an error event. + * + * After completing cleaning, telemetry setting checks, and data mix-in calls `TelemetrySender.sendEventData` to log the event. Differs from `logUsage` in that it will log the event if the telemetry setting is Error+. + * Automatically supports echoing to extension telemetry output channel. + * @param eventName The event name to log + * @param data The data to log + */ + logError(eventName: string, data?: Record): void; + + /** + * Log an error event. + * + * Calls `TelemetrySender.sendErrorData`. Does cleaning, telemetry checks, and data mix-in. + * Automatically supports echoing to extension telemetry output channel. + * Will also automatically log any exceptions thrown within the extension host process. + * @param error The error object which contains the stack trace cleaned of PII + * @param data Additional data to log alongside the stack trace + */ + logError(error: Error, data?: Record): void; + + /** + * Dispose this object and free resources. + */ + dispose(): void; + } + + /** + * The telemetry sender is the contract between a telemetry logger and some telemetry service. **Note** that extensions must NOT + * call the methods of their sender directly as the logger provides extra guards and cleaning. + * + * ```js + * const sender: vscode.TelemetrySender = {...}; + * const logger = vscode.env.createTelemetryLogger(sender); + * + * // GOOD - uses the logger + * logger.logUsage('myEvent', { myData: 'myValue' }); + * + * // BAD - uses the sender directly: no data cleansing, ignores user settings, no echoing to the telemetry output channel etc + * sender.logEvent('myEvent', { myData: 'myValue' }); + * ``` + */ + export interface TelemetrySender { + /** + * Function to send event data without a stacktrace. Used within a {@link TelemetryLogger} + * + * @param eventName The name of the event which you are logging + * @param data A serializable key value pair that is being logged + */ + sendEventData(eventName: string, data?: Record): void; + + /** + * Function to send an error. Used within a {@link TelemetryLogger} + * + * @param error The error being logged + * @param data Any additional data to be collected with the exception + */ + sendErrorData(error: Error, data?: Record): void; + + /** + * Optional flush function which will give this sender a chance to send any remaining events + * as its {@link TelemetryLogger} is being disposed + */ + flush?(): void | Thenable; + } + + /** + * Options for creating a {@link TelemetryLogger} + */ + export interface TelemetryLoggerOptions { + /** + * Whether or not you want to avoid having the built-in common properties such as os, extension name, etc injected into the data object. + * Defaults to `false` if not defined. + */ + readonly ignoreBuiltInCommonProperties?: boolean; + + /** + * Whether or not unhandled errors on the extension host caused by your extension should be logged to your sender. + * Defaults to `false` if not defined. + */ + readonly ignoreUnhandledErrors?: boolean; + + /** + * Any additional common properties which should be injected into the data object. + */ + readonly additionalCommonProperties?: Record; + } } /** diff --git a/yarn.lock b/yarn.lock index 8bfa0da9acaf6..cd96da10c3ff2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11600,6 +11600,11 @@ vhost@^3.0.2: resolved "https://registry.yarnpkg.com/vhost/-/vhost-3.0.2.tgz#2fb1decd4c466aa88b0f9341af33dc1aff2478d5" integrity sha512-S3pJdWrpFWrKMboRU4dLYgMrTgoPALsmYwOvyebK2M6X95b9kQrjZy5rwl3uzzpfpENe/XrNYu/2U+e7/bmT5g== +vscode-debugprotocol@^1.32.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.51.0.tgz#c03168dac778b6c24ce17b3511cb61e89c11b2df" + integrity sha512-dzKWTMMyebIMPF1VYMuuQj7gGFq7guR8AFya0mKacu+ayptJfaRuM0mdHCqiOth4FnRP8mPhEroFPx6Ift8wHA== + vscode-jsonrpc@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" @@ -11652,10 +11657,10 @@ vscode-textmate@5.2.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== -vscode-textmate@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.1.tgz#8118a32b02735dccd14f893b495fa5389ad7de79" - integrity sha512-zQ5U/nuXAAMsh691FtV0wPz89nSkHbs+IQV8FDk+wew9BlSDhf4UmWGlWJfTR2Ti6xZv87Tj5fENzKf6Qk7aLw== +vscode-textmate@7.0.4, vscode-textmate@^7.0.1: + version "7.0.4" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.4.tgz#a30df59ce573e998e4e2ffbca5ab82d57bc3126f" + integrity sha512-9hJp0xL7HW1Q5OgGe03NACo7yiCTMEk3WU/rtKXUbncLtdg6rVVNJnHwD88UhbIYU2KoxY0Dih0x+kIsmUKn2A== vscode-uri@^2.1.1: version "2.1.2"