Skip to content

Commit ca82fb7

Browse files
Implement issue report (#260)
* Add template * Add report command * Ass telemetry * add tests * fix extension root * prettier * fix tests
1 parent ce50793 commit ca82fb7

14 files changed

+273
-5
lines changed

package.json

+18-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"version": "2024.3.0-dev",
66
"publisher": "ms-python",
77
"enabledApiProposals": [
8-
"portsAttributes"
8+
"portsAttributes",
9+
"contribIssueReporter"
910
],
1011
"license": "MIT",
1112
"homepage": "https://github.com/Microsoft/vscode-python-debugger",
@@ -63,9 +64,19 @@
6364
"light": "resources/light/repl.svg"
6465
},
6566
"title": "%debugpy.command.viewOutput.title%"
67+
},
68+
{
69+
"category": "Python Debugger",
70+
"command": "debugpy.reportIssue",
71+
"title": "%debugpy.command.reportIssue.title%"
6672
}
6773
],
6874
"menus": {
75+
"issue/reporter": [
76+
{
77+
"command": "debugpy.reportIssue"
78+
}
79+
],
6980
"commandPalette": [
7081
{
7182
"category": "Python Debugger",
@@ -90,6 +101,12 @@
90101
"category": "Python Debugger",
91102
"command": "debugpy.viewOutput",
92103
"title": "%debugpy.command.viewOutput.title%"
104+
},
105+
{
106+
"category": "Python Debugger",
107+
"command": "debugpy.reportIssue",
108+
"title": "%debugpy.command.reportIssue.title%",
109+
"when": "!virtualWorkspace && shellExecutionSupported"
93110
}
94111
],
95112
"editor/title/run": [

package.nls.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
2+
"debugpy.command.clearCacheAndReload.title": "Clear Cache and Reload Window",
23
"debugpy.command.debugInTerminal.title": "Python Debugger: Debug Python File",
34
"debugpy.command.debugUsingLaunchConfig.title": "Python Debugger: Debug using launch.json",
4-
"debugpy.command.clearCacheAndReload.title": "Clear Cache and Reload Window",
5+
"debugpy.command.reportIssue.title": "Report Issue...",
56
"debugpy.command.viewOutput.title": "Show Output",
67
"debugpy.debugJustMyCode": "When debugging only step through user-written code. Disable this to allow stepping into library code."
78
}

resources/report_issue_template.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!-- Please fill in all XXX markers -->
2+
# Behaviour
3+
4+
XXX
5+
6+
## Steps to reproduce:
7+
8+
1. XXX
9+
10+
<!--
11+
**After** creating the issue on GitHub, you can add screenshots and GIFs of what is happening. Consider tools like https://www.cockos.com/licecap/, https://github.com/phw/peek or https://www.screentogif.com/ for GIF creation.
12+
-->
13+
14+
<!-- **NOTE**: Please do provide logs from the Python and Python Debugger Output panel. -->
15+
# Diagnostic data
16+
<details>
17+
18+
<summary><code>launch.json</code> configuration
19+
</summary>
20+
21+
<p>
22+
23+
```
24+
XXX
25+
```
26+
27+
</p>
28+
</details>
29+
30+
<details>
31+
32+
<summary>Output for <code>Python</code> in the <code>Output</code> panel (<code>View</code>→<code>Output</code>, change the drop-down the upper-right of the <code>Output</code> panel to <code>Python</code>)
33+
</summary>
34+
35+
<p>
36+
37+
```
38+
XXX
39+
```
40+
41+
</p>
42+
</details>
43+
44+
<details>
45+
46+
<summary>Output for <code>Python Debugger</code> in the <code>Output</code> panel (<code>View</code>→<code>Output</code>, change the drop-down the upper-right of the <code>Output</code> panel to <code>Python Debugger</code>)
47+
</summary>
48+
49+
<p>
50+
51+
```
52+
XXX
53+
```
54+
55+
</p>
56+
</details>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Python version (& distribution if applicable, e.g. Anaconda): {0}
2+
- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): {1}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import * as fs from 'fs-extra';
7+
import * as path from 'path';
8+
import { executeCommand } from '../../vscodeapi';
9+
import { getActiveEnvironmentPath, resolveEnvironment } from '../../python';
10+
import { EXTENSION_ROOT_DIR } from '../../constants';
11+
import { getRawVersion } from '../../settings';
12+
import { sendTelemetryEvent } from '../../../telemetry';
13+
import { EventName } from '../../../telemetry/constants';
14+
15+
/**
16+
* Allows the user to report an issue related to the Python Debugger extension using our template.
17+
*/
18+
export async function openReportIssue(): Promise<void> {
19+
const templatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_template.md');
20+
const userDataTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md');
21+
const template = await fs.readFile(templatePath, 'utf8');
22+
const userTemplate = await fs.readFile(userDataTemplatePath, 'utf8');
23+
const interpreterPath = await getActiveEnvironmentPath();
24+
const interpreter = await resolveEnvironment(interpreterPath);
25+
const virtualEnvKind = interpreter?.environment?.type || 'Unknown';
26+
27+
const pythonVersion = getRawVersion(interpreter?.version);
28+
await executeCommand('workbench.action.openIssueReporter', {
29+
extensionId: 'ms-python.debugpy',
30+
issueBody: template,
31+
data: userTemplate.replace('{0}', pythonVersion).replace('{1}', virtualEnvKind),
32+
});
33+
sendTelemetryEvent(EventName.USE_REPORT_ISSUE_COMMAND, undefined, {});
34+
}

