Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the Segment.io loader script push method names/args onto a queue which seemingly gets overwritten?

I've been dissecting the following code snippet, which is used to asynchronously load the Segment.io analytics wrapper script:

// Create a queue, but don't obliterate an existing one!
var analytics = analytics || [];

// Define a method that will asynchronously load analytics.js from our CDN.
analytics.load = function(apiKey) {

    // Create an async script element for analytics.js.
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.async = true;
    script.src = ('https:' === document.location.protocol ? 'https://' : 'http://') +
                  'd2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/' + apiKey + '/analytics.min.js';

    // Find the first script element on the page and insert our script next to it.
    var firstScript = document.getElementsByTagName('script')[0];
    firstScript.parentNode.insertBefore(script, firstScript);

    // Define a factory that generates wrapper methods to push arrays of
    // arguments onto our `analytics` queue, where the first element of the arrays
    // is always the name of the analytics.js method itself (eg. `track`).
    var methodFactory = function (type) {
        return function () {
            analytics.push([type].concat(Array.prototype.slice.call(arguments, 0)));
        };
    };

    // Loop through analytics.js' methods and generate a wrapper method for each.
    var methods = ['identify', 'track', 'trackLink', 'trackForm', 'trackClick',
                   'trackSubmit', 'pageview', 'ab', 'alias', 'ready'];

    for (var i = 0; i < methods.length; i++) {
        analytics[methods[i]] = methodFactory(methods[i]);
    }
};

// Load analytics.js with your API key, which will automatically load all of the
// analytics integrations you've turned on for your account. Boosh!
analytics.load('MYAPIKEY');

It's well commented and I can see what it's doing, but I'm puzzled when it comes to the methodFactory function, which pushes details (method name and arguments) of any method calls made before the main analytics.js script has loaded onto the global analytics array.

This is all well and good, but then if/when the main script does load, it seemingly just overwrites the global analytics variable (see last line here), so all that data will be lost.

I see how this prevents script errors in a web page by stubbing out methods which don't exist yet, but I don't understand why the stubs can't just return an empty function:

var methods = ['identify', 'track', 'trackLink', 'trackForm', 'trackClick',
               'trackSubmit', 'pageview', 'ab', 'alias', 'ready'];

for (var i = 0; i < methods.length; i++) {
    lib[methods[i]] = function () { };
}

What am I missing? Please, help me understand!

like image 643
Mark Bell Avatar asked Feb 13 '13 17:02

Mark Bell


1 Answers

Ian here, co-founder at Segment.io—I didn't actually write that code, Calvin did, but I can fill you in on what it's doing.

You're right, the methodFactory is stubbing out the methods so that they are available before the script loads, which means people can call analytics.track without wrapping those calls in an if or ready() call.

But the methods are actually better than "dumb" stubs, in that they save the method that was called, so we can replay the actions later. That's this part:

analytics.push([type].concat(Array.prototype.slice.call(arguments, 0)));

To make that more readable:

var methodFactory = function (method) {
    return function () {
        var args = Array.prototype.slice.call(arguments, 0);
        var newArgs = [method].concat(args);
        analytics.push(newArgs);
    };
};

It tacks on the name of the method that was called, which means if I analytics.identify('userId'), our queue actually gets an array that looks like:

['identify', 'userId']

Then, when our library loads in, it unloads all of the queued calls and replays them into the real methods (that are now available) so that all of the data recorded before load is still preserved. That's the key part, because we don't want to just throw away any calls that happen before our library has the chance to load. That looks like this:

// Loop through the interim analytics queue and reapply the calls to their
// proper analytics.js method.
while (window.analytics.length > 0) {
    var item = window.analytics.shift();
    var method = item.shift();
    if (analytics[method]) analytics[method].apply(analytics, item);
}

analytics is a local variable at that point, and after we're done replaying, we replace the global with the local analytics (which is the real deal).

Hope that makes sense. We're actually going to have a series on our blog about all the little tricks for 3rd-party Javascript, so you might dig that soon!

like image 85
Ian Storm Taylor Avatar answered Nov 15 '22 19:11

Ian Storm Taylor