diff --git a/package.json b/package.json index 16da1270..e9ef5f07 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "version": "2024.3.0-dev", "publisher": "ms-python", "enabledApiProposals": [ - "portsAttributes" + "portsAttributes", + "contribIssueReporter" ], "license": "MIT", "homepage": "https://github.com/Microsoft/vscode-python-debugger", @@ -63,9 +64,19 @@ "light": "resources/light/repl.svg" }, "title": "%debugpy.command.viewOutput.title%" + }, + { + "category": "Python Debugger", + "command": "debugpy.reportIssue", + "title": "%debugpy.command.reportIssue.title%" } ], "menus": { + "issue/reporter": [ + { + "command": "debugpy.reportIssue" + } + ], "commandPalette": [ { "category": "Python Debugger", @@ -90,6 +101,12 @@ "category": "Python Debugger", "command": "debugpy.viewOutput", "title": "%debugpy.command.viewOutput.title%" + }, + { + "category": "Python Debugger", + "command": "debugpy.reportIssue", + "title": "%debugpy.command.reportIssue.title%", + "when": "!virtualWorkspace && shellExecutionSupported" } ], "editor/title/run": [ diff --git a/package.nls.json b/package.nls.json index bb0904d6..2be4c634 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,7 +1,8 @@ { + "debugpy.command.clearCacheAndReload.title": "Clear Cache and Reload Window", "debugpy.command.debugInTerminal.title": "Python Debugger: Debug Python File", "debugpy.command.debugUsingLaunchConfig.title": "Python Debugger: Debug using launch.json", - "debugpy.command.clearCacheAndReload.title": "Clear Cache and Reload Window", + "debugpy.command.reportIssue.title": "Report Issue...", "debugpy.command.viewOutput.title": "Show Output", "debugpy.debugJustMyCode": "When debugging only step through user-written code. Disable this to allow stepping into library code." } diff --git a/resources/report_issue_template.md b/resources/report_issue_template.md new file mode 100644 index 00000000..81f42a52 --- /dev/null +++ b/resources/report_issue_template.md @@ -0,0 +1,56 @@ + +# Behaviour + +XXX + +## Steps to reproduce: + +1. XXX + + + + +# Diagnostic data +
+ +launch.json configuration + + +

+ +``` +XXX +``` + +

+
+ +
+ +Output for Python in the Output panel (ViewOutput, change the drop-down the upper-right of the Output panel to Python) + + +

+ +``` +XXX +``` + +

+
+ +
+ +Output for Python Debugger in the Output panel (ViewOutput, change the drop-down the upper-right of the Output panel to Python Debugger) + + +

+ +``` +XXX +``` + +

+
\ No newline at end of file diff --git a/resources/report_issue_user_data_template.md b/resources/report_issue_user_data_template.md new file mode 100644 index 00000000..c5941903 --- /dev/null +++ b/resources/report_issue_user_data_template.md @@ -0,0 +1,2 @@ +- Python version (& distribution if applicable, e.g. Anaconda): {0} +- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): {1} \ No newline at end of file diff --git a/src/extension/common/application/commands/reportIssueCommand.ts b/src/extension/common/application/commands/reportIssueCommand.ts new file mode 100644 index 00000000..104b5225 --- /dev/null +++ b/src/extension/common/application/commands/reportIssueCommand.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { executeCommand } from '../../vscodeapi'; +import { getActiveEnvironmentPath, resolveEnvironment } from '../../python'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { getRawVersion } from '../../settings'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; + +/** + * Allows the user to report an issue related to the Python Debugger extension using our template. + */ +export async function openReportIssue(): Promise { + const templatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_template.md'); + const userDataTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md'); + const template = await fs.readFile(templatePath, 'utf8'); + const userTemplate = await fs.readFile(userDataTemplatePath, 'utf8'); + const interpreterPath = await getActiveEnvironmentPath(); + const interpreter = await resolveEnvironment(interpreterPath); + const virtualEnvKind = interpreter?.environment?.type || 'Unknown'; + + const pythonVersion = getRawVersion(interpreter?.version); + await executeCommand('workbench.action.openIssueReporter', { + extensionId: 'ms-python.debugpy', + issueBody: template, + data: userTemplate.replace('{0}', pythonVersion).replace('{1}', virtualEnvKind), + }); + sendTelemetryEvent(EventName.USE_REPORT_ISSUE_COMMAND, undefined, {}); +} diff --git a/src/extension/common/constants.ts b/src/extension/common/constants.ts index dd6c219e..4d532a28 100644 --- a/src/extension/common/constants.ts +++ b/src/extension/common/constants.ts @@ -7,7 +7,7 @@ import * as path from 'path'; export const PYTHON_LANGUAGE = 'python'; const folderName = path.basename(__dirname); export const EXTENSION_ROOT_DIR = - folderName === 'common' ? path.dirname(path.dirname(__dirname)) : path.dirname(__dirname); + folderName === 'common' ? path.dirname(path.dirname(path.dirname(__dirname))) : path.dirname(__dirname); export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'bundled'); export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `server.py`); export const DEBUG_SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `_debug_server.py`); @@ -39,6 +39,7 @@ export namespace Commands { export const Enable_SourceMap_Support = 'debugpy.enableSourceMapSupport'; export const SelectDebugConfig = 'debugpy.SelectAndInsertDebugConfiguration'; export const Set_Interpreter = 'python.setInterpreter'; + export const ReportIssue = 'debugpy.reportIssue'; } export type Channel = 'stable' | 'insiders'; diff --git a/src/extension/common/settings.ts b/src/extension/common/settings.ts index eed48817..9239a4f8 100644 --- a/src/extension/common/settings.ts +++ b/src/extension/common/settings.ts @@ -5,6 +5,7 @@ import { ConfigurationChangeEvent, ConfigurationTarget, Uri, WorkspaceConfigurat import { getInterpreterDetails } from './python'; import { getConfiguration, getWorkspaceFolder, getWorkspaceFolders } from './vscodeapi'; import { isUnitTestExecution } from './constants'; +import { VersionInfo } from '@vscode/python-extension'; export interface ISettings { workspace: string; @@ -15,7 +16,6 @@ export interface ISettings { export async function getExtensionSettings(namespace: string, includeInterpreter?: boolean): Promise { const settings: ISettings[] = []; const workspaces = getWorkspaceFolders(); - for (const workspace of workspaces) { const workspaceSetting = await getWorkspaceSettings(namespace, workspace, includeInterpreter); settings.push(workspaceSetting); @@ -149,3 +149,10 @@ export async function verifySetting( } while (retries < 20); } } + +export function getRawVersion(version: VersionInfo | undefined) { + if (version) { + return `${version.major}.${version.minor}.${version.micro}`; + } + return ``; +} diff --git a/src/extension/extensionInit.ts b/src/extension/extensionInit.ts index ba739292..92128dfa 100644 --- a/src/extension/extensionInit.ts +++ b/src/extension/extensionInit.ts @@ -34,6 +34,7 @@ import { pickArgsInput } from './common/utils/localize'; import { DebugPortAttributesProvider } from './debugger/debugPort/portAttributesProvider'; import { getConfigurationsByUri } from './debugger/configuration/launch.json/launchJsonReader'; import { DebugpySocketsHandler } from './debugger/hooks/debugpySocketsHandler'; +import { openReportIssue } from './common/application/commands/reportIssueCommand'; export async function registerDebugger(context: IExtensionContext): Promise { const childProcessAttachService = new ChildProcessAttachService(); @@ -63,6 +64,8 @@ export async function registerDebugger(context: IExtensionContext): Promise openReportIssue())); + context.subscriptions.push( registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { sendTelemetryEvent(EventName.DEBUG_IN_TERMINAL_BUTTON); diff --git a/src/extension/telemetry/constants.ts b/src/extension/telemetry/constants.ts index 9f08a331..861d0217 100644 --- a/src/extension/telemetry/constants.ts +++ b/src/extension/telemetry/constants.ts @@ -19,4 +19,5 @@ export enum EventName { DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS', DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON', ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', + USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND', } diff --git a/src/extension/telemetry/index.ts b/src/extension/telemetry/index.ts index ce2cd188..d7b2c457 100644 --- a/src/extension/telemetry/index.ts +++ b/src/extension/telemetry/index.ts @@ -660,4 +660,11 @@ export interface IEventNamePropertyMapping { "envfile_variable_substitution" : { "owner": "karthiknadig" } */ [EventName.ENVFILE_VARIABLE_SUBSTITUTION]: never | undefined; + /** + * Telemetry event sent when the user use the report issue command. + */ + /* __GDPR__ + "use_report_issue_command" : { "owner": "paulacamargo25" } + */ + [EventName.USE_REPORT_ISSUE_COMMAND]: unknown; } diff --git a/src/test/resources/issueTemplate.md b/src/test/resources/issueTemplate.md new file mode 100644 index 00000000..81f42a52 --- /dev/null +++ b/src/test/resources/issueTemplate.md @@ -0,0 +1,56 @@ + +# Behaviour + +XXX + +## Steps to reproduce: + +1. XXX + + + + +# Diagnostic data +
+ +launch.json configuration + + +

+ +``` +XXX +``` + +

+
+ +
+ +Output for Python in the Output panel (ViewOutput, change the drop-down the upper-right of the Output panel to Python) + + +

+ +``` +XXX +``` + +

+
+ +
+ +Output for Python Debugger in the Output panel (ViewOutput, change the drop-down the upper-right of the Output panel to Python Debugger) + + +

+ +``` +XXX +``` + +

+
\ No newline at end of file diff --git a/src/test/resources/issueUserDataTemplate.md b/src/test/resources/issueUserDataTemplate.md new file mode 100644 index 00000000..ebfd1d0c --- /dev/null +++ b/src/test/resources/issueUserDataTemplate.md @@ -0,0 +1,2 @@ +- Python version (& distribution if applicable, e.g. Anaconda): 3.9.0 +- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Venv \ No newline at end of file diff --git a/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts b/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts new file mode 100644 index 00000000..1434d718 --- /dev/null +++ b/src/test/unittest/common/application/commands/reportIssueCommand.unit.test.ts @@ -0,0 +1,80 @@ +/* eslint-disable global-require */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { expect } from 'chai'; +import * as Telemetry from '../../../../../extension/telemetry/index'; +import { EventName } from '../../../../../extension/telemetry/constants'; +import * as vscodeapi from '../../../../../extension/common/vscodeapi'; +import * as pythonApi from '../../../../../extension/common/python'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../../constants'; +import { openReportIssue } from '../../../../../extension/common/application/commands/reportIssueCommand'; +import { PythonEnvironment } from '../../../../../extension/debugger/adapter/types'; + +suite('Report Issue Command', () => { + let executeCommandStub: sinon.SinonStub; + let resolveEnvironmentStub: sinon.SinonStub; + + setup(async () => { + executeCommandStub = sinon.stub(vscodeapi, 'executeCommand'); + resolveEnvironmentStub = sinon.stub(pythonApi, 'resolveEnvironment'); + const interpreter = { + environment: { + type: 'Venv', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + } as unknown as PythonEnvironment; + resolveEnvironmentStub.resolves(interpreter); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Test if issue body is filled correctly when including all the settings', async () => { + await openReportIssue(); + + const issueTemplatePath = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'resources', + 'issueTemplate.md', + ); + const expectedIssueBody = fs.readFileSync(issueTemplatePath, 'utf8'); + + const userDataTemplatePath = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'resources', + 'issueUserDataTemplate.md', + ); + const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8'); + + executeCommandStub.withArgs('workbench.action.openIssueReporter', sinon.match.any).resolves(); + + sinon.assert.calledOnceWithExactly(executeCommandStub, 'workbench.action.openIssueReporter', sinon.match.any); + + const { issueBody, data } = executeCommandStub.getCall(0).args[1]; + expect(issueBody).to.be.equal(expectedIssueBody); + expect(data).to.be.equal(expectedData); + }); + + test('Should send telemetry event when run Report Issue Command', async () => { + const sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent'); + await openReportIssue(); + + sinon.assert.calledWith(sendTelemetryStub, EventName.USE_REPORT_ISSUE_COMMAND); + sinon.restore(); + }); +}); diff --git a/src/test/unittest/extensionInit.unit.test.ts b/src/test/unittest/extensionInit.unit.test.ts index 8ed5739d..429a9922 100644 --- a/src/test/unittest/extensionInit.unit.test.ts +++ b/src/test/unittest/extensionInit.unit.test.ts @@ -60,6 +60,7 @@ suite('Debugging - register Debugging', () => { test('Ensure to register all the commands related to the debugger', () => { registerDebugger(context.object); + sinon.assert.calledWithExactly(registerCommandStub, Commands.ReportIssue, sinon.match.any); sinon.assert.calledWithExactly(registerCommandStub, Commands.Debug_In_Terminal, sinon.match.any); sinon.assert.calledWithExactly(registerCommandStub, Commands.Debug_Using_Launch_Config, sinon.match.any); sinon.assert.calledWithExactly(registerCommandStub, Commands.PickLocalProcess, sinon.match.any); @@ -71,7 +72,7 @@ suite('Debugging - register Debugging', () => { sinon.match.any, ); sinon.assert.calledWithExactly(registerCommandStub, Commands.ClearStorage, sinon.match.any); - expect(registerCommandStub.callCount).to.be.equal(6); + expect(registerCommandStub.callCount).to.be.equal(7); }); test('Activation will register the Debug adapter factories', async () => {