src/extension/common/constants.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as path from 'path';
77
export const PYTHON_LANGUAGE = 'python';
88
const folderName = path.basename(__dirname);
99
export const EXTENSION_ROOT_DIR =
10-
folderName === 'common' ? path.dirname(path.dirname(__dirname)) : path.dirname(__dirname);
10+
folderName === 'common' ? path.dirname(path.dirname(path.dirname(__dirname))) : path.dirname(__dirname);
1111
export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'bundled');
1212
export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `server.py`);
1313
export const DEBUG_SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `_debug_server.py`);
@@ -39,6 +39,7 @@ export namespace Commands {
3939
export const Enable_SourceMap_Support = 'debugpy.enableSourceMapSupport';
4040
export const SelectDebugConfig = 'debugpy.SelectAndInsertDebugConfiguration';
4141
export const Set_Interpreter = 'python.setInterpreter';
42+
export const ReportIssue = 'debugpy.reportIssue';
4243
}
4344

4445
export type Channel = 'stable' | 'insiders';

src/extension/common/settings.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ConfigurationChangeEvent, ConfigurationTarget, Uri, WorkspaceConfigurat
55
import { getInterpreterDetails } from './python';
66
import { getConfiguration, getWorkspaceFolder, getWorkspaceFolders } from './vscodeapi';
77
import { isUnitTestExecution } from './constants';
8+
import { VersionInfo } from '@vscode/python-extension';
89

