Path: blob/main/src/vs/workbench/contrib/issue/electron-browser/issueReporterService.ts
5249 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import { $, reset } from '../../../../base/browser/dom.js';5import { VSBuffer } from '../../../../base/common/buffer.js';6import { CancellationError } from '../../../../base/common/errors.js';7import { Schemas } from '../../../../base/common/network.js';8import { IProductConfiguration } from '../../../../base/common/product.js';9import { joinPath } from '../../../../base/common/resources.js';10import { URI } from '../../../../base/common/uri.js';11import { localize } from '../../../../nls.js';12import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';13import { isRemoteDiagnosticError } from '../../../../platform/diagnostics/common/diagnostics.js';14import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';15import { IFileService } from '../../../../platform/files/common/files.js';16import { INativeHostService } from '../../../../platform/native/common/native.js';17import { IOpenerService } from '../../../../platform/opener/common/opener.js';18import { IProcessService } from '../../../../platform/process/common/process.js';19import { IThemeService } from '../../../../platform/theme/common/themeService.js';20import { IUpdateService, StateType } from '../../../../platform/update/common/update.js';21import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';22import { applyZoom } from '../../../../platform/window/electron-browser/window.js';23import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';24import { BaseIssueReporterService } from '../browser/baseIssueReporterService.js';25import { IssueReporterData as IssueReporterModelData } from '../browser/issueReporterModel.js';26import { IIssueFormService, IssueReporterData, IssueType } from '../common/issue.js';2728// GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe.29// ref https://github.com/microsoft/vscode/issues/15919130const MAX_URL_LENGTH = 7500;3132// Github API and issues on web has a limit of 65536. We chose 65500 to play it safe.33// ref https://github.com/github/issues/issues/1285834const MAX_GITHUB_API_LENGTH = 65500;353637export class IssueReporter extends BaseIssueReporterService {38private readonly processService: IProcessService;39constructor(40disableExtensions: boolean,41data: IssueReporterData,42os: {43type: string;44arch: string;45release: string;46},47product: IProductConfiguration,48window: Window,49@INativeHostService private readonly nativeHostService: INativeHostService,50@IIssueFormService issueFormService: IIssueFormService,51@IProcessService processService: IProcessService,52@IThemeService themeService: IThemeService,53@IFileService fileService: IFileService,54@IFileDialogService fileDialogService: IFileDialogService,55@IUpdateService private readonly updateService: IUpdateService,56@IContextKeyService contextKeyService: IContextKeyService,57@IContextMenuService contextMenuService: IContextMenuService,58@IAuthenticationService authenticationService: IAuthenticationService,59@IOpenerService openerService: IOpenerService60) {61super(disableExtensions, data, os, product, window, false, issueFormService, themeService, fileService, fileDialogService, contextMenuService, authenticationService, openerService);62this.processService = processService;63this.processService.getSystemInfo().then(info => {64this.issueReporterModel.update({ systemInfo: info });65this.receivedSystemInfo = true;6667this.updateSystemInfo(this.issueReporterModel.getData());68this.updateButtonStates();69});70if (this.data.issueType === IssueType.PerformanceIssue) {71this.processService.getPerformanceInfo().then(info => {72this.updatePerformanceInfo(info as Partial<IssueReporterData>);73});74}7576this.checkForUpdates();77this.setEventHandlers();78applyZoom(this.data.zoomLevel, this.window);79this.updateExperimentsInfo(this.data.experiments);80this.updateRestrictedMode(this.data.restrictedMode);81this.updateUnsupportedMode(this.data.isUnsupported);82}8384private async checkForUpdates(): Promise<void> {85const updateState = this.updateService.state;86if (updateState.type === StateType.Ready || updateState.type === StateType.Downloaded) {87this.needsUpdate = true;88// eslint-disable-next-line no-restricted-syntax89const includeAcknowledgement = this.getElementById('version-acknowledgements');90// eslint-disable-next-line no-restricted-syntax91const updateBanner = this.getElementById('update-banner');92if (updateBanner && includeAcknowledgement) {93includeAcknowledgement.classList.remove('hidden');94updateBanner.classList.remove('hidden');95updateBanner.textContent = localize('updateAvailable', "A new version of {0} is available.", this.product.nameLong);96}97}98}99100public override setEventHandlers(): void {101super.setEventHandlers();102103this.addEventListener('issue-type', 'change', (event: Event) => {104const issueType = parseInt((<HTMLInputElement>event.target).value);105this.issueReporterModel.update({ issueType: issueType });106if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) {107this.processService.getPerformanceInfo().then(info => {108this.updatePerformanceInfo(info as Partial<IssueReporterData>);109});110}111112// Resets placeholder113// eslint-disable-next-line no-restricted-syntax114const descriptionTextArea = <HTMLInputElement>this.getElementById('issue-title');115if (descriptionTextArea) {116descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title");117}118119this.updateButtonStates();120this.setSourceOptions();121this.render();122});123}124125public override async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise<boolean> {126if (issueBody.length > MAX_GITHUB_API_LENGTH) {127const extensionData = this.issueReporterModel.getData().extensionData;128if (extensionData) {129issueBody = issueBody.replace(extensionData, '');130const date = new Date();131const formattedDate = date.toISOString().split('T')[0]; // YYYY-MM-DD132const formattedTime = date.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS133const fileName = `extensionData_${formattedDate}_${formattedTime}.md`;134try {135const downloadPath = await this.fileDialogService.showSaveDialog({136title: localize('saveExtensionData', "Save Extension Data"),137availableFileSystems: [Schemas.file],138defaultUri: joinPath(await this.fileDialogService.defaultFilePath(Schemas.file), fileName),139});140141if (downloadPath) {142await this.fileService.writeFile(downloadPath, VSBuffer.fromString(extensionData));143}144} catch (e) {145console.error('Writing extension data to file failed');146return false;147}148} else {149console.error('Issue body too large to submit to GitHub');150return false;151}152}153const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`;154const init = {155method: 'POST',156body: JSON.stringify({157title: issueTitle,158body: issueBody159}),160headers: new Headers({161'Content-Type': 'application/json',162'Authorization': `Bearer ${this.data.githubAccessToken}`163})164};165166const response = await fetch(url, init);167if (!response.ok) {168console.error('Invalid GitHub URL provided.');169return false;170}171const result = await response.json();172await this.openerService.open(result.html_url, { openExternal: true });173this.close();174return true;175}176177public override async createIssue(shouldCreate?: boolean, privateUri?: boolean): Promise<boolean> {178const selectedExtension = this.issueReporterModel.getData().selectedExtension;179// Short circuit if the extension provides a custom issue handler180if (this.nonGitHubIssueUrl) {181const url = this.getExtensionBugsUrl();182if (url) {183this.hasBeenSubmitted = true;184await this.openerService.open(url, { openExternal: true });185return true;186}187}188189if (!this.validateInputs()) {190// If inputs are invalid, set focus to the first one and add listeners on them191// to detect further changes192// eslint-disable-next-line no-restricted-syntax193const invalidInput = this.window.document.getElementsByClassName('invalid-input');194if (invalidInput.length) {195(<HTMLInputElement>invalidInput[0]).focus();196}197198this.addEventListener('issue-title', 'input', _ => {199this.validateInput('issue-title');200});201202this.addEventListener('description', 'input', _ => {203this.validateInput('description');204});205206this.addEventListener('issue-source', 'change', _ => {207this.validateInput('issue-source');208});209210if (this.issueReporterModel.fileOnExtension()) {211this.addEventListener('extension-selector', 'change', _ => {212this.validateInput('extension-selector');213this.validateInput('description');214});215}216217return false;218}219220this.hasBeenSubmitted = true;221222// eslint-disable-next-line no-restricted-syntax223const issueTitle = (<HTMLInputElement>this.getElementById('issue-title')).value;224const issueBody = this.issueReporterModel.serialize();225226let issueUrl = privateUri ? this.getPrivateIssueUrl() : this.getIssueUrl();227if (!issueUrl && selectedExtension?.uri) {228const uri = URI.revive(selectedExtension.uri);229issueUrl = uri.toString();230} else if (!issueUrl) {231console.error(`No ${privateUri ? 'private ' : ''}issue url found`);232return false;233}234235const gitHubDetails = this.parseGitHubUrl(issueUrl);236237// eslint-disable-next-line no-restricted-syntax238const baseUrl = this.getIssueUrlWithTitle((<HTMLInputElement>this.getElementById('issue-title')).value, issueUrl);239let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`;240241url = this.addTemplateToUrl(url, gitHubDetails?.owner, gitHubDetails?.repositoryName);242243if (this.data.githubAccessToken && gitHubDetails && shouldCreate) {244if (await this.submitToGitHub(issueTitle, issueBody, gitHubDetails)) {245return true;246}247}248249try {250if (url.length > MAX_URL_LENGTH || issueBody.length > MAX_GITHUB_API_LENGTH) {251url = await this.writeToClipboard(baseUrl, issueBody);252url = this.addTemplateToUrl(url, gitHubDetails?.owner, gitHubDetails?.repositoryName);253}254} catch (_) {255console.error('Writing to clipboard failed');256return false;257}258259await this.openerService.open(url, { openExternal: true });260return true;261}262263public override async writeToClipboard(baseUrl: string, issueBody: string): Promise<string> {264const shouldWrite = await this.issueFormService.showClipboardDialog();265if (!shouldWrite) {266throw new CancellationError();267}268269await this.nativeHostService.writeClipboardText(issueBody);270271return baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`;272}273274private updateSystemInfo(state: IssueReporterModelData) {275// eslint-disable-next-line no-restricted-syntax276const target = this.window.document.querySelector<HTMLElement>('.block-system .block-info');277278if (target) {279const systemInfo = state.systemInfo!;280const renderedDataTable = $('table', undefined,281$('tr', undefined,282$('td', undefined, 'CPUs'),283$('td', undefined, systemInfo.cpus || '')284),285$('tr', undefined,286$('td', undefined, 'GPU Status' as string),287$('td', undefined, Object.keys(systemInfo.gpuStatus).map(key => `${key}: ${systemInfo.gpuStatus[key]}`).join('\n'))288),289$('tr', undefined,290$('td', undefined, 'Load (avg)' as string),291$('td', undefined, systemInfo.load || '')292),293$('tr', undefined,294$('td', undefined, 'Memory (System)' as string),295$('td', undefined, systemInfo.memory)296),297$('tr', undefined,298$('td', undefined, 'Process Argv' as string),299$('td', undefined, systemInfo.processArgs)300),301$('tr', undefined,302$('td', undefined, 'Screen Reader' as string),303$('td', undefined, systemInfo.screenReader)304),305$('tr', undefined,306$('td', undefined, 'VM'),307$('td', undefined, systemInfo.vmHint)308)309);310reset(target, renderedDataTable);311312systemInfo.remoteData.forEach(remote => {313target.appendChild($<HTMLHRElement>('hr'));314if (isRemoteDiagnosticError(remote)) {315const remoteDataTable = $('table', undefined,316$('tr', undefined,317$('td', undefined, 'Remote'),318$('td', undefined, remote.hostName)319),320$('tr', undefined,321$('td', undefined, ''),322$('td', undefined, remote.errorMessage)323)324);325target.appendChild(remoteDataTable);326} else {327const remoteDataTable = $('table', undefined,328$('tr', undefined,329$('td', undefined, 'Remote'),330$('td', undefined, remote.latency ? `${remote.hostName} (latency: ${remote.latency.current.toFixed(2)}ms last, ${remote.latency.average.toFixed(2)}ms average)` : remote.hostName)331),332$('tr', undefined,333$('td', undefined, 'OS'),334$('td', undefined, remote.machineInfo.os)335),336$('tr', undefined,337$('td', undefined, 'CPUs'),338$('td', undefined, remote.machineInfo.cpus || '')339),340$('tr', undefined,341$('td', undefined, 'Memory (System)' as string),342$('td', undefined, remote.machineInfo.memory)343),344$('tr', undefined,345$('td', undefined, 'VM'),346$('td', undefined, remote.machineInfo.vmHint)347)348);349target.appendChild(remoteDataTable);350}351});352}353}354355private updateRestrictedMode(restrictedMode: boolean) {356this.issueReporterModel.update({ restrictedMode });357}358359private updateUnsupportedMode(isUnsupported: boolean) {360this.issueReporterModel.update({ isUnsupported });361}362363private updateExperimentsInfo(experimentInfo: string | undefined) {364this.issueReporterModel.update({ experimentInfo });365// eslint-disable-next-line no-restricted-syntax366const target = this.window.document.querySelector<HTMLElement>('.block-experiments .block-info');367if (target) {368target.textContent = experimentInfo ? experimentInfo : localize('noCurrentExperiments', "No current experiments.");369}370}371}372373374