From dc73c2fa3b9cf7bf460e3c7276241a046b643f5f Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Tue, 11 Jun 2019 14:46:50 -0700 Subject: [PATCH 1/2] remove code to migrate legacy cookies we've verified that the legacy cookies are not in use anymore, hence we can get rid of this migration logic to simplify the client side code. here's a link to the same change we made on the server (internal link) https://github.com/segmentio/xid/pull/12. --- lib/index.js | 10 ---------- test/index.test.js | 8 -------- 2 files changed, 18 deletions(-) diff --git a/lib/index.js b/lib/index.js index 5b6b772..1e41848 100644 --- a/lib/index.js +++ b/lib/index.js @@ -169,16 +169,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(); diff --git a/test/index.test.js b/test/index.test.js index a370a87..08442bd 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -883,14 +883,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; From f5e089e77a67ee77d995eff93321766bfd035ecc Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Tue, 11 Jun 2019 14:51:59 -0700 Subject: [PATCH 2/2] save cross domain identifier cookies from the server as an option Previously xid metadata was stored as client side cookies. This change allows us to set the cookies from a server as httpOnly cookies. We also store the identifier in localStorage To allow the current domain to read it from javascript. This is only set if the request completes succesfully. This behaviour is behind a flag `saveCrossDomainIdInLocalStorage` that is off by default. This also removes some of the metadata that we don't use (such as the domain of the cookie and timestamp of the cookie) --- lib/index.js | 182 +++++++++++++++++------- test/index.test.js | 334 +++++++++++++++++++++++++++++---------------- 2 files changed, 350 insertions(+), 166 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1e41848..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', []); @@ -274,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 }; @@ -430,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 08442bd..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(); }); @@ -897,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; @@ -1113,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' }); @@ -1122,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); }); @@ -1176,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();