diff --git a/CHANGELOG.md b/CHANGELOG.md index a425fb90c29a..b3063ba7db68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,82 @@ # Changelog +## 2020.2.2 (18 February 2020) + +### Fixes + +1. Improve error messaging when the jupyter notebook cannot be started. + ([#9904](https://github.com/Microsoft/vscode-python/issues/9904)) +1. Clear variables in notebooks and interactive-window when restarting. + ([#9991](https://github.com/Microsoft/vscode-python/issues/9991)) +1. Re-install `Jupyter` instead of installing `kernelspec` if `kernelspec` cannot be found in the python environment. + ([#10071](https://github.com/Microsoft/vscode-python/issues/10071)) +1. Fixes problem with showing ndarrays in the data viewer. + ([#10074](https://github.com/Microsoft/vscode-python/issues/10074)) +1. Fix data viewer not opening on certain data frames. + ([#10075](https://github.com/Microsoft/vscode-python/issues/10075)) + +### Code Health + +1. Add telemetry to track notebook languages + ([#9819](https://github.com/Microsoft/vscode-python/issues/9819)) +1. Telemetry around kernels not working and installs not working. + ([#9883](https://github.com/Microsoft/vscode-python/issues/9883)) +1. Change select kernel telemetry to track duration till quick pick appears. + ([#10049](https://github.com/Microsoft/vscode-python/issues/10049)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + ## 2020.2.1 (12 February 2020) ### Fixes diff --git a/PYTHON_INTERACTIVE_TROUBLESHOOTING.md b/PYTHON_INTERACTIVE_TROUBLESHOOTING.md index c24ac2d0fe77..35e3f779e765 100644 --- a/PYTHON_INTERACTIVE_TROUBLESHOOTING.md +++ b/PYTHON_INTERACTIVE_TROUBLESHOOTING.md @@ -1,6 +1,6 @@ -# Trouble shooting the Python Interactive Window +# Troubleshooting Jupyter issues in the Python Interactive Window or Notebook Editor -This document is intended to help troubleshoot problems in the Python Interactive Window. +This document is intended to help troubleshoot problems with starting Jupyter in the Python Interactive Window or Notebook Editor. --- ## Jupyter Not Starting @@ -10,12 +10,21 @@ This error can happen when * Jupyter is not installed * You picked the wrong Python environment (one that doesn't have Jupyter installed). -### The first step is to verify you are running the Python environment you want. +### The first step is to verify you are running the Python environment that you have Jupyter installed into. -The Python you're using is picked with the selection dropdown on the bottom left of the VS Code window: +The first time that you start the Interactive Window or the Notebook Editor VS Code will attempt to locate a Python environment that has Jupyter installed in it and can start a notebook. + +The first Python interpreter to check will be the one selected with the selection dropdown on the bottom left of the VS Code window: ![selector](resources/PythonSelector.png) +Once a suitable interpreter with Jupyter has been located, VS Code will continue to use that interpreter for starting up Jupyter servers. +If no interpreters are found with Jupyter installed a popup message will ask if you would like to install Jupyter into the current interpreter. + +![install Jupyter](resources/InstallJupyter.png) + +If you would like to change from using the saved Python interpreter to a new interpreter for launching Jupyter just use the "Python: Select interpreter to start Jupyter server" VS Code command to change it. + ### The second step is to check that jupyter isn't giving any errors on startup. Run the following command from an environment that matches the Python you selected: diff --git a/package.json b/package.json index db5ba88ab73d..8e11ca5be90e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "python", "displayName": "Python", "description": "Linting, Debugging (multi-threaded, remote), Intellisense, Jupyter Notebooks, code formatting, refactoring, unit tests, snippets, and more.", - "version": "2020.2.1", + "version": "2020.2.2", "languageServerVersion": "0.5.30", "publisher": "ms-python", "author": { diff --git a/package.nls.json b/package.nls.json index a70fc4d71a7d..594eb1bbde3c 100644 --- a/package.nls.json +++ b/package.nls.json @@ -193,6 +193,8 @@ "DataScience.libraryNotInstalled": "Data Science library {0} is not installed. Install?", "DataScience.libraryRequiredToLaunchJupyterNotInstalled": "Data Science library {0} is not installed.", "DataScience.librariesRequiredToLaunchJupyterNotInstalled": "Data Science libraries {0} are not installed.", + "DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter": "Data Science library {1} is not installed in interpreter {0}.", + "DataScience.librariesRequiredToLaunchJupyterNotInstalledInterpreter": "Data Science libraries {1} are not installed in interpreter {0}.", "DataScience.jupyterInstall": "Install", "DataScience.jupyterSelectURIPrompt": "Enter the URI of the running Jupyter server", "DataScience.jupyterSelectURIInvalidURI": "Invalid URI specified", diff --git a/pythonFiles/datascience/getJupyterKernelspecVersion.py b/pythonFiles/datascience/getJupyterKernelspecVersion.py new file mode 100644 index 000000000000..1dbcd7694bfc --- /dev/null +++ b/pythonFiles/datascience/getJupyterKernelspecVersion.py @@ -0,0 +1,7 @@ +# Check whether kernelspec module exists. +import sys +import jupyter_client +import jupyter_client.kernelspec + +sys.stdout.write(jupyter_client.__version__) +sys.stdout.flush() diff --git a/resources/InstallJupyter.png b/resources/InstallJupyter.png new file mode 100644 index 000000000000..f36c380608f4 Binary files /dev/null and b/resources/InstallJupyter.png differ diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index d05a0ff7a0d1..7c07fd50dec8 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -5,6 +5,7 @@ import * as os from 'os'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import '../../common/extensions'; import * as localize from '../../common/utils/localize'; +import { Telemetry } from '../../datascience/constants'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { LinterId } from '../../linters/types'; @@ -18,6 +19,7 @@ import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/type import { ITerminalServiceFactory } from '../terminal/types'; import { IConfigurationService, IInstaller, InstallerResponse, IOutputChannel, IPersistentStateFactory, ModuleNamePurpose, Product, ProductType } from '../types'; import { isResource } from '../utils/misc'; +import { StopWatch } from '../utils/stopWatch'; import { ProductNames } from './productNames'; import { IInstallationChannelManager, InterpreterUri, IProductPathService, IProductService } from './types'; @@ -278,7 +280,21 @@ export class DataScienceInstaller extends BaseInstaller { protected async promptToInstallImplementation(product: Product, resource?: InterpreterUri, cancel?: CancellationToken): Promise { const productName = ProductNames.get(product)!; const item = await this.appShell.showErrorMessage(localize.DataScience.libraryNotInstalled().format(productName), 'Yes', 'No'); - return item === 'Yes' ? this.install(product, resource, cancel) : InstallerResponse.Ignore; + if (item === 'Yes') { + const stopWatch = new StopWatch(); + try { + const response = await this.install(product, resource, cancel); + const event = product === Product.jupyter ? Telemetry.UserInstalledJupyter : Telemetry.UserInstalledModule; + sendTelemetryEvent(event, stopWatch.elapsedTime, { product: productName }); + return response; + } catch (e) { + if (product === Product.jupyter) { + sendTelemetryEvent(Telemetry.JupyterInstallFailed); + } + throw e; + } + } + return InstallerResponse.Ignore; } } diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 1ac520a1a8e2..17e8143c5614 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -113,8 +113,8 @@ export interface IPythonExecutionFactory { create(options: ExecutionFactoryCreationOptions): Promise; /** * Creates a daemon Python Process. - * On windows its cheapter to create a daemon and use that than spin up Python Processes everytime. - * If something cannot be executed within the daemin, it will resort to using the stanard IPythonExecutionService. + * On windows it's cheaper to create a daemon and use that than spin up Python Processes everytime. + * If something cannot be executed within the daemon, it will resort to using the standard IPythonExecutionService. * Note: The returned execution service is always using an activated environment. * * @param {ExecutionFactoryCreationOptions} options diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 56d85b0211bc..020ef99f1c95 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -211,6 +211,14 @@ export namespace DataScience { 'DataScience.librariesRequiredToLaunchJupyterNotInstalled', 'Data Science libraries {0} are not installed.' ); + export const libraryRequiredToLaunchJupyterNotInstalledInterpreter = localize( + 'DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter', + 'Data Science library {1} is not installed in interpreter {0}.' + ); + export const librariesRequiredToLaunchJupyterNotInstalledInterpreter = localize( + 'DataScience.librariesRequiredToLaunchJupyterNotInstalledInterpreter', + 'Data Science libraries {1} are not installed in interpreter {0}.' + ); export const selectJupyterInterpreter = localize('DataScience.selectJupyterInterpreter', 'Select an Interpreter to start Jupyter'); export const jupyterInstall = localize('DataScience.jupyterInstall', 'Install'); export const currentlySelectedJupyterInterpreterForPlaceholder = localize('Datascience.currentlySelectedJupyterInterpreterForPlaceholder', 'current: {0}'); diff --git a/src/client/datascience/activation.ts b/src/client/datascience/activation.ts index 4ff5b35c8d7c..7e73b10dd1a3 100644 --- a/src/client/datascience/activation.ts +++ b/src/client/datascience/activation.ts @@ -28,6 +28,8 @@ export class Activation implements IExtensionSingleActivationService { public async activate(): Promise { this.disposables.push(this.notebookProvider.onDidOpenNotebookEditor(this.onDidOpenNotebookEditor, this)); this.disposables.push(this.jupyterInterpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter, this)); + // Warm up our selected interpreter for the extension + this.jupyterInterpreterService.setInitialInterpreter().ignoreErrors(); await this.contextService.activate(); } diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 4f59c614cf29..07f4e8959013 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -14,6 +14,9 @@ export const JUPYTER_OUTPUT_CHANNEL = 'JUPYTER_OUTPUT_CHANNEL'; // Python Module to be used when instantiating the Python Daemon. export const PythonDaemonModule = 'datascience.jupyter_daemon'; +// List of 'language' names that we know about. All should be lower case as that's how we compare. +export const KnownNotebookLanguages: string[] = ['python', 'r', 'julia', 'c++', 'c#', 'f#', 'scala', 'haskell', 'bash', 'cling', 'sas']; + export namespace Commands { export const RunAllCells = 'python.datascience.runallcells'; export const RunAllCellsAbove = 'python.datascience.runallcellsabove'; @@ -147,7 +150,7 @@ export enum Telemetry { CollapseAll = 'DATASCIENCE.COLLAPSE_ALL', SelectJupyterURI = 'DATASCIENCE.SELECT_JUPYTER_URI', SelectLocalJupyterKernel = 'DATASCIENCE.SELECT_LOCAL_JUPYTER_KERNEL', - SelectRemoteJupyuterKernel = 'DATASCIENCE.SELECT_REMOTE_JUPYTER_KERNEL', + SelectRemoteJupyterKernel = 'DATASCIENCE.SELECT_REMOTE_JUPYTER_KERNEL', SetJupyterURIToLocal = 'DATASCIENCE.SET_JUPYTER_URI_LOCAL', SetJupyterURIToUserSpecified = 'DATASCIENCE.SET_JUPYTER_URI_USER_SPECIFIED', Interrupt = 'DATASCIENCE.INTERRUPT', @@ -239,7 +242,14 @@ export enum Telemetry { OpenedInteractiveWindow = 'DATASCIENCE.OPENED_INTERACTIVE', FindKernelForLocalConnection = 'DS_INTERNAL.FIND_KERNEL_FOR_LOCAL_CONNECTION', CompletionTimeFromLS = 'DS_INTERNAL.COMPLETION_TIME_FROM_LS', - CompletionTimeFromJupyter = 'DS_INTERNAL.COMPLETION_TIME_FROM_JUPYTER' + CompletionTimeFromJupyter = 'DS_INTERNAL.COMPLETION_TIME_FROM_JUPYTER', + NotebookLanguage = 'DATASCIENCE.NOTEBOOK_LANGUAGE', + KernelSpecNotFound = 'DS_INTERNAL.KERNEL_SPEC_NOT_FOUND', + KernelRegisterFailed = 'DS_INTERNAL.KERNEL_REGISTER_FAILED', + KernelEnumeration = 'DS_INTERNAL.KERNEL_ENUMERATION', + JupyterInstallFailed = 'DS_INTERNAL.JUPYTER_INSTALL_FAILED', + UserInstalledModule = 'DATASCIENCE.USER_INSTALLED_MODULE', + JupyterCommandLineNonDefault = 'DS_INTERNAL.JUPYTER_CUSTOM_COMMAND_LINE' } export enum NativeKeyboardCommandTelemetry { diff --git a/src/client/datascience/interactive-ipynb/nativeEditor.ts b/src/client/datascience/interactive-ipynb/nativeEditor.ts index 79bf289b00cd..8cdb2480432d 100644 --- a/src/client/datascience/interactive-ipynb/nativeEditor.ts +++ b/src/client/datascience/interactive-ipynb/nativeEditor.ts @@ -24,7 +24,7 @@ import { StopWatch } from '../../common/utils/stopWatch'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { IInterpreterService } from '../../interpreter/contracts'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { EditorContexts, Identifiers, NativeKeyboardCommandTelemetryLookup, NativeMouseCommandTelemetryLookup, Telemetry } from '../constants'; +import { EditorContexts, Identifiers, KnownNotebookLanguages, NativeKeyboardCommandTelemetryLookup, NativeMouseCommandTelemetryLookup, Telemetry } from '../constants'; import { InteractiveBase } from '../interactive-common/interactiveBase'; import { IEditCell, @@ -327,7 +327,12 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { return this.setDirty(); } - protected addSysInfo(_reason: SysInfoReason): Promise { + protected addSysInfo(reason: SysInfoReason): Promise { + // We need to send a message when restarting + if (reason === SysInfoReason.Restart || reason === SysInfoReason.New) { + this.postMessage(InteractiveWindowMessages.RestartKernel).ignoreErrors(); + } + // These are not supported. return Promise.resolve(); } @@ -398,7 +403,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { } } - @captureTelemetry(Telemetry.ExecuteNativeCell, undefined, false) + @captureTelemetry(Telemetry.ExecuteNativeCell, undefined, true) // tslint:disable-next-line:no-any protected async reexecuteCell(info: ISubmitNewCell): Promise { try { @@ -588,6 +593,9 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { // Then save the contents. We'll stick our cells back into this format when we save if (json) { this.notebookJson = json; + + // Log language or kernel telemetry + this.sendLanguageTelemetry(this.notebookJson); } this.contentsLoadedPromise.resolve(); @@ -609,6 +617,27 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { ); } + private sendLanguageTelemetry(notebookJson: Partial) { + try { + // See if we have a language + let language = ''; + if (notebookJson.metadata?.language_info?.name) { + language = notebookJson.metadata?.language_info?.name; + } else if (notebookJson.metadata?.kernelspec?.language) { + language = notebookJson.metadata?.kernelspec?.language.toString(); + } + if (language && !KnownNotebookLanguages.includes(language.toLowerCase())) { + language = 'unknown'; + } + if (language) { + sendTelemetryEvent(Telemetry.NotebookLanguage, undefined, { language }); + } + } catch { + // If this fails, doesn't really matter + noop(); + } + } + private async loadCells(cells: ICell[], forceDirty: boolean): Promise { // Make sure cells have at least 1 if (cells.length === 0) { diff --git a/src/client/datascience/jupyter/interpreter/jupyterCommand.ts b/src/client/datascience/jupyter/interpreter/jupyterCommand.ts index ba393a1351a1..c81bf28bf1f2 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterCommand.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterCommand.ts @@ -201,31 +201,52 @@ export class InterpreterJupyterKernelSpecCommand extends InterpreterJupyterComma return output; } - // We're only interested in `python -m jupyter kernelspec list --json` - const interpreter = await this.interpreter(); - if ( - !interpreter || - this.moduleName.toLowerCase() !== 'jupyter' || - this.args.join(' ').toLowerCase() !== `-m jupyter ${JupyterCommands.KernelSpecCommand}`.toLowerCase() || - args.join(' ').toLowerCase() !== 'list --json' - ) { + const defaultAction = () => { if (exception) { + traceError(`Exception attempting to enumerate kernelspecs: `, exception); throw exception; } return output; + }; + + // We're only interested in `python -m jupyter kernelspec` + const interpreter = await this.interpreter(); + if (!interpreter || this.moduleName.toLowerCase() !== 'jupyter' || this.args.join(' ').toLowerCase() !== `-m jupyter ${JupyterCommands.KernelSpecCommand}`.toLowerCase()) { + return defaultAction(); } + + // Otherwise try running a script instead. try { - // Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception. - const activatedEnv = await this.pythonExecutionFactory.createActivatedEnvironment({ interpreter, bypassCondaExecution: true }); - return activatedEnv.exec([path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterKernels.py')], { ...options, throwOnStdErr: true }); + if (args.join(' ').toLowerCase() === 'list --json') { + // Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception. + output = await this.getKernelSpecList(interpreter, options); + return output; + } else if (args.join(' ').toLowerCase() === '--version') { + // Try getting kernelspec version using python script, if that fails (even if there's output in stderr) rethrow original exception. + output = await this.getKernelSpecVersion(interpreter, options); + return output; + } } catch (innerEx) { traceError('Failed to get a list of the kernelspec using python script', innerEx); - // Rethrow original exception. - if (exception) { - throw exception; - } - return output; } + return defaultAction(); + } + + private async getKernelSpecList(interpreter: PythonInterpreter, options: SpawnOptions) { + // Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception. + const activatedEnv = await this.pythonExecutionFactory.createActivatedEnvironment({ + interpreter, + bypassCondaExecution: true + }); + return activatedEnv.exec([path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterKernels.py')], { ...options, throwOnStdErr: true }); + } + private async getKernelSpecVersion(interpreter: PythonInterpreter, options: SpawnOptions) { + // Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception. + const activatedEnv = await this.pythonExecutionFactory.createActivatedEnvironment({ + interpreter, + bypassCondaExecution: true + }); + return activatedEnv.exec([path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterKernelspecVersion.py')], { ...options, throwOnStdErr: true }); } } diff --git a/src/client/datascience/jupyter/interpreter/jupyterCommandInterpreterDependencyService.ts b/src/client/datascience/jupyter/interpreter/jupyterCommandInterpreterDependencyService.ts index 94f73889bf50..02967215910c 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterCommandInterpreterDependencyService.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterCommandInterpreterDependencyService.ts @@ -9,6 +9,7 @@ import { ProductNames } from '../../../common/installer/productNames'; import { IInstallationChannelManager } from '../../../common/installer/types'; import { Product } from '../../../common/types'; import { DataScience } from '../../../common/utils/localize'; +import { StopWatch } from '../../../common/utils/stopWatch'; import { sendTelemetryEvent } from '../../../telemetry'; import { Telemetry } from '../../constants'; import { IJupyterInterpreterDependencyManager } from '../../types'; @@ -29,12 +30,28 @@ export class JupyterCommandInterpreterDependencyService implements IJupyterInter // If Conda is available, always pick it as the user must have a Conda Environment const installer = installers.find(ins => ins.name === 'Conda'); const product = ProductNames.get(Product.jupyter); + const stopWatch = new StopWatch(); if (installer && product) { - sendTelemetryEvent(Telemetry.UserInstalledJupyter); - installer.installModule(product).catch(e => this.applicationShell.showErrorMessage(e.message, DataScience.pythonInteractiveHelpLink())); + installer + .installModule(product) + .then(() => { + sendTelemetryEvent(Telemetry.UserInstalledJupyter, stopWatch.elapsedTime); + }) + .catch(e => { + sendTelemetryEvent(Telemetry.JupyterInstallFailed, undefined, { product }); + this.applicationShell.showErrorMessage(e.message, DataScience.pythonInteractiveHelpLink()); + }); } else if (installers[0] && product) { - installers[0].installModule(product).catch(e => this.applicationShell.showErrorMessage(e.message, DataScience.pythonInteractiveHelpLink())); + installers[0] + .installModule(product) + .then(() => { + sendTelemetryEvent(Telemetry.UserInstalledJupyter, stopWatch.elapsedTime); + }) + .catch(e => { + sendTelemetryEvent(Telemetry.JupyterInstallFailed, undefined, { product }); + this.applicationShell.showErrorMessage(e.message, DataScience.pythonInteractiveHelpLink()); + }); } } } else if (response === DataScience.notebookCheckForImportNo()) { diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts index 34ef6f01aaae..362238a869ad 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts @@ -8,13 +8,14 @@ import { CancellationToken } from 'vscode'; import { IApplicationShell } from '../../../common/application/types'; import { Cancellation, createPromiseFromCancellation, wrapCancellationTokens } from '../../../common/cancellation'; import { ProductNames } from '../../../common/installer/productNames'; -import { IPythonExecutionFactory } from '../../../common/process/types'; +import { traceError } from '../../../common/logger'; import { IInstaller, InstallerResponse, Product } from '../../../common/types'; import { Common, DataScience } from '../../../common/utils/localize'; import { noop } from '../../../common/utils/misc'; import { PythonInterpreter } from '../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../telemetry'; -import { Telemetry } from '../../constants'; +import { HelpLinks, JupyterCommands, Telemetry } from '../../constants'; +import { IJupyterCommandFactory } from '../../types'; import { JupyterInstallError } from '../jupyterInstallError'; export enum JupyterInterpreterDependencyResponse { @@ -56,7 +57,7 @@ function sortProductsInOrderForInstallation(products: Product[]) { * @param {Product[]} products * @returns {string} */ -export function getMessageForLibrariesNotInstalled(products: Product[]): string { +export function getMessageForLibrariesNotInstalled(products: Product[], interpreterName?: string): string { // Even though kernelspec cannot be installed, display it so user knows what is missing. const names = products .map(product => ProductNames.get(product)) @@ -67,10 +68,14 @@ export function getMessageForLibrariesNotInstalled(products: Product[]): string case 0: return ''; case 1: - return DataScience.libraryRequiredToLaunchJupyterNotInstalled().format(names[0]); + return interpreterName + ? DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format(interpreterName, names[0]) + : DataScience.libraryRequiredToLaunchJupyterNotInstalled().format(names[0]); default: { const lastItem = names.pop(); - return DataScience.librariesRequiredToLaunchJupyterNotInstalled().format(`${names.join(', ')} ${Common.and()} ${lastItem}`); + return interpreterName + ? DataScience.librariesRequiredToLaunchJupyterNotInstalledInterpreter().format(interpreterName, `${names.join(', ')} ${Common.and()} ${lastItem}`) + : DataScience.librariesRequiredToLaunchJupyterNotInstalled().format(`${names.join(', ')} ${Common.and()} ${lastItem}`); } } } @@ -104,7 +109,7 @@ export class JupyterInterpreterDependencyService { constructor( @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, @inject(IInstaller) private readonly installer: IInstaller, - @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory + @inject(IJupyterCommandFactory) private readonly commandFactory: IJupyterCommandFactory ) {} /** * Configures the python interpreter to ensure it can run Jupyter server by installing any missing dependencies. @@ -129,15 +134,14 @@ export class JupyterInterpreterDependencyService { return JupyterInterpreterDependencyResponse.ok; } - const message = getMessageForLibrariesNotInstalled(missingProducts); + const message = getMessageForLibrariesNotInstalled(missingProducts, interpreter.displayName); sendTelemetryEvent(Telemetry.JupyterNotInstalledErrorShown); const selection = await this.applicationShell.showErrorMessage( - // tslint:disable-next-line: messages-must-be-localized - `${message}\r\n${DataScience.markdownHelpInstallingMissingDependencies()}`, + message, DataScience.jupyterInstall(), DataScience.selectDifferentJupyterInterpreter(), - Common.cancel() + DataScience.pythonInteractiveHelpLink() ); if (Cancellation.isCanceled(token)) { @@ -178,6 +182,12 @@ export class JupyterInterpreterDependencyService { return JupyterInterpreterDependencyResponse.selectAnotherInterpreter; } + case DataScience.pythonInteractiveHelpLink(): { + this.applicationShell.openUrl(HelpLinks.PythonInteractiveHelpLink); + sendTelemetryEvent(Telemetry.UserDidNotInstallJupyter); + return JupyterInterpreterDependencyResponse.cancel; + } + default: sendTelemetryEvent(Telemetry.UserDidNotInstallJupyter); return JupyterInterpreterDependencyResponse.cancel; @@ -261,15 +271,16 @@ export class JupyterInterpreterDependencyService { * @returns {Promise} * @memberof JupyterInterpreterConfigurationService */ - private async isKernelSpecAvailable(interpreter: PythonInterpreter, token?: CancellationToken): Promise { - const execService = await this.pythonExecFactory.createActivatedEnvironment({ interpreter, allowEnvironmentFetchExceptions: true, bypassCondaExecution: true }); - if (Cancellation.isCanceled(token)) { - return false; - } - return execService - .execModule('jupyter', ['kernelspec', '--version'], { throwOnStdErr: true }) + private async isKernelSpecAvailable(interpreter: PythonInterpreter, _token?: CancellationToken): Promise { + const command = this.commandFactory.createInterpreterCommand(JupyterCommands.KernelSpecCommand, 'jupyter', ['-m', 'jupyter', 'kernelspec'], interpreter, false); + return command + .exec(['--version'], { throwOnStdErr: true }) .then(() => true) - .catch(() => false); + .catch(e => { + traceError(`Kernel spec not found: `, e); + sendTelemetryEvent(Telemetry.KernelSpecNotFound); + return false; + }); } /** @@ -286,9 +297,10 @@ export class JupyterInterpreterDependencyService { */ private async checkKernelSpecAvailability(interpreter: PythonInterpreter, token?: CancellationToken): Promise { if (await this.isKernelSpecAvailable(interpreter)) { - sendTelemetryEvent(Telemetry.JupyterInstalledButNotKernelSpecModule); return JupyterInterpreterDependencyResponse.ok; } + // Indicate no kernel spec module. + sendTelemetryEvent(Telemetry.JupyterInstalledButNotKernelSpecModule); if (Cancellation.isCanceled(token)) { return JupyterInterpreterDependencyResponse.cancel; } diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts index d503a70f3793..2a6fc4bfebfa 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts @@ -8,6 +8,7 @@ import { Event, EventEmitter } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; import { createPromiseFromCancellation } from '../../../common/cancellation'; import '../../../common/extensions'; +import { noop } from '../../../common/utils/misc'; import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../telemetry'; import { Telemetry } from '../../constants'; @@ -19,8 +20,8 @@ import { JupyterInterpreterStateStore } from './jupyterInterpreterStateStore'; @injectable() export class JupyterInterpreterService { private _selectedInterpreter?: PythonInterpreter; - private _selectedInterpreterPath?: string; private _onDidChangeInterpreter = new EventEmitter(); + private getInitialInterpreterPromise: Promise | undefined; public get onDidChangeInterpreter(): Event { return this._onDidChangeInterpreter.event; } @@ -40,40 +41,30 @@ export class JupyterInterpreterService { * @memberof JupyterInterpreterService */ public async getSelectedInterpreter(token?: CancellationToken): Promise { - if (this._selectedInterpreter) { - return this._selectedInterpreter; - } + // Before we return _selected interpreter make sure that we have run our initial set interpreter once + // because _selectedInterpreter can be changed by other function and at other times, this promise + // is cached to only run once + await this.setInitialInterpreter(token); - const resolveToUndefinedWhenCancelled = createPromiseFromCancellation({ cancelAction: 'resolve', defaultValue: undefined, token }); - // For backwards compatiblity check if we have a cached interpreter (older version of extension). - // If that interpreter has everything we need then use that. - let interpreter = await Promise.race([this.getInterpreterFromChangeOfOlderVersionOfExtension(), resolveToUndefinedWhenCancelled]); - if (interpreter) { - return interpreter; - } + return this._selectedInterpreter; + } - const pythonPath = this._selectedInterpreterPath || this.interpreterSelectionState.selectedPythonPath; - if (!pythonPath) { - // Check if current interpreter has all of the required dependencies. - // If yes, then use that. - interpreter = await this.interpreterService.getActiveInterpreter(undefined); - if (!interpreter) { - return; - } - // Use this interpreter going forward. - if (await this.interpreterConfiguration.areDependenciesInstalled(interpreter)) { - this.setAsSelectedInterpreter(interpreter); - return interpreter; - } - return; + // To be run one initial time. Check our saved locations and then current interpreter to try to start off + // with a valid jupyter interpreter + public async setInitialInterpreter(token?: CancellationToken): Promise { + if (!this.getInitialInterpreterPromise) { + this.getInitialInterpreterPromise = this.getInitialInterpreterImpl(token).then(result => { + // Set ourselves as a valid interpreter if we found something + if (result) { + this.changeSelectedInterpreterProperty(result); + } + return result; + }); } - const interpreterDetails = await Promise.race([this.interpreterService.getInterpreterDetails(pythonPath, undefined), resolveToUndefinedWhenCancelled]); - if (interpreterDetails) { - this._selectedInterpreter = interpreterDetails; - } - return interpreterDetails; + return this.getInitialInterpreterPromise; } + /** * Selects and interpreter to run jupyter server. * Validates and configures the interpreter. @@ -94,7 +85,7 @@ export class JupyterInterpreterService { const result = await this.interpreterConfiguration.installMissingDependencies(interpreter, undefined, token); switch (result) { case JupyterInterpreterDependencyResponse.ok: { - this.setAsSelectedInterpreter(interpreter); + await this.setAsSelectedInterpreter(interpreter); return interpreter; } case JupyterInterpreterDependencyResponse.cancel: @@ -104,30 +95,91 @@ export class JupyterInterpreterService { return this.selectInterpreter(token); } } - private async getInterpreterFromChangeOfOlderVersionOfExtension(): Promise { + + // Check the location that we stored jupyter launch path in the old version + // if it's there, return it and clear the location + private getInterpreterFromChangeOfOlderVersionOfExtension(): string | undefined { const pythonPath = this.oldVersionCacheStateStore.getCachedInterpreterPath(); if (!pythonPath) { return; } + + // Clear the cache to not check again + this.oldVersionCacheStateStore.clearCache().ignoreErrors(); + return pythonPath; + } + + // Set the specified interpreter as our current selected interpreter + private async setAsSelectedInterpreter(interpreter: PythonInterpreter): Promise { + // Make sure that our initial set has happened before we allow a set so that + // calculation of the initial interpreter doesn't clobber the existing one + await this.setInitialInterpreter(); + this.changeSelectedInterpreterProperty(interpreter); + } + + private changeSelectedInterpreterProperty(interpreter: PythonInterpreter) { + this._selectedInterpreter = interpreter; + this._onDidChangeInterpreter.fire(interpreter); + this.interpreterSelectionState.updateSelectedPythonPath(interpreter.path); + sendTelemetryEvent(Telemetry.SelectJupyterInterpreter, undefined, { result: 'selected' }); + } + + // For a given python path check if it can run jupyter for us + // if so, return the interpreter + private async validateInterpreterPath(pythonPath: string, token?: CancellationToken): Promise { try { - const interpreter = await this.interpreterService.getInterpreterDetails(pythonPath, undefined); + const resolveToUndefinedWhenCancelled = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: undefined, + token + }); + + // First see if we can get interpreter details + const interpreter = await Promise.race([this.interpreterService.getInterpreterDetails(pythonPath, undefined), resolveToUndefinedWhenCancelled]); + if (interpreter) { + // Then check that dependencies are installed + if (await this.interpreterConfiguration.areDependenciesInstalled(interpreter, token)) { + return interpreter; + } + } + } catch (_err) { + // For any errors we are ok with just returning undefined for an invalid interpreter + noop(); + } + return undefined; + } + + private async getInitialInterpreterImpl(token?: CancellationToken): Promise { + let interpreter: PythonInterpreter | undefined; + + // Check the old version location first, we will clear it if we find it here + const oldVersionPythonPath = this.getInterpreterFromChangeOfOlderVersionOfExtension(); + if (oldVersionPythonPath) { + interpreter = await this.validateInterpreterPath(oldVersionPythonPath, token); + } + + // Next check the saved global path + if (!interpreter && this.interpreterSelectionState.selectedPythonPath) { + interpreter = await this.validateInterpreterPath(this.interpreterSelectionState.selectedPythonPath, token); + + // If we had a global path, but it's not valid, trash it if (!interpreter) { - return; + this.interpreterSelectionState.updateSelectedPythonPath(undefined); } - if (await this.interpreterConfiguration.areDependenciesInstalled(interpreter)) { - this.setAsSelectedInterpreter(interpreter); - return interpreter; + } + + // Nothing saved found, so check our current interpreter + if (!interpreter) { + const currentInterpreter = await this.interpreterService.getActiveInterpreter(undefined); + + if (currentInterpreter) { + // Ask and give a chance to install dependencies in current interpreter + if (await this.interpreterConfiguration.areDependenciesInstalled(currentInterpreter, token)) { + interpreter = currentInterpreter; + } } - // If dependencies are not installed, then ignore it. lets continue with the current logic. - } finally { - // Don't perform this check again, just clear the cache. - this.oldVersionCacheStateStore.clearCache().ignoreErrors(); } - } - private setAsSelectedInterpreter(interpreter: PythonInterpreter): void { - this._selectedInterpreter = interpreter; - this._onDidChangeInterpreter.fire(interpreter); - this.interpreterSelectionState.updateSelectedPythonPath((this._selectedInterpreterPath = interpreter.path)); - sendTelemetryEvent(Telemetry.SelectJupyterInterpreter, undefined, { result: 'selected' }); + + return interpreter; } } diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts index d4e3585b2c5c..3fecb01848db 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts @@ -11,11 +11,11 @@ import { noop } from '../../../common/utils/misc'; const key = 'INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER'; const keySelected = 'INTERPRETER_PATH_WAS_SELECTED_FOR_JUPYTER_SERVER'; /** - * Keeps track of whether the user ever selected an interpreter to be used as the gloabl jupyter interpreter. + * Keeps track of whether the user ever selected an interpreter to be used as the global jupyter interpreter. * Keeps track of the interpreter path of the interpreter used as the global jupyter interpreter. * * @export - * @class JupyterInterpreterFinderEverSet + * @class JupyterInterpreterStateStore */ @injectable() export class JupyterInterpreterStateStore { @@ -27,7 +27,6 @@ export class JupyterInterpreterStateStore { * * @readonly * @type {Promise} - * @memberof JupyterInterpreterFinderEverSet */ public get interpreterSetAtleastOnce(): boolean { return !!this.selectedPythonPath || this.memento.get(keySelected, false); @@ -35,7 +34,7 @@ export class JupyterInterpreterStateStore { public get selectedPythonPath(): string | undefined { return this._interpreterPath || this.memento.get(key, undefined); } - public updateSelectedPythonPath(value: string) { + public updateSelectedPythonPath(value: string | undefined) { this._interpreterPath = value; this.memento.update(key, value).then(noop, noop); this.memento.update(keySelected, true).then(noop, noop); diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts index 13c91dc2670c..6596e5189dc8 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts @@ -15,7 +15,8 @@ import { DataScience } from '../../../common/utils/localize'; import { noop } from '../../../common/utils/misc'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts'; -import { JUPYTER_OUTPUT_CHANNEL, PythonDaemonModule } from '../../constants'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { JUPYTER_OUTPUT_CHANNEL, PythonDaemonModule, Telemetry } from '../../constants'; import { IJupyterInterpreterDependencyManager, IJupyterSubCommandExecutionService } from '../../types'; import { JupyterServerInfo } from '../jupyterConnection'; import { JupyterInstallError } from '../jupyterInstallError'; @@ -85,7 +86,7 @@ export class JupyterInterpreterSubCommandExecutionService implements IJupyterSub return DataScience.jupyterKernelSpecModuleNotFound(); } - return getMessageForLibrariesNotInstalled(productsNotInstalled); + return getMessageForLibrariesNotInstalled(productsNotInstalled, interpreter.displayName); } public async getSelectedInterpreter(token?: CancellationToken): Promise { return this.jupyterInterpreter.getSelectedInterpreter(token); @@ -153,6 +154,7 @@ export class JupyterInterpreterSubCommandExecutionService implements IJupyterSub .execModule('jupyter', ['kernelspec', 'list', '--json'], spawnOptions) .then(output => output.stdout) .catch(daemonEx => { + sendTelemetryEvent(Telemetry.KernelSpecNotFound); traceError('Failed to list kernels from daemon', daemonEx); return ''; }); diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index 5a7ad184f48a..3fbe3fdf1cd3 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -192,7 +192,7 @@ export class JupyterExecutionBase implements IJupyterExecution { if (selection === selectKernel) { const sessionManagerFactory = this.serviceContainer.get(IJupyterSessionManagerFactory); const sessionManager = await sessionManagerFactory.create(connection); - const kernelInterpreter = await this.kernelSelector.selectLocalKernel(sessionManager, cancelToken, launchInfo.kernelSpec); + const kernelInterpreter = await this.kernelSelector.selectLocalKernel(new StopWatch(), sessionManager, cancelToken, launchInfo.kernelSpec); if (Object.keys(kernelInterpreter).length > 0) { launchInfo.interpreter = kernelInterpreter.interpreter; launchInfo.kernelSpec = kernelInterpreter.kernelSpec || kernelInterpreter.kernelModel; diff --git a/src/client/datascience/jupyter/jupyterVariables.ts b/src/client/datascience/jupyter/jupyterVariables.ts index cff731c78e20..b4b86d7a968e 100644 --- a/src/client/datascience/jupyter/jupyterVariables.ts +++ b/src/client/datascience/jupyter/jupyterVariables.ts @@ -30,7 +30,7 @@ const DocStringRegex = /.*?\[.*?;31mDocstring:.*?\[0m\s+(.*)/; const CountRegex = /.*?\[.*?;31mLength:.*?\[0m\s+(.*)/; const ShapeRegex = /^\s+\[(\d+) rows x (\d+) columns\]/m; -const DataViewableTypes: Set = new Set(['DataFrame', 'list', 'dict', 'np.array', 'Series']); +const DataViewableTypes: Set = new Set(['DataFrame', 'list', 'dict', 'ndarray', 'Series']); interface INotebookState { currentExecutionCount: number; @@ -98,8 +98,9 @@ export class JupyterVariables implements IJupyterVariables { return defaultValue; } - // Prep our targetVariable to send over - const variableString = JSON.stringify(targetVariable).replace(/\\n/g, '\\\\n'); + // Prep our targetVariable to send over. Remove the 'value' as it's not necessary for getting df info and can have invalid data in it + const pruned = { ...targetVariable, value: '' }; + const variableString = JSON.stringify(pruned); // Setup a regex const regexPattern = extraReplacements.length === 0 ? '_VSCode_JupyterTestValue' : ['_VSCode_JupyterTestValue', ...extraReplacements.map(v => v.key)].join('|'); diff --git a/src/client/datascience/jupyter/kernels/kernelSelector.ts b/src/client/datascience/jupyter/kernels/kernelSelector.ts index d91b9427b48e..43457168c804 100644 --- a/src/client/datascience/jupyter/kernels/kernelSelector.ts +++ b/src/client/datascience/jupyter/kernels/kernelSelector.ts @@ -15,7 +15,7 @@ import { noop } from '../../../common/utils/misc'; import { StopWatch } from '../../../common/utils/stopWatch'; import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts'; import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../../telemetry'; -import { Telemetry } from '../../constants'; +import { KnownNotebookLanguages, Telemetry } from '../../constants'; import { reportAction } from '../../progress/decorator'; import { ReportableAction } from '../../progress/types'; import { IJupyterKernelSpec, IJupyterSessionManager } from '../../types'; @@ -90,13 +90,14 @@ export class KernelSelector { * @memberof KernelSelector */ public async selectRemoteKernel( + stopWatch: StopWatch, session: IJupyterSessionManager, cancelToken?: CancellationToken, currentKernel?: IJupyterKernelSpec | LiveKernelModel ): Promise { let suggestions = await this.selectionProvider.getKernelSelectionsForRemoteSession(session, cancelToken); suggestions = suggestions.filter(item => !this.kernelIdsToHide.has(item.selection.kernelModel?.id || '')); - return this.selectKernel(suggestions, session, cancelToken, currentKernel); + return this.selectKernel(stopWatch, Telemetry.SelectRemoteJupyterKernel, suggestions, session, cancelToken, currentKernel); } /** * Select a kernel from a local session. @@ -107,13 +108,14 @@ export class KernelSelector { * @memberof KernelSelector */ public async selectLocalKernel( + stopWatch: StopWatch, session?: IJupyterSessionManager, cancelToken?: CancellationToken, currentKernel?: IJupyterKernelSpec | LiveKernelModel ): Promise { let suggestions = await this.selectionProvider.getKernelSelectionsForLocalSession(session, cancelToken); suggestions = suggestions.filter(item => !this.kernelIdsToHide.has(item.selection.kernelModel?.id || '')); - return this.selectKernel(suggestions, session, cancelToken, currentKernel); + return this.selectKernel(stopWatch, Telemetry.SelectLocalJupyterKernel, suggestions, session, cancelToken, currentKernel); } /** * Gets a kernel that needs to be used with a local session. @@ -151,7 +153,7 @@ export class KernelSelector { selection = await this.useInterpreterAsKernel(activeInterpreter, notebookMetadata.kernelspec.display_name, sessionManager, disableUI, cancelToken); } else { telemetryProps.promptedToSelect = true; - selection = await this.selectLocalKernel(sessionManager, cancelToken); + selection = await this.selectLocalKernel(stopWatch, sessionManager, cancelToken); } } } else { @@ -239,12 +241,15 @@ export class KernelSelector { }; } private async selectKernel( + stopWatch: StopWatch, + telemetryEvent: Telemetry, suggestions: IKernelSpecQuickPickItem[], session?: IJupyterSessionManager, cancelToken?: CancellationToken, currentKernel?: IJupyterKernelSpec | LiveKernelModel ) { const placeHolder = localize.DataScience.selectKernel() + (currentKernel ? ` (current: ${currentKernel.display_name || currentKernel.name})` : ''); + sendTelemetryEvent(telemetryEvent, stopWatch.elapsedTime); const selection = await this.applicationShell.showQuickPick(suggestions, { placeHolder }, cancelToken); if (!selection?.selection) { return {}; @@ -254,12 +259,16 @@ export class KernelSelector { sendTelemetryEvent(Telemetry.SwitchToInterpreterAsKernel); return this.useInterpreterAsKernel(selection.selection.interpreter, undefined, session, false, cancelToken); } else if (selection.selection.kernelModel) { - sendTelemetryEvent(Telemetry.SwitchToExistingKernel); + sendTelemetryEvent(Telemetry.SwitchToExistingKernel, undefined, { + language: this.computeLanguage(selection.selection.kernelModel.language) + }); // tslint:disable-next-line: no-any const interpreter = selection.selection.kernelModel ? await this.kernelService.findMatchingInterpreter(selection.selection.kernelModel, cancelToken) : undefined; return { kernelSpec: selection.selection.kernelSpec, interpreter, kernelModel: selection.selection.kernelModel }; } else if (selection.selection.kernelSpec) { - sendTelemetryEvent(Telemetry.SwitchToExistingKernel); + sendTelemetryEvent(Telemetry.SwitchToExistingKernel, undefined, { + language: this.computeLanguage(selection.selection.kernelSpec.language) + }); const interpreter = selection.selection.kernelSpec ? await this.kernelService.findMatchingInterpreter(selection.selection.kernelSpec, cancelToken) : undefined; return { kernelSpec: selection.selection.kernelSpec, interpreter }; } else { @@ -308,7 +317,12 @@ export class KernelSelector { } // Try an install this interpreter as a kernel. - kernelSpec = await this.kernelService.registerKernel(interpreter, disableUI, cancelToken); + try { + kernelSpec = await this.kernelService.registerKernel(interpreter, disableUI, cancelToken); + } catch (e) { + sendTelemetryEvent(Telemetry.KernelRegisterFailed); + throw e; + } // If we have a display name of a kernel that could not be found, // then notify user that we're using current interpreter instead. @@ -324,4 +338,11 @@ export class KernelSelector { return { kernelSpec, interpreter }; } + + private computeLanguage(language: string | undefined): string { + if (language && KnownNotebookLanguages.includes(language.toLowerCase())) { + return language; + } + return 'unknown'; + } } diff --git a/src/client/datascience/jupyter/kernels/kernelService.ts b/src/client/datascience/jupyter/kernels/kernelService.ts index a56e7f8fdf50..aee36b7050f9 100644 --- a/src/client/datascience/jupyter/kernels/kernelService.ts +++ b/src/client/datascience/jupyter/kernels/kernelService.ts @@ -354,7 +354,17 @@ export class KernelService { return []; } const specs: IJupyterKernelSpec[] = await enumerator; - return specs.filter(item => !!item); + const result = specs.filter(item => !!item); + + // Send telemetry on this enumeration. + const anyPython = result.find(k => k.language === 'python') !== undefined; + sendTelemetryEvent(Telemetry.KernelEnumeration, undefined, { + count: result.length, + isPython: anyPython, + source: sessionManager ? 'connection' : 'cli' + }); + + return result; } /** * Not all characters are allowed in a kernel name. diff --git a/src/client/datascience/jupyter/kernels/kernelSwitcher.ts b/src/client/datascience/jupyter/kernels/kernelSwitcher.ts index 177d05a49323..7f2527e26f98 100644 --- a/src/client/datascience/jupyter/kernels/kernelSwitcher.ts +++ b/src/client/datascience/jupyter/kernels/kernelSwitcher.ts @@ -8,8 +8,8 @@ import { ProgressLocation, ProgressOptions } from 'vscode'; import { IApplicationShell } from '../../../common/application/types'; import { IConfigurationService } from '../../../common/types'; import { Common, DataScience } from '../../../common/utils/localize'; -import { captureTelemetry } from '../../../telemetry'; -import { Commands, Settings, Telemetry } from '../../constants'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { Commands, Settings } from '../../constants'; import { IConnection, IJupyterKernelSpec, IJupyterSessionManagerFactory, INotebook } from '../../types'; import { JupyterSessionStartError } from '../jupyterSession'; import { KernelSelector, KernelSpecInterpreter } from './kernelSelector'; @@ -49,15 +49,14 @@ export class KernelSwitcher { return kernel; } - @captureTelemetry(Telemetry.SelectLocalJupyterKernel) private async selectLocalJupyterKernel(currentKernel?: IJupyterKernelSpec | LiveKernelModel): Promise { - return this.kernelSelector.selectLocalKernel(undefined, undefined, currentKernel); + return this.kernelSelector.selectLocalKernel(new StopWatch(), undefined, undefined, currentKernel); } - @captureTelemetry(Telemetry.SelectRemoteJupyuterKernel) private async selectRemoteJupyterKernel(connInfo: IConnection, currentKernel?: IJupyterKernelSpec | LiveKernelModel): Promise { + const stopWatch = new StopWatch(); const session = await this.jupyterSessionManagerFactory.create(connInfo); - return this.kernelSelector.selectRemoteKernel(session, undefined, currentKernel); + return this.kernelSelector.selectRemoteKernel(stopWatch, session, undefined, currentKernel); } private async switchKernelWithRetry(notebook: INotebook, kernel: KernelSpecInterpreter): Promise { const settings = this.configService.getSettings(); diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index fbe9fca4b98b..4d8ae0be241c 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1441,7 +1441,7 @@ export interface IEventNamePropertyMapping { [Telemetry.RegisterAndUseInterpreterAsKernel]: never | undefined; [Telemetry.UseInterpreterAsKernel]: never | undefined; [Telemetry.UseExistingKernel]: never | undefined; - [Telemetry.SwitchToExistingKernel]: never | undefined; + [Telemetry.SwitchToExistingKernel]: { language: string }; [Telemetry.SwitchToInterpreterAsKernel]: never | undefined; [Telemetry.ConvertToPythonFile]: never | undefined; [Telemetry.CopySourceCode]: never | undefined; @@ -1510,7 +1510,7 @@ export interface IEventNamePropertyMapping { [Telemetry.SelfCertsMessageEnabled]: never | undefined; [Telemetry.SelectJupyterURI]: never | undefined; [Telemetry.SelectLocalJupyterKernel]: never | undefined; - [Telemetry.SelectRemoteJupyuterKernel]: never | undefined; + [Telemetry.SelectRemoteJupyterKernel]: never | undefined; [Telemetry.SessionIdleTimeout]: never | undefined; [Telemetry.JupyterNotInstalledErrorShown]: never | undefined; [Telemetry.JupyterCommandSearch]: { where: 'activeInterpreter' | 'otherInterpreter' | 'path' | 'nowhere'; command: JupyterCommands }; @@ -1722,4 +1722,75 @@ export interface IEventNamePropertyMapping { * @memberof IEventNamePropertyMapping */ [Telemetry.CompletionTimeFromJupyter]: undefined | never; + /** + * Telemetry event sent to indicate the language used in a notebook + * + * @type { language: string } + * @memberof IEventNamePropertyMapping + */ + [Telemetry.NotebookLanguage]: { + /** + * Language found in the notebook if a known language. Otherwise 'unknown' + */ + language: string; + }; + /** + * Telemetry event sent to indicate 'jupyter kernelspec' is not possible. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.KernelSpecNotFound]: undefined | never; + /** + * Telemetry event sent to indicate registering a kernel with jupyter failed. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.KernelRegisterFailed]: undefined | never; + /** + * Telemetry event sent to every time a kernel enumeration is done + * + * @type {...} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.KernelEnumeration]: { + /** + * Count of the number of kernels found + */ + count: number; + /** + * Boolean indicating if any are python or not + */ + isPython: boolean; + /** + * Indicates how the enumeration was acquired. + */ + source: 'cli' | 'connection'; + }; + /** + * Telemetry event sent if there's an error installing a jupyter required dependency + * + * @type { product: string } + * @memberof IEventNamePropertyMapping + */ + [Telemetry.JupyterInstallFailed]: { + /** + * Product being installed (jupyter or ipykernel or other) + */ + product: string; + }; + /** + * Telemetry event sent when installing a jupyter dependency + * + * @type {product: string} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.UserInstalledModule]: { product: string }; + /** + * Telemetry event sent to when user customizes the jupyter command line + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.JupyterCommandLineNonDefault]: undefined | never; } diff --git a/src/datascience-ui/history-react/interactivePanel.tsx b/src/datascience-ui/history-react/interactivePanel.tsx index 6c7aa04b3f5f..3eccd238d084 100644 --- a/src/datascience-ui/history-react/interactivePanel.tsx +++ b/src/datascience-ui/history-react/interactivePanel.tsx @@ -3,6 +3,7 @@ 'use strict'; import * as React from 'react'; import { connect } from 'react-redux'; +import { noop } from '../../client/common/utils/misc'; import { Identifiers } from '../../client/datascience/constants'; import { ContentPanel, IContentPanelProps } from '../interactive-common/contentPanel'; import { handleLinkClick } from '../interactive-common/handlers'; @@ -75,13 +76,20 @@ export class InteractivePanel extends React.Component {
{this.renderContentPanel(this.props.baseTheme)}
-