blob: 7eb1ae78802c4bcf97a5860c540ccb629ad53a7d [file] [log] [blame]
/*
* Copyright (c) 2014-2018 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
import {AjaxError, App, NullLogger, scout, strings} from './index';
import * as $ from 'jquery';
export default class ErrorHandler {
constructor() {
this.logError = true;
this.displayError = true;
this.sendError = false;
this.windowErrorHandler = this._onWindowError.bind(this);
}
/**
* Use this constant to configure whether or not all instances of the ErrorHandler should write
* to the console. When you've installed a console appender to log4javascript you can set the
* value to false, because the ErrorHandler also calls $.log.error and thus the appender has
* already written the message to the console. We don't want to see it twice.
*/
static CONSOLE_OUTPUT = true;
init(options) {
$.extend(this, options);
}
// Signature matches the "window.onerror" event handler
// https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror
_onWindowError(errorMessage, fileName, lineNumber, columnNumber, error) {
try {
if (error instanceof Error) {
this.handle(error);
} else {
var code = 'J00';
var log = errorMessage + ' at ' + fileName + ':' + lineNumber + '\n(' + 'Code ' + code + ')';
this.handleErrorInfo({
code: code,
message: errorMessage,
log: log
});
}
} catch (err) {
throw new Error('Error in global JavaScript error handler: ' + err.message + ' (original error: ' + errorMessage + ' at ' + fileName + ':' + lineNumber + ')');
}
}
/**
* Handles unexpected JavaScript errors. The arguments are first analyzed and then handled.
*
* This method may be called by passing the arguments individually or as an array (or array-like object)
* in the first argument.
* Examples:
* 1. try { ... } catch (err) { handler.handle(err); }
* 2. $.get().fail(function(jqXHR, textStatus, errorThrown) { handler.handle(jqXHR, textStatus, errorThrown); }
* 3. $.get().fail(function(jqXHR, textStatus, errorThrown) { handler.handle(arguments); } // <-- recommended
*
* @return {object} the analyzed errorInfo
*/
handle() {
var args = arguments;
if (args.length === 1 && args[0] && (String(args[0]) === '[object Arguments]' || Array.isArray(args[0]))) {
args = args[0];
}
var errorInfo = this.analyzeError.apply(this, args);
this.handleErrorInfo(errorInfo);
return errorInfo;
}
/**
* Returns an "errorInfo" object for the given arguments. The following cases are handled:
* 1. Error objects (code: computed by getJsErrorCode())
* 2. jQuery AJAX errors (code: 'X' + HTTP status code)
* 3. Nothing (code: 'P3')
* 4. Everything else (code: 'P4')
*/
analyzeError(error) {
var errorInfo = {
code: null,
message: null,
location: null,
stack: null,
debugInfo: null,
log: null
};
if (error instanceof Error) {
// 1. Errors
errorInfo.code = this.getJsErrorCode(error);
errorInfo.message = String(error.message || error);
if (error.fileName) {
errorInfo.location = error.fileName + strings.join('', strings.box(':', error.lineNumber), strings.box(':', error.columnNumber));
}
if (error.stack) {
errorInfo.stack = String(error.stack);
}
if (error.debugInfo) { // scout extension
errorInfo.debugInfo = error.debugInfo;
}
errorInfo.log = 'Unexpected error: ' + errorInfo.message;
if (errorInfo.location) {
errorInfo.log += ' at ' + errorInfo.location;
}
if (errorInfo.stack) {
errorInfo.log += '\n' + errorInfo.stack;
}
if (errorInfo.debugInfo) {
// Error throwers may put a "debugInfo" string on the error object that is then added to the log string (this is a scout extension).
errorInfo.log += '\n----- Additional debug information: -----\n' + errorInfo.debugInfo;
}
} else if ($.isJqXHR(error) || (Array.isArray(error) && $.isJqXHR(error[0])) || error instanceof AjaxError) {
// 2. jQuery $.ajax() error (arguments: jqXHR, textStatus, errorThrown, requestOptions)
var jqXHR, errorThrown, requestOptions;
if (error instanceof AjaxError) {
jqXHR = error.jqXHR;
errorThrown = error.errorThrown;
requestOptions = error.requestOptions; // scout extension
} else {
var args = (Array.isArray(error) ? error : arguments);
jqXHR = args[0];
errorThrown = args[2];
requestOptions = args[3]; // scout extension
}
var ajaxRequest = (requestOptions ? strings.join(' ', requestOptions.type, requestOptions.url) : '');
var ajaxStatus = (jqXHR.status ? strings.join(' ', jqXHR.status, errorThrown) : 'Connection error');
errorInfo.code = 'X' + (jqXHR.status || '0');
errorInfo.message = 'AJAX call' + strings.box(' "', ajaxRequest, '"') + ' failed' + strings.box(' [', ajaxStatus, ']');
errorInfo.log = errorInfo.message;
if (jqXHR.responseText) {
errorInfo.debugInfo = 'Response text:\n' + jqXHR.responseText;
errorInfo.log += '\n' + errorInfo.debugInfo;
}
} else if (!error) {
// 3. No reason provided
errorInfo.code = 'P3';
errorInfo.message = 'Unknown error';
errorInfo.log = 'Unexpected error (no reason provided)';
} else {
// 4. Everything else (e.g. when strings are thrown)
var s = (typeof error === 'string' || typeof error === 'number') ? String(error) : null;
errorInfo.code = 'P4';
errorInfo.message = s || 'Unexpected error';
if (!s) {
try {
s = JSON.stringify(error); // may throw "cyclic object value" error
} catch (err) {
s = String(error);
}
}
errorInfo.log = 'Unexpected error: ' + s;
}
return errorInfo;
}
/**
* Expects an object as returned by analyzeError() and handles it:
* - If the flag "logError" is set, the log message is printed to the console
* - If there is a scout session and the flag "displayError" is set, the error is shown in a a message box.
* - If there is a scout session and the flag "sendError" is set, the error is sent to the UI server.
*/
handleErrorInfo(errorInfo) {
if (this.logError && errorInfo.log) {
$.log.error(errorInfo.log);
// Note: when the null-logger is active it has already written the error to the console
// when the $.log.error function has been called above, so we don't have to log again here.
var writeToConsole = ErrorHandler.CONSOLE_OUTPUT;
if ($.log instanceof NullLogger) {
writeToConsole = false;
}
if (writeToConsole && window && window.console) {
if (window.console.error) {
window.console.error(errorInfo.log);
} else if (window.console.log) {
window.console.log(errorInfo.log);
}
}
}
// Note: The error handler is installed globally and we cannot tell in which scout session the error happened.
// We simply use the first scout session to display the message box and log the error. This is not ideal in the
// multi-session-case (portlet), but currently there is no other way. Besides, this feature is not in use yet.
if (App.get().sessions.length > 0) {
var session = App.get().sessions[0];
if (this.displayError) {
this._showMessageBox(session, errorInfo.message, errorInfo.code, errorInfo.log);
}
if (this.sendError) {
this._sendErrorMessage(session, errorInfo.log);
}
}
}
/**
* Generate a "cool looking" error code from the JS error object, that
* does not reveal too much technical information, but at least indicates
* that a JS runtime error has occurred. (In contrast, fatal errors from
* the server have numeric error codes.)
*/
getJsErrorCode(error) {
if (error) {
if (error.name === 'EvalError') {
return 'E1';
}
if (error.name === 'InternalError') {
return 'I2';
}
if (error.name === 'RangeError') {
return 'A3';
}
if (error.name === 'ReferenceError') {
return 'R4';
}
if (error.name === 'SyntaxError') {
return 'S5';
}
if (error.name === 'TypeError') {
return 'T6';
}
if (error.name === 'URIError') {
return 'U7';
}
}
return 'J0';
}
_showMessageBox(session, errorMessage, errorCode, logMessage) {
var options = {
header: session.optText('ui.UnexpectedProblem', 'Internal UI Error'),
body: strings.join('\n\n',
session.optText('ui.InternalUiErrorMsg', errorMessage, ' (' + session.optText('ui.ErrorCodeX', 'Code ' + errorCode, errorCode) + ')'),
session.optText('ui.UiInconsistentMsg', '')),
yesButtonText: session.optText('ui.Reload', 'Reload'),
yesButtonAction: scout.reloadPage,
noButtonText: session.optText('ui.Ignore', 'Ignore'),
hiddenText: logMessage
};
session.showFatalMessage(options, errorCode);
}
_sendErrorMessage(session, logMessage) {
session.sendLogRequest(logMessage);
}
}