Mixpanel 2.2 within an AMD structured web-app - e.

2019-03-16 03:05发布

问题:

I'm attempting to make use of Mixpanel event tracking in a single page site based on Backbone.js and require.js.

Looking at the snippet that Mixpanel provide for cut-and-pasting into a regular webpage, I can tell they've rolled their own async loading mechanism that pulls in the actual Mixpanel API from a standalone resource, do some extra work to setup 'people' and other attributes, and finally expose the 'mixpanel' object through the global namespace.

I've tried to add shim config entries for either the snippet or the standalone API but neither work well.

Through my research, I found a project on github that does exactly what I want, however it's a few years old and is based on the 'old' mixpanel API. In the new version, Mixpanel have made some non-trivial changes to the snippet and API that I just can't get my head around.

I'm hoping somebody understands the Mixpanel snippet and/or AMD and require.js and can step me through this.

回答1:

There are two funny things that makes this an odd problem to solve:

  1. The mixpanel lib requires you to have window.mixpanel defined before you load it.
  2. The mixpanel lib re-defines window.mixpanel as a part of its init process.

Out of the box, the mixpanel snippet doesn't support get_distinct_id (and any call that is, by definition, synchronous) until the lib is loaded but does stub out other methods (such as track) BEFORE loading the mixpanel lib for the sake of queueing. Therefore we have two options:

Option 1. Drop async support and wait until the lib is loaded - Gist

This method works by creating a pre-init module to setup the window.mixpanel deps needed by the mixpanel lib and then specifying that as a dependency to the lib itself. Then requiring "mixpanel" will block until the lib is fully loaded.

<html>
    <head>
        <title>Mixpanel AMD Example - Sync</title>
        <script type="text/javascript" src="http://requirejs.org/docs/release/2.1.8/minified/require.js"></script>
        <script type="text/javascript">
            requirejs.config({
                paths : { 'mixpanel': "//cdn.mxpnl.com/libs/mixpanel-2.2.min" },
                shim: {
                    'mixpanel': {
                        deps: ['mixpanel-preinit'],
                        exports: 'mixpanel'
                    }
                }
            });
            define("mixpanel-preinit", function(require) {
                // this is a stripped down version of the mixpanel snippet that removes the loading of the lib via external script tag and the stubs for queuing calls
                var b=window.mixpanel=window.mixpanel||[];var i,g;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";b._i.push([a,e,d])};b.__SV=1.2;
                b.init("YOUR TOKEN");
            });
        </script>
    </head>
    <body>
        <script type="text/javascript">
            require(['mixpanel'], function(mixpanel) {
                mixpanel.track("my event", {prop1: "val1"}); 
                console.log(mixpanel.get_distinct_id()); 
            });
        </script>
    </body>
</html>

Option 2. Provide a "loaded" callback to update the module's properties. - Gist

If you REALLY want async support, you'll need to update your stub's methods once the mixpanel lib is loaded. I don't recommend this because (among other reasons) it results in window.mixpanel !== mixpanel after the copy. This also means you must protect against race conditions on synchronous calls like get_distinct_id(). If the lib hasn't loaded yet, it'll be undefined. NOTE: I recommend that if you must have async support, you should just call through window.mixpanel instead of all of this craziness.

