4

How do you pinpoint the client-side errors that occur in scripts from another domains?

For clarity let's suppose that we have an average size web application that uses several scripts hosted by another domains (like google maps JS SDK).

And one day you started receiving Script error in your error logs, which means there is a error occurred in a 3rd party code.

But how would you find the exact method in your code that invoked a 3rd party code that eventually failed?

PS: the error is not reproducible by developers and just occurs on the client machines quite rarely.

PPS: For the errors above window.onerror DOES NOT provide call stack, proper error message, file name and line number. So it provides literally nothing helpful apart of Script error error message.

PPPS:

The third party script is included using <script src="http://..."></script> tags and executed using someFunctionFromTheThirdPartyScript();

zerkms
  • 249,484
  • 69
  • 436
  • 539
  • use elmah https://code.google.com/p/elmah/ this can log all exceptions with most possible information – HaBo Oct 07 '13 at 01:22
  • @HaBo: uhm, asp.net? o_O – zerkms Oct 07 '13 at 01:23
  • if you want it to be frame work independent, you can try this window.onerror call a Ajax method to store the error. – HaBo Oct 07 '13 at 01:29
  • 1
    @HaBo: `window.onerror` doesn't provide sufficient information. That's why I asked the question. – zerkms Oct 07 '13 at 01:29
  • Perhaps you can show some code, how that script is included and executed. – Tyron Oct 08 '13 at 09:25
  • @Tyron: done. See PPPS – zerkms Oct 08 '13 at 09:37
  • @zerkms unfortunately you can't get the function names, but you can get a hash of the function contents and the arguments passed to the function, both of which can be used to identify which function is the problem. See my edited answer below (and comments). – Luke Oct 10 '13 at 15:13
  • @Luke: "but you can get a hash of the function contents and the arguments passed to the function" --- if only I put it in **every** my function. And it might be hundreds of them :-) – zerkms Oct 10 '13 at 21:16
  • You don't have to add the hash - it would generate them while producing your error stack trace. You would have to guess a little while decoding the error, but it is *some* information where otherwise you have none. The arguments passed to the function are more directly useful. If you are more interested than this in generating useful stack traces, you might want to look at a dedicated stack trace tool like: https://github.com/eriwen/javascript-stacktrace – Luke Oct 11 '13 at 15:21

1 Answers1

1

I had a similar problem once and my solution was...

//  The parameters are automatically passed to the window.onerror handler...
function myErrorFunction(message, url, linenumber) {
    $.post(
        "https://host.and/path/to/script/that/stores/entries",
        {
            "url":url, //  URL of the page where the error occured
            "lineNumber":linenumber, //  Line number where the error occer
            "message":message  //error message
        },
        function(){
            //callback function for the $.post()
            if(console)
                if(console.log)
                    console.log("Error reported.");
        }
    );
}
window.onerror = myErrorFunction;  //adds  function "myErrorFunction" to the onError Event

To get more sophisticated you will need to utilize some tricks I have put into my debug project at: https://github.com/luke80/JavaScript-DebugTools-Luke

EDIT: Ok, I'll gather out of that project the important bits that apply to your problem:

/*
  String prototype .hashCode()
  From: http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery
*/
if(typeof String['hashCode'] == "undefined") {
    String.prototype.hashCode = function(){
    var hash = 0, i, char;
    if (this.length == 0) return hash;
    for (i = 0, l = this.length; i < l; i++) {
        char  = this.charCodeAt(i);
        hash  = ((hash<<5)-hash)+char;
        hash |= 0; // Convert to 32bit integer
    }
    return hash;
};
//  Start of vars
var _LOG_CALLERARGS_ON = true,
    getCallerHash = function(funcString) {
        return callerFunc.toString().hashCode();
    },
    getCallerArgs = function(obj) {
        return JSON.stringify(Array.prototype.slice.call(obj),this._detectCircularity(Array.prototype.slice.call(obj))).replace(/^\[/,"(").replace(/\]$/,")");
    },
    detectCircularity = function(obj) {  //  From: http://stackoverflow.com/questions/4816099/chrome-sendrequest-error-typeerror-converting-circular-structure-to-json
        return (function() {
            var i = 0;
            return function(key, value) {
                if(i !== 0 && typeof(obj) === 'object' && typeof(value) == 'object' && obj == value) return '[Circular]'; 
                if(i >= 29) return '[Too deep, not mined]';
                ++i;
                return value;  
            }
        })(detectCircularity);
    },
    caller = this.o.caller || arguments.callee.caller || "top";
//  End of vars
if(typeof caller != "string") {
    if(caller) {
        var callerData = ((caller.name)?caller.name:"Unnamed Caller:"+getCallerHash(caller))+((_LOG_CALLERARGS_ON)?getCallerArgs(caller.arguments):"");
        //  Since this loop could easily become problematic (infinite loop, anyone?) lets impose a limit.
        var maxLoops = 64;
        var loopCounter = 0;
        //  Now we gather all (or 64 of them) the caller names (and optionally their parameters)
        while(caller.caller && loopCounter < maxLoops) {  //  <--- there was an error here that I fixed on Oct 15, 2013 @ 11:55AM
            callerData += " <- "+((caller.caller.name)?caller.caller.name:"Unnamed Caller:"+getCallerHash(caller.caller))+((_LOG_CALLERARGS_ON)?getCallerArgs(caller.caller.arguments):"")
            caller = caller.caller;
            loopCounter++;
        }
        // callerData is now populated with your stack trace.
    } else {
        // Can't get errors from a non-existent caller
    }
}

The callerData variable should be populated with a string of function names (or a hash of the function contents so you can semi-identify them) optionally with the parameters the functions were called with.

While you can't always get the function names, the contents of those functions can be identified (as they should stay the same) and you can still get useful-ish debug information from the parameters that were passed, etc.

NOTE: I didn't actually test that code above, but it should be mostly the same as the code from my repo. If it doesn't work, please refer to the repo and re-factor the code there to your needs. :)

I hope that helps.

Luke
  • 435
  • 4
  • 12
  • 1
    If an error happens in a 3rd party script, then `url` will be empty, `linenumber` will be `0` and `message` will be a `Script error`. Please read the `PPS` part of the question. Sorry, but not an answer. – zerkms Oct 08 '13 at 22:37
  • You're right, but "To get more sophisticated you will need to utilize some tricks I have put into my debug project at: https://github.com/luke80/JavaScript-DebugTools-Luke" -- I was going to put that as my answer, and describe how exactly one would do that. Perhaps I will. – Luke Oct 09 '13 at 16:24
  • hm, I'm not sure how to use your `Log` class – zerkms Oct 09 '13 at 18:59
  • Did you look at the sample html file? Also - if you tried my above additional javascript; are there errors? Did it work? – Luke Oct 09 '13 at 20:24
  • yes, I have looked. It's a logger. And I cannot understand how it will help to debug. – zerkms Oct 09 '13 at 21:12
  • The logger is intended to make it easier to debug your code by adding information to a console.log() type situation. Obviously that isn't what your question was asking, but there is a mechanism in my logger that gathers information about the function in which the log was called. I had thought that section might be of interest to you. – Luke Oct 10 '13 at 15:10
  • yep, logger is an obvious solution, I thought I'm missing something and you've been trying to share on how to perform a real debug. – zerkms Oct 10 '13 at 21:15