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

Commit cc762bf

Browse files
committed
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 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 0fcb8f2 commit cc762bf

File tree

2 files changed

+320
-167
lines changed

2 files changed

+320
-167
lines changed

lib/index.js

Lines changed: 105 additions & 46 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', []);
@@ -274,7 +275,7 @@ Segment.prototype.normalize = function(msg) {
274275
msg.writeKey = this.options.apiKey;
275276
ctx.userAgent = navigator.userAgent;
276277
if (!ctx.library) ctx.library = { name: 'analytics.js', version: this.analytics.VERSION };
277-
var crossDomainId = this.cookie('seg_xid');
278+
var crossDomainId = this.getCachedCrossDomainId();
278279
if (crossDomainId && this.isCrossDomainAnalyticsEnabled()) {
279280
if (!ctx.traits) {
280281
ctx.traits = { crossDomainId: crossDomainId };
@@ -430,69 +431,122 @@ Segment.prototype.isCrossDomainAnalyticsEnabled = function() {
430431
*/
431432
Segment.prototype.retrieveCrossDomainId = function(callback) {
432433
if (!this.isCrossDomainAnalyticsEnabled()) {
434+
// Callback is only provided in tests.
433435
if (callback) {
434436
callback('crossDomainId not enabled', null);
435437
}
436438
return;
437439
}
438-
if (!this.cookie('seg_xid')) {
439-
var self = this;
440-
var writeKey = this.options.apiKey;
441-
442-
// Exclude the current domain from the list of servers we're querying
443-
var currentTld = getTld(window.location.hostname);
444-
var domains = [];
445-
for (var i=0; i<this.options.crossDomainIdServers.length; i++) {
446-
var domain = this.options.crossDomainIdServers[i];
447-
if (getTld(domain) !== currentTld) {
448-
domains.push(domain);
449-
}
440+
441+
var cachedCrossDomainId = this.getCachedCrossDomainId();
442+
if (cachedCrossDomainId) {
443+
callback(null, {
444+
crossDomainId: cachedCrossDomainId
445+
});
446+
return;
447+
}
448+
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);
450459
}
460+
}
451461

452-
getCrossDomainIdFromServerList(domains, writeKey, function(err, res) {
453-
if (err) {
454-
// We optimize for no conflicting xid as much as possible. So bail out if there is an
455-
// error and we cannot be sure that xid does not exist on any other domains
456-
if (callback) {
457-
callback(err, null);
458-
}
459-
return;
460-
}
461-
var crossDomainId = null;
462-
var fromDomain = null;
463-
if (res) {
464-
crossDomainId = res.id;
465-
fromDomain = res.domain;
466-
} else {
467-
crossDomainId = uuid();
468-
fromDomain = window.location.hostname;
469-
}
470-
var currentTimeMillis = (new Date()).getTime();
471-
self.cookie('seg_xid', crossDomainId);
472-
// Not actively used. Saving for future conflict resolution purposes
473-
self.cookie('seg_xid_fd', fromDomain);
474-
self.cookie('seg_xid_ts', currentTimeMillis);
475-
self.analytics.identify({
476-
crossDomainId: crossDomainId
477-
});
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
478466
if (callback) {
479-
callback(null, {
480-
crossDomainId: crossDomainId,
481-
fromDomain: fromDomain,
482-
timestamp: currentTimeMillis
483-
});
467+
callback(err, null);
484468
}
469+
return;
470+
}
471+
472+
var crossDomainId = null;
473+
var fromDomain = null;
474+
if (res) {
475+
crossDomainId = res.id;
476+
fromDomain = res.domain;
477+
} else {
478+
crossDomainId = uuid();
479+
fromDomain = window.location.hostname;
480+
}
481+
482+
self.saveCrossDomainId(crossDomainId);
483+
self.analytics.identify({
484+
crossDomainId: crossDomainId
485485
});
486+
487+
// Callback is only provided in tests.
488+
if (callback) {
489+
callback(null, {
490+
crossDomainId: crossDomainId,
491+
fromDomain: fromDomain
492+
});
493+
}
494+
});
495+
};
496+
497+
/**
498+
* getCachedCrossDomainId returns the cross domain identifier stored on the client based on the `saveCrossDomainIdInLocalStorage` flag.
499+
* If `saveCrossDomainIdInLocalStorage` is false, it reads it from the `seg_xid` cookie.
500+
* If `saveCrossDomainIdInLocalStorage` is true, it reads it from the `seg_xid` key in localStorage.
501+
*
502+
* @return {string} crossDomainId
503+
*/
504+
Segment.prototype.getCachedCrossDomainId = function() {
505+
if (this.options.saveCrossDomainIdInLocalStorage) {
506+
return localstorage('seg_xid');
507+
}
508+
return this.cookie('seg_xid');
509+
};
510+
511+
/**
512+
* saveCrossDomainId saves the cross domain identifier. The implementation differs based on the `saveCrossDomainIdInLocalStorage` flag.
513+
* If `saveCrossDomainIdInLocalStorage` is false, it saves it as the `seg_xid` cookie.
514+
* If `saveCrossDomainIdInLocalStorage` is true, it saves it to localStorage (so that it can be accessed on the current domain)
515+
* and as a httpOnly cookie (so that can it can be provided to other domains).
516+
*
517+
* @api private
518+
*/
519+
Segment.prototype.saveCrossDomainId = function(crossDomainId) {
520+
if (!this.options.saveCrossDomainIdInLocalStorage) {
521+
this.cookie('seg_xid', crossDomainId);
522+
return;
523+
}
524+
525+
localstorage('seg_xid', crossDomainId); // Save the cookie by making a request to the xid server for the current domain.
526+
527+
var currentTld = getTld(window.location.hostname);
528+
for (var i = 0; i < this.options.crossDomainIdServers.length; i++) {
529+
var domain = this.options.crossDomainIdServers[i];
530+
if (getTld(domain) === currentTld) {
531+
var writeKey = this.options.apiKey;
532+
var url = 'https://' + domain + '/v1/saveId?writeKey=' + writeKey + '&xid=' + crossDomainId;
533+
var xhr = new XMLHttpRequest();
534+
xhr.open('GET', url, true);
535+
xhr.withCredentials = true;
536+
xhr.send();
537+
return;
538+
}
486539
}
487540
};
488541

489542
/**
490543
* Deletes any state persisted by cross domain analytics.
491544
* * seg_xid (and metadata) from cookies
545+
* * seg_xid from localStorage
492546
* * crossDomainId from traits in localStorage
493547
*
494-
* The deletion logic is run only if deletion is enabled for this project, and
495-
* when either the seg_xid cookie or crossDomainId localStorage trait exists.
548+
* The deletion logic is run only if deletion is enabled for this project, and only
549+
* deletes the data that actually exists.
496550
*
497551
* @api private
498552
*/
@@ -509,6 +563,11 @@ Segment.prototype.deleteCrossDomainIdIfNeeded = function() {
509563
this.cookie('seg_xid_ts', null);
510564
}
511565

566+
// Delete the xid from localStorage if it exists.
567+
if (localstorage('seg_xid')) {
568+
localstorage('seg_xid', null);
569+
}
570+
512571
// Delete the crossDomainId trait in localStorage if it exists.
513572
if (this.analytics.user().traits().crossDomainId) {
514573
// This intentionally uses an internal API, so that

0 commit comments

Comments
 (0)