error boundary now properly reports steam errors

This commit is contained in:
AAGaming
2024-05-27 17:21:27 -04:00
parent a84a13c76d
commit 9c8db576f5
12 changed files with 247 additions and 169 deletions
+1 -1
View File
@@ -41,4 +41,4 @@ jobs:
- name: Run tsc (TypeScript) - name: Run tsc (TypeScript)
working-directory: frontend working-directory: frontend
run: $(pnpm bin)/tsc --noEmit run: pnpm run typecheck
+10
View File
@@ -8,6 +8,7 @@ from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType from .messages import MethodCallRequest, SocketMessageType
from ..enums import PluginLoadType from ..enums import PluginLoadType
from ..localplatform.localsocket import LocalSocket from ..localplatform.localsocket import LocalSocket
from ..helpers import get_homebrew_path, mkdir_as_user
from typing import Any, Callable, Coroutine, Dict, List from typing import Any, Callable, Coroutine, Dict, List
@@ -50,6 +51,15 @@ class PluginWrapper:
# TODO enable this after websocket release # TODO enable this after websocket release
self.legacy_method_warning = False self.legacy_method_warning = False
home = get_homebrew_path()
mkdir_as_user(path.join(home, "settings", self.plugin_directory))
# TODO maybe dont chown this?
mkdir_as_user(path.join(home, "data"))
mkdir_as_user(path.join(home, "data", self.plugin_directory))
# TODO maybe dont chown this?
mkdir_as_user(path.join(home, "logs"))
mkdir_as_user(path.join(home, "logs", self.plugin_directory))
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@@ -60,14 +60,8 @@ class SandboxedPlugin:
environ["DECKY_USER_HOME"] = helpers.get_home_path() environ["DECKY_USER_HOME"] = helpers.get_home_path()
environ["DECKY_HOME"] = helpers.get_homebrew_path() environ["DECKY_HOME"] = helpers.get_homebrew_path()
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory) environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "settings"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory) environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "data"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory) environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "logs"))
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory) environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
environ["DECKY_PLUGIN_NAME"] = self.name environ["DECKY_PLUGIN_NAME"] = self.name
if self.version: if self.version:
+2 -1
View File
@@ -7,10 +7,11 @@
"build": "rollup -c", "build": "rollup -c",
"watch": "rollup -c -w", "watch": "rollup -c -w",
"lint": "prettier -c src", "lint": "prettier -c src",
"typecheck": "tsc --noEmit",
"format": "prettier -c src -w" "format": "prettier -c src -w"
}, },
"devDependencies": { "devDependencies": {
"@decky/api": "^1.0.3", "@decky/api": "^1.0.4",
"@rollup/plugin-commonjs": "^21.1.0", "@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-image": "^3.0.3", "@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
+10 -7
View File
@@ -35,8 +35,8 @@ dependencies:
devDependencies: devDependencies:
'@decky/api': '@decky/api':
specifier: ^1.0.3 specifier: ^1.0.4
version: 1.0.3 version: 1.0.4
'@rollup/plugin-commonjs': '@rollup/plugin-commonjs':
specifier: ^21.1.0 specifier: ^21.1.0
version: 21.1.0(rollup@2.79.1) version: 21.1.0(rollup@2.79.1)
@@ -318,8 +318,8 @@ packages:
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
dev: true dev: true
/@decky/api@1.0.3: /@decky/api@1.0.4:
resolution: {integrity: sha512-7hMKEHWcyz/bttx7DcKXqsOXcrtmC4CB6UwxRVrtlb/aolQtv1NVKHIEkIM6ND5hqTUU/VJ2HPUmCOwKm3Of0Q==} resolution: {integrity: sha512-YChHjlk//lOiIM2tlNSd6Qk9aduFJOtG+uRv1JaTzLewPRj4dDeupC+mbJJfarMGYa4nsLnJ6BsubTqboeb+VQ==}
dev: true dev: true
/@esbuild/aix-ppc64@0.20.2: /@esbuild/aix-ppc64@0.20.2:
@@ -1142,7 +1142,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001621 caniuse-lite: 1.0.30001623
electron-to-chromium: 1.4.783 electron-to-chromium: 1.4.783
node-releases: 2.0.14 node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.0) update-browserslist-db: 1.0.16(browserslist@4.23.0)
@@ -1190,8 +1190,8 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/caniuse-lite@1.0.30001621: /caniuse-lite@1.0.30001623:
resolution: {integrity: sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==} resolution: {integrity: sha512-X/XhAVKlpIxWPpgRTnlgZssJrF0m6YtRA0QDWgsBNT12uZM6LPRydR7ip405Y3t1LamD8cP2TZFEDZFBf5ApcA==}
dev: true dev: true
/ccount@2.0.1: /ccount@2.0.1:
@@ -1816,6 +1816,7 @@ packages:
/glob@7.2.3: /glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
dependencies: dependencies:
fs.realpath: 1.0.0 fs.realpath: 1.0.0
inflight: 1.0.6 inflight: 1.0.6
@@ -3174,6 +3175,7 @@ packages:
/rimraf@2.7.1: /rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
@@ -3181,6 +3183,7 @@ packages:
/rimraf@3.0.2: /rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
+37 -41
View File
@@ -1,17 +1,14 @@
import { sleep } from '@decky/ui'; import { sleep } from '@decky/ui';
import { ErrorInfo, FunctionComponent, useReducer, useState } from 'react'; import { FunctionComponent, useEffect, useReducer, useState } from 'react';
import { uninstallPlugin } from '../plugin'; import { uninstallPlugin } from '../plugin';
import { doRestart, doShutdown } from '../updater'; import { VerInfo, doRestart, doShutdown } from '../updater';
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
interface ReactErrorInfo {
error: Error;
info: ErrorInfo;
}
interface DeckyErrorBoundaryProps { interface DeckyErrorBoundaryProps {
error: ReactErrorInfo; error: ValveReactErrorInfo;
errorKey: string; errorKey: string;
identifier: string;
reset: () => void; reset: () => void;
} }
@@ -21,32 +18,6 @@ declare global {
} }
} }
const pluginErrorRegex = /\(http:\/\/localhost:1337\/plugins\/(.*)\//;
const pluginSourceMapErrorRegex = /\(decky:\/\/decky\/plugin\/(.*)\//;
const legacyPluginErrorRegex = /\(decky:\/\/decky\/legacy_plugin\/(.*)\/index.js/;
function getLikelyErrorSource(error: ReactErrorInfo): [source: string, wasPlugin: boolean] {
const pluginMatch = error.error.stack?.match(pluginErrorRegex);
if (pluginMatch) {
return [decodeURIComponent(pluginMatch[1]), true];
}
const pluginMatchViaMap = error.error.stack?.match(pluginSourceMapErrorRegex);
if (pluginMatchViaMap) {
return [decodeURIComponent(pluginMatchViaMap[1]), true];
}
const legacyPluginMatch = error.error.stack?.match(legacyPluginErrorRegex);
if (legacyPluginMatch) {
return [decodeURIComponent(legacyPluginMatch[1]), true];
}
if (error.error.stack?.includes('http://localhost:1337/')) {
return ['the Decky frontend', false];
}
return ['Steam', false];
}
export const startSSH = DeckyBackend.callable('utilities/start_ssh'); export const startSSH = DeckyBackend.callable('utilities/start_ssh');
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging'); export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
@@ -55,16 +26,28 @@ function ipToString(ip: number) {
} }
// Intentionally not localized since we can't really trust React here // Intentionally not localized since we can't really trust React here
const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, reset }) => { const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, identifier, reset }) => {
const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), ''); const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
const [actionsEnabled, setActionsEnabled] = useState<boolean>(true); const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
const [debugAllowed, setDebugAllowed] = useState<boolean>(true); const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
const [errorSource, wasCausedByPlugin] = getLikelyErrorSource(error); // Intentionally doesn't use DeckyState.
const [versionInfo, setVersionInfo] = useState<VerInfo>();
const [errorSource, wasCausedByPlugin] = getLikelyErrorSourceFromValveReactError(error);
useEffect(() => {
DeckyPluginLoader.updateVersion().then(setVersionInfo);
}, []);
return ( return (
<>
<style>
{`
*:has(> .deckyErrorBoundary) {
overflow: scroll !important;
}
`}
</style>
<div <div
style={{ style={{
overflow: 'scroll', overflow: 'auto',
marginLeft: '15px', marginLeft: '15px',
color: 'white', color: 'white',
fontSize: '16px', fontSize: '16px',
@@ -72,18 +55,24 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
backgroundColor: 'black', backgroundColor: 'black',
marginTop: '48px', // Incase this is a page marginTop: '48px', // Incase this is a page
}} }}
className="deckyErrorBoundary"
> >
<h1 <h1
style={{ style={{
fontSize: '20px', fontSize: '20px',
display: 'inline-block', display: 'inline-block',
marginTop: '15px',
userSelect: 'auto', userSelect: 'auto',
}} }}
> >
An error occured rendering this content. An error occured rendering this content.
</h1> </h1>
<p>This error likely occured in {getLikelyErrorSource(error)}.</p> <pre style={{}}>
<code>
{identifier && `Error Reference: ${identifier}`}
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
</code>
</pre>
<p>This error likely occured in {errorSource}.</p>
{actionLog?.length > 0 && ( {actionLog?.length > 0 && (
<pre> <pre>
<code> <code>
@@ -100,7 +89,13 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}> <button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
Retry Retry
</button> </button>
<button style={{ marginRight: '5px', padding: '5px' }} onClick={() => SteamClient.User.StartRestart()}> <button
style={{ marginRight: '5px', padding: '5px' }}
onClick={() => {
addLogLine('Restarting Steam...');
SteamClient.User.StartRestart();
}}
>
Restart Steam Restart Steam
</button> </button>
</div> </div>
@@ -195,6 +190,7 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
</code> </code>
</pre> </pre>
</div> </div>
</>
); );
}; };
@@ -1,5 +1,5 @@
import type { ToastData } from '@decky/api'; import type { ToastData } from '@decky/api';
import { FC, createContext, useContext, useEffect, useState } from 'react'; import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyToasterState { interface PublicDeckyToasterState {
toasts: Set<ToastData>; toasts: Set<ToastData>;
@@ -41,6 +41,7 @@ export const useDeckyToasterState = () => useContext(DeckyToasterContext);
interface Props { interface Props {
deckyToasterState: DeckyToasterState; deckyToasterState: DeckyToasterState;
children: ReactNode;
} }
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => { export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
+2 -2
View File
@@ -1,5 +1,5 @@
import { ButtonItem, Focusable, PanelSection, PanelSectionRow } from '@decky/ui'; import { ButtonItem, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
import { VFC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa'; import { FaEyeSlash } from 'react-icons/fa';
@@ -9,7 +9,7 @@ import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState'; import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView'; import TitleView from './TitleView';
const PluginView: VFC = () => { const PluginView: FC = () => {
const { hiddenPlugins } = useDeckyState(); const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState(); const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible(); const visible = useQuickAccessVisible();
@@ -1,10 +1,10 @@
import { FC, createContext, useContext, useState } from 'react'; import { FC, ReactNode, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false); const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState); export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<{ tab: any }> = ({ children, tab }) => { export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
const initial = tab.initialVisibility; const initial = tab.initialVisibility;
const [visible, setVisible] = useState<boolean>(initial); const [visible, setVisible] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this // HACK but i can't think of a better way to do this
+2 -2
View File
@@ -1,5 +1,5 @@
import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui'; import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
import { CSSProperties, VFC } from 'react'; import { CSSProperties, FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BsGearFill } from 'react-icons/bs'; import { BsGearFill } from 'react-icons/bs';
import { FaArrowLeft, FaStore } from 'react-icons/fa'; import { FaArrowLeft, FaStore } from 'react-icons/fa';
@@ -14,7 +14,7 @@ const titleStyles: CSSProperties = {
top: '0px', top: '0px',
}; };
const TitleView: VFC = () => { const TitleView: FC = () => {
const { activePlugin, closeActivePlugin } = useDeckyState(); const { activePlugin, closeActivePlugin } = useDeckyState();
const { t } = useTranslation(); const { t } = useTranslation();
+31 -6
View File
@@ -2,6 +2,7 @@ import { Patch, callOriginal, findModuleExport, replacePatch } from '@decky/ui';
import DeckyErrorBoundary from './components/DeckyErrorBoundary'; import DeckyErrorBoundary from './components/DeckyErrorBoundary';
import Logger from './logger'; import Logger from './logger';
import { getLikelyErrorSourceFromValveError } from './utils/errors';
declare global { declare global {
interface Window { interface Window {
@@ -11,6 +12,7 @@ declare global {
class ErrorBoundaryHook extends Logger { class ErrorBoundaryHook extends Logger {
private errorBoundaryPatch?: Patch; private errorBoundaryPatch?: Patch;
private errorCheckPatch?: Patch;
constructor() { constructor() {
super('ErrorBoundaryHook'); super('ErrorBoundaryHook');
@@ -35,13 +37,29 @@ class ErrorBoundaryHook extends Logger {
const errorReportingStore = initErrorReportingStore(); const errorReportingStore = initErrorReportingStore();
// NUH UH. // NUH UH.
Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', { // Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
get: () => false, // get: () => false,
}); // });
errorReportingStore.m_bEnabled = false; // errorReportingStore.m_bEnabled = false;
// @ts-ignore // @ts-ignore
// window.errorStore = errorReportingStore; window.errorStore = errorReportingStore;
const react15069WorkaroundRegex = / at .+\.componentDidCatch\..+\.callback /;
this.errorCheckPatch = replacePatch(Object.getPrototypeOf(errorReportingStore), 'BIsBlacklisted', (args: any[]) => {
const [errorSource, wasPlugin, shouldReport] = getLikelyErrorSourceFromValveError(args[0]);
this.debug('Caught an error', args, { errorSource, wasPlugin, shouldReport });
// react#15069 workaround. this took 2 hours to figure out.
if (
args[0]?.message?.[3]?.[0] &&
args[0]?.message?.[1]?.[0] == ' at console.error ' &&
react15069WorkaroundRegex.test(args[0].message[3][0])
) {
this.debug('ignoring early report caused by react#15069');
return true;
}
return shouldReport ? callOriginal : true;
});
const ValveErrorBoundary = findModuleExport( const ValveErrorBoundary = findModuleExport(
(e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch, (e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch,
@@ -53,8 +71,14 @@ class ErrorBoundaryHook extends Logger {
this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) { this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) {
if (this.state.error) { if (this.state.error) {
const store = Object.getPrototypeOf(this)?.constructor?.sm_ErrorReportingStore || errorReportingStore;
return ( return (
<DeckyErrorBoundary error={this.state.error} errorKey={this.state.errorKey} reset={() => this.Reset()} /> <DeckyErrorBoundary
error={this.state.error}
errorKey={this.props.errorKey}
identifier={`${store.product}_${store.version}_${this.state.identifierHash}`}
reset={() => this.Reset()}
/>
); );
} }
return callOriginal; return callOriginal;
@@ -62,6 +86,7 @@ class ErrorBoundaryHook extends Logger {
} }
deinit() { deinit() {
this.errorCheckPatch?.unpatch();
this.errorBoundaryPatch?.unpatch(); this.errorBoundaryPatch?.unpatch();
} }
} }
+48
View File
@@ -0,0 +1,48 @@
import { ErrorInfo } from 'react';
const pluginErrorRegex = /http:\/\/localhost:1337\/plugins\/([^\/]*)\//;
const pluginSourceMapErrorRegex = /decky:\/\/decky\/plugin\/([^\/]*)\//;
const legacyPluginErrorRegex = /decky:\/\/decky\/legacy_plugin\/([^\/]*)\/index.js/;
export interface ValveReactErrorInfo {
error: Error;
info: ErrorInfo;
}
export interface ValveError {
identifier: string;
identifierHash: string;
message: string | [func: string, src: string, line: number, column: number];
}
export type ErrorSource = [source: string, wasPlugin: boolean, shouldReportToValve: boolean];
export function getLikelyErrorSourceFromValveError(error: ValveError): ErrorSource {
return getLikelyErrorSource(JSON.stringify(error?.message));
}
export function getLikelyErrorSourceFromValveReactError(error: ValveReactErrorInfo): ErrorSource {
return getLikelyErrorSource(error?.error?.stack);
}
export function getLikelyErrorSource(error?: string): ErrorSource {
const pluginMatch = error?.match(pluginErrorRegex);
if (pluginMatch) {
return [decodeURIComponent(pluginMatch[1]), true, false];
}
const pluginMatchViaMap = error?.match(pluginSourceMapErrorRegex);
if (pluginMatchViaMap) {
return [decodeURIComponent(pluginMatchViaMap[1]), true, false];
}
const legacyPluginMatch = error?.match(legacyPluginErrorRegex);
if (legacyPluginMatch) {
return [decodeURIComponent(legacyPluginMatch[1]), true, false];
}
if (error?.includes('http://localhost:1337/')) {
return ['the Decky frontend', false, false];
}
return ['Steam', false, true];
}