For JavaScript errors, such as XXX undefined,syntaxerror, we are more familiar with, this article we discuss how the JavaScript error capture.
Our team names the wrong JavaScript code as BADJS, and there is an open source Badjs project that captures and analyzes JS errors and provides some basic analysis of report data.
There are generally two ways to catch errors:
Use Window.onerror () to capture global JS error messages
Use try{...} catch (E) {...} The package needs to execute the code, gets the Error object's attribute to locate errors and escalate
The first way is the simplest, but when the execution of the JS code and our site in different domains that cross domain, due to browser security restrictions, the onerror () method can only catch a fixed error code script error. Specific reference here: Click to view
Our team's current business basically will be static resources deployed to the CDN server, and the site is in different domains, so need to solve cross-domain problems.
Cross-domain issues can be resolved via server-side settings, but not access-control-allow-orgin:*. More in-depth information on this issue can be consulted here: HTTPS://GITHUB.COM/BETTERJS/BADJS-REPORT/ISSUES/3
The second way is to manually wrap some code to be instrumented, without cross-domain problems and to get detailed error information for the object of Err. This approach is relatively troublesome, but it is possible to handle most situations through the global hook, eliminating the hassle of writing try...catch every time.
We all know that the implementation of JS code through the event and timer trigger execution, so theoretically the event triggered by the callback, the timer callback package can be.
Our BADJS projects are mainly implemented in a second way, and are processed according to the existing business:
Define (), require () and other methods
Some of the events in jquery encapsulation, such as $.EVENT.ADD,$.EVENT.REMOVE,AJAX, etc.
SetTimeout SetInterval, etc.
The principle here is simple, similar to the following code:
function define () {
...
}
var a = define;
define = function () {
try{
A.apply (this,arguments);
}catch (e) {
... Error escalation
}
};
There are also some compatibility issues that need to be addressed, such as the Settimtout and SetInterval methods are not function types but object in IE versions, so it is not possible to wrap them in a way that rewrites function. Similarly, the Document.attachevent method is also object, not function.
In addition to handling the above methods separately, there are some unexpected situations that cannot be handled, such as:
Window.onload,image.prototype.onerror such as browsers and DOM events, such methods cannot directly overwrite function
Third-party plug-ins are custom events, such as those provided by the Flash Player for playback control events.
Some new APIs, such as FileReader.prototype.onload, etc.
These accidents are difficult to do the global hook, so had to manually try...catch. Our Badjs also provides a handy API, such as the source code:
var img = new Image ();
Img.onload = function () {
...
};
Use TRYJS Package
var img = new Image ();
Img.onload = Tryjs.spycustom (function () {
...
});
In addition, there are some differences between different browsers for the Err object that Try...catch can get. Fortunately someone has done a page to show the details of the differences, reference url:http://broofa.com/tests/errorproperties.htm.
Some other supplements
Back to capturing the JS error itself, is to better monitor and locate errors, to help us improve the quality of the code, so Kael also mentioned another idea, you can gray part of the user, directly using the main domain instead of the CDN JS, directly avoid cross-domain problems, this idea is worth a try.
In addition, error escalation data and access data, such as the combination of analysis, not only can be more rapid positioning problems, can even realize monitoring automatic alarm, of course, this is also very complex.
how to capture and analyze JavaScript Error in a browser
Front-end engineers know that JavaScript has basic exception handling capabilities. We can throw the new error () and the browser throws an exception when we invoke the API error. But it is estimated that most front-end engineers have not considered collecting these exception information. Anyway, as long as the JavaScript error after the refresh is no longer, the user can solve the problem by refreshing, the browser will not crash, when it did not happen well. This assumption was established before the single Page App was popular. Now the single Page APP runs for a period of time after a very complex state, the user may have a number of input operations to come here, said refresh on the refresh Ah? Do you want to redo the previous operation? So we still need to capture and analyze these exception information, and then we can modify the code to avoid impacting the user experience.
How exceptions are caught
We wrote the throw New Error () to capture, of course, because we know exactly where throw is written. However, the exception that occurs when invoking the browser API is not necessarily so easily captured, and some APIs are written in the standard to throw exceptions, and some APIs only individual browsers throw exceptions because of differences or flaws. For the former we can also capture through Try-catch, for which we must listen for global exceptions and then capture them.
Try-catch
If some browser APIs are known to throw exceptions, then we need to put the call inside the Try-catch to avoid an error causing the entire program to enter an illegal state. For example, Window.localstorage is an API that throws an exception when the write data exceeds the capacity limit, as is the case in Safari's privacy browsing mode.
try {
Localstorage.setitem (' Date ', Date.now ());
catch (Error) {
ReportError (Error);
}
Another common Try-catch scenario is the callback. Because the code for the callback function is not controllable, the quality of the code will not invoke any other API that throws an exception, we don't know. In order not to be able to execute other code after invoking the callback because of a callback error, it is necessary to return the call to the Try-catch.
Listeners.foreach (function (listener) {
try {
Listener ();
catch (Error) {
ReportError (Error);
}
});
Window.onerror
For Try-catch, an exception can only be captured by Window.onerror if it is not covered.
Window.onerror =
function (errormessage, Scripturi, linenumber) {
ReportError ({
Message:errormessage,
Script:scripturi,
Line:linenumber
});
}
Be careful not to use Window.addeventlistener or window.attachevent in the form of listening to Window.onerror. Many browsers only implement Window.onerror, or only WINDOW.ONERROR implementations are standard. Taking into account the definition of the draft standard is also Window.onerror, we use window.onerror just fine.
property is missing
Suppose we have a reporterror function to collect the caught exception and then bulk send it to the server-side store for query analysis, so what information do we want to collect? Useful information is the type of error (name), error message, script file address (scripts), line numbers (lines), column numbers (columns), stack traces (stack). If an exception is captured through Try-catch, the information is on the Error object (supported by mainstream browsers), so ReportError can collect the information as well. But if captured through Window.onerror, we all know that this event function has only 3 parameters, so the unexpected information for these 3 parameters is lost.
Serialization message
If the Error object is created by ourselves, then the error.message is controlled by us. Basically what we put into the error.message, the Window.onerror's first argument (message) is what. (browsers can actually make minor changes, such as adding ' uncaught Error: ' prefix.) So we can serialize the attributes we're focusing on (for example, JSON). stringify) is then deposited into the error.message and then deserialized on Window.onerror read. Of course, this is limited to the error object we created ourselves.
Fifth parameter
Browser vendors are also aware of the limitations of using window.onerror, so start adding new parameters to the Window.onerror. In view of the fact that only line numbers do not appear to be symmetrical, IE first add the column number and place the fourth parameter. However, we are more concerned about the ability to get the full stack, so Firefox said to put the stack on the fifth parameter bar. But Chrome said it would be better to put the entire Error object on the fifth argument, and what attributes you want to read, including custom attributes. As a result of the faster chrome action, the new Window.onerror signature was implemented in Chrome 30, which led to the standard drafts being written accordingly.
Window.onerror = function (
ErrorMessage,
Scripturi,
LineNumber,
ColumnNumber,
Error
) {
if (Error) {
ReportError (Error);
} else {
ReportError ({
Message:errormessage,
Script:scripturi,
Line:linenumber,
Column:columnnumber
});
}
}
attribute Normalization
The Error object properties that we discussed earlier, whose names are based on the Chrome naming method, differ in how different browsers name the properties of the error object, such as script file addresses that are called scripts in chrome but are called filename in Firefox. Therefore, we also need a special function to normalize the Error object, that is, to map different attribute names to the uniform attribute names. The specific procedure can refer to this article. Although the browser implementation is updated, it is not too difficult for a human to maintain such a mapping.
Similar to the format of stack traces (stacks). This property preserves the stack information of an exception when it occurs in plain text, because the text format used by each browser is different, so it also requires manual maintenance of a regular expression used to extract each frame's function name (identifier), file (script) from plain text, Line number and column number (column).
Security restrictions
If you've also encountered the message ' script error. ', you'll understand what I'm talking about, which is actually the browser's restrictions on different source (origin) script files. The reason for this security restriction is this: Suppose a network of silver in the user login after the return of HTML and anonymous users see the HTML is not the same, a Third-party Web site will be able to put the net silver URI into the script.src attribute. HTML of course can not be used as JS parsing, so the browser will throw an exception, and this Third-party Web site will be able to resolve the abnormal location to determine whether the user has logged in. This browser for different source script files thrown by the exception to filter, filtering only the "script error." Such a unchanged message, all the other attributes disappear.
For a certain size of the site, the script file on the CDN, not homologous is very normal. Now, as a small web site, common frameworks such as jQuery and backbone can directly reference the version on the public CDN to speed up user downloads. So this security constraint does cause some trouble, and the exception information we collect from Chrome and Firefox is useless ' Script error. '
CORS
wants to circumvent this limitation by ensuring that the script file and the page itself are homologous. But is the script file on the server without CDN acceleration, does not reduce the user download speed? One solution is that the script files continue to be on the CDN, using XMLHttpRequest to download the content back through CORS, and then create <script> tag into the page. The code embedded in the page is of course homologous.
This is simple to say, but there are a lot of details to implement. In a simple example:
We all know that this step1, Step2, Step3, if there is a dependency, must be executed strictly in that order, otherwise there may be an error. Browsers can request step1 and step3 files in parallel, but the order of execution is guaranteed. If we get the contents of Step1 and step3 through xmlhttprequest ourselves, we need to ensure that the order is correct. In addition, don't forget to Step2, step1 in the form of non-blocking download Step2 can be executed, so we must also artificially intervene step2 let it wait for Step1 to complete and then execute.
If we already have a complete set of tools to generate <script> labels on different pages on the site, we need to adjust the tool to make changes to <script> tags:
We need to implement Scheduleremotescript and Scheduleinlinescript These two functions, and make sure they are defined before the first <script> tag that references the external script file, and then the remaining <script> The label will be rewritten as the above form. Note that the STEP2 function, which was originally executed immediately, was placed inside a larger code function. The code function is not executed, it is just a container, so that the original step2 can be preserved without escaping, but will not be executed immediately.
Next we also need to implement a complete set of mechanisms to ensure that the content of the files downloaded by scheduleremotescript from the address and by the Scheduleinlinescript The code that is obtained directly can be executed one after the other in the correct order. Detailed code I am not here to give, we are interested to be able to achieve their own.
Line numbering
getting content through CORS and then injecting the code into the page can break the security limit, but introduces a new problem, that is, line number conflict. It was possible to navigate to a unique script file via Error.script and then navigate to a unique line number via Error.line. Now because the page is embedded code, a number of <script> tags can not be differentiated by error.script, however, each <script> tag inside the line number is from 1, The result is that we can't use the exception information to locate the source code where the error resides.
To avoid line number collisions, we can waste a number of line numbers so that the line number intervals used by the actual code in each <script> label do not overlap each other. For example, assuming that the actual code in each <script> tag is no more than 1000 lines, I can have the code in the first <script> tag occupy the 1?1000 line so that the code in the second <script> tag occupies the 1001?2000 Line (insert 1000 blank lines before), the third <script> label code occupies the first 2001?3000 line (insert 2000 blank lines before), and so on. We then use the data-* attribute to record this information for easy retrieval.
After this process, if a wrong error.line is 3005, it means that the actual error.script should be ' http://cdn.com/step3.js ' and the actual error.line should be 5. We can complete this line number in the previous mentioned ReportError function to check the work.
Of course, because we have no way to guarantee that each script file is only 1000 lines, it is possible that some script files are significantly less than 1000 lines, so there is no need to allocate 1000 lines of the interval to each <script> tag. We can allocate intervals based on the number of actual script lines, as long as the intervals used by each <script> label are guaranteed to overlap.
The security restrictions of the
crossorigin Properties
Browser for content from different sources are of course not limited to <script> labels. Since XMLHttpRequest can break through this limitation through CORS, why is the resource referenced directly through the tag not available? Of course this is possible. The restrictions that
refer to different source script files for <script> labels also work on tags referencing different source picture files. If a label is a different source, once used in <canvas> drawing, the <canvas> will become a write-only state, ensuring that the site cannot steal unauthorized different source image data through JavaScript. Later tags solved the problem by introducing the Crossorigin attribute. If you use crossorigin= "anonymous", it is equivalent to an anonymous CORS, and if you use ' crossorigin= ' use-credentials, it is equivalent to a CORS with authentication.
Since tags can do so, why <script> labels can't do that? The browser vendor then adds the same crossorigin attribute to the <script> tag to address the security restrictions mentioned above. Now the Chrome and Firefox support for this property is completely fine. Safari treats crossorigin= "anonymous" as crossorigin= "Use-credentials", and the result is that Safari will fail as a certificate if the server only supports anonymous CORS. Because the CDN server is designed to return only static content because of its performance, it is impossible to dynamically return the required HTTP Header,safari of the authentication CORS according to the request equivalent to not being able to use this feature to solve the above problem.
Summary
JavaScript exception handling looks simple, but it's not easy to really catch the exception and then analyze the attributes. Now, although there are third-party services that provide a Google Analytics service to capture JavaScript exceptions, it is necessary to make sure that the details and principles are made in your own hands.