Skip to content
This repository was archived by the owner on Nov 23, 2019. It is now read-only.

Commit 4e4de47

Browse files
authored
save cross domain identifier cookies from the server as an option (#56)
* 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. * 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)
1 parent 4e3116f commit 4e4de47

File tree

2 files changed

+350
-184
lines changed

2 files changed

+350
-184
lines changed

lib/index.js

Lines changed: 136 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ var Segment = exports = module.exports = integration('Segment.io')
6161
.option('apiHost', 'api.segment.io/v1')
6262
.option('crossDomainIdServers', [])
6363
.option('deleteCrossDomainId', false)
64+
.option('saveCrossDomainIdInLocalStorage', false)
6465
.option('retryQueue', true)
6566
.option('addBundledMetadata', false)
6667
.option('unbundledIntegrations', []);
@@ -169,16 +170,6 @@ Segment.prototype.initialize = function() {
169170
self.ready();
170171
});
171172

172-
// Migrate from old cross domain id cookie names
173-
if (this.cookie('segment_cross_domain_id')) {
174-
this.cookie('seg_xid', this.cookie('segment_cross_domain_id'));
175-
this.cookie('seg_xid_fd', this.cookie('segment_cross_domain_id_from_domain'));
176-
this.cookie('seg_xid_ts', this.cookie('segment_cross_domain_id_timestamp'));
177-
this.cookie('segment_cross_domain_id', null);
178-
this.cookie('segment_cross_domain_id_from_domain', null);
179-
this.cookie('segment_cross_domain_id_timestamp', null);
180-
}
181-
182173
// Delete cross domain identifiers.
183174
this.deleteCrossDomainIdIfNeeded();
184175

