├── .babelrc ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── SUMMARY.md ├── __mocks__ ├── config.js └── load-script.js ├── __tests__ ├── create-trackers.spec.js ├── helpers.spec.js ├── lib │ ├── event.spec.js │ ├── exception.spec.js │ ├── page.spec.js │ ├── require.spec.js │ ├── set.spec.js │ ├── social.spec.js │ └── time.spec.js └── vuex-middleware.spec.js ├── docs ├── batch.md ├── console-logs.md ├── cross-domain-tracking.md ├── custom-analytics.md ├── custom-methods.md ├── debug.md ├── ecommerce.md ├── event-tracking.md ├── exception-tracking.md ├── fields.md ├── installation.md ├── opt-out.md ├── page-tracking.md ├── require.md ├── screen-tracking.md ├── script-loader.md ├── set.md ├── social-interactions.md ├── turn-off-development.md ├── untracked-hits.md ├── user-timings.md ├── v-ga.md ├── vuex.md └── when-google-analytics-is-loaded.md ├── jest.config.js ├── package.json ├── src ├── bootstrap.js ├── collectors.js ├── config.js ├── create-trackers.js ├── directives │ └── ga.js ├── helpers.js ├── index.js ├── lib │ ├── ecommerce.js │ ├── event.js │ ├── exception.js │ ├── index.js │ ├── page.js │ ├── query.js │ ├── require.js │ ├── screenview.js │ ├── set.js │ ├── social.js │ └── time.js ├── no-ga.js ├── untracked.js └── vuex-middleware.js ├── vue-analytics.d.ts ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "last 3 versions, ie >= 9" 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | If you are reporting a bug, please fill in below. Otherwise feel free to remove this template entirely. 2 | 3 | ### Description 4 | 5 | What are you reporting? 6 | 7 | ### Expected behavior 8 | 9 | Tell us what you think should happen. 10 | 11 | ### Actual behavior 12 | 13 | Tell us what actually happens. 14 | 15 | ### Environment 16 | 17 | Run this command in the project folder and fill in their results: 18 | 19 | `npm ls vue-analytics`: 20 | 21 | Then, specify: 22 | 23 | 1. Operating system: 24 | 2. Browser and version: 25 | 26 | ### Reproducible Demo 27 | 28 | Please take the time to create a new app that reproduces the issue or at least some code example 29 | 30 | Demonstrable issues gets fixed faster. 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)** 2 | 3 | 4 | **What is the current behavior? (You can also link to an open issue here)** 5 | 6 | 7 | **What is the new behavior (if this is a feature change)?** 8 | 9 | 10 | **Does this PR introduce a breaking change?** 11 | 12 | 13 | **Please check if the PR fulfills these requirements** 14 | - [ ] The commit message follows semantic-release [guidelines](https://github.com/semantic-release/semantic-release#commit-message-format) 15 | - [ ] Fix/Feature: Docs have been added/updated 16 | - [ ] Fix/Feature: Tests have been added; existing tests pass 17 | 18 | **Other information**: 19 | 20 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 14 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "P0: Critical" 8 | - "technical debt" 9 | - "feature" 10 | - "bug" 11 | - "pending" 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .DS_Store 4 | npm-debug.log 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | node_js: 8 | - '10' 9 | before_script: 10 | - npm prune 11 | script: 12 | - npm run test 13 | after_success: 14 | - npm run semantic-release 15 | branches: 16 | only: 17 | - master 18 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Matteo Gabriele 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > :warning: Sorry but vue-analytics is not longer maintained. I would suggest you to switch to [vue-gtag](https://github.com/MatteoGabriele/vue-gtag). With love, the guy who made the package. 2 | 3 |

4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

