@@ -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 */
441432Segment . 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