12

We have a Rails application where we are including our application dependencies in the html head within application.js:

//= require jquery
//= require analytics
// other stuff...

Then on individual pages we have a script tag at the bottom of the page for analytics:

<script>
  analytics.track('on that awesome page');
</script>

This normally works fine, but very occasionally we see the error analytics is not defined, most recently on Chrome 43. Because everything should be loaded synchronously, this seems like it ought to work out of the box, but I changed the script to:

<script>
  $(document).ready(function () {
    analytics.track('on that awesome page');
  });
</script>

And now instead every once in a while we see $ is not defined instead. We don't see any other errors from the same IP, otherwise I would suspect something went wrong in application.js. Any other ideas why it might break? You can see an example page here.

The full application.js:

// Polyfills
//= require es5-shim/es5-shim
//= require es5-shim/es5-sham
//= require polyfills
//
// Third party plugins
//= require isMobile/isMobile
//= require jquery
//
//= require jquery.ui.autocomplete
//= require jquery.ui.dialog
//= require jquery.ui.draggable
//= require jquery.ui.droppable
//= require jquery.ui.effect-fade
//= require jquery.ui.effect-slide
//= require jquery.ui.resizable
//= require jquery.ui.tooltip
//
//= require jquery_ujs
//= require underscore
//= require backbone
//= require backbone-sortable-collection
//= require bootstrap
//= require load-image
//= require react
//= require react_ujs
//= require classnames
//= require routie
//= require mathjs
//= require moment
//= require stink-bomb
//= require analytics
//
// Our code
//= require_self
//= require extensions
//= require extend
//= require models
//= require collections
//= require constants
//= require templates
//= require mixins
//= require helpers
//= require singletons
//= require actions
//
//= require object
//= require components
//= require form_filler
//= require campaigns
//= require form_requests
//= require group_wizard
//= require step_adder

Chalk = {};
underscore = _;

_.templateSettings = {
  evaluate:    /\{\{(.+?)\}\}/g,
  interpolate: /\{\{=(.+?)\}\}/g,
  escape:      /\{\{-(.+?)\}\}/g
};

moment.locale('en', {
  calendar: {
    lastDay: '[Yesterday at] LT',
    sameDay: '[Today at] LT',
    nextDay: '[Tomorrow at] LT',
    lastWeek: 'dddd [at] LT',
    nextWeek: '[Next] dddd [at] LT',
    sameElse: 'L LT'
  }
});

Update:

We're still seeing this on production occasionally. We've also seen it in a case where we load a script before application.js and then reference it within:

javascript_include_tag 'mathjs'
javascript_include_tag 'application'

Every so often we see a math is not defined error. I'm wondering if an error happens during the loading of mathjs or other scripts preventing it from being loaded, but the fact that it happens on so many different libraries, and so infrequently, makes it seem less likely. We did put in some debug checks to see whether our application.js is fully loaded and it often doesn't seem to be, even if accessing something like Jquery later in the page.

One motivation in this was to avoid old browser notifications about scripts running too long, but we may just give up and pull it all into application.js to avoid the errors.