@@ -284,7 +275,7 @@ Segment.prototype.normalize = function(msg) {
284275
msg.writeKey = this.options.apiKey;
285276
ctx.userAgent = navigator.userAgent;
286277
if (!ctx.library) ctx.library = { name: 'analytics.js', version: this.analytics.VERSION };
287-
var crossDomainId = this.cookie('seg_xid');
278+
var crossDomainId = this.getCachedCrossDomainId();
288279
if (crossDomainId && this.isCrossDomainAnalyticsEnabled()) {
289280
if (!ctx.traits) {
290281
ctx.traits = { crossDomainId: crossDomainId };
@@ -440,69 +431,132 @@ Segment.prototype.isCrossDomainAnalyticsEnabled = function() {
440431
*/
441432
Segment.prototype.retrieveCrossDomainId = function(callback) {
442433
if (!this.isCrossDomainAnalyticsEnabled()) {
434+
// Callback is only provided in tests.
443435
if (callback) {
444436
callback('crossDomainId not enabled', null);
445437
}
446438
return;
447439
}
448-
if (!this.cookie('seg_xid')) {
449-
var self = this;
450-
var writeKey = this.options.apiKey;
451-
452-
// Exclude the current domain from the list of servers we're querying
453-
var currentTld = getTld(window.location.hostname);
454-
var domains = [];
455-
for (var i=0; i<this.options.crossDomainIdServers.length; i++) {
456-
var domain = this.options.crossDomainIdServers[i];
457-
if (getTld(domain) !== currentTld) {
458-
domains.push(domain);
459-
}
460-
}
461440

462-
getCrossDomainIdFromServerList(domains, writeKey, function(err, res) {
463-
if (err) {
464-
// We optimize for no conflicting xid as much as possible. So bail out if there is an
465-
// error and we cannot be sure that xid does not exist on any other domains
466-
if (callback) {
467-
callback(err, null);
468-
}
469-
return;
470-
}
471-
var crossDomainId = null;
472-
var fromDomain = null;
473-
if (res) {
474-
crossDomainId = res.id;
475-
fromDomain = res.domain;
476-
} else {
477-
crossDomainId = uuid();
478-
fromDomain = window.location.hostname;
479-
}
480-
var currentTimeMillis = (new Date()).getTime();
481-
self.cookie('seg_xid', crossDomainId);
482-
// Not actively used. Saving for future conflict resolution purposes
483-
self.cookie('seg_xid_fd', fromDomain);
484-
self.cookie('seg_xid_ts', currentTimeMillis);
485-
self.analytics.identify({
486-
crossDomainId: crossDomainId
441+
var cachedCrossDomainId = this.getCachedCrossDomainId();
442+
if (cachedCrossDomainId) {
443+
// Callback is only provided in tests.
444+
if (callback) {
445+
callback(null, {
446+
crossDomainId: cachedCrossDomainId
487447
});
448+
}
449+
return;
450+
}
451+
452+
var self = this;
453+
var writeKey = this.options.apiKey;
454+
455+
// Exclude the current domain from the list of servers we're querying
456+
var currentTld = getTld(window.location.hostname);
457+
var domains = [];
458+
for (var i = 0; i < this.options.crossDomainIdServers.length; i++) {
459+
var domain = this.options.crossDomainIdServers[i];
460+
if (getTld(domain) !== currentTld) {
461+
domains.push(domain);
462+
}
463+
}
464+
465+
getCrossDomainIdFromServerList(domains, writeKey, function(err, res) {
466+
if (err) {
467+
// Callback is only provided in tests.
488468
if (callback) {
489-
callback(null, {
490-
crossDomainId: crossDomainId,
491-
fromDomain: fromDomain,
492-
timestamp: currentTimeMillis
493-
});
469+
callback(err, null);
494470
}
471+
// We optimize for no conflicting xid as much as possible. So bail out if there is an
472+
// error and we cannot be sure that xid does not exist on any other domains.
473+
return;
474+
}
475+
476+
var crossDomainId = null;
477+
var fromDomain = null;
478+
if (res) {
479+
crossDomainId = res.id;
480+
fromDomain = res.domain;
481+
} else {
482+
crossDomainId = uuid();
483+
fromDomain = window.location.hostname;
484+
}
485+
486+
self.saveCrossDomainId(crossDomainId);
487+
self.analytics.identify({
488+
crossDomainId: crossDomainId
495489
});
490+
491+
// Callback is only provided in tests.
492+
if (callback) {
493+
callback(null, {
494+
crossDomainId: crossDomainId,
495+
fromDomain: fromDomain
496+
});
497+
}
498+
});
499+
};
500+
501+
/**
502+
* getCachedCrossDomainId returns the cross domain identifier stored on the client based on the `saveCrossDomainIdInLocalStorage` flag.
503+
* If `saveCrossDomainIdInLocalStorage` is false, it reads it from the `seg_xid` cookie.
504+
* If `saveCrossDomainIdInLocalStorage` is true, it reads it from the `seg_xid` key in localStorage.
505+
*
506+
* @return {string} crossDomainId
507+
*/
508+
Segment.prototype.getCachedCrossDomainId = function() {
509+
if (this.options.saveCrossDomainIdInLocalStorage) {
510+
return localstorage('seg_xid');
511+
}
512+
return this.cookie('seg_xid');
513+
};
514+
515+
/**
516+
* saveCrossDomainId saves the cross domain identifier. The implementation differs based on the `saveCrossDomainIdInLocalStorage` flag.
517+
* If `saveCrossDomainIdInLocalStorage` is false, it saves it as the `seg_xid` cookie.
518+
* If `saveCrossDomainIdInLocalStorage` is true, it saves it to localStorage (so that it can be accessed on the current domain)
519+
* and as a httpOnly cookie (so that can it can be provided to other domains).
520+
*
521+
* @api private
522+
*/
523+
Segment.prototype.saveCrossDomainId = function(crossDomainId) {
524+
if (!this.options.saveCrossDomainIdInLocalStorage) {
525+
this.cookie('seg_xid', crossDomainId);
526+
return;
527+
}
528+
529+
var self = this;
530+
531+
// Save the cookie by making a request to the xid server for the current domain.
532+
var currentTld = getTld(window.location.hostname);
533+
for (var i = 0; i < this.options.crossDomainIdServers.length; i++) {
534+
var domain = this.options.crossDomainIdServers[i];
535+
if (getTld(domain) === currentTld) {
536+
var writeKey = this.options.apiKey;
537+
var url = 'https://' + domain + '/v1/saveId?writeKey=' + writeKey + '&xid=' + crossDomainId;
538+
539+
httpGet(url, function(err, res) {
540+
if (err) {
541+
self.debug('could not save id on %O, received %O', url, [err, res]);
542+
return;
543+
}
544+
545+
localstorage('seg_xid', crossDomainId);
546+
});
547+
return;
548+
}
496549
}
497550
};
498551

499552
/**
500553
* Deletes any state persisted by cross domain analytics.
501554
* * seg_xid (and metadata) from cookies
555+
* * seg_xid from localStorage
502556
* * crossDomainId from traits in localStorage
503557
*
504-
* The deletion logic is run only if deletion is enabled for this project, and
505-
* when either the seg_xid cookie or crossDomainId localStorage trait exists.
558+
* The deletion logic is run only if deletion is enabled for this project, and only
559+
* deletes the data that actually exists.
506560
*
507561
* @api private
508562
*/
@@ -519,6 +573,11 @@ Segment.prototype.deleteCrossDomainIdIfNeeded = function() {
519573
this.cookie('seg_xid_ts', null);
520574
}
521575

576+
// Delete the xid from localStorage if it exists.
577+
if (localstorage('seg_xid')) {
578+
localstorage('seg_xid', null);
579+
}
580+
522581
// Delete the crossDomainId trait in localStorage if it exists.
523582
if (this.analytics.user().traits().crossDomainId) {
524583
// This intentionally uses an internal API, so that
@@ -609,6 +668,27 @@ function getJson(url, callback) {
609668
xhr.send();
610669
}
611670

671+
/**
672+
* get makes a get request to the given URL.
673+
* @param {string} url
674+
* @param {function} callback => err, response
675+
*/
676+
function httpGet(url, callback) {
677+
var xhr = new XMLHttpRequest();
678+
xhr.open('GET', url, true);
679+
xhr.withCredentials = true;
680+
xhr.onreadystatechange = function() {
681+
if (xhr.readyState === XMLHttpRequest.DONE) {
682+
if (xhr.status >= 200 && xhr.status < 300) {
683+
callback(null, xhr.responseText);
684+
} else {
685+
callback(xhr.statusText || xhr.responseText || 'Unknown Error', null);
686+
}
687+
}
688+
};
689+
xhr.send();
690+
}
691+
612692
/**
613693
* getTld
614694
* Get domain.com from subdomain.domain.com, etc.

0 commit comments

Comments
 (0)