Skip to content
This repository was archived by the owner on Nov 23, 2019. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 136 additions & 56 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', []);
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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<this.options.crossDomainIdServers.length; i++) {
var domain = this.options.crossDomainIdServers[i];
if (getTld(domain) !== currentTld) {
domains.push(domain);
}
}

getCrossDomainIdFromServerList(domains, writeKey, function(err, res) {
if (err) {
// We optimize for no conflicting xid as much as possible. So bail out if there is an
// error and we cannot be sure that xid does not exist on any other domains
if (callback) {
callback(err, null);
}
return;
}
var crossDomainId = null;
var fromDomain = null;
if (res) {
crossDomainId = res.id;
fromDomain = res.domain;
} else {
crossDomainId = uuid();
fromDomain = window.location.hostname;
}
var currentTimeMillis = (new Date()).getTime();
self.cookie('seg_xid', crossDomainId);
// Not actively used. Saving for future conflict resolution purposes
self.cookie('seg_xid_fd', fromDomain);
self.cookie('seg_xid_ts', currentTimeMillis);
self.analytics.identify({
crossDomainId: crossDomainId
var cachedCrossDomainId = this.getCachedCrossDomainId();
if (cachedCrossDomainId) {
// Callback is only provided in tests.
if (callback) {
callback(null, {
crossDomainId: cachedCrossDomainId
});
}
return;
}

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 < this.options.crossDomainIdServers.length; i++) {
var domain = this.options.crossDomainIdServers[i];
if (getTld(domain) !== currentTld) {
domains.push(domain);
}
}

getCrossDomainIdFromServerList(domains, writeKey, function(err, res) {
if (err) {
// Callback is only provided in tests.
if (callback) {
callback(null, {
crossDomainId: crossDomainId,
fromDomain: fromDomain,
timestamp: currentTimeMillis
});
callback(err, null);
}
// We optimize for no conflicting xid as much as possible. So bail out if there is an
// error and we cannot be sure that xid does not exist on any other domains.
return;
}

var crossDomainId = null;
var fromDomain = null;
if (res) {
crossDomainId = res.id;
fromDomain = res.domain;
} else {
crossDomainId = uuid();
fromDomain = window.location.hostname;
}

self.saveCrossDomainId(crossDomainId);
self.analytics.identify({
crossDomainId: crossDomainId
});

// Callback is only provided in tests.
if (callback) {
callback(null, {
crossDomainId: crossDomainId,
fromDomain: fromDomain
});
}
});
};

/**
* getCachedCrossDomainId returns the cross domain identifier stored on the client based on the `saveCrossDomainIdInLocalStorage` flag.
* If `saveCrossDomainIdInLocalStorage` is false, it reads it from the `seg_xid` cookie.
* If `saveCrossDomainIdInLocalStorage` is true, it reads it from the `seg_xid` key in localStorage.
*
* @return {string} crossDomainId
*/
Segment.prototype.getCachedCrossDomainId = function() {
if (this.options.saveCrossDomainIdInLocalStorage) {
return localstorage('seg_xid');
}
return this.cookie('seg_xid');
};

/**
* saveCrossDomainId saves the cross domain identifier. The implementation differs based on the `saveCrossDomainIdInLocalStorage` flag.
* If `saveCrossDomainIdInLocalStorage` is false, it saves it as the `seg_xid` cookie.
* If `saveCrossDomainIdInLocalStorage` is true, it saves it to localStorage (so that it can be accessed on the current domain)
* and as a httpOnly cookie (so that can it can be provided to other domains).
*
* @api private
*/
Segment.prototype.saveCrossDomainId = function(crossDomainId) {
if (!this.options.saveCrossDomainIdInLocalStorage) {
this.cookie('seg_xid', crossDomainId);
return;
}

var self = this;

// Save the cookie by making a request to the xid server for the current domain.
var currentTld = getTld(window.location.hostname);
for (var i = 0; i < this.options.crossDomainIdServers.length; i++) {
var domain = this.options.crossDomainIdServers[i];
if (getTld(domain) === currentTld) {
var writeKey = this.options.apiKey;
var url = 'https://' + domain + '/v1/saveId?writeKey=' + writeKey + '&xid=' + crossDomainId;

httpGet(url, function(err, res) {
if (err) {
self.debug('could not save id on %O, received %O', url, [err, res]);
return;
}

localstorage('seg_xid', crossDomainId);
});
return;
}
}
};

/**
* Deletes any state persisted by cross domain analytics.
* * seg_xid (and metadata) from cookies
* * seg_xid from localStorage
* * crossDomainId from traits in localStorage
*
* The deletion logic is run only if deletion is enabled for this project, and
* when either the seg_xid cookie or crossDomainId localStorage trait exists.
* The deletion logic is run only if deletion is enabled for this project, and only
* deletes the data that actually exists.
*
* @api private
*/
Expand All @@ -519,6 +573,11 @@ Segment.prototype.deleteCrossDomainIdIfNeeded = function() {
this.cookie('seg_xid_ts', null);
}

// Delete the xid from localStorage if it exists.
if (localstorage('seg_xid')) {
localstorage('seg_xid', null);
}

// Delete the crossDomainId trait in localStorage if it exists.
if (this.analytics.user().traits().crossDomainId) {
// This intentionally uses an internal API, so that
Expand Down Expand Up @@ -609,6 +668,27 @@ function getJson(url, callback) {
xhr.send();
}

/**
* get makes a get request to the given URL.
* @param {string} url
* @param {function} callback => 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.
Expand Down
Loading