lobati
  • 5,978
  • 4
  • 33
  • 53
  • I've never had such a problem, but the easiest solution I can think about is adding a do {} while; before the $(document).ready. According to that fiddle: http://jsfiddle.net/295v5p8j/ (just tested it now) this will do nothing at all until jQuery is defined, perhaps in this way you can prevent that despite it should load everything syncronously? – briosheje Jul 03 '15 at 14:55
  • Do you have the non-minified source for the script that loads `analytics`? – light Jul 03 '15 at 17:41
  • @briosheje the problem is that if jquery/analytics isn't loaded by this point, it might never load. Then our users won't be able to do anything on the page. – lobati Jul 03 '15 at 18:19
  • @light the script that does all of the requires is a rails `application.js`. I'll add the full thing to the question. – lobati Jul 03 '15 at 18:22
  • When you get '$ is undefined' then jQuery isn't loaded when your script tag is loaded. That much seems pretty clear. So, have you looked at the state of the JavaScript that the application is serving? Is jQuery included? Is this with compiled assets in Production or is it also happening with uncompiled assets in dev? – hightempo Jun 12 '16 at 15:05
  • @hightempo This is on production with precompiled assets. Haven't seen it in dev mode. It's one in a thousand, but that means it happens almost daily for us. We were trying to separate things out to reduce script running complaints on old browsers, but may just move everything into `application.js`. – lobati Jun 12 '16 at 22:54
  • @lobati -- interesting, and yeah, hard to debug considering how rarely it occurs. But you're also right that it should never occur especially not at variable times like that. Rails doesn't recompile the assets randomly every time for each visitor! The JS load order should be consistent for each Production deploy. How are you trying to separate out the assets? Are you using conditional comments to serve old-browser versions of the assets like: ``? – hightempo Jun 12 '16 at 23:49
  • @hightempo No, we were trying to just separate out some of the scripts into different include tags. Old versions of IE have an arbitrary limit on the number of statements in a js dependency before it pops up that message. Added an update above with an example of how we tried this with `mathjs`. – lobati Jun 13 '16 at 00:05
  • Ah, ok. Well, that would be one pattern you could try. It's not a 'silver bullet' type answer to your problem but it might help. Create a separate 'application_ie.js' file along with other files that you want to break down for old versions of IE, *only*. Then, most users will just see your up-to-date, modern application.js. Users of old IE browsers will see the divided-JavaScript versions while users of other browsers will see your modern, single-file application.js. If you like that idea, I'm happy to add it with some code samples here as an 'answer'. – hightempo Jun 13 '16 at 17:11

7 Answers7

7

This can happen if you don't wait for the script to load that defines analytics or if you do not define the order in which the javascript files are loaded. Make sure that the script that defines analytics is always loaded before you try to call its method track. Depending on your setup the scripts could load in random order, leading to this unpredictable behavior.

You tried to make sure everything was loaded, but the listener $(document).ready(function () {}); just makes sure that the DOM is ready, not that analytics is available. And here you have the same problem. $ is just jQuery so $ is not defined means jQuery hasn't been loaded yet. So probably your script came before jQuery was loaded and tried to call what wasn't defined yet.

