diff --git a/lib/index.js b/lib/index.js index 5b6b772..6a966f7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -61,6 +61,7 @@ var Segment = exports = module.exports = integration('Segment.io') .option('apiHost', 'api.segment.io/v1') .option('crossDomainIdServers', []) .option('deleteCrossDomainId', false) + .option('saveCrossDomainIdInLocalStorage', false) .option('retryQueue', true) .option('addBundledMetadata', false) .option('unbundledIntegrations', []); @@ -169,16 +170,6 @@ Segment.prototype.initialize = function() { self.ready(); }); - // Migrate from old cross domain id cookie names - if (this.cookie('segment_cross_domain_id')) { - this.cookie('seg_xid', this.cookie('segment_cross_domain_id')); - this.cookie('seg_xid_fd', this.cookie('segment_cross_domain_id_from_domain')); - this.cookie('seg_xid_ts', this.cookie('segment_cross_domain_id_timestamp')); - this.cookie('segment_cross_domain_id', null); - this.cookie('segment_cross_domain_id_from_domain', null); - this.cookie('segment_cross_domain_id_timestamp', null); - } - // Delete cross domain identifiers. this.deleteCrossDomainIdIfNeeded(); @@ -284,7 +275,7 @@ Segment.prototype.normalize = function(msg) { msg.writeKey = this.options.apiKey; ctx.userAgent = navigator.userAgent; if (!ctx.library) ctx.library = { name: 'analytics.js', version: this.analytics.VERSION }; - var crossDomainId = this.cookie('seg_xid'); + var crossDomainId = this.getCachedCrossDomainId(); if (crossDomainId && this.isCrossDomainAnalyticsEnabled()) { if (!ctx.traits) { ctx.traits = { crossDomainId: crossDomainId }; @@ -440,69 +431,132 @@ Segment.prototype.isCrossDomainAnalyticsEnabled = function() { */ Segment.prototype.retrieveCrossDomainId = function(callback) { if (!this.isCrossDomainAnalyticsEnabled()) { + // Callback is only provided in tests. if (callback) { callback('crossDomainId not enabled', null); } return; } - if (!this.cookie('seg_xid')) { - var self = this; - var writeKey = this.options.apiKey; - - // Exclude the current domain from the list of servers we're querying - var currentTld = getTld(window.location.hostname); - var domains = []; - for (var i=0; i err, response + */ +function httpGet(url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.withCredentials = true; + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status >= 200 && xhr.status < 300) { + callback(null, xhr.responseText); + } else { + callback(xhr.statusText || xhr.responseText || 'Unknown Error', null); + } + } + }; + xhr.send(); +} + /** * getTld * Get domain.com from subdomain.domain.com, etc. diff --git a/test/index.test.js b/test/index.test.js index a370a87..456c0f3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -44,12 +44,18 @@ describe('Segment.io', function() { analytics.add(segment); analytics.assert(Segment.global === window); resetCookies(); + if (window.localStorage) { + window.localStorage.clear(); + } }); afterEach(function() { analytics.restore(); analytics.reset(); resetCookies(); + if (window.localStorage) { + window.localStorage.clear(); + } segment.reset(); sandbox(); }); @@ -883,14 +889,6 @@ describe('Segment.io', function() { server.restore(); }); - it('should migrate cookies from old to new name', function() { - segment.cookie('segment_cross_domain_id', 'xid-test-1'); - segment.initialize(); - - analytics.assert(segment.cookie('segment_cross_domain_id') == null); - analytics.assert(segment.cookie('seg_xid') === 'xid-test-1'); - }); - it('should not crash with invalid config', function() { segment.options.crossDomainIdServers = undefined; @@ -905,149 +903,237 @@ describe('Segment.io', function() { analytics.assert(err === 'crossDomainId not enabled'); }); - it('should generate xid locally if there is only one (current hostname) server', function() { + it('should use cached cross domain identifier from LS when saveCrossDomainIdInLocalStorage is true', function() { segment.options.crossDomainIdServers = [ 'localhost' ]; + segment.options.saveCrossDomainIdInLocalStorage = true; + + store('seg_xid', 'test_xid_cache_ls'); var res = null; - segment.retrieveCrossDomainId(function(err, response) { + var err = null; + segment.retrieveCrossDomainId(function(error, response) { res = response; + err = error; }); - var identify = segment.onidentify.args[0]; - var crossDomainId = identify[0].traits().crossDomainId; - analytics.assert(crossDomainId); - - analytics.assert(res.crossDomainId === crossDomainId); - analytics.assert(res.fromDomain === 'localhost'); - }); - - it('should obtain crossDomainId', function() { - var res = null; - segment.retrieveCrossDomainId(function(err, response) { - res = response; + assert.isNull(err); + assert.deepEqual(res, { + crossDomainId: 'test_xid_cache_ls' }); - server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ - 200, - { 'Content-Type': 'application/json' }, - '{ "id": "xdomain-id-1" }' - ]); - server.respond(); - - var identify = segment.onidentify.args[0]; - analytics.assert(identify[0].traits().crossDomainId === 'xdomain-id-1'); - - analytics.assert(res.crossDomainId === 'xdomain-id-1'); - analytics.assert(res.fromDomain === 'xid.domain2.com'); }); - it('should generate crossDomainId if no server has it', function() { - var res = null; - segment.retrieveCrossDomainId(function(err, response) { - res = response; - }); + it('should use cached cross domain identifier from cookies when saveCrossDomainIdInLocalStorage is false', function() { + segment.options.crossDomainIdServers = [ + 'localhost' + ]; + segment.options.saveCrossDomainIdInLocalStorage = false; - server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ - 200, - { 'Content-Type': 'application/json' }, - '{ "id": null }' - ]); - server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ - 200, - { 'Content-Type': 'application/json' }, - '{ "id": null }' - ]); - server.respond(); - - var identify = segment.onidentify.args[0]; - var crossDomainId = identify[0].traits().crossDomainId; - analytics.assert(crossDomainId); - - analytics.assert(res.crossDomainId === crossDomainId); - analytics.assert(res.fromDomain === 'localhost'); - }); + segment.cookie('seg_xid', 'test_xid_cache_cookie'); - it('should bail if all servers error', function() { - var err = null; var res = null; + var err = null; segment.retrieveCrossDomainId(function(error, response) { - err = error; res = response; + err = error; }); - server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ - 500, - { 'Content-Type': 'application/json' }, - '' - ]); - server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ - 500, - { 'Content-Type': 'application/json' }, - '' - ]); - server.respond(); - - var identify = segment.onidentify.args[0]; - analytics.assert(!identify); - analytics.assert(!res); - analytics.assert(err === 'Internal Server Error'); + assert.isNull(err); + assert.deepEqual(res, { + crossDomainId: 'test_xid_cache_cookie' + }); }); - it('should bail if some servers fail and others have no xid', function() { - var err = null; - var res = null; - segment.retrieveCrossDomainId(function(error, response) { - err = error; - res = response; - }); + describe('getCachedCrossDomainId', function() { + it('should return identifiers from localstorage when saveCrossDomainIdInLocalStorage is true', function() { + store('seg_xid', 'test_xid_cache_ls'); + segment.cookie('seg_xid', 'test_xid_cache_cookie'); - server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ - 400, - { 'Content-Type': 'application/json' }, - '' - ]); - server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ - 200, - { 'Content-Type': 'application/json' }, - '{ "id": null }' - ]); - server.respond(); - - var identify = segment.onidentify.args[0]; - analytics.assert(!identify); - analytics.assert(!res); - analytics.assert(err === 'Bad Request'); - }); + segment.options.saveCrossDomainIdInLocalStorage = true; - it('should succeed even if one server fails', function() { - var err = null; - var res = null; - segment.retrieveCrossDomainId(function(error, response) { - err = error; - res = response; + assert.equal(segment.getCachedCrossDomainId(), 'test_xid_cache_ls'); }); - server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ - 500, - { 'Content-Type': 'application/json' }, - '' - ]); - server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ - 200, - { 'Content-Type': 'application/json' }, - '{ "id": "xidxid" }' - ]); - server.respond(); - - var identify = segment.onidentify.args[0]; - analytics.assert(identify[0].traits().crossDomainId === 'xidxid'); - - analytics.assert(res.crossDomainId === 'xidxid'); - analytics.assert(res.fromDomain === 'userdata.example1.com'); - analytics.assert(!err); + it('should return identifiers from localstorage when saveCrossDomainIdInLocalStorage is true', function() { + store('seg_xid', 'test_xid_cache_ls'); + segment.cookie('seg_xid', 'test_xid_cache_cookie'); + + segment.options.saveCrossDomainIdInLocalStorage = false; + + assert.equal(segment.getCachedCrossDomainId(), 'test_xid_cache_cookie'); + }); }); + var cases = { + 'saveCrossDomainIdInLocalStorage true': true, + 'saveCrossDomainIdInLocalStorage false': false + }; + + for (var scenario in cases) { + if (!cases.hasOwnProperty(scenario)) { + continue; + } + + describe('with ' + scenario, function() { + it('should generate xid locally if there is only one (current hostname) server', function() { + segment.options.crossDomainIdServers = [ + 'localhost' + ]; + segment.options.saveCrossDomainIdInLocalStorage = cases[scenario]; + + var res = null; + segment.retrieveCrossDomainId(function(err, response) { + res = response; + }); + + var identify = segment.onidentify.args[0]; + var crossDomainId = identify[0].traits().crossDomainId; + analytics.assert(crossDomainId); + + analytics.assert(res.crossDomainId === crossDomainId); + analytics.assert(res.fromDomain === 'localhost'); + + assert.equal(segment.getCachedCrossDomainId(), crossDomainId); + }); + + it('should obtain crossDomainId', function() { + var res = null; + segment.retrieveCrossDomainId(function(err, response) { + res = response; + }); + server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ + 200, + { 'Content-Type': 'application/json' }, + '{ "id": "xdomain-id-1" }' + ]); + server.respond(); + + var identify = segment.onidentify.args[0]; + analytics.assert(identify[0].traits().crossDomainId === 'xdomain-id-1'); + + analytics.assert(res.crossDomainId === 'xdomain-id-1'); + analytics.assert(res.fromDomain === 'xid.domain2.com'); + + assert.equal(segment.getCachedCrossDomainId(), 'xdomain-id-1'); + }); + + it('should generate crossDomainId if no server has it', function() { + var res = null; + segment.retrieveCrossDomainId(function(err, response) { + res = response; + }); + + server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ + 200, + { 'Content-Type': 'application/json' }, + '{ "id": null }' + ]); + server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ + 200, + { 'Content-Type': 'application/json' }, + '{ "id": null }' + ]); + server.respond(); + + var identify = segment.onidentify.args[0]; + var crossDomainId = identify[0].traits().crossDomainId; + analytics.assert(crossDomainId); + + analytics.assert(res.crossDomainId === crossDomainId); + analytics.assert(res.fromDomain === 'localhost'); + + assert.equal(segment.getCachedCrossDomainId(), crossDomainId); + }); + + it('should bail if all servers error', function() { + var err = null; + var res = null; + segment.retrieveCrossDomainId(function(error, response) { + err = error; + res = response; + }); + + server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ + 500, + { 'Content-Type': 'application/json' }, + '' + ]); + server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ + 500, + { 'Content-Type': 'application/json' }, + '' + ]); + server.respond(); + + var identify = segment.onidentify.args[0]; + analytics.assert(!identify); + analytics.assert(!res); + analytics.assert(err === 'Internal Server Error'); + + assert.equal(segment.getCachedCrossDomainId(), null); + }); + + it('should bail if some servers fail and others have no xid', function() { + var err = null; + var res = null; + segment.retrieveCrossDomainId(function(error, response) { + err = error; + res = response; + }); + + server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ + 400, + { 'Content-Type': 'application/json' }, + '' + ]); + server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ + 200, + { 'Content-Type': 'application/json' }, + '{ "id": null }' + ]); + server.respond(); + + var identify = segment.onidentify.args[0]; + analytics.assert(!identify); + analytics.assert(!res); + analytics.assert(err === 'Bad Request'); + + assert.equal(segment.getCachedCrossDomainId(), null); + }); + + it('should succeed even if one server fails', function() { + var err = null; + var res = null; + segment.retrieveCrossDomainId(function(error, response) { + err = error; + res = response; + }); + + server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ + 500, + { 'Content-Type': 'application/json' }, + '' + ]); + server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ + 200, + { 'Content-Type': 'application/json' }, + '{ "id": "xidxid" }' + ]); + server.respond(); + + var identify = segment.onidentify.args[0]; + analytics.assert(identify[0].traits().crossDomainId === 'xidxid'); + + analytics.assert(res.crossDomainId === 'xidxid'); + analytics.assert(res.fromDomain === 'userdata.example1.com'); + analytics.assert(!err); + + assert.equal(segment.getCachedCrossDomainId(), 'xidxid'); + }); + }); + } + describe('isCrossDomainAnalyticsEnabled', function() { it('should return false when crossDomainIdServers is undefined', function() { segment.options.crossDomainIdServers = undefined; @@ -1121,6 +1207,8 @@ describe('Segment.io', function() { segment.cookie('seg_xid', 'test_xid'); segment.cookie('seg_xid_ts', 'test_xid_ts'); segment.cookie('seg_xid_fd', 'test_xid_fd'); + store('seg_xid', 'test_xid'); + analytics.identify({ crossDomainId: 'test_xid' }); @@ -1130,6 +1218,7 @@ describe('Segment.io', function() { assert.equal(segment.cookie('seg_xid'), null); assert.equal(segment.cookie('seg_xid_ts'), null); assert.equal(segment.cookie('seg_xid_fd'), null); + assert.equal(store('seg_xid'), null); assert.equal(analytics.user().traits().crossDomainId, null); }); @@ -1184,9 +1273,6 @@ describe('Segment.io', function() { beforeEach(function(done) { xhr = sinon.useFakeXMLHttpRequest(); - if (window.localStorage) { - window.localStorage.clear(); - } analytics.once('ready', done); segment.options.retryQueue = true; analytics.initialize();