UNPKG

@segment/analytics.js-integration-segmentio

Version:
1,405 lines (1,208 loc) 57.4 kB
'use strict'; var Analytics = require('@segment/analytics.js-core').constructor; var JSON = require('json3'); var Segment = require('../lib/'); var assert = require('proclaim'); var cookie = require('component-cookie'); var integration = require('@segment/analytics.js-integration'); var protocol = require('@segment/protocol'); var sandbox = require('@segment/clear-env'); var store = require('yields-store'); var tester = require('@segment/analytics.js-integration-tester'); var type = require('component-type'); var sinon = require('sinon'); var send = require('@segment/send-json'); var Schedule = require('@segment/localstorage-retry/lib/schedule'); var lolex = require('lolex'); // FIXME(ndhoule): clear-env's AJAX request clearing interferes with PhantomJS 2 // Detect Phantom env and use it to disable affected tests. We should use a // better/more robust way of intercepting and canceling AJAX requests to avoid // this hackery var isPhantomJS = /PhantomJS/.test(window.navigator.userAgent); describe('Segment.io', function() { var segment; var analytics; var options; before(function() { // Just to make sure that `cookie()` // doesn't throw URIError we add a cookie // that will cause `decodeURIComponent()` to throw. document.cookie = 'bad=%'; }); beforeEach(function() { options = { apiKey: 'oq0vdlg7yi' }; protocol.reset(); analytics = new Analytics(); segment = new Segment(options); analytics.use(Segment); analytics.use(tester); analytics.add(segment); analytics.assert(Segment.global === window); resetCookies(); if (window.localStorage) { window.localStorage.clear(); } }); afterEach(function() { analytics.restore(); analytics.reset(); resetCookies(); if (window.localStorage) { window.localStorage.clear(); } segment.reset(); sandbox(); }); function resetCookies() { store('s:context.referrer', null); cookie('s:context.referrer', null, { maxage: -1, path: '/' }); store('_ga', null); cookie('_ga', null, { maxage: -1, path: '/' }); store('seg_xid', null); cookie('seg_xid', null, { maxage: -1, path: '/' }); store('seg_xid_fd', null); cookie('seg_xid_fd', null, { maxage: -1, path: '/' }); store('seg_xid_ts', null); cookie('seg_xid_ts', null, { maxage: -1, path: '/' }); } it('should have the right settings', function() { analytics.compare( Segment, integration('Segment.io') .option('apiKey', '') .option('retryQueue', true) ); }); it('should always be turned on', function(done) { var Analytics = analytics.constructor; var ajs = new Analytics(); ajs.use(Segment); ajs.initialize({ 'Segment.io': options }); ajs.ready(function() { var segment = ajs._integrations['Segment.io']; segment.ontrack = sinon.spy(); ajs.track('event', {}, { All: false }); assert(segment.ontrack.calledOnce); done(); }); }); describe('Segment.storage()', function() { it('should return cookie() when the protocol isnt file://', function() { analytics.assert(Segment.storage(), cookie); }); it('should return store() when the protocol is file://', function() { analytics.assert(Segment.storage(), cookie); protocol('file:'); analytics.assert(Segment.storage(), store); }); it('should return store() when the protocol is chrome-extension://', function() { analytics.assert(Segment.storage(), cookie); protocol('chrome-extension:'); analytics.assert(Segment.storage(), store); }); }); describe('before loading', function() { beforeEach(function() { analytics.stub(segment, 'load'); }); describe('#normalize', function() { var object; beforeEach(function() { segment.cookie('s:context.referrer', null); analytics.initialize(); object = {}; }); it('should add .anonymousId', function() { analytics.user().anonymousId('anon-id'); segment.normalize(object); analytics.assert(object.anonymousId === 'anon-id'); }); it('should add .sentAt', function() { segment.normalize(object); analytics.assert(object.sentAt); analytics.assert(type(object.sentAt) === 'date'); }); it('should add .userId', function() { analytics.user().id('user-id'); segment.normalize(object); analytics.assert(object.userId === 'user-id'); }); it('should not replace the .userId', function() { analytics.user().id('user-id'); object.userId = 'existing-id'; segment.normalize(object); analytics.assert(object.userId === 'existing-id'); }); it('should always add .anonymousId even if .userId is given', function() { var object = { userId: 'baz' }; segment.normalize(object); analytics.assert(object.anonymousId.length === 36); }); it('should add .context', function() { segment.normalize(object); analytics.assert(object.context); }); it('should not rewrite context if provided', function() { var ctx = {}; var object = { context: ctx }; segment.normalize(object); analytics.assert(object.context === ctx); }); it('should copy .options to .context', function() { var opts = {}; var object = { options: opts }; segment.normalize(object); analytics.assert(object.context === opts); analytics.assert(object.options == null); }); it('should add .writeKey', function() { segment.normalize(object); analytics.assert(object.writeKey === segment.options.apiKey); }); it('should add .library', function() { segment.normalize(object); analytics.assert(object.context.library); analytics.assert(object.context.library.name === 'analytics.js'); analytics.assert(object.context.library.version === analytics.VERSION); }); it('should allow override of .library', function() { var ctx = { library: { name: 'analytics-wordpress', version: '1.0.3' } }; var object = { context: ctx }; segment.normalize(object); analytics.assert(object.context.library); analytics.assert(object.context.library.name === 'analytics-wordpress'); analytics.assert(object.context.library.version === '1.0.3'); }); it('should add .userAgent', function() { segment.normalize(object); analytics.assert(object.context.userAgent === navigator.userAgent); }); it('should add .locale', function() { segment.normalize(object); analytics.assert(object.context.locale === navigator.language); }); it('should not replace .locale if provided', function() { var ctx = { locale: 'foobar' }; var object = { context: ctx }; segment.normalize(object); analytics.assert(object.context.locale === 'foobar'); }); it('should add .campaign', function() { Segment.global = { navigator: {}, location: {} }; Segment.global.location.search = '?utm_source=source&utm_medium=medium&utm_term=term&utm_content=content&utm_campaign=name'; Segment.global.location.hostname = 'localhost'; segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(object.context.campaign); analytics.assert(object.context.campaign.source === 'source'); analytics.assert(object.context.campaign.medium === 'medium'); analytics.assert(object.context.campaign.term === 'term'); analytics.assert(object.context.campaign.content === 'content'); analytics.assert(object.context.campaign.name === 'name'); Segment.global = window; }); it('should decode .campaign', function() { Segment.global = { navigator: {}, location: {} }; Segment.global.location.search = '?utm_source=%5BFoo%5D'; segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(object.context.campaign); analytics.assert(object.context.campaign.source === '[Foo]'); Segment.global = window; }); it('should allow override of .campaign', function() { Segment.global = { navigator: {}, location: {} }; Segment.global.location.search = '?utm_source=source&utm_medium=medium&utm_term=term&utm_content=content&utm_campaign=name'; Segment.global.location.hostname = 'localhost'; var object = { context: { campaign: { source: 'overrideSource', medium: 'overrideMedium', term: 'overrideTerm', content: 'overrideContent', name: 'overrideName' } } }; segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(object.context.campaign); analytics.assert(object.context.campaign.source === 'overrideSource'); analytics.assert(object.context.campaign.medium === 'overrideMedium'); analytics.assert(object.context.campaign.term === 'overrideTerm'); analytics.assert(object.context.campaign.content === 'overrideContent'); analytics.assert(object.context.campaign.name === 'overrideName'); Segment.global = window; }); it('should add .referrer.id and .referrer.type', function() { Segment.global = { navigator: {}, location: {} }; Segment.global.location.search = '?utm_source=source&urid=medium'; Segment.global.location.hostname = 'localhost'; segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(object.context.referrer); analytics.assert(object.context.referrer.id === 'medium'); analytics.assert(object.context.referrer.type === 'millennial-media'); Segment.global = window; }); it('should add .referrer.id and .referrer.type from cookie', function() { segment.cookie( 's:context.referrer', '{"id":"baz","type":"millennial-media"}' ); Segment.global = { navigator: {}, location: {} }; Segment.global.location.search = '?utm_source=source'; Segment.global.location.hostname = 'localhost'; segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(object.context.referrer); analytics.assert(object.context.referrer.id === 'baz'); analytics.assert(object.context.referrer.type === 'millennial-media'); Segment.global = window; }); it('should add .referrer.id and .referrer.type from cookie when no query is given', function() { segment.cookie( 's:context.referrer', '{"id":"medium","type":"millennial-media"}' ); Segment.global = { navigator: {}, location: {} }; Segment.global.location.search = ''; Segment.global.location.hostname = 'localhost'; segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(object.context.referrer); analytics.assert(object.context.referrer.id === 'medium'); analytics.assert(object.context.referrer.type === 'millennial-media'); Segment.global = window; }); it('shouldnt add non amp ga cookie', function() { segment.cookie('_ga', 'some-nonamp-id'); segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(!object.context.amp); }); it('should add .amp.id from store', function() { segment.cookie('_ga', 'amp-foo'); segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(object.context.amp); analytics.assert(object.context.amp.id === 'amp-foo'); }); it('should not add .amp if theres no _ga', function() { segment.normalize(object); analytics.assert(object); analytics.assert(object.context); analytics.assert(!object.context.amp); }); it('should set xid from cookie, and context.traits is not defined', function() { segment.cookie('seg_xid', 'test_id'); segment.options.crossDomainIdServers = [ 'userdata.example1.com', 'xid.domain2.com', 'localhost' ]; segment.options.saveCrossDomainIdInLocalStorage = false; segment.normalize(object); assert.equal(object.context.traits.crossDomainId, 'test_id'); }); it('should set xid from cookie, and context.traits is defined', function() { segment.cookie('seg_xid', 'test_id'); segment.options.crossDomainIdServers = [ 'userdata.example1.com', 'xid.domain2.com', 'localhost' ]; segment.options.saveCrossDomainIdInLocalStorage = false; var msg = { context: { traits: { email: 'prateek@segment.com' } } }; segment.normalize(msg); assert.equal(msg.context.traits.crossDomainId, 'test_id'); }); it('should set xid from localStorage, and context.traits is not defined', function() { window.localStorage.setItem('seg_xid', 'test_id'); segment.options.crossDomainIdServers = [ 'userdata.example1.com', 'xid.domain2.com', 'localhost' ]; segment.options.saveCrossDomainIdInLocalStorage = true; segment.normalize(object); assert.equal(object.context.traits.crossDomainId, 'test_id'); }); it('should set xid from localStorage, is enabled, and context.traits is defined', function() { window.localStorage.setItem('seg_xid', 'test_id'); segment.options.crossDomainIdServers = [ 'userdata.example1.com', 'xid.domain2.com', 'localhost' ]; segment.options.saveCrossDomainIdInLocalStorage = true; var msg = { context: { traits: { email: 'prateek@segment.com' } } }; segment.normalize(msg); assert.equal(msg.context.traits.crossDomainId, 'test_id'); }); it('should not set xid if available, and is disabled', function() { segment.cookie('seg_xid', 'test_id'); segment.options.crossDomainIdServers = []; segment.normalize(object); // context.traits will not be set, which implicitly verifies that // context.traits.crossDomainId is not set. assert.equal(object.context.traits, undefined); }); it('should not set xid if not available, and context.traits is not defined', function() { segment.cookie('seg_xid', null); segment.normalize(object); // context.traits will not be set, which implicitly verifies that // context.traits.crossDomainId is not set. assert.equal(object.context.traits, undefined); }); it('should not set xid if not available and context.traits is defined', function() { segment.cookie('seg_xid', null); var msg = { context: { traits: { email: 'prateek@segment.com' } } }; segment.normalize(msg); assert.equal(msg.context.traits.crossDomainId, undefined); }); describe('failed initializations', function() { it('should add failedInitializations as part of _metadata object if this.analytics.failedInitilizations is not empty', function() { var spy = sinon.spy(segment, 'normalize'); var TestIntegration = integration('TestIntegration'); TestIntegration.prototype.initialize = function() { throw new Error('Uh oh!'); }; TestIntegration.prototype.page = function() {}; var testIntegration = new TestIntegration(); analytics.use(TestIntegration); analytics.add(testIntegration); analytics.initialize(); analytics.page(); assert( spy.returnValues[0]._metadata.failedInitializations[0] === 'TestIntegration' ); }); }); describe('unbundling', function() { var segment; beforeEach(function() { var Analytics = analytics.constructor; var ajs = new Analytics(); segment = new Segment(options); ajs.use(Segment); ajs.use(integration('other')); ajs.use(integration('another')); ajs.add(segment); ajs.initialize({ other: {}, another: {} }); }); it('should add a list of bundled integrations when `addBundledMetadata` is set', function() { segment.options.addBundledMetadata = true; segment.normalize(object); assert(object); assert(object._metadata); assert.deepEqual(object._metadata.bundled, ['Segment.io', 'other', 'another']); }); it('should add a list of unbundled integrations when `addBundledMetadata` and `unbundledIntegrations` are set', function() { segment.options.addBundledMetadata = true; segment.options.unbundledIntegrations = ['other2']; segment.normalize(object); assert(object); assert(object._metadata); assert.deepEqual(object._metadata.unbundled, ['other2']); }); it('should not add _metadata when `addBundledMetadata` is unset', function() { segment.normalize(object); assert(object); assert(!object._metadata); }); it('should generate and add a list of bundled destination config ids when `addBundledMetadata` is set', function() { segment.options.addBundledMetadata = true; segment.options.maybeBundledConfigIds = { 'other': ['config21'], 'slack': ['slack99'] // should be ignored }; segment.normalize(object); assert(object); assert(object._metadata); assert.deepEqual(object._metadata.bundledIds, ['config21']); }); it('should generate a list of multiple bundled destination config ids when `addBundledMetadata` is set', function() { segment.options.addBundledMetadata = true; segment.options.maybeBundledConfigIds = { 'other': ['config21'], 'another': ['anotherConfig99'], 'slack': ['slack99'] // should be ignored }; segment.normalize(object); assert(object); assert(object._metadata); assert.deepEqual(object._metadata.bundledIds, ['config21', 'anotherConfig99']); }); }); it('should pick up messageId from AJS', function() { object = analytics.normalize(object); // ajs core generates the message ID here var messageId = object.messageId; segment.normalize(object); assert.equal(object.messageId, messageId); }); }); }); describe('after loading', function() { beforeEach(function(done) { analytics.once('ready', done); analytics.initialize(); analytics.page(); }); describe('#settings', function() { it('should have retryQueue enabled', function() { analytics.assert(segment.options.retryQueue === true); }); }); var cases = { 'retryQueue enabled': true, 'retryQueue disabled': false }; for (var scenario in cases) { if (!cases.hasOwnProperty(scenario)) { continue; } describe('with ' + scenario, function() { beforeEach(function() { segment.options.retryQueue = cases[scenario]; }); describe('#page', function() { beforeEach(function() { analytics.stub(segment, 'enqueue'); }); it('should enqueue section, name and properties', function() { analytics.page( 'section', 'name', { property: true }, { opt: true } ); var args = segment.enqueue.args[0]; analytics.assert(args[0] === '/p'); analytics.assert(args[1].name === 'name'); analytics.assert(args[1].category === 'section'); analytics.assert(args[1].properties.property === true); analytics.assert(args[1].context.opt === true); analytics.assert(args[1].timestamp); }); }); describe('#identify', function() { beforeEach(function() { analytics.stub(segment, 'enqueue'); }); it('identify should not ultimately call getCachedCrossDomainId if crossDomainAnalytics is not enabled', function() { segment.options.crossDomainIdServers = []; var getCachedCrossDomainIdSpy = sinon.spy( segment, 'getCachedCrossDomainId' ); segment.normalize({}); sinon.assert.notCalled(getCachedCrossDomainIdSpy); segment.getCachedCrossDomainId.restore(); }); it('should enqueue an id and traits', function() { analytics.identify('id', { trait: true }, { opt: true }); var args = segment.enqueue.args[0]; analytics.assert(args[0] === '/i'); analytics.assert(args[1].userId === 'id'); analytics.assert(args[1].traits.trait === true); analytics.assert(args[1].context.opt === true); analytics.assert(args[1].timestamp); }); }); describe('#track', function() { beforeEach(function() { analytics.stub(segment, 'enqueue'); }); it('should enqueue an event and properties', function() { analytics.track('event', { prop: true }, { opt: true }); var args = segment.enqueue.args[0]; analytics.assert(args[0] === '/t'); analytics.assert(args[1].event === 'event'); analytics.assert(args[1].context.opt === true); analytics.assert(args[1].properties.prop === true); analytics.assert(args[1].traits == null); analytics.assert(args[1].timestamp); }); }); describe('#group', function() { beforeEach(function() { analytics.stub(segment, 'enqueue'); }); it('should enqueue groupId and traits', function() { analytics.group('id', { trait: true }, { opt: true }); var args = segment.enqueue.args[0]; analytics.assert(args[0] === '/g'); analytics.assert(args[1].groupId === 'id'); analytics.assert(args[1].context.opt === true); analytics.assert(args[1].traits.trait === true); analytics.assert(args[1].timestamp); }); }); describe('#alias', function() { beforeEach(function() { analytics.stub(segment, 'enqueue'); }); it('should enqueue .userId and .previousId', function() { analytics.alias('to', 'from'); var args = segment.enqueue.args[0]; analytics.assert(args[0] === '/a'); analytics.assert(args[1].previousId === 'from'); analytics.assert(args[1].userId === 'to'); analytics.assert(args[1].timestamp); }); it('should fallback to user.anonymousId if .previousId is omitted', function() { analytics.user().anonymousId('anon-id'); analytics.alias('to'); var args = segment.enqueue.args[0]; analytics.assert(args[0] === '/a'); analytics.assert(args[1].previousId === 'anon-id'); analytics.assert(args[1].userId === 'to'); analytics.assert(args[1].timestamp); }); it('should fallback to user.anonymousId if .previousId and user.id are falsey', function() { analytics.alias('to'); var args = segment.enqueue.args[0]; analytics.assert(args[0] === '/a'); analytics.assert(args[1].previousId); analytics.assert(args[1].previousId.length === 36); analytics.assert(args[1].userId === 'to'); }); it('should rename `.from` and `.to` to `.previousId` and `.userId`', function() { analytics.alias('user-id', 'previous-id'); var args = segment.enqueue.args[0]; analytics.assert(args[0] === '/a'); analytics.assert(args[1].previousId === 'previous-id'); analytics.assert(args[1].userId === 'user-id'); analytics.assert(args[1].from == null); analytics.assert(args[1].to == null); }); }); describe('#enqueue', function() { var xhr; beforeEach(function() { analytics.spy(segment, 'session'); sinon.spy(segment, 'debug'); xhr = sinon.useFakeXMLHttpRequest(); }); afterEach(function() { if (xhr.restore) xhr.restore(); if (segment.debug.restore) segment.debug.restore(); }); it( 'should use https: protocol when http:', sinon.test(function() { var spy = sinon.spy(); xhr.onCreate = spy; protocol('http:'); segment.enqueue('/i', { userId: 'id' }); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual(req.url, 'https://api.segment.io/v1/i'); }) ); it( 'should use https: protocol when https:', sinon.test(function() { var spy = sinon.spy(); xhr.onCreate = spy; protocol('https:'); segment.enqueue('/i', { userId: 'id' }); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual(req.url, 'https://api.segment.io/v1/i'); }) ); it( 'should use https: protocol when https:', sinon.test(function() { var spy = sinon.spy(); xhr.onCreate = spy; protocol('file:'); segment.enqueue('/i', { userId: 'id' }); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual(req.url, 'https://api.segment.io/v1/i'); }) ); it( 'should use https: protocol when chrome-extension:', sinon.test(function() { var spy = sinon.spy(); xhr.onCreate = spy; protocol('chrome-extension:'); segment.enqueue('/i', { userId: 'id' }); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual(req.url, 'https://api.segment.io/v1/i'); }) ); it( 'should enqueue to `api.segment.io/v1` by default', sinon.test(function() { var spy = sinon.spy(); xhr.onCreate = spy; protocol('https:'); segment.enqueue('/i', { userId: 'id' }); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual(req.url, 'https://api.segment.io/v1/i'); }) ); it( 'should enqueue to `options.apiHost` when set', sinon.test(function() { segment.options.apiHost = 'api.example.com'; var spy = sinon.spy(); xhr.onCreate = spy; protocol('https:'); segment.enqueue('/i', { userId: 'id' }); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual(req.url, 'https://api.example.com/i'); }) ); it( 'should enqueue a normalized payload', sinon.test(function() { var spy = sinon.spy(); xhr.onCreate = spy; var payload = { key1: 'value1', key2: 'value2' }; segment.normalize = function() { return payload; }; segment.enqueue('/i', {}); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual(JSON.parse(req.requestBody).key1, 'value1'); assert.strictEqual(JSON.parse(req.requestBody).key2, 'value2'); }) ); it( 'should not log a normal payload', sinon.test(function() { var spy = sinon.spy(); xhr.onCreate = spy; var payload = { key1: 'value1', key2: 'value2' }; segment.normalize = function() { return payload; }; segment.enqueue('/i', {}); sinon.assert.neverCalledWith( segment.debug, 'message must be less than 32kb %O', payload ); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual(JSON.parse(req.requestBody).key1, 'value1'); assert.strictEqual(JSON.parse(req.requestBody).key2, 'value2'); }) ); it( 'should enqueue an oversized payload', sinon.test(function() { var spy = sinon.spy(); xhr.onCreate = spy; var payload = {}; for (var i = 0; i < 1750; i++) { payload['key' + i] = 'value' + i; } segment.normalize = function() { return payload; }; segment.enqueue('/i', {}); sinon.assert.calledWith( segment.debug, 'message must be less than 32kb %O', payload ); assert(spy.calledOnce); var req = spy.getCall(0).args[0]; assert.strictEqual( JSON.parse(req.requestBody).key1749, 'value1749' ); }) ); }); // FIXME(ndhoule): See note at `isPhantomJS` definition (isPhantomJS ? xdescribe : describe)('e2e tests — without queueing', function() { beforeEach(function() { segment.options.retryQueue = false; }); describe('/g', function() { it('should succeed', function(done) { segment.enqueue('/g', { groupId: 'gid', userId: 'uid' }, function( err, res ) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); }); }); describe('/p', function() { it('should succeed', function(done) { var data = { userId: 'id', name: 'page', properties: {} }; segment.enqueue('/p', data, function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); }); }); describe('/a', function() { it('should succeed', function(done) { var data = { userId: 'id', from: 'b', to: 'a' }; segment.enqueue('/a', data, function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); }); }); describe('/t', function() { it('should succeed', function(done) { var data = { userId: 'id', event: 'my-event', properties: {} }; segment.enqueue('/t', data, function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); }); }); describe('/i', function() { it('should succeed', function(done) { var data = { userId: 'id' }; segment.enqueue('/i', data, function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); }); }); }); (isPhantomJS ? xdescribe : describe)('e2e tests — with queueing', function() { beforeEach(function() { segment.options.retryQueue = true; analytics.initialize(); }); describe('/g', function() { it('should succeed', function(done) { segment._lsqueue.on('processed', function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); segment.enqueue('/g', { groupId: 'gid', userId: 'uid' }); }); }); describe('/p', function() { it('should succeed', function(done) { segment._lsqueue.on('processed', function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); segment.enqueue('/p', { userId: 'id', name: 'page', properties: {} }); }); }); describe('/a', function() { it('should succeed', function(done) { segment._lsqueue.on('processed', function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); segment.enqueue('/a', { userId: 'id', from: 'b', to: 'a' }); }); }); describe('/t', function() { it('should succeed', function(done) { segment._lsqueue.on('processed', function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); segment.enqueue('/t', { userId: 'id', event: 'my-event', properties: {} }); }); }); describe('/i', function() { it('should succeed', function(done) { segment._lsqueue.on('processed', function(err, res) { if (err) return done(err); analytics.assert(JSON.parse(res.responseText).success); done(); }); segment.enqueue('/i', { userId: 'id' }); }); }); }); describe('#cookie', function() { beforeEach(function() { segment.cookie('foo', null); }); it('should persist the cookie even when the hostname is "dev"', function() { Segment.global = { navigator: {}, location: {} }; Segment.global.location.href = 'https://dev:300/path'; analytics.assert(segment.cookie('foo') == null); segment.cookie('foo', 'bar'); analytics.assert(segment.cookie('foo') === 'bar'); Segment.global = window; }); it('should persist the cookie even when the hostname is "127.0.0.1"', function() { Segment.global = { navigator: {}, location: {} }; Segment.global.location.href = 'http://127.0.0.1:3000/'; analytics.assert(segment.cookie('foo') == null); segment.cookie('foo', 'bar'); analytics.assert(segment.cookie('foo') === 'bar'); Segment.global = window; }); it('should persist the cookie even when the hostname is "app.herokuapp.com"', function() { Segment.global = { navigator: {}, location: {} }; Segment.global.location.href = 'https://app.herokuapp.com/about'; Segment.global.location.hostname = 'app.herokuapp.com'; analytics.assert(segment.cookie('foo') == null); segment.cookie('foo', 'bar'); analytics.assert(segment.cookie('foo') === 'bar'); Segment.global = window; }); }); describe('#crossDomainId', function() { var server; beforeEach(function() { server = sinon.fakeServer.create(); segment.options.crossDomainIdServers = [ 'userdata.example1.com', 'xid.domain2.com', 'localhost' ]; analytics.stub(segment, 'onidentify'); }); afterEach(function() { server.restore(); }); it('should not crash with invalid config', function() { segment.options.crossDomainIdServers = undefined; var res = null; var err = null; segment.retrieveCrossDomainId(function(error, response) { res = response; err = error; }); analytics.assert(!res); analytics.assert(err === 'crossDomainId not enabled'); }); it('should use cached cross domain identifier from LS when saveCrossDomainIdInLocalStorage is true', function() { segment.options.crossDomainIdServers = ['localhost']; segment.options.saveCrossDomainIdInLocalStorage = true; store('seg_xid', 'test_xid_cache_ls'); var res = null; var err = null; segment.retrieveCrossDomainId(function(error, response) { res = response; err = error; }); assert.isNull(err); assert.deepEqual(res, { crossDomainId: 'test_xid_cache_ls' }); }); it('should use cached cross domain identifier from cookies when saveCrossDomainIdInLocalStorage is false', function() { segment.options.crossDomainIdServers = ['localhost']; segment.options.saveCrossDomainIdInLocalStorage = false; segment.cookie('seg_xid', 'test_xid_cache_cookie'); var res = null; var err = null; segment.retrieveCrossDomainId(function(error, response) { res = response; err = error; }); assert.isNull(err); assert.deepEqual(res, { crossDomainId: 'test_xid_cache_cookie' }); }); describe('getCachedCrossDomainId', function() { it('should return identifiers from localstorage when saveCrossDomainIdInLocalStorage is true', function() { store('seg_xid', 'test_xid_cache_ls'); segment.cookie('seg_xid', 'test_xid_cache_cookie'); segment.options.saveCrossDomainIdInLocalStorage = true; assert.equal( segment.getCachedCrossDomainId(), 'test_xid_cache_ls' ); }); it('should return identifiers from localstorage when saveCrossDomainIdInLocalStorage is true', function() { store('seg_xid', 'test_xid_cache_ls'); segment.cookie('seg_xid', 'test_xid_cache_cookie'); segment.options.saveCrossDomainIdInLocalStorage = false; assert.equal( segment.getCachedCrossDomainId(), 'test_xid_cache_cookie' ); }); }); var cases = { 'saveCrossDomainIdInLocalStorage true': true, 'saveCrossDomainIdInLocalStorage false': false }; for (var scenario in cases) { if (!cases.hasOwnProperty(scenario)) { continue; } describe('with ' + scenario, function() { it('should obtain crossDomainId', function() { server.respondWith( 'GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ 200, { 'Content-Type': 'application/json' }, '{ "id": "xdomain-id-1" }' ] ); if (segment.options.saveCrossDomainIdInLocalStorage) { server.respondWith( 'GET', 'https://localhost/v1/saveId?writeKey=' + segment.options.apiKey + '&xid=xdomain-id-1', [200, { 'Content-Type': 'text/plan' }, 'OK'] ); } server.respondImmediately = true; var res = null; segment.retrieveCrossDomainId(function(err, response) { res = response; }); var identify = segment.onidentify.args[0]; analytics.assert( identify[0].traits().crossDomainId === 'xdomain-id-1' ); analytics.assert(res.crossDomainId === 'xdomain-id-1'); analytics.assert(res.fromDomain === 'xid.domain2.com'); assert.equal(segment.getCachedCrossDomainId(), 'xdomain-id-1'); }); it('should generate crossDomainId if no server has it', function() { server.respondWith( 'GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [ 200, { 'Content-Type': 'application/json' }, '{ "id": null }' ] ); server.respondWith( 'GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ 200, { 'Content-Type': 'application/json' }, '{ "id": null }' ] ); server.respondWith( 'GET', 'https://localhost/v1/id/' + segment.options.apiKey, [ 200, { 'Content-Type': 'application/json' }, '{ "id": null }' ] ); if (segment.options.saveCrossDomainIdInLocalStorage) { server.respondWith('GET', /https:\/\/localhost\/v1\/saveId/, [ 200, { 'Content-Type': 'text/plan' }, 'OK' ]); } server.respondImmediately = true; var res = null; segment.retrieveCrossDomainId(function(err, response) { res = response; }); var identify = segment.onidentify.args[0]; var crossDomainId = identify[0].traits().crossDomainId; analytics.assert(crossDomainId); analytics.assert(res.crossDomainId === crossDomainId); analytics.assert(res.fromDomain === 'localhost'); assert.equal(segment.getCachedCrossDomainId(), crossDomainId); }); it('should bail if all servers error', function() { var err = null; var res = null; segment.retrieveCrossDomainId(function(error, response) { err = error; res = response; }); server.respondWith( 'GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [500, { 'Content-Type': 'application/json' }, ''] ); server.respondWith( 'GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [500, { 'Content-Type': 'application/json' }, ''] ); server.respondWith( 'GET', 'https://localhost/v1/id/' + segment.options.apiKey, [500, { 'Content-Type': 'application/json' }, ''] ); server.respond(); var identify = segment.onidentify.args[0]; analytics.assert(!identify); analytics.assert(!res); analytics.assert.equal(err, 'Internal Server Error'); assert.equal(segment.getCachedCrossDomainId(), null); }); it('should bail if some servers fail and others have no xid', function() { var err = null; var res = null; segment.retrieveCrossDomainId(function(error, response) { err = error; res = response; }); server.respondWith( 'GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [400, { 'Content-Type': 'application/json' }, ''] ); server.respondWith( 'GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ 200, { 'Content-Type': 'application/json' }, '{ "id": null }' ] ); server.respondWith( 'GET', 'https://localhost/v1/id/' + segment.options.apiKey, [ 200, { 'Content-Type': 'application/json' }, '{ "id": null }' ] ); server.respond(); var identify = segment.onidentify.args[0]; analytics.assert(!identify); analytics.assert(!res); analytics.assert(err === 'Bad Request'); assert.equal(segment.getCachedCrossDomainId(), null); }); it('should succeed even if one server fails', function() { server.respondWith( 'GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [500, { 'Content-Type': 'application/json' }, ''] ); server.respondWith( 'GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [ 200, { 'Content-Type': 'application/json' }, '{ "id": "xidxid" }' ] ); if (segment.options.saveCrossDomainIdInLocalStorage) { server.respondWith( 'GET', 'https://localhost/v1/saveId?writeKey=' + segment.options.apiKey + '&xid=xidxid', [200, { 'Content-Type': 'text/plan' }, 'OK'] ); } server.respondImmediately = true; var err = null; var res = null; segment.retrieveCrossDomainId(function(error, response) { err = error; res = response; }); var identify = segment.onidentify.args[0]; analytics.assert( identify[0].traits().crossDomainId === 'xidxid' ); analytics.assert(res.crossDomainId === 'xidxid'); analytics.assert(res.fromDomain === 'userdata.example1.com'); analytics.assert(!err); assert.equal(segment.getCachedCrossDomainId(), 'xidxid'); }); }); } describe('isCrossDomainAnalyticsEnabled', function() { it('should return false when crossDomainIdServers is undefined', function() { segment.options.crossDomainIdServers = undefined; assert.equal(segment.isCrossDomainAnalyticsEnabled(), false); }); it('should return false when crossDomainIdServers is empty', function() { segment.options.crossDomainIdServers = []; assert.equal(segment.isCrossDomainAnalyticsEnabled(), false); }); it('should return true when crossDomainIdServers is set', function() { segment.options.crossDomainIdServers = [ 'userdata.example1.com', 'xid.domain2.com', 'localhost' ]; assert.equal(segment.isCrossDomainAnalyticsEnabled(), true); }); it('should return true even when crossDomainIdServers is set with 1 server', function() { segment.options.crossDomainIdServers = ['localhost']; assert.equal(segment.isCrossDomainAnalyticsEnabled(), true); }); }); describe('deleteCrossDomainId', function() {