diff --git a/src/test/datascience/interactiveWindow.vscode.test.ts b/src/test/datascience/interactiveWindow.vscode.test.ts index 506a076e5fa..f428e3af23f 100644 --- a/src/test/datascience/interactiveWindow.vscode.test.ts +++ b/src/test/datascience/interactiveWindow.vscode.test.ts @@ -62,7 +62,7 @@ suite('Interactive window', async function () { sinon.restore(); await closeNotebooksAndCleanUpAfterTests(disposables); }); - + test('Execute cell from Python file', async () => { const source = 'print(42)'; const { activeInteractiveWindow } = await submitFromPythonFile(interactiveWindowProvider, source, disposables); diff --git a/src/test/testHooks.ts b/src/test/testHooks.ts index 1c2299cf758..e9e7f64a491 100644 --- a/src/test/testHooks.ts +++ b/src/test/testHooks.ts @@ -1,5 +1,5 @@ import { Context } from 'mocha'; -import { AppinsightsKey, JVSC_EXTENSION_ID, Telemetry } from '../platform/common/constants'; +import { JVSC_EXTENSION_ID, Telemetry } from '../platform/common/constants'; import { IS_CI_SERVER } from './ciConstants.node'; import { extensions } from 'vscode'; import { sleep } from './core'; @@ -8,13 +8,14 @@ import { CiTelemetryReporter } from './utils/ciTelemetry/ciTelemetryReporter.nod let telemetryReporter: CiTelemetryReporter | undefined; export const rootHooks = { - beforeAll() { + beforeAll: async function () { if (!IS_CI_SERVER) { return; } const extensionVersion = extensions.getExtension(JVSC_EXTENSION_ID)?.packageJSON.version; - telemetryReporter = new CiTelemetryReporter(JVSC_EXTENSION_ID, extensionVersion, AppinsightsKey, true); + telemetryReporter = new CiTelemetryReporter(JVSC_EXTENSION_ID, extensionVersion); + await telemetryReporter.initialize(); }, afterEach(this: Context) { if (!IS_CI_SERVER) { @@ -25,7 +26,7 @@ export const rootHooks = { if (this.currentTest?.title) { const duration = this.currentTest?.duration; const measures = typeof duration === 'number' ? { duration: duration } : duration ? duration : undefined; - telemetryReporter?.sendRawTelemetryEvent( + telemetryReporter?.sendTelemetryEvent( Telemetry.RunTest, { testName: this.currentTest?.title, @@ -40,7 +41,7 @@ export const rootHooks = { return; } - await telemetryReporter?.dispose(); + await telemetryReporter?.flush(); // allow some time for the telemetry to flush await sleep(2000); } diff --git a/src/test/utils/ciTelemetry/TelemetryAppender.ts b/src/test/utils/ciTelemetry/TelemetryAppender.ts deleted file mode 100644 index 644822b00c3..00000000000 --- a/src/test/utils/ciTelemetry/TelemetryAppender.ts +++ /dev/null @@ -1,98 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import { AppenderData, ITelemetryAppender } from './baseCiTelemetryReporter'; - -export interface BaseTelemetryClient { - logEvent(eventName: string, data?: AppenderData): void; - logException(exception: Error, data?: AppenderData): void; - flush(): void | Promise; -} - -export class TelemetryAppender implements ITelemetryAppender { - private _telemetryClient: BaseTelemetryClient | undefined; - private _clientInitialization: Promise | undefined; - - // Queues used to store events until the appender is ready - private _eventQueue: Array<{ eventName: string; data: AppenderData | undefined }> = []; - private _exceptionQueue: Array<{ exception: Error; data: AppenderData | undefined }> = []; - - // Necessary information to create a telemetry client - private _clientFactory: (key: string) => Promise; - private _key: string; - - constructor(key: string, clientFactory: (key: string) => Promise) { - this._clientFactory = clientFactory; - this._key = key; - this.instantiateAppender(); - } - - /** - * Sends the event to the passed in telemetry client - * @param eventName The named of the event to log - * @param data The data contanied in the event - */ - logEvent(eventName: string, data?: AppenderData): void { - if (this._telemetryClient) { - this._telemetryClient.logEvent(eventName, data); - } else { - this._eventQueue.push({ eventName, data }); - } - } - - /** - * Sends an exception to the passed in telemetry client - * @param exception The exception to collect - * @param data Data associated with the exception - */ - logException(exception: Error, data?: AppenderData): void { - if (this._telemetryClient) { - this._telemetryClient.logException(exception, data); - } else { - this._exceptionQueue.push({ exception, data }); - } - } - - /** - * Flushes the buffered telemetry data - */ - async flush(): Promise { - if (this._clientInitialization) { - await this._clientInitialization; - if (this._telemetryClient) { - await this._telemetryClient.flush(); - this._telemetryClient = undefined; - } - } - return; - } - - /** - * Flushes the queued events that existed before the client was instantiated - */ - private _flushQueues(): void { - this._eventQueue.forEach(({ eventName, data }) => this.logEvent(eventName, data)); - this._eventQueue = []; - this._exceptionQueue.forEach(({ exception, data }) => this.logException(exception, data)); - this._exceptionQueue = []; - } - - /** - * Instantiates the telemetry client to make the appender "active" - */ - instantiateAppender(): void { - if (this._clientInitialization) { - return; - } - // Call the client factory to get the client and then let it know it's instatntiated - this._clientInitialization = this._clientFactory(this._key) - .then((client) => { - this._telemetryClient = client; - this._flushQueues(); - }) - .catch((err) => { - console.error(err); - }); - } -} diff --git a/src/test/utils/ciTelemetry/appInsightsClientFactory.node.ts b/src/test/utils/ciTelemetry/appInsightsClientFactory.node.ts deleted file mode 100644 index 802c25dfff4..00000000000 --- a/src/test/utils/ciTelemetry/appInsightsClientFactory.node.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { TelemetryClient } from 'applicationinsights'; -import { BaseTelemetryClient } from './TelemetryAppender'; -import { AppenderData } from './baseCiTelemetryReporter'; -import * as vscode from 'vscode'; - -/** - * A factory function which creates a telemetry client to be used by an appender to send telemetry in a node application. - * - * @param key The app insights key - * @param replacementOptions Optional list of {@link ReplacementOption replacements} to apply to the telemetry client. This allows - * the appender to filter out any sensitive or unnecessary information from the telemetry server. - * - * @returns A promise which resolves to the telemetry client or rejects upon error - */ -export async function appInsightsClientFactory(key: string): Promise { - let appInsightsClient: TelemetryClient | undefined; - try { - process.env['APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL'] = '1'; - const appInsights = await import('applicationinsights'); - //check if another instance is already initialized - if (appInsights.defaultClient) { - appInsightsClient = new appInsights.TelemetryClient(key); - // no other way to enable offline mode - appInsightsClient.channel.setUseDiskRetryCaching(true); - } else { - appInsights - .setup(key) - .setAutoCollectRequests(false) - .setAutoCollectPerformance(false) - .setAutoCollectExceptions(false) - .setAutoCollectDependencies(false) - .setAutoDependencyCorrelation(false) - .setAutoCollectConsole(false) - .setAutoCollectHeartbeat(false) - .setUseDiskRetryCaching(true) - .start(); - appInsightsClient = appInsights.defaultClient; - } - if (vscode && vscode.env) { - appInsightsClient.context.tags[appInsightsClient.context.keys.userId] = vscode.env.machineId; - appInsightsClient.context.tags[appInsightsClient.context.keys.sessionId] = vscode.env.sessionId; - appInsightsClient.context.tags[appInsightsClient.context.keys.cloudRole] = vscode.env.appName; - appInsightsClient.context.tags[appInsightsClient.context.keys.cloudRoleInstance] = vscode.env.appName; - } - //check if it's an Asimov key to change the endpoint - if (key && key.indexOf('AIF-') === 0) { - appInsightsClient.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; - } - } catch (e) { - return Promise.reject('Failed to initialize app insights!\n' + e.message); - } - - // Sets the appinsights client into a standardized form - const telemetryClient: BaseTelemetryClient = { - logEvent: (eventName: string, data?: AppenderData) => { - try { - appInsightsClient?.trackEvent({ - name: eventName, - properties: data?.properties, - measurements: data?.measurements - }); - } catch (e) { - throw new Error('Failed to log event to app insights!\n' + e.message); - } - }, - logException: (exception: Error, data?: AppenderData) => { - try { - appInsightsClient?.trackException({ - exception, - properties: data?.properties, - measurements: data?.measurements - }); - } catch (e) { - throw new Error('Failed to log exception to app insights!\n' + e.message); - } - }, - flush: async () => { - try { - appInsightsClient?.flush(); - } catch (e) { - throw new Error('Failed to flush app insights!\n' + e.message); - } - } - }; - return telemetryClient; -} diff --git a/src/test/utils/ciTelemetry/baseCiTelemetryReporter.ts b/src/test/utils/ciTelemetry/baseCiTelemetryReporter.ts deleted file mode 100644 index f9db69ca5d8..00000000000 --- a/src/test/utils/ciTelemetry/baseCiTelemetryReporter.ts +++ /dev/null @@ -1,374 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import * as vscode from 'vscode'; - -export interface AppenderData { - properties?: RawTelemetryEventProperties; - measurements?: TelemetryEventMeasurements; -} -export interface ITelemetryAppender { - logEvent(eventName: string, data?: AppenderData): void; - logException(exception: Error, data?: AppenderData): void; - flush(): void | Promise; - instantiateAppender(): void; -} - -export interface TelemetryEventProperties { - readonly [key: string]: string; -} - -export interface RawTelemetryEventProperties { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly [key: string]: any; -} - -export interface TelemetryEventMeasurements { - readonly [key: string]: number; -} - -export class BaseTelemetryReporter { - private userOptIn = true; - private errorOptIn = true; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _extension: vscode.Extension | undefined; - - constructor( - private extensionId: string, - private extensionVersion: string, - private telemetryAppender: ITelemetryAppender, - private osShim: { release: string; platform: string; architecture: string }, - private firstParty: boolean - ) {} - - /** - * Given a remoteName ensures it is in the list of valid ones - * @param remoteName The remotename - * @returns The "cleaned" one - */ - private cleanRemoteName(remoteName?: string): string { - if (!remoteName) { - return 'none'; - } - - let ret = 'other'; - // Allowed remote authorities - ['ssh-remote', 'dev-container', 'attached-container', 'wsl', 'codespaces'].forEach((res: string) => { - if (remoteName!.indexOf(`${res}`) === 0) { - ret = res; - } - }); - - return ret; - } - - /** - * Retrieves the current extension based on the extension id - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private get extension(): vscode.Extension | undefined { - if (this._extension === undefined) { - this._extension = vscode.extensions.getExtension(this.extensionId); - } - - return this._extension; - } - - /** - * Given an object and a callback creates a clone of the object and modifies it according to the callback - * @param obj The object to clone and modify - * @param change The modifying function - * @returns A new changed object - */ - private cloneAndChange( - obj?: { [key: string]: string }, - change?: (key: string, val: string) => string - ): { [key: string]: string } | undefined { - if (obj === null || typeof obj !== 'object') return obj; - if (typeof change !== 'function') return obj; - - const ret: { [key: string]: string } = {}; - for (const key of Object.keys(obj)) { - ret[key] = change(key, obj[key]!); - } - - return ret; - } - - /** - * Whether or not it is safe to send error telemetry - */ - private shouldSendErrorTelemetry(): boolean { - if (this.errorOptIn === false) { - return false; - } - - if (this.firstParty) { - // Don't collect errors from unknown remotes - if (vscode.env.remoteName && this.cleanRemoteName(vscode.env.remoteName) === 'other') { - return false; - } - - return true; - } - return true; - } - - // __GDPR__COMMON__ "common.os" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.platformversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.extname" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.extversion" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.vscodemachineid" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.vscodesessionid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.vscodeversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.isnewappinstall" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // __GDPR__COMMON__ "common.product" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - private getCommonProperties(): TelemetryEventProperties { - const commonProperties = Object.create(null); - commonProperties['common.os'] = this.osShim.platform; - commonProperties['common.nodeArch'] = this.osShim.architecture; - commonProperties['common.platformversion'] = (this.osShim.release || '').replace( - /^(\d+)(\.\d+)?(\.\d+)?(.*)/, - '$1$2$3' - ); - commonProperties['common.extname'] = this.extensionId; - commonProperties['common.extversion'] = this.extensionVersion; - if (vscode && vscode.env) { - commonProperties['common.vscodemachineid'] = vscode.env.machineId; - commonProperties['common.vscodesessionid'] = vscode.env.sessionId; - commonProperties['common.vscodeversion'] = vscode.version; - commonProperties['common.isnewappinstall'] = vscode.env.isNewAppInstall - ? vscode.env.isNewAppInstall.toString() - : 'false'; - commonProperties['common.product'] = vscode.env.appHost; - - switch (vscode.env.uiKind) { - case vscode.UIKind.Web: - commonProperties['common.uikind'] = 'web'; - break; - case vscode.UIKind.Desktop: - commonProperties['common.uikind'] = 'desktop'; - break; - default: - commonProperties['common.uikind'] = 'unknown'; - } - - commonProperties['common.remotename'] = this.cleanRemoteName(vscode.env.remoteName); - } - return commonProperties; - } - - /** - * Given an error stack cleans up the file paths within - * @param stack The stack to clean - * @param anonymizeFilePaths Whether or not to clean the file paths or anonymize them as well - * @returns The cleaned stack - */ - private anonymizeFilePaths(stack?: string, anonymizeFilePaths?: boolean): string { - let result: RegExpExecArray | null | undefined; - if (stack === undefined || stack === null) { - return ''; - } - - const cleanupPatterns = []; - if (vscode.env.appRoot !== '') { - cleanupPatterns.push(new RegExp(vscode.env.appRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')); - } - if (this.extension) { - cleanupPatterns.push(new RegExp(this.extension.extensionPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')); - } - - let updatedStack = stack; - - if (anonymizeFilePaths) { - const cleanUpIndexes: [number, number][] = []; - for (const regexp of cleanupPatterns) { - while ((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 ((result = fileRegex.exec(stack))) { - if (!result) { - break; - } - // Anoynimize user file paths that do not need to be retained or cleaned up. - if ( - result[0] && - !nodeModulesRegex.test(result[0]) && - cleanUpIndexes.every(([x, y]) => result!.index < x || result!.index >= y) - ) { - updatedStack += stack.substring(lastIndex, result.index) + ''; - lastIndex = fileRegex.lastIndex; - } - } - if (lastIndex < stack.length) { - updatedStack += stack.substr(lastIndex); - } - } - - // sanitize with configured cleanup patterns - for (const regexp of cleanupPatterns) { - updatedStack = updatedStack.replace(regexp, ''); - } - return updatedStack; - } - - private removePropertiesWithPossibleUserInfo( - properties: TelemetryEventProperties | undefined - ): TelemetryEventProperties | undefined { - if (typeof properties !== 'object') { - return; - } - const cleanedObject = Object.create(null); - // Loop through key and values of the properties object - for (const key of Object.keys(properties)) { - const value = properties[key]; - // If for some reason it is undefined we skip it (this shouldn't be possible); - if (!value) { - continue; - } - - // Regex which matches @*.site - const emailRegex = /@[a-zA-Z0-9-.]+/; - const secretRegex = /(key|token|sig|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]/; - // last +? is lazy as a microoptimization since we don't care about the full value - const tokenRegex = /xox[pbaors]-[a-zA-Z0-9]+-[a-zA-Z0-9-]+?/; - - // Check for common user data in the telemetry events - if (secretRegex.test(value.toLowerCase())) { - cleanedObject[key] = ''; - } else if (emailRegex.test(value)) { - cleanedObject[key] = ''; - } else if (tokenRegex.test(value)) { - cleanedObject[key] = ''; - } else { - cleanedObject[key] = value; - } - } - return cleanedObject; - } - - /** - * Given an event name, some properties, and measurements sends a telemetry event. - * Properties are sanitized on best-effort basis to remove sensitive data prior to sending. - * @param eventName The name of the event - * @param properties The properties to send with the event - * @param measurements The measurements (numeric values) to send with the event - */ - public sendTelemetryEvent( - eventName: string, - properties?: TelemetryEventProperties, - measurements?: TelemetryEventMeasurements - ): void { - if (this.userOptIn && eventName !== '') { - properties = { ...properties, ...this.getCommonProperties() }; - const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => - this.anonymizeFilePaths(prop, this.firstParty) - ); - this.telemetryAppender.logEvent(`${this.extensionId}/${eventName}`, { - properties: this.removePropertiesWithPossibleUserInfo(cleanProperties), - measurements: measurements - }); - } - } - - /** - * Given an event name, some properties, and measurements sends a raw (unsanitized) telemetry event - * @param eventName The name of the event - * @param properties The properties to send with the event - * @param measurements The measurements (numeric values) to send with the event - */ - public sendRawTelemetryEvent( - eventName: string, - properties?: RawTelemetryEventProperties, - measurements?: TelemetryEventMeasurements - ): void { - if (eventName !== '') { - properties = { ...properties, ...this.getCommonProperties() }; - this.telemetryAppender.logEvent(`${this.extensionId}/${eventName}`, { properties, measurements }); - } - } - - /** - * Given an event name, some properties, and measurements sends an error event - * @param eventName The name of the event - * @param properties The properties to send with the event - * @param measurements The measurements (numeric values) to send with the event - * @param errorProps If not present then we assume all properties belong to the error prop and will be anonymized - */ - public sendTelemetryErrorEvent( - eventName: string, - properties?: { [key: string]: string }, - measurements?: { [key: string]: number }, - errorProps?: string[] - ): void { - if (this.errorOptIn && eventName !== '') { - // always clean the properties if first party - // do not send any error properties if we shouldn't send error telemetry - // if we have no errorProps, assume all are error props - properties = { ...properties, ...this.getCommonProperties() }; - const cleanProperties = this.cloneAndChange(properties, (key: string, prop: string) => { - if (this.shouldSendErrorTelemetry()) { - return this.anonymizeFilePaths(prop, this.firstParty); - } - - if (errorProps === undefined || errorProps.indexOf(key) !== -1) { - return 'REDACTED'; - } - - return this.anonymizeFilePaths(prop, this.firstParty); - }); - this.telemetryAppender.logEvent(`${this.extensionId}/${eventName}`, { - properties: this.removePropertiesWithPossibleUserInfo(cleanProperties), - measurements: measurements - }); - } - } - - /** - * Given an error, properties, and measurements. Sends an exception event - * @param error The error to send - * @param properties The properties to send with the event - * @param measurements The measurements (numeric values) to send with the event - */ - public sendTelemetryException( - error: Error, - properties?: TelemetryEventProperties, - measurements?: TelemetryEventMeasurements - ): void { - if (this.shouldSendErrorTelemetry() && this.errorOptIn && error) { - properties = { ...properties, ...this.getCommonProperties() }; - const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => - this.anonymizeFilePaths(prop, this.firstParty) - ); - // Also run the error stack through the anonymizer - if (error.stack) { - error.stack = this.anonymizeFilePaths(error.stack, this.firstParty); - } - this.telemetryAppender.logException(error, { - properties: this.removePropertiesWithPossibleUserInfo(cleanProperties), - measurements: measurements - }); - } - } - - /** - * Disposes of the telemetry reporter - */ - public async dispose(): Promise { - await this.telemetryAppender.flush(); - } -} diff --git a/src/test/utils/ciTelemetry/ciTelemetryReporter.node.ts b/src/test/utils/ciTelemetry/ciTelemetryReporter.node.ts index bdd0994fa95..a7dcec60c88 100644 --- a/src/test/utils/ciTelemetry/ciTelemetryReporter.node.ts +++ b/src/test/utils/ciTelemetry/ciTelemetryReporter.node.ts @@ -2,31 +2,93 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +import { TelemetryClient } from 'applicationinsights'; import * as os from 'os'; -import { BaseTelemetryReporter } from './baseCiTelemetryReporter'; -import { TelemetryAppender } from './TelemetryAppender'; -import { appInsightsClientFactory } from './appInsightsClientFactory.node'; - +import { AppinsightsKey } from '../../../platform/common/constants.node'; +import * as vscode from 'vscode'; /** * An isolated wrapper for an Application Insights client that is specifically for sending telemetry during CI jobs. * This won't run on a users machine, so there is no need to check for opt-in status. */ -export class CiTelemetryReporter extends BaseTelemetryReporter { - constructor(extensionId: string, extensionVersion: string, key: string, firstParty: boolean) { - const appender = new TelemetryAppender(key, (key) => appInsightsClientFactory(key)); - if (key && key.indexOf('AIF-') === 0) { - firstParty = true; +export class CiTelemetryReporter { + private telemetryClient: TelemetryClient | undefined; + + constructor(private readonly extensionId: string, private readonly extensionVersion: string) {} + + public async initialize() { + let appInsightsClient: TelemetryClient | undefined; + try { + const appInsights = await import('applicationinsights'); + //check if another instance is already initialized + if (appInsights.defaultClient) { + appInsightsClient = new appInsights.TelemetryClient(AppinsightsKey); + // no other way to enable offline mode + appInsightsClient.channel.setUseDiskRetryCaching(true); + } else { + appInsights + .setup(AppinsightsKey) + .setAutoCollectRequests(false) + .setAutoCollectPerformance(false) + .setAutoCollectExceptions(false) + .setAutoCollectDependencies(false) + .setAutoDependencyCorrelation(false) + .setAutoCollectConsole(false) + .setAutoCollectHeartbeat(false) + .setUseDiskRetryCaching(true) + .start(); + appInsightsClient = appInsights.defaultClient; + } + if (vscode && vscode.env) { + appInsightsClient.context.tags[appInsightsClient.context.keys.userId] = vscode.env.machineId; + appInsightsClient.context.tags[appInsightsClient.context.keys.sessionId] = vscode.env.sessionId; + appInsightsClient.context.tags[appInsightsClient.context.keys.cloudRole] = vscode.env.appName; + appInsightsClient.context.tags[appInsightsClient.context.keys.cloudRoleInstance] = vscode.env.appName; + } + // change the endpoint for the Asimov key + + appInsightsClient.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; + } catch (e) { + return Promise.reject('Failed to initialize app insights!\n' + e.message); + } + + this.telemetryClient = appInsightsClient; + } + + public sendTelemetryEvent( + eventName: string, + properties: Record, + measures?: Record + ) { + let eventNameSent = 'ms-toolsai.jupyter/' + eventName + let allProperties = { ...this.getCommonProperties(), ...properties }; + try { + this.telemetryClient?.trackEvent({ + name: eventNameSent, + properties: allProperties, + measurements: measures + }); + } catch (e) { + throw new Error('Failed to log event to app insights!\n' + e.message); + } + } + + public async flush() { + await this.telemetryClient?.flush(); + } + + private getCommonProperties(): Record { + const commonProperties = Object.create(null); + commonProperties['common.os'] = os.platform(); + commonProperties['common.nodeArch'] = os.arch(); + commonProperties['common.platformversion'] = os.release().replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3'); + commonProperties['common.extname'] = this.extensionId; + commonProperties['common.extversion'] = this.extensionVersion; + if (vscode && vscode.env) { + commonProperties['common.vscodemachineid'] = vscode.env.machineId; + commonProperties['common.vscodesessionid'] = vscode.env.sessionId; + commonProperties['common.vscodeversion'] = vscode.version; + commonProperties['common.product'] = vscode.env.appHost; } - super( - extensionId, - extensionVersion, - appender, - { - release: os.release(), - platform: os.platform(), - architecture: os.arch() - }, - firstParty - ); + return commonProperties; } }