Ole Spaarmann
  • 13,483
  • 24
  • 85
  • 142
  • But we're loading the `analytics`/`jQuery` dependencies synchronously in the html head, so shouldn't those be fully loaded before hitting this script tag? – lobati Jun 23 '15 at 19:29
  • That depends on how you load it. But nothing ever happens without a reason and when $ is sometimes not defined that means that $ hasn't been loaded yet. I can only speculate what the reasons might be, that depends on your setup and code. – Ole Spaarmann Jun 23 '15 at 19:31
  • Can you tell anything about my specific setup that would cause it? Are there details I haven't given you that would help? – lobati Jun 23 '15 at 20:29
  • [This question on SO](http://stackoverflow.com/questions/11285941/rails-specify-load-order-of-javascript-files) might help you. You have to look into the RoR docs, especially into require_tree. This might solve your problem. If not load the page until the error occurs and then check the HTML source for the order the js files are included and also check your networking tab in the Chrome dev tools if all files are loaded without any errors. – Ole Spaarmann Jun 23 '15 at 20:48
  • We don't have `require_tree .` in our `application.js`, and even if we did, it should still all be loaded before the script tag in the body of the document. – lobati Jun 23 '15 at 20:53
  • I've tried reloading the page a number of times as well. It doesn't occur for me. – lobati Jun 23 '15 at 20:57
  • Well it is difficult to debug from distance. The only thing I can suggest is to try to reproduce the error and then find out why jQuery or analytics is not defined and what went wrong and go from there. Your code looks fine to me. – Ole Spaarmann Jun 23 '15 at 20:58
  • That's what I'm trying to do. I've spent a while looking at it and poking around for ways to reproduce it with no luck, which is why I asked the question in the first place. – lobati Jun 23 '15 at 21:01
  • I once had it with a font that I required in a css file and just sometimes it wouldn't load. In this case it had to do with the www subdomain. Sometimes a user would access the page with www and because of CSRF protection the font wouldn't load. The solution was to make sure everything always loads from the same domain. This unpredictable, seemingly not reproducible behavior still has a reason, you just have to narrow down the possible reasons. Maybe the browser that requests the page has something like Adblocker defined, that can also cause problems for certain users. – Ole Spaarmann Jun 23 '15 at 21:05
  • Hmm, that's an interesting thought. We are serving static assets from CloudFlare, so I wonder if our `application.js` is blocked, while this js loads because it is in the page... – lobati Jun 23 '15 at 21:11
  • This definitely could cause problems. Including js over multiple domains is a well known reason for headaches... – Ole Spaarmann Jun 23 '15 at 21:13
  • Hmm, but I can't block it. CloudFlare transparently serves up assets on the same domain. And being that it's synchronous, I would expect the page not even to load if that was the problem. – lobati Jun 23 '15 at 21:17
  • True. Maybe put Cloudflare into Dev Mode and check if the problem still shows up. I'm also a bit critical about Cloudflare. I couldn't really see performance increases. In my experience it is faster to set up your own caching solution with something like Ngnix or Varnish, which isn't terribly difficult to do. But maybe this is also not the reason, it was just an idea... – Ole Spaarmann Jun 23 '15 at 21:21
  • Well, that's kind of beside the point, but performance increases depend on a lot of factors. Our site loads fast enough, so we're not too worried about eking out a few milliseconds from our CDN layer. It's extremely simple, provides SSL, and means we can devote our attention to the application layer. – lobati Jun 23 '15 at 22:02
2

The basis of your problem probably lies in your assumption:

everything should be loaded synchronously

Everything is most decidedly not loaded synchronously. The HTTP 1.1 protocol supports piplining and due to chunked transfer encoding, your referenced objects may or may not complete loading before your main webpage has finished loading.

All of this happens asynchronously and you can't guarantee the order in which they are loaded. This is why browsers make multiple parallel connections to your web server when loading a single webpage. Because javascript and jQuery are event driven, they are by nature asynchronous which can become confusing if you don't understand that behavior well.

Compounding your problem is the fact that document onload JavaScript event (remember, jQuery just extends JavaScript) "is called when the DOM is ready which can be prior to images and other external content is loaded." And yes, this external content can include your link to the jquery.js script. If that loads after the DOM, then you will see the error "$ is not defined". Because the linked script has not yet loaded, the jquery selector is undefined. Likewise, with your other linked libraries.

Try using $(window).load() instead. This should work when all the referenced objects and the DOM has loaded.

Community
  • 1
  • 1
James Shewey
  • 222
  • 2
  • 18
  • I think you're conflating download order with load order. Yes, I can believe browsers may download resources asynchronously, but actually executing the js code asynchronously by default doesn't make sense. It would defy the point of ever having separate script tags. More on that here: http://stackoverflow.com/a/8996894/372479 Likewise, document load vs window load is irrelevant, because the problem here is that `$ is not defined`. Either way we get the same error. – lobati Jun 13 '16 at 20:25
  • It's a [lot more complicated than that.](https://goo.gl/) The jQuery website even states that ["A page can't be manipulated safely until the document is 'ready.'"](https://learn.jquery.com/using-jquery-core/document-ready/). They also suggest using the $( window ).load() event instead of $( document ).ready() if you need to ensure all objects, not just the DOM are loaded. The point of having separate script tags it so 1) provide for abstraction and 2) allow for scripts from multiple URLs, not just to ensure load order. – James Shewey Jun 13 '16 at 22:07
  • I updated my answer to explain why $ is undefined, but ultimately, regardless of the theoretical, the proof is in the pudding. Does $(window).load() fix the problem? – James Shewey Jun 13 '16 at 22:07
  • 1
    It does not fix it. We've tried both. The call to `$` gets executed immediately either way. Whether you're doing `$(window).load(callback)` or `$(document).ready(callback)`, the `$()` function gets run immediately. The only difference is when `callback` gets run. We're not getting an error from the callback. We're getting the error `$ is not defined`. – lobati Jun 13 '16 at 22:52
  • "$ is not defined" definitely means you are having some sort of error loading the link to the jquery.js script, but it is unclear to me at this point, what it might be. – James Shewey Jun 14 '16 at 05:02
1

Since scripts tend to load at random orders you may force your analytics script to load after everything is up and ready.

There are various approaches for this task.

HTML5 rocks has given a quite nice snippet for this kind of stuff. Moreover, depending on your needs you may use a module loader like require.js. Using loading promises and Require.js is pretty sweet too. Generally, you use a lot of JavaScript a module loader will help you with this mess.

I kept the ugliest and least efficient, still one solid approach for the end. Waiting for an external library to load may create huge bottlenecks and should be considered and also handled really carefully. Analytics fetching script is async and really hard to handle. On these cases I prefer

Loading external deferred scripts is somehow easy:

function loadExtScript(src, test, callback) {
  var s = document.createElement('script');
  s.src = src;
  document.body.appendChild(s);

  var callbackTimer = setInterval(function() {
    var call = false;
    try {
      call = test.call();
    } catch (e) {}

    if (call) {
      clearInterval(callbackTimer);
      callback.call();
    }
  }, 100);
}

Take a look at this example where I am loading jQuery dynamically with a promise function when the script is loaded: http://jsfiddle.net/4mtyu/487/

Here is a chaining demo loading Angular and jQuery in order :http://jsfiddle.net/4mtyu/488/

Considering the example above you may load your analytics as:

loadExtScript('https://code.jquery.com/jquery-2.1.4.js', function () {
    return (typeof jQuery == 'function');
}, jQueryLoaded);

function jQueryLoaded() {
    loadExtScript('https://analytics-lib.com/script.js', function () {
        return (typeof jQuery == 'function');
    }, analyticsLoadedToo);
}

function analyticsLoadedToo() {
    //cool we are up and running
    console.log("ready");
}
vorillaz
  • 5,285
  • 2
  • 24
  • 41
  • The thing is, none of these scripts are being loaded from an external source or asynchronously. jQuery is bundled with our application on our servers, as is analytics. – lobati Jul 04 '15 at 15:18
  • a setTimeout is not helping because you don't know how many times it takes to load the file. – Kulvar Jun 14 '16 at 12:40
  • @Kulvar It's `setInterval()` – vorillaz Jun 15 '16 at 10:10
  • Don't change much. You waste time in both case. You have `load` and `error` event to know when the script was retrieved or failed to be retrieved ^_^ – Kulvar Jun 15 '16 at 12:36
  • It really doesn't matter if jquery is on your servers or not, if it is not inline to the page/code, then it is independent from the page and therefore asynchronous. As long as you have ` – James Shewey Jun 16 '16 at 05:57
  • It's asynchronous only if loaded from a js file with a the `async=""` attribute. If you don't use the async option, it's synchronized. The browser will wait for the file to load and then execute it. – Kulvar Jun 16 '16 at 14:52
1

This code will load each script URL put in libs in the exact order, waiting for one to fully load before adding the next one. It's not as optimised than letting the browser doing it, but it allow you to monitor the errors and force the order of the loading.

(function(){
    var libs = [
        "http://example.com/jquery.js",
        "http://example.com/tracker.js",
        "http://example.com/myscript.js"
    ];
    var handle_error = function() {
        console.log("unable to load:", this.src);
    };
    var load_next_lib = function() {
        if(libs.length) {
            var script = document.createElement("script");
            script.type = "text/javascript";
            script.src = libs.shift();
            script.addEventListener("load", load_next_lib);
            script.addEventListener("error", handle_error);
            document.body.appendChild(script);
        }
    };
    load_next_lib();
})();

But I would advise you to check every <script> tag of your website and see if they have a defer="" or async="" attribute. Most issues come from these because they tell the browser to execute the script later. They may also just be in the wrong order.

Kulvar
  • 989
  • 7
  • 21
0

As others have described, you are loading scripts and then trying to execute code before your scripts have finished loading. Since this is an occasionally intermittent error around a relatively small amount of code located in your HTML, you can fix it with some simple polling:

(function runAnalytics () {
  if (typeof analytics === 'undefined') {
    return setTimeout(runAnalytics, 100);
  }
  analytics.track('on that awesome page');
}());

... the same can be used for other libraries, hopefully you see the pattern:

(function runJQuery () {
  if (typeof $ === 'undefined') {
    return setTimeout(runJQuery, 100);
  }
  $(document).ready(...);
}());

Edit: As pointed out in the comments, the load events would be better suited for this. The problem is that those events might have already fired by the time the script executes and you have to write fallback code for those scenarios. In favor of not writing 10+ lines of code to execute 1 line, I suggested a simple, non-invasive, quick and dirty, sure to work on every browser polling solution. But in order to not get any more downvotes, here's a more proper tedious and over complicated solution in case you have some beef with polling:

<script>
// you can reuse this function
function waitForScript (src, callback) {
  var scripts = document.getElementsByTagName('script');
  for(var i = 0, l = scripts.length; i < l; i++) {
    if (scripts[i].src.indexOf(src) > -1) {
      break;
    }
  }
  if (i < l) {
    scripts[i].onload = callback;
  }
}

function runAnalytics () {
  analytics.track('on that awesome page');
}

if (typeof analytics === 'undefined') {
  waitForScript('analytics.js', runAnalytics);
} else {
  runAnalytics();
}

function runJQuery () {
  $(document).ready(...);
}

if (typeof $ === 'undefined') {
  waitForScript('jquery.min.js', runJQuery);
} else {
  runJQuery();
}
</script>
Ryan Wheale
  • 18,754
  • 5
  • 58
  • 83
  • 1
    `load` and `error` event are better than `setTimeout` or `setInterval` to know if it successfully or failed to retrieve the script. – Kulvar Jun 15 '16 at 12:38
  • You are correct - there are a dozen things better than polling. But if a `` from the top of the page has already loaded or error'd by the time an inline `` at the bottom of the page executes, there is no way for the inline script to know - the event has already passed. Therefore polling is the next best option. FWIW - my company builds and maintains a widely used open source module loader - I am well aware of _better_ options. – Ryan Wheale Jun 15 '16 at 16:20
  • Or you can make the script nodes with javascript, then monitor their status with events. Using Promise or something else, you can handle dependancies too. ^_^ – Kulvar Jun 16 '16 at 14:49
0

We're still seeing this error a few times a day and haven't reached any conclusive answer to what is causing it. My best guess is that either the earlier scripts time out downloading, or a user is quickly navigating between links, preventing the page from loading completely. They might also just be using the back button causing weird behavior in scripts on the page.

lobati
  • 5,978
  • 4
  • 33
  • 53
-1

It has a cool solution. Make js loading correctly for your application. Here, I am loading jQuery first then loading analytics.js . I hope this will solve your problem for html5 supported browsers. Code:

    var fileList =[
                    'your_file_path/jQuery.js',
                    'your_file_path/analytics.js'
                  ];

    fileList.forEach(function(src) {
    var script = document.createElement('script');
    script.src = src;
    script.async = false;
    document.head.appendChild(script);
 });

This code snippet will load jQuery first and then load analytics.js file. Hopefully, this will fix your issue.

  • This doesn't seem substantively different from just having the script tags in the head in the first place. Scripts tags are by default sync: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script – lobati Jun 12 '16 at 23:15