summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/typecheck.yml2
-rw-r--r--backend/decky_loader/plugin/plugin.py10
-rw-r--r--backend/decky_loader/plugin/sandboxed_plugin.py6
-rw-r--r--frontend/package.json3
-rw-r--r--frontend/pnpm-lock.yaml17
-rw-r--r--frontend/src/components/DeckyErrorBoundary.tsx278
-rw-r--r--frontend/src/components/DeckyToasterState.tsx3
-rw-r--r--frontend/src/components/PluginView.tsx4
-rw-r--r--frontend/src/components/QuickAccessVisibleState.tsx4
-rw-r--r--frontend/src/components/TitleView.tsx4
-rw-r--r--frontend/src/errorboundary-hook.tsx37
-rw-r--r--frontend/src/utils/errors.ts48
12 files changed, 247 insertions, 169 deletions
diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml
index 181a4211..763d8590 100644
--- a/.github/workflows/typecheck.yml
+++ b/.github/workflows/typecheck.yml
@@ -41,4 +41,4 @@ jobs:
- name: Run tsc (TypeScript)
working-directory: frontend
- run: $(pnpm bin)/tsc --noEmit \ No newline at end of file
+ run: pnpm run typecheck \ No newline at end of file
diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py
index 75e52c6a..aff35e5c 100644
--- a/backend/decky_loader/plugin/plugin.py
+++ b/backend/decky_loader/plugin/plugin.py
@@ -8,6 +8,7 @@ from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType
from ..enums import PluginLoadType
from ..localplatform.localsocket import LocalSocket
+from ..helpers import get_homebrew_path, mkdir_as_user
from typing import Any, Callable, Coroutine, Dict, List
@@ -50,6 +51,15 @@ class PluginWrapper:
# TODO enable this after websocket release
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:
return self.name
diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py
index 6c2bcee2..cb149816 100644
--- a/backend/decky_loader/plugin/sandboxed_plugin.py
+++ b/backend/decky_loader/plugin/sandboxed_plugin.py
@@ -60,14 +60,8 @@ class SandboxedPlugin:
environ["DECKY_USER_HOME"] = helpers.get_home_path()
environ["DECKY_HOME"] = helpers.get_homebrew_path()
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)
- 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)
- 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_NAME"] = self.name
if self.version:
diff --git a/frontend/package.json b/frontend/package.json
index 45be0b98..2fb9c617 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,10 +7,11 @@
"build": "rollup -c",
"watch": "rollup -c -w",
"lint": "prettier -c src",
+ "typecheck": "tsc --noEmit",
"format": "prettier -c src -w"
},
"devDependencies": {
- "@decky/api": "^1.0.3",
+ "@decky/api": "^1.0.4",
"@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-json": "^4.1.0",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index e3a24143..10ba5c2c 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -35,8 +35,8 @@ dependencies:
devDependencies:
'@decky/api':
- specifier: ^1.0.3
- version: 1.0.3
+ specifier: ^1.0.4
+ version: 1.0.4
'@rollup/plugin-commonjs':
specifier: ^21.1.0
version: 21.1.0(rollup@2.79.1)
@@ -318,8 +318,8 @@ packages:
to-fast-properties: 2.0.0
dev: true
- /@decky/api@1.0.3:
- resolution: {integrity: sha512-7hMKEHWcyz/bttx7DcKXqsOXcrtmC4CB6UwxRVrtlb/aolQtv1NVKHIEkIM6ND5hqTUU/VJ2HPUmCOwKm3Of0Q==}
+ /@decky/api@1.0.4:
+ resolution: {integrity: sha512-YChHjlk//lOiIM2tlNSd6Qk9aduFJOtG+uRv1JaTzLewPRj4dDeupC+mbJJfarMGYa4nsLnJ6BsubTqboeb+VQ==}
dev: true
/@esbuild/aix-ppc64@0.20.2:
@@ -1142,7 +1142,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
- caniuse-lite: 1.0.30001621
+ caniuse-lite: 1.0.30001623
electron-to-chromium: 1.4.783
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.0)
@@ -1190,8 +1190,8 @@ packages:
engines: {node: '>=4'}
dev: true
- /caniuse-lite@1.0.30001621:
- resolution: {integrity: sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==}
+ /caniuse-lite@1.0.30001623:
+ resolution: {integrity: sha512-X/XhAVKlpIxWPpgRTnlgZssJrF0m6YtRA0QDWgsBNT12uZM6LPRydR7ip405Y3t1LamD8cP2TZFEDZFBf5ApcA==}
dev: true
/ccount@2.0.1:
@@ -1816,6 +1816,7 @@ packages:
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Glob versions prior to v9 are no longer supported
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
@@ -3174,6 +3175,7 @@ packages:
/rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
dependencies:
glob: 7.2.3
@@ -3181,6 +3183,7 @@ packages:
/rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
dependencies:
glob: 7.2.3
diff --git a/frontend/src/components/DeckyErrorBoundary.tsx b/frontend/src/components/DeckyErrorBoundary.tsx
index a851b2e1..6fe234b7 100644
--- a/frontend/src/components/DeckyErrorBoundary.tsx
+++ b/frontend/src/components/DeckyErrorBoundary.tsx
@@ -1,17 +1,14 @@
import { sleep } from '@decky/ui';
-import { ErrorInfo, FunctionComponent, useReducer, useState } from 'react';
+import { FunctionComponent, useEffect, useReducer, useState } from 'react';
import { uninstallPlugin } from '../plugin';
-import { doRestart, doShutdown } from '../updater';
-
-interface ReactErrorInfo {
- error: Error;
- info: ErrorInfo;
-}
+import { VerInfo, doRestart, doShutdown } from '../updater';
+import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
interface DeckyErrorBoundaryProps {
- error: ReactErrorInfo;
+ error: ValveReactErrorInfo;
errorKey: string;
+ identifier: string;
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 starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
@@ -55,146 +26,171 @@ function ipToString(ip: number) {
}
// 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 [actionsEnabled, setActionsEnabled] = 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 (
- <div
- style={{
- overflow: 'scroll',
- marginLeft: '15px',
- color: 'white',
- fontSize: '16px',
- userSelect: 'auto',
- backgroundColor: 'black',
- marginTop: '48px', // Incase this is a page
- }}
- >
- <h1
+ <>
+ <style>
+ {`
+ *:has(> .deckyErrorBoundary) {
+ overflow: scroll !important;
+ }
+ `}
+ </style>
+ <div
style={{
- fontSize: '20px',
- display: 'inline-block',
- marginTop: '15px',
+ overflow: 'auto',
+ marginLeft: '15px',
+ color: 'white',
+ fontSize: '16px',
userSelect: 'auto',
+ backgroundColor: 'black',
+ marginTop: '48px', // Incase this is a page
}}
+ className="deckyErrorBoundary"
>
- ⚠️ An error occured rendering this content.
- </h1>
- <p>This error likely occured in {getLikelyErrorSource(error)}.</p>
- {actionLog?.length > 0 && (
- <pre>
+ <h1
+ style={{
+ fontSize: '20px',
+ display: 'inline-block',
+ userSelect: 'auto',
+ }}
+ >
+ ⚠️ An error occured rendering this content.
+ </h1>
+ <pre style={{}}>
<code>
- Running actions...
- {actionLog}
+ {identifier && `Error Reference: ${identifier}`}
+ {versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
</code>
</pre>
- )}
- {actionsEnabled && (
- <>
- <h3>Actions: </h3>
- <p>Use the touch screen.</p>
- <div style={{ display: 'block', marginBottom: '5px' }}>
- <button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
- Retry
- </button>
- <button style={{ marginRight: '5px', padding: '5px' }} onClick={() => SteamClient.User.StartRestart()}>
- Restart Steam
- </button>
- </div>
- <div style={{ display: 'block', marginBottom: '5px' }}>
- <button
- style={{ marginRight: '5px', padding: '5px' }}
- onClick={async () => {
- setActionsEnabled(false);
- addLogLine('Restarting Decky...');
- doRestart();
- await sleep(2000);
- addLogLine('Reloading UI...');
- }}
- >
- Restart Decky
- </button>
- <button
- style={{ marginRight: '5px', padding: '5px' }}
- onClick={async () => {
- setActionsEnabled(false);
- addLogLine('Stopping Decky...');
- doShutdown();
- await sleep(5000);
- addLogLine('Restarting Steam...');
- SteamClient.User.StartRestart();
- }}
- >
- Disable Decky until next boot
- </button>
- </div>
- {debugAllowed && (
+ <p>This error likely occured in {errorSource}.</p>
+ {actionLog?.length > 0 && (
+ <pre>
+ <code>
+ Running actions...
+ {actionLog}
+ </code>
+ </pre>
+ )}
+ {actionsEnabled && (
+ <>
+ <h3>Actions: </h3>
+ <p>Use the touch screen.</p>
<div style={{ display: 'block', marginBottom: '5px' }}>
+ <button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
+ Retry
+ </button>
<button
style={{ marginRight: '5px', padding: '5px' }}
- onClick={async () => {
- setDebugAllowed(false);
- addLogLine('Enabling CEF debugger forwarding...');
- await starrCEFForwarding();
- addLogLine('Enabling SSH...');
- await startSSH();
- addLogLine('Ready for debugging!');
- if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
- const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
- addLogLine(`CEF Debugger: http://${ip}:8081`);
- addLogLine(`SSH: deck@${ip}`);
- }
+ onClick={() => {
+ addLogLine('Restarting Steam...');
+ SteamClient.User.StartRestart();
}}
>
- Allow remote debugging and SSH until next boot
+ Restart Steam
</button>
</div>
- )}
- {wasCausedByPlugin && (
<div style={{ display: 'block', marginBottom: '5px' }}>
- {'\n'}
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
- addLogLine(`Uninstalling ${errorSource}...`);
- await uninstallPlugin(errorSource);
- await DeckyPluginLoader.frozenPluginsService.invalidate();
- await DeckyPluginLoader.hiddenPluginsService.invalidate();
- await sleep(1000);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
+ addLogLine('Reloading UI...');
+ }}
+ >
+ Restart Decky
+ </button>
+ <button
+ style={{ marginRight: '5px', padding: '5px' }}
+ onClick={async () => {
+ setActionsEnabled(false);
+ addLogLine('Stopping Decky...');
+ doShutdown();
+ await sleep(5000);
addLogLine('Restarting Steam...');
- await sleep(500);
SteamClient.User.StartRestart();
}}
>
- Uninstall {errorSource} and restart Decky
+ Disable Decky until next boot
</button>
</div>
- )}
- </>
- )}
+ {debugAllowed && (
+ <div style={{ display: 'block', marginBottom: '5px' }}>
+ <button
+ style={{ marginRight: '5px', padding: '5px' }}
+ onClick={async () => {
+ setDebugAllowed(false);
+ addLogLine('Enabling CEF debugger forwarding...');
+ await starrCEFForwarding();
+ addLogLine('Enabling SSH...');
+ await startSSH();
+ addLogLine('Ready for debugging!');
+ if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
+ const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
+ addLogLine(`CEF Debugger: http://${ip}:8081`);
+ addLogLine(`SSH: deck@${ip}`);
+ }
+ }}
+ >
+ Allow remote debugging and SSH until next boot
+ </button>
+ </div>
+ )}
+ {wasCausedByPlugin && (
+ <div style={{ display: 'block', marginBottom: '5px' }}>
+ {'\n'}
+ <button
+ style={{ marginRight: '5px', padding: '5px' }}
+ onClick={async () => {
+ setActionsEnabled(false);
+ addLogLine(`Uninstalling ${errorSource}...`);
+ await uninstallPlugin(errorSource);
+ await DeckyPluginLoader.frozenPluginsService.invalidate();
+ await DeckyPluginLoader.hiddenPluginsService.invalidate();
+ await sleep(1000);
+ addLogLine('Restarting Decky...');
+ doRestart();
+ await sleep(2000);
+ addLogLine('Restarting Steam...');
+ await sleep(500);
+ SteamClient.User.StartRestart();
+ }}
+ >
+ Uninstall {errorSource} and restart Decky
+ </button>
+ </div>
+ )}
+ </>
+ )}
- <pre
- style={{
- marginTop: '15px',
- opacity: 0.7,
- userSelect: 'auto',
- }}
- >
- <code>
- {error.error.stack}
- {'\n\n'}
- Component Stack:
- {error.info.componentStack}
- </code>
- </pre>
- </div>
+ <pre
+ style={{
+ marginTop: '15px',
+ opacity: 0.7,
+ userSelect: 'auto',
+ }}
+ >
+ <code>
+ {error.error.stack}
+ {'\n\n'}
+ Component Stack:
+ {error.info.componentStack}
+ </code>
+ </pre>
+ </div>
+ </>
);
};
diff --git a/frontend/src/components/DeckyToasterState.tsx b/frontend/src/components/DeckyToasterState.tsx
index 8d0a5d45..715ed76d 100644
--- a/frontend/src/components/DeckyToasterState.tsx
+++ b/frontend/src/components/DeckyToasterState.tsx
@@ -1,5 +1,5 @@
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 {
toasts: Set<ToastData>;
@@ -41,6 +41,7 @@ export const useDeckyToasterState = () => useContext(DeckyToasterContext);
interface Props {
deckyToasterState: DeckyToasterState;
+ children: ReactNode;
}
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx
index ce20ac4a..997e576b 100644
--- a/frontend/src/components/PluginView.tsx
+++ b/frontend/src/components/PluginView.tsx
@@ -1,5 +1,5 @@
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 { FaEyeSlash } from 'react-icons/fa';
@@ -9,7 +9,7 @@ import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';
-const PluginView: VFC = () => {
+const PluginView: FC = () => {
const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible();
diff --git a/frontend/src/components/QuickAccessVisibleState.tsx b/frontend/src/components/QuickAccessVisibleState.tsx
index 1bfe0e65..f5c05061 100644
--- a/frontend/src/components/QuickAccessVisibleState.tsx
+++ b/frontend/src/components/QuickAccessVisibleState.tsx
@@ -1,10 +1,10 @@
-import { FC, createContext, useContext, useState } from 'react';
+import { FC, ReactNode, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false);
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 [visible, setVisible] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this
diff --git a/frontend/src/components/TitleView.tsx b/frontend/src/components/TitleView.tsx
index c49e6df6..8b45aae4 100644
--- a/frontend/src/components/TitleView.tsx
+++ b/frontend/src/components/TitleView.tsx
@@ -1,5 +1,5 @@
import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
-import { CSSProperties, VFC } from 'react';
+import { CSSProperties, FC } from 'react';
import { useTranslation } from 'react-i18next';
import { BsGearFill } from 'react-icons/bs';
import { FaArrowLeft, FaStore } from 'react-icons/fa';
@@ -14,7 +14,7 @@ const titleStyles: CSSProperties = {
top: '0px',
};
-const TitleView: VFC = () => {
+const TitleView: FC = () => {
const { activePlugin, closeActivePlugin } = useDeckyState();
const { t } = useTranslation();
diff --git a/frontend/src/errorboundary-hook.tsx b/frontend/src/errorboundary-hook.tsx
index 175b3ff6..6963f207 100644
--- a/frontend/src/errorboundary-hook.tsx
+++ b/frontend/src/errorboundary-hook.tsx
@@ -2,6 +2,7 @@ import { Patch, callOriginal, findModuleExport, replacePatch } from '@decky/ui';
import DeckyErrorBoundary from './components/DeckyErrorBoundary';
import Logger from './logger';
+import { getLikelyErrorSourceFromValveError } from './utils/errors';
declare global {
interface Window {
@@ -11,6 +12,7 @@ declare global {
class ErrorBoundaryHook extends Logger {
private errorBoundaryPatch?: Patch;
+ private errorCheckPatch?: Patch;
constructor() {
super('ErrorBoundaryHook');
@@ -35,13 +37,29 @@ class ErrorBoundaryHook extends Logger {
const errorReportingStore = initErrorReportingStore();
// NUH UH.
- Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
- get: () => false,
- });
- errorReportingStore.m_bEnabled = false;
+ // Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
+ // get: () => false,
+ // });
+ // errorReportingStore.m_bEnabled = false;
// @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(
(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) {
if (this.state.error) {
+ const store = Object.getPrototypeOf(this)?.constructor?.sm_ErrorReportingStore || errorReportingStore;
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;
@@ -62,6 +86,7 @@ class ErrorBoundaryHook extends Logger {
}
deinit() {
+ this.errorCheckPatch?.unpatch();
this.errorBoundaryPatch?.unpatch();
}
}
diff --git a/frontend/src/utils/errors.ts b/frontend/src/utils/errors.ts
new file mode 100644
index 00000000..0bf82986
--- /dev/null
+++ b/frontend/src/utils/errors.ts
@@ -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];
+}