Error logging refactoring (#454)

* Create new logging methods on yomichan object

* Use new yomichan.logError instead of global logError

* Remove old logError

* Handle unhandledrejection events

* Add addEventListener stub

* Update log function

* Update error conversion to support more types

* Add log event

* Add API log function

* Log errors to the backend

* Make error/warning logs update the badge

* Clear log error indicator on extension button click

* Log correct URL on the background page

* Fix incorrect error conversion

* Remove unhandledrejection handling

Firefox doesn't support it properly.

* Remove unused argument type from log function

* Improve function name

* Change console.warn to yomichan.logWarning

* Move log forwarding initialization into main scripts
This commit is contained in:
toasted-nutbread 2020-04-26 16:55:25 -04:00 committed by GitHub
parent ca033a87a0
commit 5b96559df8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 186 additions and 45 deletions

View File

@ -80,7 +80,6 @@
"yomichan": "readonly",
"errorToJson": "readonly",
"jsonToError": "readonly",
"logError": "readonly",
"isObject": "readonly",
"hasOwn": "readonly",
"toIterable": "readonly",

View File

@ -78,6 +78,7 @@ class Backend {
this._isPrepared = false;
this._prepareError = false;
this._badgePrepareDelayTimer = null;
this._logErrorLevel = null;
this._messageHandlers = new Map([
['yomichanCoreReady', {handler: this._onApiYomichanCoreReady.bind(this), async: false}],
@ -112,7 +113,9 @@ class Backend {
['getDictionaryInfo', {handler: this._onApiGetDictionaryInfo.bind(this), async: true}],
['getDictionaryCounts', {handler: this._onApiGetDictionaryCounts.bind(this), async: true}],
['purgeDatabase', {handler: this._onApiPurgeDatabase.bind(this), async: true}],
['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}]
['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}],
['log', {handler: this._onApiLog.bind(this), async: false}],
['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}]
]);
this._commandHandlers = new Map([
@ -164,7 +167,7 @@ class Backend {
this._isPrepared = true;
} catch (e) {
this._prepareError = true;
logError(e);
yomichan.logError(e);
throw e;
} finally {
if (this._badgePrepareDelayTimer !== null) {
@ -260,7 +263,7 @@ class Backend {
this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, utilIsolate(options));
} catch (e) {
// This shouldn't happen, but catch errors just in case of bugs
logError(e);
yomichan.logError(e);
}
}
@ -767,8 +770,34 @@ class Backend {
return await this.database.getMedia(targets);
}
_onApiLog({error, level, context}) {
yomichan.log(jsonToError(error), level, context);
const levelValue = this._getErrorLevelValue(level);
if (levelValue <= this._getErrorLevelValue(this._logErrorLevel)) { return; }
this._logErrorLevel = level;
this._updateBadge();
}
_onApiLogIndicatorClear() {
if (this._logErrorLevel === null) { return; }
this._logErrorLevel = null;
this._updateBadge();
}
// Command handlers
_getErrorLevelValue(errorLevel) {
switch (errorLevel) {
case 'info': return 0;
case 'debug': return 0;
case 'warn': return 1;
case 'error': return 2;
default: return 0;
}
}
async _onCommandSearch(params) {
const {mode='existingOrNewTab', query} = params || {};
@ -890,7 +919,20 @@ class Backend {
let color = null;
let status = null;
if (!this._isPrepared) {
if (this._logErrorLevel !== null) {
switch (this._logErrorLevel) {
case 'error':
text = '!!';
color = '#f04e4e';
status = 'Error';
break;
default: // 'warn'
text = '!';
color = '#f0ad4e';
status = 'Warning';
break;
}
} else if (!this._isPrepared) {
if (this._prepareError) {
text = '!!';
color = '#f04e4e';

View File

@ -17,7 +17,9 @@
/* global
* apiCommandExec
* apiForwardLogsToBackend
* apiGetEnvironmentInfo
* apiLogIndicatorClear
* apiOptionsGet
*/
@ -52,8 +54,11 @@ function setupButtonEvents(selector, command, url) {
}
async function mainInner() {
apiForwardLogsToBackend();
await yomichan.prepare();
await apiLogIndicatorClear();
showExtensionInfo();
apiGetEnvironmentInfo().then(({browser}) => {

View File

@ -104,7 +104,7 @@ class Database {
});
return true;
} catch (e) {
logError(e);
yomichan.logError(e);
return false;
}
}

View File

@ -24,7 +24,7 @@ class Mecab {
}
onError(error) {
logError(error, false);
yomichan.logError(error);
}
async checkVersion() {

View File

@ -17,6 +17,7 @@
/* global
* DisplaySearch
* apiForwardLogsToBackend
* apiOptionsGet
*/
@ -53,6 +54,7 @@ function injectSearchFrontend() {
}
(async () => {
apiForwardLogsToBackend();
await yomichan.prepare();
const displaySearch = new DisplaySearch();

View File

@ -45,7 +45,7 @@ class QueryParser extends TextScanner {
}
onError(error) {
logError(error, false);
yomichan.logError(error);
}
onClick(e) {

View File

@ -122,7 +122,7 @@ class DisplaySearch extends Display {
}
onError(error) {
logError(error, true);
yomichan.logError(error);
}
onSearchClear() {

View File

@ -133,7 +133,7 @@ async function _settingsImportSetOptionsFull(optionsFull) {
}
function _showSettingsImportError(error) {
logError(error);
yomichan.logError(error);
document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
$('#settings-import-error-modal').modal('show');
}

View File

@ -554,7 +554,7 @@ function dictionaryErrorsShow(errors) {
if (errors !== null && errors.length > 0) {
const uniqueErrors = new Map();
for (let e of errors) {
logError(e);
yomichan.logError(e);
e = dictionaryErrorToString(e);
let count = uniqueErrors.get(e);
if (typeof count === 'undefined') {

View File

@ -21,6 +21,7 @@
* ankiInitialize
* ankiTemplatesInitialize
* ankiTemplatesUpdateValue
* apiForwardLogsToBackend
* apiOptionsSave
* appearanceInitialize
* audioSettingsInitialize
@ -284,6 +285,7 @@ function showExtensionInformation() {
async function onReady() {
apiForwardLogsToBackend();
await yomichan.prepare();
showExtensionInformation();

View File

@ -17,8 +17,10 @@
/* global
* SettingsPopupPreview
* apiForwardLogsToBackend
*/
(() => {
apiForwardLogsToBackend();
new SettingsPopupPreview();
})();

View File

@ -22,6 +22,7 @@
* PopupProxy
* PopupProxyHost
* apiBroadcastTab
* apiForwardLogsToBackend
* apiOptionsGet
*/
@ -62,6 +63,7 @@ async function createPopupProxy(depth, id, parentFrameId) {
}
(async () => {
apiForwardLogsToBackend();
await yomichan.prepare();
const data = window.frontendInitializationData || {};

View File

@ -17,6 +17,7 @@
/* global
* DisplayFloat
* apiForwardLogsToBackend
* apiOptionsGet
*/
@ -68,5 +69,6 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) {
}
(async () => {
apiForwardLogsToBackend();
new DisplayFloat();
})();

View File

@ -84,7 +84,7 @@ class DisplayFloat extends Display {
if (this._orphaned) {
this.setContent('orphaned');
} else {
logError(error, true);
yomichan.logError(error);
}
}

View File

@ -81,12 +81,12 @@ class FrontendApiSender {
onAck(id) {
const info = this.callbacks.get(id);
if (typeof info === 'undefined') {
console.warn(`ID ${id} not found for ack`);
yomichan.logWarning(new Error(`ID ${id} not found for ack`));
return;
}
if (info.ack) {
console.warn(`Request ${id} already ack'd`);
yomichan.logWarning(new Error(`Request ${id} already ack'd`));
return;
}
@ -98,12 +98,12 @@ class FrontendApiSender {
onResult(id, data) {
const info = this.callbacks.get(id);
if (typeof info === 'undefined') {
console.warn(`ID ${id} not found`);
yomichan.logWarning(new Error(`ID ${id} not found`));
return;
}
if (!info.ack) {
console.warn(`Request ${id} not ack'd`);
yomichan.logWarning(new Error(`Request ${id} not ack'd`));
return;
}

View File

@ -148,7 +148,7 @@ class PopupProxy {
}
this._frameOffsetUpdatedAt = now;
} catch (e) {
logError(e);
yomichan.logError(e);
} finally {
this._frameOffsetPromise = null;
}

View File

@ -144,6 +144,14 @@ function apiGetMedia(targets) {
return _apiInvoke('getMedia', {targets});
}
function apiLog(error, level, context) {
return _apiInvoke('log', {error, level, context});
}
function apiLogIndicatorClear() {
return _apiInvoke('logIndicatorClear');
}
function _apiInvoke(action, params={}) {
const data = {action, params};
return new Promise((resolve, reject) => {
@ -171,3 +179,17 @@ function _apiInvoke(action, params={}) {
function _apiCheckLastError() {
// NOP
}
let _apiForwardLogsToBackendEnabled = false;
function apiForwardLogsToBackend() {
if (_apiForwardLogsToBackendEnabled) { return; }
_apiForwardLogsToBackendEnabled = true;
yomichan.on('log', async ({error, level, context}) => {
try {
await apiLog(errorToJson(error), level, context);
} catch (e) {
// NOP
}
});
}

View File

@ -52,15 +52,28 @@ if (EXTENSION_IS_BROWSER_EDGE) {
*/
function errorToJson(error) {
try {
if (isObject(error)) {
return {
name: error.name,
message: error.message,
stack: error.stack,
data: error.data
};
}
} catch (e) {
// NOP
}
return {
name: error.name,
message: error.message,
stack: error.stack,
data: error.data
value: error,
hasValue: true
};
}
function jsonToError(jsonError) {
if (jsonError.hasValue) {
return jsonError.value;
}
const error = new Error(jsonError.message);
error.name = jsonError.name;
error.stack = jsonError.stack;
@ -68,28 +81,6 @@ function jsonToError(jsonError) {
return error;
}
function logError(error, alert) {
const manifest = chrome.runtime.getManifest();
let errorMessage = `${manifest.name} v${manifest.version} has encountered an error.\n`;
errorMessage += `Originating URL: ${window.location.href}\n`;
const errorString = `${error.toString ? error.toString() : error}`;
const stack = `${error.stack}`.trimRight();
if (!stack.startsWith(errorString)) { errorMessage += `${errorString}\n`; }
errorMessage += stack;
const data = error.data;
if (typeof data !== 'undefined') { errorMessage += `\nData: ${JSON.stringify(data, null, 4)}`; }
errorMessage += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues';
console.error(errorMessage);
if (alert) {
window.alert(`${errorString}\n\nCheck the developer console for more details.`);
}
}
/*
* Common helpers
@ -361,8 +352,77 @@ const yomichan = (() => {
});
}
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}`;
}
const manifest = chrome.runtime.getManifest();
let message = `${manifest.name} v${manifest.version} 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});
}
// Private
_getLogContext() {
return {
url: window.location.href
};
}
_onMessage({action, params}, sender, callback) {
const handler = this._messageHandlers.get(action);
if (typeof handler !== 'function') { return false; }

View File

@ -201,7 +201,7 @@ class TextScanner {
}
onError(error) {
logError(error, false);
yomichan.logError(error);
}
async scanTimerWait() {

View File

@ -145,7 +145,10 @@ const vm = new VM({
XMLHttpRequest,
indexedDB: global.indexedDB,
IDBKeyRange: global.IDBKeyRange,
JSZip: yomichanTest.JSZip
JSZip: yomichanTest.JSZip,
addEventListener() {
// NOP
}
});
vm.context.window = vm.context;