From 00f41f9a389e9accfcad90fe825d2c8777136a8e Mon Sep 17 00:00:00 2001
From: Nightt <87569709+nightt5879@users.noreply.github.com>
Date: Wed, 1 Jul 2026 16:47:42 +0800
Subject: [PATCH 1/2] Expose background load errors in options
---
rollup.config.js | 1 +
source/background-loader.js | 89 +++++++++++++++++++++++++++++++++++++
source/background.ts | 4 ++
source/manifest.json | 4 +-
source/options.css | 7 +++
source/options.html | 9 ++--
source/options.tsx | 36 ++++++++++++++-
7 files changed, 143 insertions(+), 7 deletions(-)
create mode 100644 source/background-loader.js
diff --git a/rollup.config.js b/rollup.config.js
index 5b7898b0d148..12043ae600e5 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -119,6 +119,7 @@ const rollup = {
targets: [
{src: './source/manifest.json', dest: 'distribution'},
{src: './source/*.+(html|png)', dest: 'distribution/assets'},
+ {src: './source/background-loader.js', dest: 'distribution/assets'},
{src: './source/options-preflight.js', dest: 'distribution/assets'},
],
}),
diff --git a/source/background-loader.js b/source/background-loader.js
new file mode 100644
index 000000000000..ecf7e364b381
--- /dev/null
+++ b/source/background-loader.js
@@ -0,0 +1,89 @@
+const backgroundPageLoadErrorsKey = 'backgroundPageLoadErrors';
+const storage = chrome.storage.session ?? chrome.storage.local;
+
+async function storageGet(key) {
+ return new Promise((resolve, reject) => {
+ storage.get(key, result => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve(result);
+ }
+ });
+ });
+}
+
+async function storageRemove(key) {
+ return new Promise((resolve, reject) => {
+ storage.remove(key, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
+async function storageSet(value) {
+ return new Promise((resolve, reject) => {
+ storage.set(value, () => {
+ if (chrome.runtime.lastError) {
+ reject(new Error(chrome.runtime.lastError.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
+function serializeError(error) {
+ const serialized = {
+ message: error instanceof Error ? error.message : String(error),
+ };
+
+ if (error instanceof Error && error.stack) {
+ serialized.stack = error.stack;
+ }
+
+ return serialized;
+}
+
+async function storeBackgroundPageLoadError(error) {
+ const serialized = serializeError(error);
+ const storedErrors = await storageGet(backgroundPageLoadErrorsKey);
+ const errors = storedErrors[backgroundPageLoadErrorsKey] ?? [];
+
+ if (errors.some(storedError => storedError.message === serialized.message && storedError.stack === serialized.stack)) {
+ return;
+ }
+
+ await storageSet({
+ [backgroundPageLoadErrorsKey]: [
+ ...errors,
+ serialized,
+ ],
+ });
+}
+
+function backgroundPageLoadErrorListener(event) {
+ storeBackgroundPageLoadError(event.error ?? event.message);
+}
+
+globalThis.addEventListener('error', backgroundPageLoadErrorListener);
+Object.defineProperty(globalThis, 'removeBackgroundPageLoadErrorListener', {
+ configurable: true,
+ value() {
+ globalThis.removeEventListener('error', backgroundPageLoadErrorListener);
+ },
+});
+
+await storageRemove(backgroundPageLoadErrorsKey);
+
+try {
+ // eslint-disable-next-line import-x/extensions -- The loader is copied to `distribution/assets`, where the built file is `background.js`.
+ await import('./background.js');
+} catch (error) {
+ await storeBackgroundPageLoadError(error);
+ throw error;
+}
diff --git a/source/background.ts b/source/background.ts
index e05c04e7cebd..b0f32ce299ca 100644
--- a/source/background.ts
+++ b/source/background.ts
@@ -115,3 +115,7 @@ chrome.runtime.onInstalled.addListener(async () => {
// Call after the reset above just in case we nuked Safari's base permissions
await showWelcomePage();
});
+
+(globalThis as typeof globalThis & {
+ removeBackgroundPageLoadErrorListener?: () => void;
+}).removeBackgroundPageLoadErrorListener?.();
diff --git a/source/manifest.json b/source/manifest.json
index 614ddbe83c80..f31433a88a6b 100644
--- a/source/manifest.json
+++ b/source/manifest.json
@@ -37,10 +37,10 @@
},
"options_page": "assets/options.html",
"background": {
- "service_worker": "assets/background.js",
+ "service_worker": "assets/background-loader.js",
"type": "module",
"scripts": [
- "assets/background.js"
+ "assets/background-loader.js"
]
},
"content_scripts": [
diff --git a/source/options.css b/source/options.css
index 5c6a0e0d542f..ed9d16e7a708 100644
--- a/source/options.css
+++ b/source/options.css
@@ -239,3 +239,10 @@ summary:hover {
border: 1px solid var(--rgh-red);
background: color-mix(in srgb, var(--rgh-red) 10%, transparent);
}
+
+.js-background-fail-error {
+ overflow: auto;
+ white-space: pre-wrap;
+ margin: 1em 0 0;
+ font-size: 12px;
+}
diff --git a/source/options.html b/source/options.html
index e3d91c6274a8..f5e789557079 100644
--- a/source/options.html
+++ b/source/options.html
@@ -24,9 +24,12 @@
-
- It seems that the background page failed to load. This breaks some features. Please report it.
-
+
+
+ It seems that the background page failed to load. This breaks some features. Please report it.
+
+
+
🔑 Personal token
diff --git a/source/options.tsx b/source/options.tsx
index 7f524471e366..3ba17ff2f41c 100644
--- a/source/options.tsx
+++ b/source/options.tsx
@@ -11,6 +11,7 @@ import {messageRuntime} from 'webext-msg';
import {startFeatureIdentification} from './helpers/bisect.js';
import clearCacheHandler from './helpers/clear-cache-handler.js';
+import delay from './helpers/delay.js';
import {doesBrowserActionOpenOptions} from './helpers/feature-utils.js';
import {brokenFeatures, styleHotfixes} from './helpers/hotfix.js';
import isDevelopmentVersion from './helpers/is-development-version.js';
@@ -22,6 +23,23 @@ import initTokenValidation from './options/token-validation.js';
let syncedForm: SyncedForm | undefined;
const {version} = chrome.runtime.getManifest();
+const backgroundPageLoadErrorsKey = 'backgroundPageLoadErrors';
+const backgroundPageLoadErrorsStorage = chrome.storage.session ?? chrome.storage.local;
+const backgroundPageValidationTimeout = 3000;
+
+type BackgroundPageLoadError = {
+ message: string;
+ stack?: string;
+};
+
+async function getBackgroundPageLoadErrors(): Promise {
+ const storedErrors: Record = await backgroundPageLoadErrorsStorage.get(backgroundPageLoadErrorsKey);
+ return storedErrors[backgroundPageLoadErrorsKey] ?? [];
+}
+
+function formatBackgroundPageLoadError({message, stack}: BackgroundPageLoadError): string {
+ return stack ?? message;
+}
async function findFeatureHandler(this: HTMLButtonElement): Promise {
// TODO: Add support for GHE
@@ -105,9 +123,23 @@ async function fetchHotfixes(event: MouseEvent): Promise {
}
async function validateBackgroundPage(): Promise {
- if (await messageRuntime({ping: true}) !== 'pong') {
- $('.js-background-fail-banner').hidden = false;
+ try {
+ if (await Promise.race([
+ messageRuntime({ping: true}),
+ delay(backgroundPageValidationTimeout).then(() => undefined),
+ ]) === 'pong') {
+ return;
+ }
+ } catch {}
+
+ const errors = await getBackgroundPageLoadErrors();
+ if (errors.length > 0) {
+ const errorField = $('.js-background-fail-error');
+ errorField.textContent = errors.map(error => formatBackgroundPageLoadError(error)).join('\n\n');
+ errorField.hidden = false;
}
+
+ $('.js-background-fail-banner').hidden = false;
}
async function generateDom(): Promise {
From f793aa90147b7aa0ca1b9359ff330809c451ac47 Mon Sep 17 00:00:00 2001
From: Nightt <87569709+nightt5879@users.noreply.github.com>
Date: Fri, 3 Jul 2026 15:00:04 +0800
Subject: [PATCH 2/2] Simplify background load error storage
---
source/background-loader.js | 86 ++++---------------------------------
source/background.ts | 4 +-
source/options.tsx | 44 ++++---------------
3 files changed, 17 insertions(+), 117 deletions(-)
diff --git a/source/background-loader.js b/source/background-loader.js
index ecf7e364b381..890c45a6285b 100644
--- a/source/background-loader.js
+++ b/source/background-loader.js
@@ -1,89 +1,19 @@
-const backgroundPageLoadErrorsKey = 'backgroundPageLoadErrors';
-const storage = chrome.storage.session ?? chrome.storage.local;
-
-async function storageGet(key) {
- return new Promise((resolve, reject) => {
- storage.get(key, result => {
- if (chrome.runtime.lastError) {
- reject(new Error(chrome.runtime.lastError.message));
- } else {
- resolve(result);
- }
- });
- });
-}
-
-async function storageRemove(key) {
- return new Promise((resolve, reject) => {
- storage.remove(key, () => {
- if (chrome.runtime.lastError) {
- reject(new Error(chrome.runtime.lastError.message));
- } else {
- resolve();
- }
- });
- });
-}
-
-async function storageSet(value) {
- return new Promise((resolve, reject) => {
- storage.set(value, () => {
- if (chrome.runtime.lastError) {
- reject(new Error(chrome.runtime.lastError.message));
- } else {
- resolve();
- }
- });
- });
-}
-
-function serializeError(error) {
- const serialized = {
- message: error instanceof Error ? error.message : String(error),
- };
-
- if (error instanceof Error && error.stack) {
- serialized.stack = error.stack;
- }
-
- return serialized;
+function storeBackgroundLoadError(error) {
+ localStorage.backgroundLoadErrors ??= '';
+ localStorage.backgroundLoadErrors += `${error?.stack ?? error?.message ?? String(error)}\n\n`;
}
-async function storeBackgroundPageLoadError(error) {
- const serialized = serializeError(error);
- const storedErrors = await storageGet(backgroundPageLoadErrorsKey);
- const errors = storedErrors[backgroundPageLoadErrorsKey] ?? [];
-
- if (errors.some(storedError => storedError.message === serialized.message && storedError.stack === serialized.stack)) {
- return;
- }
-
- await storageSet({
- [backgroundPageLoadErrorsKey]: [
- ...errors,
- serialized,
- ],
- });
+function backgroundLoadErrorListener(event) {
+ storeBackgroundLoadError(event.error ?? event.message);
}
-function backgroundPageLoadErrorListener(event) {
- storeBackgroundPageLoadError(event.error ?? event.message);
-}
-
-globalThis.addEventListener('error', backgroundPageLoadErrorListener);
-Object.defineProperty(globalThis, 'removeBackgroundPageLoadErrorListener', {
- configurable: true,
- value() {
- globalThis.removeEventListener('error', backgroundPageLoadErrorListener);
- },
-});
-
-await storageRemove(backgroundPageLoadErrorsKey);
+globalThis.addEventListener('error', backgroundLoadErrorListener);
try {
// eslint-disable-next-line import-x/extensions -- The loader is copied to `distribution/assets`, where the built file is `background.js`.
await import('./background.js');
+ globalThis.removeEventListener('error', backgroundLoadErrorListener);
} catch (error) {
- await storeBackgroundPageLoadError(error);
+ storeBackgroundLoadError(error);
throw error;
}
diff --git a/source/background.ts b/source/background.ts
index b0f32ce299ca..25bc462f6b3c 100644
--- a/source/background.ts
+++ b/source/background.ts
@@ -116,6 +116,4 @@ chrome.runtime.onInstalled.addListener(async () => {
await showWelcomePage();
});
-(globalThis as typeof globalThis & {
- removeBackgroundPageLoadErrorListener?: () => void;
-}).removeBackgroundPageLoadErrorListener?.();
+delete localStorage.backgroundLoadErrors;
diff --git a/source/options.tsx b/source/options.tsx
index 3ba17ff2f41c..29dd3a7f8629 100644
--- a/source/options.tsx
+++ b/source/options.tsx
@@ -7,11 +7,8 @@ import {isChrome, isFirefox} from 'webext-detect';
import type {SyncedForm} from 'webext-options-sync-per-domain';
import 'webext-bugs/target-blank';
-import {messageRuntime} from 'webext-msg';
-
import {startFeatureIdentification} from './helpers/bisect.js';
import clearCacheHandler from './helpers/clear-cache-handler.js';
-import delay from './helpers/delay.js';
import {doesBrowserActionOpenOptions} from './helpers/feature-utils.js';
import {brokenFeatures, styleHotfixes} from './helpers/hotfix.js';
import isDevelopmentVersion from './helpers/is-development-version.js';
@@ -23,23 +20,6 @@ import initTokenValidation from './options/token-validation.js';
let syncedForm: SyncedForm | undefined;
const {version} = chrome.runtime.getManifest();
-const backgroundPageLoadErrorsKey = 'backgroundPageLoadErrors';
-const backgroundPageLoadErrorsStorage = chrome.storage.session ?? chrome.storage.local;
-const backgroundPageValidationTimeout = 3000;
-
-type BackgroundPageLoadError = {
- message: string;
- stack?: string;
-};
-
-async function getBackgroundPageLoadErrors(): Promise {
- const storedErrors: Record = await backgroundPageLoadErrorsStorage.get(backgroundPageLoadErrorsKey);
- return storedErrors[backgroundPageLoadErrorsKey] ?? [];
-}
-
-function formatBackgroundPageLoadError({message, stack}: BackgroundPageLoadError): string {
- return stack ?? message;
-}
async function findFeatureHandler(this: HTMLButtonElement): Promise {
// TODO: Add support for GHE
@@ -122,23 +102,15 @@ async function fetchHotfixes(event: MouseEvent): Promise {
}
}
-async function validateBackgroundPage(): Promise {
- try {
- if (await Promise.race([
- messageRuntime({ping: true}),
- delay(backgroundPageValidationTimeout).then(() => undefined),
- ]) === 'pong') {
- return;
- }
- } catch {}
-
- const errors = await getBackgroundPageLoadErrors();
- if (errors.length > 0) {
- const errorField = $('.js-background-fail-error');
- errorField.textContent = errors.map(error => formatBackgroundPageLoadError(error)).join('\n\n');
- errorField.hidden = false;
+function validateBackgroundPage(): void {
+ const backgroundLoadErrors = localStorage.backgroundLoadErrors?.trim();
+ if (!backgroundLoadErrors) {
+ return;
}
+ const errorField = $('.js-background-fail-error');
+ errorField.textContent = backgroundLoadErrors;
+ errorField.hidden = false;
$('.js-background-fail-banner').hidden = false;
}
@@ -170,7 +142,7 @@ async function generateDom(): Promise {
// Show stored CSS hotfixes
void showStoredCssHotfixes();
- void validateBackgroundPage();
+ validateBackgroundPage();
}
function addEventListeners(): void {