diff --git a/src/gdpr.ts b/src/gdpr.ts index 0e9eaf1a217..7efe7c6790a 100644 --- a/src/gdpr.ts +++ b/src/gdpr.ts @@ -398,6 +398,17 @@ "${include}": [ "${F1}" + ] + } + */ +//Telemetry.ExtensionCallerIdentification +/* __GDPR__ + "DATASCIENCE.JUPYTER_EXTENSION_CALLER_IDENTIFICATION" : { + "extensionId": {"classification":"PublicNonPersonalData","purpose":"FeatureInsight","comment":"Extension Id that's attempting to use the API.","owner":"donjayamanne"}, + "result": {"classification":"PublicNonPersonalData","purpose":"FeatureInsight","comment":"","owner":"donjayamanne"}, + "${include}": [ + "${F1}" + ] } */ diff --git a/src/platform/common/application/extensions.node.ts b/src/platform/common/application/extensions.node.ts index 5f88d3ab8e5..91c13aca584 100644 --- a/src/platform/common/application/extensions.node.ts +++ b/src/platform/common/application/extensions.node.ts @@ -9,8 +9,9 @@ import { DataScience } from '../utils/localize'; import { EXTENSION_ROOT_DIR } from '../../constants.node'; import { IFileSystem } from '../platform/types'; import { parseStack } from '../../errors'; -import { unknownExtensionId } from '../constants'; +import { JVSC_EXTENSION_ID, Telemetry, unknownExtensionId } from '../constants'; import { traceError } from '../../logging'; +import { sendTelemetryEvent } from '../../telemetry'; /** * Provides functions for tracking the list of extensions that VS code has installed (besides our own) @@ -33,6 +34,7 @@ export class Extensions implements IExtensions { } public async determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { const stack = new Error().stack; + const syncId = this.determineExtensionFromCallStackSync(stack || ''); if (stack) { const jupyterExtRoot = path.join(EXTENSION_ROOT_DIR.toLowerCase(), path.sep); const frames = stack @@ -76,6 +78,23 @@ export class Extensions implements IExtensions { (frame.startsWith(matchingExt.extensionUri.toString()) || frame.startsWith(matchingExt.extensionUri.fsPath.toString())) ) { + if ( + syncId.extensionId === unknownExtensionId && + matchingExt.id !== unknownExtensionId + ) { + sendTelemetryEvent(Telemetry.ExtensionCallerIdentification, undefined, { + extensionId: matchingExt.id, + result: 'WorkedOnlyInAsync' + }); + } else if ( + syncId.extensionId === unknownExtensionId && + syncId.extensionId === matchingExt.id + ) { + sendTelemetryEvent(Telemetry.ExtensionCallerIdentification, undefined, { + extensionId: matchingExt.id, + result: 'WorkedInBoth' + }); + } return { extensionId: matchingExt.id, displayName: matchingExt.packageJSON.displayName @@ -90,8 +109,50 @@ export class Extensions implements IExtensions { dirName = path.dirname(dirName); } } + if (syncId.extensionId !== unknownExtensionId) { + sendTelemetryEvent(Telemetry.ExtensionCallerIdentification, undefined, { + extensionId: syncId.extensionId, + result: 'WorkedOnlyInSync' + }); + } traceError(`Unable to determine the caller of the extension API for trace stack.`, stack); } return { extensionId: unknownExtensionId, displayName: DataScience.unknownPackage }; } + private determineExtensionFromCallStackSync(stack: string): { extensionId: string; displayName: string } { + try { + if (stack) { + const jupyterExtRoot = this.getExtension(JVSC_EXTENSION_ID)!.extensionUri.toString().toLowerCase(); + const frames = stack + .split('\n') + .map((f) => { + const result = /\((.*)\)/.exec(f); + if (result) { + return result[1]; + } + }) + // Since this is web, look for paths that start with http (which also includes https). + .filter((item) => item && item.toLowerCase().startsWith('http')) + .filter((item) => item && !item.toLowerCase().startsWith(jupyterExtRoot)) as string[]; + parseStack(new Error('Ex')).forEach((item) => { + const fileName = item.getFileName(); + if (fileName && !fileName.toLowerCase().startsWith(jupyterExtRoot)) { + frames.push(fileName); + } + }); + for (const frame of frames) { + const matchingExt = this.all.find( + (ext) => ext.id !== JVSC_EXTENSION_ID && frame.startsWith(ext.extensionUri.toString()) + ); + if (matchingExt) { + return { extensionId: matchingExt.id, displayName: matchingExt.packageJSON.displayName }; + } + } + } + return { extensionId: unknownExtensionId, displayName: DataScience.unknownPackage }; + } catch { + return { extensionId: unknownExtensionId, displayName: DataScience.unknownPackage }; + // + } + } } diff --git a/src/platform/common/constants.ts b/src/platform/common/constants.ts index d85edaf317b..2841abcfa0f 100644 --- a/src/platform/common/constants.ts +++ b/src/platform/common/constants.ts @@ -411,6 +411,7 @@ export enum Telemetry { CellOutputMimeType = 'DS_INTERNAL.CELL_OUTPUT_MIME_TYPE', JupyterApiUsage = 'DATASCIENCE.JUPYTER_API_USAGE', JupyterServerProviderResponseApi = 'DATASCIENCE.JUPYTER_SERVER_PROVIDER_RESPONSE_API', + ExtensionCallerIdentification = 'DATASCIENCE.JUPYTER_EXTENSION_CALLER_IDENTIFICATION', JupyterKernelApiUsage = 'DATASCIENCE.JUPYTER_KERNEL_API_USAGE', JupyterKernelApiAccess = 'DATASCIENCE.JUPYTER_KERNEL_API_ACCESS', JupyterKernelStartupHook = 'DATASCIENCE.JUPYTER_KERNEL_STARTUP_HOOK', diff --git a/src/telemetry.ts b/src/telemetry.ts index 1deba8fcac5..945be231f76 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -3523,6 +3523,30 @@ export class IEventNamePropertyMapping { } } }; + /** + * Telemetry sent when an extension uses our 3rd party Kernel API. + */ + [Telemetry.ExtensionCallerIdentification]: TelemetryEventInfo<{ + /** + * Extension Id that's attempting to use the API. + */ + extensionId: string; + result: 'WorkedInBoth' | 'WorkedOnlyInSync' | 'WorkedOnlyInAsync'; + }> = { + owner: 'donjayamanne', + feature: 'N/A', + source: 'N/A', + properties: { + extensionId: { + classification: 'PublicNonPersonalData', + purpose: 'FeatureInsight' + }, + result: { + classification: 'PublicNonPersonalData', + purpose: 'FeatureInsight' + } + } + }; /** * Telemetry sent when an extension uses our 3rd party Kernel API. */