From f8f92748b1bace2da1fb6300a8336a4572b30228 Mon Sep 17 00:00:00 2001 From: Remi Schnekenburger Date: Sat, 22 Apr 2023 18:56:00 +0200 Subject: [PATCH] [vscode] support TelemetryLogger resolves eclipse-theia#12232 Contributed on behalf of STMicroelectronics Signed-off-by: Remi Schnekenburger --- packages/core/src/common/index.ts | 1 + packages/core/src/common/objects.ts | 49 ++- packages/core/src/common/telemetry.ts | 45 +++ .../plugin-ext/src/common/plugin-api-rpc.ts | 10 +- packages/plugin-ext/src/plugin/env.ts | 13 - .../plugin-ext/src/plugin/plugin-context.ts | 11 +- .../plugin-ext/src/plugin/telemetry-ext.ts | 299 ++++++++++++++++++ packages/plugin-ext/src/plugin/types-impl.ts | 48 +++ packages/plugin/src/theia.d.ts | 147 +++++++++ 9 files changed, 606 insertions(+), 17 deletions(-) create mode 100644 packages/core/src/common/telemetry.ts create mode 100644 packages/plugin-ext/src/plugin/telemetry-ext.ts diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 020585aa3e311..a37d038c966d5 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -46,4 +46,5 @@ export * from './strings'; export * from './types'; export { default as URI } from './uri'; export * from './view-column'; +export * from './telemetry'; diff --git a/packages/core/src/common/objects.ts b/packages/core/src/common/objects.ts index 0e7859614faa8..a63fca5909040 100644 --- a/packages/core/src/common/objects.ts +++ b/packages/core/src/common/objects.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { isObject } from './types'; +import { isObject, isUndefined } from './types'; export function deepClone(obj: T): T { if (!isObject(obj)) { @@ -70,3 +70,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/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index eeee279baca3e..46a1fdf604422 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -2079,6 +2079,12 @@ export interface TabsMain { $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise; } +export interface TelemetryMain { +} + +export interface TelemetryExt { +} + // endregion export const PLUGIN_RPC_CONTEXT = { @@ -2116,6 +2122,7 @@ export const PLUGIN_RPC_CONTEXT = { THEMING_MAIN: >createProxyIdentifier('ThemingMain'), COMMENTS_MAIN: >createProxyIdentifier('CommentsMain'), TABS_MAIN: >createProxyIdentifier('TabsMain'), + TELEMETRY_MAIN: >createProxyIdentifier('TelemetryMain'), LOCALIZATION_MAIN: >createProxyIdentifier('LocalizationMain'), }; @@ -2151,7 +2158,8 @@ export const MAIN_RPC_CONTEXT = { TIMELINE_EXT: createProxyIdentifier('TimeLineExt'), THEMING_EXT: createProxyIdentifier('ThemingExt'), COMMENTS_EXT: createProxyIdentifier('CommentsExt'), - TABS_EXT: createProxyIdentifier('TabsExt') + TABS_EXT: createProxyIdentifier('TabsExt'), + TELEMETRY_EXT: createProxyIdentifier('TelemetryExt)') }; export interface TasksExt { 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 c171e4f54c065..b20f2b803edcd 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -163,6 +163,7 @@ import { InlayHint, InlayHintKind, InlayHintLabelPart, + TelemetryTrustedValue, NotebookCell, NotebookCellKind, NotebookCellStatusBarAlignment, @@ -236,6 +237,7 @@ import { Endpoint } from '@theia/core/lib/browser/endpoint'; import { FilePermission } from '@theia/filesystem/lib/common/files'; import { TabsExtImpl } from './tabs'; import { LocalizationExtImpl } from './localization-ext'; +import { TelemetryExtImpl } from './telemetry-ext'; export function createAPIFactory( rpc: RPCProtocol, @@ -277,6 +279,7 @@ export function createAPIFactory( const tabsExt = rpc.set(MAIN_RPC_CONTEXT.TABS_EXT, new TabsExtImpl(rpc)); 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 { @@ -724,9 +727,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; }, @@ -1307,6 +1313,7 @@ export function createAPIFactory( InlayHint, InlayHintKind, InlayHintLabelPart, + TelemetryTrustedValue, NotebookCellData, NotebookCellKind, NotebookCellOutput, 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 13371907138bb..13d7bf7a9fe66 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -3413,6 +3413,54 @@ export class WebviewEditorTabInput { constructor(readonly viewType: string) { } } +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; +} + export class NotebookEditorTabInput { constructor(readonly uri: URI, readonly notebookType: string) { } } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index e55241cb6b938..b2cffc95ea97f 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -7565,6 +7565,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. @@ -14209,6 +14218,144 @@ export module '@theia/plugin' { close(tabGroup: TabGroup | readonly TabGroup[], preserveFocus?: boolean): Thenable; } + /** + * A special value wrapper denoting a value that is safe to not clean. + * This is to be used when you can guarantee no identifiable information is contained in the value and the cleaning is improperly redacting it. + */ + 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; + } + /** * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. */