diff --git a/.eslintrc.json b/.eslintrc.json index e2197916..fca1449b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -111,9 +111,11 @@ "deepEqual": "readonly", "generateId": "readonly", "promiseAnimationFrame": "readonly", + "log": "readonly", "DynamicProperty": "readonly", "EventDispatcher": "readonly", - "EventListenerCollection": "readonly" + "EventListenerCollection": "readonly", + "Logger": "readonly" } }, { diff --git a/ext/js/app/content-script-main.js b/ext/js/app/content-script-main.js index a09e52ea..1c0c6d49 100644 --- a/ext/js/app/content-script-main.js +++ b/ext/js/app/content-script-main.js @@ -52,6 +52,6 @@ yomichan.ready(); } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index 74cc63d2..02707f41 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -276,7 +276,7 @@ class Frontend { this._showExtensionUnloaded(textSource); } } else { - yomichan.logError(error); + log.error(error); } } if (type !== null) { this._stopClearSelectionDelayed(); @@ -563,7 +563,7 @@ class Frontend { ); this._lastShowPromise.catch((error) => { if (yomichan.isExtensionUnloaded) { return; } - yomichan.logError(error); + log.error(error); }); return this._lastShowPromise; } diff --git a/ext/js/app/popup-proxy.js b/ext/js/app/popup-proxy.js index 19856e3f..63ecc254 100644 --- a/ext/js/app/popup-proxy.js +++ b/ext/js/app/popup-proxy.js @@ -201,7 +201,7 @@ class PopupProxy extends EventDispatcher { } this._frameOffsetUpdatedAt = now; } catch (e) { - yomichan.logError(e); + log.error(e); } finally { this._frameOffsetPromise = null; } diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 69c90aca..698724fc 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -204,7 +204,7 @@ class Backend { try { await this._dictionaryDatabase.prepare(); } catch (e) { - yomichan.logError(e); + log.error(e); } const deinflectionReasions = await this._fetchAsset('/data/deinflect.json', true); @@ -226,7 +226,7 @@ class Backend { this._sendMessageAllTabsIgnoreResponse('backendReady', {}); this._sendMessageIgnoreResponse({action: 'backendReady', params: {}}); } catch (e) { - yomichan.logError(e); + log.error(e); throw e; } finally { if (this._badgePrepareDelayTimer !== null) { @@ -358,7 +358,7 @@ class Backend { forwardPort.onDisconnect.addListener(cleanup); } catch (e) { port.disconnect(); - yomichan.logError(e); + log.error(e); } } @@ -612,7 +612,7 @@ class Backend { } _onApiLog({error, level, context}) { - yomichan.log(deserializeError(error), level, context); + log.log(deserializeError(error), level, context); } _onApiLogIndicatorClear() { diff --git a/ext/js/comm/cross-frame-api.js b/ext/js/comm/cross-frame-api.js index 997249c8..7dbfb411 100644 --- a/ext/js/comm/cross-frame-api.js +++ b/ext/js/comm/cross-frame-api.js @@ -111,7 +111,7 @@ class CrossFrameAPIPort extends EventDispatcher { _onAck(id) { const invocation = this._activeInvocations.get(id); if (typeof invocation === 'undefined') { - yomichan.logWarning(new Error(`Request ${id} not found for acknowledgement`)); + log.warn(new Error(`Request ${id} not found for acknowledgement`)); return; } @@ -140,7 +140,7 @@ class CrossFrameAPIPort extends EventDispatcher { _onResult(id, data) { const invocation = this._activeInvocations.get(id); if (typeof invocation === 'undefined') { - yomichan.logWarning(new Error(`Request ${id} not found`)); + log.warn(new Error(`Request ${id} not found`)); return; } @@ -269,7 +269,7 @@ class CrossFrameAPI { this._setupCommPort(otherTabId, otherFrameId, port); } catch (e) { port.disconnect(); - yomichan.logError(e); + log.error(e); } } diff --git a/ext/js/core.js b/ext/js/core.js index 9305739a..4384d9f0 100644 --- a/ext/js/core.js +++ b/ext/js/core.js @@ -607,3 +607,111 @@ class DynamicProperty extends EventDispatcher { this.trigger('change', {value}); } } + +/** + * This class handles logging of messages to the console and triggering + * an event for log calls. + */ +class Logger extends EventDispatcher { + /** + * Creates a new instance. + */ + constructor() { + super(); + this._extensionName = 'Yomichan'; + try { + const {name, version} = chrome.runtime.getManifest(); + this._extensionName = `${name} ${version}`; + } catch (e) { + // NOP + } + } + + /** + * Logs a generic error. This will trigger the 'log' event with the same arguments as the function invocation. + * @param error The error to log. This is typically an `Error` or `Error`-like object. + * @param level The level to log at. Values include `'info'`, `'debug'`, `'warn'`, and `'error'`. + * Other values will be logged at a non-error level. + * @param context An optional context object for the error which should typically include a `url` field. + */ + log(error, level, context=null) { + if (!isObject(context)) { + context = {url: location.href}; + } + + let errorString; + try { + if (typeof error === 'string') { + errorString = error; + } else { + errorString = error.toString(); + if (/^\[object \w+\]$/.test(errorString)) { + errorString = JSON.stringify(error); + } + } + } catch (e) { + errorString = `${error}`; + } + + let errorStack; + try { + errorStack = (typeof error.stack === 'string' ? error.stack.trimRight() : ''); + } catch (e) { + errorStack = ''; + } + + let errorData; + try { + errorData = error.data; + } catch (e) { + // NOP + } + + if (errorStack.startsWith(errorString)) { + errorString = errorStack; + } else if (errorStack.length > 0) { + errorString += `\n${errorStack}`; + } + + let message = `${this._extensionName} has encountered a problem.`; + message += `\nOriginating URL: ${context.url}\n`; + message += errorString; + if (typeof errorData !== 'undefined') { + message += `\nData: ${JSON.stringify(errorData, null, 4)}`; + } + message += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; + + switch (level) { + case 'info': console.info(message); break; + case 'debug': console.debug(message); break; + case 'warn': console.warn(message); break; + case 'error': console.error(message); break; + default: console.log(message); break; + } + + this.trigger('log', {error, level, context}); + } + + /** + * Logs a warning. This function invokes `log` internally. + * @param error The error to log. This is typically an `Error` or `Error`-like object. + * @param context An optional context object for the error which should typically include a `url` field. + */ + warn(error, context=null) { + this.log(error, 'warn', context); + } + + /** + * Logs an error. This function invokes `log` internally. + * @param error The error to log. This is typically an `Error` or `Error`-like object. + * @param context An optional context object for the error which should typically include a `url` field. + */ + error(error, context=null) { + this.log(error, 'error', context); + } +} + +/** + * This object is the default logger used by the runtime. + */ +const log = new Logger(); diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 553c17e7..b7477bb8 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -272,7 +272,7 @@ class Display extends EventDispatcher { onError(error) { if (yomichan.isExtensionUnloaded) { return; } - yomichan.logError(error); + log.error(error); } getOptions() { @@ -1552,7 +1552,7 @@ class Display extends EventDispatcher { } await this._frontendSetupPromise; } catch (e) { - yomichan.logError(e); + log.error(e); return; } finally { this._frontendSetupPromise = null; @@ -1721,7 +1721,7 @@ class Display extends EventDispatcher { _onDefinitionTextScannerSearched({type, definitions, sentence, textSource, optionsContext, error}) { if (error !== null && !yomichan.isExtensionUnloaded) { - yomichan.logError(error); + log.error(error); } if (type === null) { return; } diff --git a/ext/js/display/popup-main.js b/ext/js/display/popup-main.js index 24be6c01..55f19984 100644 --- a/ext/js/display/popup-main.js +++ b/ext/js/display/popup-main.js @@ -49,6 +49,6 @@ yomichan.ready(); } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/display/query-parser.js b/ext/js/display/query-parser.js index 29401657..d6a3b4da 100644 --- a/ext/js/display/query-parser.js +++ b/ext/js/display/query-parser.js @@ -89,7 +89,7 @@ class QueryParser extends EventDispatcher { _onSearched(e) { const {error} = e; if (error !== null) { - yomichan.logError(error); + log.error(error); return; } if (e.type === null) { return; } diff --git a/ext/js/display/search-main.js b/ext/js/display/search-main.js index 08645833..5ad50500 100644 --- a/ext/js/display/search-main.js +++ b/ext/js/display/search-main.js @@ -50,6 +50,6 @@ yomichan.ready(); } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/general/task-accumulator.js b/ext/js/general/task-accumulator.js index 82691b43..3f3d8ff7 100644 --- a/ext/js/general/task-accumulator.js +++ b/ext/js/general/task-accumulator.js @@ -69,7 +69,7 @@ class TaskAccumulator { ]; await this._runTasks(allTasks); } catch (e) { - yomichan.logError(e); + log.error(e); } } diff --git a/ext/js/language/dictionary-database.js b/ext/js/language/dictionary-database.js index b363ed25..62c22289 100644 --- a/ext/js/language/dictionary-database.js +++ b/ext/js/language/dictionary-database.js @@ -122,7 +122,7 @@ class DictionaryDatabase { await Database.deleteDatabase(this._dbName); result = true; } catch (e) { - yomichan.logError(e); + log.error(e); } await this.prepare(); return result; diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js index e91498ba..073b1a6c 100644 --- a/ext/js/language/text-scanner.js +++ b/ext/js/language/text-scanner.js @@ -830,7 +830,7 @@ class TextScanner extends EventDispatcher { } } } catch (e) { - yomichan.logError(e); + log.error(e); } finally { this._pendingLookup = false; } diff --git a/ext/js/pages/info-main.js b/ext/js/pages/info-main.js index 45c28d25..ca7d792e 100644 --- a/ext/js/pages/info-main.js +++ b/ext/js/pages/info-main.js @@ -120,6 +120,6 @@ function getOperatingSystemDisplayName(os) { document.documentElement.dataset.loaded = 'true'; } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/pages/permissions-main.js b/ext/js/pages/permissions-main.js index 0cb37e93..782ea9ec 100644 --- a/ext/js/pages/permissions-main.js +++ b/ext/js/pages/permissions-main.js @@ -96,6 +96,6 @@ function setupPermissionsToggles() { document.documentElement.dataset.loaded = 'true'; } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js index 5a6bb2a5..27d0af6a 100644 --- a/ext/js/pages/welcome-main.js +++ b/ext/js/pages/welcome-main.js @@ -81,6 +81,6 @@ async function setupGenericSettingsController(genericSettingController) { const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); settingsDisplayController.prepare(); } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/settings/anki-controller.js b/ext/js/settings/anki-controller.js index db3e3c14..26cab68f 100644 --- a/ext/js/settings/anki-controller.js +++ b/ext/js/settings/anki-controller.js @@ -638,7 +638,7 @@ class AnkiCardController { try { await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true); } catch (e) { - yomichan.logError(e); + log.error(e); } } diff --git a/ext/js/settings/backup-controller.js b/ext/js/settings/backup-controller.js index 117f2422..649645d4 100644 --- a/ext/js/settings/backup-controller.js +++ b/ext/js/settings/backup-controller.js @@ -173,7 +173,7 @@ class BackupController { } _showSettingsImportError(error) { - yomichan.logError(error); + log.error(error); document.querySelector('#settings-import-error-message').textContent = `${error}`; this._settingsImportErrorModal.setVisible(true); } @@ -412,7 +412,7 @@ class BackupController { try { await this._settingsImportSetOptionsFull(optionsFull); } catch (e) { - yomichan.logError(e); + log.error(e); } } } diff --git a/ext/js/settings/dictionary-controller.js b/ext/js/settings/dictionary-controller.js index 78173202..e12017f2 100644 --- a/ext/js/settings/dictionary-controller.js +++ b/ext/js/settings/dictionary-controller.js @@ -476,7 +476,7 @@ class DictionaryController { await this._deleteDictionaryInternal(dictionaryTitle, onProgress); await this._deleteDictionarySettings(dictionaryTitle); } catch (e) { - yomichan.logError(e); + log.error(e); } finally { prevention.end(); for (const progress of progressContainers) { progress.hidden = true; } diff --git a/ext/js/settings/dictionary-import-controller.js b/ext/js/settings/dictionary-import-controller.js index 00eb7b95..1389b7f0 100644 --- a/ext/js/settings/dictionary-import-controller.js +++ b/ext/js/settings/dictionary-import-controller.js @@ -253,7 +253,7 @@ class DictionaryImportController { _showErrors(errors) { const uniqueErrors = new Map(); for (const error of errors) { - yomichan.logError(error); + log.error(error); const errorString = this._errorToString(error); let count = uniqueErrors.get(errorString); if (typeof count === 'undefined') { diff --git a/ext/js/settings/main.js b/ext/js/settings/main.js index 51cd5f27..9785ee0e 100644 --- a/ext/js/settings/main.js +++ b/ext/js/settings/main.js @@ -104,6 +104,6 @@ async function setupEnvironmentInfo() { yomichan.ready(); } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/settings/pitch-accents-preview-main.js b/ext/js/settings/pitch-accents-preview-main.js index fe3bd36d..d9d56727 100644 --- a/ext/js/settings/pitch-accents-preview-main.js +++ b/ext/js/settings/pitch-accents-preview-main.js @@ -30,6 +30,6 @@ await displayGenerator.prepare(); displayGenerator.preparePitchAccents(); } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/settings/popup-preview-frame-main.js b/ext/js/settings/popup-preview-frame-main.js index a62e8d28..80e248be 100644 --- a/ext/js/settings/popup-preview-frame-main.js +++ b/ext/js/settings/popup-preview-frame-main.js @@ -38,6 +38,6 @@ document.documentElement.dataset.loaded = 'true'; } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/settings/settings-main.js b/ext/js/settings/settings-main.js index a5bb642c..273142cd 100644 --- a/ext/js/settings/settings-main.js +++ b/ext/js/settings/settings-main.js @@ -149,6 +149,6 @@ async function setupGenericSettingsController(genericSettingController) { const settingsDisplayController = new SettingsDisplayController(settingsController, modalController); settingsDisplayController.prepare(); } catch (e) { - yomichan.logError(e); + log.error(e); } })(); diff --git a/ext/js/yomichan.js b/ext/js/yomichan.js index 73deeab9..107694e9 100644 --- a/ext/js/yomichan.js +++ b/ext/js/yomichan.js @@ -103,7 +103,7 @@ class Yomichan extends EventDispatcher { this.sendMessage({action: 'requestBackendReadySignal'}); await this._isBackendReadyPromise; - this.on('log', this._onForwardLog.bind(this)); + log.on('log', this._onForwardLog.bind(this)); } } @@ -156,68 +156,6 @@ class Yomichan extends EventDispatcher { }); } - logWarning(error) { - this.log(error, 'warn'); - } - - logError(error) { - this.log(error, 'error'); - } - - log(error, level, context=null) { - if (!isObject(context)) { - context = this._getLogContext(); - } - - let errorString; - try { - errorString = error.toString(); - if (/^\[object \w+\]$/.test(errorString)) { - errorString = JSON.stringify(error); - } - } catch (e) { - errorString = `${error}`; - } - - let errorStack; - try { - errorStack = (typeof error.stack === 'string' ? error.stack.trimRight() : ''); - } catch (e) { - errorStack = ''; - } - - let errorData; - try { - errorData = error.data; - } catch (e) { - // NOP - } - - if (errorStack.startsWith(errorString)) { - errorString = errorStack; - } else if (errorStack.length > 0) { - errorString += `\n${errorStack}`; - } - - let message = `${this._extensionName} has encountered a problem.`; - message += `\nOriginating URL: ${context.url}\n`; - message += errorString; - if (typeof errorData !== 'undefined') { - message += `\nData: ${JSON.stringify(errorData, null, 4)}`; - } - message += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; - - switch (level) { - case 'info': console.info(message); break; - case 'debug': console.debug(message); break; - case 'warn': console.warn(message); break; - case 'error': console.error(message); break; - default: console.log(message); break; - } - - this.trigger('log', {error, level, context}); - } - sendMessage(...args) { try { return chrome.runtime.sendMessage(...args);