// System documented in https://zulip.readthedocs.io/en/latest/subsystems/logging.html // This must be included before the first call to $(document).ready // in order to be able to report exceptions that occur during their // execution. var blueslip = (function () { var exports = {}; if (Error.stackTraceLimit !== undefined) { Error.stackTraceLimit = 100000; } function Logger() { this._memory_log = []; } Logger.prototype = (function () { function pad(num, width) { var ret = num.toString(); while (ret.length < width) { ret = "0" + ret; } return ret; } function make_logger_func(name) { return function Logger_func() { var now = new Date(); var date_str = now.getUTCFullYear() + '-' + pad(now.getUTCMonth() + 1, 2) + '-' + pad(now.getUTCDate(), 2) + ' ' + pad(now.getUTCHours(), 2) + ':' + pad(now.getUTCMinutes(), 2) + ':' + pad(now.getUTCSeconds(), 2) + '.' + pad(now.getUTCMilliseconds(), 3) + ' UTC'; var str_args = _.map(arguments, function (x) { if (typeof(x) === 'object') { return JSON.stringify(x); } else { return x; } }); var log_entry = date_str + " " + name.toUpperCase() + ': ' + str_args.join(""); this._memory_log.push(log_entry); // Don't let the log grow without bound if (this._memory_log.length > 1000) { this._memory_log.shift(); } if (console[name] !== undefined) { return console[name].apply(console, arguments); } return; }; } var proto = { get_log: function Logger_get_log() { return this._memory_log; } }; var methods = ['debug', 'log', 'info', 'warn', 'error']; var i; for (i = 0; i < methods.length; i++) { proto[methods[i]] = make_logger_func(methods[i]); } return proto; }()); var logger = new Logger(); exports.get_log = function blueslip_get_log() { return logger.get_log(); }; // Format error stacks using the ErrorStackParser // external library function getErrorStack(stack) { var ex = new Error(); ex.stack = stack; return ErrorStackParser .parse(ex) .map(function (stackFrame) { return stackFrame.lineNumber + ': ' + stackFrame.fileName + ' | ' + stackFrame.functionName; }).join('\n'); } var reported_errors = {}; var last_report_attempt = {}; function report_error(msg, stack, opts) { opts = _.extend({show_ui_msg: false}, opts); if (stack === undefined) { stack = 'No stacktrace available'; } if (page_params.debug_mode) { // In development, we display blueslip errors in the web UI, // to make them hard to miss. stack = getErrorStack(stack); exports.display_errors_on_screen(msg, stack); } var key = ':' + msg + stack; if (reported_errors.hasOwnProperty(key) || (last_report_attempt.hasOwnProperty(key) // Only try to report a given error once every 5 minutes && (Date.now() - last_report_attempt[key] <= 60 * 5 * 1000))) { return; } last_report_attempt[key] = Date.now(); // TODO: If an exception gets thrown before we setup ajax calls // to include the CSRF token, our ajax call will fail. The // elegant thing to do in that case is to either wait until that // setup is done or do it ourselves and then retry. $.ajax({ type: 'POST', url: '/json/report/error', dataType: 'json', data: { message: msg, stacktrace: stack, ui_message: opts.show_ui_msg, more_info: JSON.stringify(opts.more_info), href: window.location.href, user_agent: window.navigator.userAgent, log: logger.get_log().join("\n")}, timeout: 3*1000, success: function () { reported_errors[key] = true; if (opts.show_ui_msg && ui_report !== undefined) { // There are a few races here (and below in the error // callback): // 1) The ui_report module or something it requires might // not have been compiled or initialized yet. // 2) The DOM might not be ready yet and so fetching // the #home-error div might fail. // For (1) we just don't show the message if the ui // hasn't been loaded yet. The user will probably // get another error once it does. We can't solve // (2) by using $(document).ready because the // callback never gets called (I think what's going // on here is if the exception was raised by a // function that was called as a result of the DOM // becoming ready then the internal state of jQuery // gets messed up and our callback never gets // invoked). In any case, it will pretty clear that // something is wrong with the page and the user will // probably try to reload anyway. ui_report.message("Oops. It seems something has gone wrong. " + "The error has been reported to the fine " + "folks at Zulip, but, in the mean time, " + "please try reloading the page.", $("#home-error"), "alert-error"); } }, error: function () { if (opts.show_ui_msg && ui_report !== undefined) { ui_report.message("Oops. It seems something has gone wrong. " + "Please try reloading the page.", $("#home-error"), "alert-error"); } } }); if (page_params.save_stacktraces) { // Save the stacktrace so it can be examined even in // development servers. (N.B. This assumes you have set DEBUG // = False on your development server, or else this code path // won't execute to begin with -- useful for testing // (un)minification.) window.last_stacktrace = stack; } } function BlueslipError(msg, more_info) { // One can't subclass Error normally so we have to play games // with setting __proto__ var self = new Error(msg); self.name = "BlueslipError"; // Indirect access to __proto__ keeps jslint quiet var proto = '__proto__'; self[proto] = BlueslipError.prototype; if (more_info !== undefined) { self.more_info = more_info; } return self; } BlueslipError.prototype = Object.create(Error.prototype); exports.exception_msg = function blueslip_exception_msg(ex) { var message = ex.message; if (ex.hasOwnProperty('fileName')) { message += " at " + ex.fileName; if (ex.hasOwnProperty('lineNumber')) { message += ":" + ex.lineNumber; } } return message; }; $(window).on('error', function (event) { var ex = event.originalEvent.error; if (!ex || ex instanceof BlueslipError) { return; } var message = exports.exception_msg(ex); report_error(message, ex.stack); }); function build_arg_list(msg, more_info) { var args = [msg]; if (more_info !== undefined) { args.push("\nAdditional information: ", more_info); } return args; } exports.debug = function blueslip_debug (msg, more_info) { var args = build_arg_list(msg, more_info); logger.debug.apply(logger, args); }; exports.log = function blueslip_log (msg, more_info) { var args = build_arg_list(msg, more_info); logger.log.apply(logger, args); }; exports.info = function blueslip_info (msg, more_info) { var args = build_arg_list(msg, more_info); logger.info.apply(logger, args); }; exports.warn = function blueslip_warn (msg, more_info) { var args = build_arg_list(msg, more_info); logger.warn.apply(logger, args); if (page_params.debug_mode) { console.trace(); } }; exports.display_errors_on_screen = function (error, stack) { var $exit = "
"; var $error = "" + stack + ""; var $alert = $("").html($error + $exit + $pre); $(".app .alert-box").append($alert.addClass("show")); }; exports.error = function blueslip_error (msg, more_info, stack) { if (stack === undefined) { stack = Error().stack; } var args = build_arg_list(msg, more_info); logger.error.apply(logger, args); report_error(msg, stack, {more_info: more_info}); if (page_params.debug_mode) { throw new BlueslipError(msg, more_info); } }; exports.fatal = function blueslip_fatal (msg, more_info) { report_error(msg, Error().stack, {more_info: more_info}); throw new BlueslipError(msg, more_info); }; // Produces an easy-to-read preview on an HTML element. Currently // only used for including in error report emails; be sure to discuss // with other developers before using it in a user-facing context // because it is not XSS-safe. exports.preview_node = function (node) { if (node.constructor === jQuery) { node = node[0]; } var tag = node.tagName.toLowerCase(); var className = node.className.length ? node.className : false; var id = node.id.length ? node.id : false; var node_preview = "<" + tag + (id ? " id='" + id + "'" : "") + (className ? " class='" + className + "'" : "") + ">" + tag + ">"; return node_preview; }; return exports; }()); if (typeof module !== 'undefined') { module.exports = blueslip; } window.blueslip = blueslip;