<html>
    <head>
        <title>Mixpanel AMD Example - Async</title>
        <script type="text/javascript" src="http://requirejs.org/docs/release/2.1.8/minified/require.js"></script>
        <script type="text/javascript">
            requirejs.config({
                paths : { 'mixpanel-lib': "//cdn.mxpnl.com/libs/mixpanel-2.2.min" }
            });

            define("mixpanel", function(require) {
                var b = window.mixpanel || [];
                if (!b.__SV) { var i, g; window.mixpanel = b; b._i = []; b.init = function (a, e, d) { function f(b, h) { var a = h.split("."); 2 == a.length && (b = b[a[0]], h = a[1]); b[h] = function () { b.push([h].concat(Array.prototype.slice.call(arguments, 0))) } } var c = b; "undefined" !== typeof d ? c = b[d] = [] : d = "mixpanel"; c.people = c.people || []; c.toString = function (b) { var a = "mixpanel"; "mixpanel" !== d && (a += "." + d); b || (a += " (stub)"); return a }; c.people.toString = function () { return c.toString(1) + ".people (stub)" }; i = "disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" "); for (g = 0; g < i.length; g++) f(c, i[g]); b._i.push([a, e, d]) }; b.__SV = 1.2 }

                // go ahead and start loading the mixpanel-lib
                require(['mixpanel-lib']);

                b.init("YOUR TOKEN", {loaded: function() { 
                    // now that we know mixpanel is loaded, copy the prop references to our module def
                    for(var prop in window.mixpanel) {
                        b[prop] = window.mixpanel[prop];
                    }
                }}); 
                return b;
            });
        </script>
    </head>
    <body>
        <script type="text/javascript">
            require(['mixpanel'], function(mixpanel) {
                mixpanel.track("my event", {prop1: "val1"}); 
                console.log(mixpanel.get_distinct_id()); // probably undefined
            });
        </script>
    </body>
</html>


回答2:

Following solution works for mixpanel api 2.2

add mixpanel with following shim -

path : {
    'mixpanel' : '//cdn.mxpnl.com/libs/mixpanel-2.2.min'
}

shim : {
    'mixpanel' : {
        exports : 'mixpanel'
    },
}

and use following requirejs module instead of the the snippet given by mixpanel -

define('mixpanel-snippet', [], function(){
    var b = window.mixpanel || [];
    if (!b.__SV) {
        var i, g;
        window.mixpanel = b;
        b._i = [];
        b.init = function (a, e, d) {
            function f(b, h) {
                var a = h.split(".");
                2 == a.length && (b = b[a[0]], h = a[1]);
                b[h] = function () {
                    b.push([h].concat(Array.prototype.slice.call(arguments, 0)))
                }
            }
            var c = b;
            "undefined" !==
            typeof d ? c = b[d] = [] : d = "mixpanel";
            c.people = c.people || [];
            c.toString = function (b) {
                var a = "mixpanel";
                "mixpanel" !== d && (a += "." + d);
                b || (a += " (stub)");
                return a
            };
            c.people.toString = function () {
                return c.toString(1) + ".people (stub)"
            };
            i = "disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");
            for (g = 0; g < i.length; g++)
                f(c, i[g]);
            b._i.push([a, e, d])
        };
        b.__SV = 1.2
    }
    b.init("YOUR TOKEN");
    require(['mixpanel'], function(mixpanel){});

    return b;
});

I simply took the snippet from mixpanel, removed the async mixpanel load and wrapped it in a requirejs module definition.

Change "YOUR TOKEN" at the bottom of the module.

Example use with a require call --

require([
    'mixpanel-snippet',
], function (mixpanel) {
        mixpanel.track("Landing Page with AMD SHIM");
});

EDIT: The second one is the right answer after a little modification. the way mixpanel script works is it needs the init call in the snippet to happen before the actual mixpanel load. the trick is to require mixpanel after the init call.i have edited the 2nd answer and removed the first one and here's the gist

EDIT: Answer to comment from @johanandren Requirejs follows AMD principle and the order in which scripts will load is not fixed. In case you need to load mixpanel before using mixpanel-snippet, following hack can be used.

//at the end of mixpanel-snippet code mentioned above force the script to block until mixpanel is loaded

b.init("YOUR TOKEN");
var wait = true;
require(['mixpanel'], function(mixpanel){wait = false;});
while(wait){}
return b;

** it violates the Async load features of AMD, forces the script to block plus even in vanila mixpanel snippet the load is async and availability for initial api calls is not guaranteed



回答3:

This worked for me. Place your mixpanel snippet in your js/lib directory named mixpanel-snippet.js.

In your app.js add the following shim to require.config:

'mixpanel-snippet': {
  exports: 'mixpanel'
}

In your require function add 'mixpanel-snippet' to the required array and initialize mixpanel:

require(['jquery', 'backbone', 'app/router', 'mixpanel'], function ($, Backbone, Router) {
    var router = new Router();
    Backbone.history.start();
    mixpanel.init(key);
    mixpanel.track("Landed on Start up Page");
});

I can provide a full app.js example if it would help but this should get your started. Let me know if this works.



回答4:

As of mixpanel's 2.7.x release, they now support multiple AMD/UMD versions compatible with requirejs, you can grab it on their github page: https://github.com/mixpanel/mixpanel-js

requirejs(['./mixpanel.amd'], function(mixpanel) {

    mixpanel.init("FAKE_TOKEN", {
        debug: true,
        loaded: function() {
            mixpanel.track('loaded() callback works but is unnecessary');
            alert("Mixpanel loaded successfully via RequireJS/AMD");
        }
    });

    mixpanel.track('Tracking after mixpanel.init');

});