├── .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 | [](https://circleci.com/gh/segmentio/analytics.js-core)
4 | [](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 |
--------------------------------------------------------------------------------