diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8d730b971..355123cfd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,31 +1,38 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' +# Bug/Crash Report: {{title}} ---- +## System Information +- **Application Version**: {{appVersion}} +- **Platform**: {{platformVersion}} +- **Architecture**: {{architecture}} +- **Process**: renderer + +## Error Message +{{errorMessage}} + +## Context +- **Game/Mode**: {{gameMode}} +- **Extension Version**: {{extensionVersion}} -**Describe the bug** -A clear and concise description of what the bug is. +## External File (if applicable) +- **File**: [Archive Attachment]({{externalFileUrl}}) -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +## Steps to Reproduce +{{steps}} -**Expected behavior** -A clear and concise description of what you expected to happen. +## Expected Behavior +{{expectedBehavior}} -**Screenshots** -If applicable, add screenshots to help explain your problem. +## Actual Behavior +{{actualBehavior}} + +## Reported By +- **User**: {{reportedBy}} + +--- -**Platform (please complete the following information):** - - OS: [e.g. Windows 10] - - Vortex Version [e.g. 0.16.12] +### Additional Information: +- **Date Reported**: +{{dateReported}} -**Additional context** -Add any other context about the problem here. +- **Additional Context**: +{{additionalContext}} \ No newline at end of file diff --git a/app/package.json b/app/package.json index 858c17c56..e0a6f791a 100644 --- a/app/package.json +++ b/app/package.json @@ -13,7 +13,7 @@ "dependencies": { "@electron/remote": "^2.0.12", "@msgpack/msgpack": "^2.7.0", - "@nexusmods/nexus-api": "Nexus-Mods/node-nexus-api", + "@nexusmods/nexus-api": "Nexus-Mods/node-nexus-api#new-feedback-system", "bbcode-to-react": "TanninOne/bbcode-to-react", "big.js": "^5.2.2", "bluebird": "^3.7.2", diff --git a/extensions/feedback b/extensions/feedback index bce876f28..328e0a5f1 160000 --- a/extensions/feedback +++ b/extensions/feedback @@ -1 +1 @@ -Subproject commit bce876f28f8be08409a587315bb683b95953ab1e +Subproject commit 328e0a5f1b434744f4ab5d58416bb0ec5ef04c06 diff --git a/package.json b/package.json index c61bbde82..d959aeb5d 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,7 @@ "tslint-eslint-rules": "^5.4.0", "tslint-react": "^4.1.0", "typescript": "^4.4.3", - "vortex-api": "Nexus-Mods/vortex-api#fblo_api_update_1.8.11", + "vortex-api": "Nexus-Mods/vortex-api#api_update_1.8.12", "webpack": "^5.76.0", "webpack-cli": "^4.8.0", "webpack-node-externals": "^3.0.0" @@ -205,7 +205,7 @@ "dependencies": { "@electron/remote": "^2.0.12", "@msgpack/msgpack": "^2.7.0", - "@nexusmods/nexus-api": "Nexus-Mods/node-nexus-api", + "@nexusmods/nexus-api": "Nexus-Mods/node-nexus-api#new-feedback-system", "bbcode-to-react": "TanninOne/bbcode-to-react", "big.js": "^5.2.2", "bluebird": "^3.7.2", diff --git a/src/extensions/nexus_integration/eventHandlers.ts b/src/extensions/nexus_integration/eventHandlers.ts index b416e7da6..94c4e5a81 100644 --- a/src/extensions/nexus_integration/eventHandlers.ts +++ b/src/extensions/nexus_integration/eventHandlers.ts @@ -13,6 +13,8 @@ import { activeGameId, currentGame, downloadPathForGame, gameById } from '../../ import { getSafe } from '../../util/storeHelper'; import { toPromise, truthy } from '../../util/util'; +import { IFeedbackReport } from '../../types/IFeedbackReport'; + import { resolveCategoryName } from '../category_management'; import { AlreadyDownloaded, DownloadIsHTML } from '../download_management/DownloadManager'; import { SITE_ID } from '../gamemode_management/constants'; @@ -646,12 +648,23 @@ export function onDownloadUpdate(api: IExtensionApi, }; } -export function onSubmitFeedback(nexus: Nexus) { +export function onSubmitFeedback(api: IExtensionApi, nexus: Nexus) { return (title: string, message: string, hash: string, feedbackFiles: string[], anonymous: boolean, callback: (err: Error, response?: IFeedbackResponse) => void) => { - submitFeedback(nexus, title, message, feedbackFiles, anonymous, hash) - .then(response => callback(null, response)) - .catch(err => callback(err)); + const report: IFeedbackReport = { + title, + message, + files: feedbackFiles, + hash, + callback, + } + + api.events.emit('report-feedback', report); + // Vortex used to send feedback reports to the Nexus Mods admin area + // for curation. Left this here for posterity. + // submitFeedback(nexus, title, message, feedbackFiles, anonymous, hash) + // .then(response => callback(null, response)) + // .catch(err => callback(err)); }; } diff --git a/src/extensions/nexus_integration/index.tsx b/src/extensions/nexus_integration/index.tsx index 2b29d5b1b..5f00007e6 100644 --- a/src/extensions/nexus_integration/index.tsx +++ b/src/extensions/nexus_integration/index.tsx @@ -885,7 +885,7 @@ function extendAPI(api: IExtensionApi, nexus: NexusT): INexusAPIExtension nexusGetLatestMods: eh.onGetLatestMods(api, nexus), nexusGetTrendingMods: eh.onGetTrendingMods(api, nexus), nexusEndorseMod: eh.onEndorseMod(api, nexus), - nexusSubmitFeedback: eh.onSubmitFeedback(nexus), + nexusSubmitFeedback: eh.onSubmitFeedback(api, nexus), nexusSubmitCollection: eh.onSubmitCollection(nexus), nexusModUpdate: eh.onModUpdate(api, nexus), nexusOpenCollectionPage: eh.onOpenCollectionPage(api), @@ -1038,7 +1038,7 @@ function once(api: IExtensionApi, callbacks: Array<(nexus: NexusT) => void>) { api.onAsync('get-trending-mods', eh.onGetTrendingMods(api, nexus)); api.events.on('refresh-user-info', eh.onRefreshUserInfo(nexus, api)); api.events.on('endorse-mod', eh.onEndorseMod(api, nexus)); - api.events.on('submit-feedback', eh.onSubmitFeedback(nexus)); + api.events.on('submit-feedback', eh.onSubmitFeedback(api, nexus)); api.events.on('submit-collection', eh.onSubmitCollection(nexus)); api.events.on('mod-update', eh.onModUpdate(api, nexus)); api.events.on('open-collection-page', eh.onOpenCollectionPage(api)); diff --git a/src/extensions/nexus_integration/selectors.ts b/src/extensions/nexus_integration/selectors.ts index 395257b1e..e8f69c113 100644 --- a/src/extensions/nexus_integration/selectors.ts +++ b/src/extensions/nexus_integration/selectors.ts @@ -7,6 +7,9 @@ import { truthy } from '../../util/util'; export const apiKey = (state: IState) => getSafe(state, ['confidential', 'account', 'nexus', 'APIKey'], undefined); +export const userName = (state: IState) => + getSafe(state, ['confidential', 'account', 'nexus', 'userInfo', 'name'], undefined); + export const isLoggedIn = (state: IState) => { const APIKEY = state.confidential.account['nexus']?.APIKey; const OAuthCredentials = state.confidential.account['nexus']?.OAuthCredentials; diff --git a/src/extensions/nexus_integration/util.ts b/src/extensions/nexus_integration/util.ts index 4fa20d2c3..4e2561b11 100644 --- a/src/extensions/nexus_integration/util.ts +++ b/src/extensions/nexus_integration/util.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ import * as RemoteT from '@electron/remote'; import Nexus, { EndorsedStatus, ICollectionQuery, IEndorsement, IFileInfo, IGameListEntry, IModInfo, diff --git a/src/extensions/nexus_integration/util/submitFeedback.ts b/src/extensions/nexus_integration/util/submitFeedback.ts index 60b3ceac8..a1306c9a8 100644 --- a/src/extensions/nexus_integration/util/submitFeedback.ts +++ b/src/extensions/nexus_integration/util/submitFeedback.ts @@ -4,6 +4,7 @@ import NexusT, { IFeedbackResponse } from '@nexusmods/nexus-api'; import Promise from 'bluebird'; import ZipT = require('node-7z'); import { tmpName } from 'tmp'; +import { util } from '../../..'; function zipFiles(files: string[]): Promise { if (files.length === 0) { @@ -26,6 +27,7 @@ function zipFiles(files: string[]): Promise { function submitFeedback(nexus: NexusT, title: string, message: string, feedbackFiles: string[], anonymous: boolean, hash: string): Promise { + return Promise.reject(new util.NotSupportedError()); let archive: string; return zipFiles(feedbackFiles) .then(tmpPath => { diff --git a/src/types/IErrorContext.ts b/src/types/IErrorContext.ts new file mode 100644 index 000000000..b62740ce8 --- /dev/null +++ b/src/types/IErrorContext.ts @@ -0,0 +1,3 @@ +export interface IErrorContext { + [id: string]: string; +} \ No newline at end of file diff --git a/src/types/IFeedbackReport.ts b/src/types/IFeedbackReport.ts new file mode 100644 index 000000000..2cce52560 --- /dev/null +++ b/src/types/IFeedbackReport.ts @@ -0,0 +1,15 @@ +// feedback-report event handler expects this structure. +import { IError } from './IError'; +import { IErrorContext } from './IErrorContext'; +export interface IFeedbackReport { + title: string; + message: string; + files: string[]; + sourceProcess?: string; + reporterProcess?: string; + labels?: string[]; + context?: IErrorContext; + error?: IError; + hash?: string; + callback?: (err: Error, response?: any) => void; +} \ No newline at end of file diff --git a/src/types/api.ts b/src/types/api.ts index df861f277..ffafb043c 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -27,6 +27,10 @@ export { IStarterInfo } from '../util/StarterInfo'; export { IRegisteredExtension } from '../util/ExtensionManager'; +export { IErrorContext } from './IErrorContext'; +export { IError } from './IError'; +export { IFeedbackReport } from './IFeedbackReport'; + export { IAvailableExtension, IExtension } from '../extensions/extension_manager/types'; export { LoadOrder, diff --git a/src/util/ExtensionManager.ts b/src/util/ExtensionManager.ts index 053ebc447..26d200452 100644 --- a/src/util/ExtensionManager.ts +++ b/src/util/ExtensionManager.ts @@ -32,7 +32,7 @@ import { Archive } from './archives'; import { COMPANY_ID } from './constants'; import { MissingDependency, NotSupportedError, ProcessCanceled, ThirdPartyError, TimeoutError, UserCanceled } from './CustomErrors'; -import { disableErrorReport, isOutdated } from './errorHandling'; +import { disableErrorReport, isOutdated, setErrorContext } from './errorHandling'; import getVortexPath from './getVortexPath'; import { i18n, TString } from './i18n'; import lazyRequire from './lazyRequire'; diff --git a/src/util/api.ts b/src/util/api.ts index 7f1d724f1..20c658cd8 100644 --- a/src/util/api.ts +++ b/src/util/api.ts @@ -67,6 +67,8 @@ import walk from './walk'; import SevenZip = require('node-7z'); import { runElevated, runThreaded } from 'vortex-run'; +import { genHash } from './genHash'; +import { getErrorContext, setErrorContext, clearErrorContext } from './errorHandling'; export * from './network'; @@ -81,6 +83,7 @@ export { calculateFolderSize, Campaign, checksum, + clearErrorContext, convertGameIdReverse, copyFileAtomic, copyRecursive, @@ -105,8 +108,10 @@ export { getCurrentActivator, getCurrentLanguage, getDriveList, + getErrorContext, getGame, getGames, + genHash, getManifest, getModSource, getModSources, @@ -160,6 +165,7 @@ export { Section, semverCoerce, setdefault, + setErrorContext, SetupError, SevenZip, sortMods, diff --git a/src/util/errorHandling.ts b/src/util/errorHandling.ts index 79924ccbf..94090fa19 100644 --- a/src/util/errorHandling.ts +++ b/src/util/errorHandling.ts @@ -1,5 +1,6 @@ +/* eslint-disable */ import { NEXUS_BASE_URL, OAUTH_CLIENT_ID } from '../extensions/nexus_integration/constants'; -import { IErrorOptions, IExtensionApi } from '../types/api'; +import { IErrorOptions, IExtensionApi, IFeedbackReport } from '../types/api'; import { IError } from '../types/IError'; import { COMPANY_ID } from './constants'; @@ -33,12 +34,12 @@ import lazyRequire from './lazyRequire'; const remote = lazyRequire(() => require('@electron/remote')); -function createTitle(type: string, error: IError, hash: string) { +function createTitle(type: string, error: IError) { return `${type}: ${error.message}`; } interface IErrorContext { - [id: string]: string; + [id: string]: any; } const globalContext: IErrorContext = {}; @@ -91,7 +92,7 @@ ${error.details} if (Object.keys(context).length > 0) { sections.push(`#### Context \`\`\` -${Object.keys(context).map(key => `${key} = ${context[key]}`)} +${Object.keys(context).filter(key => context[key] !== 'extension-api').map(key => `${key} = ${context[key]}`)} \`\`\``); } @@ -125,6 +126,26 @@ export function createErrorReport(type: string, error: IError, context: IErrorCo spawnSelf(['--report', reportPath]); } +function githubReport(api: IExtensionApi, type: string, error: IError, labels: string[], + context: IErrorContext, oauthToken: any, reporterProcess: string, + sourceProcess: string) { + const title = createTitle(type, error); + const body = createReport(type, error, context, getApplication().version, reporterProcess, sourceProcess); + const hash = genHash(body); + const feedbackReport: IFeedbackReport = { + title, + message: body, + files: [], + reporterProcess, + labels, + context, + error, + hash, + } + api.events.emit('report-feedback', feedbackReport); + return Promise.resolve(undefined); + } + function nexusReport(hash: string, type: string, error: IError, labels: string[], context: IErrorContext, oauthToken: any, reporterProcess: string, sourceProcess: string, attachment: string) @@ -145,14 +166,12 @@ function nexusReport(hash: string, type: string, error: IError, labels: string[] const anonymous = (oauthCredentials === undefined); return Promise.resolve(Nexus.createWithOAuth(oauthCredentials, config, 'Vortex', getApplication().version, undefined)) .then(nexus => nexus.sendFeedback( - createTitle(type, error, hash), + createTitle(type, error), createReport(type, error, context, getApplication().version, reporterProcess, sourceProcess), attachment, anonymous, hash, referenceId)) - .tap(() => - opn(`${NEXUS_BASE_URL}/crash-report/?key=${referenceId}`).catch(() => null)) .catch(err => { log('error', 'failed to report error to nexus', err.message); return undefined; @@ -243,22 +262,17 @@ export function sendReportFile(fileName: string): Promise { export function sendReport(type: string, error: IError, context: IErrorContext, labels: string[], reporterToken: any, reporterProcess: string, - sourceProcess: string, attachment: string): Promise { + sourceProcess: string, attachment: string): Promise { const dialog = process.type === 'renderer' ? remote.dialog : dialogIn; - const hash = genHash(error); - if (process.env.NODE_ENV === 'development') { - const fullMessage = error.title !== undefined - ? error.message + `\n(${error.title})` - : error.message; - dialog.showErrorBox(fullMessage, JSON.stringify({ - type, error, labels, context, reporterProcess, sourceProcess, - attachment, - }, undefined, 2)); + // return nexusReport(hash, type, error, labels, context, reporterToken || fallbackOauthToken, + // reporterProcess, sourceProcess, attachment); + const api: IExtensionApi = context['extension-api']; + if (api === undefined) { + dialog.showErrorBox(error.title, error.message); return Promise.resolve(undefined); - } else { - return nexusReport(hash, type, error, labels, context, reporterToken || fallbackOauthToken, - reporterProcess, sourceProcess, attachment); } + return githubReport(api, type, error, labels, context, reporterToken || fallbackOauthToken, + reporterProcess, sourceProcess); } let defaultWindow: BrowserWindow = null; @@ -532,7 +546,7 @@ export function toError(input: any, title?: string, * @param id context id * @param value context value */ -export function setErrorContext(id: string, value: string) { +export function setErrorContext(id: string, value: any) { globalContext[id] = value; } diff --git a/src/util/genHash.ts b/src/util/genHash.ts index 54cf7a2b7..f092ed6af 100644 --- a/src/util/genHash.ts +++ b/src/util/genHash.ts @@ -78,8 +78,12 @@ export function extractToken(error: IError): string { return hashStack.join('\n'); } -export function genHash(error: IError) { +export function genHash(input: IError | string): string { const { createHash } = require('crypto'); const hash = createHash('md5'); - return hash.update(extractToken(error)).digest('hex'); -} + if (typeof input === 'string') { + return hash.update((input)).digest('hex'); + } else { + return hash.update(extractToken(input)).digest('hex'); + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 143da7e98..0702d8bc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1753,9 +1753,9 @@ resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-2.8.0.tgz#4210deb771ee3912964f14a15ddfb5ff877e70b9" integrity sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ== -"@nexusmods/nexus-api@Nexus-Mods/node-nexus-api": - version "1.4.25" - resolved "https://codeload.github.com/Nexus-Mods/node-nexus-api/tar.gz/c2721a699504204397554afbe55ee28f1f7d6839" +"@nexusmods/nexus-api@Nexus-Mods/node-nexus-api#new-feedback-system": + version "1.5.1" + resolved "https://codeload.github.com/Nexus-Mods/node-nexus-api/tar.gz/3cdd3f6252fc051f74b43953c6a0003da2e0af77" dependencies: form-data "^4.0.0" jsonwebtoken "^9.0.0" @@ -3521,9 +3521,9 @@ aws-sign2@~0.7.0: integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" - integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== + version "1.13.2" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" + integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== babel-jest@^29.5.0: version "29.5.0" @@ -6238,9 +6238,9 @@ form-data@^2.5.0: mime-types "^2.1.12" form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -10770,9 +10770,9 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== set-cookie-parser@^2.4.6: - version "2.6.0" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" - integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" @@ -12153,9 +12153,9 @@ void-elements@3.1.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== -vortex-api@Nexus-Mods/vortex-api#fblo_api_update_1.8.11: - version "1.8.11" - resolved "https://codeload.github.com/Nexus-Mods/vortex-api/tar.gz/9d51cfafea5f96eb6eca2be4371e32b236f77891" +vortex-api@Nexus-Mods/vortex-api#api_update_1.8.12: + version "1.8.12" + resolved "https://codeload.github.com/Nexus-Mods/vortex-api/tar.gz/8f0395ee141126e975a7a7cdbe99673625bdc434" vortex-parse-ini@Nexus-Mods/vortex-parse-ini: version "0.3.0"