UNPKG

@cocalc/static

Version:

CoCalc's static frontend Webpack-based build system and framework

353 lines (309 loc) 14.1 kB
######################################################################### # This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. # License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details ######################################################################### # Catch and report webapp client errors to the SMC server. # This is based on bugsnag's MIT licensed lib: https://github.com/bugsnag/bugsnag-js # The basic idea is to wrap very early at a very low level of the event system, # such that all libraries loaded later are sitting on top of this. # Additionally, special care is taken to browser brands and their capabilities. # Finally, additional data about the webapp client is gathered and sent with the error report. # list of string-identifyers of errors, that were already reported. # this avoids excessive resubmission of errors already_reported = [] FUNCTION_REGEX = /function\s*([\w\-$]+)?\s*\(/i ignoreOnError = 0 shouldCatch = true # set this to true, to enable the webapp error reporter for development enable_for_testing = false if BACKEND? and BACKEND # never enable on the backend -- used by static react rendering. ENABLED = false else ENABLED = (not DEBUG) or enable_for_testing # this is the MAIN function of this module # it's exported publicly and also used in various spots where exceptions are already # caught and reported to the browser's console. reportException = (exception, name, severity, comment) -> if !exception or typeof exception == "string" return # setting those *Number defaults to `undefined` breaks somehow on its way # to the DB (it only wants NULL or an int). -1 is signaling that there is no info. sendError( name: name || exception.name message: exception.message || exception.description comment: comment ? '' stacktrace: stacktraceFromException(exception) || generateStacktrace() file: exception.fileName || exception.sourceURL path: window.location.href lineNumber: exception.lineNumber || exception.line || -1 columnNumber: exception.columnNumber || -1 severity: severity || "default" ) WHITELIST = ['componentWillMount has been renamed', 'componentWillReceiveProps has been renamed', 'a whole package of antd'] isWhitelisted = (opts) -> s = JSON.stringify(opts) for x in WHITELIST if s.indexOf(x) != -1 return true return false # this is the final step sending the error report. # it gathers additional information about the webapp client. currentlySendingError = false; sendError = (opts) -> #console.log("sendError", currentlySendingError, opts); if currentlySendingError # errors can be crazy and easily DOS the user's connection. Since this table is # just something we manually check sometimes, not sending too many errors is # best. We send at most one at a time. See https://github.com/sagemathinc/cocalc/issues/5771 return currentlySendingError = true require.ensure [], => try #console.log 'sendError', opts if isWhitelisted(opts) #console.log 'sendError: whitelisted' # Ignore this antd message in browser. return misc = require('@cocalc/util/misc') opts = misc.defaults opts, name : misc.required message : misc.required comment : '' stacktrace : '' file : '' path : '' lineNumber : -1 columnNumber : -1 severity : 'default' fingerprint = misc.uuidsha1([opts.name, opts.message, opts.comment].join('::')) if fingerprint in already_reported and not DEBUG return already_reported.push(fingerprint) # attaching some additional info feature = require('@cocalc/frontend/feature') opts.user_agent = navigator?.userAgent opts.browser = feature.get_browser() opts.mobile = feature.IS_MOBILE opts.smc_version = SMC_VERSION opts.build_date = BUILD_DATE opts.smc_git_rev = COCALC_GIT_REVISION opts.uptime = misc.get_uptime() opts.start_time = misc.get_start_time_ts() if DEBUG then console.info('error reporter sending:', opts) try # During initial load in some situations evidently webapp_client # is not yet initialized, and webapp_client is undefined. (Maybe # a typescript rewrite of everything relevant will help...). In # any case, for now we # https://github.com/sagemathinc/cocalc/issues/4769 # As an added bonus, by try/catching and retrying once at least, # we are more likely to get the error report in case of a temporary # network or other glitch.... console.log 'sendError: import webapp_client' {webapp_client} = require('@cocalc/frontend/webapp-client') # can possibly be undefined # console.log 'sendError: sending error' await webapp_client.tracking_client.webapp_error(opts) # might fail. # console.log 'sendError: got response' catch err console.info("failed to report error; trying again in 30 seconds", err, opts) {delay} = require('awaiting'); await delay(30000) try {webapp_client} = require('@cocalc/frontend/webapp-client') await webapp_client.tracking_client.webapp_error(opts) catch err console.info("failed to report error", err) finally currentlySendingError = false # neat trick to get a stacktrace when there is none generateStacktrace = () -> generated = stacktrace = null MAX_FAKE_STACK_SIZE = 10 ANONYMOUS_FUNCTION_PLACEHOLDER = "[anonymous]" try throw new Error("") catch exception generated = "<generated>\n" stacktrace = stacktraceFromException(exception) if not stacktrace generated = "<generated-ie>\n" functionStack = [] try curr = arguments.callee.caller.caller while curr && functionStack.length < MAX_FAKE_STACK_SIZE if FUNCTION_REGEX.test(curr.toString()) fn = RegExp.$1 ? ANONYMOUS_FUNCTION_PLACEHOLDER else fn = ANONYMOUS_FUNCTION_PLACEHOLDER functionStack.push(fn) curr = curr.caller catch e #console.error(e) stacktrace = functionStack.join("\n") return generated + stacktrace stacktraceFromException = (exception) -> return exception.stack || exception.backtrace || exception.stacktrace # Disable catching on IE < 10 as it destroys stack-traces from generateStackTrace() # OF COURSE, COCALC doesn't support any version of IE at all, so ... if (not window.atob) shouldCatch = false # Disable catching on browsers that support HTML5 ErrorEvents properly. # This lets debug on unhandled exceptions work. # TODO: enabling the block below distorts (at least) Chrome error messages. # Maybe Chrome's window.onerror doesn't work as assumed? # else if window.ErrorEvent # try # if new window.ErrorEvent("test").colno == 0 # shouldCatch = false # catch e # # No action needed # flag to ignore "onerror" when already wrapped in the event handler ignoreNextOnError = () -> ignoreOnError += 1 window.setTimeout((-> ignoreOnError -= 1)) # this is the "brain" of all this wrap = (_super) -> try if typeof _super != "function" return _super if !_super._wrapper _super._wrapper = () -> if shouldCatch try return _super.apply(this, arguments) catch e reportException(e, null, "error") ignoreNextOnError() throw e else return _super.apply(this, arguments) _super._wrapper._wrapper = _super._wrapper return _super._wrapper catch e return _super # replaces an attribute of an object by a function that has it as an argument polyFill = (obj, name, makeReplacement) -> original = obj[name] replacement = makeReplacement(original) obj[name] = replacement # wrap all prototype objects that have event handlers # first one is for chrome, the first three for FF, the rest for IE, Safari, etc. if ENABLED "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(/\w+/g, (global) -> prototype = window[global]?.prototype if prototype?.hasOwnProperty?("addEventListener") polyFill(prototype, "addEventListener", (_super) -> return (e, f, capture, secure) -> try if f and f.handleEvent f.handleEvent = wrap(f.handleEvent) catch err #console.log(err) return _super.call(this, e, wrap(f), capture, secure) ) polyFill(prototype, "removeEventListener", (_super) -> return (e, f, capture, secure) -> _super.call(this, e, f, capture, secure) return _super.call(this, e, wrap(f), capture, secure) ) ) if ENABLED polyFill(window, "onerror", (_super) -> return (message, url, lineNo, charNo, exception) -> # IE 6+ support. if !charNo and window.event charNo = window.event.errorCharacter #if DEBUG # console.log("intercepted window.onerror", message, url, lineNo, charNo, exception) if ignoreOnError == 0 name = exception?.name or "window.onerror" stacktrace = (exception and stacktraceFromException(exception)) or generateStacktrace() sendError( name : name message : message file : url path : window.location.href lineNumber : lineNo columnNumber: charNo stacktrace : stacktrace severity : "error" ) # Fire the existing `window.onerror` handler, if one exists if _super _super(message, url, lineNo, charNo, exception) ) # timing functions hijackTimeFunc = (_super) -> return (f, t) -> if typeof f == "function" f = wrap(f) args = Array.prototype.slice.call(arguments, 2) return _super((-> f.apply(this, args)), t) else return _super(f, t) if ENABLED polyFill(window, "setTimeout", hijackTimeFunc) polyFill(window, "setInterval", hijackTimeFunc) if ENABLED and window.requestAnimationFrame polyFill(window, "requestAnimationFrame", (_super) -> (callback) -> return _super(wrap(callback)) ) if ENABLED and window.setImmediate polyFill(window, "setImmediate", (_super) -> return () -> args = Array.prototype.slice.call(arguments) args[0] = wrap(args[0]) return _super.apply(this, args) ) # console terminal sendLogLine = (severity, args) -> require.ensure [], => misc = require('@cocalc/util/misc') if typeof(args) == 'object' message = misc.trunc_middle(misc.to_json(args), 1000) else message = Array.prototype.slice.call(args).join(", ") sendError( name : 'Console Output' message : message file : '' path : window.location.href lineNumber : -1 columnNumber: -1 stacktrace : generateStacktrace() severity : severity ) wrapFunction = (object, property, newFunction) -> oldFunction = object[property] object[property] = () -> newFunction.apply(this, arguments) if typeof oldFunction == "function" oldFunction.apply(this, arguments) if ENABLED and window.console? wrapFunction(console, "warn", (-> sendLogLine("warn", arguments))) wrapFunction(console, "error", (-> sendLogLine("error", arguments))) if ENABLED window.addEventListener "unhandledrejection",(e) -> require.ensure [], => # just to make sure there is a message reason = e.reason ? '<no reason>' if typeof(reason) == 'object' misc = require('@cocalc/util/misc') reason = "#{reason.stack ? reason.message ? misc.trunc_middle(misc.to_json(reason), 1000)}" e.message = "unhandledrejection: #{reason}" reportException(e, "unhandledrejection") # public API exports.reportException = reportException if DEBUG window.cc ?= {} window.cc.webapp_error_reporter = shouldCatch : -> shouldCatch ignoreOnError : -> ignoreOnError already_reported : -> already_reported stacktraceFromException : stacktraceFromException generateStacktrace : generateStacktrace sendLogLine : sendLogLine reportException : reportException is_enabled : -> ENABLED