Path: blob/master/src/packages/static/src/webapp-error-reporter.js
7073 views
/*1* decaffeinate suggestions:2* DS102: Remove unnecessary code created because of implicit returns3* DS104: Avoid inline assignments4* DS207: Consider shorter variations of null checks5* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md6*/7//########################################################################8// This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.9// License: MS-RSL – see LICENSE.md for details10//########################################################################1112// Catch and report webapp client errors to the SMC server.13// This is based on bugsnag's MIT licensed lib: https://github.com/bugsnag/bugsnag-js14// The basic idea is to wrap very early at a very low level of the event system,15// such that all libraries loaded later are sitting on top of this.16// Additionally, special care is taken to browser brands and their capabilities.17// Finally, additional data about the webapp client is gathered and sent with the error report.1819// list of string-identifyers of errors, that were already reported.20// this avoids excessive resubmission of errors21let ENABLED;22const already_reported = [];2324const FUNCTION_REGEX = /function\s*([\w\-$]+)?\s*\(/i;2526let ignoreOnError = 0;2728let shouldCatch = true;2930// set this to true, to enable the webapp error reporter for development31const enable_for_testing = false;32if (typeof BACKEND !== "undefined" && BACKEND) {33// never enable on the backend -- used by static react rendering.34ENABLED = false;35} else {36ENABLED = !DEBUG || enable_for_testing;37}3839// this is the MAIN function of this module40// it's exported publicly and also used in various spots where exceptions are already41// caught and reported to the browser's console.42const reportException = function (exception, name, severity, comment) {43if (!exception || typeof exception === "string") {44return;45}46// setting those *Number defaults to `undefined` breaks somehow on its way47// to the DB (it only wants NULL or an int). -1 is signaling that there is no info.48return sendError({49name: name || exception.name,50message: exception.message || exception.description,51comment: comment != null ? comment : "",52stacktrace: stacktraceFromException(exception) || generateStacktrace(),53file: exception.fileName || exception.sourceURL,54path: window.location.href,55lineNumber: exception.lineNumber || exception.line || -1,56columnNumber: exception.columnNumber || -1,57severity: severity || "default",58});59};6061const WHITELIST = [62"componentWillMount has been renamed",63"componentWillReceiveProps has been renamed",64// Ignore this antd message in browser:65"a whole package of antd",66// we can't do anything about bokeh crashes in their own code67"cdn.bokeh.org",68// xtermjs69"renderRows",70];71const isWhitelisted = function (opts) {72const s = JSON.stringify(opts);73for (let x of WHITELIST) {74if (s.indexOf(x) !== -1) {75return true;76}77}78return false;79};8081// this is the final step sending the error report.82// it gathers additional information about the webapp client.83let currentlySendingError = false;84const sendError = async function (opts) {85// console.log("sendError", currentlySendingError, opts);86if (currentlySendingError) {87// errors can be crazy and easily DOS the user's connection. Since this table is88// just something we manually check sometimes, not sending too many errors is89// best. We send at most one at a time. See https://github.com/sagemathinc/cocalc/issues/577190return;91}92currentlySendingError = true;93try {94//console.log 'sendError', opts95let webapp_client;96if (isWhitelisted(opts)) {97//console.log 'sendError: whitelisted'98return;99}100const misc = require("@cocalc/util/misc");101opts = misc.defaults(opts, {102name: misc.required,103message: misc.required,104comment: "",105stacktrace: "",106file: "",107path: "",108lineNumber: -1,109columnNumber: -1,110severity: "default",111});112const fingerprint = misc.uuidsha1(113[opts.name, opts.message, opts.comment].join("::"),114);115if (already_reported.includes(fingerprint) && !DEBUG) {116return;117}118already_reported.push(fingerprint);119// attaching some additional info120const feature = require("@cocalc/frontend/feature");121opts.user_agent = navigator?.userAgent;122opts.browser = feature.get_browser();123opts.mobile = feature.IS_MOBILE;124opts.smc_version = SMC_VERSION;125opts.build_date = BUILD_DATE;126opts.smc_git_rev = COCALC_GIT_REVISION;127opts.uptime = misc.get_uptime();128opts.start_time = misc.get_start_time_ts();129if (DEBUG) {130console.info("error reporter sending:", opts);131}132try {133// During initial load in some situations evidently webapp_client134// is not yet initialized, and webapp_client is undefined. (Maybe135// a typescript rewrite of everything relevant will help...). In136// any case, for now we137// https://github.com/sagemathinc/cocalc/issues/4769138// As an added bonus, by try/catching and retrying once at least,139// we are more likely to get the error report in case of a temporary140// network or other glitch....141// console.log("sendError: import webapp_client");142143({ webapp_client } = require("@cocalc/frontend/webapp-client")); // can possibly be undefined144// console.log 'sendError: sending error'145return await webapp_client.tracking_client.webapp_error(opts); // might fail.146// console.log 'sendError: got response'147} catch (err) {148console.info(149"failed to report error; trying again in 30 seconds",150err,151opts,152);153const { delay } = require("awaiting");154await delay(30000);155try {156({ webapp_client } = require("@cocalc/frontend/webapp-client"));157return await webapp_client.tracking_client.webapp_error(opts);158} catch (error) {159err = error;160return console.info("failed to report error", err);161}162}163} finally {164currentlySendingError = false;165}166};167168// neat trick to get a stacktrace when there is none169var generateStacktrace = function () {170let stacktrace;171let generated = (stacktrace = null);172const MAX_FAKE_STACK_SIZE = 10;173const ANONYMOUS_FUNCTION_PLACEHOLDER = "[anonymous]";174175try {176throw new Error("");177} catch (exception) {178generated = "<generated>\n";179stacktrace = stacktraceFromException(exception);180}181182if (!stacktrace) {183generated = "<generated-ie>\n";184const functionStack = [];185try {186let curr = arguments.callee.caller.caller;187while (curr && functionStack.length < MAX_FAKE_STACK_SIZE) {188var fn;189if (FUNCTION_REGEX.test(curr.toString())) {190fn = RegExp.$1 != null ? RegExp.$1 : ANONYMOUS_FUNCTION_PLACEHOLDER;191} else {192fn = ANONYMOUS_FUNCTION_PLACEHOLDER;193}194functionStack.push(fn);195curr = curr.caller;196}197} catch (e) {}198//console.error(e)199stacktrace = functionStack.join("\n");200}201return generated + stacktrace;202};203204var stacktraceFromException = (exception) =>205exception.stack || exception.backtrace || exception.stacktrace;206207// Disable catching on IE < 10 as it destroys stack-traces from generateStackTrace()208// OF COURSE, COCALC doesn't support any version of IE at all, so ...209if (!window.atob) {210shouldCatch = false;211}212213// Disable catching on browsers that support HTML5 ErrorEvents properly.214// This lets debug on unhandled exceptions work.215// TODO: enabling the block below distorts (at least) Chrome error messages.216// Maybe Chrome's window.onerror doesn't work as assumed?217// else if window.ErrorEvent218// try219// if new window.ErrorEvent("test").colno == 0220// shouldCatch = false221// catch e222// # No action needed223224// flag to ignore "onerror" when already wrapped in the event handler225const ignoreNextOnError = function () {226ignoreOnError += 1;227return window.setTimeout(() => (ignoreOnError -= 1));228};229230// this is the "brain" of all this231const wrap = function (_super) {232try {233if (typeof _super !== "function") {234return _super;235}236237if (!_super._wrapper) {238_super._wrapper = function () {239if (shouldCatch) {240try {241return _super.apply(this, arguments);242} catch (e) {243reportException(e, null, "error");244ignoreNextOnError();245throw e;246}247} else {248return _super.apply(this, arguments);249}250};251252_super._wrapper._wrapper = _super._wrapper;253}254255return _super._wrapper;256} catch (error) {257const e = error;258return _super;259}260};261262// replaces an attribute of an object by a function that has it as an argument263const polyFill = function (obj, name, makeReplacement) {264const original = obj[name];265const replacement = makeReplacement(original);266return (obj[name] = replacement);267};268269// wrap all prototype objects that have event handlers270// first one is for chrome, the first three for FF, the rest for IE, Safari, etc.271if (ENABLED) {272"EventTarget Window Node ApplicationCache AudioTrackList ChannelMergerNode CryptoOperation EventSource FileReader HTMLUnknownElement IDBDatabase IDBRequest IDBTransaction KeyOperation MediaController MessagePort ModalWindow Notification SVGElementInstance Screen TextTrack TextTrackCue TextTrackList WebSocket WebSocketWorker Worker XMLHttpRequest XMLHttpRequestEventTarget XMLHttpRequestUpload".replace(273/\w+/g,274function (global) {275const prototype = window[global]?.prototype;276if (prototype?.hasOwnProperty?.("addEventListener")) {277polyFill(278prototype,279"addEventListener",280(_super) =>281function (e, f, capture, secure) {282try {283if (f && f.handleEvent) {284f.handleEvent = wrap(f.handleEvent);285}286} catch (err) {}287//console.log(err)288return _super.call(this, e, wrap(f), capture, secure);289},290);291292return polyFill(293prototype,294"removeEventListener",295(_super) =>296function (e, f, capture, secure) {297_super.call(this, e, f, capture, secure);298return _super.call(this, e, wrap(f), capture, secure);299},300);301}302},303);304}305306if (ENABLED) {307polyFill(308window,309"onerror",310(_super) =>311function (message, url, lineNo, charNo, exception) {312// IE 6+ support.313if (!charNo && window.event) {314charNo = window.event.errorCharacter;315}316317//if DEBUG318// console.log("intercepted window.onerror", message, url, lineNo, charNo, exception)319320if (ignoreOnError === 0) {321const name = exception?.name || "window.onerror";322const stacktrace =323(exception && stacktraceFromException(exception)) ||324generateStacktrace();325sendError({326name,327message,328file: url,329path: window.location.href,330lineNumber: lineNo,331columnNumber: charNo,332stacktrace,333severity: "error",334});335}336337// Fire the existing `window.onerror` handler, if one exists338if (_super) {339return _super(message, url, lineNo, charNo, exception);340}341},342);343}344345// timing functions346347const hijackTimeFunc = (_super) =>348function (f, t) {349if (typeof f === "function") {350f = wrap(f);351const args = Array.prototype.slice.call(arguments, 2);352return _super(function () {353return f.apply(this, args);354}, t);355} else {356return _super(f, t);357}358};359360if (ENABLED) {361polyFill(window, "setTimeout", hijackTimeFunc);362polyFill(window, "setInterval", hijackTimeFunc);363}364365if (ENABLED && window.requestAnimationFrame) {366polyFill(367window,368"requestAnimationFrame",369(_super) => (callback) => _super(wrap(callback)),370);371}372373if (ENABLED && window.setImmediate) {374polyFill(375window,376"setImmediate",377(_super) =>378function () {379const args = Array.prototype.slice.call(arguments);380args[0] = wrap(args[0]);381return _super.apply(this, args);382},383);384}385386// console terminal387388function argsToJson(args) {389let v = [];390try {391const misc = require("@cocalc/util/misc");392for (const arg of args) {393try {394const s = JSON.stringify(arg);395v.push(s.length > 1000 ? misc.trunc_middle(s) : JSON.parse(s));396} catch (_) {397v.push("(non-jsonable-arg)");398}399if (v.length > 10) {400v.push("(skipping JSON of some args)");401break;402}403}404} catch (_) {405// must be robust.406v.push("(unable to JSON some args)");407}408return JSON.stringify(v);409}410411const sendLogLine = (severity, args) => {412let message;413if (typeof args === "object") {414message = argsToJson(args);415} else {416message = Array.prototype.slice.call(args).join(", ");417}418sendError({419name: "Console Output",420message,421file: "",422path: window.location.href,423lineNumber: -1,424columnNumber: -1,425stacktrace: generateStacktrace(),426severity,427});428};429430const wrapFunction = function (object, property, newFunction) {431const oldFunction = object[property];432return (object[property] = function () {433newFunction.apply(this, arguments);434if (typeof oldFunction === "function") {435return oldFunction.apply(this, arguments);436}437});438};439440if (ENABLED && window.console != null) {441wrapFunction(console, "warn", function () {442return sendLogLine("warn", arguments);443});444wrapFunction(console, "error", function () {445return sendLogLine("error", arguments);446});447}448449if (ENABLED) {450window.addEventListener("unhandledrejection", (e) => {451// just to make sure there is a message452let reason = e.reason != null ? e.reason : "<no reason>";453if (typeof reason === "object") {454let left;455const misc = require("@cocalc/util/misc");456reason = `${457(left = reason.stack != null ? reason.stack : reason.message) != null458? left459: misc.trunc_middle(misc.to_json(reason), 1000)460}`;461}462e.message = `unhandledrejection: ${reason}`;463reportException(e, "unhandledrejection");464});465}466467// public API468469exports.reportException = reportException;470471if (DEBUG) {472if (window.cc == null) {473window.cc = {};474}475window.cc.webapp_error_reporter = {476shouldCatch() {477return shouldCatch;478},479ignoreOnError() {480return ignoreOnError;481},482already_reported() {483return already_reported;484},485stacktraceFromException,486generateStacktrace,487sendLogLine,488reportException,489is_enabled() {490return ENABLED;491},492};493}494495496