20 | 21 | # vue-analytics 22 | 23 | Vue plugin for Google Analytics 24 | 25 | ## Why should I use it? 26 | 27 | The plugin isn't just a wrapper of the Google Analytics API, but provides a solution to issues that most of the time you don't want to deal with or you not even know you have to deal with. 28 | 29 | For example: 30 | 31 | * Automatic Google Analytics script loading 32 | * Automatic page tracking 33 | * Event batching 34 | * Opt-out from Google Analytics with promise support 35 | * Multiple domain ID tracking system 36 | * Vuex support 37 | * E-commerce API 38 | * Vue error exception tracking system 39 | * Debugging API 40 | 41 | ## Does this library support GA4? 42 | Nope! GA4 is only supported by the new gtag.js library which you can find in the `vue-gtag` package. 43 | This package only uses analytics.js which doesn't provide that feature. 44 | 45 | ## Requirements 46 | 47 | Vue ^2.0.0 48 | 49 | ## Articles 50 | 51 | [Google Analytics, GDPR and Vuejs](https://medium.com/@matteo_gabriele/google-analytics-gdpr-and-vuejs-e1bd6affd2b4) 52 | 53 | [Vuejs and Google Analytics](https://medium.com/@matteo_gabriele/vuejs-and-google-analytics-689a07e00116) 54 | 55 | [Tips & tricks for vue-analytics](https://medium.com/@matteo_gabriele/tips-tricks-for-vue-analytics-87a9d2838915) 56 | 57 | ## Install 58 | 59 | ```bash 60 | npm install vue-analytics 61 | ``` 62 | 63 | ## User guide 64 | 65 | * [Get started](/docs/installation.md) 66 | * [How to load Google Analytics](/docs/script-loader.md) 67 | * [Page tracking](/docs/page-tracking.md) 68 | * [Event tracking](/docs/event-tracking.md) 69 | * [Screen tracking](/docs/screen-tracking.md) 70 | * [Event batches](/docs/batch.md) 71 | * [v-ga](/docs/v-ga.md) 72 | * [Cross-domain tracking](/docs/cross-domain-tracking.md) 73 | * [User timings](/docs/user-timings.md#user-timings) 74 | * [Exception tracking](/docs/exception-tracking.md) 75 | * [Require](/docs/require.md) 76 | * [Set](/docs/set.md) 77 | * [Social interactions](/docs/social-interactions.md) 78 | * [Fields](/docs/fields.md) 79 | * [On Analytics script ready](/docs/when-google-analytics-is-loaded.md) 80 | * [Custom methods](/docs/custom-methods.md) 81 | * [E-commerce](/docs/ecommerce.md) 82 | * [Untracked hits](/docs/untracked-hits.md) 83 | * [Vuex](/docs/vuex.md) 84 | * [Turn off during development](/docs/turn-off-development.md) 85 | * [Console logs](/docs/console-logs.md) 86 | * [Opt-out from Google Analytics](/docs/opt-out.md) 87 | * [Custom analytics.js URL](/docs/custom-analytics.md) 88 | * [Debug](/docs/debug.md) 89 | 90 | Follow me on twitter [@matteo\_gabriele](https://twitter.com/matteo_gabriele) 91 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Get started](/docs/installation.md) 4 | * [How to load Google Analytics](/docs/script-loader.md) 5 | * [Page tracking](/docs/page-tracking.md) 6 | * [Event tracking](/docs/event-tracking.md) 7 | * [Screen tracking](/docs/screen-tracking.md) 8 | * [Event batches](/docs/batch.md) 9 | * [v-ga](/docs/v-ga.md) 10 | * [Cross-domain tracking](/docs/cross-domain-tracking.md) 11 | * [User timings](/docs/user-timings.md#user-timings) 12 | * [Exception tracking](/docs/exception-tracking.md) 13 | * [Require](/docs/require.md) 14 | * [Set](/docs/set.md) 15 | * [Social interactions](/docs/social-interactions.md) 16 | * [Fields](/docs/fields.md) 17 | * [On Analytics script ready](/docs/when-google-analytics-is-loaded.md) 18 | * [Custom methods](/docs/custom-methods.md) 19 | * [E-commerce](/docs/ecommerce.md) 20 | * [Untracked hits](/docs/untracked-hits.md) 21 | * [Vuex](/docs/vuex.md) 22 | * [Turn off during development](/docs/turn-off-development.md) 23 | * [Console logs](/docs/console-logs.md) 24 | * [Opt-out from Google Analytics](/docs/opt-out.md) 25 | * [Custom analytics.js URL](/docs/custom-analytics.md) 26 | * [Debug](/docs/debug.md) 27 | -------------------------------------------------------------------------------- /__mocks__/config.js: -------------------------------------------------------------------------------- 1 | import config, { update, getId, reset } from '../src/config' 2 | 3 | export const mockUpdate = update 4 | export const mockGetId = getId 5 | export const mockReset = reset 6 | 7 | export default config 8 | -------------------------------------------------------------------------------- /__mocks__/load-script.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return new Promise((resolve, reject) => { 3 | process.nextTick(() => { 4 | window.ga = jest.fn() 5 | return resolve() 6 | }) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /__tests__/create-trackers.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('config') 2 | 3 | import config, { mockUpdate, mockGetId, mockReset } from 'config' 4 | import createTrackers from '../src/create-trackers' 5 | import { getTracker } from '../src/helpers' 6 | 7 | window.ga = jest.fn() 8 | 9 | afterEach(() => { 10 | mockReset() 11 | }) 12 | 13 | it ('should initialize single tracker', () => { 14 | mockUpdate({ id: 'UA-1234-1' }) 15 | 16 | createTrackers() 17 | expect(window.ga).toBeCalledWith('create', config.id, 'auto', {}) 18 | expect(window.ga).not.toBeCalledWith('set', 'sendHitTask', null) 19 | }) 20 | 21 | it ('should initialize multiple trackers', () => { 22 | mockUpdate({ id: [ 'UA-1234-1', 'UA-1234-2' ] }) 23 | 24 | createTrackers() 25 | 26 | mockGetId().forEach((id) => { 27 | expect(window.ga).toBeCalledWith('create', id, 'auto', { 'name': getTracker(id) }) 28 | }) 29 | }) 30 | 31 | it('should intialize each id with its own configuration', () => { 32 | const customIdFields = { 33 | 'UA-12345-1': { 34 | clientId: '1' 35 | }, 36 | 'UA-54321-1': { 37 | clientId: '2' 38 | }, 39 | } 40 | 41 | mockUpdate({ 42 | id: ['UA-12345-1', 'UA-54321-1'], 43 | fields: { 44 | 'global': true 45 | }, 46 | customIdFields, 47 | }) 48 | 49 | createTrackers() 50 | 51 | mockGetId().forEach(id => { 52 | expect(window.ga).toBeCalledWith('create', id, 'auto', { 53 | global: true, 54 | ...customIdFields[id], 55 | name: getTracker(id), 56 | }) 57 | }) 58 | }) 59 | 60 | it ('should add linkers if list is not empty', function () { 61 | mockUpdate({ 62 | id: 'UA-1234-1', 63 | linkers: ['www.google.com', 'www.bing.com'] 64 | }) 65 | 66 | createTrackers() 67 | 68 | expect(config.linkers).toHaveLength(2) 69 | expect(window.ga).toBeCalledWith('require', 'linker') 70 | expect(window.ga).toBeCalledWith('linker:autoLink', config.linkers) 71 | }) 72 | 73 | it ('should stop sending hit if sendHitTask is set to false', () => { 74 | mockUpdate({ 75 | id: 'UA-1234-1', 76 | debug: { 77 | sendHitTask: false 78 | } 79 | }) 80 | 81 | createTrackers() 82 | 83 | expect(config.debug.sendHitTask).toBe(false) 84 | expect(window.ga).toBeCalledWith('set', 'sendHitTask', null) 85 | }) 86 | 87 | it ('should set the trace property if debug is enabled', () => { 88 | mockUpdate({ 89 | id: 'UA-1234-1', 90 | debug: { 91 | enabled: true, 92 | trace: true 93 | } 94 | }) 95 | 96 | createTrackers() 97 | 98 | expect(config.debug.enabled).toBe(true) 99 | expect(window.ga_debug.trace).toBe(true) 100 | }) 101 | -------------------------------------------------------------------------------- /__tests__/helpers.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('config') 2 | 3 | import * as helpers from '../src/helpers' 4 | import config, { mockGetId, mockUpdate } from 'config' 5 | 6 | describe('noop', () => { 7 | it ('should be a function', () => { 8 | expect(typeof helpers.noop).toEqual('function') 9 | }) 10 | }) 11 | 12 | describe('merge', () => { 13 | it ('should merge two objects', () => { 14 | const a = { a: 1, c: { a: 1 } } 15 | const b = { b: 1, c: { b: 1 } } 16 | const c = helpers.merge(a, b) 17 | 18 | expect(c).toMatchObject({ 19 | a: 1, 20 | b: 1, 21 | c: { 22 | b: 1, 23 | a: 1 24 | } 25 | }) 26 | }) 27 | 28 | it ('should merge objects containing non-objects', () => { 29 | const a = { c: [ 1, 2, 3 ], d: Promise.resolve({ a: 1 }) } 30 | const b = { c: [ 4, 5 ], d: { b: 1 } } 31 | const c = helpers.merge(a, b) 32 | 33 | expect(c).toEqual({ 34 | c: [ 35 | 4, 36 | 5, 37 | 3 38 | ], 39 | d: { 40 | b: 1 41 | } 42 | }) 43 | }) 44 | }) 45 | 46 | describe('getMethod', () => { 47 | it ('should return the plain method name if single tracker', () => { 48 | mockUpdate({ id: 'UA-1234-5' }) 49 | 50 | const method = helpers.getMethod('send', config.id) 51 | expect(method).toEqual('send') 52 | }) 53 | 54 | it ('should return the method name prepended with tracker name if multiple trackers', () => { 55 | mockUpdate({ id: ['UA-1234-5', 'UA-1234-6'] }) 56 | 57 | const method = helpers.getMethod('send', 'UA-1234-5') 58 | expect(method).toEqual('UA12345.send') 59 | }) 60 | }) 61 | 62 | describe('getTracker', () => { 63 | it ('should return the tracking id without dashes', () => { 64 | const tracker1 = helpers.getTracker('UA-1234-5') 65 | expect(tracker1).toEqual('UA12345') 66 | }) 67 | }) 68 | 69 | describe('getQueryString', () => { 70 | it ('should return a query string from an object literal', () => { 71 | const obj = { name: 'matteo', surname: 'gabriele' } 72 | const queryString = helpers.getQueryString(obj) 73 | 74 | expect(queryString).toEqual('?name=matteo&surname=gabriele') 75 | }) 76 | 77 | it ('should return a empty string if object literal is empty', () => { 78 | const obj = {} 79 | const queryString = helpers.getQueryString(obj) 80 | 81 | expect(queryString).toEqual('') 82 | }) 83 | }) 84 | 85 | describe('isRouteIgnored', () => { 86 | it ('should be truthy if home route name is added to ignoreRoutes list', () => { 87 | mockUpdate({ id: 'UA-1234-5', ignoreRoutes: ['home'] }) 88 | expect(helpers.isRouteIgnored({ name: 'home' })).toBeTruthy() 89 | }) 90 | 91 | it ('should be truthy if home route path is added to ignoreRoutes list', () => { 92 | mockUpdate({ id: 'UA-1234-5', ignoreRoutes: ['/'] }) 93 | expect(helpers.isRouteIgnored({ path: '/' })).toBeTruthy() 94 | }) 95 | 96 | it ('should be falsy if a `null` value is added to ignoreRoutes list', () => { 97 | mockUpdate({ id: 'UA-1234-5', ignoreRoutes: [null] }) 98 | expect(helpers.isRouteIgnored({ path: '/' })).toBeFalsy() 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /__tests__/lib/event.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueAnalytics from '../../src' 3 | 4 | window.ga = jest.fn() 5 | 6 | let $vm 7 | 8 | beforeEach(() => { 9 | window.ga.mockClear() 10 | 11 | Vue.use(VueAnalytics, { 12 | id: 'UA-1234-5' 13 | }) 14 | 15 | $vm = new Vue({}) 16 | 17 | $vm.$mount() 18 | }) 19 | 20 | it ('should track an event', () => { 21 | $vm.$ga.event('foo', 'bar') 22 | expect(window.ga).toBeCalledWith('send', 'event', 'foo', 'bar') 23 | }) 24 | -------------------------------------------------------------------------------- /__tests__/lib/exception.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueAnalytics from '../../src' 3 | 4 | window.ga = jest.fn() 5 | 6 | let $vm 7 | 8 | beforeEach(() => { 9 | window.ga.mockClear() 10 | 11 | Vue.config.errorHandler = jest.fn() 12 | 13 | Vue.use(VueAnalytics, { 14 | id: 'UA-1234-5' 15 | }) 16 | 17 | $vm = new Vue({}) 18 | 19 | $vm.$mount() 20 | }) 21 | 22 | it ('should track an error exception', () => { 23 | $vm.$ga.exception('bad stuff', true) 24 | 25 | expect(window.ga).toBeCalledWith('send', 'exception', { 26 | exDescription: 'bad stuff', 27 | exFatal: true 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /__tests__/lib/page.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('config') 2 | jest.mock('load-script') 3 | 4 | import Vue from 'vue' 5 | import VueAnalytics from '../../src/index' 6 | import VueRouter from 'vue-router' 7 | import config, { mockUpdate } from 'config' 8 | 9 | const routes = [ 10 | { 11 | name: 'home', 12 | path: '/', 13 | component: { 14 | name: 'home', 15 | render: h => h('div') 16 | } 17 | }, 18 | { 19 | name: 'about', 20 | path: '/about', 21 | component: { 22 | name: 'about', 23 | render: h => h('div') 24 | } 25 | } 26 | ] 27 | 28 | window.ga = jest.fn() 29 | 30 | let $vm 31 | 32 | beforeEach(done => { 33 | window.ga.mockClear() 34 | 35 | Vue.use(VueRouter) 36 | 37 | const router = new VueRouter({ 38 | mode: 'hash', 39 | routes 40 | }) 41 | 42 | Vue.use(VueAnalytics, { 43 | id: 'UA-1234-5', 44 | router 45 | }) 46 | 47 | $vm = new Vue({ 48 | router, 49 | render: (h) => h('router-view') 50 | }) 51 | 52 | $vm.$mount() 53 | 54 | Vue.nextTick(done) 55 | }) 56 | 57 | it ('should track a page', () => { 58 | $vm.$ga.page('/') 59 | 60 | expect(window.ga).toBeCalledWith('set', 'page', '/') 61 | expect(window.ga).toBeCalledWith('send', 'pageview', '/') 62 | }) 63 | 64 | it ('should set and track page with a VueRouter instance', () => { 65 | $vm.$ga.page($vm.$router) 66 | 67 | expect(window.ga).toBeCalledWith('set', 'page', '/') 68 | expect(window.ga).toBeCalledWith('send', 'pageview', '/') 69 | }) 70 | 71 | it ('should skip tracking when page first argument is a falsy value', () => { 72 | $vm.$ga.page(null) 73 | $vm.$ga.page(false) 74 | $vm.$ga.page(undefined) 75 | // Google officially states that page path must begin with '/' 76 | // https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#page 77 | $vm.$ga.page('') 78 | 79 | expect(window.ga).not.toHaveBeenCalled() 80 | expect(window.ga).not.toHaveBeenCalled() 81 | 82 | // Skip behavior must be explicit 83 | $vm.$ga.page() 84 | 85 | expect(window.ga).toHaveBeenCalled() 86 | expect(window.ga).toHaveBeenCalled() 87 | }) 88 | -------------------------------------------------------------------------------- /__tests__/lib/require.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueAnalytics from '../../src' 3 | 4 | window.ga = jest.fn() 5 | 6 | let $vm 7 | 8 | beforeEach(() => { 9 | window.ga.mockClear() 10 | 11 | Vue.use(VueAnalytics, { 12 | id: 'UA-1234-5' 13 | }) 14 | 15 | $vm = new Vue({}) 16 | 17 | $vm.$mount() 18 | }) 19 | 20 | it ('should require a plugin', () => { 21 | $vm.$ga.require('myplugin') 22 | 23 | expect(window.ga).toBeCalledWith('require', 'myplugin') 24 | }) 25 | 26 | it ('should require a plugin with options', () => { 27 | $vm.$ga.require('myplugin', { 28 | foo: 'bar' 29 | }) 30 | 31 | expect(window.ga).toBeCalledWith('require', 'myplugin', { 32 | foo: 'bar' 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /__tests__/lib/set.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueAnalytics from '../../src' 3 | 4 | window.ga = jest.fn() 5 | 6 | let $vm 7 | 8 | beforeEach(() => { 9 | window.ga.mockClear() 10 | 11 | Vue.use(VueAnalytics, { 12 | id: 'UA-1234-5' 13 | }) 14 | 15 | $vm = new Vue({}) 16 | 17 | $vm.$mount() 18 | }) 19 | 20 | it ('should set a variable on Google Analytics', () => { 21 | $vm.$ga.set('foo', 'bar') 22 | 23 | expect(window.ga).toBeCalledWith('set', 'foo', 'bar') 24 | }) 25 | 26 | it ('should set a variable on Google Analytics with an object literal', () => { 27 | $vm.$ga.set({ 28 | fieldName: 'foo', 29 | fieldValue: 'bar' 30 | }) 31 | 32 | expect(window.ga).toBeCalledWith('set', { 33 | fieldName: 'foo', 34 | fieldValue: 'bar' 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/lib/social.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueAnalytics from '../../src' 3 | 4 | window.ga = jest.fn() 5 | 6 | let $vm 7 | 8 | beforeEach(() => { 9 | window.ga.mockClear() 10 | 11 | Vue.use(VueAnalytics, { 12 | id: 'UA-1234-5' 13 | }) 14 | 15 | $vm = new Vue({}) 16 | 17 | $vm.$mount() 18 | }) 19 | 20 | it ('should track a social interaction', () => { 21 | $vm.$ga.social('Facebook', 'like', 'http://foo.com') 22 | expect(window.ga).toBeCalledWith('send', 'social', 'Facebook', 'like', 'http://foo.com') 23 | }) 24 | 25 | it ('should track a social interaction with an object literal', () => { 26 | $vm.$ga.social({ 27 | socialNetwork: 'Facebook', 28 | socialAction: 'like', 29 | socialTarget: 'http://foo.com' 30 | }) 31 | 32 | expect(window.ga).toBeCalledWith('send', 'social', { 33 | socialNetwork: 'Facebook', 34 | socialAction: 'like', 35 | socialTarget: 'http://foo.com' 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /__tests__/lib/time.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueAnalytics from '../../src' 3 | 4 | window.ga = jest.fn() 5 | 6 | let $vm 7 | 8 | beforeEach(() => { 9 | window.ga.mockClear() 10 | 11 | Vue.use(VueAnalytics, { 12 | id: 'UA-1234-5' 13 | }) 14 | 15 | $vm = new Vue({}) 16 | 17 | $vm.$mount() 18 | }) 19 | 20 | it ('should track timing', () => { 21 | $vm.$ga.time('category', 'variable', 123, 'label') 22 | 23 | expect(window.ga).toBeCalledWith('send', 'timing', 'category', 'variable', 123, 'label') 24 | }) 25 | 26 | it ('should track timing with an object literal', () => { 27 | $vm.$ga.time({ 28 | timingCategory: 'category', 29 | timingVar: 'variable', 30 | timingValue: 123, 31 | timingLabel: 'label' 32 | }) 33 | 34 | expect(window.ga).toBeCalledWith('send', 'timing', { 35 | timingCategory: 'category', 36 | timingVar: 'variable', 37 | timingValue: 123, 38 | timingLabel: 'label' 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /__tests__/vuex-middleware.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('load-script') 2 | 3 | import Vue from 'vue' 4 | import VueAnalytics from '../src' 5 | import analyticsMiddleware from '../src/vuex-middleware' 6 | window.ga = jest.fn() 7 | 8 | let $vm 9 | let store 10 | beforeEach(() => { 11 | window.ga.mockClear() 12 | store = { 13 | subscribers: [], 14 | subscribe: function (fn) { 15 | this.subscribers.push(fn) 16 | }, 17 | fire: function (mutation) { 18 | this.subscribers.forEach(x => { x(mutation) }) 19 | } 20 | } 21 | 22 | analyticsMiddleware(store) 23 | Vue.use(VueAnalytics, { 24 | id: 'UA-1234-5' 25 | }) 26 | $vm = new Vue({}) 27 | 28 | $vm.$mount() 29 | }) 30 | 31 | describe('input check', () => { 32 | it('should allow meta tags without analytics', () => { 33 | let mutation = { 34 | payload: { 35 | meta: { 36 | 37 | } 38 | } 39 | } 40 | expect(() => { store.fire(mutation) }).not.toThrowError() 41 | }) 42 | 43 | it('should throw an error when analytics is not an array', () => { 44 | let mutation = { 45 | payload: { 46 | meta: { 47 | analytics: {} 48 | } 49 | } 50 | } 51 | expect(() => { store.fire(mutation) }).toThrowError() 52 | }) 53 | 54 | it('should throw an unknown type error', () => { 55 | let mutation = { 56 | payload: { 57 | meta: { 58 | analytics: [['unknown']] 59 | } 60 | } 61 | } 62 | expect(() => { store.fire(mutation) }).toThrowError() 63 | }) 64 | 65 | it('should throw an unknown method for type error', () => { 66 | let mutation = { 67 | payload: { 68 | meta: { 69 | analytics: [['event:unknown']] 70 | } 71 | } 72 | } 73 | expect(() => { store.fire(mutation) }).toThrowError() 74 | }) 75 | 76 | it('should throw an error because ecommerce requires a method call', () => { 77 | let mutation = { 78 | payload: { 79 | meta: { 80 | analytics: [['ecommerce']] 81 | } 82 | } 83 | } 84 | expect(() => { store.fire(mutation) }).toThrowError() 85 | }) 86 | }) 87 | 88 | describe('fire event', () => { 89 | it('should fire a simple tracking event based on the meta payload', () => { 90 | let mutation = { 91 | payload: { 92 | meta: { 93 | analytics: [ 94 | [ 95 | 'event', 'CLICK', 'TEST', 'LABEL', 1 96 | ] 97 | ] 98 | } 99 | } 100 | } 101 | store.fire(mutation) 102 | expect(window.ga).toBeCalledWith('send', 'event', 'CLICK', 'TEST', 'LABEL', 1) 103 | }) 104 | 105 | 106 | it('should fire a simple event based on the meta payload', () => { 107 | let mutation = { 108 | payload: { 109 | meta: { 110 | analytics: [ 111 | [ 112 | 'event', 'foo', 'bar' 113 | ] 114 | ] 115 | } 116 | } 117 | } 118 | store.fire(mutation) 119 | expect(window.ga).toBeCalledWith('send', 'event', 'foo', 'bar') 120 | }) 121 | 122 | it('should fire a simple exception event based on the meta payload', () => { 123 | let mutation = { 124 | payload: { 125 | meta: { 126 | analytics: [ 127 | [ 128 | 'exception', 'CLICK', 'TEST' 129 | ] 130 | ] 131 | } 132 | } 133 | } 134 | store.fire(mutation) 135 | expect(window.ga).toBeCalledWith('send', 'exception', { exDescription: 'CLICK', exFatal: 'TEST' }) 136 | }) 137 | 138 | it('should fire a simple set event based on the meta payload', () => { 139 | let mutation = { 140 | payload: { 141 | meta: { 142 | analytics: [ 143 | [ 144 | 'set', 'foo', 'bar' 145 | ] 146 | ] 147 | } 148 | } 149 | } 150 | store.fire(mutation) 151 | expect(window.ga).toBeCalledWith('set', 'foo', 'bar') 152 | }) 153 | 154 | 155 | it('should fire a simple social event based on the meta payload', () => { 156 | let mutation = { 157 | payload: { 158 | meta: { 159 | analytics: [ 160 | [ 161 | 'social', 'Facebook', 'like', 'http://foo.com' 162 | ] 163 | ] 164 | } 165 | } 166 | } 167 | store.fire(mutation) 168 | expect(window.ga).toBeCalledWith('send', 'social', 'Facebook', 'like', 'http://foo.com') 169 | }) 170 | 171 | it('should fire a simple timing event based on the meta payload', () => { 172 | let mutation = { 173 | payload: { 174 | meta: { 175 | analytics: [ 176 | [ 177 | 'time', 'category', 'variable', 123, 'label' 178 | ] 179 | ] 180 | } 181 | } 182 | } 183 | store.fire(mutation) 184 | expect(window.ga).toBeCalledWith('send', 'timing', 'category', 'variable', 123, 'label') 185 | }) 186 | 187 | 188 | it('should fire a simple timing event based on the meta payload', () => { 189 | let mutation = { 190 | payload: { 191 | meta: { 192 | analytics: [ 193 | [ 194 | 'time', 'category', 'variable', 123, 'label' 195 | ] 196 | ] 197 | } 198 | } 199 | } 200 | store.fire(mutation) 201 | expect(window.ga).toBeCalledWith('send', 'timing', 'category', 'variable', 123, 'label') 202 | }) 203 | 204 | it('should fire a simple page event based on the meta payload', () => { 205 | let mutation = { 206 | payload: { 207 | meta: { 208 | analytics: [ 209 | [ 210 | 'page', '/' 211 | ] 212 | ] 213 | } 214 | } 215 | } 216 | store.fire(mutation) 217 | expect(window.ga).toBeCalledWith('set', 'page', '/') 218 | expect(window.ga).toBeCalledWith('send', 'pageview', '/') 219 | }) 220 | 221 | it('should fire a simple require event based on the meta payload', () => { 222 | let mutation = { 223 | payload: { 224 | meta: { 225 | analytics: [ 226 | [ 227 | 'require', 'myplugin', { foo: 'bar' } 228 | ] 229 | ] 230 | } 231 | } 232 | } 233 | store.fire(mutation) 234 | expect(window.ga).toBeCalledWith('require', 'myplugin', { 235 | foo: 'bar' 236 | }) 237 | }) 238 | 239 | it('should fire a simple query event based on the meta payload', () => { 240 | let mutation = { 241 | payload: { 242 | meta: { 243 | analytics: [ 244 | [ 245 | 'query', 'test' 246 | ] 247 | ] 248 | } 249 | } 250 | } 251 | store.fire(mutation) 252 | expect(window.ga).toBeCalledWith('test') 253 | }) 254 | 255 | it('should fire a ecommerce query event based on the meta payload', () => { 256 | let item = { 257 | id: '1234', 258 | name: 'Fluffy Pink Bunnies', 259 | sku: 'DD23444', 260 | category: 'Party Toys', 261 | price: '11.99', 262 | quantity: '1' 263 | } 264 | let mutation = { 265 | payload: { 266 | meta: { 267 | analytics: [ 268 | ['ecommerce:addItem', item] 269 | ] 270 | } 271 | } 272 | } 273 | store.fire(mutation) 274 | expect(window.ga).toBeCalledWith('ecommerce:addItem', item) 275 | }) 276 | 277 | it('should allow multiple events to fire', () => { 278 | let mutation = { 279 | payload: { 280 | meta: { 281 | analytics: [ 282 | [ 283 | 'event', 'foo', 'bar' 284 | ], 285 | [ 286 | 'exception', 'CLICK', 'TEST', 'LABEL', 1 287 | ] 288 | ] 289 | } 290 | } 291 | } 292 | store.fire(mutation) 293 | expect(window.ga).toBeCalledWith('send', 'event', 'foo', 'bar') 294 | expect(window.ga).toBeCalledWith('send', 'exception', { exDescription: 'CLICK', exFatal: 'TEST' }) 295 | 296 | }) 297 | }) 298 | -------------------------------------------------------------------------------- /docs/batch.md: -------------------------------------------------------------------------------- 1 | ## Event batches 2 | 3 | It is possible to fire event using a queue that will be processed on a certain rate. 4 | The gap between each cycle and the amount of events fired on each cycle is totally customizable: default options are 2 events every 500ms. 5 | 6 | ```js 7 | import Vue from 'vue' 8 | import VueAnalytics from 'vue-analytics' 9 | 10 | Vue.use(VueAnalytics, { 11 | id: 'UA-XXX-X', 12 | batch: { 13 | enabled: true, // enable/disable 14 | amount: 2, // amount of events fired 15 | delay: 500 // delay in milliseconds 16 | } 17 | }) 18 | ``` -------------------------------------------------------------------------------- /docs/console-logs.md: -------------------------------------------------------------------------------- 1 | ## Console logs 2 | 3 | Implements Google Analaytics debug logs in your console. 4 | 5 | **Please remember that it is for debug only. The file size of analytics\_debug.js is way larger than analytics.js** 6 | 7 | Example: 8 | 9 | ```js 10 | Vue.use(VueAnalytics, { 11 | id: 'UA-XXX-X', 12 | debug: { 13 | enabled: true 14 | } 15 | }) 16 | ``` 17 | 18 | Google Analytics docs: [debugging](https://developers.google.com/analytics/devguides/collection/analyticsjs/debugging) 19 | -------------------------------------------------------------------------------- /docs/cross-domain-tracking.md: -------------------------------------------------------------------------------- 1 | ## Cross-domain tracking 2 | 3 | To enable cross-domain tracking you need to add your links in the `linkers` array in the configuration object 4 | 5 | ```js 6 | import Vue from 'vue' 7 | import VueAnalytics from 'vue-analytics' 8 | 9 | Vue.use(VueAnalytics, { 10 | id: 'UA-XXX-X', 11 | linkers: ['example1.com', 'example2.com'] 12 | }) 13 | ``` 14 | 15 | More information about [cross-domain tracking](https://support.google.com/analytics/answer/1034342?hl=en) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/custom-analytics.md: -------------------------------------------------------------------------------- 1 | ## Custom analytics.js URL 2 | 3 | Due to country restrictions, in specific cases is necessary to add a custom URL to load the analytics.js file. 4 | 5 | It is possible to use the `customResourceURL` prop in the plugin options 6 | 7 | ```js 8 | Vue.use(VueAnalytics, { 9 | id: 'UA-XXX-X', 10 | customResourceURL: 'http://your-custom-url/analytics.js' 11 | }) 12 | ``` 13 | 14 | By adding the `customResourceURL` you won't be able to switch between debug and production file, so you need to do it manually, depending on which file you want to use. -------------------------------------------------------------------------------- /docs/custom-methods.md: -------------------------------------------------------------------------------- 1 | ## Custom methods 2 | 3 | Google Analytics has a very big api and sometimes the wrapper might limit you on what you need, so it's still possible to access the plain Analytics api 4 | 5 | ```js 6 | this.$ga.query(...) 7 | ``` 8 | 9 | if you need more interaction or you want to add specific features, please add an issue on the repo or make a pull request 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/debug.md: -------------------------------------------------------------------------------- 1 | ## Debug 2 | 3 | Implements Google Analaytics debug library. 4 | 5 | **Please remember that it is for debug only. The file size of analytics\_debug.js is way larger than analytics.js** 6 | 7 | Example: 8 | 9 | ```js 10 | Vue.use(VueAnalytics, { 11 | id: 'UA-XXX-X', 12 | debug: { 13 | enabled: false, // default value 14 | trace: false, // default value 15 | sendHitTask: true // default value 16 | } 17 | }) 18 | ``` 19 | 20 | Google Analytics docs: [debugging](https://developers.google.com/analytics/devguides/collection/analyticsjs/debugging) 21 | -------------------------------------------------------------------------------- /docs/ecommerce.md: -------------------------------------------------------------------------------- 1 | ## E-commerce 2 | 3 | All e-commerce features are built-in in the plugin, so there's no need to require any e-commerce libraries: just enable the e-commerce features from the plugin configuration 4 | 5 | ```js 6 | import Vue from 'vue' 7 | import VueAnalytics from 'vue-analytics' 8 | 9 | Vue.use(VueAnalytics, { 10 | id: 'UA-XXX-X', 11 | ecommerce: { 12 | enabled: true 13 | } 14 | }) 15 | ``` 16 | 17 | It is also possible to use the Enhanced E-commerce library just by changing the configuration like so 18 | 19 | ```js 20 | Vue.use(VueAnalytics, { 21 | id: 'UA-XXX-X', 22 | ecommerce: { 23 | enabled: true, 24 | enhanced: true 25 | } 26 | }) 27 | ``` 28 | 29 | Finally it's possible to pass additional options during the installation of the library 30 | 31 | ```js 32 | Vue.use(VueAnalytics, { 33 | id: 'UA-XXX-X', 34 | ecommerce: { 35 | enabled: true, 36 | options: { ... } 37 | } 38 | }) 39 | ``` 40 | 41 | ### Usage 42 | All e-commerce features are accessable via the `ecommerce` object 43 | 44 | - addItem 45 | - addTransaction 46 | - addProduct 47 | - addImpression 48 | - setAction 49 | - addPromo 50 | - send 51 | 52 | Remember that not all methods are included in the E-commerce or the Enhanced E-commerce, so please check the relative documentation in the Google Analytics dev guide. 53 | 54 | - [E-commerce](https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce) 55 | - [Enhanced E-commerce](https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce) 56 | 57 | 58 | ```html 59 | 64 | 65 | 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/event-tracking.md: -------------------------------------------------------------------------------- 1 | ## Event tracking 2 | 3 | Event tracking can be achieved in different ways, following Google specifications 4 | 5 | passing parameters in this exact order 6 | 7 | ```js 8 | this.$ga.event('category', 'action', 'label', 123) 9 | ``` 10 | 11 | an object literal is also possible 12 | 13 | ```js 14 | this.$ga.event({ 15 | eventCategory: 'category', 16 | eventAction: 'action', 17 | eventLabel: 'label', 18 | eventValue: 123 19 | }) 20 | ``` 21 | 22 | Google Analytics docs: [event tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/events) 23 | 24 | ## 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/exception-tracking.md: -------------------------------------------------------------------------------- 1 | ## Exception tracking 2 | 3 | Exception tracking allows you to measure the number and type of crashes or errors that occur in your application 4 | 5 | is possible to track single exceptions using a try catch 6 | 7 | ```js 8 | try { 9 | // some code that might crash 10 | } catch (error) { 11 | // handle your error here 12 | 13 | // track the error with analytics 14 | // depending on the error you might want to check 15 | // if a `message` property exists or not 16 | const exception = error.message || error 17 | this.$ga.exception(exception) 18 | } 19 | ``` 20 | 21 | ### Enable exception auto tracking 22 | 23 | It is also possible to just enable the plugin exception auto tracking and the plugin will do everything for you 24 | 25 | ```js 26 | Vue.use(VueAnalytics, { 27 | id: 'UA-XXX-X', 28 | autoTracking: { 29 | exception: true 30 | } 31 | }) 32 | ``` 33 | 34 | **important: the auto tracking script uses Vue.config.errorHandler, if you need to add your handler, do it before installing the plugin or will be overwritten** 35 | 36 | ### Error logs 37 | 38 | When auto-tracking errors logs are automatically logged in the console, if you want to disable them, you can add this property to your configuration 39 | 40 | ```js 41 | Vue.use(VueAnalytics, { 42 | id: 'UA-XXX-X', 43 | autoTracking: { 44 | exception: true, 45 | exceptionLogs: false 46 | } 47 | }) 48 | ``` 49 | 50 | 51 | Google Analytics docs: [exceptions](https://developers.google.com/analytics/devguides/collection/analyticsjs/exceptions) 52 | 53 | -------------------------------------------------------------------------------- /docs/fields.md: -------------------------------------------------------------------------------- 1 | ## Tracker fields 2 | 3 | It is possible to setup initial tracker fields using the `fields` prop 4 | 5 | ```js 6 | Vue.use(VueAnalytics, { 7 | id: 'UA-XXX-X', 8 | fields: { 9 | userId: 'xxx' 10 | } 11 | }) 12 | ``` 13 | 14 | It's also possible to add fields per id 15 | 16 | ```js 17 | Vue.use(VueAnalytics, { 18 | id: ['UA-12345-1', 'UA-54321-2'], 19 | //fields for both IDS 20 | fields: { 21 | userId: '1', 22 | }, 23 | customIdFields: { 24 | 'UA-12345-1': { 25 | clientId: '2' 26 | }, 27 | 'UA-54321-2': { 28 | clientId: '3' 29 | } 30 | } 31 | }) 32 | ``` 33 | 34 | **it is also possible to set fields in runtime using the **[**set**](/docs/set.md)** method** 35 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Get started 2 | 3 | ### Installation 4 | ```bash 5 | npm install vue-analytics 6 | ``` 7 | 8 | Start using it your Vue application 9 | ```js 10 | import Vue from 'vue' 11 | import VueAnalytics from 'vue-analytics' 12 | 13 | Vue.use(VueAnalytics, { 14 | id: 'UA-XXX-X' 15 | }) 16 | ``` 17 | 18 | **Important** 19 | 20 | For all the ES5 users out there, this package uses a default export so if you want to use `require` instead of `import` you should import the plugin like this 21 | 22 | ```js 23 | const VueAnalytics = require('vue-analytics').default 24 | 25 | Vue.use(VueAnalytics, { ... }) 26 | ``` 27 | 28 | ### Usage 29 | it is possible to use the api in two different ways: 30 | - within the component scope 31 | - importing methods separately 32 | 33 | #### Component scope 34 | 35 | ```js 36 | export default { 37 | name: 'MyComponent', 38 | 39 | methods: { 40 | track () { 41 | this.$ga.page('/') 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | #### Import methods 48 | 49 | To be able to use methods import, make sure you install vue-analytics **before** you want to use them 50 | 51 | ```js 52 | import { page } from 'vue-analytics' 53 | 54 | export default { 55 | name: 'MyComponent', 56 | 57 | methods: { 58 | track () { 59 | page('/') 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | #### API 66 | - event 67 | - ecommerce 68 | - set 69 | - page 70 | - query 71 | - screenview 72 | - time 73 | - require 74 | - exception 75 | - social 76 | 77 | ## Track multiple accounts 78 | 79 | Pass an array of strings for a multiple tracking system. Every hit will be fired twice: each time with a different tracker name 80 | 81 | ```js 82 | import Vue from 'vue' 83 | import VueAnalytics from 'vue-analytics' 84 | 85 | Vue.use(VueAnalytics, { 86 | id: ['UA-XXX-A', 'UA-XXX-B'] 87 | }) 88 | ``` 89 | 90 | ## Use functions or/and Promises 91 | 92 | It is also possible to pass a function, a Promise or a function that returns a Promise: as soon as it returns always a string or an array of strings 93 | 94 | ```js 95 | import Vue from 'vue' 96 | import VueAnalytics from 'vue-analytics' 97 | import axios from 'axios' 98 | 99 | // a function 100 | Vue.use(VueAnalytics, { 101 | id () { 102 | return 'UA-XXX-A' 103 | } 104 | }) 105 | 106 | // a Promise 107 | Vue.use(VueAnalytics, { 108 | id: axios.get('/api/foo').then(response => { 109 | return response.data 110 | }) 111 | }) 112 | 113 | // a function that returns a Promise 114 | Vue.use(VueAnalytics, { 115 | id: () => axios.get('/api/foo').then(response => { 116 | return response.data 117 | }) 118 | }) 119 | ``` 120 | -------------------------------------------------------------------------------- /docs/opt-out.md: -------------------------------------------------------------------------------- 1 | ## Opt-out from Google Analytics 2 | 3 | It is possible to opt-out from Google Analytics by simply setting the `disabled` property to true. 4 | The `disabled` property accepts also a function, a promise or a function that returns a promise, but it needs to return a boolean. 5 | 6 | Take in mind that when using a promise, the plug-in won't start tracking until it's resolved, because the opt-out needs to happen before trackers or queues are initialized. 7 | 8 | If you are using more then one domain name, all of them will be disabled from tracking. 9 | 10 | **if you need to disable tracking just for development, is better to use the `sendHitTask` property in the `debug` object. Read more [here](/docs/turn-off-development.md)** 11 | 12 | ```js 13 | import Vue from 'vue' 14 | import VueAnalytics from 'vue-analytics' 15 | 16 | // boolean 17 | Vue.use(VueAnalytics, { 18 | id: 'UA-XXX-X', 19 | disabled: true 20 | }) 21 | 22 | // function 23 | Vue.use(VueAnalytics, { 24 | id: 'UA-XXX-X', 25 | disabled: () => { 26 | return true 27 | } 28 | }) 29 | 30 | // promise 31 | Vue.use(VueAnalytics, { 32 | id: 'UA-XXX-X', 33 | disabled: Promise.resolve(true) 34 | }) 35 | 36 | // function that returns a promise 37 | Vue.use(VueAnalytics, { 38 | id: 'UA-XXX-X', 39 | disabled: () => { 40 | return Promise.resolve(true) 41 | } 42 | }) 43 | ``` 44 | 45 | It is also possible to disable tracking from everywhere at any time using the `disable` method. 46 | 47 | ```js 48 | export default { 49 | methods: { 50 | disableTracking () { 51 | this.$ga.disable() 52 | // from now on analytics is disabled 53 | }, 54 | enableTracking () { 55 | this.$ga.enable() 56 | // from now on analytics is enabled 57 | } 58 | } 59 | } 60 | ``` 61 | or 62 | 63 | ```js 64 | Vue.$ga.disable() 65 | Vue.$ga.enable() 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/page-tracking.md: -------------------------------------------------------------------------------- 1 | ## Page tracking 2 | 3 | Page tracking is the most important feature of Google Analytics and you can achieve that in different ways 4 | 5 | ### Enable page auto tracking 6 | 7 | The most easy way to track your application, is to pass your VueRouter instance to the plugin and let it handle everything for you 8 | 9 | ```js 10 | import Vue from 'vue' 11 | import VueRouter from 'vue-router' 12 | import VueAnalytics from 'vue-analytics' 13 | 14 | const router = new VueRouter({ 15 | router: // your routes 16 | }) 17 | 18 | Vue.use(VueAnalytics, { 19 | id: 'UA-XXX-X', 20 | router 21 | }) 22 | ``` 23 | 24 | ### Manual page tracking 25 | 26 | The standard way is just passing the current page path 27 | 28 | ```js 29 | this.$ga.page('/') 30 | ``` 31 | 32 | passing as an object literal 33 | 34 | ```js 35 | this.$ga.page({ 36 | page: '/', 37 | title: 'Home page', 38 | location: window.location.href 39 | }) 40 | ``` 41 | 42 | or you can even pass the VueRouter instance scoped in your component and the plugin will automatically detect the current route name, path and location: just be sure to add the `name` property in your route object 43 | 44 | ```js 45 | this.$ga.page(this.$router) 46 | ``` 47 | 48 | Google Analytics docs: [page tracking](https://developers.google.com/analytics/devguides/collection/analyticsjs/pages) 49 | 50 | ### Use screenview with autotracking 51 | 52 | It is also possible to use autotracking and screen tracking by passing true to the `screenview` property in the `autoTracking` object 53 | 54 | ```js 55 | import Vue from 'vue' 56 | import VueAnalytics from 'vue-analytics' 57 | 58 | Vue.use(VueAnalytics, { 59 | id: 'UA-XXX-X', 60 | autoTracking: { 61 | screenview: true 62 | } 63 | }) 64 | ``` 65 | 66 | ### Disable pageview hit on page load 67 | 68 | Page auto tracking sends a pageview event on page load, but it is possible to disable that 69 | 70 | ```js 71 | import Vue from 'vue' 72 | import VueRouter from 'vue-router' 73 | import VueAnalytics from 'vue-analytics' 74 | 75 | const router = new VueRouter({ 76 | router: // your routes 77 | }) 78 | 79 | Vue.use(VueAnalytics, { 80 | id: 'UA-XXX-X', 81 | router, 82 | autoTracking: { 83 | pageviewOnLoad: false 84 | } 85 | }) 86 | ``` 87 | 88 | ### Disable page auto tracking 89 | 90 | To disable auto tracking we can just remove the VueRouter instance, but if you need to track only in specific environment or situations, it is also possible to disable page auto tracking like so 91 | 92 | ```js 93 | Vue.use(VueAnalytics, { 94 | id: 'UA-XXX-X', 95 | router, 96 | autoTracking: { 97 | page: false 98 | } 99 | }) 100 | ``` 101 | 102 | ### Ignore routes on page auto tracking 103 | 104 | To disable auto tracking for specific routes, you need to pass an array of strings to the plugin options. 105 | The string needs to be the route `name` or the route `path`. 106 | 107 | ```js 108 | Vue.use(VueAnalytics, { 109 | router, 110 | ignoreRoutes: ['home', '/contacts'] 111 | }) 112 | ``` 113 | 114 | ### Auto track with custom data 115 | 116 | When auto-tracking is possible to pass a function with a custom object shape to use as a tracker. 117 | 118 | The `pageViewTemplate` passes the current route as parameter 119 | 120 | ```js 121 | Vue.use(VueAnalytics, { 122 | id: 'UA-XXX-X', 123 | router, 124 | autoTracking: { 125 | pageviewTemplate (route) { 126 | return { 127 | page: route.path, 128 | title: document.title, 129 | location: window.location.href 130 | } 131 | } 132 | } 133 | }) 134 | ``` 135 | 136 | It is also possible to add custom data structure for each route, using the meta object 137 | 138 | ```js 139 | import Vue from 'vue' 140 | import VueAnalytics from 'vue-analytics' 141 | import VueRouter from 'vue-router' 142 | 143 | const router = new VueRouter({ 144 | routes: [ 145 | { 146 | name: 'home', 147 | path: '/', 148 | component: {...}, 149 | meta: { 150 | analytics: { 151 | pageviewTemplate (route) { 152 | return { 153 | title: 'This is my custom title', 154 | page: route.path, 155 | location: 'www.mydomain.com' 156 | } 157 | } 158 | } 159 | } 160 | } 161 | ] 162 | }) 163 | 164 | ``` 165 | important: the route pageviewTemplate has always priority over the global one. 166 | 167 | `pageviewTemplate` can return a falsy value to skip tracking, which can be useful for specific needs: 168 | 169 | - `shouldRouterUpdate` documented below is more appropriate for tracking control based on routing, but is not enough when you need to disable initial tracking on some pages, since it only applies to navigation after initial page load. 170 | - `pageviewOnLoad: false` is global and can’t depend on current route. 171 | 172 | ## Avoid transforming route query object into querystring 173 | It is possible to avoid route query to be sent as querystring using the `transformQueryString` property 174 | 175 | ```js 176 | Vue.use(VueAnalytics, { 177 | id: 'UA-XXX-X', 178 | router, 179 | autoTracking: { 180 | transformQueryString: false 181 | } 182 | }) 183 | ``` 184 | 185 | ## Remove vue-router base option 186 | When a base path is added to the VueRouter instance, the path is merged to the actual router path during the automatic tracking: however it is still possible to remove this behaviour modifying the `prependBase` property in the configuration object 187 | 188 | ```js 189 | Vue.use(VueAnalytics, { 190 | id: 'UA-XXX-X', 191 | router, 192 | autoTracking: { 193 | prependBase: false 194 | } 195 | }) 196 | ``` 197 | 198 | ## Customize router updates 199 | On every route change, the plugin will track the new route: when we change hashes, query strings or other parameters. 200 | 201 | To avoid router to update and start tracking when a route has the same path of the previous one, it is possible to use the `skipSamePath` property in the `autoTracking` object 202 | 203 | ```js 204 | Vue.use(VueAnalytics, { 205 | id: 'UA-XXX-X', 206 | router, 207 | autoTracking: { 208 | skipSamePath: true 209 | } 210 | }) 211 | ``` 212 | 213 | For other use cases it is also possible to use the `shouldRouterUpdate`, accessible in the plugin configuration object, inside the `autoTracking` property. 214 | The methods has the previous and current route as parameters and it needs to return a truthy or falsy value. 215 | 216 | ```js 217 | Vue.use(VueAnalytics, { 218 | id: 'UA-XXX-X', 219 | router, 220 | autoTracking: { 221 | shouldRouterUpdate (to, from) { 222 | // Here I'm allowing tracking only when 223 | // next route path is not the same as the previous 224 | return to.path !== from.path 225 | } 226 | } 227 | }) 228 | ``` 229 | -------------------------------------------------------------------------------- /docs/require.md: -------------------------------------------------------------------------------- 1 | ## Require 2 | 3 | It's possible to use this method to require an Analytics plugin or add your own plugin 4 | 5 | example adding [Google Optimize](https://optimize.google.com/optimize/home/#/accounts) 6 | 7 | ```js 8 | this.$ga.require('GMT-XXXXXXX') 9 | ``` 10 | 11 | or adding a custom plugin 12 | 13 | ```js 14 | const options = {} 15 | this.$ga.require('pluginName', options) 16 | ``` 17 | 18 | Google Analytics docs: [require](https://developers.google.com/analytics/devguides/collection/analyticsjs/command-queue-reference#require) 19 | 20 | -------------------------------------------------------------------------------- /docs/screen-tracking.md: -------------------------------------------------------------------------------- 1 | ## Screen tracking 2 | 3 | Screen hits can be sent using the `screenview` method. 4 | 5 | By passing a string it will track the page by sending a screenview event with the string as screenName property. 6 | 7 | ```js 8 | export default { 9 | name: 'MyComponent', 10 | 11 | methods: { 12 | track () { 13 | this.$ga.screenview('home') 14 | } 15 | } 16 | } 17 | ``` 18 | 19 | it is also possible to pass an object literal to fully customize the event 20 | 21 | ```js 22 | export default { 23 | name: 'MyComponent', 24 | 25 | methods: { 26 | track () { 27 | this.$ga.screenview({ 28 | screenName: 'home', 29 | ... // other properties 30 | }) 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | ## Screen autotracking 37 | 38 | It is also possible to use autotracking and screen tracking by passing true to the `screeview` property in the `autoTracking` object 39 | 40 | ```js 41 | import Vue from 'vue' 42 | import VueAnalytics from 'vue-analytics' 43 | 44 | Vue.use(VueAnalytics, { 45 | id: 'UA-XXX-X', 46 | autoTracking: { 47 | screenview: true 48 | } 49 | }) 50 | ``` 51 | 52 | **Route name property is mandatory** -------------------------------------------------------------------------------- /docs/script-loader.md: -------------------------------------------------------------------------------- 1 | ## How to load Google Analytics script 2 | 3 | **By default nothing is needed: the plugin does everything for you!** 4 | 5 | However, in some cases, you might want to avoid auto-loading the analytics.js script because: 6 | - maybe the framework you're using already does it for you 7 | - you really can't remove it from your project 8 | - other issue that I can't come up with 9 | 10 | So for all those cases it is possible to let the plugin detect if an analytics script has been added already in your html 11 | 12 | ```js 13 | import Vue from 'vue' 14 | import VueAnalytics from 'vue-analytics' 15 | 16 | Vue.use(VueAnalytics, { 17 | id: 'UA-XXX-X', 18 | checkDuplicatedScript: true 19 | }) 20 | ``` 21 | 22 | or just disable the script loader 23 | 24 | ```js 25 | import Vue from 'vue' 26 | import VueAnalytics from 'vue-analytics' 27 | 28 | Vue.use(VueAnalytics, { 29 | id: 'UA-XXX-X', 30 | disableScriptLoader: true 31 | }) 32 | ``` 33 | 34 | ### Important 35 | It is not possible for the plugin to remove the initial trackers, because it needs them to create all methods for the multi-tracking feature. 36 | **If you can't remove initial trackers, don't use this plugin: the outcome could be unpredictable.** 37 | -------------------------------------------------------------------------------- /docs/set.md: -------------------------------------------------------------------------------- 1 | ## Set 2 | 3 | Sets a single field and value pair or a group of field/value pairs on a tracker object. 4 | 5 | ```js 6 | this.$ga.set(fieldName, fieldValue) 7 | ``` 8 | 9 | also possible to pass an object literal 10 | 11 | ```js 12 | this.$ga.set({ fieldName: fieldValue }) 13 | ``` 14 | 15 | ### Set multiple fields before first hit 16 | Adding the `set` property to the configuration object, we can set multiple fields automatically before the first hit 17 | 18 | ```js 19 | import Vue from 'vue' 20 | import VueAnalytics from 'vue-analytics' 21 | 22 | Vue.use(VueAnalytics, { 23 | id: 'UA-XXX-X', 24 | set: [ 25 | { field: 'fieldname', value: 'fieldvalue' } 26 | ] 27 | }) 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/social-interactions.md: -------------------------------------------------------------------------------- 1 | ## Social interactions 2 | 3 | You can use social interaction analytics to measure the number of times users click on social buttons embedded in webpages. For example, you might measure a Facebook "Like" or a Twitter "Tweet". 4 | 5 | is possible to impletement this feature passing parameters in this exact order 6 | 7 | ```js 8 | this.$ga.social('Facebook', 'like', 'http://myownpersonaldomain.com') 9 | ``` 10 | 11 | also possible to pass an object literal 12 | 13 | ```js 14 | this.$ga.social({ 15 | socialNetwork: 'Facebook', 16 | socialAction: 'like', 17 | socialTarget: 'http://myownpersonaldomain.com' 18 | }) 19 | ``` 20 | 21 | Google Analytics docs: [social interactions](https://developers.google.com/analytics/devguides/collection/analyticsjs/social-interactions) 22 | 23 | -------------------------------------------------------------------------------- /docs/turn-off-development.md: -------------------------------------------------------------------------------- 1 | ## Turn off during development 2 | 3 | Stop sending hit to your domain during development 4 | 5 | Example: 6 | 7 | ```js 8 | Vue.use(VueAnalytics, { 9 | id: 'UA-XXX-X', 10 | debug: { 11 | sendHitTask: false 12 | } 13 | }) 14 | ``` 15 | 16 | or assign directly your NODE_ENV variable to enable/disable it automatically 17 | 18 | ```js 19 | Vue.use(VueAnalytics, { 20 | id: 'UA-XXX-X', 21 | debug: { 22 | sendHitTask: process.env.NODE_ENV === 'production' 23 | } 24 | }) 25 | ``` 26 | 27 | Google Analytics docs: [debugging](https://developers.google.com/analytics/devguides/collection/analyticsjs/debugging) 28 | -------------------------------------------------------------------------------- /docs/untracked-hits.md: -------------------------------------------------------------------------------- 1 | ## Untracked hits 2 | 3 | Due to different types of connections, loading Google Analytics script and having the application up and running at the same time can be difficult and leading to untracked hits. 4 | 5 | VueAnalytics takes care of this eventuality by storing all untracked events and track them later on, but if for some reasons you don't like that, is possible to turn this feature off 6 | 7 | ```js 8 | import Vue from 'vue' 9 | import VueAnalytics from 'vue-analytics' 10 | 11 | Vue.use(VueAnalytics, { 12 | id: 'UA-XXX-X', 13 | autotracking: { 14 | untracked: false 15 | } 16 | }) 17 | ``` -------------------------------------------------------------------------------- /docs/user-timings.md: -------------------------------------------------------------------------------- 1 | ## User timings 2 | 3 | User timing measurement can be achieved in different ways, following Google specifications 4 | 5 | passing parameters in this exact order 6 | 7 | ```js 8 | this.$ga.time('category', 'variable', 123, 'label') 9 | ``` 10 | 11 | or use an object literal 12 | 13 | ```js 14 | this.$ga.time({ 15 | timingCategory: 'category', 16 | timingVar: 'variable', 17 | timingValue: 123, 18 | timingLabel: 'label' 19 | }) 20 | ``` 21 | 22 | Google Analytics docs: [user timings](https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings) 23 | 24 | ## 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/v-ga.md: -------------------------------------------------------------------------------- 1 | ## v-ga 2 | 3 | This directive allows us to centralize all track events in one object and share it across the entire application without needs to add extra logic to our component methods 4 | 5 | Taking as an example a button that only has to log a name in the console 6 | 7 | ```html 8 | 11 | 12 | 27 | ``` 28 | 29 | To start tracking the value of `name` we can start by adding a method in the commands object that handles this action 30 | 31 | ```js 32 | import Vue from 'vue' 33 | import VueAnalytics from 'vue-analytics' 34 | 35 | Vue.use(VueAnalytics, { 36 | id: 'UA-XXX-X', 37 | commands: { 38 | trackName (name = 'unknown') { 39 | this.$ga.event('randomClicks', 'click', 'name', name) 40 | } 41 | } 42 | }) 43 | ``` 44 | Lets take in mind that every function we write in the `commands` object is also bound to the current component scope, so that we have access to the Vue instance prototype object and eventually to methods, data and computed values of the component itself. __(I do not recommend to use any component specific properties to avoid the method to be coupled to a specific component structure)__ 45 | 46 | Then we only need to add the `v-ga` directive to your element and access the method from the `commands` list that now is shared in the `$ga` object 47 | 48 | ```html 49 | 56 | 57 | 72 | ``` 73 | 74 | By default, the directive is executed when the element is clicked. However, if you want to change the event type for the logging, you can add the proper event as a modifier. 75 | 76 | ```html 77 | 82 | 83 | 98 | ``` 99 | 100 | If there's no need to pass any arguments, we could also just pass the name of the method as a string, and the plugin will look it up for us 101 | 102 | ```html 103 | 106 | 107 | 112 | ``` 113 | 114 | ### v-ga in v-for loops 115 | 'this' is not available on child elements in a v-for loop. To get the current component scope, use '$parent'. 116 | 117 | ```html 118 | 123 | 124 | 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/vuex.md: -------------------------------------------------------------------------------- 1 | ## Vuex and Google Analytics 2 | 3 | To be able to use vue-analytics from your Vuex store, just import the methods you need and fire it directly from the store 4 | 5 | ## First step 6 | Make sure to have your vue-analytics package installed **before** start using it in your store 7 | 8 | ```js 9 | // main.js 10 | import Vue from 'vue' 11 | import store from './store' 12 | import App from './App' 13 | import VueAnalytics from 'vue-analytics' 14 | 15 | Vue.use(VueAnalytics, { 16 | id: 'UA-xxxx-1' 17 | }) 18 | 19 | new Vue({ 20 | store, 21 | render: h => h(App) 22 | }).$mount('#app') 23 | ``` 24 | 25 | ## Second step 26 | Start using vue-analytics API in your store 27 | 28 | ```js 29 | // store.js 30 | import Vue from 'vue' 31 | import Vuex from 'vuex' 32 | import { event } from 'vue-analytics' 33 | 34 | Vue.use(Vuex) 35 | 36 | export default new Vuex.Store({ 37 | state: { 38 | counter: 0 39 | }, 40 | actions: { 41 | increase ({ commit, state }) { 42 | commit('increase', state.counter + 1) 43 | } 44 | }, 45 | mutations: { 46 | increase (state, payload) { 47 | state.counter = payload 48 | event('user-click', 'increase', 'counter', state.counter) 49 | } 50 | } 51 | }) 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/when-google-analytics-is-loaded.md: -------------------------------------------------------------------------------- 1 | ## Analytics ready! 2 | 3 | In the option object of the plugin there's a callback function available that fires when analytics.js or analytics\_debug.js is loaded 4 | 5 | always remember that the debug version is more heavy than the production one and might take more to load 6 | 7 | ```js 8 | import VueAnalytics from 'vue-analytics' 9 | 10 | Vue.use(VueAnalytics, { 11 | beforeFirstHit () { 12 | // this is right after the tracker and before every other hit to Google Analytics 13 | }, 14 | ready () { 15 | // here Google Analytics is ready to track! 16 | } 17 | }) 18 | ``` 19 | 20 | It is also possible to use the `onAnalyticsReady` method, which returns a promise. 21 | 22 | ```js 23 | import VueAnalytics, { onAnalyticsReady } from 'vue-analytics' 24 | 25 | Vue.use(VueAnalytics, { ... }) 26 | 27 | const App = new Vue({ ... }) 28 | 29 | onAnalyticsReady().then(() => { 30 | App.$mount('#app') 31 | }) 32 | ``` 33 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '^lib/(.*)$': '/src/lib/$1', 4 | '^directives/(.*)$': '/src/directives/$1', 5 | 'vue$': 'vue/dist/vue.min.js' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-analytics", 3 | "version": "0.0.0-development", 4 | "description": "Google Analytics plugin for Vue", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/MatteoGabriele/vue-analytics" 8 | }, 9 | "scripts": { 10 | "commit": "git-cz", 11 | "build": "webpack --mode production", 12 | "dev": "webpack --mode development --watch", 13 | "test": "jest", 14 | "prepublish": "webpack --mode production", 15 | "semantic-release": "semantic-release" 16 | }, 17 | "config": { 18 | "commitizen": { 19 | "path": "cz-conventional-changelog" 20 | } 21 | }, 22 | "keywords": [ 23 | "Google", 24 | "google", 25 | "Google Analytics", 26 | "google analytics", 27 | "tracking", 28 | "google tracking", 29 | "vue-analytics", 30 | "vue" 31 | ], 32 | "main": "dist/vue-analytics.js", 33 | "types": "vue-analytics.d.ts", 34 | "files": [ 35 | "dist", 36 | "vue-analytics.d.ts" 37 | ], 38 | "author": { 39 | "name": "Matteo Gabriele", 40 | "email": "m.gabriele.dev@gmail.com" 41 | }, 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/MatteoGabriele/vue-analytics/issues" 45 | }, 46 | "homepage": "https://github.com/MatteoGabriele/vue-analytics#readme", 47 | "devDependencies": { 48 | "@babel/core": "^7.7.2", 49 | "@babel/preset-env": "^7.7.1", 50 | "babel-loader": "^8.0.6", 51 | "commitizen": "^4.0.3", 52 | "compression-webpack-plugin": "^3.0.0", 53 | "cz-conventional-changelog": "^3.0.2", 54 | "jest": "^24.9.0", 55 | "semantic-release": "^15.13.30", 56 | "terser-webpack-plugin": "^2.2.1", 57 | "vue": "^2.6.10", 58 | "vue-router": "^3.1.3", 59 | "vuex": "^3.1.2", 60 | "webpack": "^4.41.2", 61 | "webpack-cli": "^3.3.10" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { promisify, loadScript, shouldGaLoad, log } from './helpers' 2 | import config, { update } from './config' 3 | import createTrackers from './create-trackers' 4 | import collectors from './collectors' 5 | import { autoTracking } from './lib/page' 6 | import untracked from './untracked' 7 | import noga from './no-ga' 8 | 9 | export default () => { 10 | if (typeof document === 'undefined' || typeof window === 'undefined') { 11 | return 12 | } 13 | 14 | if (!config.id) { 15 | log('Missing the "id" parameter. Add at least one tracking domain ID') 16 | return 17 | } 18 | 19 | const queue = [ 20 | promisify(config.id), 21 | promisify(config.disabled) 22 | ] 23 | 24 | if (shouldGaLoad()) { 25 | const domain = 'https://www.google-analytics.com' 26 | const filename = config.debug.enabled ? 'analytics_debug' : 'analytics' 27 | const resourcePromise = config.customResourceURL 28 | ? loadScript(config.customResourceURL) 29 | : loadScript(`${domain}/${filename}.js`, domain) 30 | 31 | queue.push( 32 | resourcePromise.catch(() => { 33 | log('An error occured! Please check your connection or disable your AD blocker') 34 | }) 35 | ) 36 | } 37 | 38 | return Promise.all(queue).then(response => { 39 | update({ 40 | id: response[0], 41 | disabled: response[1] 42 | }) 43 | 44 | // Opt-in/opt-out #gdpr 45 | noga(config.disabled) 46 | 47 | // Creates necessary trackers 48 | createTrackers() 49 | 50 | // Fires all shorthand fields in the options 51 | collectors() 52 | 53 | // Fires all untracked event that have been fired 54 | // meanwhile GoogleAnalayitcs script was loading 55 | untracked() 56 | 57 | // Starts auto tracking 58 | autoTracking() 59 | 60 | config.ready() 61 | }).catch(error => { 62 | if (!config.debug.enabled) { 63 | return 64 | } 65 | 66 | log(error.message) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/collectors.js: -------------------------------------------------------------------------------- 1 | import _require from './lib/require' 2 | import set from './lib/set' 3 | import config from './config' 4 | 5 | export const setters = function () { 6 | config.set.forEach(({ field, value }) => { 7 | if (typeof field === 'undefined' || typeof value === 'undefined') { 8 | throw new Error( 9 | '[vue-analytics] Wrong configuration in the plugin options.\n' + 10 | 'The "set" array requires each item to have a "field" and a "value" property.' 11 | ) 12 | } 13 | 14 | set(field, value) 15 | }) 16 | } 17 | 18 | export const requires = function () { 19 | const ecommerce = ['ec', 'ecommerce'] 20 | 21 | config.require.forEach(value => { 22 | if (ecommerce.indexOf(value) !== -1 || ecommerce.indexOf(value.name) !== -1) { 23 | throw new Error( 24 | '[vue-analytics] The ecommerce features are built-in in the plugin. \n' + 25 | 'Follow the ecommerce instructions available in the documentation.' 26 | ) 27 | } 28 | 29 | if (typeof value !== 'string' && typeof value !== 'object') { 30 | throw new Error( 31 | '[vue-analytics] Wrong configuration in the plugin options. \n' + 32 | 'The "require" array requires each item to be a string or to have a "name" and an "options" property.' 33 | ) 34 | } 35 | 36 | const pluginName = value.name || value 37 | 38 | if (value.options) { 39 | _require(pluginName, value.options) 40 | return 41 | } 42 | 43 | _require(pluginName) 44 | }) 45 | } 46 | 47 | export default function () { 48 | setters() 49 | requires() 50 | } -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import { merge, noop } from './helpers' 2 | 3 | const defaultConfig = { 4 | $vue: null, 5 | id: null, 6 | router: null, 7 | fields: {}, 8 | customIdFields: {}, 9 | ignoreRoutes: [], 10 | linkers: [], 11 | commands: {}, 12 | 13 | // https://github.com/MatteoGabriele/vue-analytics/issues/103 14 | disabled: false, 15 | 16 | customResourceURL: null, 17 | 18 | set: [], 19 | require: [], 20 | 21 | ecommerce: { 22 | enabled: false, 23 | options: null, 24 | enhanced: false 25 | }, 26 | 27 | autoTracking: { 28 | screenview: false, 29 | shouldRouterUpdate: null, 30 | skipSamePath: false, 31 | exception: false, 32 | exceptionLogs: true, 33 | page: true, 34 | transformQueryString: true, 35 | pageviewOnLoad: true, 36 | pageviewTemplate: null, 37 | untracked: true, 38 | prependBase: true 39 | }, 40 | 41 | debug: { 42 | enabled: false, 43 | trace: false, 44 | sendHitTask: true 45 | }, 46 | 47 | batch: { 48 | enabled: false, 49 | delay: 500, 50 | amount: 2 51 | }, 52 | 53 | checkDuplicatedScript: false, 54 | disableScriptLoader: false, 55 | 56 | beforeFirstHit: noop, 57 | ready: noop, 58 | 59 | untracked: [] 60 | } 61 | 62 | let config = { ...defaultConfig } 63 | 64 | export function update (params) { 65 | merge(config, params) 66 | } 67 | 68 | export function reset () { 69 | config = { ...defaultConfig } 70 | } 71 | 72 | export function getId () { 73 | return !config.id ? [] : [].concat(config.id) 74 | } 75 | 76 | export default config 77 | -------------------------------------------------------------------------------- /src/create-trackers.js: -------------------------------------------------------------------------------- 1 | import set from './lib/set' 2 | import query from './lib/query' 3 | import config, { getId } from './config' 4 | import { log, getTracker } from './helpers' 5 | 6 | export default function createTrackers () { 7 | if (!window.ga && config.debug.enabled) { 8 | log('Google Analytics has probably been blocked.') 9 | return 10 | } 11 | 12 | if (!window.ga) { 13 | return 14 | } 15 | 16 | const ids = getId() 17 | 18 | if (config.debug.enabled) { 19 | window.ga_debug = { 20 | trace: config.debug.trace 21 | } 22 | } 23 | 24 | ids.forEach(function (domain) { 25 | const name = getTracker(domain) 26 | const customIdConfig = config.customIdFields[domain] || {} 27 | const options = ids.length > 1 ? { ...config.fields, ...customIdConfig, name } : config.fields 28 | 29 | window.ga('create', (domain.id || domain), 'auto', options) 30 | }) 31 | 32 | config.beforeFirstHit() 33 | 34 | const { ecommerce } = config 35 | 36 | if (ecommerce.enabled) { 37 | const plugin = ecommerce.enhanced ? 'ec' : 'ecommerce' 38 | 39 | if (ecommerce.options) { 40 | query('require', plugin, ecommerce.options) 41 | } else { 42 | query('require', plugin) 43 | } 44 | } 45 | 46 | if (config.linkers.length > 0) { 47 | query('require', 'linker') 48 | query('linker:autoLink', config.linkers) 49 | } 50 | 51 | if (!config.debug.sendHitTask) { 52 | set('sendHitTask', null) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/directives/ga.js: -------------------------------------------------------------------------------- 1 | import config from '../config' 2 | 3 | export default { 4 | inserted: function (el, binding, vnode) { 5 | const events = Object.keys(binding.modifiers) 6 | 7 | if (events.length === 0) { 8 | events.push('click') 9 | } 10 | 11 | events.forEach(event => { 12 | el.addEventListener(event, function () { 13 | let fn = typeof binding.value === 'string' 14 | ? config.commands[binding.value] 15 | : binding.value 16 | 17 | if (!fn) { 18 | throw new Error( 19 | '[vue-analytics] The value passed to v-ga is not defined in the commands list.' 20 | ) 21 | } 22 | 23 | fn.apply(vnode.context) 24 | }) 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | import config, { getId } from './config' 2 | 3 | export function noop () {} 4 | 5 | export const log = message => { 6 | console.warn(`[vue-analytics] ${message}`) 7 | } 8 | 9 | export function loadScript (url, domain) { 10 | return new Promise((resolve, reject) => { 11 | const head = document.head || document.getElementsByTagName('head')[0] 12 | const script = document.createElement('script') 13 | 14 | script.async = true 15 | script.src = url 16 | script.charset = 'utf-8' 17 | 18 | if (domain) { 19 | const link = document.createElement('link') 20 | 21 | link.href = domain 22 | link.rel = 'preconnect' 23 | 24 | head.appendChild(link) 25 | } 26 | 27 | head.appendChild(script) 28 | 29 | script.onload = resolve 30 | script.onerror = reject 31 | }) 32 | } 33 | 34 | export function getBasePath (base, path) { 35 | const pathAsArray = path.split('/') 36 | const baseAsArray = base.split('/') 37 | 38 | if (pathAsArray[0] === '' && base[base.length - 1] === '/') { 39 | pathAsArray.shift() 40 | } 41 | 42 | return baseAsArray.join('/') + pathAsArray.join('/') 43 | } 44 | 45 | export function merge (obj, src) { 46 | Object.keys(src).forEach(function (key) { 47 | const type = obj[key] && Object.prototype.toString.call(obj[key]) 48 | 49 | if (type === '[object Object]' || type === '[object Array]') { 50 | merge(obj[key], src[key]) 51 | return 52 | } 53 | obj[key] = src[key] 54 | }) 55 | 56 | return obj 57 | } 58 | 59 | export function hasScript () { 60 | const scriptTags = Array.prototype.slice.call( 61 | document.getElementsByTagName('script') 62 | ).filter(script => { 63 | return (script.src.indexOf('analytics') !== -1) || 64 | (script.src.indexOf('gtag') !== -1) 65 | }) 66 | 67 | return scriptTags.length > 0 68 | } 69 | 70 | export function shouldGaLoad () { 71 | const { 72 | checkDuplicatedScript, 73 | disableScriptLoader 74 | } = config 75 | 76 | const requires = [ 77 | Boolean(window && window.ga), 78 | (checkDuplicatedScript && !hasScript()), 79 | !disableScriptLoader 80 | ] 81 | 82 | return requires.some(Boolean) 83 | } 84 | 85 | export function getTracker (tracker) { 86 | return tracker.name || tracker.replace(/-/gi, '') 87 | } 88 | 89 | export function onAnalyticsReady () { 90 | return new Promise((resolve, reject) => { 91 | const poll = setInterval(() => { 92 | if (typeof window === 'undefined' || !window.ga) { 93 | return 94 | } 95 | 96 | resolve() 97 | clearInterval(poll) 98 | }, 10) 99 | }) 100 | } 101 | 102 | export function getMethod (name, trackerId) { 103 | if (getId().length > 1) { 104 | const domain = getTracker(trackerId) 105 | return `${domain}.${name}` 106 | } 107 | 108 | return name 109 | } 110 | 111 | export function getQueryString (queryMap) { 112 | const queryString = Object.keys(queryMap) 113 | .reduce((string, key, index, keys) => { 114 | const isLastKey = index === (keys.length - 1) 115 | const value = queryMap[key] 116 | 117 | if (value == null) { 118 | return string 119 | } 120 | 121 | string += `${key}=${value}${isLastKey ? '' : '&'}` 122 | return string 123 | }, '') 124 | 125 | return queryString !== '' ? `?${queryString}` : '' 126 | } 127 | 128 | export function isRouteIgnored ({ name, path }) { 129 | return [name, path] 130 | .filter(Boolean) 131 | .find(value => config.ignoreRoutes.indexOf(value) !== -1) 132 | } 133 | 134 | export function isRoute (data) { 135 | return data.query && data.params 136 | } 137 | 138 | export function isRouter (data) { 139 | return data.currentRoute 140 | } 141 | 142 | export const promisify = value => { 143 | if (value.then) { 144 | return value 145 | } 146 | 147 | if (typeof value === 'function') { 148 | const payload = value() 149 | 150 | return payload.then ? payload : Promise.resolve(payload) 151 | } 152 | 153 | return Promise.resolve(value) 154 | } 155 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import bootstrap from './bootstrap' 2 | import lib from './lib' 3 | import { update } from './config' 4 | import * as helpers from './helpers' 5 | import ga from './directives/ga' 6 | import { autotracking as expectionAutotracking } from './lib/exception' 7 | import vuexMiddleware from './vuex-middleware' 8 | 9 | export default function install (Vue, options = {}) { 10 | update({ ...options, $vue: Vue }) 11 | 12 | Vue.directive('ga', ga) 13 | Vue.prototype.$ga = Vue.$ga = lib 14 | 15 | expectionAutotracking(Vue) 16 | bootstrap() 17 | } 18 | 19 | // Vuex middleware 20 | export const analyticsMiddleware = vuexMiddleware 21 | 22 | // Helpers 23 | export const onAnalyticsReady = helpers.onAnalyticsReady 24 | 25 | // Event library 26 | export const event = lib.event 27 | export const ecommerce = lib.ecommerce 28 | export const set = lib.set 29 | export const page = lib.page 30 | export const query = lib.query 31 | export const screenview = lib.screenview 32 | export const time = lib.time 33 | export const require = lib.require 34 | export const exception = lib.exception 35 | export const social = lib.social 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/lib/ecommerce.js: -------------------------------------------------------------------------------- 1 | import query from './query' 2 | import config from '../config' 3 | 4 | const getMethod = function (name) { 5 | return `${config.ecommerce.enhanced ? 'ec' : 'ecommerce'}:${name}` 6 | } 7 | 8 | const featuresList = [ 9 | 'addItem', 10 | 'addTransaction', 11 | 'addProduct', 12 | 'addImpression', 13 | 'setAction', 14 | 'addPromo', 15 | 'send' 16 | ] 17 | 18 | export default featuresList.reduce((coll, feature) => { 19 | return { 20 | ...coll, 21 | [feature]: (...args) => { 22 | query(getMethod(feature), ...args) 23 | } 24 | } 25 | }, {}) -------------------------------------------------------------------------------- /src/lib/event.js: -------------------------------------------------------------------------------- 1 | import query from './query' 2 | 3 | export default function event (...args) { 4 | query('send', 'event', ...args) 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/exception.js: -------------------------------------------------------------------------------- 1 | import query from './query' 2 | import config from '../config' 3 | 4 | const exception = (error, fatal = false) => { 5 | query('send', 'exception', { 6 | exDescription: error, 7 | exFatal: fatal 8 | }) 9 | } 10 | 11 | export const autotracking = Vue => { 12 | if (!config.autoTracking.exception) { 13 | return 14 | } 15 | 16 | // Handler errors outside Vue components 17 | window.addEventListener('error', error => { 18 | exception(error.message) 19 | }) 20 | 21 | // Save the Vue.config.errorHandler if one already registered 22 | const oldErrorHandler = Vue.config.errorHandler 23 | 24 | // Handles errors inside component life 25 | Vue.config.errorHandler = (error, vm, info) => { 26 | exception(error.message) 27 | 28 | if (config.autoTracking.exceptionLogs) { 29 | console.error(error) 30 | } 31 | 32 | if (typeof oldErrorHandler === 'function') { 33 | oldErrorHandler.call(this, error, vm, info) 34 | } 35 | } 36 | } 37 | 38 | export default exception 39 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | import event from './event' 2 | import exception from './exception' 3 | import page from './page' 4 | import query from './query' 5 | import require from './require' 6 | import set from './set' 7 | import social from './social' 8 | import time from './time' 9 | import ecommerce from './ecommerce' 10 | import screenview from './screenview' 11 | import config from '../config' 12 | import noga from '../no-ga' 13 | 14 | export default { 15 | event, 16 | exception, 17 | page, 18 | query, 19 | require, 20 | set, 21 | social, 22 | time, 23 | screenview, 24 | ecommerce, 25 | disable: () => noga(true), 26 | enable: () => noga(false), 27 | commands: config.commands 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/page.js: -------------------------------------------------------------------------------- 1 | import config from '../config' 2 | import set from './set' 3 | import screenview from './screenview' 4 | import query from './query' 5 | import { 6 | getQueryString, 7 | isRouteIgnored, 8 | isRoute, 9 | isRouter, 10 | getBasePath 11 | } from '../helpers' 12 | 13 | export default function page (...args) { 14 | if (args.length && !args[0]) { 15 | // allows to dynamically prevent tracking in pageviewTemplate proxy 16 | return 17 | } 18 | 19 | let route 20 | 21 | if (args.length && isRouter(args[0])) { 22 | route = args[0].currentRoute 23 | } 24 | 25 | if (args.length && isRoute(args[0])) { 26 | route = args[0] 27 | } 28 | 29 | if (route) { 30 | trackRoute(route) 31 | } else { 32 | // We can call with `page('/my/path')` 33 | let page = typeof args[0] === 'object' ? args[0].page : args[0] 34 | set('page', page) 35 | query('send', 'pageview', ...args) 36 | } 37 | } 38 | 39 | export function trackRoute (route) { 40 | if (isRouteIgnored(route)) { 41 | return 42 | } 43 | 44 | const { autoTracking } = config 45 | const { meta: { analytics = {} } } = route 46 | const proxy = analytics.pageviewTemplate || autoTracking.pageviewTemplate 47 | 48 | if (autoTracking.screenview && !route.name) { 49 | throw new Error( 50 | '[vue-analytics] Route name is mandatory when using screenview.' 51 | ) 52 | } 53 | 54 | if (autoTracking.screenview) { 55 | screenview(route.name) 56 | return 57 | } 58 | 59 | if (proxy) { 60 | page(proxy(route)) 61 | } else { 62 | const { 63 | router, 64 | autoTracking: { 65 | transformQueryString, 66 | prependBase 67 | } 68 | } = config 69 | 70 | const queryString = getQueryString(route.query) 71 | const base = router && router.options.base 72 | const needsBase = prependBase && base 73 | 74 | let path = route.path + (transformQueryString ? queryString : '') 75 | path = needsBase ? getBasePath(base, path) : path 76 | 77 | page(path) 78 | } 79 | } 80 | 81 | export function autoTracking () { 82 | const { router, autoTracking, $vue } = config 83 | 84 | if (!autoTracking.page || !router) { 85 | return 86 | } 87 | 88 | router.onReady(() => { 89 | if (autoTracking.pageviewOnLoad && router.history.ready) { 90 | trackRoute(router.currentRoute) 91 | } 92 | 93 | router.afterEach(function (to, from) { 94 | const { skipSamePath, shouldRouterUpdate } = autoTracking 95 | 96 | // Default behaviour of the router when the `skipSamePath` is turned on. 97 | // Skip router change when current and previous route have the same path 98 | // https://github.com/MatteoGabriele/vue-analytics/issues/73 99 | if (skipSamePath && to.path === from.path) { 100 | return 101 | } 102 | 103 | // Adds a custom way to define when the router should track 104 | if (typeof shouldRouterUpdate === 'function' && !shouldRouterUpdate(to, from)) { 105 | return 106 | } 107 | 108 | // see https://github.com/nuxt-community/analytics-module/issues/8 109 | $vue.nextTick().then(() => { 110 | trackRoute(router.currentRoute) 111 | }) 112 | }) 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /src/lib/query.js: -------------------------------------------------------------------------------- 1 | import config, { getId } from '../config' 2 | import { getMethod } from '../helpers' 3 | 4 | let intr 5 | let coll = [] 6 | 7 | export default function query (method, ...args) { 8 | if (typeof window === 'undefined') { 9 | return 10 | } 11 | 12 | getId().forEach(function (id) { 13 | const t = { 14 | m: getMethod(method, id), 15 | a: args 16 | } 17 | 18 | if(!window.ga) { 19 | config.untracked.push(t) 20 | return 21 | } 22 | 23 | if (config.batch.enabled) { 24 | coll.push(t) 25 | 26 | if (!intr) { 27 | intr = setInterval(() => { 28 | if (!coll.length) { 29 | clearInterval(intr) 30 | intr = null 31 | } else { 32 | coll.splice(0, config.batch.amount).forEach(q => { 33 | window.ga(q.m, ...q.a) 34 | }) 35 | } 36 | }, config.batch.delay) 37 | } 38 | } else { 39 | window.ga(getMethod(method, id), ...args) 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/require.js: -------------------------------------------------------------------------------- 1 | import query from './query' 2 | 3 | export default function (...args) { 4 | if (args.length == 2) { 5 | query('require', args[0], args[1]) 6 | return 7 | } 8 | 9 | query('require', args[0]) 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/screenview.js: -------------------------------------------------------------------------------- 1 | import query from './query' 2 | 3 | export default function screenview (...args) { 4 | const [screenName] = args 5 | 6 | if (args.length === 1 && typeof screenName === 'string') { 7 | return query('send', 'screenview', { screenName }) 8 | } 9 | 10 | query('send', 'screenview', ...args) 11 | } -------------------------------------------------------------------------------- /src/lib/set.js: -------------------------------------------------------------------------------- 1 | import query from './query' 2 | 3 | export default function set (...args) { 4 | if (typeof args[0] === 'object' && args[0].constructor === Object) { 5 | query('set', args[0]) 6 | return 7 | } 8 | 9 | query('set', args[0], args[1]) 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/social.js: -------------------------------------------------------------------------------- 1 | import query from './query' 2 | 3 | export default function social (...args) { 4 | query('send', 'social', ...args) 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/time.js: -------------------------------------------------------------------------------- 1 | import query from './query' 2 | 3 | export default function time (...args) { 4 | query('send', 'timing', ...args) 5 | } 6 | -------------------------------------------------------------------------------- /src/no-ga.js: -------------------------------------------------------------------------------- 1 | import { getId } from './config' 2 | 3 | export default (disable = true) => { 4 | if (typeof window === 'undefined') { 5 | return 6 | } 7 | 8 | getId().forEach(id => { 9 | window[`ga-disable-${id}`] = disable 10 | }) 11 | } -------------------------------------------------------------------------------- /src/untracked.js: -------------------------------------------------------------------------------- 1 | import config from './config' 2 | import query from './lib/query' 3 | 4 | export default () => { 5 | config.untracked.forEach(t => { 6 | query(t.m, ...t.a) 7 | }) 8 | } -------------------------------------------------------------------------------- /src/vuex-middleware.js: -------------------------------------------------------------------------------- 1 | import lib from './lib' 2 | 3 | export default store => { 4 | store.subscribe(({ payload }) => { 5 | if (!payload || !payload.meta || !payload.meta.analytics) { 6 | return 7 | } 8 | 9 | const { analytics } = payload.meta 10 | 11 | if (!Array.isArray(analytics)) { 12 | throw new Error('The "analytics" property needs to be an array') 13 | } 14 | 15 | analytics.forEach(event => { 16 | let method 17 | let type = event.shift() 18 | 19 | const props = event 20 | 21 | if (type.includes(':')) { 22 | [type, method] = type.split(':') 23 | } 24 | 25 | if (!(type in lib)) { 26 | throw new Error( 27 | `[vue-analytics:vuex] The type "${type}" doesn't exist.` 28 | ) 29 | } 30 | 31 | if (method && !(method in lib[type])) { 32 | throw new Error( 33 | `[vue-analytics:vuex] The type "${type}" has not method "${method}".` 34 | ) 35 | } 36 | 37 | if (type === 'ecommerce' && !method) { 38 | throw new Error( 39 | `[vue-analytics:vuex] The type "${type}" needs to call a method. Check documentation.` 40 | ) 41 | } 42 | 43 | if (method) { 44 | lib[type][method](...props) 45 | } else { 46 | lib[type](...props) 47 | } 48 | }) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /vue-analytics.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module 'vue-analytics' { 3 | import _Vue, { PluginFunction } from 'vue'; 4 | import VueRouter, { Route } from 'vue-router'; 5 | import { Store } from 'vuex'; 6 | 7 | interface EventPayload { 8 | eventCategory: string; 9 | eventAction?: string; 10 | eventLabel?: string; 11 | eventValue?: number; 12 | } 13 | 14 | interface eventFn { 15 | (category: string, action?: string, label?: string, value?: number): void; 16 | (options: EventPayload): void; 17 | } 18 | 19 | type pageDetails = { 20 | page: string, 21 | title: string, 22 | location: string 23 | }; 24 | 25 | interface pageFn { 26 | (path: string): void; 27 | (options: pageDetails): void; 28 | (route: VueRouter): void; 29 | } 30 | 31 | interface SetFieldValue { 32 | field: string; 33 | value: any; 34 | } 35 | 36 | interface setFn { 37 | (fieldName: string, fieldValue: any): void; 38 | (options: Record): void; 39 | } 40 | 41 | interface SocialPayload { 42 | socialNetwork: string; 43 | socialAction: string; 44 | socialTarget: string; 45 | } 46 | 47 | interface socialFn { 48 | (network: string, action: string, target: string): void; 49 | (options: SocialPayload): void; 50 | } 51 | 52 | interface TimePayload { 53 | timingCategory: string; 54 | timingVar: string; 55 | timingValue: number; 56 | timingLabel: string; 57 | } 58 | 59 | interface timeFn { 60 | (category: string, variable: string, value: number, label: string): void; 61 | (options: TimePayload): void; 62 | } 63 | 64 | interface EcommerceItem { 65 | id: string; 66 | name: string; 67 | sku?: string; 68 | category?: string; 69 | price?: string; 70 | quantity?: number; 71 | } 72 | 73 | interface EcommerceTransaction { 74 | id: string; 75 | affiliation?: string; 76 | revenue?: string; 77 | shipping?: string; 78 | tax?: string; 79 | } 80 | 81 | interface EcommerceImpressionBase { 82 | list?: string; 83 | brand?: string; 84 | category?: string; 85 | variant?: string; 86 | position?: number; 87 | price?: string; 88 | } 89 | 90 | interface EcommerceImpressionWithId extends EcommerceImpressionBase { 91 | id: string; 92 | } 93 | 94 | interface EcommerceImpressionWithName extends EcommerceImpressionBase { 95 | name: string; 96 | } 97 | 98 | type EcommerceImpression = EcommerceImpressionWithId | EcommerceImpressionWithName; 99 | 100 | interface EcommerceProductBase { 101 | brand?: string; 102 | category?: string; 103 | variant?: string; 104 | price?: string; 105 | quantity?: number; 106 | coupon?: string; 107 | position?: number; 108 | } 109 | 110 | interface EcommerceProductWithId extends EcommerceProductBase { 111 | id: string; 112 | } 113 | 114 | interface EcommerceProductWithName extends EcommerceProductBase { 115 | name: string; 116 | } 117 | 118 | type EcommerceProduct = EcommerceImpressionWithId | EcommerceImpressionWithName; 119 | 120 | type EcommerceAction = 121 | | 'click' 122 | | 'detail' 123 | | 'add' 124 | | 'remove' 125 | | 'checkout' 126 | | 'checkout_option' 127 | | 'purchase' 128 | | 'refund' 129 | | 'promo_click' 130 | 131 | interface EcommerceActionData { 132 | id?: string; 133 | affiliation?: string; 134 | revenue?: number; 135 | tax?: number; 136 | shipping?: number; 137 | coupon?: string; 138 | list?: string; 139 | step?: number; 140 | option?: string; 141 | } 142 | 143 | interface EcommercePromoBase { 144 | creative?: string; 145 | position?: string; 146 | } 147 | 148 | interface EcommercePromoWithId extends EcommercePromoBase { 149 | id: string; 150 | } 151 | 152 | interface EcommercePromoWithName extends EcommercePromoBase { 153 | name: string; 154 | } 155 | 156 | type EcommercePromo = EcommercePromoWithId | EcommercePromoWithName; 157 | 158 | interface Ecommerce { 159 | addItem(item: EcommerceItem): void; 160 | addTransaction(transaction: EcommerceTransaction): void; 161 | addProduct(product: EcommerceProduct): void; 162 | addImpression(impression: EcommerceImpression): void; 163 | setAction(action: EcommerceAction, data: EcommerceActionData): void; 164 | addPromo(product: EcommercePromo): void; 165 | send(): void; 166 | } 167 | 168 | interface ScreenViewPayload { 169 | screenName: string; 170 | [otherProperties: string]: any; 171 | } 172 | 173 | interface screenviewFn { 174 | (screen: string) :void; 175 | (option: ScreenViewPayload): void; 176 | } 177 | 178 | interface requireFn { 179 | (pluginName: string, options?: any): void 180 | } 181 | 182 | interface exceptionFn { 183 | (exception: Error | string): void; 184 | } 185 | 186 | interface queryFn { 187 | (...args: any[]): any; 188 | } 189 | 190 | interface analyticsMiddlewareFn { 191 | (store: Store): void; 192 | } 193 | 194 | interface onAnalyticsReadyFn { 195 | (): Promise; 196 | } 197 | 198 | export interface InstallOptions { 199 | id: string | string[] | (() => string) | (() => Promise) | Promise, 200 | router?: VueRouter, 201 | ignoreRoutes?: string[], 202 | debug?: { 203 | enabled?: boolean, 204 | trace?: boolean, 205 | sendHitTask?: boolean 206 | }, 207 | batch?: { 208 | enabled?: boolean, 209 | amount?: number, 210 | delay?: number 211 | }, 212 | linkers?: string[], 213 | customResourceURL?: string, 214 | ecommerce?: { 215 | enabled?: boolean, 216 | enhanced?: boolean, 217 | options?: any 218 | }, 219 | autoTracking?: { 220 | exception?: boolean, 221 | exceptionLogs?: boolean, 222 | screenview?: boolean, 223 | pageviewOnLoad?: boolean, 224 | page?: boolean, 225 | pageviewTemplate?: (route: Route) => pageDetails, 226 | transformQueryString?: boolean, 227 | prependBase?: boolean, 228 | skipSamePath: boolean, 229 | shouldRouterUpdate: (to: Route, from: Route) => string, 230 | untracked?: boolean 231 | }, 232 | fields?: { 233 | [field: string]: any 234 | }, 235 | customIdFields?: { 236 | [id: string]: { 237 | [field: string]: any 238 | } 239 | }, 240 | disabled?: boolean | (() => boolean) | (() => Promise) | Promise, 241 | checkDuplicatedScript?: boolean, 242 | disableScriptLoader?: boolean 243 | set?: SetFieldValue[], 244 | commands?: any, 245 | beforeFirstHit?: () => void, 246 | ready?: () => void 247 | } 248 | 249 | export default class VueAnalytics { 250 | static install(Vue: typeof _Vue, options: InstallOptions): void; 251 | analyticsMiddleware(store: Store): void; 252 | onAnalyticsReady: onAnalyticsReadyFn; 253 | event: eventFn; 254 | ecommerce: Ecommerce; 255 | set: setFn; 256 | page: pageFn; 257 | query: queryFn; 258 | screenview: screenviewFn; 259 | time: timeFn; 260 | require: requireFn; 261 | exception: exceptionFn; 262 | social: socialFn; 263 | disable: () => void; 264 | enable: () => void; 265 | } 266 | 267 | export const analyticsMiddleware: analyticsMiddlewareFn; 268 | export const onAnalyticsReady: onAnalyticsReadyFn; 269 | export const event: eventFn; 270 | export const ecommerce: Ecommerce; 271 | export const set: setFn; 272 | export const page: pageFn; 273 | export const query: queryFn; 274 | export const screenview: screenviewFn; 275 | export const time: timeFn; 276 | export const require: requireFn; 277 | export const exception: exceptionFn; 278 | export const social: socialFn; 279 | 280 | module 'vue/types/options' { 281 | interface ComponentOptions { 282 | ga?: VueAnalytics; 283 | } 284 | } 285 | 286 | module 'vue/types/vue' { 287 | interface Vue { 288 | $ga: VueAnalytics; 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const TerserPlugin = require('terser-webpack-plugin') 3 | const CompressionPlugin = require('compression-webpack-plugin') 4 | 5 | module.exports = function (env, argv) { 6 | const isProduction = argv.mode === 'production' 7 | 8 | return { 9 | output: { 10 | libraryTarget: 'umd', 11 | filename: 'vue-analytics.js', 12 | globalObject: 'typeof self !== \'undefined\' ? self : this' 13 | }, 14 | optimization: { 15 | minimize: isProduction, 16 | minimizer: [ 17 | new TerserPlugin({ 18 | terserOptions: { 19 | compress: isProduction 20 | } 21 | }) 22 | ] 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.js$/, 28 | include: path.resolve(__dirname, './src'), 29 | loader: 'babel-loader' 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | ...(isProduction ? [new CompressionPlugin()] : []) 35 | ] 36 | } 37 | } --------------------------------------------------------------------------------