910
export interface ISettings {
1011
workspace: string;
@@ -15,7 +16,6 @@ export interface ISettings {
1516
export async function getExtensionSettings(namespace: string, includeInterpreter?: boolean): Promise<ISettings[]> {
1617
const settings: ISettings[] = [];
1718
const workspaces = getWorkspaceFolders();
18-
1919
for (const workspace of workspaces) {
2020
const workspaceSetting = await getWorkspaceSettings(namespace, workspace, includeInterpreter);
2121
settings.push(workspaceSetting);
@@ -149,3 +149,10 @@ export async function verifySetting(
149149
} while (retries < 20);
150150
}
151151
}
152+
153+
export function getRawVersion(version: VersionInfo | undefined) {
154+
if (version) {
155+
return `${version.major}.${version.minor}.${version.micro}`;
156+
}
157+
return ``;
158+
}

src/extension/extensionInit.ts

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { pickArgsInput } from './common/utils/localize';
3434
import { DebugPortAttributesProvider } from './debugger/debugPort/portAttributesProvider';
3535
import { getConfigurationsByUri } from './debugger/configuration/launch.json/launchJsonReader';
3636
import { DebugpySocketsHandler } from './debugger/hooks/debugpySocketsHandler';
37+
import { openReportIssue } from './common/application/commands/reportIssueCommand';
3738

3839
export async function registerDebugger(context: IExtensionContext): Promise<void> {
3940
const childProcessAttachService = new ChildProcessAttachService();
@@ -63,6 +64,8 @@ export async function registerDebugger(context: IExtensionContext): Promise<void
6364
),
6465
);
6566

67+
context.subscriptions.push(registerCommand(Commands.ReportIssue, () => openReportIssue()));
68+
6669
context.subscriptions.push(
6770
registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => {
6871
sendTelemetryEvent(EventName.DEBUG_IN_TERMINAL_BUTTON);

src/extension/telemetry/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ export enum EventName {
1919
DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS',
2020
DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON',
2121
ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION',
22+
USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND',
2223
}

src/extension/telemetry/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -660,4 +660,11 @@ export interface IEventNamePropertyMapping {
660660
"envfile_variable_substitution" : { "owner": "karthiknadig" }
661661
*/
662662
[EventName.ENVFILE_VARIABLE_SUBSTITUTION]: never | undefined;
663+
/**
664+
* Telemetry event sent when the user use the report issue command.
665+
*/
666+
/* __GDPR__
667+
"use_report_issue_command" : { "owner": "paulacamargo25" }
668+
*/
669+
[EventName.USE_REPORT_ISSUE_COMMAND]: unknown;
663670
}

src/test/resources/issueTemplate.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!-- Please fill in all XXX markers -->
2+
# Behaviour
3+
4+
XXX
5+
6+
## Steps to reproduce:
7+
8+
1. XXX
9+
10+
<!--
11+
**After** creating the issue on GitHub, you can add screenshots and GIFs of what is happening. Consider tools like https://www.cockos.com/licecap/, https://github.com/phw/peek or https://www.screentogif.com/ for GIF creation.
12+
-->
13+
14+
<!-- **NOTE**: Please do provide logs from the Python and Python Debugger Output panel. -->
15+
# Diagnostic data
16+
<details>
17+
18+
<summary><code>launch.json</code> configuration
19+
</summary>
20+
21+
<p>
22+
23+
```
24+
XXX
25+
```
26+
27+
</p>
28+
</details>
29+
30+
<details>
31+
32+
<summary>Output for <code>Python</code> in the <code>Output</code> panel (<code>View</code>→<code>Output</code>, change the drop-down the upper-right of the <code>Output</code> panel to <code>Python</code>)
33+
</summary>
34+
35+
<p>
36+
37+
```
38+
XXX
39+
```
40+
41+
</p>
42+
</details>
43+
44+
<details>
45+
46+
<summary>Output for <code>Python Debugger</code> in the <code>Output</code> panel (<code>View</code>→<code>Output</code>, change the drop-down the upper-right of the <code>Output</code> panel to <code>Python Debugger</code>)
47+
</summary>
48+
49+
<p>
50+
51+
```
52+
XXX
53+
```
54+
55+
</p>
56+
</details>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Python version (& distribution if applicable, e.g. Anaconda): 3.9.0
2+
- Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Venv
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* eslint-disable global-require */
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
5+
'use strict';
6+
7+
import * as sinon from 'sinon';
8+
import * as fs from 'fs-extra';
9+
import * as path from 'path';
10+
import { expect } from 'chai';
11+
import * as Telemetry from '../../../../../extension/telemetry/index';
12+
import { EventName } from '../../../../../extension/telemetry/constants';
13+
import * as vscodeapi from '../../../../../extension/common/vscodeapi';
14+
import * as pythonApi from '../../../../../extension/common/python';
15+
import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../../constants';
16+
import { openReportIssue } from '../../../../../extension/common/application/commands/reportIssueCommand';
17+
import { PythonEnvironment } from '../../../../../extension/debugger/adapter/types';
18+
19+
suite('Report Issue Command', () => {
20+
let executeCommandStub: sinon.SinonStub;
21+
let resolveEnvironmentStub: sinon.SinonStub;
22+
23+
setup(async () => {
24+
executeCommandStub = sinon.stub(vscodeapi, 'executeCommand');
25+
resolveEnvironmentStub = sinon.stub(pythonApi, 'resolveEnvironment');
26+
const interpreter = {
27+
environment: {
28+
type: 'Venv',
29+
},
30+
version: {
31+
major: 3,
32+
minor: 9,
33+
micro: 0,
34+
},
35+
} as unknown as PythonEnvironment;
36+
resolveEnvironmentStub.resolves(interpreter);
37+
});
38+
39+
teardown(() => {
40+
sinon.restore();
41+
});
42+
43+
test('Test if issue body is filled correctly when including all the settings', async () => {
44+
await openReportIssue();
45+
46+
const issueTemplatePath = path.join(
47+
EXTENSION_ROOT_DIR_FOR_TESTS,
48+
'src',
49+
'test',
50+
'resources',
51+
'issueTemplate.md',
52+
);
53+
const expectedIssueBody = fs.readFileSync(issueTemplatePath, 'utf8');
54+
55+
const userDataTemplatePath = path.join(
56+
EXTENSION_ROOT_DIR_FOR_TESTS,
57+
'src',
58+
'test',
59+
'resources',
60+
'issueUserDataTemplate.md',
61+
);
62+
const expectedData = fs.readFileSync(userDataTemplatePath, 'utf8');
63+
64+
executeCommandStub.withArgs('workbench.action.openIssueReporter', sinon.match.any).resolves();
65+
66+
sinon.assert.calledOnceWithExactly(executeCommandStub, 'workbench.action.openIssueReporter', sinon.match.any);
67+
68+
const { issueBody, data } = executeCommandStub.getCall(0).args[1];
69+
expect(issueBody).to.be.equal(expectedIssueBody);
70+
expect(data).to.be.equal(expectedData);
71+
});
72+
73+
test('Should send telemetry event when run Report Issue Command', async () => {
74+
const sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent');
75+
await openReportIssue();
76+
77+
sinon.assert.calledWith(sendTelemetryStub, EventName.USE_REPORT_ISSUE_COMMAND);
78+
sinon.restore();
79+
});
80+
});

src/test/unittest/extensionInit.unit.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ suite('Debugging - register Debugging', () => {
6060
test('Ensure to register all the commands related to the debugger', () => {
6161
registerDebugger(context.object);
6262

63+
sinon.assert.calledWithExactly(registerCommandStub, Commands.ReportIssue, sinon.match.any);
6364
sinon.assert.calledWithExactly(registerCommandStub, Commands.Debug_In_Terminal, sinon.match.any);
6465
sinon.assert.calledWithExactly(registerCommandStub, Commands.Debug_Using_Launch_Config, sinon.match.any);
6566
sinon.assert.calledWithExactly(registerCommandStub, Commands.PickLocalProcess, sinon.match.any);
@@ -71,7 +72,7 @@ suite('Debugging - register Debugging', () => {
7172
sinon.match.any,
7273
);
7374
sinon.assert.calledWithExactly(registerCommandStub, Commands.ClearStorage, sinon.match.any);
74-
expect(registerCommandStub.callCount).to.be.equal(6);
75+
expect(registerCommandStub.callCount).to.be.equal(7);
7576
});
7677

7778
test('Activation will register the Debug adapter factories', async () => {

0 commit comments

Comments
 (0)