Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/static/src/webapp-error-reporter.js
7073 views
1
/*
2
* decaffeinate suggestions:
3
* DS102: Remove unnecessary code created because of implicit returns
4
* DS104: Avoid inline assignments
5
* DS207: Consider shorter variations of null checks
6
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
7
*/
8
//########################################################################
9
// This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
10
// License: MS-RSL – see LICENSE.md for details
11
//########################################################################
12
13
// Catch and report webapp client errors to the SMC server.
14
// This is based on bugsnag's MIT licensed lib: https://github.com/bugsnag/bugsnag-js
15
// The basic idea is to wrap very early at a very low level of the event system,
16
// such that all libraries loaded later are sitting on top of this.
17
// Additionally, special care is taken to browser brands and their capabilities.
18
// Finally, additional data about the webapp client is gathered and sent with the error report.
19
20
// list of string-identifyers of errors, that were already reported.
21
// this avoids excessive resubmission of errors
22
let ENABLED;
23
const already_reported = [];
24
25
const FUNCTION_REGEX = /function\s*([\w\-$]+)?\s*\(/i;
26
27
let ignoreOnError = 0;
28
29
let shouldCatch = true;
30
31
// set this to true, to enable the webapp error reporter for development
32
const enable_for_testing = false;
33
if (typeof BACKEND !== "undefined" && BACKEND) {
34
// never enable on the backend -- used by static react rendering.
35
ENABLED = false;
36
} else {
37
ENABLED = !DEBUG || enable_for_testing;
38
}
39
40
// this is the MAIN function of this module
41
// it's exported publicly and also used in various spots where exceptions are already
42
// caught and reported to the browser's console.
43
const reportException = function (exception, name, severity, comment) {
44
if (!exception || typeof exception === "string") {
45
return;
46
}
47
// setting those *Number defaults to `undefined` breaks somehow on its way
48
// to the DB (it only wants NULL or an int). -1 is signaling that there is no info.
49
return sendError({
50
name: name || exception.name,
51
message: exception.message || exception.description,
52
comment: comment != null ? comment : "",
53
stacktrace: stacktraceFromException(exception) || generateStacktrace(),
54
file: exception.fileName || exception.sourceURL,
55
path: window.location.href,
56
lineNumber: exception.lineNumber || exception.line || -1,
57
columnNumber: exception.columnNumber || -1,
58
severity: severity || "default",
59
});
60
};
61
62
const WHITELIST = [
63
"componentWillMount has been renamed",
64
"componentWillReceiveProps has been renamed",
65
// Ignore this antd message in browser:
66
"a whole package of antd",
67
// we can't do anything about bokeh crashes in their own code
68
"cdn.bokeh.org",
69
// xtermjs
70
"renderRows",
71
];
72
const isWhitelisted = function (opts) {
73
const s = JSON.stringify(opts);
74
for (let x of WHITELIST) {
75
if (s.indexOf(x) !== -1) {
76
return true;
77
}
78
}
79
return false;
80
};
81
82
// this is the final step sending the error report.
83
// it gathers additional information about the webapp client.
84
let currentlySendingError = false;
85
const sendError = async function (opts) {
86
// console.log("sendError", currentlySendingError, opts);
87
if (currentlySendingError) {
88
// errors can be crazy and easily DOS the user's connection. Since this table is
89
// just something we manually check sometimes, not sending too many errors is
90
// best. We send at most one at a time. See https://github.com/sagemathinc/cocalc/issues/5771
91
return;
92
}
93
currentlySendingError = true;
94
try {
95
//console.log 'sendError', opts
96
let webapp_client;
97
if (isWhitelisted(opts)) {
98
//console.log 'sendError: whitelisted'
99
return;
100
}
101
const misc = require("@cocalc/util/misc");
102
opts = misc.defaults(opts, {
103
name: misc.required,
104
message: misc.required,
105
comment: "",
106
stacktrace: "",
107
file: "",
108
path: "",
109
lineNumber: -1,
110
columnNumber: -1,
111
severity: "default",
112
});
113
const fingerprint = misc.uuidsha1(
114
[opts.name, opts.message, opts.comment].join("::"),
115
);
116
if (already_reported.includes(fingerprint) && !DEBUG) {
117
return;
118
}
119
already_reported.push(fingerprint);
120
// attaching some additional info
121
const feature = require("@cocalc/frontend/feature");
122
opts.user_agent = navigator?.userAgent;
123
opts.browser = feature.get_browser();
124
opts.mobile = feature.IS_MOBILE;
125
opts.smc_version = SMC_VERSION;
126
opts.build_date = BUILD_DATE;
127
opts.smc_git_rev = COCALC_GIT_REVISION;
128
opts.uptime = misc.get_uptime();
129
opts.start_time = misc.get_start_time_ts();
130
if (DEBUG) {
131
console.info("error reporter sending:", opts);
132
}
133
try {
134
// During initial load in some situations evidently webapp_client
135
// is not yet initialized, and webapp_client is undefined. (Maybe
136
// a typescript rewrite of everything relevant will help...). In
137
// any case, for now we
138
// https://github.com/sagemathinc/cocalc/issues/4769
139
// As an added bonus, by try/catching and retrying once at least,
140
// we are more likely to get the error report in case of a temporary
141
// network or other glitch....
142
// console.log("sendError: import webapp_client");
143
144
({ webapp_client } = require("@cocalc/frontend/webapp-client")); // can possibly be undefined
145
// console.log 'sendError: sending error'
146
return await webapp_client.tracking_client.webapp_error(opts); // might fail.
147
// console.log 'sendError: got response'
148
} catch (err) {
149
console.info(
150
"failed to report error; trying again in 30 seconds",
151
err,
152
opts,
153
);
154
const { delay } = require("awaiting");
155
await delay(30000);
156
try {
157
({ webapp_client } = require("@cocalc/frontend/webapp-client"));
158
return await webapp_client.tracking_client.webapp_error(opts);
159
} catch (error) {
160
err = error;
161
return console.info("failed to report error", err);
162
}
163
}
164
} finally {
165
currentlySendingError = false;
166
}
167
};
168
169
// neat trick to get a stacktrace when there is none
170
var generateStacktrace = function () {
171
let stacktrace;
172
let generated = (stacktrace = null);
173
const MAX_FAKE_STACK_SIZE = 10;
174
const ANONYMOUS_FUNCTION_PLACEHOLDER = "[anonymous]";
175
176
try {
177
throw new Error("");
178
} catch (exception) {
179
generated = "<generated>\n";
180
stacktrace = stacktraceFromException(exception);
181
}
182
183
if (!stacktrace) {
184
generated = "<generated-ie>\n";
185
const functionStack = [];
186
try {
187
let curr = arguments.callee.caller.caller;
188
while (curr && functionStack.length < MAX_FAKE_STACK_SIZE) {
189
var fn;
190
if (FUNCTION_REGEX.test(curr.toString())) {
191
fn = RegExp.$1 != null ? RegExp.$1 : ANONYMOUS_FUNCTION_PLACEHOLDER;
192
} else {
193
fn = ANONYMOUS_FUNCTION_PLACEHOLDER;
194
}
195
functionStack.push(fn);
196
curr = curr.caller;
197
}
198
} catch (e) {}
199
//console.error(e)
200
stacktrace = functionStack.join("\n");
201
}
202
return generated + stacktrace;
203
};
204
205
var stacktraceFromException = (exception) =>
206
exception.stack || exception.backtrace || exception.stacktrace;
207
208
// Disable catching on IE < 10 as it destroys stack-traces from generateStackTrace()
209
// OF COURSE, COCALC doesn't support any version of IE at all, so ...
210
if (!window.atob) {
211
shouldCatch = false;
212
}
213
214
// Disable catching on browsers that support HTML5 ErrorEvents properly.
215
// This lets debug on unhandled exceptions work.
216
// TODO: enabling the block below distorts (at least) Chrome error messages.
217
// Maybe Chrome's window.onerror doesn't work as assumed?
218
// else if window.ErrorEvent
219
// try
220
// if new window.ErrorEvent("test").colno == 0
221
// shouldCatch = false
222
// catch e
223
// # No action needed
224
225
// flag to ignore "onerror" when already wrapped in the event handler
226
const ignoreNextOnError = function () {
227
ignoreOnError += 1;
228
return window.setTimeout(() => (ignoreOnError -= 1));
229
};
230
231
// this is the "brain" of all this
232
const wrap = function (_super) {
233
try {
234
if (typeof _super !== "function") {
235
return _super;
236
}
237
238
if (!_super._wrapper) {
239
_super._wrapper = function () {
240
if (shouldCatch) {
241
try {
242
return _super.apply(this, arguments);
243
} catch (e) {
244
reportException(e, null, "error");
245
ignoreNextOnError();
246
throw e;
247
}
248
} else {
249
return _super.apply(this, arguments);
250
}
251
};
252
253
_super._wrapper._wrapper = _super._wrapper;
254
}
255
256
return _super._wrapper;
257
} catch (error) {
258
const e = error;
259
return _super;
260
}
261
};
262
263
// replaces an attribute of an object by a function that has it as an argument
264
const polyFill = function (obj, name, makeReplacement) {
265
const original = obj[name];
266
const replacement = makeReplacement(original);
267
return (obj[name] = replacement);
268
};
269
270
// wrap all prototype objects that have event handlers
271
// first one is for chrome, the first three for FF, the rest for IE, Safari, etc.
272
if (ENABLED) {
273
"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(
274
/\w+/g,
275
function (global) {
276
const prototype = window[global]?.prototype;
277
if (prototype?.hasOwnProperty?.("addEventListener")) {
278
polyFill(
279
prototype,
280
"addEventListener",
281
(_super) =>
282
function (e, f, capture, secure) {
283
try {
284
if (f && f.handleEvent) {
285
f.handleEvent = wrap(f.handleEvent);
286
}
287
} catch (err) {}
288
//console.log(err)
289
return _super.call(this, e, wrap(f), capture, secure);
290
},
291
);
292
293
return polyFill(
294
prototype,
295
"removeEventListener",
296
(_super) =>
297
function (e, f, capture, secure) {
298
_super.call(this, e, f, capture, secure);
299
return _super.call(this, e, wrap(f), capture, secure);
300
},
301
);
302
}
303
},
304
);
305
}
306
307
if (ENABLED) {
308
polyFill(
309
window,
310
"onerror",
311
(_super) =>
312
function (message, url, lineNo, charNo, exception) {
313
// IE 6+ support.
314
if (!charNo && window.event) {
315
charNo = window.event.errorCharacter;
316
}
317
318
//if DEBUG
319
// console.log("intercepted window.onerror", message, url, lineNo, charNo, exception)
320
321
if (ignoreOnError === 0) {
322
const name = exception?.name || "window.onerror";
323
const stacktrace =
324
(exception && stacktraceFromException(exception)) ||
325
generateStacktrace();
326
sendError({
327
name,
328
message,
329
file: url,
330
path: window.location.href,
331
lineNumber: lineNo,
332
columnNumber: charNo,
333
stacktrace,
334
severity: "error",
335
});
336
}
337
338
// Fire the existing `window.onerror` handler, if one exists
339
if (_super) {
340
return _super(message, url, lineNo, charNo, exception);
341
}
342
},
343
);
344
}
345
346
// timing functions
347
348
const hijackTimeFunc = (_super) =>
349
function (f, t) {
350
if (typeof f === "function") {
351
f = wrap(f);
352
const args = Array.prototype.slice.call(arguments, 2);
353
return _super(function () {
354
return f.apply(this, args);
355
}, t);
356
} else {
357
return _super(f, t);
358
}
359
};
360
361
if (ENABLED) {
362
polyFill(window, "setTimeout", hijackTimeFunc);
363
polyFill(window, "setInterval", hijackTimeFunc);
364
}
365
366
if (ENABLED && window.requestAnimationFrame) {
367
polyFill(
368
window,
369
"requestAnimationFrame",
370
(_super) => (callback) => _super(wrap(callback)),
371
);
372
}
373
374
if (ENABLED && window.setImmediate) {
375
polyFill(
376
window,
377
"setImmediate",
378
(_super) =>
379
function () {
380
const args = Array.prototype.slice.call(arguments);
381
args[0] = wrap(args[0]);
382
return _super.apply(this, args);
383
},
384
);
385
}
386
387
// console terminal
388
389
function argsToJson(args) {
390
let v = [];
391
try {
392
const misc = require("@cocalc/util/misc");
393
for (const arg of args) {
394
try {
395
const s = JSON.stringify(arg);
396
v.push(s.length > 1000 ? misc.trunc_middle(s) : JSON.parse(s));
397
} catch (_) {
398
v.push("(non-jsonable-arg)");
399
}
400
if (v.length > 10) {
401
v.push("(skipping JSON of some args)");
402
break;
403
}
404
}
405
} catch (_) {
406
// must be robust.
407
v.push("(unable to JSON some args)");
408
}
409
return JSON.stringify(v);
410
}
411
412
const sendLogLine = (severity, args) => {
413
let message;
414
if (typeof args === "object") {
415
message = argsToJson(args);
416
} else {
417
message = Array.prototype.slice.call(args).join(", ");
418
}
419
sendError({
420
name: "Console Output",
421
message,
422
file: "",
423
path: window.location.href,
424
lineNumber: -1,
425
columnNumber: -1,
426
stacktrace: generateStacktrace(),
427
severity,
428
});
429
};
430
431
const wrapFunction = function (object, property, newFunction) {
432
const oldFunction = object[property];
433
return (object[property] = function () {
434
newFunction.apply(this, arguments);
435
if (typeof oldFunction === "function") {
436
return oldFunction.apply(this, arguments);
437
}
438
});
439
};
440
441
if (ENABLED && window.console != null) {
442
wrapFunction(console, "warn", function () {
443
return sendLogLine("warn", arguments);
444
});
445
wrapFunction(console, "error", function () {
446
return sendLogLine("error", arguments);
447
});
448
}
449
450
if (ENABLED) {
451
window.addEventListener("unhandledrejection", (e) => {
452
// just to make sure there is a message
453
let reason = e.reason != null ? e.reason : "<no reason>";
454
if (typeof reason === "object") {
455
let left;
456
const misc = require("@cocalc/util/misc");
457
reason = `${
458
(left = reason.stack != null ? reason.stack : reason.message) != null
459
? left
460
: misc.trunc_middle(misc.to_json(reason), 1000)
461
}`;
462
}
463
e.message = `unhandledrejection: ${reason}`;
464
reportException(e, "unhandledrejection");
465
});
466
}
467
468
// public API
469
470
exports.reportException = reportException;
471
472
if (DEBUG) {
473
if (window.cc == null) {
474
window.cc = {};
475
}
476
window.cc.webapp_error_reporter = {
477
shouldCatch() {
478
return shouldCatch;
479
},
480
ignoreOnError() {
481
return ignoreOnError;
482
},
483
already_reported() {
484
return already_reported;
485
},
486
stacktraceFromException,
487
generateStacktrace,
488
sendLogLine,
489
reportException,
490
is_enabled() {
491
return ENABLED;
492
},
493
};
494
}
495
496