📜 ⬆️ ⬇️

Telepathy on steroids in js / node.js

image The stage of product support takes a lot of energy and nerves. The path from “I press and it does not work” to solve the problem, even with a first-class telepath, can take a lot of time. The time during which the client / boss will be angry and unhappy.

To reduce the time to solve a problem, you need to quickly find out about errors, to have as precise information as possible about what led to it and, preferably, to put everything together.

I also will tell about the decision under a cat.

1. Tasks


After discussion, it was decided to create a mechanism that collects error information from the client and server, and allows data to be transmitted or processed for subsequent response. The mechanism should provide an opportunity in the future to add ways to work with data without unnecessary rewriting of the code and to allow changing the way of working, order, etc. from the config.
')
Key points:

2. Decision


It was decided at the server start to load special error handlers - drivers, the order and priority of which will be loaded from the config. Errors on the frontend will be sent to the server, where they will be processed along with the rest.

The basic idea is that when an error occurs, and as the stack rolls back to the global area, debugging information will be added to the error class using distributed collectors. When falling into the global area, the error will be intercepted and processed using the error driver.

2.1 Error class


The class of an error inherited from standard was written. With a constructor accepting an error, the ability to specify an “alarm level” and the addition of debug data. The class is located in a single file for the front- and backend file of tools.

Hereinafter, the code uses the co, socket.io, and sugar.js libraries.
Full class code
app.Error = function Error(error,lastFn){ if(error && error.name && error.message && error.stack){ ,       this.name=error.name; this.message=error.message; this.stack=error.stack; this.clueData=error.clueData||[]; this._alarmLvl=error._alarmLvl||'trivial'; this._side=error._side || (module ? "backend" : "frontend");//  return; } if(!app.isString(error)) error='unknown error'; this.name='Error'; this.message=error; this._alarmLvl='trivial'; this._side=module ? "backend" : "frontend"; this.clueData=[]; if (Error.captureStackTrace) { Error.captureStackTrace(this, app.isFunction(lastFn)? lastFn : this.constructor); } else { this.stack = (new Error()).stack.split('\n').removeAt(1).join();//       } }; app.Error.prototype = Object.create(Error.prototype); app.Error.prototype.constructor = app.Error; app.Error.prototype.setFatal = function () {//getter/setters    this._alarmLvl='fatal'; return this; }; app.Error.prototype.setTrivial = function () { this._alarmLvl='trivial'; return this; }; app.Error.prototype.setWarning = function () { this._alarmLvl='warning'; return this; }; app.Error.prototype.getAlarmLevel = function () { return this._alarmLvl; }; app.Error.prototype.addClueData = function(name,data){//   var dataObj={}; dataObj[name]=data; this.clueData.push(dataObj); return this; }; 


And immediately use example for promise:

 socket.on(fullName, function (values) { <...> method(values)//  api .then(<...>) .catch(function (error) {//  throw new app.Error(error)//         .setFatal()// " " .addClueData('api', {//   fullName, values, handshake: socket.handshake }) }); }); 

For try-catch, we do the same.

2.2 Frontend


For the frontend, the snag is that an error can occur even before the transport library loads (socket.io in this case).

We go around this problem by collecting errors in a temporary variable. To intercept errors from the global scope, use window.onerror:

 app.errorForSending=[]; app.sendError = function (error) {//     app.io.emit('server error send', new app.Error(error)); }; window.onerror = function (message, source, lineno, colno, error) {//     app.errorForSending.push(//    . new app.Error(error) .setFatal());//    ,       }; app.events.on('socket.io ready', ()=> {//    window.onerror = function (message, source, lineno, colno, error) {//  app.sendError(new app.Error(error).setFatal()); }; app.errorForSending.forEach((error)=> {//  ,   app.sendError(error); }); delete app.errorForSending; }); app.events.on('client ready', ()=> {//      window.onerror = function (message, source, lineno, colno, error) { app.sendError(error); }; }); 

The problem remains that some libraries like not to throw out errors, but simply print them to the console. Overwrite console functions to intercept data.

 function wrapConsole(name, action) { console['$' + name] = console[name];//   console[name] = function () { console['$' + name](...arguments);//   app.sendError( new app.Error(`From console.${name}: ` + [].join.call(arguments, '' ),//      console[name])//     (     v8) .addClueData('console', {//        consoleMethod: name, arg : Array.create(arguments) })[action]());//    }; } wrapConsole('error', 'setTrivial'); wrapConsole('warn', 'setWarning'); wrapConsole('info', 'setWarning'); 

2.3 Server


We are left with the most interesting, for all who read up to this point and did not die of fatigue. After all, it remains to implement not just the initialization and execution of drivers that receive errors,

All the code can be viewed on the githaba (link below), and now let's go through the main tasks:

  1. Parallel start for speed
    For these purposes, we use yield [...] (or Promise.all (...)) given that each function from the array should not throw an error otherwise, if there are several functions with errors, we will not be able to process them all
  2. Flexible configuration
    All drivers are in the "driver package", which are located in the array by priority. The error is sent immediately to the entire driver package, if the entire package does not work, the system proceeds to the next, etc.
  3. Dynamic start
    During initialization, we mark all drivers as “not started”.
    When starting the first driver package, we mark either as “started” or “bad”.
    When sending, in the current package, skip “bad”, send to “started” and start “not started”. Drivers that throw out an error, mark as bad and go on. If all drivers in the current package are marked as bad go to the next package.
  4. Sending driver errors to still live drivers
    If errors occur in the error drivers themselves (a bit of tautology), we write them into a special array. After finding the first live driver, we send driver errors through it and the error itself (if the drivers fell while sending an error) and driver errors.
  5. We catch errors with front / backend
    Create a special api for frontend and catch the node.js exceptions via process.on ('uncaughtException', fn) and process.on ('unhandledRejection', fn)

3. Conclusion


The outlined mechanism for collecting and sending error messages will allow you to instantly respond to errors, even before the end user, and do without questioning the end user for the last buttons pressed.

If you think about the development, in the future you can add some useful features:


A working example can be seen on the githaba . For architecture, please do not scold, an example was made by the method remove-from-project-all-unnecessary.

I would welcome comments.

Source: https://habr.com/ru/post/307482/


All Articles