mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 20:13:46 +00:00 
			
		
		
		
	Noun: backup, checkout, cleanup, login, logout, setup, shutdown, signup, timeout. Verb: back up, check out, clean up, log in, log out, set up, shut down, sign up, time out. Signed-off-by: Anders Kaseorg <anders@zulip.com>
		
			
				
	
	
		
			284 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			284 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* eslint-disable no-console */
 | |
| 
 | |
| // 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.
 | |
| 
 | |
| import $ from "jquery";
 | |
| 
 | |
| import * as blueslip_stacktrace from "./blueslip_stacktrace";
 | |
| import {page_params} from "./page_params";
 | |
| import * as ui_report from "./ui_report";
 | |
| 
 | |
| if (Error.stackTraceLimit !== undefined) {
 | |
|     Error.stackTraceLimit = 100000;
 | |
| }
 | |
| 
 | |
| function pad(num, width) {
 | |
|     return num.toString().padStart(width, "0");
 | |
| }
 | |
| 
 | |
| function make_logger_func(name) {
 | |
|     return function Logger_func(...args) {
 | |
|         const now = new Date();
 | |
|         const 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";
 | |
| 
 | |
|         const str_args = args.map((x) => (typeof x === "object" ? JSON.stringify(x) : x));
 | |
| 
 | |
|         const 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](...args);
 | |
|         }
 | |
|     };
 | |
| }
 | |
| 
 | |
| class Logger {
 | |
|     _memory_log = [];
 | |
| 
 | |
|     get_log() {
 | |
|         return this._memory_log;
 | |
|     }
 | |
| }
 | |
| 
 | |
| for (const name of ["debug", "log", "info", "warn", "error"]) {
 | |
|     Logger.prototype[name] = make_logger_func(name);
 | |
| }
 | |
| 
 | |
| const logger = new Logger();
 | |
| 
 | |
| export function get_log() {
 | |
|     return logger.get_log();
 | |
| }
 | |
| 
 | |
| const reported_errors = new Set();
 | |
| const last_report_attempt = new Map();
 | |
| 
 | |
