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 (View→Output, change the drop-down the upper-right of the Output panel to Python)
+
+
+
+
+```
+XXX
+```
+
+
+
+
+
+
+Output for Python Debugger in the Output panel (View→Output, 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 (View→Output, change the drop-down the upper-right of the Output panel to Python)
+
+
+
+
+```
+XXX
+```
+
+
+
+
+
+
+Output for Python Debugger in the Output panel (View→Output, 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 () => {