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