| function report_error(
 | |
|     msg,
 | |
|     stack = "No stacktrace available",
 | |
|     {show_ui_msg = false, more_info} = {},
 | |
| ) {
 | |
|     if (page_params.debug_mode) {
 | |
|         // In development, we display blueslip errors in the web UI,
 | |
|         // to make them hard to miss.
 | |
|         blueslip_stacktrace.display_stacktrace(msg, stack);
 | |
|     }
 | |
| 
 | |
|     const key = ":" + msg + stack;
 | |
|     if (
 | |
|         reported_errors.has(key) ||
 | |
|         (last_report_attempt.has(key) &&
 | |
|             // Only try to report a given error once every 5 minutes
 | |
|             Date.now() - last_report_attempt.get(key) <= 60 * 5 * 1000)
 | |
|     ) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     last_report_attempt.set(key, Date.now());
 | |
| 
 | |
|     // TODO: If an exception gets thrown before we set up 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.
 | |
|     //
 | |
|     // Important: We don't use channel.js here so that exceptions
 | |
|     // always make it to the server even if reload_state.is_in_progress.
 | |
|     $.ajax({
 | |
|         type: "POST",
 | |
|         url: "/json/report/error",
 | |
|         dataType: "json",
 | |
|         data: {
 | |
|             message: msg,
 | |
|             stacktrace: stack,
 | |
|             ui_message: show_ui_msg,
 | |
|             more_info: JSON.stringify(more_info),
 | |
|             href: window.location.href,
 | |
|             user_agent: window.navigator.userAgent,
 | |
|             log: logger.get_log().join("\n"),
 | |
|         },
 | |
|         timeout: 3 * 1000,
 | |
|         success() {
 | |
|             reported_errors.add(key);
 | |
|             if (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.client_error(
 | |
|                     "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"),
 | |
|                 );
 | |
|             }
 | |
|         },
 | |
|         error() {
 | |
|             if (show_ui_msg && ui_report !== undefined) {
 | |
|                 ui_report.client_error(
 | |
|                     "Oops.  It seems something has gone wrong. Please try reloading the page.",
 | |
|                     $("#home-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;
 | |
|     }
 | |
| }
 | |
| 
 | |
| class BlueslipError extends Error {
 | |
|     name = "BlueslipError";
 | |
| 
 | |
|     constructor(msg, more_info) {
 | |
|         super(msg);
 | |
|         if (more_info !== undefined) {
 | |
|             this.more_info = more_info;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| export function exception_msg(ex) {
 | |
|     let message = ex.message;
 | |
|     if (ex.fileName !== undefined) {
 | |
|         message += " at " + ex.fileName;
 | |
|         if (ex.lineNumber !== undefined) {
 | |
|             message += ":" + ex.lineNumber;
 | |
|         }
 | |
|     }
 | |
|     return message;
 | |
| }
 | |
| 
 | |
| $(window).on("error", (event) => {
 | |
|     const ex = event.originalEvent.error;
 | |
|     if (!ex || ex instanceof BlueslipError) {
 | |
|         return;
 | |
|     }
 | |
|     const message = exception_msg(ex);
 | |
|     report_error(message, ex.stack);
 | |
| });
 | |
| 
 | |
| function build_arg_list(msg, more_info) {
 | |
|     const args = [msg];
 | |
|     if (more_info !== undefined) {
 | |
|         args.push("\nAdditional information: ", more_info);
 | |
|     }
 | |
|     return args;
 | |
| }
 | |
| 
 | |
| export function debug(msg, more_info) {
 | |
|     const args = build_arg_list(msg, more_info);
 | |
|     logger.debug(...args);
 | |
| }
 | |
| 
 | |
| export function log(msg, more_info) {
 | |
|     const args = build_arg_list(msg, more_info);
 | |
|     logger.log(...args);
 | |
| }
 | |
| 
 | |
| export function info(msg, more_info) {
 | |
|     const args = build_arg_list(msg, more_info);
 | |
|     logger.info(...args);
 | |
| }
 | |
| 
 | |
| export function warn(msg, more_info) {
 | |
|     const args = build_arg_list(msg, more_info);
 | |
|     logger.warn(...args);
 | |
|     if (page_params.debug_mode) {
 | |
|         console.trace();
 | |
|     }
 | |
| }
 | |
| 
 | |
| export function error(msg, more_info, stack = new Error("dummy").stack) {
 | |
|     const args = build_arg_list(msg, more_info);
 | |
|     logger.error(...args);
 | |
|     report_error(msg, stack, {more_info});
 | |
| 
 | |
|     if (page_params.debug_mode) {
 | |
|         throw new BlueslipError(msg, more_info);
 | |
|     }
 | |
| 
 | |
|     // This function returns to its caller in production!  To raise a
 | |
|     // fatal error even in production, use throw new Error(…) instead.
 | |
| }
 | |
| 
 | |
| export const timings = new Map();
 | |
| 
 | |
| export function measure_time(label, f) {
 | |
|     const t1 = performance.now();
 | |
|     const ret = f();
 | |
|     const t2 = performance.now();
 | |
|     const elapsed = t2 - t1;
 | |
|     timings.set(label, elapsed);
 | |
|     return ret;
 | |
| }
 | |
| 
 | |
| // 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.
 | |
| export function preview_node(node) {
 | |
|     if (node instanceof $) {
 | |
|         node = node[0];
 | |
|     }
 | |
| 
 | |
|     const tag = node.tagName.toLowerCase();
 | |
|     const className = node.className.length ? node.className : false;
 | |
|     const id = node.id.length ? node.id : false;
 | |
| 
 | |
|     const node_preview =
 | |
|         "<" +
 | |
|         tag +
 | |
|         (id ? " id='" + id + "'" : "") +
 | |
|         (className ? " class='" + className + "'" : "") +
 | |
|         "></" +
 | |
|         tag +
 | |
|         ">";
 | |
| 
 | |
|     return node_preview;
 | |
| }
 |