segment-boneyard / analytics.js-integrations

All of the third-party analytics.js integrations.
71 stars 0 forks source link

formalize only loading snippet once for tests #339

Closed lancejpollard closed 10 years ago

lancejpollard commented 10 years ago

/cc @ianstormtaylor putting thoughts here on how to handle only loading once

awesomatic

Awesomatic.prototype.initialize = function(page){
  var self = this;
  this.load(function(){
    window.Awesomatic.initialize(options, function(){
      self.ready(); // need to wait for initialize to callback
    });
  });
};

What we could do is create a snippet function that is similar to the original load function, and it has everything we only want to run once for tests. But then the new load can still load all sorts of tags.

Awesomatic.prototype.initialize = function(page){
  this.snippet(this.ready);
};

Awesomatic.prototype.snippet = function(fn){
  var self = this;
  this.load(function(){
    window.Awesomatic.initialize(options, fn);
  });
};

So then in the tests we would just stub out snippet in the after loading block.

describe('after loading', function(){
  beforeEach(function(done){
    analytics.stub(integration, 'snippet', function(fn) { fn(); });
    analytics.once('ready', done);
    analytics.initialize();
    analytics.page();
  });
});

Maybe we can clean that up somewhat so it's not repeated in every test, but we can come back to that later too. Or, we can do the _wrapSnippet like _wrapLoad, that does the .loaded() check to not run the snippet if it's already loaded.

This way:


Another option is to rename the new load method to something else, and name the snippet method back to load.


olark

Olark.prototype.initialize = function(page){
  var self = this;
  this.load(function(){
    tick(self.ready);
  });

  // assign chat to a specific site
  var groupId = this.options.groupId;
  if (groupId) {
    chat('setOperatorGroup', { group: groupId });
  }

  // keep track of the widget's open state
  var self = this;
  box('onExpand', function(){ self._open = true; });
  box('onShrink', function(){ self._open = false; });
};

This one is tougher. It creates an iframe, which loads scripts, and the scripts attach event handlers through the iframe.parent. So removing the iframe causes problems. I would love to spend the time eventually to figure out how to clean up the iframe so we can recreate it for each test. But for now, it should only be created once like we're saying for everything else.

So then it's like, what is happening in that initialize function? Or a better question:

What part of that initialize function do we test "before initialize" and what part do we test "after initialize"?

Right now, we're only testing one thing "before initialize":

it('should pass in group id to `configure`', function(){
  olark.options.groupId = 'groupId';
  analytics.initialize();
  analytics.page();
  analytics.called(window.olark, 'api.chat.setOperatorGroup', { group: 'groupId' });
});

This is possible because we can stub out the window.olark function. However, this only works to tell that the function got the correct arguments.

How about this next case? Why can't we test these in "before initialize"?

var self = this;
box('onExpand', function(){ self._open = true; });
box('onShrink', function(){ self._open = false; });
// which becomes just
window.olark('api.box.onExpand', fn);
window.olark('api.box.onShrink', fn);

The reason is, it depends on the iframe loading, whereas that api.chat.setOperatorGroup doesn't depend on anything: we can mock out the method.

We could mock out the method for window.olark again, and tell that it has the corrent function, but then we would have to emit a fake event, and then see if self._open = true. I dunno, maybe we do that? The problem is, we would have to stub out some internal details of olark itself.

What we're actually doing now, though, is waiting until the native event from olark gets emitted, which takes a second or two the first time.

The problem is, if we mock that out, we could potentially have named the event incorrectly, and so while our tests may pass, the code won't quite be right. So it seems we should still test the actual olark event.

So then it boils down to:

A couple of possibilities (there are probably way more too):

... more soon

lancejpollard commented 10 years ago

/cc @ianstormtaylor there's actually a bunch of cases like we're describing:

simple cases

adroll

AdRoll.prototype.initialize = function(page){
  window.adroll_adv_id = this.options.advId;
  window.adroll_pix_id = this.options.pixId;
  window.__adroll_loaded = true;
  var name = useHttps() ? 'https' : 'http';
  this.load(name, this.ready);
};

awesm

Awesm.prototype.initialize = function(page){
  window.AWESM = { api_key: this.options.apiKey };
  this.load(this.ready);
};

bugherd

BugHerd.prototype.initialize = function(page){
  window.BugHerdConfig = { feedback: { hide: !this.options.showFeedbackTab }};
  this.load(this.ready);
};

chartbeat

Chartbeat.prototype.initialize = function(page){
  var self = this;

  window._sf_async_config = window._sf_async_config || {};
  window._sf_async_config.useCanonical = true;
  defaults(window._sf_async_config, this.options);

  onBody(function(){
    window._sf_endpt = new Date().getTime();
    // Note: Chartbeat depends on document.body existing so the script does
    // not load until that is confirmed. Otherwise it may trigger errors.
    self.load(self.ready);
  });
};

churnbee

ChurnBee.prototype.initialize = function(page){
  push('_setApiKey', this.options.apiKey);
  this.load(this.ready);
};

clicky

