├── .gitignore ├── .eslintrc ├── test ├── .eslintrc ├── support │ ├── mock.html │ └── global.js ├── tests.test.js ├── memory.test.js ├── index.test.js ├── store.test.js ├── cookie.test.js ├── normalize.test.js ├── group.test.js ├── user.test.js └── analytics.test.js ├── lib ├── index.js ├── group.js ├── memory.js ├── pageDefaults.js ├── store.js ├── normalize.js ├── cookie.js ├── user.js ├── entity.js └── analytics.js ├── circle.yml ├── README.md ├── LICENSE ├── karma.conf.js ├── Makefile ├── karma.conf.ci.js ├── package.json └── HISTORY.md /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@segment/eslint-config/browser/legacy" 3 | } 4 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@segment/eslint-config/mocha" 3 | } 4 | -------------------------------------------------------------------------------- /test/support/mock.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/support/global.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mockGlobalAnalytics = {}; 4 | global.analytics = mockGlobalAnalytics; 5 | module.exports = mockGlobalAnalytics; 6 | -------------------------------------------------------------------------------- /test/tests.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO: Remove this file 4 | // require('./analytics.js'); 5 | // require('./normalize.js'); 6 | // require('./memory.js'); 7 | // require('./cookie.js'); 8 | // require('./index.js'); 9 | // require('./group.js'); 10 | // require('./store.js'); 11 | // require('./user.js'); 12 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Analytics.js 5 | * 6 | * (C) 2013-2016 Segment.io Inc. 7 | */ 8 | 9 | var Analytics = require('./analytics'); 10 | 11 | // Create a new `analytics` singleton. 12 | var analytics = new Analytics(); 13 | 14 | // Expose `require`. 15 | // TODO(ndhoule): Look into deprecating, we no longer need to expose it in tests 16 | analytics.require = require; 17 | 18 | // Expose package version. 19 | analytics.VERSION = require('../package.json').version; 20 | 21 | /* 22 | * Exports. 23 | */ 24 | 25 | module.exports = analytics; 26 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4 4 | environment: 5 | NPM_CONFIG_PROGRESS: false 6 | NPM_CONFIG_SPIN: false 7 | TEST_REPORTS_DIR: $CIRCLE_TEST_REPORTS 8 | 9 | dependencies: 10 | pre: 11 | - npm config set "//registry.npmjs.org/:_authToken" $NPM_AUTH 12 | # - npm -g install codecov 13 | override: 14 | - make install 15 | 16 | test: 17 | override: 18 | - make test 19 | # XXX(ndhoule): Coverage disabled while supporting IE7/8, see karma.conf.js 20 | # post: 21 | # - cp -R coverage $CIRCLE_ARTIFACTS/ 22 | # - codecov 23 | 24 | deployment: 25 | publish: 26 | owner: segmentio 27 | # Works on e.g. `1.0.0-alpha.1` 28 | tag: /[0-9]+(\.[0-9]+)*(-.+)?/ 29 | commands: 30 | - npm publish . 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # analytics.js-core 2 | 3 | [![CircleCI](https://circleci.com/gh/segmentio/analytics.js-core.svg?style=shield&circle-token=802e83e9a76e911d83be24df8497b6283ee5dfeb)](https://circleci.com/gh/segmentio/analytics.js-core) 4 | [![Codecov](https://img.shields.io/codecov/c/github/segmentio/analytics.js-core/master.svg?maxAge=2592000)](https://codecov.io/gh/segmentio/analytics.js-core) 5 | 6 | This is the core of [Analytics.js][], the open-source library that powers data collection at [Segment](https://segment.com). 7 | 8 | To build this into a full, usable library, see the [Analytics.js](https://github.com/segmentio/analytics.js) repository. 9 | 10 | ## License 11 | 12 | Released under the [MIT license](License.md). 13 | 14 | [analytics.js]: https://segment.com/docs/libraries/analytics.js/ 15 | 16 | ## Releasing 17 | 18 | 1. Update the version in `package.json`. 19 | 2. Run `robo release x.y.z` where `x.y.z` is the new version. 20 | -------------------------------------------------------------------------------- /lib/group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Module dependencies. 5 | */ 6 | 7 | var Entity = require('./entity'); 8 | var bindAll = require('bind-all'); 9 | var debug = require('debug')('analytics:group'); 10 | var inherit = require('inherits'); 11 | 12 | /** 13 | * Group defaults 14 | */ 15 | 16 | Group.defaults = { 17 | persist: true, 18 | cookie: { 19 | key: 'ajs_group_id' 20 | }, 21 | localStorage: { 22 | key: 'ajs_group_properties' 23 | } 24 | }; 25 | 26 | 27 | /** 28 | * Initialize a new `Group` with `options`. 29 | * 30 | * @param {Object} options 31 | */ 32 | 33 | function Group(options) { 34 | this.defaults = Group.defaults; 35 | this.debug = debug; 36 | Entity.call(this, options); 37 | } 38 | 39 | 40 | /** 41 | * Inherit `Entity` 42 | */ 43 | 44 | inherit(Group, Entity); 45 | 46 | 47 | /** 48 | * Expose the group singleton. 49 | */ 50 | 51 | module.exports = bindAll(new Group()); 52 | 53 | 54 | /** 55 | * Expose the `Group` constructor. 56 | */ 57 | 58 | module.exports.Group = Group; 59 | -------------------------------------------------------------------------------- /test/memory.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('proclaim'); 4 | var memory = require('../lib').constructor.memory; 5 | 6 | describe('memory', function() { 7 | afterEach(function() { 8 | memory.remove('x'); 9 | }); 10 | 11 | describe('#get', function() { 12 | it('should not not get an empty record', function() { 13 | assert(memory.get('abc') === undefined); 14 | }); 15 | 16 | it('should get an existing record', function() { 17 | memory.set('x', { a: 'b' }); 18 | assert.deepEqual(memory.get('x'), { a: 'b' }); 19 | }); 20 | }); 21 | 22 | describe('#set', function() { 23 | it('should be able to set a record', function() { 24 | memory.set('x', { a: 'b' }); 25 | assert.deepEqual(memory.get('x'), { a: 'b' }); 26 | }); 27 | }); 28 | 29 | describe('#remove', function() { 30 | it('should be able to remove a record', function() { 31 | memory.set('x', { a: 'b' }); 32 | assert.deepEqual(memory.get('x'), { a: 'b' }); 33 | memory.remove('x'); 34 | assert(memory.get('x') === undefined); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Segment.io, Inc. (friends@segment.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/memory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Module Dependencies. 5 | */ 6 | 7 | var bindAll = require('bind-all'); 8 | var clone = require('@ndhoule/clone'); 9 | 10 | /** 11 | * HOP. 12 | */ 13 | 14 | var has = Object.prototype.hasOwnProperty; 15 | 16 | /** 17 | * Expose `Memory` 18 | */ 19 | 20 | module.exports = bindAll(new Memory()); 21 | 22 | /** 23 | * Initialize `Memory` store 24 | */ 25 | 26 | function Memory() { 27 | this.store = {}; 28 | } 29 | 30 | /** 31 | * Set a `key` and `value`. 32 | * 33 | * @param {String} key 34 | * @param {Mixed} value 35 | * @return {Boolean} 36 | */ 37 | 38 | Memory.prototype.set = function(key, value) { 39 | this.store[key] = clone(value); 40 | return true; 41 | }; 42 | 43 | /** 44 | * Get a `key`. 45 | * 46 | * @param {String} key 47 | */ 48 | 49 | Memory.prototype.get = function(key) { 50 | if (!has.call(this.store, key)) return; 51 | return clone(this.store[key]); 52 | }; 53 | 54 | /** 55 | * Remove a `key`. 56 | * 57 | * @param {String} key 58 | * @return {Boolean} 59 | */ 60 | 61 | Memory.prototype.remove = function(key) { 62 | delete this.store[key]; 63 | return true; 64 | }; 65 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | files: [ 7 | { pattern: 'test/support/*.html', included: false }, 8 | // NOTE: This must run before all tests 9 | 'test/support/global.js', 10 | 'test/**/*.test.js' 11 | ], 12 | 13 | browsers: ['PhantomJS'], 14 | 15 | frameworks: ['browserify', 'mocha'], 16 | 17 | reporters: ['spec'/* , 'coverage' */], 18 | 19 | preprocessors: { 20 | 'test/**/*.js': 'browserify' 21 | }, 22 | 23 | client: { 24 | mocha: { 25 | grep: process.env.GREP 26 | } 27 | }, 28 | 29 | browserify: { 30 | debug: true 31 | // Edge and Safari 9 still panic with coverage. Keeping disabled. 32 | // transform: [ 33 | // [ 34 | // 'browserify-istanbul', 35 | // { 36 | // instrumenterConfig: { 37 | // embedSource: true 38 | // } 39 | // } 40 | // ] 41 | // ] 42 | } 43 | 44 | // Edge and Safari 9 still panic with coverage. Keeping disabled. 45 | // coverageReporter: { 46 | // reporters: [ 47 | // { type: 'text' }, 48 | // { type: 'html' }, 49 | // { type: 'json' } 50 | // ] 51 | // } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var analytics = require('../lib'); 4 | var assert = require('proclaim'); 5 | 6 | describe('analytics', function() { 7 | it('should expose a .VERSION', function() { 8 | var pkg = require('../package.json'); 9 | assert.equal(analytics.VERSION, pkg.version); 10 | }); 11 | 12 | describe('noConflict', function() { 13 | var previousAnalyticsGlobal; 14 | 15 | beforeEach(function() { 16 | // Defined in test/support/global.js 17 | previousAnalyticsGlobal = window.analytics; 18 | assert(previousAnalyticsGlobal, 'test harness expected global.analytics to be defined but it is not'); 19 | }); 20 | 21 | afterEach(function() { 22 | previousAnalyticsGlobal = undefined; 23 | }); 24 | 25 | // TODO(ndhoule): this test and support/global.js are a little ghetto; we 26 | // should refactor this to run in a separate test suite 27 | it('should restore global.analytics to its previous value', function() { 28 | assert(global.analytics === previousAnalyticsGlobal); 29 | 30 | var analytics = require('../lib'); 31 | global.analytics = analytics; 32 | 33 | assert(global.analytics === analytics); 34 | 35 | var noConflictAnalytics = global.analytics.noConflict(); 36 | 37 | assert(global.analytics === previousAnalyticsGlobal); 38 | assert(noConflictAnalytics === analytics); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/store.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('proclaim'); 4 | var store = require('../lib').constructor.store; 5 | 6 | describe('store', function() { 7 | afterEach(function() { 8 | // reset to defaults 9 | store.options({}); 10 | store.remove('x'); 11 | }); 12 | 13 | describe('#get', function() { 14 | it('should not not get an empty record', function() { 15 | assert(store.get('abc') === undefined); 16 | }); 17 | 18 | it('should get an existing record', function() { 19 | store.set('x', { a: 'b' }); 20 | assert.deepEqual(store.get('x'), { a: 'b' }); 21 | }); 22 | }); 23 | 24 | describe('#set', function() { 25 | it('should be able to set a record', function() { 26 | store.set('x', { a: 'b' }); 27 | assert.deepEqual(store.get('x'), { a: 'b' }); 28 | }); 29 | }); 30 | 31 | describe('#remove', function() { 32 | it('should be able to remove a record', function() { 33 | store.set('x', { a: 'b' }); 34 | assert.deepEqual(store.get('x'), { a: 'b' }); 35 | store.remove('x'); 36 | assert(store.get('x') === undefined); 37 | }); 38 | }); 39 | 40 | describe('#options', function() { 41 | it('should be able to save options', function() { 42 | store.options({ enabled: false }); 43 | assert(store.options().enabled === false); 44 | assert(store.enabled === false); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /lib/pageDefaults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Module dependencies. 5 | */ 6 | 7 | var canonical = require('@segment/canonical'); 8 | var includes = require('@ndhoule/includes'); 9 | var url = require('component-url'); 10 | 11 | /** 12 | * Return a default `options.context.page` object. 13 | * 14 | * https://segment.com/docs/spec/page/#properties 15 | * 16 | * @return {Object} 17 | */ 18 | 19 | function pageDefaults() { 20 | return { 21 | path: canonicalPath(), 22 | referrer: document.referrer, 23 | search: location.search, 24 | title: document.title, 25 | url: canonicalUrl(location.search) 26 | }; 27 | } 28 | 29 | /** 30 | * Return the canonical path for the page. 31 | * 32 | * @return {string} 33 | */ 34 | 35 | function canonicalPath() { 36 | var canon = canonical(); 37 | if (!canon) return window.location.pathname; 38 | var parsed = url.parse(canon); 39 | return parsed.pathname; 40 | } 41 | 42 | /** 43 | * Return the canonical URL for the page concat the given `search` 44 | * and strip the hash. 45 | * 46 | * @param {string} search 47 | * @return {string} 48 | */ 49 | 50 | function canonicalUrl(search) { 51 | var canon = canonical(); 52 | if (canon) return includes('?', canon) ? canon : canon + search; 53 | var url = window.location.href; 54 | var i = url.indexOf('#'); 55 | return i === -1 ? url : url.slice(0, i); 56 | } 57 | 58 | /* 59 | * Exports. 60 | */ 61 | 62 | module.exports = pageDefaults; 63 | -------------------------------------------------------------------------------- /lib/store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Module dependencies. 5 | */ 6 | 7 | var bindAll = require('bind-all'); 8 | var defaults = require('@ndhoule/defaults'); 9 | var store = require('@segment/store'); 10 | 11 | /** 12 | * Initialize a new `Store` with `options`. 13 | * 14 | * @param {Object} options 15 | */ 16 | 17 | function Store(options) { 18 | this.options(options); 19 | } 20 | 21 | /** 22 | * Set the `options` for the store. 23 | * 24 | * @param {Object} options 25 | * @field {Boolean} enabled (true) 26 | */ 27 | 28 | Store.prototype.options = function(options) { 29 | if (arguments.length === 0) return this._options; 30 | 31 | options = options || {}; 32 | defaults(options, { enabled: true }); 33 | 34 | this.enabled = options.enabled && store.enabled; 35 | this._options = options; 36 | }; 37 | 38 | 39 | /** 40 | * Set a `key` and `value` in local storage. 41 | * 42 | * @param {string} key 43 | * @param {Object} value 44 | */ 45 | 46 | Store.prototype.set = function(key, value) { 47 | if (!this.enabled) return false; 48 | return store.set(key, value); 49 | }; 50 | 51 | 52 | /** 53 | * Get a value from local storage by `key`. 54 | * 55 | * @param {string} key 56 | * @return {Object} 57 | */ 58 | 59 | Store.prototype.get = function(key) { 60 | if (!this.enabled) return null; 61 | return store.get(key); 62 | }; 63 | 64 | 65 | /** 66 | * Remove a value from local storage by `key`. 67 | * 68 | * @param {string} key 69 | */ 70 | 71 | Store.prototype.remove = function(key) { 72 | if (!this.enabled) return false; 73 | return store.remove(key); 74 | }; 75 | 76 | 77 | /** 78 | * Expose the store singleton. 79 | */ 80 | 81 | module.exports = bindAll(new Store()); 82 | 83 | 84 | /** 85 | * Expose the `Store` constructor. 86 | */ 87 | 88 | module.exports.Store = Store; 89 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## 2 | # Binaries 3 | ## 4 | 5 | ESLINT := node_modules/.bin/eslint 6 | KARMA := node_modules/.bin/karma 7 | 8 | ## 9 | # Files 10 | ## 11 | 12 | LIBS = $(shell find lib -type f -name "*.js") 13 | TESTS = $(shell find test -type f -name "*.test.js") 14 | SUPPORT = $(wildcard karma.conf*.js) 15 | ALL_FILES = $(LIBS) $(TESTS) $(SUPPORT) 16 | 17 | ## 18 | # Program options/flags 19 | ## 20 | 21 | # A list of options to pass to Karma 22 | # Overriding this overwrites all options specified in this file (e.g. BROWSERS) 23 | KARMA_FLAGS ?= 24 | 25 | # A list of Karma browser launchers to run 26 | # http://karma-runner.github.io/0.13/config/browsers.html 27 | BROWSERS ?= 28 | ifdef BROWSERS 29 | KARMA_FLAGS += --browsers $(BROWSERS) 30 | endif 31 | 32 | ifdef CI 33 | KARMA_CONF ?= karma.conf.ci.js 34 | else 35 | KARMA_CONF ?= karma.conf.js 36 | endif 37 | 38 | # Mocha flags. 39 | GREP ?= . 40 | 41 | ## 42 | # Tasks 43 | ## 44 | 45 | # Install node modules. 46 | node_modules: package.json $(wildcard node_modules/*/package.json) 47 | @npm install 48 | @touch $@ 49 | 50 | # Install dependencies. 51 | install: node_modules 52 | 53 | # Remove temporary files and build artifacts. 54 | clean: 55 | rm -rf *.log coverage 56 | .PHONY: clean 57 | 58 | # Remove temporary files, build artifacts, and vendor dependencies. 59 | distclean: clean 60 | rm -rf node_modules 61 | .PHONY: distclean 62 | 63 | # Lint JavaScript source files. 64 | lint: install 65 | @$(ESLINT) $(ALL_FILES) 66 | .PHONY: lint 67 | 68 | # Attempt to fix linting errors. 69 | fmt: install 70 | @$(ESLINT) --fix $(ALL_FILES) 71 | .PHONY: fmt 72 | 73 | # Run browser unit tests in a browser. 74 | test-browser: install 75 | @$(KARMA) start $(KARMA_FLAGS) $(KARMA_CONF) 76 | .PHONY: test-browser 77 | 78 | # Default test target. 79 | test: lint test-browser 80 | .PHONY: test 81 | .DEFAULT_GOAL = test 82 | -------------------------------------------------------------------------------- /lib/normalize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module Dependencies. 5 | */ 6 | 7 | var debug = require('debug')('analytics.js:normalize'); 8 | var defaults = require('@ndhoule/defaults'); 9 | var each = require('@ndhoule/each'); 10 | var includes = require('@ndhoule/includes'); 11 | var map = require('@ndhoule/map'); 12 | var type = require('component-type'); 13 | 14 | /** 15 | * HOP. 16 | */ 17 | 18 | var has = Object.prototype.hasOwnProperty; 19 | 20 | /** 21 | * Expose `normalize` 22 | */ 23 | 24 | module.exports = normalize; 25 | 26 | /** 27 | * Toplevel properties. 28 | */ 29 | 30 | var toplevel = [ 31 | 'integrations', 32 | 'anonymousId', 33 | 'timestamp', 34 | 'context' 35 | ]; 36 | 37 | /** 38 | * Normalize `msg` based on integrations `list`. 39 | * 40 | * @param {Object} msg 41 | * @param {Array} list 42 | * @return {Function} 43 | */ 44 | 45 | function normalize(msg, list) { 46 | var lower = map(function(s) { return s.toLowerCase(); }, list); 47 | var opts = msg.options || {}; 48 | var integrations = opts.integrations || {}; 49 | var providers = opts.providers || {}; 50 | var context = opts.context || {}; 51 | var ret = {}; 52 | debug('<-', msg); 53 | 54 | // integrations. 55 | each(function(value, key) { 56 | if (!integration(key)) return; 57 | if (!has.call(integrations, key)) integrations[key] = value; 58 | delete opts[key]; 59 | }, opts); 60 | 61 | // providers. 62 | delete opts.providers; 63 | each(function(value, key) { 64 | if (!integration(key)) return; 65 | if (type(integrations[key]) === 'object') return; 66 | if (has.call(integrations, key) && typeof providers[key] === 'boolean') return; 67 | integrations[key] = value; 68 | }, providers); 69 | 70 | // move all toplevel options to msg 71 | // and the rest to context. 72 | each(function(value, key) { 73 | if (includes(key, toplevel)) { 74 | ret[key] = opts[key]; 75 | } else { 76 | context[key] = opts[key]; 77 | } 78 | }, opts); 79 | 80 | // cleanup 81 | delete msg.options; 82 | ret.integrations = integrations; 83 | ret.context = context; 84 | ret = defaults(ret, msg); 85 | debug('->', ret); 86 | return ret; 87 | 88 | function integration(name) { 89 | return !!(includes(name, list) || name.toLowerCase() === 'all' || includes(name.toLowerCase(), lower)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/cookie.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('proclaim'); 4 | var cookie = require('../lib').constructor.cookie; 5 | 6 | describe('cookie', function() { 7 | before(function() { 8 | // Just to make sure that 9 | // URIError is never thrown here. 10 | document.cookie = 'bad=%'; 11 | }); 12 | 13 | afterEach(function() { 14 | // reset to defaults 15 | cookie.options({}); 16 | cookie.remove('x'); 17 | }); 18 | 19 | describe('#get', function() { 20 | it('should not not get an empty cookie', function() { 21 | assert(cookie.get('abc') === null); 22 | }); 23 | 24 | it('should get an existing cookie', function() { 25 | cookie.set('x', { a: 'b' }); 26 | assert.deepEqual(cookie.get('x'), { a: 'b' }); 27 | }); 28 | 29 | it('should not throw an error on a malformed cookie', function() { 30 | document.cookie = 'x=y'; 31 | assert(cookie.get('x') === null); 32 | }); 33 | }); 34 | 35 | describe('#set', function() { 36 | it('should set a cookie', function() { 37 | cookie.set('x', { a: 'b' }); 38 | assert.deepEqual(cookie.get('x'), { a: 'b' }); 39 | }); 40 | }); 41 | 42 | describe('#remove', function() { 43 | it('should remove a cookie', function() { 44 | cookie.set('x', { a: 'b' }); 45 | assert.deepEqual(cookie.get('x'), { a: 'b' }); 46 | cookie.remove('x'); 47 | assert(cookie.get('x') === null); 48 | }); 49 | }); 50 | 51 | describe('#options', function() { 52 | it('should save options', function() { 53 | cookie.options({ path: '/xyz' }); 54 | assert(cookie.options().path === '/xyz'); 55 | assert(cookie.options().maxage === 31536000000); 56 | }); 57 | 58 | it('should set the domain correctly', function() { 59 | cookie.options({ domain: '' }); 60 | assert(cookie.options().domain === ''); 61 | }); 62 | 63 | it('should fallback to `domain=null` when it cant set the test cookie', function() { 64 | cookie.options({ domain: 'baz.com' }); 65 | assert(cookie.options().domain === null); 66 | assert(cookie.get('ajs:test') === null); 67 | }); 68 | 69 | // TODO: unskip once we don't use `window`, instead mock it :/ 70 | it.skip('should set domain localhost to `""`', function() { 71 | cookie.options({}); 72 | assert(cookie.options().domain === ''); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /karma.conf.ci.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | var baseConfig = require('./karma.conf'); 5 | 6 | var customLaunchers = { 7 | sl_chrome_latest: { 8 | base: 'SauceLabs', 9 | browserName: 'chrome', 10 | platform: 'linux', 11 | version: 'latest' 12 | }, 13 | sl_chrome_latest_1: { 14 | base: 'SauceLabs', 15 | browserName: 'chrome', 16 | platform: 'linux', 17 | version: 'latest-1' 18 | }, 19 | sl_firefox_latest: { 20 | base: 'SauceLabs', 21 | browserName: 'firefox', 22 | platform: 'linux', 23 | version: 'latest' 24 | }, 25 | sl_firefox_latest_1: { 26 | base: 'SauceLabs', 27 | browserName: 'firefox', 28 | platform: 'linux', 29 | version: 'latest-1' 30 | }, 31 | sl_safari_9: { 32 | base: 'SauceLabs', 33 | browserName: 'safari', 34 | version: '9.0' 35 | }, 36 | sl_ie_9: { 37 | base: 'SauceLabs', 38 | browserName: 'internet explorer', 39 | version: '9' 40 | }, 41 | sl_ie_10: { 42 | base: 'SauceLabs', 43 | browserName: 'internet explorer', 44 | version: '10' 45 | }, 46 | sl_ie_11: { 47 | base: 'SauceLabs', 48 | browserName: 'internet explorer', 49 | version: '11' 50 | }, 51 | sl_edge_latest: { 52 | base: 'SauceLabs', 53 | browserName: 'microsoftedge' 54 | } 55 | }; 56 | 57 | module.exports = function(config) { 58 | baseConfig(config); 59 | 60 | if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) { 61 | throw new Error('SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are required but are missing'); 62 | } 63 | 64 | config.set({ 65 | browserDisconnectTolerance: 1, 66 | 67 | browserDisconnectTimeout: 60000, 68 | 69 | browserNoActivityTimeout: 60000, 70 | 71 | singleRun: true, 72 | 73 | concurrency: 2, 74 | 75 | retryLimit: 5, 76 | 77 | reporters: ['progress', 'junit'], 78 | 79 | browsers: ['PhantomJS'].concat(Object.keys(customLaunchers)), 80 | 81 | customLaunchers: customLaunchers, 82 | 83 | junitReporter: { 84 | outputDir: process.env.TEST_REPORTS_DIR, 85 | suite: require('./package.json').name 86 | }, 87 | 88 | sauceLabs: { 89 | testName: require('./package.json').name 90 | } 91 | 92 | // Edge and Safari 9 still panic with coverage. Keeping disabled. 93 | // coverageReporter: { 94 | // reporters: [ 95 | // { type: 'lcov' } 96 | // ] 97 | // } 98 | }); 99 | }; 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@segment/analytics.js-core", 3 | "author": "Segment ", 4 | "version": "3.4.1", 5 | "description": "The hassle-free way to integrate analytics into any web application.", 6 | "keywords": [ 7 | "analytics", 8 | "analytics.js", 9 | "segment", 10 | "segment.io" 11 | ], 12 | "main": "lib/index.js", 13 | "scripts": { 14 | "test": "make test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/segmentio/analytics.js-core" 19 | }, 20 | "license": "SEE LICENSE IN LICENSE", 21 | "bugs": { 22 | "url": "https://github.com/segmentio/analytics.js-core/issues" 23 | }, 24 | "homepage": "https://github.com/segmentio/analytics.js-core#readme", 25 | "dependencies": { 26 | "@ndhoule/after": "^1.0.0", 27 | "@ndhoule/clone": "^1.0.0", 28 | "@ndhoule/defaults": "^2.0.1", 29 | "@ndhoule/each": "^2.0.1", 30 | "@ndhoule/extend": "^2.0.0", 31 | "@ndhoule/foldl": "^2.0.1", 32 | "@ndhoule/includes": "^2.0.1", 33 | "@ndhoule/keys": "^2.0.0", 34 | "@ndhoule/map": "^2.0.1", 35 | "@ndhoule/pick": "^2.0.0", 36 | "@segment/canonical": "^1.0.0", 37 | "@segment/is-meta": "^1.0.0", 38 | "@segment/isodate": "^1.0.2", 39 | "@segment/isodate-traverse": "^1.0.1", 40 | "@segment/prevent-default": "^1.0.0", 41 | "@segment/store": "^1.3.20", 42 | "@segment/top-domain": "^3.0.0", 43 | "bind-all": "^1.0.0", 44 | "extend": "3.0.1", 45 | "component-cookie": "^1.1.2", 46 | "component-emitter": "^1.2.1", 47 | "component-event": "^0.1.4", 48 | "component-querystring": "^2.0.0", 49 | "component-type": "^1.2.1", 50 | "component-url": "^0.2.1", 51 | "debug": "^0.7.4", 52 | "inherits": "^2.0.1", 53 | "install": "^0.7.3", 54 | "is": "^3.1.0", 55 | "json3": "^3.3.2", 56 | "new-date": "^1.0.0", 57 | "next-tick": "^0.2.2", 58 | "segmentio-facade": "^3.0.2", 59 | "uuid": "^2.0.2" 60 | }, 61 | "devDependencies": { 62 | "@segment/analytics.js-integration": "^3.2.1", 63 | "@segment/eslint-config": "^3.1.1", 64 | "browserify": "13.0.0", 65 | "compat-trigger-event": "^1.0.0", 66 | "component-each": "^0.2.6", 67 | "eslint": "^2.9.0", 68 | "eslint-plugin-mocha": "^2.2.0", 69 | "eslint-plugin-require-path-exists": "^1.1.5", 70 | "jquery": "^3.2.1", 71 | "karma": "1.3.0", 72 | "karma-browserify": "^5.0.4", 73 | "karma-chrome-launcher": "^1.0.1", 74 | "karma-coverage": "^1.0.0", 75 | "karma-junit-reporter": "^1.0.0", 76 | "karma-mocha": "1.0.1", 77 | "karma-phantomjs-launcher": "^1.0.0", 78 | "karma-sauce-launcher": "^1.0.0", 79 | "karma-spec-reporter": "0.0.26", 80 | "mocha": "^2.2.5", 81 | "phantomjs-prebuilt": "^2.1.7", 82 | "proclaim": "^3.4.1", 83 | "sinon": "^1.7.3", 84 | "watchify": "^3.7.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/cookie.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var bindAll = require('bind-all'); 8 | var clone = require('@ndhoule/clone'); 9 | var cookie = require('component-cookie'); 10 | var debug = require('debug')('analytics.js:cookie'); 11 | var defaults = require('@ndhoule/defaults'); 12 | var json = require('json3'); 13 | var topDomain = require('@segment/top-domain'); 14 | 15 | /** 16 | * Initialize a new `Cookie` with `options`. 17 | * 18 | * @param {Object} options 19 | */ 20 | 21 | function Cookie(options) { 22 | this.options(options); 23 | } 24 | 25 | 26 | /** 27 | * Get or set the cookie options. 28 | * 29 | * @param {Object} options 30 | * @field {Number} maxage (1 year) 31 | * @field {String} domain 32 | * @field {String} path 33 | * @field {Boolean} secure 34 | */ 35 | 36 | Cookie.prototype.options = function(options) { 37 | if (arguments.length === 0) return this._options; 38 | 39 | options = options || {}; 40 | 41 | var domain = '.' + topDomain(window.location.href); 42 | if (domain === '.') domain = null; 43 | 44 | this._options = defaults(options, { 45 | // default to a year 46 | maxage: 31536000000, 47 | path: '/', 48 | domain: domain 49 | }); 50 | 51 | // http://curl.haxx.se/rfc/cookie_spec.html 52 | // https://publicsuffix.org/list/effective_tld_names.dat 53 | // 54 | // try setting a dummy cookie with the options 55 | // if the cookie isn't set, it probably means 56 | // that the domain is on the public suffix list 57 | // like myapp.herokuapp.com or localhost / ip. 58 | this.set('ajs:test', true); 59 | if (!this.get('ajs:test')) { 60 | debug('fallback to domain=null'); 61 | this._options.domain = null; 62 | } 63 | this.remove('ajs:test'); 64 | }; 65 | 66 | 67 | /** 68 | * Set a `key` and `value` in our cookie. 69 | * 70 | * @param {String} key 71 | * @param {Object} value 72 | * @return {Boolean} saved 73 | */ 74 | 75 | Cookie.prototype.set = function(key, value) { 76 | try { 77 | value = json.stringify(value); 78 | cookie(key, value, clone(this._options)); 79 | return true; 80 | } catch (e) { 81 | return false; 82 | } 83 | }; 84 | 85 | 86 | /** 87 | * Get a value from our cookie by `key`. 88 | * 89 | * @param {String} key 90 | * @return {Object} value 91 | */ 92 | 93 | Cookie.prototype.get = function(key) { 94 | try { 95 | var value = cookie(key); 96 | value = value ? json.parse(value) : null; 97 | return value; 98 | } catch (e) { 99 | return null; 100 | } 101 | }; 102 | 103 | 104 | /** 105 | * Remove a value from our cookie by `key`. 106 | * 107 | * @param {String} key 108 | * @return {Boolean} removed 109 | */ 110 | 111 | Cookie.prototype.remove = function(key) { 112 | try { 113 | cookie(key, null, clone(this._options)); 114 | return true; 115 | } catch (e) { 116 | return false; 117 | } 118 | }; 119 | 120 | 121 | /** 122 | * Expose the cookie singleton. 123 | */ 124 | 125 | module.exports = bindAll(new Cookie()); 126 | 127 | 128 | /** 129 | * Expose the `Cookie` constructor. 130 | */ 131 | 132 | module.exports.Cookie = Cookie; 133 | -------------------------------------------------------------------------------- /lib/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Module dependencies. 5 | */ 6 | 7 | var Entity = require('./entity'); 8 | var bindAll = require('bind-all'); 9 | var cookie = require('./cookie'); 10 | var debug = require('debug')('analytics:user'); 11 | var inherit = require('inherits'); 12 | var rawCookie = require('component-cookie'); 13 | var uuid = require('uuid'); 14 | 15 | /** 16 | * User defaults 17 | */ 18 | 19 | User.defaults = { 20 | persist: true, 21 | cookie: { 22 | key: 'ajs_user_id', 23 | oldKey: 'ajs_user' 24 | }, 25 | localStorage: { 26 | key: 'ajs_user_traits' 27 | } 28 | }; 29 | 30 | 31 | /** 32 | * Initialize a new `User` with `options`. 33 | * 34 | * @param {Object} options 35 | */ 36 | 37 | function User(options) { 38 | this.defaults = User.defaults; 39 | this.debug = debug; 40 | Entity.call(this, options); 41 | } 42 | 43 | 44 | /** 45 | * Inherit `Entity` 46 | */ 47 | 48 | inherit(User, Entity); 49 | 50 | /** 51 | * Set/get the user id. 52 | * 53 | * When the user id changes, the method will reset his anonymousId to a new one. 54 | * 55 | * // FIXME: What are the mixed types? 56 | * @param {string} id 57 | * @return {Mixed} 58 | * @example 59 | * // didn't change because the user didn't have previous id. 60 | * anonymousId = user.anonymousId(); 61 | * user.id('foo'); 62 | * assert.equal(anonymousId, user.anonymousId()); 63 | * 64 | * // didn't change because the user id changed to null. 65 | * anonymousId = user.anonymousId(); 66 | * user.id('foo'); 67 | * user.id(null); 68 | * assert.equal(anonymousId, user.anonymousId()); 69 | * 70 | * // change because the user had previous id. 71 | * anonymousId = user.anonymousId(); 72 | * user.id('foo'); 73 | * user.id('baz'); // triggers change 74 | * user.id('baz'); // no change 75 | * assert.notEqual(anonymousId, user.anonymousId()); 76 | */ 77 | 78 | User.prototype.id = function(id) { 79 | var prev = this._getId(); 80 | var ret = Entity.prototype.id.apply(this, arguments); 81 | if (prev == null) return ret; 82 | // FIXME: We're relying on coercion here (1 == "1"), but our API treats these 83 | // two values differently. Figure out what will break if we remove this and 84 | // change to strict equality 85 | /* eslint-disable eqeqeq */ 86 | if (prev != id && id) this.anonymousId(null); 87 | /* eslint-enable eqeqeq */ 88 | return ret; 89 | }; 90 | 91 | /** 92 | * Set / get / remove anonymousId. 93 | * 94 | * @param {String} anonymousId 95 | * @return {String|User} 96 | */ 97 | 98 | User.prototype.anonymousId = function(anonymousId) { 99 | var store = this.storage(); 100 | 101 | // set / remove 102 | if (arguments.length) { 103 | store.set('ajs_anonymous_id', anonymousId); 104 | return this; 105 | } 106 | 107 | // new 108 | anonymousId = store.get('ajs_anonymous_id'); 109 | if (anonymousId) { 110 | return anonymousId; 111 | } 112 | 113 | // old - it is not stringified so we use the raw cookie. 114 | anonymousId = rawCookie('_sio'); 115 | if (anonymousId) { 116 | anonymousId = anonymousId.split('----')[0]; 117 | store.set('ajs_anonymous_id', anonymousId); 118 | store.remove('_sio'); 119 | return anonymousId; 120 | } 121 | 122 | // empty 123 | anonymousId = uuid.v4(); 124 | store.set('ajs_anonymous_id', anonymousId); 125 | return store.get('ajs_anonymous_id'); 126 | }; 127 | 128 | /** 129 | * Remove anonymous id on logout too. 130 | */ 131 | 132 | User.prototype.logout = function() { 133 | Entity.prototype.logout.call(this); 134 | this.anonymousId(null); 135 | }; 136 | 137 | /** 138 | * Load saved user `id` or `traits` from storage. 139 | */ 140 | 141 | User.prototype.load = function() { 142 | if (this._loadOldCookie()) return; 143 | Entity.prototype.load.call(this); 144 | }; 145 | 146 | 147 | /** 148 | * BACKWARDS COMPATIBILITY: Load the old user from the cookie. 149 | * 150 | * @api private 151 | * @return {boolean} 152 | */ 153 | 154 | User.prototype._loadOldCookie = function() { 155 | var user = cookie.get(this._options.cookie.oldKey); 156 | if (!user) return false; 157 | 158 | this.id(user.id); 159 | this.traits(user.traits); 160 | cookie.remove(this._options.cookie.oldKey); 161 | return true; 162 | }; 163 | 164 | 165 | /** 166 | * Expose the user singleton. 167 | */ 168 | 169 | module.exports = bindAll(new User()); 170 | 171 | 172 | /** 173 | * Expose the `User` constructor. 174 | */ 175 | 176 | module.exports.User = User; 177 | -------------------------------------------------------------------------------- /lib/entity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Module dependencies. 5 | */ 6 | 7 | var clone = require('@ndhoule/clone'); 8 | var cookie = require('./cookie'); 9 | var debug = require('debug')('analytics:entity'); 10 | var defaults = require('@ndhoule/defaults'); 11 | var extend = require('@ndhoule/extend'); 12 | var memory = require('./memory'); 13 | var store = require('./store'); 14 | var isodateTraverse = require('@segment/isodate-traverse'); 15 | 16 | /** 17 | * Expose `Entity` 18 | */ 19 | 20 | module.exports = Entity; 21 | 22 | 23 | /** 24 | * Initialize new `Entity` with `options`. 25 | * 26 | * @param {Object} options 27 | */ 28 | 29 | function Entity(options) { 30 | this.options(options); 31 | this.initialize(); 32 | } 33 | 34 | /** 35 | * Initialize picks the storage. 36 | * 37 | * Checks to see if cookies can be set 38 | * otherwise fallsback to localStorage. 39 | */ 40 | 41 | Entity.prototype.initialize = function() { 42 | cookie.set('ajs:cookies', true); 43 | 44 | // cookies are enabled. 45 | if (cookie.get('ajs:cookies')) { 46 | cookie.remove('ajs:cookies'); 47 | this._storage = cookie; 48 | return; 49 | } 50 | 51 | // localStorage is enabled. 52 | if (store.enabled) { 53 | this._storage = store; 54 | return; 55 | } 56 | 57 | // fallback to memory storage. 58 | debug('warning using memory store both cookies and localStorage are disabled'); 59 | this._storage = memory; 60 | }; 61 | 62 | /** 63 | * Get the storage. 64 | */ 65 | 66 | Entity.prototype.storage = function() { 67 | return this._storage; 68 | }; 69 | 70 | 71 | /** 72 | * Get or set storage `options`. 73 | * 74 | * @param {Object} options 75 | * @property {Object} cookie 76 | * @property {Object} localStorage 77 | * @property {Boolean} persist (default: `true`) 78 | */ 79 | 80 | Entity.prototype.options = function(options) { 81 | if (arguments.length === 0) return this._options; 82 | this._options = defaults(options || {}, this.defaults || {}); 83 | }; 84 | 85 | 86 | /** 87 | * Get or set the entity's `id`. 88 | * 89 | * @param {String} id 90 | */ 91 | 92 | Entity.prototype.id = function(id) { 93 | switch (arguments.length) { 94 | case 0: return this._getId(); 95 | case 1: return this._setId(id); 96 | default: 97 | // No default case 98 | } 99 | }; 100 | 101 | 102 | /** 103 | * Get the entity's id. 104 | * 105 | * @return {String} 106 | */ 107 | 108 | Entity.prototype._getId = function() { 109 | var ret = this._options.persist 110 | ? this.storage().get(this._options.cookie.key) 111 | : this._id; 112 | return ret === undefined ? null : ret; 113 | }; 114 | 115 | 116 | /** 117 | * Set the entity's `id`. 118 | * 119 | * @param {String} id 120 | */ 121 | 122 | Entity.prototype._setId = function(id) { 123 | if (this._options.persist) { 124 | this.storage().set(this._options.cookie.key, id); 125 | } else { 126 | this._id = id; 127 | } 128 | }; 129 | 130 | 131 | /** 132 | * Get or set the entity's `traits`. 133 | * 134 | * BACKWARDS COMPATIBILITY: aliased to `properties` 135 | * 136 | * @param {Object} traits 137 | */ 138 | 139 | Entity.prototype.properties = Entity.prototype.traits = function(traits) { 140 | switch (arguments.length) { 141 | case 0: return this._getTraits(); 142 | case 1: return this._setTraits(traits); 143 | default: 144 | // No default case 145 | } 146 | }; 147 | 148 | 149 | /** 150 | * Get the entity's traits. Always convert ISO date strings into real dates, 151 | * since they aren't parsed back from local storage. 152 | * 153 | * @return {Object} 154 | */ 155 | 156 | Entity.prototype._getTraits = function() { 157 | var ret = this._options.persist ? store.get(this._options.localStorage.key) : this._traits; 158 | return ret ? isodateTraverse(clone(ret)) : {}; 159 | }; 160 | 161 | 162 | /** 163 | * Set the entity's `traits`. 164 | * 165 | * @param {Object} traits 166 | */ 167 | 168 | Entity.prototype._setTraits = function(traits) { 169 | traits = traits || {}; 170 | if (this._options.persist) { 171 | store.set(this._options.localStorage.key, traits); 172 | } else { 173 | this._traits = traits; 174 | } 175 | }; 176 | 177 | 178 | /** 179 | * Identify the entity with an `id` and `traits`. If we it's the same entity, 180 | * extend the existing `traits` instead of overwriting. 181 | * 182 | * @param {String} id 183 | * @param {Object} traits 184 | */ 185 | 186 | Entity.prototype.identify = function(id, traits) { 187 | traits = traits || {}; 188 | var current = this.id(); 189 | if (current === null || current === id) traits = extend(this.traits(), traits); 190 | if (id) this.id(id); 191 | this.debug('identify %o, %o', id, traits); 192 | this.traits(traits); 193 | this.save(); 194 | }; 195 | 196 | 197 | /** 198 | * Save the entity to local storage and the cookie. 199 | * 200 | * @return {Boolean} 201 | */ 202 | 203 | Entity.prototype.save = function() { 204 | if (!this._options.persist) return false; 205 | cookie.set(this._options.cookie.key, this.id()); 206 | store.set(this._options.localStorage.key, this.traits()); 207 | return true; 208 | }; 209 | 210 | 211 | /** 212 | * Log the entity out, reseting `id` and `traits` to defaults. 213 | */ 214 | 215 | Entity.prototype.logout = function() { 216 | this.id(null); 217 | this.traits({}); 218 | cookie.remove(this._options.cookie.key); 219 | store.remove(this._options.localStorage.key); 220 | }; 221 | 222 | 223 | /** 224 | * Reset all entity state, logging out and returning options to defaults. 225 | */ 226 | 227 | Entity.prototype.reset = function() { 228 | this.logout(); 229 | this.options({}); 230 | }; 231 | 232 | 233 | /** 234 | * Load saved entity `id` or `traits` from storage. 235 | */ 236 | 237 | Entity.prototype.load = function() { 238 | this.id(cookie.get(this._options.cookie.key)); 239 | this.traits(store.get(this._options.localStorage.key)); 240 | }; 241 | 242 | -------------------------------------------------------------------------------- /test/normalize.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('proclaim'); 4 | var normalize = require('../lib/normalize'); 5 | 6 | describe('normalize', function() { 7 | var list = ['Segment', 'KISSmetrics']; 8 | var opts; 9 | var msg; 10 | 11 | beforeEach(function() { 12 | msg = {}; 13 | opts = msg.options = {}; 14 | }); 15 | 16 | describe('message', function() { 17 | it('should merge original with normalized', function() { 18 | msg.userId = 'user-id'; 19 | opts.integrations = { Segment: true }; 20 | assert.deepEqual(normalize(msg, list), { 21 | integrations: { Segment: true }, 22 | userId: 'user-id', 23 | context: {} 24 | }); 25 | }); 26 | }); 27 | 28 | describe('options', function() { 29 | it('should move all toplevel keys to the message', function() { 30 | var date = opts.timestamp = new Date(); 31 | opts.anonymousId = 'anonymous-id'; 32 | opts.integrations = { foo: 1 }; 33 | opts.context = { context: 1 }; 34 | 35 | var out = normalize(msg, list); 36 | assert(out.timestamp.getTime() === date.getTime()); 37 | assert(out.anonymousId === 'anonymous-id'); 38 | assert.deepEqual(out.integrations, { foo: 1 }); 39 | assert.deepEqual(out.context, { context: 1 }); 40 | }); 41 | 42 | it('should move all other keys to context', function() { 43 | opts.context = { foo: 1 }; 44 | opts.campaign = { name: 'campaign-name' }; 45 | opts.library = 'analytics-wordpress'; 46 | opts.traits = { trait: true }; 47 | assert.deepEqual(normalize(msg, list), { 48 | integrations: {}, 49 | context: { 50 | campaign: { name: 'campaign-name' }, 51 | library: 'analytics-wordpress', 52 | traits: { trait: true }, 53 | foo: 1 54 | } 55 | }); 56 | }); 57 | }); 58 | 59 | describe('integrations', function() { 60 | describe('as options', function() { 61 | it('should move to .integrations', function() { 62 | opts.Segment = true; 63 | opts.KISSmetrics = false; 64 | assert.deepEqual(normalize(msg, list), { 65 | context: {}, 66 | integrations: { 67 | Segment: true, 68 | KISSmetrics: false 69 | } 70 | }); 71 | }); 72 | 73 | it('should match integration names', function() { 74 | opts.segment = true; 75 | opts.KissMetrics = false; 76 | assert.deepEqual(normalize(msg, list), { 77 | context: {}, 78 | integrations: { 79 | segment: true, 80 | KissMetrics: false 81 | } 82 | }); 83 | }); 84 | 85 | it('should move .All', function() { 86 | opts.All = true; 87 | assert.deepEqual(normalize(msg, list), { 88 | context: {}, 89 | integrations: { 90 | All: true 91 | } 92 | }); 93 | }); 94 | 95 | it('should move .all', function() { 96 | opts.all = true; 97 | assert.deepEqual(normalize(msg, list), { 98 | context: {}, 99 | integrations: { 100 | all: true 101 | } 102 | }); 103 | }); 104 | 105 | it('should not clobber', function() { 106 | opts.all = false; 107 | opts.Segment = {}; 108 | opts.integrations = {}; 109 | opts.integrations.all = true; 110 | opts.integrations.Segment = true; 111 | assert.deepEqual(normalize(msg, list), { 112 | context: {}, 113 | integrations: { 114 | all: true, 115 | Segment: true 116 | } 117 | }); 118 | }); 119 | }); 120 | 121 | describe('as providers', function() { 122 | var providers; 123 | 124 | beforeEach(function() { 125 | opts.providers = providers = {}; 126 | }); 127 | 128 | it('should move to .integrations', function() { 129 | providers.Segment = true; 130 | providers.KISSmetrics = false; 131 | assert.deepEqual(normalize(msg, list), { 132 | context: {}, 133 | integrations: { 134 | Segment: true, 135 | KISSmetrics: false 136 | } 137 | }); 138 | }); 139 | 140 | it('should match integration names', function() { 141 | providers.segment = true; 142 | providers.KissMetrics = false; 143 | assert.deepEqual(normalize(msg, list), { 144 | context: {}, 145 | integrations: { 146 | segment: true, 147 | KissMetrics: false 148 | } 149 | }); 150 | }); 151 | 152 | it('should move .All', function() { 153 | providers.All = true; 154 | assert.deepEqual(normalize(msg, list), { 155 | context: {}, 156 | integrations: { 157 | All: true 158 | } 159 | }); 160 | }); 161 | 162 | it('should move .all', function() { 163 | providers.all = true; 164 | assert.deepEqual(normalize(msg, list), { 165 | context: {}, 166 | integrations: { 167 | all: true 168 | } 169 | }); 170 | }); 171 | 172 | it('should not clobber booleans', function() { 173 | providers.all = false; 174 | providers.Segment = false; 175 | opts.integrations = {}; 176 | opts.integrations.all = true; 177 | opts.integrations.Segment = true; 178 | assert.deepEqual(normalize(msg, list), { 179 | context: {}, 180 | integrations: { 181 | all: true, 182 | Segment: true 183 | } 184 | }); 185 | }); 186 | 187 | it('should override if providers[key] is an object', function() { 188 | providers.Segment = {}; 189 | opts.integrations = { Segment: true }; 190 | assert.deepEqual(normalize(msg, list), { 191 | context: {}, 192 | integrations: { 193 | Segment: {} 194 | } 195 | }); 196 | }); 197 | }); 198 | 199 | describe('as providers and options', function() { 200 | var providers; 201 | 202 | beforeEach(function() { 203 | opts.providers = providers = {}; 204 | }); 205 | 206 | it('should move to .integrations', function() { 207 | providers.Segment = true; 208 | opts.KISSmetrics = false; 209 | assert.deepEqual(normalize(msg, list), { 210 | context: {}, 211 | integrations: { 212 | Segment: true, 213 | KISSmetrics: false 214 | } 215 | }); 216 | }); 217 | 218 | it('should prefer options object', function() { 219 | providers.Segment = { option: true }; 220 | opts.Segment = true; 221 | assert.deepEqual(normalize(msg, list), { 222 | context: {}, 223 | integrations: { 224 | Segment: { option: true } 225 | } 226 | }); 227 | }); 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /test/group.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Analytics = require('../lib').constructor; 4 | var analytics = require('../lib'); 5 | var assert = require('proclaim'); 6 | var sinon = require('sinon'); 7 | 8 | var cookie = Analytics.cookie; 9 | var group = analytics.group(); 10 | var Group = group.Group; 11 | var memory = Analytics.memory; 12 | var store = Analytics.store; 13 | 14 | describe('group', function() { 15 | var cookieKey = group._options.cookie.key; 16 | var localStorageKey = group._options.localStorage.key; 17 | 18 | beforeEach(function() { 19 | group = new Group(); 20 | group.reset(); 21 | }); 22 | 23 | afterEach(function() { 24 | group.reset(); 25 | cookie.remove(cookieKey); 26 | store.remove(cookieKey); 27 | store.remove(localStorageKey); 28 | group.protocol = location.protocol; 29 | }); 30 | 31 | describe('()', function() { 32 | beforeEach(function() { 33 | cookie.set(cookieKey, 'gid'); 34 | store.set(localStorageKey, { trait: true }); 35 | }); 36 | 37 | it('should not reset group id and traits', function() { 38 | var group = new Group(); 39 | assert(group.id() === 'gid'); 40 | assert(group.traits().trait === true); 41 | }); 42 | }); 43 | 44 | describe('#id', function() { 45 | describe('when cookies are disabled', function() { 46 | beforeEach(function() { 47 | sinon.stub(cookie, 'get', function() {}); 48 | group = new Group(); 49 | }); 50 | 51 | afterEach(function() { 52 | cookie.get.restore(); 53 | }); 54 | 55 | it('should get an id from store', function() { 56 | store.set(cookieKey, 'id'); 57 | assert(group.id() === 'id'); 58 | }); 59 | 60 | it('should get an id when not persisting', function() { 61 | group.options({ persist: false }); 62 | group._id = 'id'; 63 | assert(group.id() === 'id'); 64 | }); 65 | 66 | it('should set an id to the store', function() { 67 | group.id('id'); 68 | assert(store.get(cookieKey) === 'id'); 69 | }); 70 | 71 | it('should set the id when not persisting', function() { 72 | group.options({ persist: false }); 73 | group.id('id'); 74 | assert(group._id === 'id'); 75 | }); 76 | 77 | it('should be null by default', function() { 78 | assert(group.id() === null); 79 | }); 80 | }); 81 | 82 | describe('when cookies and localStorage are disabled', function() { 83 | beforeEach(function() { 84 | sinon.stub(cookie, 'get', function() {}); 85 | store.enabled = false; 86 | group = new Group(); 87 | }); 88 | 89 | afterEach(function() { 90 | store.enabled = true; 91 | cookie.get.restore(); 92 | }); 93 | 94 | it('should get an id from the store', function() { 95 | memory.set(cookieKey, 'id'); 96 | assert(group.id() === 'id'); 97 | }); 98 | 99 | it('should get an id when not persisting', function() { 100 | group.options({ persist: false }); 101 | group._id = 'id'; 102 | assert(group.id() === 'id'); 103 | }); 104 | 105 | it('should set an id to the store', function() { 106 | group.id('id'); 107 | assert(memory.get(cookieKey) === 'id'); 108 | }); 109 | 110 | it('should set the id when not persisting', function() { 111 | group.options({ persist: false }); 112 | group.id('id'); 113 | assert(group._id === 'id'); 114 | }); 115 | 116 | it('should be null by default', function() { 117 | assert(group.id() === null); 118 | }); 119 | }); 120 | 121 | describe('when cookies are enabled', function() { 122 | it('should get an id from the cookie', function() { 123 | cookie.set(cookieKey, 'id'); 124 | 125 | assert(group.id() === 'id'); 126 | }); 127 | 128 | it('should get an id when not persisting', function() { 129 | group.options({ persist: false }); 130 | group._id = 'id'; 131 | assert(group.id() === 'id'); 132 | }); 133 | 134 | it('should set an id to the cookie', function() { 135 | group.id('id'); 136 | assert(cookie.get(cookieKey) === 'id'); 137 | }); 138 | 139 | it('should set the id when not persisting', function() { 140 | group.options({ persist: false }); 141 | group.id('id'); 142 | assert(group._id === 'id'); 143 | }); 144 | 145 | it('should be null by default', function() { 146 | assert(group.id() === null); 147 | }); 148 | }); 149 | }); 150 | 151 | describe('#properties', function() { 152 | it('should get properties', function() { 153 | store.set(localStorageKey, { property: true }); 154 | assert.deepEqual(group.properties(), { property: true }); 155 | }); 156 | 157 | it('should get a copy of properties', function() { 158 | store.set(localStorageKey, { property: true }); 159 | assert(group._traits !== group.properties()); 160 | }); 161 | 162 | it('should get properties when not persisting', function() { 163 | group.options({ persist: false }); 164 | group._traits = { property: true }; 165 | assert.deepEqual(group.properties(), { property: true }); 166 | }); 167 | 168 | it('should get a copy of properties when not persisting', function() { 169 | group.options({ persist: false }); 170 | group._traits = { property: true }; 171 | assert(group._traits !== group.properties()); 172 | }); 173 | 174 | it('should set properties', function() { 175 | group.properties({ property: true }); 176 | assert.deepEqual(store.get(localStorageKey), { property: true }); 177 | }); 178 | 179 | it('should set the id when not persisting', function() { 180 | group.options({ persist: false }); 181 | group.properties({ property: true }); 182 | assert.deepEqual(group._traits, { property: true }); 183 | }); 184 | 185 | it('should default properties to an empty object', function() { 186 | group.properties(null); 187 | assert.deepEqual(store.get(localStorageKey), {}); 188 | }); 189 | 190 | it('should default properties to an empty object when not persisting', function() { 191 | group.options({ persist: false }); 192 | group.properties(null); 193 | assert.deepEqual(group._traits, {}); 194 | }); 195 | 196 | it('should be an empty object by default', function() { 197 | assert.deepEqual(group.properties(), {}); 198 | }); 199 | }); 200 | 201 | describe('#options', function() { 202 | it('should get options', function() { 203 | var options = group.options(); 204 | assert(options === group._options); 205 | }); 206 | 207 | it('should set options with defaults', function() { 208 | group.options({ option: true }); 209 | assert.deepEqual(group._options, { 210 | option: true, 211 | persist: true, 212 | cookie: { 213 | key: 'ajs_group_id' 214 | }, 215 | localStorage: { 216 | key: 'ajs_group_properties' 217 | } 218 | }); 219 | }); 220 | }); 221 | 222 | describe('#save', function() { 223 | it('should save an id to a cookie', function() { 224 | group.id('id'); 225 | group.save(); 226 | assert(cookie.get(cookieKey) === 'id'); 227 | }); 228 | 229 | it('should save properties to local storage', function() { 230 | group.properties({ property: true }); 231 | group.save(); 232 | assert.deepEqual(store.get(localStorageKey), { property: true }); 233 | }); 234 | 235 | it('shouldnt save if persist is false', function() { 236 | group.options({ persist: false }); 237 | group.id('id'); 238 | group.save(); 239 | assert(cookie.get(cookieKey) === null); 240 | }); 241 | }); 242 | 243 | describe('#logout', function() { 244 | it('should reset an id and properties', function() { 245 | group.id('id'); 246 | group.properties({ property: true }); 247 | group.logout(); 248 | assert(group.id() === null); 249 | assert.deepEqual(group.properties(), {}); 250 | }); 251 | 252 | it('should clear a cookie', function() { 253 | group.id('id'); 254 | group.save(); 255 | group.logout(); 256 | assert(cookie.get(cookieKey) === null); 257 | }); 258 | 259 | it('should clear local storage', function() { 260 | group.properties({ property: true }); 261 | group.save(); 262 | group.logout(); 263 | assert(store.get(localStorageKey) === undefined); 264 | }); 265 | }); 266 | 267 | describe('#identify', function() { 268 | it('should save an id', function() { 269 | group.identify('id'); 270 | assert(group.id() === 'id'); 271 | assert(cookie.get(cookieKey) === 'id'); 272 | }); 273 | 274 | it('should save properties', function() { 275 | group.identify(null, { property: true }); 276 | assert(group.properties(), { property: true }); 277 | assert(store.get(localStorageKey), { property: true }); 278 | }); 279 | 280 | it('should save an id and properties', function() { 281 | group.identify('id', { property: true }); 282 | assert(group.id() === 'id'); 283 | assert.deepEqual(group.properties(), { property: true }); 284 | assert(cookie.get(cookieKey) === 'id'); 285 | assert.deepEqual(store.get(localStorageKey), { property: true }); 286 | }); 287 | 288 | it('should extend existing properties', function() { 289 | group.properties({ one: 1 }); 290 | group.identify('id', { two: 2 }); 291 | assert.deepEqual(group.properties(), { one: 1, two: 2 }); 292 | assert.deepEqual(store.get(localStorageKey), { one: 1, two: 2 }); 293 | }); 294 | 295 | it('shouldnt extend existing properties for a new id', function() { 296 | group.id('id'); 297 | group.properties({ one: 1 }); 298 | group.identify('new', { two: 2 }); 299 | assert.deepEqual(group.properties(), { two: 2 }); 300 | assert.deepEqual(store.get(localStorageKey), { two: 2 }); 301 | }); 302 | 303 | it('should reset properties for a new id', function() { 304 | group.id('id'); 305 | group.properties({ one: 1 }); 306 | group.identify('new'); 307 | assert.deepEqual(group.properties(), {}); 308 | assert.deepEqual(store.get(localStorageKey), {}); 309 | }); 310 | }); 311 | 312 | describe('#load', function() { 313 | it('should load an empty group', function() { 314 | group.load(); 315 | assert(group.id() === null); 316 | assert.deepEqual(group.properties(), {}); 317 | }); 318 | 319 | it('should load an id from a cookie', function() { 320 | cookie.set(cookieKey, 'id'); 321 | group.load(); 322 | assert(group.id() === 'id'); 323 | }); 324 | 325 | it('should load properties from local storage', function() { 326 | store.set(localStorageKey, { property: true }); 327 | group.load(); 328 | assert.deepEqual(group.properties(), { property: true }); 329 | }); 330 | }); 331 | }); 332 | -------------------------------------------------------------------------------- /test/user.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('proclaim'); 4 | var rawCookie = require('component-cookie'); 5 | var sinon = require('sinon'); 6 | var analytics = require('../lib'); 7 | var Analytics = require('../lib').constructor; 8 | 9 | var cookie = Analytics.cookie; 10 | var store = Analytics.store; 11 | var memory = Analytics.memory; 12 | var user = analytics.user(); 13 | var User = user.User; 14 | 15 | describe('user', function() { 16 | var cookieKey = user._options.cookie.key; 17 | var localStorageKey = user._options.localStorage.key; 18 | 19 | beforeEach(function() { 20 | user = new User(); 21 | user.reset(); 22 | }); 23 | 24 | afterEach(function() { 25 | user.reset(); 26 | cookie.remove(cookieKey); 27 | store.remove(cookieKey); 28 | store.remove(localStorageKey); 29 | store.remove('_sio'); 30 | cookie.remove('_sio'); 31 | rawCookie('_sio', null); 32 | }); 33 | 34 | describe('()', function() { 35 | beforeEach(function() { 36 | cookie.set(cookieKey, 'my id'); 37 | store.set(localStorageKey, { trait: true }); 38 | }); 39 | 40 | it('should not reset user id and traits', function() { 41 | var user = new User(); 42 | assert(user.id() === 'my id'); 43 | assert(user.traits().trait === true); 44 | }); 45 | 46 | it('should pick the old "_sio" anonymousId', function() { 47 | rawCookie('_sio', 'anonymous-id----user-id'); 48 | var user = new User(); 49 | assert(user.anonymousId() === 'anonymous-id'); 50 | }); 51 | 52 | it('should not pick the old "_sio" if anonymous id is present', function() { 53 | rawCookie('_sio', 'old-anonymous-id----user-id'); 54 | cookie.set('ajs_anonymous_id', 'new-anonymous-id'); 55 | assert(new User().anonymousId() === 'new-anonymous-id'); 56 | }); 57 | 58 | it('should create anonymous id if missing', function() { 59 | var user = new User(); 60 | assert(user.anonymousId().length === 36); 61 | }); 62 | 63 | it('should not overwrite anonymous id', function() { 64 | cookie.set('ajs_anonymous_id', 'anonymous'); 65 | assert(new User().anonymousId() === 'anonymous'); 66 | }); 67 | }); 68 | 69 | describe('#id', function() { 70 | describe('when cookies are disabled', function() { 71 | beforeEach(function() { 72 | sinon.stub(cookie, 'get', function() {}); 73 | user = new User(); 74 | }); 75 | 76 | afterEach(function() { 77 | cookie.get.restore(); 78 | }); 79 | 80 | it('should get an id from the store', function() { 81 | store.set(cookieKey, 'id'); 82 | assert(user.id() === 'id'); 83 | }); 84 | 85 | it('should get an id when not persisting', function() { 86 | user.options({ persist: false }); 87 | user._id = 'id'; 88 | assert(user.id() === 'id'); 89 | }); 90 | 91 | it('should set an id to the store', function() { 92 | user.id('id'); 93 | assert(store.get(cookieKey) === 'id'); 94 | }); 95 | 96 | it('should set the id when not persisting', function() { 97 | user.options({ persist: false }); 98 | user.id('id'); 99 | assert(user._id === 'id'); 100 | }); 101 | 102 | it('should be null by default', function() { 103 | assert(user.id() === null); 104 | }); 105 | 106 | it('should not reset anonymousId if the user didnt have previous id', function() { 107 | var prev = user.anonymousId(); 108 | user.id('foo'); 109 | user.id('foo'); 110 | user.id('foo'); 111 | assert(user.anonymousId() === prev); 112 | }); 113 | 114 | it('should reset anonymousId if the user id changed', function() { 115 | var prev = user.anonymousId(); 116 | user.id('foo'); 117 | user.id('baz'); 118 | assert(user.anonymousId() !== prev); 119 | assert(user.anonymousId().length === 36); 120 | }); 121 | 122 | it('should not reset anonymousId if the user id changed to null', function() { 123 | var prev = user.anonymousId(); 124 | user.id('foo'); 125 | user.id(null); 126 | assert(user.anonymousId() === prev); 127 | assert(user.anonymousId().length === 36); 128 | }); 129 | }); 130 | 131 | describe('when cookies and localStorage are disabled', function() { 132 | beforeEach(function() { 133 | sinon.stub(cookie, 'get', function() {}); 134 | store.enabled = false; 135 | user = new User(); 136 | }); 137 | 138 | afterEach(function() { 139 | store.enabled = true; 140 | cookie.get.restore(); 141 | }); 142 | 143 | it('should get an id from the memory', function() { 144 | memory.set(cookieKey, 'id'); 145 | assert(user.id() === 'id'); 146 | }); 147 | 148 | it('should get an id when not persisting', function() { 149 | user.options({ persist: false }); 150 | user._id = 'id'; 151 | assert(user.id() === 'id'); 152 | }); 153 | 154 | it('should set an id to the memory', function() { 155 | user.id('id'); 156 | assert(memory.get(cookieKey) === 'id'); 157 | }); 158 | 159 | it('should set the id when not persisting', function() { 160 | user.options({ persist: false }); 161 | user.id('id'); 162 | assert(user._id === 'id'); 163 | }); 164 | 165 | it('should be null by default', function() { 166 | assert(user.id() === null); 167 | }); 168 | 169 | it('should not reset anonymousId if the user didnt have previous id', function() { 170 | var prev = user.anonymousId(); 171 | user.id('foo'); 172 | user.id('foo'); 173 | user.id('foo'); 174 | assert(user.anonymousId() === prev); 175 | }); 176 | 177 | it('should reset anonymousId if the user id changed', function() { 178 | var prev = user.anonymousId(); 179 | user.id('foo'); 180 | user.id('baz'); 181 | assert(user.anonymousId() !== prev); 182 | assert(user.anonymousId().length === 36); 183 | }); 184 | 185 | it('should not reset anonymousId if the user id changed to null', function() { 186 | var prev = user.anonymousId(); 187 | user.id('foo'); 188 | user.id(null); 189 | assert(user.anonymousId() === prev); 190 | assert(user.anonymousId().length === 36); 191 | }); 192 | }); 193 | 194 | describe('when cookies are enabled', function() { 195 | it('should get an id from the cookie', function() { 196 | cookie.set(cookieKey, 'id'); 197 | assert(user.id() === 'id'); 198 | }); 199 | 200 | it('should get an id when not persisting', function() { 201 | user.options({ persist: false }); 202 | user._id = 'id'; 203 | assert(user.id() === 'id'); 204 | }); 205 | 206 | it('should set an id to the cookie', function() { 207 | user.id('id'); 208 | assert(cookie.get(cookieKey) === 'id'); 209 | }); 210 | 211 | it('should set the id when not persisting', function() { 212 | user.options({ persist: false }); 213 | user.id('id'); 214 | assert(user._id === 'id'); 215 | }); 216 | 217 | it('should be null by default', function() { 218 | assert(user.id() === null); 219 | }); 220 | 221 | it('should not reset anonymousId if the user didnt have previous id', function() { 222 | var prev = user.anonymousId(); 223 | user.id('foo'); 224 | user.id('foo'); 225 | user.id('foo'); 226 | assert(user.anonymousId() === prev); 227 | }); 228 | 229 | it('should reset anonymousId if the user id changed', function() { 230 | var prev = user.anonymousId(); 231 | user.id('foo'); 232 | user.id('baz'); 233 | assert(user.anonymousId() !== prev); 234 | assert(user.anonymousId().length === 36); 235 | }); 236 | }); 237 | }); 238 | 239 | describe('#anonymousId', function() { 240 | var noop = { set: function() {}, get: function() {} }; 241 | var storage = user.storage; 242 | 243 | afterEach(function() { 244 | user.storage = storage; 245 | }); 246 | 247 | describe('when cookies are disabled', function() { 248 | beforeEach(function() { 249 | sinon.stub(cookie, 'get', function() {}); 250 | user = new User(); 251 | }); 252 | 253 | afterEach(function() { 254 | cookie.get.restore(); 255 | }); 256 | 257 | it('should get an id from the store', function() { 258 | store.set('ajs_anonymous_id', 'anon-id'); 259 | assert(user.anonymousId() === 'anon-id'); 260 | }); 261 | 262 | it('should set an id to the store', function() { 263 | user.anonymousId('anon-id'); 264 | assert(store.get('ajs_anonymous_id') === 'anon-id'); 265 | }); 266 | 267 | it('should return anonymousId using the store', function() { 268 | user.storage = function() { return noop; }; 269 | assert(user.anonymousId() === undefined); 270 | }); 271 | }); 272 | 273 | describe('when cookies and localStorage are disabled', function() { 274 | beforeEach(function() { 275 | sinon.stub(cookie, 'get', function() {}); 276 | store.enabled = false; 277 | user = new User(); 278 | }); 279 | 280 | afterEach(function() { 281 | store.enabled = true; 282 | cookie.get.restore(); 283 | }); 284 | 285 | it('should get an id from the memory', function() { 286 | memory.set('ajs_anonymous_id', 'anon-id'); 287 | assert(user.anonymousId() === 'anon-id'); 288 | }); 289 | 290 | it('should set an id to the memory', function() { 291 | user.anonymousId('anon-id'); 292 | assert(memory.get('ajs_anonymous_id') === 'anon-id'); 293 | }); 294 | 295 | it('should return anonymousId using the store', function() { 296 | user.storage = function() { return noop; }; 297 | assert(user.anonymousId() === undefined); 298 | }); 299 | }); 300 | 301 | describe('when cookies are enabled', function() { 302 | it('should get an id from the cookie', function() { 303 | cookie.set('ajs_anonymous_id', 'anon-id'); 304 | assert(user.anonymousId() === 'anon-id'); 305 | }); 306 | 307 | it('should set an id to the cookie', function() { 308 | user.anonymousId('anon-id'); 309 | assert(cookie.get('ajs_anonymous_id') === 'anon-id'); 310 | }); 311 | 312 | it('should return anonymousId using the store', function() { 313 | user.storage = function() { return noop; }; 314 | assert(user.anonymousId() === undefined); 315 | }); 316 | }); 317 | }); 318 | 319 | describe('#traits', function() { 320 | it('should get traits', function() { 321 | store.set(localStorageKey, { trait: true }); 322 | assert.deepEqual(user.traits(), { trait: true }); 323 | }); 324 | 325 | it('should get a copy of traits', function() { 326 | store.set(localStorageKey, { trait: true }); 327 | assert(user.traits() !== user._traits); 328 | }); 329 | 330 | it('should get traits when not persisting', function() { 331 | user.options({ persist: false }); 332 | user._traits = { trait: true }; 333 | assert.deepEqual(user.traits(), { trait: true }); 334 | }); 335 | 336 | it('should get a copy of traits when not persisting', function() { 337 | user.options({ persist: false }); 338 | user._traits = { trait: true }; 339 | assert(user.traits() !== user._traits); 340 | }); 341 | 342 | it('should set traits', function() { 343 | user.traits({ trait: true }); 344 | assert(store.get(localStorageKey), { trait: true }); 345 | }); 346 | 347 | it('should set the id when not persisting', function() { 348 | user.options({ persist: false }); 349 | user.traits({ trait: true }); 350 | assert.deepEqual(user._traits, { trait: true }); 351 | }); 352 | 353 | it('should default traits to an empty object', function() { 354 | user.traits(null); 355 | assert.deepEqual(store.get(localStorageKey), {}); 356 | }); 357 | 358 | it('should default traits to an empty object when not persisting', function() { 359 | user.options({ persist: false }); 360 | user.traits(null); 361 | assert.deepEqual(user._traits, {}); 362 | }); 363 | 364 | it('should be an empty object by default', function() { 365 | assert.deepEqual(user.traits(), {}); 366 | }); 367 | }); 368 | 369 | describe('#options', function() { 370 | it('should get options', function() { 371 | assert(user.options() === user._options); 372 | }); 373 | 374 | it('should set options with defaults', function() { 375 | user.options({ option: true }); 376 | assert.deepEqual(user._options, { 377 | option: true, 378 | persist: true, 379 | cookie: { 380 | key: 'ajs_user_id', 381 | oldKey: 'ajs_user' 382 | }, 383 | localStorage: { 384 | key: 'ajs_user_traits' 385 | } 386 | }); 387 | }); 388 | }); 389 | 390 | describe('#save', function() { 391 | it('should save an id to a cookie', function() { 392 | user.id('id'); 393 | user.save(); 394 | assert(cookie.get(cookieKey) === 'id'); 395 | }); 396 | 397 | it('should save traits to local storage', function() { 398 | user.traits({ trait: true }); 399 | user.save(); 400 | assert(store.get(localStorageKey), { trait: true }); 401 | }); 402 | 403 | it('shouldnt save if persist is false', function() { 404 | user.options({ persist: false }); 405 | user.id('id'); 406 | user.save(); 407 | assert(cookie.get(cookieKey) === null); 408 | }); 409 | }); 410 | 411 | describe('#logout', function() { 412 | it('should reset an id and traits', function() { 413 | user.id('id'); 414 | user.anonymousId('anon-id'); 415 | user.traits({ trait: true }); 416 | user.logout(); 417 | assert(cookie.get('ajs_anonymous_id') === null); 418 | assert(user.id() === null); 419 | assert(user.traits(), {}); 420 | }); 421 | 422 | it('should clear a cookie', function() { 423 | user.id('id'); 424 | user.save(); 425 | user.logout(); 426 | assert(cookie.get(cookieKey) === null); 427 | }); 428 | 429 | it('should clear local storage', function() { 430 | user.traits({ trait: true }); 431 | user.save(); 432 | user.logout(); 433 | assert(store.get(localStorageKey) === undefined); 434 | }); 435 | }); 436 | 437 | describe('#identify', function() { 438 | it('should save an id', function() { 439 | user.identify('id'); 440 | assert(user.id() === 'id'); 441 | assert(cookie.get(cookieKey) === 'id'); 442 | }); 443 | 444 | it('should save traits', function() { 445 | user.identify(null, { trait: true }); 446 | assert.deepEqual(user.traits(), { trait: true }); 447 | assert.deepEqual(store.get(localStorageKey), { trait: true }); 448 | }); 449 | 450 | it('should save an id and traits', function() { 451 | user.identify('id', { trait: true }); 452 | assert(user.id() === 'id'); 453 | assert.deepEqual(user.traits(), { trait: true }); 454 | assert(cookie.get(cookieKey) === 'id'); 455 | assert.deepEqual(store.get(localStorageKey), { trait: true }); 456 | }); 457 | 458 | it('should extend existing traits', function() { 459 | user.traits({ one: 1 }); 460 | user.identify('id', { two: 2 }); 461 | assert.deepEqual(user.traits(), { one: 1, two: 2 }); 462 | assert.deepEqual(store.get(localStorageKey), { one: 1, two: 2 }); 463 | }); 464 | 465 | it('shouldnt extend existing traits for a new id', function() { 466 | user.id('id'); 467 | user.traits({ one: 1 }); 468 | user.identify('new', { two: 2 }); 469 | assert.deepEqual(user.traits(), { two: 2 }); 470 | assert.deepEqual(store.get(localStorageKey), { two: 2 }); 471 | }); 472 | 473 | it('should reset traits for a new id', function() { 474 | user.id('id'); 475 | user.traits({ one: 1 }); 476 | user.identify('new'); 477 | assert.deepEqual(user.traits(), {}); 478 | assert.deepEqual(store.get(localStorageKey), {}); 479 | }); 480 | }); 481 | 482 | describe('#load', function() { 483 | it('should load an empty user', function() { 484 | user.load(); 485 | assert(user.id() === null); 486 | assert.deepEqual(user.traits(), {}); 487 | }); 488 | 489 | it('should load an id from a cookie', function() { 490 | cookie.set(cookieKey, 'id'); 491 | user.load(); 492 | assert(user.id() === 'id'); 493 | }); 494 | 495 | it('should load traits from local storage', function() { 496 | store.set(localStorageKey, { trait: true }); 497 | user.load(); 498 | assert.deepEqual(user.traits(), { trait: true }); 499 | }); 500 | 501 | it('should load from an old cookie', function() { 502 | cookie.set(user._options.cookie.oldKey, { id: 'old', traits: { trait: true } }); 503 | user.load(); 504 | assert(user.id() === 'old'); 505 | assert.deepEqual(user.traits(), { trait: true }); 506 | }); 507 | }); 508 | }); 509 | -------------------------------------------------------------------------------- /lib/analytics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _analytics = global.analytics; 4 | 5 | /* 6 | * Module dependencies. 7 | */ 8 | 9 | var Alias = require('segmentio-facade').Alias; 10 | var Emitter = require('component-emitter'); 11 | var Group = require('segmentio-facade').Group; 12 | var Identify = require('segmentio-facade').Identify; 13 | var Page = require('segmentio-facade').Page; 14 | var Track = require('segmentio-facade').Track; 15 | var after = require('@ndhoule/after'); 16 | var bindAll = require('bind-all'); 17 | var clone = require('@ndhoule/clone'); 18 | var extend = require('extend'); 19 | var cookie = require('./cookie'); 20 | var debug = require('debug'); 21 | var defaults = require('@ndhoule/defaults'); 22 | var each = require('@ndhoule/each'); 23 | var foldl = require('@ndhoule/foldl'); 24 | var group = require('./group'); 25 | var is = require('is'); 26 | var isMeta = require('@segment/is-meta'); 27 | var keys = require('@ndhoule/keys'); 28 | var memory = require('./memory'); 29 | var nextTick = require('next-tick'); 30 | var normalize = require('./normalize'); 31 | var on = require('component-event').bind; 32 | var pageDefaults = require('./pageDefaults'); 33 | var pick = require('@ndhoule/pick'); 34 | var prevent = require('@segment/prevent-default'); 35 | var querystring = require('component-querystring'); 36 | var store = require('./store'); 37 | var user = require('./user'); 38 | var type = require('component-type'); 39 | 40 | /** 41 | * Initialize a new `Analytics` instance. 42 | */ 43 | 44 | function Analytics() { 45 | this._options({}); 46 | this.Integrations = {}; 47 | this._integrations = {}; 48 | this._readied = false; 49 | this._timeout = 300; 50 | // XXX: BACKWARDS COMPATIBILITY 51 | this._user = user; 52 | this.log = debug('analytics.js'); 53 | bindAll(this); 54 | 55 | var self = this; 56 | this.on('initialize', function(settings, options) { 57 | if (options.initialPageview) self.page(); 58 | self._parseQuery(window.location.search); 59 | }); 60 | } 61 | 62 | /** 63 | * Mix in event emitter. 64 | */ 65 | 66 | Emitter(Analytics.prototype); 67 | 68 | /** 69 | * Use a `plugin`. 70 | * 71 | * @param {Function} plugin 72 | * @return {Analytics} 73 | */ 74 | 75 | Analytics.prototype.use = function(plugin) { 76 | plugin(this); 77 | return this; 78 | }; 79 | 80 | /** 81 | * Define a new `Integration`. 82 | * 83 | * @param {Function} Integration 84 | * @return {Analytics} 85 | */ 86 | 87 | Analytics.prototype.addIntegration = function(Integration) { 88 | var name = Integration.prototype.name; 89 | if (!name) throw new TypeError('attempted to add an invalid integration'); 90 | this.Integrations[name] = Integration; 91 | return this; 92 | }; 93 | 94 | /** 95 | * Initialize with the given integration `settings` and `options`. 96 | * 97 | * Aliased to `init` for convenience. 98 | * 99 | * @param {Object} [settings={}] 100 | * @param {Object} [options={}] 101 | * @return {Analytics} 102 | */ 103 | 104 | Analytics.prototype.init = Analytics.prototype.initialize = function(settings, options) { 105 | settings = settings || {}; 106 | options = options || {}; 107 | 108 | this._options(options); 109 | this._readied = false; 110 | 111 | // clean unknown integrations from settings 112 | var self = this; 113 | each(function(opts, name) { 114 | var Integration = self.Integrations[name]; 115 | if (!Integration) delete settings[name]; 116 | }, settings); 117 | 118 | // add integrations 119 | each(function(opts, name) { 120 | var Integration = self.Integrations[name]; 121 | var clonedOpts = {}; 122 | extend(true, clonedOpts, opts); // deep clone opts 123 | var integration = new Integration(clonedOpts); 124 | self.log('initialize %o - %o', name, opts); 125 | self.add(integration); 126 | }, settings); 127 | 128 | var integrations = this._integrations; 129 | 130 | // load user now that options are set 131 | user.load(); 132 | group.load(); 133 | 134 | // make ready callback 135 | var integrationCount = keys(integrations).length; 136 | var ready = after(integrationCount, function() { 137 | self._readied = true; 138 | self.emit('ready'); 139 | }); 140 | 141 | // init if no integrations 142 | if (integrationCount <= 0) { 143 | ready(); 144 | } 145 | 146 | // initialize integrations, passing ready 147 | // create a list of any integrations that did not initialize - this will be passed with all events for replay support: 148 | this.failedInitializations = []; 149 | each(function(integration) { 150 | if (options.initialPageview && integration.options.initialPageview === false) { 151 | integration.page = after(2, integration.page); 152 | } 153 | 154 | integration.analytics = self; 155 | integration.once('ready', ready); 156 | try { 157 | integration.initialize(); 158 | } catch (e) { 159 | var integrationName = integration.name; 160 | self.failedInitializations.push(integrationName); 161 | self.log('Error initializing %s integration: %o', integrationName, e); 162 | // Mark integration as ready to prevent blocking of anyone listening to analytics.ready() 163 | integration.ready(); 164 | } 165 | }, integrations); 166 | 167 | // backwards compat with angular plugin. 168 | // TODO: remove 169 | this.initialized = true; 170 | 171 | this.emit('initialize', settings, options); 172 | return this; 173 | }; 174 | 175 | /** 176 | * Set the user's `id`. 177 | * 178 | * @param {Mixed} id 179 | */ 180 | 181 | Analytics.prototype.setAnonymousId = function(id) { 182 | this.user().anonymousId(id); 183 | return this; 184 | }; 185 | 186 | /** 187 | * Add an integration. 188 | * 189 | * @param {Integration} integration 190 | */ 191 | 192 | Analytics.prototype.add = function(integration) { 193 | this._integrations[integration.name] = integration; 194 | return this; 195 | }; 196 | 197 | /** 198 | * Identify a user by optional `id` and `traits`. 199 | * 200 | * @param {string} [id=user.id()] User ID. 201 | * @param {Object} [traits=null] User traits. 202 | * @param {Object} [options=null] 203 | * @param {Function} [fn] 204 | * @return {Analytics} 205 | */ 206 | 207 | Analytics.prototype.identify = function(id, traits, options, fn) { 208 | // Argument reshuffling. 209 | /* eslint-disable no-unused-expressions, no-sequences */ 210 | if (is.fn(options)) fn = options, options = null; 211 | if (is.fn(traits)) fn = traits, options = null, traits = null; 212 | if (is.object(id)) options = traits, traits = id, id = user.id(); 213 | /* eslint-enable no-unused-expressions, no-sequences */ 214 | 215 | // clone traits before we manipulate so we don't do anything uncouth, and take 216 | // from `user` so that we carryover anonymous traits 217 | user.identify(id, traits); 218 | 219 | var msg = this.normalize({ 220 | options: options, 221 | traits: user.traits(), 222 | userId: user.id() 223 | }); 224 | 225 | this._invoke('identify', new Identify(msg)); 226 | 227 | // emit 228 | this.emit('identify', id, traits, options); 229 | this._callback(fn); 230 | return this; 231 | }; 232 | 233 | /** 234 | * Return the current user. 235 | * 236 | * @return {Object} 237 | */ 238 | 239 | Analytics.prototype.user = function() { 240 | return user; 241 | }; 242 | 243 | /** 244 | * Identify a group by optional `id` and `traits`. Or, if no arguments are 245 | * supplied, return the current group. 246 | * 247 | * @param {string} [id=group.id()] Group ID. 248 | * @param {Object} [traits=null] Group traits. 249 | * @param {Object} [options=null] 250 | * @param {Function} [fn] 251 | * @return {Analytics|Object} 252 | */ 253 | 254 | Analytics.prototype.group = function(id, traits, options, fn) { 255 | /* eslint-disable no-unused-expressions, no-sequences */ 256 | if (!arguments.length) return group; 257 | if (is.fn(options)) fn = options, options = null; 258 | if (is.fn(traits)) fn = traits, options = null, traits = null; 259 | if (is.object(id)) options = traits, traits = id, id = group.id(); 260 | /* eslint-enable no-unused-expressions, no-sequences */ 261 | 262 | 263 | // grab from group again to make sure we're taking from the source 264 | group.identify(id, traits); 265 | 266 | var msg = this.normalize({ 267 | options: options, 268 | traits: group.traits(), 269 | groupId: group.id() 270 | }); 271 | 272 | this._invoke('group', new Group(msg)); 273 | 274 | this.emit('group', id, traits, options); 275 | this._callback(fn); 276 | return this; 277 | }; 278 | 279 | /** 280 | * Track an `event` that a user has triggered with optional `properties`. 281 | * 282 | * @param {string} event 283 | * @param {Object} [properties=null] 284 | * @param {Object} [options=null] 285 | * @param {Function} [fn] 286 | * @return {Analytics} 287 | */ 288 | 289 | Analytics.prototype.track = function(event, properties, options, fn) { 290 | // Argument reshuffling. 291 | /* eslint-disable no-unused-expressions, no-sequences */ 292 | if (is.fn(options)) fn = options, options = null; 293 | if (is.fn(properties)) fn = properties, options = null, properties = null; 294 | /* eslint-enable no-unused-expressions, no-sequences */ 295 | 296 | // figure out if the event is archived. 297 | var plan = this.options.plan || {}; 298 | var events = plan.track || {}; 299 | 300 | // normalize 301 | var msg = this.normalize({ 302 | properties: properties, 303 | options: options, 304 | event: event 305 | }); 306 | 307 | // plan. 308 | plan = events[event]; 309 | if (plan) { 310 | this.log('plan %o - %o', event, plan); 311 | if (plan.enabled === false) { 312 | // Disabled events should always be sent to Segment. 313 | defaults(msg.integrations, { All: false, 'Segment.io': true }); 314 | } else { 315 | defaults(msg.integrations, plan.integrations || {}); 316 | } 317 | } else { 318 | var defaultPlan = events.__default || { enabled: true }; 319 | if (!defaultPlan.enabled) { 320 | // Disabled events should always be sent to Segment. 321 | defaults(msg.integrations, { All: false, 'Segment.io': true }); 322 | } 323 | } 324 | 325 | this._invoke('track', new Track(msg)); 326 | 327 | this.emit('track', event, properties, options); 328 | this._callback(fn); 329 | return this; 330 | }; 331 | 332 | /** 333 | * Helper method to track an outbound link that would normally navigate away 334 | * from the page before the analytics calls were sent. 335 | * 336 | * BACKWARDS COMPATIBILITY: aliased to `trackClick`. 337 | * 338 | * @param {Element|Array} links 339 | * @param {string|Function} event 340 | * @param {Object|Function} properties (optional) 341 | * @return {Analytics} 342 | */ 343 | 344 | Analytics.prototype.trackClick = Analytics.prototype.trackLink = function(links, event, properties) { 345 | if (!links) return this; 346 | // always arrays, handles jquery 347 | if (type(links) === 'element') links = [links]; 348 | 349 | var self = this; 350 | each(function(el) { 351 | if (type(el) !== 'element') { 352 | throw new TypeError('Must pass HTMLElement to `analytics.trackLink`.'); 353 | } 354 | on(el, 'click', function(e) { 355 | var ev = is.fn(event) ? event(el) : event; 356 | var props = is.fn(properties) ? properties(el) : properties; 357 | var href = el.getAttribute('href') 358 | || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') 359 | || el.getAttribute('xlink:href'); 360 | 361 | self.track(ev, props); 362 | 363 | if (href && el.target !== '_blank' && !isMeta(e)) { 364 | prevent(e); 365 | self._callback(function() { 366 | window.location.href = href; 367 | }); 368 | } 369 | }); 370 | }, links); 371 | 372 | return this; 373 | }; 374 | 375 | /** 376 | * Helper method to track an outbound form that would normally navigate away 377 | * from the page before the analytics calls were sent. 378 | * 379 | * BACKWARDS COMPATIBILITY: aliased to `trackSubmit`. 380 | * 381 | * @param {Element|Array} forms 382 | * @param {string|Function} event 383 | * @param {Object|Function} properties (optional) 384 | * @return {Analytics} 385 | */ 386 | 387 | Analytics.prototype.trackSubmit = Analytics.prototype.trackForm = function(forms, event, properties) { 388 | if (!forms) return this; 389 | // always arrays, handles jquery 390 | if (type(forms) === 'element') forms = [forms]; 391 | 392 | var self = this; 393 | each(function(el) { 394 | if (type(el) !== 'element') throw new TypeError('Must pass HTMLElement to `analytics.trackForm`.'); 395 | function handler(e) { 396 | prevent(e); 397 | 398 | var ev = is.fn(event) ? event(el) : event; 399 | var props = is.fn(properties) ? properties(el) : properties; 400 | self.track(ev, props); 401 | 402 | self._callback(function() { 403 | el.submit(); 404 | }); 405 | } 406 | 407 | // Support the events happening through jQuery or Zepto instead of through 408 | // the normal DOM API, because `el.submit` doesn't bubble up events... 409 | var $ = window.jQuery || window.Zepto; 410 | if ($) { 411 | $(el).submit(handler); 412 | } else { 413 | on(el, 'submit', handler); 414 | } 415 | }, forms); 416 | 417 | return this; 418 | }; 419 | 420 | /** 421 | * Trigger a pageview, labeling the current page with an optional `category`, 422 | * `name` and `properties`. 423 | * 424 | * @param {string} [category] 425 | * @param {string} [name] 426 | * @param {Object|string} [properties] (or path) 427 | * @param {Object} [options] 428 | * @param {Function} [fn] 429 | * @return {Analytics} 430 | */ 431 | 432 | Analytics.prototype.page = function(category, name, properties, options, fn) { 433 | // Argument reshuffling. 434 | /* eslint-disable no-unused-expressions, no-sequences */ 435 | if (is.fn(options)) fn = options, options = null; 436 | if (is.fn(properties)) fn = properties, options = properties = null; 437 | if (is.fn(name)) fn = name, options = properties = name = null; 438 | if (type(category) === 'object') options = name, properties = category, name = category = null; 439 | if (type(name) === 'object') options = properties, properties = name, name = null; 440 | if (type(category) === 'string' && type(name) !== 'string') name = category, category = null; 441 | /* eslint-enable no-unused-expressions, no-sequences */ 442 | 443 | properties = clone(properties) || {}; 444 | if (name) properties.name = name; 445 | if (category) properties.category = category; 446 | 447 | // Ensure properties has baseline spec properties. 448 | // TODO: Eventually move these entirely to `options.context.page` 449 | var defs = pageDefaults(); 450 | defaults(properties, defs); 451 | 452 | // Mirror user overrides to `options.context.page` (but exclude custom properties) 453 | // (Any page defaults get applied in `this.normalize` for consistency.) 454 | // Weird, yeah--moving special props to `context.page` will fix this in the long term. 455 | var overrides = pick(keys(defs), properties); 456 | if (!is.empty(overrides)) { 457 | options = options || {}; 458 | options.context = options.context || {}; 459 | options.context.page = overrides; 460 | } 461 | 462 | var msg = this.normalize({ 463 | properties: properties, 464 | category: category, 465 | options: options, 466 | name: name 467 | }); 468 | 469 | this._invoke('page', new Page(msg)); 470 | 471 | this.emit('page', category, name, properties, options); 472 | this._callback(fn); 473 | return this; 474 | }; 475 | 476 | /** 477 | * FIXME: BACKWARDS COMPATIBILITY: convert an old `pageview` to a `page` call. 478 | * 479 | * @param {string} [url] 480 | * @return {Analytics} 481 | * @api private 482 | */ 483 | 484 | Analytics.prototype.pageview = function(url) { 485 | var properties = {}; 486 | if (url) properties.path = url; 487 | this.page(properties); 488 | return this; 489 | }; 490 | 491 | /** 492 | * Merge two previously unassociated user identities. 493 | * 494 | * @param {string} to 495 | * @param {string} from (optional) 496 | * @param {Object} options (optional) 497 | * @param {Function} fn (optional) 498 | * @return {Analytics} 499 | */ 500 | 501 | Analytics.prototype.alias = function(to, from, options, fn) { 502 | // Argument reshuffling. 503 | /* eslint-disable no-unused-expressions, no-sequences */ 504 | if (is.fn(options)) fn = options, options = null; 505 | if (is.fn(from)) fn = from, options = null, from = null; 506 | if (is.object(from)) options = from, from = null; 507 | /* eslint-enable no-unused-expressions, no-sequences */ 508 | 509 | var msg = this.normalize({ 510 | options: options, 511 | previousId: from, 512 | userId: to 513 | }); 514 | 515 | this._invoke('alias', new Alias(msg)); 516 | 517 | this.emit('alias', to, from, options); 518 | this._callback(fn); 519 | return this; 520 | }; 521 | 522 | /** 523 | * Register a `fn` to be fired when all the analytics services are ready. 524 | * 525 | * @param {Function} fn 526 | * @return {Analytics} 527 | */ 528 | 529 | Analytics.prototype.ready = function(fn) { 530 | if (is.fn(fn)) { 531 | if (this._readied) { 532 | nextTick(fn); 533 | } else { 534 | this.once('ready', fn); 535 | } 536 | } 537 | return this; 538 | }; 539 | 540 | /** 541 | * Set the `timeout` (in milliseconds) used for callbacks. 542 | * 543 | * @param {Number} timeout 544 | */ 545 | 546 | Analytics.prototype.timeout = function(timeout) { 547 | this._timeout = timeout; 548 | }; 549 | 550 | /** 551 | * Enable or disable debug. 552 | * 553 | * @param {string|boolean} str 554 | */ 555 | 556 | Analytics.prototype.debug = function(str) { 557 | if (!arguments.length || str) { 558 | debug.enable('analytics:' + (str || '*')); 559 | } else { 560 | debug.disable(); 561 | } 562 | }; 563 | 564 | /** 565 | * Apply options. 566 | * 567 | * @param {Object} options 568 | * @return {Analytics} 569 | * @api private 570 | */ 571 | 572 | Analytics.prototype._options = function(options) { 573 | options = options || {}; 574 | this.options = options; 575 | cookie.options(options.cookie); 576 | store.options(options.localStorage); 577 | user.options(options.user); 578 | group.options(options.group); 579 | return this; 580 | }; 581 | 582 | /** 583 | * Callback a `fn` after our defined timeout period. 584 | * 585 | * @param {Function} fn 586 | * @return {Analytics} 587 | * @api private 588 | */ 589 | 590 | Analytics.prototype._callback = function(fn) { 591 | if (is.fn(fn)) { 592 | this._timeout ? setTimeout(fn, this._timeout) : nextTick(fn); 593 | } 594 | return this; 595 | }; 596 | 597 | /** 598 | * Call `method` with `facade` on all enabled integrations. 599 | * 600 | * @param {string} method 601 | * @param {Facade} facade 602 | * @return {Analytics} 603 | * @api private 604 | */ 605 | 606 | Analytics.prototype._invoke = function(method, facade) { 607 | var self = this; 608 | this.emit('invoke', facade); 609 | 610 | var failedInitializations = self.failedInitializations || []; 611 | each(function(integration, name) { 612 | if (!facade.enabled(name)) return; 613 | // Check if an integration failed to initialize. 614 | // If so, do not process the message as the integration is in an unstable state. 615 | if (failedInitializations.indexOf(name) >= 0) { 616 | self.log('Skipping invokation of .%s method of %s integration. Integation failed to initialize properly.', method, name); 617 | } else { 618 | try { 619 | integration.invoke.call(integration, method, facade); 620 | } catch (e) { 621 | self.log('Error invoking .%s method of %s integration: %o', method, name, e); 622 | } 623 | } 624 | }, this._integrations); 625 | 626 | return this; 627 | }; 628 | 629 | /** 630 | * Push `args`. 631 | * 632 | * @param {Array} args 633 | * @api private 634 | */ 635 | 636 | Analytics.prototype.push = function(args) { 637 | var method = args.shift(); 638 | if (!this[method]) return; 639 | this[method].apply(this, args); 640 | }; 641 | 642 | /** 643 | * Reset group and user traits and id's. 644 | * 645 | * @api public 646 | */ 647 | 648 | Analytics.prototype.reset = function() { 649 | this.user().logout(); 650 | this.group().logout(); 651 | }; 652 | 653 | /** 654 | * Parse the query string for callable methods. 655 | * 656 | * @param {String} query 657 | * @return {Analytics} 658 | * @api private 659 | */ 660 | 661 | Analytics.prototype._parseQuery = function(query) { 662 | // Parse querystring to an object 663 | var q = querystring.parse(query); 664 | // Create traits and properties objects, populate from querysting params 665 | var traits = pickPrefix('ajs_trait_', q); 666 | var props = pickPrefix('ajs_prop_', q); 667 | // Trigger based on callable parameters in the URL 668 | if (q.ajs_uid) this.identify(q.ajs_uid, traits); 669 | if (q.ajs_event) this.track(q.ajs_event, props); 670 | if (q.ajs_aid) user.anonymousId(q.ajs_aid); 671 | return this; 672 | 673 | /** 674 | * Create a shallow copy of an input object containing only the properties 675 | * whose keys are specified by a prefix, stripped of that prefix 676 | * 677 | * @param {String} prefix 678 | * @param {Object} object 679 | * @return {Object} 680 | * @api private 681 | */ 682 | 683 | function pickPrefix(prefix, object) { 684 | var length = prefix.length; 685 | var sub; 686 | return foldl(function(acc, val, key) { 687 | if (key.substr(0, length) === prefix) { 688 | sub = key.substr(length); 689 | acc[sub] = val; 690 | } 691 | return acc; 692 | }, {}, object); 693 | } 694 | }; 695 | 696 | /** 697 | * Normalize the given `msg`. 698 | * 699 | * @param {Object} msg 700 | * @return {Object} 701 | */ 702 | 703 | Analytics.prototype.normalize = function(msg) { 704 | msg = normalize(msg, keys(this._integrations)); 705 | if (msg.anonymousId) user.anonymousId(msg.anonymousId); 706 | msg.anonymousId = user.anonymousId(); 707 | 708 | // Ensure all outgoing requests include page data in their contexts. 709 | msg.context.page = defaults(msg.context.page || {}, pageDefaults()); 710 | 711 | return msg; 712 | }; 713 | 714 | /** 715 | * No conflict support. 716 | */ 717 | 718 | Analytics.prototype.noConflict = function() { 719 | window.analytics = _analytics; 720 | return this; 721 | }; 722 | 723 | /* 724 | * Exports. 725 | */ 726 | 727 | module.exports = Analytics; 728 | module.exports.cookie = cookie; 729 | module.exports.memory = memory; 730 | module.exports.store = store; 731 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 2 | 3.4.1 / 2018-04-23 3 | ================== 4 | 5 | * Catch and guard against Integration errors 6 | 7 | 3.4.0 / 2018-03-05 8 | ================== 9 | 10 | * Revert "[SCH-297][SCH-298] Add tracking plan support to identify and group traits" (#63) 11 | 12 | 3.3.0 / 2018-03-01 13 | ================== 14 | 15 | * Add tracking plan support to identify and group traits (#61) 16 | 17 | 3.2.7 / 2018-02-09 18 | ================== 19 | 20 | * Replace lodash deepclone with extend to lower ajs size 21 | 22 | 3.2.6 / 2018-02-06 23 | ================== 24 | 25 | * Replace ndhoule clone with lodash clone to handle circular references in objects 26 | 27 | 3.2.5 / 2017-11-09 28 | ================== 29 | 30 | * This release has no application changes - it's an attempt to fix release commits on CI. 31 | 32 | 3.2.4 / 2017-11-09 33 | ================== 34 | 35 | * Revert "update page defaults search method" (#51). 36 | 37 | 3.2.3 / 2017-11-09 38 | ================== 39 | 40 | * Add support for schema defaults (#50). 41 | 42 | 3.2.2 / 2017-11-05 43 | ================== 44 | 45 | * Build updates on CI. 46 | * This release has no application changes - it's an attempt to fix release commits on CI. 47 | 48 | 3.2.1 / 2017-11-03 49 | ================== 50 | 51 | * Fix release commit in 3.2.0 52 | 53 | 3.2.0 / 2017-11-03 54 | ================== 55 | 56 | * Send disabled events to Segment. 57 | 58 | 3.1.3 / 2017-11-01 59 | ================== 60 | 61 | * Adds invocation of integration.ready in initialize catch statement to ensure analytics.ready callbacks are fired. 62 | 63 | 3.1.2 / 2017-10-31 64 | ================== 65 | 66 | * Updates try/catch logic during initializations of integrations to look for integration.name - not integration.prototype.name 67 | * Adds a check during `analytics._invoke` to check if the integration failed to initialize and if so, logs that it is passing and does not invoke it's corresponding method. 68 | 69 | 3.1.1 / 2017-10-31 70 | ================== 71 | 72 | * Wrap initialize functions of integrations in try/catch statement. 73 | * Add logging of failed initializations. 74 | * Add a `failedInitializations` array to prototype to capture names of any failed integrations. 75 | 76 | 3.1.0 / 2017-06-29 77 | ================== 78 | 79 | * Deprecate IE7/8 testing support 80 | * Re-modernize test dependencies 81 | 82 | 3.0.0 / 2016-05-25 83 | ================== 84 | 85 | * Remove Duo support, add browserify support 86 | * Modernize test harness 87 | 88 | 2.12.2 / 2016-05-24 89 | =================== 90 | 91 | * Replace component/assert with segmentio/assert to fix build issues 92 | 93 | 2.12.1 / 2016-05-23 94 | =================== 95 | 96 | * Fix bad dependency pin 97 | 98 | 2.12.0 / 2016-05-06 99 | =================== 100 | 101 | * Update facade dependency to 2.x 102 | 103 | 2.11.1 / 2015-10-13 104 | =================== 105 | 106 | * publishing new version to fix bower 107 | 108 | 2.11.0 / 2015-09-02 109 | =================== 110 | 111 | * add support for populating traits and properties in querystring-triggered calls 112 | 113 | 2.10.1 / 2015-07-30 114 | =================== 115 | 116 | * Bump component/querystring to 2.0.0 to fix a URL encoding issue 117 | 118 | 2.10.0 / 2015-06-16 119 | ================== 120 | 121 | * remove git hooks 122 | * add circle.yml 123 | * remove travis ci 124 | * ignore built files 125 | * remove integrations 126 | 127 | 2.9.1 / 2015-06-11 128 | ================== 129 | 130 | * Remove deprecated analytics.js-integrations dependency 131 | * Update build 132 | 133 | 2.9.0 / 2015-06-11 134 | ================== 135 | 136 | * Pull integrations from individual repositories, located in the [segment-integrations GitHub organization](https://github.com/segment-integrations/). This change should be unnoticeable from a user perspective, but has huge benefits in that excluding integrations from custom builds is now much, much easier, and one integration's test failures will no longer prevent another integration's tests from running. 137 | 138 | A noteworthy part of this change: All integrations are now pulled into Analytics.js in `component.json`, using an explicit version number. 139 | In the future this part of the build process is very likely to change to be more of an automatic process, but for now--baby steps. 140 | 141 | 2.8.25 / 2015-06-03 142 | =================== 143 | 144 | * Update build (for real this time) 145 | 146 | 2.8.24 / 2015-06-03 147 | =================== 148 | 149 | * Update build 150 | 151 | 2.8.23 / 2015-05-27 152 | =================== 153 | 154 | * Update component/url dependency to 0.2.0 155 | 156 | 2.8.22 / 2015-05-22 157 | =================== 158 | 159 | * Update build 160 | 161 | 2.8.21 / 2015-05-22 162 | =================== 163 | 164 | * Update build 165 | 166 | 2.8.20 / 2015-05-22 167 | =================== 168 | 169 | * Update build 170 | * Clean up Makefile 171 | 172 | 2.8.19 / 2015-05-16 173 | =================== 174 | 175 | * Pin all dependencies 176 | * Bump Node.js engine dependency to 0.12 177 | 178 | 2.8.18 / 2015-05-14 179 | =================== 180 | 181 | * Bump duo-test dependency 182 | 183 | 2.8.17 / 2015-05-02 184 | =================== 185 | 186 | * Build updated 187 | 188 | 2.8.16 / 2015-05-01 189 | =================== 190 | 191 | * Build updated 192 | 193 | 2.8.15 / 2015-04-29 194 | =================== 195 | 196 | * Build updated 197 | 198 | 2.8.14 / 2015-04-29 199 | =================== 200 | 201 | * Build updated 202 | 203 | 2.8.13 / 2015-04-28 204 | =================== 205 | 206 | * Build updated 207 | 208 | 2.8.12 / 2015-04-23 209 | =================== 210 | 211 | * deps: bump top-domain for test cookie deletion fix 212 | * cookie: bump top-domain to v2 to catch all top domains 213 | 214 | 215 | 2.8.10 / 2015-04-20 216 | =================== 217 | 218 | * Build updated 219 | 220 | 2.8.9 / 2015-04-16 221 | ================== 222 | * Fix conflicts 223 | 224 | 2.8.8 / 2015-04-16 225 | ================== 226 | 227 | * Updating analytics.js-integrations 228 | * Updating analytics.js-integrations 229 | 230 | 2.8.7 / 2015-04-09 231 | ================== 232 | 233 | * Build updated 234 | * adding pre-release hook (and make targets for hooks) 235 | 236 | 2.8.6 / 2015-04-09 237 | ================== 238 | 239 | * Build updated 240 | 241 | 2.8.5 / 2015-04-09 242 | ================== 243 | 244 | * Build updated 245 | 246 | 2.8.4 / 2015-04-02 247 | ================== 248 | 249 | * Build updated 250 | 251 | 2.8.3 / 2015-03-31 252 | ================== 253 | 254 | * Build updated 255 | 256 | 2.8.2 / 2015-03-24 257 | ================== 258 | 259 | * Build updated 260 | 261 | 2.8.1 / 2015-03-20 262 | ================== 263 | 264 | * Build updated 265 | * adding a build phony target 266 | 267 | 2.8.0 / 2015-03-07 268 | ================== 269 | 270 | * group: fix typo 271 | * entity: add debug warning for memory store 272 | * test: add fallback to memory tests 273 | * entity: fallback to memory 274 | * add memory store 275 | * entity: fallback to localstorage when cookies are disabled 276 | * tests: add localstorage fallback tests 277 | * dist: rebuild 278 | 279 | 2.7.1 / 2015-03-05 280 | ================== 281 | 282 | * Updating analytics.js-integrations 283 | 284 | 2.7.0 / 2015-03-05 285 | ================== 286 | 287 | * Attach page metadata to all calls as `context.page` 288 | 289 | 2.6.13 / 2015-03-04 290 | =================== 291 | 292 | * normalize: remove trailing comma 293 | * dist: rebuild to make tests pass 294 | * normalize: remove redundant keys from toplevel 295 | 296 | 2.6.12 / 2015-03-03 297 | =================== 298 | 299 | * Release 2.6.11 300 | * normalize: keep traits in options 301 | 302 | 2.6.11 / 2015-02-28 303 | ================== 304 | 305 | * normalize: keep traits in options 306 | 307 | 2.6.10 / 2015-02-25 308 | =================== 309 | 310 | * Updating analytics.js-integrations 311 | 312 | 2.6.9 / 2015-02-25 313 | ================== 314 | 315 | * Updating analytics.js-integrations 316 | 317 | 2.6.8 / 2015-02-25 318 | ================== 319 | 320 | * Updating analytics.js-integrations 321 | 322 | 2.6.7 / 2015-02-24 323 | ================== 324 | 325 | * Updating analytics.js-integrations 326 | * removed duplicate .on('initialize') from analytics constructor 327 | 328 | 2.6.6 / 2015-02-23 329 | ================== 330 | 331 | * update integrations 332 | 333 | 334 | 2.6.5 / 2015-02-19 335 | ================== 336 | 337 | * analytics: less verbose logging 338 | * analytics.js: cleanup plan 339 | * analytics.js: add debugs 340 | * normalize: dont clobber and add tests 341 | * analytics: use normalize removing message() 342 | * add normalize.js 343 | 344 | 345 | 2.6.4 / 2015-02-19 346 | ================== 347 | 348 | * Updating analytics.js-integrations 349 | 350 | 2.6.3 / 2015-02-17 351 | ================== 352 | 353 | * plan: .archived -> .enabled 354 | 355 | 2.6.1 / 2015-02-12 356 | ================== 357 | 358 | * user: fix old anonymous id 359 | 360 | 361 | 2.6.0 / 2015-02-09 362 | ================== 363 | 364 | * .track(): ignore archived events 365 | * ._options(): preserve options 366 | 367 | 2.5.17 / 2015-02-04 368 | =================== 369 | 370 | * Updating analytics.js-integrations 371 | 372 | 2.5.16 / 2015-02-04 373 | =================== 374 | 375 | * Updating analytics.js-integrations 376 | 377 | 2.5.15 / 2015-02-03 378 | =================== 379 | 380 | * Updating analytics.js-integrations 381 | 382 | 2.5.14 / 2015-02-03 383 | =================== 384 | 385 | * Updating analytics.js-integrations 386 | 387 | 2.5.13 / 2015-01-29 388 | =================== 389 | 390 | * Updating analytics.js-integrations 391 | 392 | 2.5.12 / 2015-01-23 393 | =================== 394 | 395 | * Updating analytics.js-integrations 396 | 397 | 2.5.10 / 2015-01-22 398 | =================== 399 | 400 | * Updating analytics.js-integrations 401 | 402 | 2.5.9 / 2015-01-22 403 | ================== 404 | 405 | * Updating analytics.js-integrations 406 | 407 | 2.5.8 / 2015-01-21 408 | ================== 409 | 410 | * Updating analytics.js-integrations 411 | 412 | 2.5.7 / 2015-01-15 413 | ================== 414 | 415 | * Updating analytics.js-integrations 416 | 417 | 2.5.6 / 2015-01-15 418 | ================== 419 | 420 | * Updating analytics.js-integrations 421 | 422 | 2.5.5 / 2015-01-14 423 | ================== 424 | 425 | * Updating analytics.js-integrations 426 | 427 | 2.5.4 / 2015-01-14 428 | ================== 429 | 430 | * Updating analytics.js-integrations 431 | 432 | 2.5.3 / 2015-01-08 433 | ================== 434 | 435 | * Fix release 436 | 437 | 2.5.2 / 2015-01-08 438 | ================== 439 | 440 | * Updating analytics.js-integrations 441 | 442 | 2.5.0 / 2015-01-01 443 | ================== 444 | 445 | * update integrations 446 | * analytics: add setAnonymousId 447 | 448 | 2.4.21 / 2014-12-11 449 | =================== 450 | 451 | * Updating analytics.js-integrations 452 | * tests: skip svg tests on legacy browsers 453 | * travis: node 0.11.13 454 | * trackLink: support svg anchor tags 455 | * add cross browser tests 456 | 457 | 2.4.18 / 2014-11-22 458 | =================== 459 | 460 | * Updating analytics.js-integrations 461 | 462 | 2.4.16 / 2014-11-13 463 | =================== 464 | 465 | * Updating analytics.js-integrations 466 | 467 | 2.4.15 / 2014-11-11 468 | ================== 469 | 470 | * clean: --force to ignore errs 471 | * Updating analytics.js-integrations 472 | 473 | 2.4.14 / 2014-11-06 474 | =================== 475 | 476 | * Updating analytics.js-integrations 477 | 478 | 2.4.10 / 2014-10-27 479 | ================== 480 | 481 | * support umd 482 | 483 | 2.4.9 / 2014-10-25 484 | ================== 485 | 486 | * Updating analytics.js-integrations 487 | 488 | 2.4.7 / 2014-10-21 489 | ================== 490 | 491 | * Updating analytics.js integrations to 1.3.2 492 | 493 | 2.4.6 / 2014-10-17 494 | ================== 495 | 496 | * upgrade integrations to 2.3.1 497 | 498 | 2.4.5 / 2014-10-17 499 | ================== 500 | 501 | * upgrade integrations to 2.3 502 | 503 | 2.4.4 / 2014-10-16 504 | ================== 505 | 506 | * upgrade integrations. 507 | 508 | 2.4.3 / 2014-10-15 509 | ================== 510 | 511 | * Merge pull request #407 from segmentio/prevent/duplicates 512 | * fix: prevent duplicates when cookie cannot be set 513 | 514 | 2.4.2 / 2014-10-15 515 | ================== 516 | 517 | * Merge pull request #406 from segmentio/fix/user-id-reset 518 | * fix: prevent anonymousId from changing when user id is reset 519 | 520 | 2.4.1 / 2014-10-14 521 | ================== 522 | 523 | * Merge pull request #405 from segmentio/fix/old-anonymous-id 524 | * fix: old anonymousId is not stringified, use raw cookie 525 | * Release 2.4.0 526 | 527 | 2.4.0 / 2014-10-14 528 | ================== 529 | 530 | * anonymousId: re-generate when user id changes 531 | * Merge pull request #401 from segmentio/anonymous-id 532 | * analytics.reset(): use .logout() to preserve options 533 | * logout: remove anonymous id 534 | * parseQuery: add ajs_aid 535 | * analytics add anonymousId support 536 | * add User#anonymousId 537 | * Release 2.3.33 538 | 539 | 2.3.33 / 2014-10-14 540 | =================== 541 | 542 | * upgrade integrations 543 | 544 | 2.3.33 / 2014-10-10 545 | ================== 546 | 547 | * upgrade integrations 548 | 549 | 2.3.32 / 2014-10-09 550 | =================== 551 | 552 | * upgrade integrations 553 | 554 | 2.3.31 / 2014-10-08 555 | =================== 556 | 557 | * history.md: ocd 558 | 559 | 2.3.30 / 2014-10-07 560 | =================== 561 | 562 | * upgrade integrations 563 | 564 | 2.3.29 / 2014-10-06 565 | =================== 566 | 567 | * add reset(), closes #378 568 | 569 | 2.3.28 / 2014-10-01 570 | =================== 571 | 572 | * upgrade integrations 573 | 574 | 575 | 2.3.27 / 2014-09-26 576 | =================== 577 | 578 | * upgrade integrations 579 | 580 | 581 | 2.3.26 / 2014-09-26 582 | =================== 583 | 584 | * upgrade integrations 585 | 586 | 587 | 2.3.25 / 2014-09-22 588 | =================== 589 | 590 | * add node 0.11 notice for now 591 | 592 | 2.3.24 / 2014-09-17 593 | =================== 594 | 595 | * upgrade integrations 596 | 597 | 598 | 2.3.23 / 2014-09-08 599 | =================== 600 | 601 | * upgrade integrations 602 | 603 | 604 | 2.3.22 / 2014-09-05 605 | =================== 606 | 607 | * upgrade integrations 608 | 609 | 610 | 2.3.21 / 2014-09-04 611 | =================== 612 | 613 | * ocd 614 | 615 | 2.3.20 / 2014-09-04 616 | =================== 617 | 618 | * upgrade integrations 619 | 620 | 621 | 2.3.19 / 2014-09-02 622 | =================== 623 | 624 | * upgrade integrations 625 | 626 | 627 | 2.3.18 / 2014-09-02 628 | =================== 629 | 630 | * upgrade integrations 631 | 632 | 633 | 2.3.17 / 2014-08-28 634 | =================== 635 | 636 | * deps: duo 0.7 637 | * deps: duo 0.7 638 | * Merge pull request #397 from segmentio/add/anonymous-id 639 | * add checking for anonymous id in options 640 | 641 | 2.3.15 / 2014-08-22 642 | ================== 643 | 644 | * google adwords: directly pass remarketing option 645 | 646 | 2.3.14 / 2014-08-22 647 | ================== 648 | 649 | * deps: upgrade to duo-test@0.3.x 650 | * google adwords: switch to async api 651 | 652 | 2.3.13 / 2014-08-20 653 | ================== 654 | 655 | * localstorage fallback: add implementation 656 | * localstorage fallback: add tests 657 | * rebuild 658 | * deps: upgrade to duo 0.7 659 | * make: dont clean my npm cache :P 660 | 661 | 2.3.12 / 2014-08-07 662 | ================== 663 | 664 | * remove userfox 665 | 666 | 2.3.11 / 2014-08-07 667 | ================== 668 | 669 | * merge a few more fixes (keen.io) 670 | 671 | 2.3.10 / 2014-07-25 672 | ================== 673 | 674 | * Make lots of analytics.js-integrations fixes 675 | 676 | 2.3.7 / 2014-07-21 677 | ================== 678 | 679 | * Merge pull request #390 from segmentio/test/element-error 680 | * throw helpful error when passing string to `trackLink`, closes #389 681 | * Merge pull request #386 from segmentio/context 682 | * add integrations select test 683 | * add backwards compat options object support 684 | 685 | 2.3.6 / 2014-07-16 686 | ================== 687 | 688 | * upgrade integrations 689 | 690 | 691 | 2.3.5 / 2014-07-16 692 | ================== 693 | 694 | * upgrade integrations 695 | 696 | 697 | 2.3.4 / 2014-07-16 698 | ================== 699 | 700 | * upgrade integrations 701 | 702 | 703 | 2.3.3 / 2014-07-16 704 | ================== 705 | 706 | * fix: History.md 707 | 708 | 2.3.2 / 2014-07-13 709 | ================== 710 | 711 | * rebuild 712 | 713 | 2.3.1 / 2014-07-13 714 | ================== 715 | 716 | * deps: remove duo-package 717 | * make: test-saucelabs -> test-sauce 718 | 719 | 2.3.0 / 2014-07-11 720 | ================== 721 | 722 | * use analytics.js-integrations 1.2.0 which removes plugin.Integration 723 | * set .analytics on integration instance 724 | 725 | 2.2.5 / 2014-07-08 726 | ================== 727 | 728 | * loosen deps 729 | 730 | 2.2.4 / 2014-07-08 731 | =================== 732 | 733 | * rebuild 734 | 735 | 2.2.3 / 2014-07-07 736 | =================== 737 | 738 | * rebuild 739 | 740 | 2.2.2 / 2014-06-24 741 | ================== 742 | 743 | * fix fxn 744 | 745 | 2.2.1 / 2014-06-24 746 | ================== 747 | 748 | * fix typo 749 | 750 | 2.2.0 / 2014-06-24 751 | ================== 752 | 753 | * bump analytics.js-integrations with bing/bronto fixes 754 | 755 | 2.1.0 / 2014-06-23 756 | ================== 757 | 758 | * add `.add` for test-friendliness 759 | * make-test: kill the server when done testing 760 | * tests: add reporter option 761 | * update readme 762 | * make-test: make sure we use the correct phantomjs(1) 763 | 764 | 2.0.1 / 2014-06-13 765 | ================== 766 | 767 | * bumping store.js dep to 2.0.0 768 | * update readme 769 | 770 | 2.0.0 / 2014-06-12 771 | ================== 772 | 773 | * converting to use duo 774 | 775 | 1.5.12 / 2014-06-11 776 | ================== 777 | 778 | * bump analytics.js-integrations to 0.9.9 779 | 780 | 1.5.11 / 2014-06-05 781 | ================== 782 | 783 | * bump analytics.js-integrations to 0.9.8 784 | 785 | 1.5.10 / 2014-06-04 786 | ================== 787 | 788 | * bump analytics.js-integrations to 0.9.7 789 | 790 | 1.5.9 / 2014-06-04 791 | ================== 792 | 793 | * bump analytics.js-integrations to 0.9.6 794 | 795 | 1.5.8 / 2014-06-04 796 | ================== 797 | 798 | * bump analytics.js-integrations to 0.9.5 799 | 800 | 1.5.6 / 2014-06-02 801 | ================== 802 | 803 | * bump analytics.js-integrations to 0.9.3 804 | 805 | 1.5.5 / 2014-06-02 806 | ================== 807 | 808 | * bump analytics.js-integrations to 0.9.2 809 | 810 | 1.5.4 / 2014-05-30 811 | ================== 812 | 813 | * upgrade integrations to 0.9.1 814 | 815 | 1.5.3 / 2014-05-29 816 | ================== 817 | 818 | * upgrade integrations to 0.9.0 819 | 820 | 1.5.1 / 2014-05-20 821 | ================== 822 | 823 | * update analytics.js-integrations dep for reverting KISSmetrics fixes 824 | 825 | 1.5.0 / 2014-05-19 826 | ================== 827 | 828 | * updating analytics.js-integrations to 0.8.0 for KISSmetrics fixes 829 | 830 | 1.4.0 / 2014-05-17 831 | ================== 832 | 833 | * upgrade integrations to 0.7.0 834 | * upgrade facade to 0.3.10 835 | 836 | 1.3.31 / 2014-05-17 837 | ================== 838 | 839 | * handle dev envs correctly, closes #359 840 | 841 | 1.3.30 / 2014-05-07 842 | ================== 843 | 844 | * upgrade integrations to 0.6.1 for google analytics custom dimensions and metrics 845 | 846 | 1.3.28 / 2014-04-29 847 | ================== 848 | 849 | * upgrade integrations to 0.5.10 for navilytics fix and mixpanel fix 850 | * component: upgrade to 0.19.6 and add githubusercontent to remotes 851 | 852 | 1.3.26 / 2014-04-17 853 | ================== 854 | 855 | * upgrade integrations to 0.5.8 856 | 857 | 1.3.25 / 2014-04-16 858 | ================== 859 | 860 | * upgrade integrations to 0.5.6 861 | 862 | 1.3.24 / 2014-04-15 863 | ================== 864 | 865 | * move analytics.js-integration to dev deps 866 | 867 | 1.3.23 / 2014-04-14 868 | ================== 869 | 870 | * upgrade integrations to 0.5.5 871 | * update querystring to 1.3.0 872 | 873 | 1.3.22 / 2014-04-11 874 | ================== 875 | 876 | * upgrade integrations to 0.5.4 877 | 878 | 1.3.21 / 2014-04-10 879 | ================== 880 | 881 | * add "invoke" event 882 | 883 | 1.3.20 / 2014-04-07 884 | ================== 885 | 886 | * upgrade integrations to 0.5.3 887 | 888 | 1.3.19 / 2014-04-05 889 | ================== 890 | 891 | * upgrade querystring to 1.2.0 892 | 893 | 1.3.18 / 2014-04-05 894 | ================== 895 | 896 | * upgrade integrations to 0.5.1 897 | 898 | 1.3.17 / 2014-04-04 899 | ================== 900 | 901 | * upgrade integrations to 0.5.0 902 | * fix: add .search to .url when url is pulled from canonical tag 903 | * tests: upgrade gravy to 0.2.0 904 | 905 | 1.3.16 / 2014-04-01 906 | ================== 907 | 908 | * upgrade integrations to 0.4.14 909 | 910 | 1.3.15 / 2014-03-26 911 | ================== 912 | 913 | * upgrade integrations to 0.4.13 914 | 915 | 1.3.14 / 2014-03-26 916 | ================== 917 | 918 | * upgrade integrations to 0.4.12 919 | 920 | 1.3.13 / 2014-03-25 921 | ================== 922 | 923 | * upgrade integrations to 0.4.11 924 | 925 | 1.3.12 / 2014-03-19 926 | ================== 927 | 928 | * upgrade integrations to 0.4.10 929 | 930 | 1.3.11 / 2014-03-14 931 | =================== 932 | 933 | * upgrade integrations to 0.4.9 934 | 935 | 1.3.10 / 2014-03-14 936 | =================== 937 | 938 | * upgrade integrations to 0.4.8 939 | 940 | 1.3.9 / 2014-03-14 941 | ================== 942 | 943 | * upgrade integrations to 0.4.7 944 | 945 | 1.3.8 / 2014-03-13 946 | ================== 947 | 948 | * upgrade integrations to 0.4.6 949 | 950 | 1.3.7 / 2014-03-06 951 | ================== 952 | 953 | * upgrade integrations to 0.4.5 954 | * upgrade facade to 0.2.11 955 | 956 | 1.3.6 / 2014-03-05 957 | ================== 958 | 959 | * upgrade integrations to 0.4.4 960 | 961 | 1.3.4 / 2014-02-26 962 | ================== 963 | 964 | * update integrations to 0.4.2 965 | 966 | 1.3.3 / 2014-02-18 967 | ================== 968 | 969 | * upgrade analytics.js-integrations to 0.4.1 970 | * dont reset ids and traits 971 | 972 | 1.3.2 / 2014-02-07 973 | ================== 974 | 975 | * upgrade analytics.js-integrations to 0.4.0 976 | * upgrade analytics.js-integration to 0.1.7 977 | * upgrade facade to 0.2.7 978 | * fix page url default to check canonical and remove hash 979 | 980 | 1.3.1 / 2014-01-30 981 | ================== 982 | 983 | * upgrade isodate-traverse to `0.3.0` 984 | * upgrade facade to `0.2.4` 985 | * upgrade analytics.js-integrations to `0.3.10` 986 | 987 | 1.3.0 / 2014-01-23 988 | ================== 989 | 990 | * update analytics.js-integrations to 0.3.9 991 | 992 | 1.2.9 / 2014-01-18 993 | ================== 994 | 995 | * update `analytics.js-integrations` to `0.3.8` 996 | * expose `require()` 997 | 998 | 1.2.8 / 2014-01-15 999 | ================== 1000 | 1001 | * update `analytics.js-integrations` to `0.3.7` 1002 | * upgrade `facade` to `0.2.3` 1003 | 1004 | 1.2.7 / 2014-01-10 1005 | ================== 1006 | 1007 | * update `analytics.js-integrations` to `0.3.6` 1008 | 1009 | 1.2.6 - January 3, 2014 1010 | ----------------------- 1011 | * upgrade `component(1)` for json support 1012 | 1013 | 1.2.5 - January 3, 2014 1014 | ----------------------- 1015 | * upgrade `analytics.js-integrations` to `0.3.5` 1016 | * upgrade `facade` to `0.2.1` 1017 | 1018 | 1.2.4 - January 2, 2014 1019 | ------------------------- 1020 | * upgrade `analytics.js-integrations` to `0.3.4` 1021 | 1022 | 1.2.3 - December 18, 2013 1023 | ------------------------- 1024 | * fix `facade` dependency 1025 | 1026 | 1.2.2 - December 18, 2013 1027 | ------------------------- 1028 | * upgrade `analytics.js-integrations` to `0.3.2` 1029 | 1030 | 1.2.1 - December 16, 2013 1031 | ------------------------- 1032 | * add #push, fixes #253 1033 | 1034 | 1.2.0 - December 13, 2013 1035 | ------------------------- 1036 | * add [`facade`](https://github.com/segmentio/facade) 1037 | 1038 | 1.1.9 - December 11, 2013 1039 | ------------------------- 1040 | * upgrade `analytics.js-integrations` to `0.2.16` 1041 | * add `search` to page property defaults 1042 | 1043 | 1.1.8 - December 11, 2013 1044 | ------------------------ 1045 | * upgrade `analytics.js-integrations` to `0.2.15` 1046 | * add [WebEngage](http://webengage.com) 1047 | * heap: fallback to user id as handle 1048 | 1049 | 1.1.7 - December 4, 2013 1050 | ------------------------ 1051 | * upgrade `analytics.js-integrations` to `0.2.13` 1052 | 1053 | 1.1.6 - December 2, 2013 1054 | ------------------------ 1055 | * update `analytics.js-integrations` to `0.2.12` 1056 | * add `entity` 1057 | * change `user` to inherit from `entity` 1058 | * change `group` to inherit from `entity` 1059 | 1060 | 1.1.5 - November 26, 2013 1061 | ------------------------- 1062 | * update `analytics.js-integration` to `0.1.5` 1063 | * update `analytics.js-integrations` to `0.2.11` 1064 | 1065 | 1.1.4 - November 25, 2013 1066 | ------------------------- 1067 | * fix `page` method properties overload 1068 | 1069 | 1.1.3 - November 21, 2013 1070 | ------------------------- 1071 | * update `analytics.js-integrations` to `0.2.10` 1072 | 1073 | 1.1.2 - November 21, 2013 1074 | ------------------------- 1075 | * update `analytics.js-integrations` to `0.2.9` 1076 | 1077 | 1.1.1 - November 20, 2013 1078 | ------------------------- 1079 | * update `analytics.js-integrations` to `0.2.8` 1080 | 1081 | 1.1.0 - November 20, 2013 1082 | ------------------------- 1083 | * add `name` and `category` defaults to `page` method calls 1084 | * update `analytics.js-integrations` to `0.2.7` 1085 | 1086 | 1.0.9 - November 15, 2013 1087 | ------------------------- 1088 | * update `analytics.js-integrations` to `0.2.6` 1089 | * update dependencies 1090 | 1091 | 1.0.8 - November 14, 2013 1092 | ------------------------- 1093 | * update `analytics.js-integrations` to `0.2.5` 1094 | 1095 | 1.0.7 - November 13, 2013 1096 | ------------------------ 1097 | * update `analytics.js-integrations` to `0.2.4` 1098 | 1099 | 1.0.6 - November 12, 2013 1100 | ------------------------- 1101 | * update `analytics.js-integrations` to `0.2.3` 1102 | * update `analytics.js-integration` to `0.1.4` 1103 | 1104 | 1.0.5 - November 12, 2013 1105 | ------------------------- 1106 | * update `analytics.js-integrations` to `0.2.2` 1107 | * fix `properties` overload for `page` method 1108 | 1109 | 1.0.4 - November 12, 2013 1110 | ------------------------- 1111 | * update `analytics.js-integrations` to `0.2.1` 1112 | 1113 | 1.0.3 - November 11, 2013 1114 | ------------------------- 1115 | * update `analytics.js-integrations` to `0.2.0` 1116 | 1117 | 1.0.2 - November 11, 2013 1118 | ------------------------- 1119 | * rename the page methods `section` argument to `category` 1120 | * update `analytics.js-integration` 1121 | * update `analytics.js-integrations` 1122 | 1123 | 1.0.1 - November 11, 2013 1124 | ------------------------- 1125 | * change `page` to take a `section` 1126 | * update `analytics.js-integration` 1127 | * update `analytics.js-integrations` 1128 | 1129 | 1.0.0 - November 10, 2013 1130 | ------------------------- 1131 | * change `pageview` method to `page` 1132 | * add call to `page` as mandatory to initialize some analytics tools 1133 | * remove ability to `initialize` by `key` 1134 | * add checking for an integration already being loaded before loading 1135 | * add `#use` method for plugins 1136 | * add event emitter to `analytics` 1137 | * move integrations to [`analytics.js-integrations`](https://github.com/segmentio/analytics.js-integrations) 1138 | * add debugging to all integrations 1139 | * move integration factory to [`analytics.js-integration`](https://github.com/segmentio/analytics.js-integration) 1140 | * Amplitude: rename `pageview` option to `trackAllPages` 1141 | * Amplitude: add `trackNamedPages` option 1142 | * Google Analytics: add `trackNamedPages` option 1143 | * Google Analytics: remove `initialPageview` option 1144 | * Keen IO: rename `pageview` option to `trackAllPages` 1145 | * Keen IO: add `trackNamedPages` option 1146 | * Keen IO: remove `initialPageview` option 1147 | * Lytics: remove `initialPageview` option 1148 | * Mixpanel: rename `pageview` option to `trackAllPages` 1149 | * Mixpanel: add `trackNamedPages` option 1150 | * Mixpanel: remove `initialPageview` option 1151 | * Olark: rename `pageview` option to `page` 1152 | * Tapstream: remove `initialPageview` option 1153 | * Tapstream: add `trackAllPages` option 1154 | * Tapstream: add `trackNamedPages` option 1155 | * Trak.io: remove `pageview` option 1156 | * Trak.io: remove `initialPageview` option 1157 | * Trak.io: add `trackNamedPages` option 1158 | * Woopra: remove `initialPageview` option 1159 | 1160 | 0.18.4 - October 29, 2013 1161 | ------------------------- 1162 | * adding convert-date 0.1.0 support 1163 | 1164 | 0.18.3 - October 29, 2013 1165 | ------------------------- 1166 | * hubspot: adding fix for date traits/properties (calvinfo) 1167 | 1168 | 0.18.2 - October 28, 2013 1169 | ------------------------- 1170 | * upgrade visionmedia/debug to most recent version, fixes security warnings when cookies are disabled. 1171 | 1172 | 0.18.1 - October 28, 2013 1173 | ------------------------- 1174 | * add [Evergage](http://evergage.com), by [@glajchs](https://github.com/glajchs) 1175 | 1176 | 0.18.0 - October 24, 2013 1177 | ------------------------- 1178 | * add event emitter 1179 | * add `initialize`, `ready`, `identify`, `alias`, `pageview`, `track`, and `group` events and tests 1180 | * fix date equality tests 1181 | 1182 | 0.17.9 - October 24, 2013 1183 | ------------------------- 1184 | * Google Analytics: fix ip anonymization should come after `create` 1185 | * Google Analytics: fix domain to default to `"none"` 1186 | 1187 | 0.17.8 - October 14, 2013 1188 | ------------------------- 1189 | * Customer.io: added preliminary `group` support 1190 | 1191 | 0.17.7 - October 10, 2013 1192 | ------------------------- 1193 | * propagating traverse isodate fix 1194 | 1195 | 0.17.6 - October 7, 2013 1196 | ------------------------ 1197 | * added [Yandex Metrica](http://metrika.yandex.com), by [@yury-egorenkov](https://github.com/yury-egorenkov) 1198 | 1199 | 0.17.5 - October 2, 2013 1200 | ------------------------ 1201 | * fixed bug in `_invoke` not cloning arguments 1202 | 1203 | 0.17.4 - September 30, 2013 1204 | --------------------------- 1205 | * added conversion of ISO strings to dates for `track` calls 1206 | 1207 | 0.17.3 - September 30, 2013 1208 | --------------------------- 1209 | * fixed bug in key-only initialization 1210 | 1211 | 0.17.2 - September 30, 2013 1212 | --------------------------- 1213 | * UserVoice: added `classicMode` option 1214 | 1215 | 0.17.1 - September 30, 2013 1216 | --------------------------- 1217 | * UserVoice: fixed bug loading trigger with new widget 1218 | 1219 | 0.17.0 - September 30, 2013 1220 | --------------------------- 1221 | * added `debug` method, by [@yields](https://github.com/yields) 1222 | 1223 | 0.16.0 - September 27, 2013 1224 | --------------------------- 1225 | * UserVoice: updated integration to handle the new widget 1226 | 1227 | 0.15.2 - September 26, 2013 1228 | --------------------------- 1229 | * added Awesomatic, by [@robv](https://github.com/robv) 1230 | 1231 | 0.15.1 - September 24, 2013 1232 | --------------------------- 1233 | * fixed bug in `ready` causing it to never fire with faulty settings 1234 | * fixed all `ready()` calls to always be async 1235 | * cleared ready state after all analytics core `initialize` tests 1236 | 1237 | 0.15.0 - September 18, 2013 1238 | --------------------------- 1239 | * Crazy Egg: renamed from `CrazyEgg` 1240 | * Google Analytics: changed `universalClient` option to `classic` 1241 | * Google Analytics: changed `classic` default to `false` 1242 | * Keen IO: changed pageview options defaults to `false` 1243 | * LeadLander: changed `llactid` option to human-readable `accountId`* Intercom: make `#IntercomDefaultWidget` the default activator 1244 | 1245 | 0.14.3 - September 18, 2013 1246 | --------------------------- 1247 | * exposed `createIntegration` and `addIntegration` 1248 | 1249 | 0.14.2 - September 17, 2013 1250 | --------------------------- 1251 | * added [Spinnakr](http://spinnakr.com) 1252 | 1253 | 0.14.1 - September 17, 2013 1254 | --------------------------- 1255 | * removed old `Provider` for an `integration` factory 1256 | 1257 | 0.14.0 - September 16, 2013 1258 | --------------------------- 1259 | * exposed `group` via the `#group` method 1260 | * exposed `user` via the `#user` method 1261 | * started caching `group` in cookie and local storage like `user` 1262 | * changed `user` and `group` info to always be queried from storage 1263 | * bound all `analytics` methods as a singleton 1264 | * added `identify(traits, options)` override 1265 | * added `timeout` setter method 1266 | 1267 | 0.13.2 - September 16, 2013 1268 | --------------------------- 1269 | * added [Rollbar](https://rollbar.com/), by [@coryvirok](https://github.com/coryvirok) 1270 | 1271 | 0.13.1 - September 12, 2013 1272 | --------------------------- 1273 | * Olark: added tests for empty emails, names and phone numbers 1274 | 1275 | 0.13.0 - September 11, 2013 1276 | --------------------------- 1277 | * converted all integrations and their tests to a cleaner format 1278 | * renamed all instances of "provider" to "integration" 1279 | * built integration list from their own `name` to avoid bugs 1280 | * changed `_providers` array to an `_integrations` map 1281 | 1282 | 0.12.2 - September 5, 2013 1283 | -------------------------- 1284 | * added [awe.sm](http://awe.sm) 1285 | 1286 | 0.12.1 - September 5, 2013 1287 | -------------------------- 1288 | * UserVoice: fix bug where some installations wouldn't show the tab 1289 | 1290 | 0.12.0 - September 4, 2013 1291 | -------------------------- 1292 | * Clicky: fixed custom tracking, added `pageview` 1293 | 1294 | 0.11.16 - September 3, 2013 1295 | --------------------------- 1296 | * updated `segmentio/new-date` for old browser support 1297 | * Woopra: fixed default pageview properties 1298 | * Intercom: cleaned up identify logic and tests 1299 | 1300 | 0.11.15 - September 2, 2013 1301 | --------------------------- 1302 | * pinned all dependencies 1303 | * added [Inspectlet](https://www.inspectlet.com) 1304 | * fixed storage options tests 1305 | * AdRoll: added custom data tracking 1306 | 1307 | 0.11.14 - August 30, 2013 1308 | ------------------------- 1309 | * bumped version of [`ianstormtaylor/is`](https://github.com/ianstormtaylor/is) for bugfix 1310 | 1311 | 0.11.13 - August 29, 2013 1312 | ------------------------- 1313 | * Spinnakr: added global variable for site id 1314 | * LeadLander: switched to non `document.write` version 1315 | * Customer.io: convert date objects to seconds 1316 | * fixed `is.function` bug in old browsers 1317 | 1318 | 0.11.12 - August 27, 2013 1319 | ------------------------- 1320 | * cleaned up core 1321 | * fixed breaking tests 1322 | * removed Bitdeli, by @jtuulos 1323 | * updated Woopra to use new tracker, by @billyvg 1324 | * added trak.io, by @msaspence 1325 | * added `createIntegration` interim method 1326 | * added more Lytics options, by @slindberg and @araddon 1327 | * added trait alias to trak.io 1328 | * added MouseStats, by @Koushan 1329 | * added Tapstream, by @adambard 1330 | * allow Mixpanel to name users by `username` 1331 | * allow GoSquared to name users by `email` or `username` 1332 | * make Google Analytics ignored referrers an array 1333 | * update Errorception cdn 1334 | 1335 | 0.11.11 - August 9, 2013 1336 | ------------------------ 1337 | * Added LeadLander 1338 | 1339 | 0.11.10 - July 12, 2013 1340 | ----------------------- 1341 | * Added cookieName to Mixpanel options - 0a53afd 1342 | 1343 | 0.11.9 - June 11, 2013 1344 | ---------------------- 1345 | * Added [Visual Website Optimizer](http://visualwebsiteoptimizer.com/) 1346 | 1347 | 0.11.8 - June 10, 2013 1348 | ---------------------- 1349 | * Intercom: added `group` support 1350 | 1351 | 0.11.7 - June 7, 2013 1352 | --------------------- 1353 | * Fix for cookie domains, now sets to subdomain friendly by default. 1354 | * Renaming bindAll -> bind-all 1355 | 1356 | 0.11.6 - June 6, 2013 1357 | --------------------- 1358 | * Added `group` support to Preact by [@azcoov](https://github.com/azcoov) 1359 | * Fixed `created` bug with userfox 1360 | * Changed to new Vero CDN URL 1361 | * Fixed bug when initializing unknown providers 1362 | * Added `options` object to `pageview` by [@debangpaliwal](https://github.com/devangpaliwal) 1363 | 1364 | 0.11.5 - June 3, 2013 1365 | --------------------- 1366 | * Adding segmentio/json temporarily, fixing json-fallback 1367 | 1368 | 0.11.4 - May 31, 2013 1369 | --------------------- 1370 | * Updated Intercom's library URL 1371 | 1372 | 0.11.3 - May 31, 2013 1373 | --------------------- 1374 | * Added trailing comma fix 1375 | 1376 | 0.11.2 - May 30, 2013 1377 | --------------------- 1378 | * Added fix for UserVoice displaying `'null'` 1379 | * Added `make clean` before running components (fixes json fallback) 1380 | 1381 | 0.11.1 - May 29, 2013 1382 | --------------------- 1383 | * Fixed bug with Google Analytics not tracking integer `value`s 1384 | 1385 | 0.11.0 - May 28, 2013 1386 | --------------------- 1387 | * Switched from cookie-ing to localStorage 1388 | 1389 | 0.10.6 - May 23, 2013 1390 | --------------------- 1391 | * Moved trait parsing logic to the global level 1392 | * Added [Improvely](http://www.improvely.com/) 1393 | * Added [Get Satisfaction](https://getsatisfaction.com/) 1394 | * Added a `$phone` alias for Mixpanel 1395 | * Added the ability to pass a function for the `event` to `trackLink` and `trackForm` 1396 | 1397 | 0.10.5 - May 22, 2013 1398 | --------------------- 1399 | * Added [Amplitude](https://amplitude.com/) support 1400 | * Fixed improperly parsed cookies 1401 | 1402 | 0.10.4 - May 17, 2013 1403 | --------------------- 1404 | * Fixed bug with Google Analytics being ready to soon 1405 | 1406 | 0.10.3 - May 15, 2013 1407 | --------------------- 1408 | * Added [Optimizely](https://www.optimizely.com) 1409 | 1410 | 0.10.2 - May 14, 2013 1411 | --------------------- 1412 | * Fixed handling of `increments` and `userHash` from `options.Intercom` 1413 | 1414 | 0.10.1 - May 14, 2013 1415 | --------------------- 1416 | * Added `identify` to SnapEngage integration 1417 | 1418 | 0.10.0 - May 9, 2013 1419 | -------------------- 1420 | * Added `group` method 1421 | 1422 | 0.9.18 - May 9, 2013 1423 | -------------------- 1424 | * Added [Preact](http://www.preact.io/) support by [@azcoov](https://github.com/azcoov) 1425 | 1426 | 0.9.17 - May 1, 2013 1427 | -------------------- 1428 | * Updated Keen to version 2.1.0 1429 | 1430 | 0.9.16 - April 30, 2013 1431 | ----------------------- 1432 | * Fixed bug affecting Pingdom users 1433 | 1434 | 0.9.15 - April 30, 2013 1435 | ----------------------- 1436 | * Added identify to UserVoice 1437 | 1438 | 0.9.14 - April 29, 2013 1439 | ----------------------- 1440 | * Fixing userfox integration to accept all traits not just signup_date 1441 | 1442 | 0.9.13 - April 29, 2013 1443 | ----------------------- 1444 | * Fixing ordering of ignore referrer option in Google Analytics 1445 | 1446 | 0.9.12 - April 27, 2013 1447 | ----------------------- 1448 | * Adding support for [userfox](https://www.userfox.com) 1449 | 1450 | 0.9.11 - April 26, 2013 1451 | ----------------------- 1452 | * Adding new ignoreReferrer option to Google Analytics provider 1453 | * Adding new showFeedbackTab option to BugHerd provider 1454 | * Updating UserVoice provider to work with their new snippet(s) 1455 | * Fixing Errorception window.onerror binding to be friendlier 1456 | 1457 | 0.9.10 - April 17, 2013 1458 | ----------------------- 1459 | * Adding url and title to mixpanel pageviews 1460 | * Addiung url and title to keen pageviews 1461 | 1462 | 0.9.9 - April 17, 2013 1463 | ---------------------- 1464 | * Fixed GoSquared relying on `document.body 1465 | 1466 | 0.9.8 - April 16, 2013 1467 | ---------------------- 1468 | * Adding support for Pingdom RUM 1469 | * Adding support for AdRoll 1470 | 1471 | 0.9.7 - April 16, 2013 1472 | ---------------------- 1473 | * Fixing LiveChat test 1474 | * Updating mixpanel snippet to wait for ready until script loads 1475 | * Adding full traits pulled in from identify. 1476 | 1477 | 0.9.6 - April 10, 2013 1478 | ---------------------- 1479 | * Renaming Provider.options to Provider.defaults 1480 | * Adding universal analytics support to Google Analytics 1481 | 1482 | 0.9.5 - April 10, 2013 1483 | ---------------------- 1484 | * Adding support for new Olark Javascript API functions, see #121 1485 | 1486 | 0.9.4 - April 4, 2013 1487 | --------------------- 1488 | * Fixing Uservoice integration 1489 | * Fixing ready tests. 1490 | * Adding lytics integration by [@araddon](https://github.com/araddon) 1491 | * Adding bower support by [@jede](https://github.com/jede) 1492 | 1493 | 0.9.3 - April 2, 2013 1494 | --------------------- 1495 | * Olark provider now only notifies the operator of track and pageview when the chat box is expanded. 1496 | 1497 | 0.9.2 - March 28, 2013 1498 | ---------------------- 1499 | * Qualaroo provider now prefers to identify with traits.email over a non-email userId --- makes the survey responses human readable. 1500 | 1501 | 0.9.1 - March 28, 2013 1502 | ---------------------- 1503 | * Woopra no longer tracks after each identify so that duplicate page views aren't generated. 1504 | 1505 | 0.9.0 - March 27, 2013 1506 | ---------------------- 1507 | * Changed default Keen IO settings to record all pageviews by default 1508 | * Removed Keen IO API Key option since that is no longer used for data "writes" to their API 1509 | * Renamed Keen IO projectId to projectToken to match their docs 1510 | 1511 | 0.8.13 - March 25, 2013 1512 | ----------------------- 1513 | * Added ability to pass variables into `intercomSettings` via `context.intercom` 1514 | 1515 | 0.8.12 - March 25, 2013 1516 | ----------------------- 1517 | * Added [Heap](https://heapanalytics.com) 1518 | 1519 | 0.8.11 - March 24, 2013 1520 | ----------------------- 1521 | * Removed [Storyberg](http://storyberg.com/2013/03/18/the-end.html), best of luck guys 1522 | 1523 | 0.8.10 - March 14, 2013 1524 | ------------------ 1525 | * Added fix for conversion of `company`'s `created` date 1526 | * Added extra tests for `trackForm` 1527 | * Fixing issue with ClickTale https bug 1528 | 1529 | 0.8.9 - March 13, 2013 1530 | ---------------------- 1531 | * Migrated to new Intercom Javascript API 1532 | * Removed un-used Intercom traits 1533 | * Fix bug in `trackForm` when using jQuery 1534 | 1535 | 0.8.8 - March 12, 2013 1536 | ---------------------- 1537 | * Added `userId` to Errorception metadata 1538 | * Made date parsing more lenient (ms & sec) for trait.created 1539 | 1540 | 0.8.7 - March 7, 2013 1541 | --------------------- 1542 | * Added [Qualaroo](https://qualaroo.com/) 1543 | * Fixed bug with Chartbeat and page load times 1544 | 1545 | 0.8.6 - March 7, 2013 1546 | --------------------- 1547 | * Fixed bug in `trackLink` reported by [@quirkyjack](https://github.com/quirkyjack) 1548 | * Fixed bug in ClickTale where it didn't create the ClickTaleDiv 1549 | 1550 | 0.8.5 - March 7, 2013 1551 | --------------------- 1552 | * Added [Storyberg](http://storyberg.com/) by [@kevinicus](https://github.com/kevinicus) 1553 | * Added [BugHerd](http://bugherd.com) 1554 | * Added [ClickTale](http://clicktale.com) 1555 | * Cleaned up extraneous `require`'s in many providers 1556 | 1557 | 0.8.4 - March 5, 2013 1558 | --------------------- 1559 | * Added support for strings for the `created` trait 1560 | * Added `load-date` for getting the page's load time 1561 | 1562 | 0.8.3 - March 4, 2013 1563 | --------------------- 1564 | * Added [Sentry](https://getsentry.com) 1565 | * Added initial pageview support to more providers 1566 | * Allowed HubSpot to recognize email `userId` 1567 | * Added support for DoubleClick [via Google Analytics](http://support.google.com/analytics/bin/answer.py?hl-en&answer-2444872) 1568 | 1569 | 0.8.2 - March 4, 2013 1570 | --------------------- 1571 | * Fixed bug in FoxMetrics provider 1572 | * Added queue for providers which don't support ready immediately. 1573 | 1574 | 0.8.1 - March 3, 2013 1575 | --------------------- 1576 | * Fixed bug in `trackForm` when submitted via jQuery 1577 | 1578 | 0.8.0 - March 1, 2013 1579 | --------------------- 1580 | * Added cookie-ing to keep identity and traits across page loads 1581 | * Added `identify` support for Clicky 1582 | * Added `identify` support for GoSquared 1583 | * Added `identify` support for Woopra 1584 | * Updated tracking for Usercycle 1585 | 1586 | 0.7.1 - February 26, 2013 1587 | ------------------------- 1588 | * Added Intercom companies by [@adrianrego](https://github.com/adrianrego) 1589 | * Added Intercom setting for use_counter 1590 | * Fixed Intercom traits passed without a created field 1591 | 1592 | 0.7.0 - February 25, 2013 1593 | ------------------------- 1594 | * Switched over to [Component](http://component.io/) 1595 | 1596 | 0.6.0 - February 7, 2013 1597 | ------------------------ 1598 | * Added `ready` method for binding to when analytics are initialized 1599 | * Added [UserVoice](https://www.uservoice.com) 1600 | * Added [Perfect Audience](https://www.perfectaudience.com/) 1601 | * Added [LiveChat](http://livechatinc.com) 1602 | * Fixed Intercom to allow multiple `identify` calls 1603 | 1604 | 0.5.1 - February 4, 2013 1605 | ------------------------ 1606 | * Merged in fix for Keen IO's branding 1607 | * Added fix to `utils.parseUrl()` field `pathname` in IE 1608 | 1609 | 0.5.0 - February 1, 2013 1610 | ------------------------ 1611 | * Added an `alias` method for merging two user's by ID 1612 | 1613 | 0.4.10 - January 30, 2013 1614 | ------------------------- 1615 | * Fixed multiple elements on `trackLink` and `trackForm` 1616 | * Fixed CrazyEgg `apiKey` to `accountNumber` 1617 | * Fixed Keen to Keen.io 1618 | 1619 | 1620 | 0.4.9 - January 29, 2013 1621 | ------------------------ 1622 | * Fixed `alias` and `extend` breaking on non-objects 1623 | 1624 | 0.4.8 - January 29, 2013 1625 | ------------------------ 1626 | * Fixed `trackForm` timeout by [@Plasma](https://github.com/Plasma) 1627 | 1628 | 0.4.7 - January 29, 2013 1629 | ------------------------ 1630 | * Added support for Mixpanel's [revenue](https://mixpanel.com/docs/people-analytics/javascript#track_charge) feature 1631 | * Added support for KISSmetrics' `"Billing Amount"` property for revenue 1632 | * Added support for `revenue` being passed to Google Analytics' `value` property 1633 | 1634 | 0.4.6 - January 28, 2013 1635 | ------------------------ 1636 | * Added automatic canonical URL support in Google Analytics 1637 | 1638 | 0.4.5 - January 25, 2013 1639 | ------------------------ 1640 | * Added Intercom widget setting 1641 | * Fixed Chartbeat from requiring `body` element to exist 1642 | 1643 | 0.4.4 - January 21, 2013 1644 | ------------------------ 1645 | * Added [Bitdeli](https://bitdeli.com/) by [@jtuulos](https://github.com/jtuulos) 1646 | * Added Mixpanel `$first_name` and `$last_name` aliases by [@dwradcliffe](https://github.com/dwradcliffe) 1647 | * Fixed Mixpanel `$last_seen` alias 1648 | * Added `parseUrl` util 1649 | * Moved GoSquared queue to snippet 1650 | 1651 | 0.4.3 - January 20, 2013 1652 | ------------------------ 1653 | * Added support for Errorception [user metadata](http://blog.errorception.com/2012/11/capture-custom-data-with-your-errors.html) 1654 | 1655 | 0.4.2 - January 18, 2013 1656 | ------------------------ 1657 | * Added option to use a properties function in `trackLink` and `trackForm` methods 1658 | 1659 | 0.4.1 - January 18, 2013 1660 | ------------------------ 1661 | * Added [Keen.io](http://keen.io/) by [@dkador](https://github.com/dkador) 1662 | * Added [Foxmetrics](http://foxmetrics.com/) by [@rawsoft](https://github.com/rawsoft) 1663 | * Updated Google Analytics to include noninteraction and value added by [@rantav](https://github.com/rantav) 1664 | * Moved to expect.js from chai for cross-broser support 1665 | 1666 | 0.4.0 - January 18, 2013 1667 | ------------------------ 1668 | * Renamed `trackClick` to `trackLink` 1669 | * Renamed `trackSubmit` to `trackForm` 1670 | 1671 | 0.3.8 - January 18, 2013 1672 | ------------------------ 1673 | * Fixed Clicky loading slowly 1674 | 1675 | 0.3.7 - January 17, 2013 1676 | ------------------------ 1677 | * Added [HitTail](http://hittail.com) 1678 | * Added [USERcycle](http://usercycle.com) 1679 | * Fixed Travis testing 1680 | 1681 | 0.3.6 - January 14, 2013 1682 | ------------------------ 1683 | * Added [SnapEngage](http://snapengage.com) 1684 | 1685 | 0.3.5 - January 14, 2013 1686 | ------------------------ 1687 | * Added `trackClick` and `trackForm` helpers 1688 | 1689 | 0.3.4 - January 13, 2013 1690 | ------------------------ 1691 | * Upgraded to Mixpanel 2.2 by [@loganfuller](https://github.com/loganfuller) 1692 | 1693 | 0.3.3 - January 11, 2013 1694 | ------------------------ 1695 | * Added [comScore Direct](http://direct.comscore.com) 1696 | 1697 | 0.3.2 - January 11, 2013 1698 | ------------------------ 1699 | * Added [Quantcast](http://quantcast.com) 1700 | * Fixed breaking issue on Clicky 1701 | * Updated Makefile for new providers 1702 | 1703 | 1704 | 0.3.1 - January 11, 2013 1705 | ------------------------ 1706 | * Added `label` and `category` support to Google Analytics 1707 | 1708 | 0.3.0 - January 9, 2013 1709 | ----------------------- 1710 | * Added [Gauges](http://get.gaug.es/) by [@bdougherty](https://github.com/bdougherty) 1711 | * Added [Vero](http://www.getvero.com/) 1712 | * Added optional `url` argument to `pageview` method 1713 | 1714 | 0.2.5 - January 8, 2013 1715 | ----------------------- 1716 | * Added [Errorception](http://errorception.com/) 1717 | * Added [Clicky](http://clicky.com/) 1718 | * Fixed IE 7 bug reported by [@yefremov](https://github.com/yefremov) 1719 | 1720 | 0.2.4 - January 3, 2013 1721 | ----------------------- 1722 | * Fixed GoSquared trailing comma by [@cmer](https://github.com/cmer) 1723 | 1724 | 0.2.3 - January 2, 2013 1725 | ----------------------- 1726 | * Added domain field to GA by [@starrhorne](https://github.com/starrhorne) 1727 | * Removed phantom install to get travis working 1728 | * Added window._gaq fix in initialize 1729 | 1730 | 0.2.2 - December 19, 2012 1731 | ------------------------- 1732 | * Added link query tag support for `ajs_uid` and `ajs_event` 1733 | * Added docco, uglify, and phantom to `devDependencies` by [@peleteiro](https://github.com/peleteiro) 1734 | 1735 | 0.2.1 - December 18, 2012 1736 | ------------------------- 1737 | * Added the `pageview` method for tracking virtual pageviews 1738 | * Added Travis-CI 1739 | * Fixed window level objects in customerio and gosquared 1740 | * Added for Intercom's "secure" mode by [@buger](https://github.com/buger) 1741 | * Removed root references 1742 | 1743 | 0.2.0 - December 16, 2012 1744 | ------------------------- 1745 | * Separated providers into separate files for easier maintenance 1746 | * Changed special `createdAt` trait to `created` for cleanliness 1747 | * Moved `utils` directly onto the analytics object 1748 | * Added `extend` and `alias` utils 1749 | * Added `settings` defaults for all providers 1750 | 1751 | 0.1.2 - December 14, 2012 1752 | ------------------------- 1753 | * Fixed bug with HubSpot calls pre-script load 1754 | * Upgraded sinon-chai to use [callWithMatch version](https://github.com/obmarg/sinon-chai/blob/f7aa7eccd6c0c18a3e1fc524a246a50c1a29c916/lib/sinon-chai.js) 1755 | * Added [Klaviyo](http://www.klaviyo.com/) by [@bialecki](https://github.com/bialecki) 1756 | * Added [HubSpot](http://www.hubspot.com/) by [@jessbrandi](https://github.com/jessbrandi) 1757 | * Added [GoSquared](https://www.gosquared.com/) by [@simontabor](https://github.com/simontabor) 1758 | 1759 | 0.1.1 - November 25, 2012 1760 | ------------------------- 1761 | * Added "Enhanced Link Attribution" for Google Analytics by [@nscott](https://github.com/nscott) 1762 | * Added "Site Speed Sample Rate" for Google Analytics by [@nscott](https://github.com/nscott) 1763 | 1764 | 0.1.0 - November 11, 2012 1765 | ------------------------- 1766 | * Added [Olark](http://www.olark.com/) 1767 | * Added terse `initialize` syntax 1768 | * Added tests for all providers 1769 | * Added README 1770 | -------------------------------------------------------------------------------- /test/analytics.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Analytics = require('../lib').constructor; 4 | var Facade = require('segmentio-facade'); 5 | var analytics = require('../lib'); 6 | var assert = require('proclaim'); 7 | var bind = require('component-event').bind; 8 | var createIntegration = require('@segment/analytics.js-integration'); 9 | var extend = require('@ndhoule/extend'); 10 | var type = require('component-type'); 11 | var jQuery = require('jquery'); 12 | var pageDefaults = require('../lib/pageDefaults'); 13 | var sinon = require('sinon'); 14 | var tick = require('next-tick'); 15 | var trigger = require('compat-trigger-event'); 16 | 17 | var Identify = Facade.Identify; 18 | var cookie = Analytics.cookie; 19 | var group = analytics.group(); 20 | var store = Analytics.store; 21 | var user = analytics.user(); 22 | 23 | describe('Analytics', function() { 24 | var analytics; 25 | var contextPage; 26 | var Test; 27 | var settings; 28 | 29 | beforeEach(function() { 30 | settings = { 31 | Test: { 32 | key: 'key' 33 | } 34 | }; 35 | 36 | contextPage = pageDefaults(); 37 | }); 38 | 39 | beforeEach(function() { 40 | analytics = new Analytics(); 41 | analytics.timeout(0); 42 | Test = createIntegration('Test'); 43 | }); 44 | 45 | afterEach(function() { 46 | user.reset(); 47 | group.reset(); 48 | user.anonymousId(null); 49 | // clear the hash 50 | // FIXME(ndhoule): Uhhh... causes Safari 9 to freak out. Maybe Karma issue? 51 | // if (window.history && window.history.pushState) { 52 | // window.history.pushState('', '', window.location.pathname); 53 | // } 54 | }); 55 | 56 | it('should setup an Integrations object', function() { 57 | assert(type(analytics.Integrations) === 'object'); 58 | }); 59 | 60 | it('should setup an _integrations object', function() { 61 | assert(type(analytics._integrations) === 'object'); 62 | }); 63 | 64 | it('should set a _readied state', function() { 65 | assert(analytics._readied === false); 66 | }); 67 | 68 | it('should set a default timeout', function() { 69 | analytics = new Analytics(); 70 | assert(analytics._timeout === 300); 71 | }); 72 | 73 | it('should set the _user for backwards compatibility', function() { 74 | assert(analytics._user === user); 75 | }); 76 | 77 | describe('#use', function() { 78 | it('should work', function(done) { 79 | analytics.use(function(singleton) { 80 | assert(analytics === singleton); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | 86 | describe('#addIntegration', function() { 87 | it('should add an integration', function() { 88 | analytics.addIntegration(Test); 89 | assert(analytics.Integrations.Test === Test); 90 | }); 91 | }); 92 | 93 | describe('#setAnonymousId', function() { 94 | it('should set the user\'s anonymous id', function() { 95 | var prev = analytics.user().anonymousId(); 96 | assert(prev.length === 36); 97 | analytics.setAnonymousId('new-id'); 98 | var curr = analytics.user().anonymousId(); 99 | assert(curr === 'new-id'); 100 | }); 101 | }); 102 | 103 | describe('#initialize', function() { 104 | beforeEach(function() { 105 | sinon.spy(user, 'load'); 106 | sinon.spy(group, 'load'); 107 | }); 108 | 109 | afterEach(function() { 110 | user.load.restore(); 111 | group.load.restore(); 112 | }); 113 | 114 | it('should gracefully handle integrations that fail to initialize', function() { 115 | Test.prototype.initialize = function() { throw new Error('Uh oh!'); }; 116 | var test = new Test(); 117 | analytics.use(Test); 118 | analytics.add(test); 119 | analytics.initialize(); 120 | assert(analytics.initialized); 121 | }); 122 | 123 | it('should store the names of integrations that did not initialize', function() { 124 | Test.prototype.initialize = function() { throw new Error('Uh oh!'); }; 125 | var test = new Test(); 126 | analytics.use(Test); 127 | analytics.add(test); 128 | analytics.initialize(); 129 | assert(analytics.failedInitializations.length === 1); 130 | assert(analytics.failedInitializations[0] === Test.prototype.name); 131 | }); 132 | 133 | it('should not process events for any integrations that failed to initialize', function() { 134 | Test.prototype.initialize = function() { throw new Error('Uh oh!'); }; 135 | Test.prototype.page = sinon.spy(); 136 | var test = new Test(); 137 | test.invoke = sinon.spy(); 138 | analytics.use(Test); 139 | analytics.add(test); 140 | analytics.initialize(); 141 | analytics.page('Test Page Event'); 142 | assert(test.invoke.notCalled); 143 | }); 144 | 145 | it('should still invoke the integrations .ready method', function(done) { 146 | Test.prototype.initialize = function() { throw new Error('Uh oh!'); }; 147 | var spy = sinon.spy(Test.prototype, 'ready'); 148 | var test = new Test(); 149 | analytics.use(Test); 150 | analytics.add(test); 151 | analytics.ready(function() { 152 | assert(spy.called); 153 | done(); 154 | }); 155 | analytics.initialize(); 156 | }); 157 | 158 | it('should not error without settings', function() { 159 | analytics.initialize(); 160 | }); 161 | 162 | it('should set options', function() { 163 | analytics._options = sinon.spy(); 164 | analytics.initialize({}, { option: true }); 165 | assert(analytics._options.calledWith({ option: true })); 166 | }); 167 | 168 | it('should reset analytics._readied to false', function() { 169 | analytics.addIntegration(Test); 170 | analytics._readied = true; 171 | analytics.initialize(settings); 172 | assert(!analytics._readied); 173 | }); 174 | 175 | it('should add integration instance', function(done) { 176 | Test.readyOnInitialize(); 177 | analytics.addIntegration(Test); 178 | analytics.ready(done); 179 | var test = new Test(settings.Test); 180 | analytics.add(test); 181 | analytics.initialize(); 182 | }); 183 | 184 | it('should set `.analytics` to self on integration', function(done) { 185 | Test.readyOnInitialize(); 186 | analytics.addIntegration(Test); 187 | analytics.ready(done); 188 | var test = new Test(settings.Test); 189 | analytics.add(test); 190 | analytics.initialize(); 191 | assert(test.analytics === analytics); 192 | }); 193 | 194 | it('should listen on integration ready events', function(done) { 195 | Test.readyOnInitialize(); 196 | analytics.addIntegration(Test); 197 | analytics.ready(done); 198 | analytics.initialize(settings); 199 | }); 200 | 201 | it('should still call ready with unknown integrations', function(done) { 202 | analytics.ready(done); 203 | analytics.initialize({ Unknown: { key: 'key' } }); 204 | }); 205 | 206 | it('should set analytics._readied to true', function(done) { 207 | analytics.ready(function() { 208 | assert(analytics._readied); 209 | done(); 210 | }); 211 | analytics.initialize(); 212 | }); 213 | 214 | it('should call #load on the user', function() { 215 | analytics.initialize(); 216 | assert(user.load.called); 217 | }); 218 | 219 | it('should call #load on the group', function() { 220 | analytics.initialize(); 221 | assert(group.load.called); 222 | }); 223 | 224 | it('should store enabled integrations', function(done) { 225 | Test.readyOnInitialize(); 226 | analytics.addIntegration(Test); 227 | analytics.ready(function() { 228 | assert(analytics._integrations.Test instanceof Test); 229 | done(); 230 | }); 231 | analytics.initialize(settings); 232 | }); 233 | 234 | it('should send settings to an integration', function(done) { 235 | Test = function(options) { 236 | assert.deepEqual(settings.Test, options); 237 | done(); 238 | }; 239 | Test.prototype.name = 'Test'; 240 | Test.prototype.once = Test.prototype.initialize = function() {}; 241 | analytics.addIntegration(Test); 242 | analytics.initialize(settings); 243 | }); 244 | 245 | it('should parse the query string', function() { 246 | sinon.stub(analytics, '_parseQuery'); 247 | analytics.initialize(); 248 | assert(analytics._parseQuery.called); 249 | }); 250 | 251 | it('should set initialized state', function() { 252 | analytics.initialize(); 253 | assert(analytics.initialized); 254 | }); 255 | 256 | it('should emit initialize', function(done) { 257 | analytics.once('initialize', function() { 258 | done(); 259 | }); 260 | analytics.initialize(); 261 | }); 262 | }); 263 | 264 | describe('#ready', function() { 265 | it('should push a handler on to the queue', function(done) { 266 | analytics.ready(done); 267 | analytics.emit('ready'); 268 | }); 269 | 270 | it('should callback on next tick when already ready', function(done) { 271 | analytics.ready(function() { 272 | var spy = sinon.spy(); 273 | analytics.ready(spy); 274 | assert(!spy.called); 275 | tick(function() { 276 | assert(spy.called); 277 | done(); 278 | }); 279 | }); 280 | analytics.initialize(); 281 | }); 282 | 283 | it('should emit ready', function(done) { 284 | analytics.once('ready', done); 285 | analytics.initialize(); 286 | }); 287 | 288 | it('should not error when passed a non-function', function() { 289 | analytics.ready('callback'); 290 | }); 291 | }); 292 | 293 | describe('#_invoke', function() { 294 | beforeEach(function(done) { 295 | Test.readyOnInitialize(); 296 | Test.prototype.invoke = sinon.spy(); 297 | analytics.addIntegration(Test); 298 | analytics.ready(done); 299 | analytics.initialize(settings); 300 | }); 301 | 302 | it('should invoke a method on integration with facade', function() { 303 | var a = new Identify({ userId: 'id', traits: { trait: true } }); 304 | analytics._invoke('identify', a); 305 | var b = Test.prototype.invoke.args[0][1]; 306 | assert(b === a); 307 | assert(b.userId() === 'id'); 308 | assert(b.traits().trait === true); 309 | }); 310 | 311 | it('shouldnt call a method when the `all` option is false', function() { 312 | var opts = { providers: { all: false } }; 313 | var facade = new Facade({ options: opts }); 314 | analytics._invoke('identify', facade); 315 | assert(!Test.prototype.invoke.called); 316 | }); 317 | 318 | it('shouldnt call a method when the integration option is false', function() { 319 | var opts = { providers: { Test: false } }; 320 | var facade = new Facade({ options: opts }); 321 | analytics._invoke('identify', facade); 322 | assert(!Test.prototype.invoke.called); 323 | }); 324 | 325 | it('should not crash when invoking integration fails', function() { 326 | Test.prototype.invoke = function() { throw new Error('Uh oh!'); }; 327 | analytics.track('Test Event'); 328 | }); 329 | 330 | it('should support .integrations to disable / select integrations', function() { 331 | var opts = { integrations: { Test: false } }; 332 | analytics.identify('123', {}, opts); 333 | assert(!Test.prototype.invoke.called); 334 | }); 335 | 336 | it('should emit "invoke" with facade', function(done) { 337 | var opts = { All: false }; 338 | var identify = new Identify({ options: opts }); 339 | analytics.on('invoke', function(msg) { 340 | assert(msg === identify); 341 | assert(msg.action() === 'identify'); 342 | done(); 343 | }); 344 | analytics._invoke('identify', identify); 345 | }); 346 | }); 347 | 348 | describe('#_options', function() { 349 | beforeEach(function() { 350 | sinon.stub(cookie, 'options'); 351 | sinon.stub(store, 'options'); 352 | sinon.stub(user, 'options'); 353 | sinon.stub(group, 'options'); 354 | }); 355 | 356 | afterEach(function() { 357 | cookie.options.restore(); 358 | store.options.restore(); 359 | user.options.restore(); 360 | group.options.restore(); 361 | }); 362 | 363 | it('should set cookie options', function() { 364 | analytics._options({ cookie: { option: true } }); 365 | assert(cookie.options.calledWith({ option: true })); 366 | }); 367 | 368 | it('should set store options', function() { 369 | analytics._options({ localStorage: { option: true } }); 370 | assert(store.options.calledWith({ option: true })); 371 | }); 372 | 373 | it('should set user options', function() { 374 | analytics._options({ user: { option: true } }); 375 | assert(user.options.calledWith({ option: true })); 376 | }); 377 | 378 | it('should set group options', function() { 379 | analytics._options({ group: { option: true } }); 380 | assert(group.options.calledWith({ option: true })); 381 | }); 382 | }); 383 | 384 | describe('#_parseQuery', function() { 385 | describe('user settings', function() { 386 | beforeEach(function() { 387 | sinon.spy(analytics, 'identify'); 388 | }); 389 | 390 | it('should parse `ajs_aid` and set anonymousId', function() { 391 | sinon.spy(user, 'anonymousId'); 392 | analytics._parseQuery('?ajs_aid=123'); 393 | assert(user.anonymousId.calledWith('123')); 394 | }); 395 | 396 | it('should parse `ajs_uid` and call identify', function() { 397 | analytics._parseQuery('?ajs_uid=123'); 398 | assert(analytics.identify.calledWith('123', {})); 399 | }); 400 | 401 | it('should include traits in identify', function() { 402 | analytics._parseQuery('?ajs_uid=123&ajs_trait_name=chris'); 403 | assert(analytics.identify.calledWith('123', { name: 'chris' })); 404 | }); 405 | }); 406 | 407 | describe('events', function() { 408 | beforeEach(function() { 409 | sinon.spy(analytics, 'track'); 410 | }); 411 | 412 | it('should parse `ajs_event` and call track', function() { 413 | analytics._parseQuery('?ajs_event=test'); 414 | assert(analytics.track.calledWith('test', {})); 415 | }); 416 | 417 | it('should include properties in track', function() { 418 | analytics._parseQuery('?ajs_event=Started+Trial&ajs_prop_plan=Silver'); 419 | assert(analytics.track.calledWith('Started Trial', { plan: 'Silver' })); 420 | }); 421 | }); 422 | }); 423 | 424 | describe('#_timeout', function() { 425 | it('should set the timeout for callbacks', function() { 426 | analytics.timeout(500); 427 | assert(analytics._timeout === 500); 428 | }); 429 | }); 430 | 431 | describe('#_callback', function() { 432 | it('should callback after a timeout', function(done) { 433 | var spy = sinon.spy(); 434 | analytics._callback(spy); 435 | assert(!spy.called); 436 | tick(function() { 437 | assert(spy.called); 438 | done(); 439 | }); 440 | }); 441 | }); 442 | 443 | describe('#page', function() { 444 | var head = document.getElementsByTagName('head')[0]; 445 | var defaults; 446 | 447 | beforeEach(function() { 448 | defaults = { 449 | path: window.location.pathname, 450 | referrer: document.referrer, 451 | title: document.title, 452 | url: window.location.href, 453 | search: window.location.search 454 | }; 455 | sinon.spy(analytics, '_invoke'); 456 | }); 457 | 458 | it('should call #_invoke', function() { 459 | analytics.page(); 460 | assert(analytics._invoke.calledWith('page')); 461 | }); 462 | 463 | it('should default .anonymousId', function() { 464 | analytics.page(); 465 | var msg = analytics._invoke.args[0][1]; 466 | assert(msg.anonymousId().length === 36); 467 | }); 468 | 469 | it('should override .anonymousId', function() { 470 | analytics.page('category', 'name', {}, { anonymousId: 'anon-id' }); 471 | var msg = analytics._invoke.args[0][1]; 472 | assert(msg.anonymousId() === 'anon-id'); 473 | }); 474 | 475 | it('should call #_invoke with Page instance', function() { 476 | analytics.page(); 477 | var page = analytics._invoke.args[0][1]; 478 | assert(page.action() === 'page'); 479 | }); 480 | 481 | it('should default .url to .location.href', function() { 482 | analytics.page(); 483 | var page = analytics._invoke.args[0][1]; 484 | assert(page.properties().url === window.location.href); 485 | }); 486 | 487 | it('should respect canonical', function() { 488 | var el = document.createElement('link'); 489 | el.rel = 'canonical'; 490 | el.href = 'baz.com'; 491 | head.appendChild(el); 492 | analytics.page(); 493 | var page = analytics._invoke.args[0][1]; 494 | assert(page.properties().url === 'baz.com' + window.location.search); 495 | el.parentNode.removeChild(el); 496 | }); 497 | 498 | it('should accept (category, name, properties, options, callback)', function(done) { 499 | defaults.category = 'category'; 500 | defaults.name = 'name'; 501 | analytics.page('category', 'name', {}, {}, function() { 502 | var page = analytics._invoke.args[0][1]; 503 | assert(page.category() === 'category'); 504 | assert(page.name() === 'name'); 505 | assert(typeof page.properties() === 'object'); 506 | assert(typeof page.options() === 'object'); 507 | done(); 508 | }); 509 | }); 510 | 511 | it('should accept (category, name, properties, callback)', function(done) { 512 | defaults.category = 'category'; 513 | defaults.name = 'name'; 514 | analytics.page('category', 'name', {}, function() { 515 | var page = analytics._invoke.args[0][1]; 516 | assert(page.category() === 'category'); 517 | assert(page.name() === 'name'); 518 | assert(typeof page.properties() === 'object'); 519 | done(); 520 | }); 521 | }); 522 | 523 | it('should accept (category, name, callback)', function(done) { 524 | defaults.category = 'category'; 525 | defaults.name = 'name'; 526 | analytics.page('category', 'name', function() { 527 | var page = analytics._invoke.args[0][1]; 528 | assert(page.category() === 'category'); 529 | assert(page.name() === 'name'); 530 | done(); 531 | }); 532 | }); 533 | 534 | it('should accept (name, properties, options, callback)', function(done) { 535 | defaults.name = 'name'; 536 | analytics.page('name', {}, {}, function() { 537 | var page = analytics._invoke.args[0][1]; 538 | assert(page.name() === 'name'); 539 | assert(typeof page.properties() === 'object'); 540 | assert(typeof page.options() === 'object'); 541 | done(); 542 | }); 543 | }); 544 | 545 | it('should accept (name, properties, callback)', function(done) { 546 | defaults.name = 'name'; 547 | analytics.page('name', {}, function() { 548 | var page = analytics._invoke.args[0][1]; 549 | assert(page.name() === 'name'); 550 | assert(typeof page.properties() === 'object'); 551 | done(); 552 | }); 553 | }); 554 | 555 | it('should accept (name, callback)', function(done) { 556 | defaults.name = 'name'; 557 | analytics.page('name', function() { 558 | var page = analytics._invoke.args[0][1]; 559 | assert(page.name() === 'name'); 560 | done(); 561 | }); 562 | }); 563 | 564 | it('should accept (properties, options, callback)', function(done) { 565 | analytics.page({}, {}, function() { 566 | var page = analytics._invoke.args[0][1]; 567 | assert(page.category() === null); 568 | assert(page.name() === null); 569 | assert(typeof page.properties() === 'object'); 570 | assert(typeof page.options() === 'object'); 571 | done(); 572 | }); 573 | }); 574 | 575 | it('should accept (properties, callback)', function(done) { 576 | analytics.page({}, function() { 577 | var page = analytics._invoke.args[0][1]; 578 | assert(page.category() === null); 579 | assert(page.name() === null); 580 | assert(typeof page.options() === 'object'); 581 | done(); 582 | }); 583 | }); 584 | 585 | it('should back properties with defaults', function() { 586 | defaults.property = true; 587 | analytics.page({ property: true }); 588 | var page = analytics._invoke.args[0][1]; 589 | assert.deepEqual(page.properties(), defaults); 590 | }); 591 | 592 | it('should accept top level option .timestamp', function() { 593 | var date = new Date(); 594 | analytics.page({ prop: true }, { timestamp: date }); 595 | var page = analytics._invoke.args[0][1]; 596 | assert.deepEqual(page.timestamp(), date); 597 | }); 598 | 599 | it('should accept top level option .integrations', function() { 600 | analytics.page({ prop: true }, { integrations: { AdRoll: { opt: true } } }); 601 | var page = analytics._invoke.args[0][1]; 602 | assert.deepEqual(page.options('AdRoll'), { opt: true }); 603 | }); 604 | 605 | it('should accept top level option .context', function() { 606 | var app = { name: 'segment' }; 607 | analytics.page({ prop: true }, { context: { app: app } }); 608 | var page = analytics._invoke.args[0][1]; 609 | assert.deepEqual(app, page.obj.context.app); 610 | }); 611 | 612 | it('should accept top level option .anonymousId', function() { 613 | analytics.page({ prop: true }, { anonymousId: 'id' }); 614 | var page = analytics._invoke.args[0][1]; 615 | assert(page.obj.anonymousId === 'id'); 616 | }); 617 | 618 | it('should include context.page', function() { 619 | analytics.page(); 620 | var page = analytics._invoke.args[0][1]; 621 | assert.deepEqual(page.context(), { page: defaults }); 622 | }); 623 | 624 | it('should accept context.traits', function() { 625 | analytics.page({ prop: true }, { traits: { trait: true } }); 626 | var page = analytics._invoke.args[0][1]; 627 | assert.deepEqual(page.context(), { 628 | page: defaults, 629 | traits: { trait: true } 630 | }); 631 | }); 632 | 633 | it('should emit page', function(done) { 634 | analytics.once('page', function(category, name, props, opts) { 635 | assert(category === 'category'); 636 | assert(name === 'name'); 637 | assert.deepEqual(opts, { context: { page: defaults } }); 638 | assert.deepEqual(props, extend(defaults, { category: 'category', name: 'name' })); 639 | done(); 640 | }); 641 | analytics.page('category', 'name', {}, {}); 642 | }); 643 | }); 644 | 645 | describe('#pageview', function() { 646 | beforeEach(function() { 647 | analytics.initialize(); 648 | sinon.spy(analytics, 'page'); 649 | }); 650 | 651 | it('should call #page with a path', function() { 652 | analytics.pageview('/path'); 653 | assert(analytics.page.calledWith({ path: '/path' })); 654 | }); 655 | }); 656 | 657 | describe('#identify', function() { 658 | beforeEach(function() { 659 | sinon.spy(analytics, '_invoke'); 660 | sinon.spy(user, 'identify'); 661 | }); 662 | 663 | afterEach(function() { 664 | user.identify.restore(); 665 | }); 666 | 667 | it('should call #_invoke', function() { 668 | analytics.identify(); 669 | assert(analytics._invoke.calledWith('identify')); 670 | }); 671 | 672 | it('should default .anonymousId', function() { 673 | analytics.identify('user-id'); 674 | var msg = analytics._invoke.args[0][1]; 675 | assert(msg.anonymousId().length === 36); 676 | }); 677 | 678 | it('should override .anonymousId', function() { 679 | analytics.identify('user-id', {}, { anonymousId: 'anon-id' }); 680 | var msg = analytics._invoke.args[0][1]; 681 | assert(msg.anonymousId() === 'anon-id'); 682 | }); 683 | 684 | it('should call #_invoke with Identify', function() { 685 | analytics.identify(); 686 | var identify = analytics._invoke.getCall(0).args[1]; 687 | assert(identify.action() === 'identify'); 688 | }); 689 | 690 | it('should accept (id, traits, options, callback)', function(done) { 691 | analytics.identify('id', {}, {}, function() { 692 | var identify = analytics._invoke.getCall(0).args[1]; 693 | assert(identify.userId() === 'id'); 694 | assert(typeof identify.traits() === 'object'); 695 | assert(typeof identify.options() === 'object'); 696 | done(); 697 | }); 698 | }); 699 | 700 | it('should accept (id, traits, callback)', function(done) { 701 | analytics.identify('id', { trait: true }, function() { 702 | var identify = analytics._invoke.getCall(0).args[1]; 703 | assert(identify.userId() === 'id'); 704 | assert(typeof identify.traits() === 'object'); 705 | done(); 706 | }); 707 | }); 708 | 709 | it('should accept (id, callback)', function(done) { 710 | analytics.identify('id', function() { 711 | var identify = analytics._invoke.getCall(0).args[1]; 712 | assert(identify.action() === 'identify'); 713 | assert(identify.userId() === 'id'); 714 | done(); 715 | }); 716 | }); 717 | 718 | it('should accept (traits, options, callback)', function(done) { 719 | analytics.identify({}, {}, function() { 720 | var identify = analytics._invoke.getCall(0).args[1]; 721 | assert(typeof identify.traits() === 'object'); 722 | assert(typeof identify.options() === 'object'); 723 | done(); 724 | }); 725 | }); 726 | 727 | it('should accept (traits, callback)', function(done) { 728 | analytics.identify({}, function() { 729 | var identify = analytics._invoke.getCall(0).args[1]; 730 | assert(typeof identify.traits() === 'object'); 731 | done(); 732 | }); 733 | }); 734 | 735 | it('should identify the user', function() { 736 | analytics.identify('id', { trait: true }); 737 | assert(user.identify.calledWith('id', { trait: true })); 738 | }); 739 | 740 | it('should back traits with stored traits', function() { 741 | user.traits({ one: 1 }); 742 | user.save(); 743 | analytics.identify('id', { two: 2 }); 744 | var call = analytics._invoke.getCall(0); 745 | var identify = call.args[1]; 746 | assert(call.args[0] === 'identify'); 747 | assert(identify.userId() === 'id'); 748 | assert(identify.traits().one === 1); 749 | assert(identify.traits().two === 2); 750 | }); 751 | 752 | it('should emit identify', function(done) { 753 | analytics.once('identify', function(id, traits, options) { 754 | assert(id === 'id'); 755 | assert.deepEqual(traits, { a: 1 }); 756 | assert.deepEqual(options, { b: 2 }); 757 | done(); 758 | }); 759 | analytics.identify('id', { a: 1 }, { b: 2 }); 760 | }); 761 | 762 | it('should parse a created string into a date', function() { 763 | var date = new Date(); 764 | var string = date.getTime().toString(); 765 | analytics.identify({ created: string }); 766 | var created = analytics._invoke.args[0][1].created(); 767 | assert(type(created) === 'date'); 768 | assert(created.getTime() === date.getTime()); 769 | }); 770 | 771 | it('should parse created milliseconds into a date', function() { 772 | var date = new Date(); 773 | var milliseconds = date.getTime(); 774 | analytics.identify({ created: milliseconds }); 775 | var created = analytics._invoke.args[0][1].created(); 776 | assert(type(created) === 'date'); 777 | assert(created.getTime() === milliseconds); 778 | }); 779 | 780 | it('should parse created seconds into a date', function() { 781 | var date = new Date(); 782 | var seconds = Math.floor(date.getTime() / 1000); 783 | analytics.identify({ created: seconds }); 784 | var identify = analytics._invoke.args[0][1]; 785 | var created = identify.created(); 786 | assert(type(created) === 'date'); 787 | assert(created.getTime() === seconds * 1000); 788 | }); 789 | 790 | it('should parse a company created string into a date', function() { 791 | var date = new Date(); 792 | var string = date.getTime() + ''; 793 | analytics.identify({ company: { created: string } }); 794 | var identify = analytics._invoke.args[0][1]; 795 | var created = identify.companyCreated(); 796 | assert(type(created) === 'date'); 797 | assert(created.getTime() === date.getTime()); 798 | }); 799 | 800 | it('should parse company created milliseconds into a date', function() { 801 | var date = new Date(); 802 | var milliseconds = date.getTime(); 803 | analytics.identify({ company: { created: milliseconds } }); 804 | var identify = analytics._invoke.args[0][1]; 805 | var created = identify.companyCreated(); 806 | assert(type(created) === 'date'); 807 | assert(created.getTime() === milliseconds); 808 | }); 809 | 810 | it('should parse company created seconds into a date', function() { 811 | var date = new Date(); 812 | var seconds = Math.floor(date.getTime() / 1000); 813 | analytics.identify({ company: { created: seconds } }); 814 | var identify = analytics._invoke.args[0][1]; 815 | var created = identify.companyCreated(); 816 | assert(type(created) === 'date'); 817 | assert(created.getTime() === seconds * 1000); 818 | }); 819 | 820 | it('should accept top level option .timestamp', function() { 821 | var date = new Date(); 822 | analytics.identify(1, { trait: true }, { timestamp: date }); 823 | var identify = analytics._invoke.args[0][1]; 824 | assert.deepEqual(identify.timestamp(), date); 825 | }); 826 | 827 | it('should accept top level option .integrations', function() { 828 | analytics.identify(1, { trait: true }, { integrations: { AdRoll: { opt: true } } }); 829 | var identify = analytics._invoke.args[0][1]; 830 | assert.deepEqual({ opt: true }, identify.options('AdRoll')); 831 | }); 832 | 833 | it('should accept top level option .context', function() { 834 | analytics.identify(1, { trait: true }, { context: { app: { name: 'segment' } } }); 835 | var identify = analytics._invoke.args[0][1]; 836 | assert.deepEqual(identify.obj.context.app, { name: 'segment' }); 837 | }); 838 | 839 | it('should include context.page', function() { 840 | analytics.identify(1); 841 | var identify = analytics._invoke.args[0][1]; 842 | assert.deepEqual(identify.context(), { page: contextPage }); 843 | }); 844 | 845 | it('should accept context.traits', function() { 846 | analytics.identify(1, { trait: 1 }, { traits: { trait: true } }); 847 | var identify = analytics._invoke.args[0][1]; 848 | assert.deepEqual(identify.traits(), { trait: 1, id: 1 }); 849 | assert.deepEqual(identify.context(), { 850 | page: contextPage, 851 | traits: { trait: true } 852 | }); 853 | }); 854 | }); 855 | 856 | describe('#user', function() { 857 | it('should return the user singleton', function() { 858 | assert(analytics.user() === user); 859 | }); 860 | }); 861 | 862 | describe('#group', function() { 863 | beforeEach(function() { 864 | sinon.spy(analytics, '_invoke'); 865 | sinon.spy(group, 'identify'); 866 | }); 867 | 868 | afterEach(function() { 869 | group.identify.restore(); 870 | }); 871 | 872 | it('should return the group singleton', function() { 873 | assert(analytics.group() === group); 874 | }); 875 | 876 | it('should call #_invoke', function() { 877 | analytics.group('id'); 878 | assert(analytics._invoke.calledWith('group')); 879 | }); 880 | 881 | it('should default .anonymousId', function() { 882 | analytics.group('group-id'); 883 | var msg = analytics._invoke.args[0][1]; 884 | assert(msg.anonymousId().length === 36); 885 | }); 886 | 887 | it('should override .anonymousId', function() { 888 | analytics.group('group-id', {}, { anonymousId: 'anon-id' }); 889 | var msg = analytics._invoke.args[0][1]; 890 | assert(msg.anonymousId() === 'anon-id'); 891 | }); 892 | 893 | it('should call #_invoke with group facade instance', function() { 894 | analytics.group('id'); 895 | var group = analytics._invoke.args[0][1]; 896 | assert(group.action() === 'group'); 897 | }); 898 | 899 | it('should accept (id, properties, options, callback)', function(done) { 900 | analytics.group('id', {}, {}, function() { 901 | var group = analytics._invoke.args[0][1]; 902 | assert(group.groupId() === 'id'); 903 | assert(typeof group.properties() === 'object'); 904 | assert(typeof group.options() === 'object'); 905 | done(); 906 | }); 907 | }); 908 | 909 | it('should accept (id, properties, callback)', function(done) { 910 | analytics.group('id', {}, function() { 911 | var group = analytics._invoke.args[0][1]; 912 | assert(group.groupId() === 'id'); 913 | assert(typeof group.properties() === 'object'); 914 | done(); 915 | }); 916 | }); 917 | 918 | it('should accept (id, callback)', function(done) { 919 | analytics.group('id', function() { 920 | var group = analytics._invoke.args[0][1]; 921 | assert(group.groupId() === 'id'); 922 | done(); 923 | }); 924 | }); 925 | 926 | it('should accept (properties, options, callback)', function(done) { 927 | analytics.group({}, {}, function() { 928 | var group = analytics._invoke.args[0][1]; 929 | assert(typeof group.properties() === 'object'); 930 | assert(typeof group.options() === 'object'); 931 | done(); 932 | }); 933 | }); 934 | 935 | it('should accept (properties, callback)', function(done) { 936 | analytics.group({}, function() { 937 | var group = analytics._invoke.args[0][1]; 938 | assert(typeof group.properties() === 'object'); 939 | done(); 940 | }); 941 | }); 942 | 943 | it('should call #identify on the group', function() { 944 | analytics.group('id', { property: true }); 945 | assert(group.identify.calledWith('id', { property: true })); 946 | }); 947 | 948 | it('should back properties with stored properties', function() { 949 | group.properties({ one: 1 }); 950 | group.save(); 951 | analytics.group('id', { two: 2 }); 952 | var g = analytics._invoke.args[0][1]; 953 | assert(g.groupId() === 'id'); 954 | assert(typeof g.properties() === 'object'); 955 | assert(g.properties().one === 1); 956 | assert(g.properties().two === 2); 957 | }); 958 | 959 | it('should emit group', function(done) { 960 | analytics.once('group', function(groupId, traits, options) { 961 | assert(groupId === 'id'); 962 | assert.deepEqual(traits, { a: 1 }); 963 | assert.deepEqual(options, { b: 2 }); 964 | done(); 965 | }); 966 | analytics.group('id', { a: 1 }, { b: 2 }); 967 | }); 968 | 969 | it('should parse a created string into a date', function() { 970 | var date = new Date(); 971 | var string = date.getTime().toString(); 972 | analytics.group({ created: string }); 973 | var g = analytics._invoke.args[0][1]; 974 | var created = g.created(); 975 | assert(type(created) === 'date'); 976 | assert(created.getTime() === date.getTime()); 977 | }); 978 | 979 | it('should parse created milliseconds into a date', function() { 980 | var date = new Date(); 981 | var milliseconds = date.getTime(); 982 | analytics.group({ created: milliseconds }); 983 | var g = analytics._invoke.args[0][1]; 984 | var created = g.created(); 985 | assert(type(created) === 'date'); 986 | assert(created.getTime() === milliseconds); 987 | }); 988 | 989 | it('should parse created seconds into a date', function() { 990 | var date = new Date(); 991 | var seconds = Math.floor(date.getTime() / 1000); 992 | analytics.group({ created: seconds }); 993 | var g = analytics._invoke.args[0][1]; 994 | var created = g.created(); 995 | assert(type(created) === 'date'); 996 | assert(created.getTime() === seconds * 1000); 997 | }); 998 | 999 | it('should accept top level option .timestamp', function() { 1000 | var date = new Date(); 1001 | analytics.group(1, { trait: true }, { timestamp: date }); 1002 | var group = analytics._invoke.args[0][1]; 1003 | assert.deepEqual(group.timestamp(), date); 1004 | }); 1005 | 1006 | it('should accept top level option .integrations', function() { 1007 | analytics.group(1, { trait: true }, { integrations: { AdRoll: { opt: true } } }); 1008 | var group = analytics._invoke.args[0][1]; 1009 | assert.deepEqual(group.options('AdRoll'), { opt: true }); 1010 | }); 1011 | 1012 | it('should accept top level option .context', function() { 1013 | var app = { name: 'segment' }; 1014 | analytics.group(1, { trait: true }, { context: { app: app } }); 1015 | var group = analytics._invoke.args[0][1]; 1016 | assert.deepEqual(group.obj.context.app, app); 1017 | }); 1018 | 1019 | it('should include context.page', function() { 1020 | analytics.group(1); 1021 | var group = analytics._invoke.args[0][1]; 1022 | assert.deepEqual(group.context(), { page: contextPage }); 1023 | }); 1024 | 1025 | it('should accept context.traits', function() { 1026 | analytics.group(1, { trait: 1 }, { traits: { trait: true } }); 1027 | var group = analytics._invoke.args[0][1]; 1028 | assert.deepEqual(group.traits(), { trait: 1, id: 1 }); 1029 | assert.deepEqual(group.context(), { 1030 | page: contextPage, 1031 | traits: { trait: true } 1032 | }); 1033 | }); 1034 | }); 1035 | 1036 | describe('#track', function() { 1037 | beforeEach(function() { 1038 | sinon.spy(analytics, '_invoke'); 1039 | }); 1040 | 1041 | it('should call #_invoke', function() { 1042 | analytics.track(); 1043 | assert(analytics._invoke.calledWith('track')); 1044 | }); 1045 | 1046 | it('should default .anonymousId', function() { 1047 | analytics.track(); 1048 | var msg = analytics._invoke.args[0][1]; 1049 | assert(msg.anonymousId().length === 36); 1050 | }); 1051 | 1052 | it('should override .anonymousId', function() { 1053 | analytics.track('event', {}, { anonymousId: 'anon-id' }); 1054 | var msg = analytics._invoke.args[0][1]; 1055 | assert(msg.anonymousId() === 'anon-id'); 1056 | }); 1057 | 1058 | it('should transform arguments into Track', function() { 1059 | analytics.track(); 1060 | var track = analytics._invoke.getCall(0).args[1]; 1061 | assert(track.action() === 'track'); 1062 | }); 1063 | 1064 | it('should accept (event, properties, options, callback)', function(done) { 1065 | analytics.track('event', {}, {}, function() { 1066 | var track = analytics._invoke.args[0][1]; 1067 | assert(track.event() === 'event'); 1068 | assert(typeof track.properties() === 'object'); 1069 | assert(typeof track.options() === 'object'); 1070 | done(); 1071 | }); 1072 | }); 1073 | 1074 | it('should accept (event, properties, callback)', function(done) { 1075 | analytics.track('event', {}, function() { 1076 | var track = analytics._invoke.args[0][1]; 1077 | assert(track.event() === 'event'); 1078 | assert(typeof track.properties() === 'object'); 1079 | done(); 1080 | }); 1081 | }); 1082 | 1083 | it('should accept (event, callback)', function(done) { 1084 | analytics.track('event', function() { 1085 | var track = analytics._invoke.args[0][1]; 1086 | assert(track.event() === 'event'); 1087 | done(); 1088 | }); 1089 | }); 1090 | 1091 | it('should emit track', function(done) { 1092 | analytics.once('track', function(event, properties, options) { 1093 | assert(event === 'event'); 1094 | assert.deepEqual(properties, { a: 1 }); 1095 | assert.deepEqual(options, { b: 2 }); 1096 | done(); 1097 | }); 1098 | analytics.track('event', { a: 1 }, { b: 2 }); 1099 | }); 1100 | 1101 | it('should safely convert ISO dates to date objects', function() { 1102 | var date = new Date(Date.UTC(2013, 9, 5)); 1103 | analytics.track('event', { 1104 | date: '2013-10-05T00:00:00.000Z', 1105 | nonDate: '2013' 1106 | }); 1107 | var track = analytics._invoke.args[0][1]; 1108 | assert(track.properties().date.getTime() === date.getTime()); 1109 | assert(track.properties().nonDate === '2013'); 1110 | }); 1111 | 1112 | it('should accept top level option .timestamp', function() { 1113 | var date = new Date(); 1114 | analytics.track('event', { prop: true }, { timestamp: date }); 1115 | var track = analytics._invoke.args[0][1]; 1116 | assert.deepEqual(date, track.timestamp()); 1117 | }); 1118 | 1119 | it('should accept top level option .integrations', function() { 1120 | analytics.track('event', { prop: true }, { integrations: { AdRoll: { opt: true } } }); 1121 | var track = analytics._invoke.args[0][1]; 1122 | assert.deepEqual({ opt: true }, track.options('AdRoll')); 1123 | }); 1124 | 1125 | it('should accept top level option .context', function() { 1126 | var app = { name: 'segment' }; 1127 | analytics.track('event', { prop: true }, { context: { app: app } }); 1128 | var track = analytics._invoke.args[0][1]; 1129 | assert.deepEqual(app, track.obj.context.app); 1130 | }); 1131 | 1132 | it('should call #_invoke for Segment if the event is disabled', function() { 1133 | analytics.options.plan = { 1134 | track: { 1135 | event: { enabled: false } 1136 | } 1137 | }; 1138 | analytics.track('event'); 1139 | assert(analytics._invoke.called); 1140 | var track = analytics._invoke.args[0][1]; 1141 | assert.deepEqual({ All: false, 'Segment.io': true }, track.obj.integrations); 1142 | }); 1143 | 1144 | it('should call #_invoke if the event is enabled', function() { 1145 | analytics.options.plan = { 1146 | track: { 1147 | event: { enabled: true } 1148 | } 1149 | }; 1150 | analytics.track('event'); 1151 | assert(analytics._invoke.called); 1152 | }); 1153 | 1154 | it('should call the callback even if the event is disabled', function(done) { 1155 | analytics.options.plan = { 1156 | track: { 1157 | event: { enabled: false } 1158 | } 1159 | }; 1160 | assert(!analytics._invoke.called); 1161 | analytics.track('event', {}, {}, function() { 1162 | done(); 1163 | }); 1164 | }); 1165 | 1166 | it('should default .integrations to plan.integrations', function() { 1167 | analytics.options.plan = { 1168 | track: { 1169 | event: { 1170 | integrations: { All: true } 1171 | } 1172 | } 1173 | }; 1174 | 1175 | analytics.track('event', {}, { integrations: { Segment: true } }); 1176 | var msg = analytics._invoke.args[0][1]; 1177 | assert(msg.event() === 'event'); 1178 | assert.deepEqual(msg.integrations(), { All: true, Segment: true }); 1179 | }); 1180 | 1181 | it('should call #_invoke if new events are enabled', function() { 1182 | analytics.options.plan = { 1183 | track: { 1184 | __default: { enabled: true } 1185 | } 1186 | }; 1187 | analytics.track('event'); 1188 | assert(analytics._invoke.called); 1189 | var track = analytics._invoke.args[0][1]; 1190 | assert.deepEqual({}, track.obj.integrations); 1191 | }); 1192 | 1193 | it('should call #_invoke for Segment if new events are disabled', function() { 1194 | analytics.options.plan = { 1195 | track: { 1196 | __default: { enabled: false } 1197 | } 1198 | }; 1199 | analytics.track('even'); 1200 | assert(analytics._invoke.called); 1201 | var track = analytics._invoke.args[0][1]; 1202 | assert.deepEqual({ All: false, 'Segment.io': true }, track.obj.integrations); 1203 | }); 1204 | 1205 | it('should use the event plan if it exists and ignore defaults', function() { 1206 | analytics.options.plan = { 1207 | track: { 1208 | event: { enabled: true }, 1209 | __default: { enabled: false } 1210 | } 1211 | }; 1212 | analytics.track('event'); 1213 | assert(analytics._invoke.called); 1214 | var track = analytics._invoke.args[0][1]; 1215 | assert.deepEqual({}, track.obj.integrations); 1216 | }); 1217 | 1218 | it('should merge the event plan if it exists and ignore defaults', function() { 1219 | analytics.options.plan = { 1220 | track: { 1221 | event: { enabled: true, integrations: { Mixpanel: false } }, 1222 | __default: { enabled: false } 1223 | } 1224 | }; 1225 | analytics.track('event'); 1226 | assert(analytics._invoke.called); 1227 | var track = analytics._invoke.args[0][1]; 1228 | assert.deepEqual({ Mixpanel: false }, track.obj.integrations); 1229 | }); 1230 | 1231 | it('should not set ctx.integrations if plan.integrations is empty', function() { 1232 | analytics.options.plan = { track: { event: {} } }; 1233 | analytics.track('event', {}, { campaign: {} }); 1234 | var msg = analytics._invoke.args[0][1]; 1235 | assert.deepEqual({}, msg.proxy('context.campaign')); 1236 | }); 1237 | 1238 | it('should include context.page', function() { 1239 | analytics.track('event'); 1240 | var track = analytics._invoke.args[0][1]; 1241 | assert.deepEqual(track.context(), { page: contextPage }); 1242 | }); 1243 | 1244 | it('should accept context.traits', function() { 1245 | analytics.track('event', { prop: 1 }, { traits: { trait: true } }); 1246 | var track = analytics._invoke.args[0][1]; 1247 | assert.deepEqual(track.properties(), { prop: 1 }); 1248 | assert.deepEqual(track.context(), { 1249 | page: contextPage, 1250 | traits: { trait: true } 1251 | }); 1252 | }); 1253 | }); 1254 | 1255 | describe('#trackLink', function() { 1256 | var link; 1257 | var wrap; 1258 | var svg; 1259 | 1260 | beforeEach(function() { 1261 | // FIXME: IE8 doesn't have createElementNS. 1262 | if (!document.createElementNS) return; 1263 | wrap = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 1264 | svg = document.createElementNS('http://www.w3.org/2000/svg', 'a'); 1265 | wrap.appendChild(svg); 1266 | document.body.appendChild(wrap); 1267 | }); 1268 | 1269 | beforeEach(function() { 1270 | sinon.spy(analytics, 'track'); 1271 | link = document.createElement('a'); 1272 | link.href = '#'; 1273 | document.body.appendChild(link); 1274 | }); 1275 | 1276 | afterEach(function() { 1277 | window.location.hash = ''; 1278 | if (wrap) document.body.removeChild(wrap); 1279 | document.body.removeChild(link); 1280 | }); 1281 | 1282 | it('should trigger a track on an element click', function() { 1283 | analytics.trackLink(link); 1284 | trigger(link, 'click'); 1285 | assert(analytics.track.called); 1286 | }); 1287 | 1288 | it('should accept a jquery object for an element', function() { 1289 | var $link = jQuery(link); 1290 | analytics.trackLink($link); 1291 | trigger(link, 'click'); 1292 | assert(analytics.track.called); 1293 | }); 1294 | 1295 | it('should not accept a string for an element', function() { 1296 | assert['throws'](function() { 1297 | analytics.trackLink('a'); 1298 | }, TypeError, 'Must pass HTMLElement to `analytics.trackLink`.'); 1299 | trigger(link, 'click'); 1300 | assert(!analytics.track.called); 1301 | }); 1302 | 1303 | it('should send an event and properties', function() { 1304 | analytics.trackLink(link, 'event', { property: true }); 1305 | trigger(link, 'click'); 1306 | assert(analytics.track.calledWith('event', { property: true })); 1307 | }); 1308 | 1309 | it('should accept an event function', function() { 1310 | function event(el) { return el.nodeName; } 1311 | analytics.trackLink(link, event); 1312 | trigger(link, 'click'); 1313 | assert(analytics.track.calledWith('A')); 1314 | }); 1315 | 1316 | it('should accept a properties function', function() { 1317 | function properties(el) { return { type: el.nodeName }; } 1318 | analytics.trackLink(link, 'event', properties); 1319 | trigger(link, 'click'); 1320 | assert(analytics.track.calledWith('event', { type: 'A' })); 1321 | }); 1322 | 1323 | it('should load an href on click', function(done) { 1324 | link.href = '#test'; 1325 | analytics.trackLink(link); 1326 | trigger(link, 'click'); 1327 | tick(function() { 1328 | assert(window.location.hash === '#test'); 1329 | done(); 1330 | }); 1331 | }); 1332 | 1333 | it('should support svg .href attribute', function(done) { 1334 | if (!svg) return done(); 1335 | // not correct svg, but should work. 1336 | svg.setAttribute('href', '#svg'); 1337 | analytics.trackLink(svg); 1338 | trigger(svg, 'click'); 1339 | tick(function() { 1340 | assert.equal(window.location.hash, '#svg'); 1341 | done(); 1342 | }); 1343 | }); 1344 | 1345 | it('should fallback to getAttributeNS', function(done) { 1346 | if (!wrap) return done(); 1347 | svg.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#svg'); 1348 | analytics.trackLink(svg); 1349 | trigger(svg, 'click'); 1350 | tick(function() { 1351 | assert.equal(window.location.hash, '#svg'); 1352 | done(); 1353 | }); 1354 | }); 1355 | 1356 | it('should support xlink:href', function(done) { 1357 | if (!wrap) return done(); 1358 | svg.setAttribute('xlink:href', '#svg'); 1359 | analytics.trackLink(svg); 1360 | trigger(svg, 'click'); 1361 | tick(function() { 1362 | assert.equal(window.location.hash, '#svg'); 1363 | done(); 1364 | }); 1365 | }); 1366 | 1367 | it('should not load an href for a link with a blank target', function(done) { 1368 | link.href = '/base/test/support/mock.html'; 1369 | link.target = '_blank'; 1370 | analytics.trackLink(link); 1371 | trigger(link, 'click'); 1372 | tick(function() { 1373 | assert(window.location.hash !== '#test'); 1374 | done(); 1375 | }); 1376 | }); 1377 | }); 1378 | 1379 | describe('#trackForm', function() { 1380 | var form; 1381 | var submit; 1382 | 1383 | before(function() { 1384 | window.jQuery = jQuery; 1385 | }); 1386 | 1387 | after(function() { 1388 | window.jQuery = null; 1389 | }); 1390 | 1391 | beforeEach(function() { 1392 | sinon.spy(analytics, 'track'); 1393 | form = document.createElement('form'); 1394 | form.action = '/base/test/support/mock.html'; 1395 | form.target = '_blank'; 1396 | submit = document.createElement('input'); 1397 | submit.type = 'submit'; 1398 | form.appendChild(submit); 1399 | document.body.appendChild(form); 1400 | }); 1401 | 1402 | afterEach(function() { 1403 | window.location.hash = ''; 1404 | document.body.removeChild(form); 1405 | }); 1406 | 1407 | it('should trigger a track on a form submit', function() { 1408 | analytics.trackForm(form); 1409 | submit.click(); 1410 | assert(analytics.track.called); 1411 | }); 1412 | 1413 | it('should accept a jquery object for an element', function() { 1414 | analytics.trackForm(form); 1415 | submit.click(); 1416 | assert(analytics.track.called); 1417 | }); 1418 | 1419 | it('should not accept a string for an element', function() { 1420 | var str = 'form'; 1421 | assert['throws'](function() { 1422 | analytics.trackForm(str); 1423 | }, TypeError, 'Must pass HTMLElement to `analytics.trackForm`.'); 1424 | submit.click(); 1425 | assert(!analytics.track.called); 1426 | }); 1427 | 1428 | it('should send an event and properties', function() { 1429 | analytics.trackForm(form, 'event', { property: true }); 1430 | submit.click(); 1431 | assert(analytics.track.calledWith('event', { property: true })); 1432 | }); 1433 | 1434 | it('should accept an event function', function() { 1435 | function event() { return 'event'; } 1436 | analytics.trackForm(form, event); 1437 | submit.click(); 1438 | assert(analytics.track.calledWith('event')); 1439 | }); 1440 | 1441 | it('should accept a properties function', function() { 1442 | function properties() { return { property: true }; } 1443 | analytics.trackForm(form, 'event', properties); 1444 | submit.click(); 1445 | assert(analytics.track.calledWith('event', { property: true })); 1446 | }); 1447 | 1448 | it('should call submit after a timeout', function(done) { 1449 | var spy = form.submit = sinon.spy(); 1450 | analytics.trackForm(form); 1451 | submit.click(); 1452 | setTimeout(function() { 1453 | assert(spy.called); 1454 | done(); 1455 | }, 50); 1456 | }); 1457 | 1458 | it('should trigger an existing submit handler', function(done) { 1459 | bind(form, 'submit', function() { done(); }); 1460 | analytics.trackForm(form); 1461 | submit.click(); 1462 | }); 1463 | 1464 | it('should trigger an existing jquery submit handler', function(done) { 1465 | var $form = jQuery(form); 1466 | $form.submit(function() { done(); }); 1467 | analytics.trackForm(form); 1468 | submit.click(); 1469 | }); 1470 | 1471 | it('should track on a form submitted via jquery', function() { 1472 | var $form = jQuery(form); 1473 | analytics.trackForm(form); 1474 | $form.submit(); 1475 | assert(analytics.track.called); 1476 | }); 1477 | 1478 | it('should trigger an existing jquery submit handler on a form submitted via jquery', function(done) { 1479 | var $form = jQuery(form); 1480 | $form.submit(function() { done(); }); 1481 | analytics.trackForm(form); 1482 | $form.submit(); 1483 | }); 1484 | }); 1485 | 1486 | describe('#alias', function() { 1487 | beforeEach(function() { 1488 | sinon.spy(analytics, '_invoke'); 1489 | }); 1490 | 1491 | it('should call #_invoke', function() { 1492 | analytics.alias(); 1493 | assert(analytics._invoke.calledWith('alias')); 1494 | }); 1495 | 1496 | it('should call #_invoke with instanceof Alias', function() { 1497 | analytics.alias(); 1498 | var alias = analytics._invoke.args[0][1]; 1499 | assert(alias.action() === 'alias'); 1500 | }); 1501 | 1502 | it('should default .anonymousId', function() { 1503 | analytics.alias('previous-id', 'user-id'); 1504 | var msg = analytics._invoke.args[0][1]; 1505 | assert(msg.anonymousId().length === 36); 1506 | }); 1507 | 1508 | it('should override .anonymousId', function() { 1509 | analytics.alias('previous-id', 'user-id', { anonymousId: 'anon-id' }); 1510 | var msg = analytics._invoke.args[0][1]; 1511 | assert(msg.anonymousId() === 'anon-id'); 1512 | }); 1513 | 1514 | it('should accept (new, old, options, callback)', function(done) { 1515 | analytics.alias('new', 'old', {}, function() { 1516 | var alias = analytics._invoke.args[0][1]; 1517 | assert(alias.from() === 'old'); 1518 | assert(alias.to() === 'new'); 1519 | assert(typeof alias.options() === 'object'); 1520 | done(); 1521 | }); 1522 | }); 1523 | 1524 | it('should accept (new, old, callback)', function(done) { 1525 | analytics.alias('new', 'old', function() { 1526 | var alias = analytics._invoke.args[0][1]; 1527 | assert(alias.from() === 'old'); 1528 | assert(alias.to() === 'new'); 1529 | assert(typeof alias.options() === 'object'); 1530 | done(); 1531 | }); 1532 | }); 1533 | 1534 | it('should accept (new, callback)', function(done) { 1535 | analytics.alias('new', function() { 1536 | var alias = analytics._invoke.args[0][1]; 1537 | assert(alias.to() === 'new'); 1538 | assert(typeof alias.options() === 'object'); 1539 | done(); 1540 | }); 1541 | }); 1542 | 1543 | it('should include context.page', function() { 1544 | analytics.alias(); 1545 | var alias = analytics._invoke.args[0][1]; 1546 | assert.deepEqual(alias.context(), { page: contextPage }); 1547 | }); 1548 | 1549 | it('should emit alias', function(done) { 1550 | analytics.once('alias', function(newId, oldId, options) { 1551 | assert(newId === 'new'); 1552 | assert(oldId === 'old'); 1553 | assert.deepEqual(options, { opt: true }); 1554 | done(); 1555 | }); 1556 | analytics.alias('new', 'old', { opt: true }); 1557 | }); 1558 | }); 1559 | 1560 | describe('#push', function() { 1561 | beforeEach(function() { 1562 | analytics.track = sinon.spy(); 1563 | }); 1564 | 1565 | it('should call methods with args', function() { 1566 | analytics.push(['track', 'event', { prop: true }]); 1567 | assert(analytics.track.calledWith('event', { prop: true })); 1568 | }); 1569 | }); 1570 | 1571 | describe('#reset', function() { 1572 | beforeEach(function() { 1573 | user.id('user-id'); 1574 | user.traits({ name: 'John Doe' }); 1575 | group.id('group-id'); 1576 | group.traits({ name: 'Example' }); 1577 | }); 1578 | 1579 | it('should remove persisted group and user', function() { 1580 | assert(user.id() === 'user-id'); 1581 | assert(user.traits().name === 'John Doe'); 1582 | assert(group.id() === 'group-id'); 1583 | assert(group.traits().name === 'Example'); 1584 | analytics.reset(); 1585 | assert(user.id() === null); 1586 | assert.deepEqual({}, user.traits()); 1587 | assert(group.id() === null); 1588 | assert.deepEqual({}, group.traits()); 1589 | }); 1590 | }); 1591 | }); 1592 | --------------------------------------------------------------------------------