
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:
- Catching errors on both the frontend and the backend
- Ability to add multiple error handlers including in future
- Large amount of debug information
- Flexible configuration for each project
- High reliability
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 codeapp.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");
And immediately use example for promise:
socket.on(fullName, function (values) { <...> method(values)
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) {
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];
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,
- Everything should work as quickly as possible, even if each driver in the process of initialization / error handling, you need to "talk heart to heart" with another server or calculate the answer to the main question of the universe of life and all that;
- A flexible system of spare and duplicate drivers;
- Dynamically run spare drivers, in case of failure of the previous ones;
- Exceptions that occur while drivers are running should be sent to working drivers;
- Catch and handle errors with frontend, as well as node.js falling into the global area.
All the code can be viewed on the githaba (link below), and now let's go through the main tasks:
- 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 - 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. - 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. - 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. - 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:
- Change Disable Driver Disabling Policy
For example, add the ability to re-check the driver for performance after some time. - Ability to insert driver code on frontend
Can be used to collect additional information. - Logging preset
DRY for repetitive functions of collecting general information (last downloaded pages, last used api)
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.