Clicky.prototype.initialize = function(page){
  var user = this.analytics.user();
  window.clicky_site_ids = window.clicky_site_ids || [this.options.siteId];
  this.identify(new Identify({
    userId: user.id(),
    traits: user.traits()
  }));
  this.load(this.ready);
};

comscore

Comscore.prototype.initialize = function(page){
  window._comscore = window._comscore || [this.options];
  var name = useHttps() ? 'https' : 'http';
  this.load(name, this.ready);
};

crazyegg

CrazyEgg.prototype.initialize = function(page){
  var number = this.options.accountNumber;
  var path = number.slice(0,4) + '/' + number.slice(4);
  var cache = Math.floor(new Date().getTime() / 3600000);
  this.load({ path: path, cache: cache }, this.ready);
};

curebit

Curebit.prototype.initialize = function(page){
  push('init', { site_id: this.options.siteId, server: this.options.server });
  this.load(this.ready);

  // throttle the call to `page` since curebit needs to append an iframe
  this.page = throttle(bind(this, this.page), 250);
};

customerio?

Customerio.prototype.initialize = function(page){
  window._cio = window._cio || [];
  (function(){var a,b,c; a = function(f){return function(){window._cio.push([f].concat(Array.prototype.slice.call(arguments,0))); }; }; b = ['identify', 'track']; for (c = 0; c < b.length; c++) {window._cio[b[c]] = a(b[c]); } })();
  this.load(this.ready);
};

drip

Drip.prototype.initialize = function(page){
  window._dcq = window._dcq || [];
  window._dcs = window._dcs || {};
  window._dcs.account = this.options.account;
  this.load(this.ready);
};

errorception

Errorception.prototype.initialize = function(page){
  window._errs = window._errs || [this.options.projectId];
  onError(push);
  this.load(this.ready);
};

evergage

Evergage.prototype.initialize = function(page){
  var account = this.options.account;
  var dataset = this.options.dataset;

  window._aaq = window._aaq || [];
  push('setEvergageAccount', account);
  push('setDataset', dataset);
  push('setUseSiteConfig', true);

  this.load(this.ready);
};

facebook

Facebook.prototype.initialize = function(page){
  window._fbq = window._fbq || [];
  this.load(this.ready);
  window._fbq.loaded = true;
};

complex cases

alexa

Alexa.prototype.initialize = function(page){
  var self = this;
  window._atrk_opts = {
    atrk_acct: this.options.account,
    domain: this.options.domain,
    dynamic: this.options.dynamic
  };
  this.load(function(){
    window.atrk();
    self.ready();
  });
};

amplitude

init script multiple times, don't know what it will do if we don't reload the library more than once

Amplitude.prototype.initialize = function(page){
  (function(e,t){var r=e.amplitude||{}; r._q=[];function i(e){r[e]=function(){r._q.push([e].concat(Array.prototype.slice.call(arguments,0)));};} var s=["init","logEvent","setUserId","setGlobalUserProperties","setVersionName","setDomain"]; for (var c=0;c<s.length;c++){i(s[c]);}e.amplitude=r;})(window,document);
  window.amplitude.init(this.options.apiKey);
  this.load(this.ready);
};

awesomatic

Awesomatic.prototype.initialize = function(page){
  var self = this;
  var user = this.analytics.user();
  var id = user.id();
  var options = user.traits();

  options.appId = this.options.appId;
  if (id) options.user_id = id;

  this.load(function(){
    window.Awesomatic.initialize(options, function(){
      self.ready(); // need to wait for initialize to callback
    });
  });
};

bing

Bing.prototype.initialize = function(page){
  if (!window.mstag) {
    window.mstag = {
      loadTag: noop,
      time: (new Date()).getTime(),
      _write: writeToAppend
    };
  };
  var self = this;
  onbody(function(){
    self.load(function(){
      var loaded = bind(self, self.loaded);
      when(loaded, self.ready);
    });
  });
};

bronto

Bronto.prototype.initialize = function(page){
  var self = this;
  var params = qs.parse(window.location.search);
  if (!params._bta_tid && !params._bta_c) {
    this.debug('missing tracking URL parameters `_bta_tid` and `_bta_c`.');
  }
  this.load(function(){
    var opts = self.options;
    self.bta = new window.__bta(opts.siteId);
    if (opts.host) self.bta.setHost(opts.host);
    self.ready();
  });
};

bugsnag

Bugsnag.prototype.initialize = function(page){
  var self = this;
  this.load(function(){
    window.Bugsnag.apiKey = self.options.apiKey;
    self.ready();
  });
};

clicktale

ClickTale.prototype.initialize = function(page){
  var self = this;
  window.WRInitTime = date.getTime();

  onBody(function(body){
    body.appendChild(domify('<div id="ClickTaleDiv" style="display: none;">'));
  });

  var http = this.options.httpCdnUrl;
  var https = this.options.httpsCdnUrl;
  if (useHttps() && !https) return this.debug('https option required');
  var src = useHttps() ? https : http;

  this.load({ src: src }, function(){
    window.ClickTale(
      self.options.projectId, 
      self.options.recordingRatio, 
      self.options.partitionId
    );
    self.ready();
  });
};