diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7f7200c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +## 1.2 - December 2016 + +- Refactored code organisation in separated modules for simplicity. +- Support for changing language during app navigation, useful in case of i18n apps. +- Support for metrics +- Support for exceptions + diff --git a/README.md b/README.md index 6120bba..cbfbe55 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,16 @@


-Dispatch events and track views from Vue components. +This plugin will helps you in your common analytics tasks. Dispatching events, register some dimensions, metric and track views from Vue components. - +# Requirements -This is a work in progress, this is stable for production but breaking changes may be introduced through minor revisions, but never in patch version. +- **Vue.js.** >= 2.0.0 +- **Google Analytics account.** To send data to -# Requirements +**Optionnals dependencies** -- **Vue.js.** > 2.0.0 -- **Google Analytics account.** To retrieve Data +- **Vue Router** >= 2.x - In order to use auto-tracking of screens # Configuration @@ -35,22 +35,26 @@ import VueRouter from 'vue-router' const router = new VueRouter({routes, mode, linkActiveClass}) Vue.use(VueAnalytics, { - appName: '', - appVersion: '', - trackingId: '', - debug: true, // Whether or not display console logs (optional) + appName: '', // Mandatory + appVersion: '', // Mandatory + trackingId: '', // Mandatory + debug: true, // Whether or not display console logs debugs (optional) vueRouter: router, // Pass the router instance to automatically sync with router (optional) - ignoredViews: ['homepage'], // If router, you can exclude some routes name (case insensitive) - globalDimensions: [ + ignoredViews: ['homepage'], // If router, you can exclude some routes name (case insensitive) (optional) + globalDimensions: [ // Optional {dimension: 1, value: 'MyDimensionValue'}, {dimension: 2, value: 'AnotherDimensionValue'} - ] + ], + globalMetrics: [ // Optional + {metric: 1, value: 'MyMetricValue'}, + {metric: 2, value: 'AnotherMetricValue'} + ] }) ``` # Documentation -Once registered you can access vue analytics in your components like this : +Once the configuration is completed, you can access vue analytics instance in your components like that : ```javascript export default { @@ -73,17 +77,35 @@ export default { } ``` -You can also access the instance everywhere using `Vue.analytics`, it's useful when you are in the store or somewhere else than components. +You can also access the instance anywhere whenever you imported `Vue` by using `Vue.analytics`. It is especially useful when you are in a store module or +somewhere else than a component's scope. + +## Sync analytics with your router + +Thanks to vue-router guards, you can automatically dispatch new screen views on router change ! +To use this feature, you just need to inject the router instance on plugin initialization. -## Using vue-router guards +This feature will generate the view name according to a priority rule : +- If you defined a meta field for you route named `analytics` this will take the value of this field for the view name. +- Otherwise, if the plugin don't have a value for the `meta.analytics` it will fallback to the internal route name. -You can automatically dispatch new screen views on router change, to do this simply pass the router instance on plugin initialization. -At the moment, this is using the `route name` to name the HIT, but this is going to be updated to allow you to specify whatever values you wants. +Most of time the second case is enough, but sometimes you want to have more control on what is sent, this is where the first rule shine. +Example : +```javascript +const myRoute = { + path: 'myRoute', + name: 'MyRouteName', + component: SomeComponent, + meta: {analytics: 'MyCustomValue'} +} +``` + +> This will use `MyCustomValue` as the view name. ## API reference -### trackEvent (category, action = null, label = null, value = null, fieldsObject = {}) +### trackEvent (category, action = null, label = null, value = null) ```javascript /** * Dispatch an analytics event. @@ -93,7 +115,6 @@ At the moment, this is using the `route name` to name the HIT, but this is going * @param action * @param label * @param value - * @param fieldsObject */ ``` @@ -115,6 +136,22 @@ At the moment, this is using the `route name` to name the HIT, but this is going * * @param {int} dimensionNumber * @param {string|int} value + * + * @throws Error - If already defined + */ +``` + +### injectGlobalMetric (metricNumber, value) +```javascript + /** + * Inject a new GlobalMetric that will be sent every time. + * + * Prefer inject through plugin configuration. + * + * @param {int} metricNumber + * @param {string|int} value + * + * @throws Error - If already defined */ ``` @@ -127,3 +164,12 @@ At the moment, this is using the `route name` to name the HIT, but this is going * @param {boolean} isFatal - Specifies whether the exception was fatal */ ``` + +### changeSessionLanguage (code) +```javascript + /** + * Set the current session language, use this if you change lang in the application after initialization. + * + * @param {string} code - Must be like in that : http://www.lingoes.net/en/translator/langcode.htm + */ +``` diff --git a/analytics.js b/analytics.js deleted file mode 100644 index 5c823bd..0000000 --- a/analytics.js +++ /dev/null @@ -1,156 +0,0 @@ -let debug = false - -/** - * Console log depending on config debug mode - * @param {...*} message - */ -const logDebug = function (message) { - if (debug) { - console.log('VueAnalytics :', ...arguments) - } -} - -/** - * Plugin main class - */ -class AnalyticsPlugin { - constructor (conf) { - this.conf = conf - } - - trackView (screenName) { - logDebug('Dispatching TrackView', { screenName }) - - ga('set', 'screenName', screenName) - ga('send', 'screenview') - } - - /** - * Dispatch an analytics event - * - * @param category - * @param action - * @param label - * @param value - * @param fieldsObject - */ - trackEvent (category, action = null, label = null, value = null, fieldsObject = {}) { - // TODO : FieldObject is full syntax, refactor this - logDebug('Dispatching event', { category, action, label, value, fieldsObject }) - - ga('send', 'event', category, action, label, value, fieldsObject) - } - - /** - * Track an exception that occurred in the application. - * - * @param {string} description - Something describing the error (max. 150 Bytes) - * @param {boolean} isFatal - Specifies whether the exception was fatal - */ - trackException (description, isFatal = false) { - ga('send', 'exception', { 'exDescription': description, 'exFatal': isFatal }); - } - - /** - * Inject a new GlobalDimension that will be sent every time. - * - * Prefer inject through plugin configuration. - * - * @param {int} dimensionNumber - * @param {string|int} value - * @throws Error - If already defined - */ - injectGlobalDimension (dimensionNumber, value) { - logDebug('Trying dimension Injection...', { dimensionNumber, value }) - - // Test if dimension already registered - if (this.conf.globalDimensions.find(el => el.dimension === dimensionNumber)) { - throw new Error('VueAnalytics : Dimension already registered') - } - - // Otherwise add dimension - const newDimension = { dimension: dimensionNumber, value } - - this.conf.globalDimensions.push(newDimension) - ga('set', `dimension${newDimension.dimension}`, newDimension.value) - logDebug('Dimension injected') - } -} - -/** - * Installation procedure - * - * @param Vue - * @param conf - */ -const install = function (Vue, conf) { - - // Default - conf.debug = conf.debug || false - debug = conf.debug // Module debug mode - - if (!conf.trackingId) { - throw new Error('VueAnalytics : Please provide a "trackingId" from the config') - } - - if (!conf.appName) { - throw new Error('VueAnalytics : Please provide a "appName" from the config') - } - - if (!conf.appVersion) { - throw new Error('VueAnalytics : Please provide a "appVersion" from the config') - } - - // Declare analytics snipper - (function (i, s, o, g, r, a, m) { - i[ 'GoogleAnalyticsObject' ] = r; - i[ r ] = i[ r ] || function () { - (i[ r ].q = i[ r ].q || []).push(arguments) - }, i[ r ].l = 1 * new Date(); - a = s.createElement(o), - m = s.getElementsByTagName(o)[ 0 ]; - a.async = 1; - a.src = g; - m.parentNode.insertBefore(a, m) - })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); - - // Register tracker, TODO : Set language and refactor in the create statement - ga('create', conf.trackingId, 'auto') - ga('set', 'appName', conf.appName) - ga('set', 'appVersion', conf.appVersion) - - // Set beacon method if available - // TODO : Simplify use, because the GA directive already fallback to default if not defined - if (!conf.debug && navigator.sendBeacon) { - ga('set', 'transport', 'beacon'); - } - - // Create global dimensions - if (conf.globalDimensions) { - conf.globalDimensions.forEach(dimension => { - ga('set', `dimension${dimension.dimension}`, dimension.value) - }) - } - - // Handle vue-router if defined - if (conf.vueRouter) { - // Flatten routes name - if (conf.ignoredViews) { - conf.ignoredViews = conf.ignoredViews.map(view => view.toLowerCase()) - } - - conf.vueRouter.afterEach(({ name: routeName }) => { - if (conf.ignoredViews && conf.ignoredViews.indexOf(routeName.toLowerCase()) !== -1) { - return - } - - // Dispatch vue event - Vue.analytics.trackView(routeName) - }) - } - - // Add to vue prototype and also from globals - Vue.prototype.$analytics = Vue.prototype.$ua = Vue.analytics = new AnalyticsPlugin(conf) -} - -export default { install } \ No newline at end of file diff --git a/dist/vue-analytics.min.js b/dist/vue-analytics.min.js new file mode 100644 index 0000000..ce616cc --- /dev/null +++ b/dist/vue-analytics.min.js @@ -0,0 +1 @@ +module.exports=function(e){function n(a){if(t[a])return t[a].exports;var o=t[a]={exports:{},id:a,loaded:!1};return e[a].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}var t={};return n.m=e,n.c=t,n.p="",n(0)}([function(e,n,t){e.exports=t(1)},function(e,n,t){"use strict";function a(e){if(e&&e.__esModule)return e;var n={};if(null!=e)for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(n[t]=e[t]);return n.default=e,n}function o(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(n,"__esModule",{value:!0});var i=Object.assign||function(e){for(var n=1;n1&&void 0!==arguments[1]?arguments[1]:{};n=i({},l.default,n),f.checkMandatoryParams(n),l.default.debug=n.debug,l.default.globalDimensions=n.globalDimensions,l.default.globalMetrics=n.globalMetrics,ga("create",n.trackingId,"auto",{transport:"beacon",appName:n.appName,appVersion:n.appVersion}),n.globalDimensions&&n.globalDimensions.forEach(function(e){ga("set","dimension"+e.dimension,e.value)}),n.globalMetrics&&n.globalMetrics.forEach(function(e){ga("set","metric"+e.metric,e.value)}),n.vueRouter&&g(e,n.vueRouter,n.ignoredViews),e.prototype.$analytics=e.prototype.$ua=e.analytics=new c.default},g=function(e,n,t){return t&&(t=t.map(function(e){return e.toLowerCase()})),n.afterEach(function(n){t&&t.indexOf(n.name.toLowerCase())!==-1||e.analytics.trackView(n.meta.analytics||n.name)}),t};n.default={install:d}},function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.default={debug:!1,globalDimensions:[],globalMetrics:[]}},function(e,n,t){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function o(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(n,"__esModule",{value:!0});var i=function(){function e(e,n){for(var t=0;t1&&void 0!==arguments[1]?arguments[1]:null,t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;(0,r.logDebug)("Dispatching event",{category:e,action:n,label:t,value:a}),ga("send","event",e,n,t,a)}},{key:"trackException",value:function(e){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];ga("send","exception",{exDescription:e,exFatal:n})}},{key:"injectGlobalDimension",value:function(e,n){if((0,r.logDebug)("Trying dimension Injection...",{dimensionNumber:e,value:n}),u.default.globalDimensions.find(function(n){return n.dimension===e}))throw new Error("VueAnalytics : Dimension already registered");var t={dimension:e,value:n};u.default.globalDimensions.push(t),ga("set","dimension"+t.dimension,t.value),(0,r.logDebug)("Dimension injected")}},{key:"injectGlobalMetric",value:function(e,n){if((0,r.logDebug)("Trying metric Injection...",{metricNumber:e,value:n}),u.default.globalMetrics.find(function(n){return n.metric===e}))throw new Error("VueAnalytics : Metric already registered");var t={metric:e,value:n};u.default.globalMetrics.push(t),ga("set","metric"+t.metric,t.value),(0,r.logDebug)("Metric injected")}},{key:"changeSessionLanguage",value:function(e){(0,r.logDebug)("Changing application localisation & language to "+e),ga("set","language",e)}}]),e}();n.default=c},function(e,n,t){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(n,"__esModule",{value:!0}),n.cordovaApp=n.checkMandatoryParams=n.logDebug=void 0;var o=t(2),i=a(o);n.logDebug=function(e){if(i.default.debug){var n;(n=console).log.apply(n,["VueAnalytics :"].concat(Array.prototype.slice.call(arguments)))}},n.checkMandatoryParams=function(e){var n=["trackingId","appName","appVersion"];n.forEach(function(n){if(!e[n])throw new Error('VueAnalytics : Please provide a "'+n+'" from the config.')})},n.cordovaApp={bootstrapWindows:function(){window.ActiveXObject=void 0,ga("set","checkProtocolTask",null)}}}]); \ No newline at end of file diff --git a/package.json b/package.json index 2ba156b..dbc35cd 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "vue-ua", - "version": "1.1.2", + "version": "1.2.0", "description": "Help for Google Universal Analytics in Vue application", - "main": "./vue-analytics.min.js", + "main": "./dist/vue-analytics.min.js", "author": "Andréas \"ScreamZ\" Hanss", "license": "Apache-2.0", "homepage": "https://github.com/ScreamZ/vue-analytics", diff --git a/src/AnalyticsPlugin.js b/src/AnalyticsPlugin.js new file mode 100644 index 0000000..6d7c2e3 --- /dev/null +++ b/src/AnalyticsPlugin.js @@ -0,0 +1,100 @@ +import { logDebug } from './utils' +import pluginConfig from './config' +/** + * Plugin main class + */ +export default class AnalyticsPlugin { + trackView (screenName) { + logDebug('Dispatching TrackView', { screenName }) + + ga('set', 'screenName', screenName) + ga('send', 'screenview') + } + + /** + * Dispatch an analytics event + * + * @param category + * @param action + * @param label + * @param value + */ + trackEvent (category, action = null, label = null, value = null) { + // TODO : FieldObject is full syntax, refactor this at one moment + logDebug('Dispatching event', { category, action, label, value}) + + ga('send', 'event', category, action, label, value) + } + + /** + * Track an exception that occurred in the application. + * + * @param {string} description - Something describing the error (max. 150 Bytes) + * @param {boolean} isFatal - Specifies whether the exception was fatal + */ + trackException (description, isFatal = false) { + ga('send', 'exception', { 'exDescription': description, 'exFatal': isFatal }); + } + + /** + * Inject a new GlobalDimension that will be sent every time. + * + * Prefer inject through plugin configuration. + * + * @param {int} dimensionNumber + * @param {string|int} value + * + * @throws Error - If already defined + */ + injectGlobalDimension (dimensionNumber, value) { + logDebug('Trying dimension Injection...', { dimensionNumber, value }) + + // Test if dimension already registered + if (pluginConfig.globalDimensions.find(el => el.dimension === dimensionNumber)) { + throw new Error('VueAnalytics : Dimension already registered') + } + + // Otherwise add dimension + const newDimension = { dimension: dimensionNumber, value } + + pluginConfig.globalDimensions.push(newDimension) + ga('set', `dimension${newDimension.dimension}`, newDimension.value) + logDebug('Dimension injected') + } + + /** + * Inject a new GlobalMetric that will be sent every time. + * + * Prefer inject through plugin configuration. + * + * @param {int} metricNumber + * @param {string|int} value + * + * @throws Error - If already defined + */ + injectGlobalMetric (metricNumber, value) { + logDebug('Trying metric Injection...', { metricNumber, value }) + + // Test if dimension already registered + if (pluginConfig.globalMetrics.find(el => el.metric === metricNumber)) { + throw new Error('VueAnalytics : Metric already registered') + } + + // Otherwise add dimension + const newMetric = { metric: metricNumber, value } + + pluginConfig.globalMetrics.push(newMetric) + ga('set', `metric${newMetric.metric}`, newMetric.value) + logDebug('Metric injected') + } + + /** + * Set the current session language, use this if you change lang in the application after initialization. + * + * @param {string} code - Must be like in that : http://www.lingoes.net/en/translator/langcode.htm + */ + changeSessionLanguage (code) { + logDebug(`Changing application localisation & language to ${code}`); + ga('set', 'language', code); + } +} \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..54aed1f --- /dev/null +++ b/src/config.js @@ -0,0 +1,5 @@ +export default { + debug: false, + globalDimensions: [], + globalMetrics: [] +} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..71abd25 --- /dev/null +++ b/src/index.js @@ -0,0 +1,79 @@ +import pluginConfig from './config' +import AnalyticsPlugin from './AnalyticsPlugin' +import * as Utils from './utils' + +/** + * Installation procedure + * + * @param Vue + * @param initConf + */ +const install = function (Vue, initConf = {}) { + // Apply default configuration + initConf = { ...pluginConfig, ...initConf } + Utils.checkMandatoryParams(initConf) + + pluginConfig.debug = initConf.debug + pluginConfig.globalDimensions = initConf.globalDimensions + pluginConfig.globalMetrics = initConf.globalMetrics + + // Register tracker + ga('create', initConf.trackingId, 'auto', { + transport: 'beacon', + appName: initConf.appName, + appVersion: initConf.appVersion + }) + + // Inject global dimensions + if (initConf.globalDimensions) { + initConf.globalDimensions.forEach(dimension => { + ga('set', `dimension${dimension.dimension}`, dimension.value) + }) + } + + // Inject global metrics + if (initConf.globalMetrics) { + initConf.globalMetrics.forEach(metric => { + ga('set', `metric${metric.metric}`, metric.value) + }) + } + + // Handle vue-router if defined + if (initConf.vueRouter) { + initVueRouterGuard(Vue, initConf.vueRouter, initConf.ignoredViews) + } + + // Add to vue prototype and also from globals + Vue.prototype.$analytics = Vue.prototype.$ua = Vue.analytics = new AnalyticsPlugin() +} + +/** + * Init the router guard. + * + * @param Vue - The Vue instance + * @param vueRouter - The Vue router instance to attach guard + * @param {string[]} ignoredViews - An array of route name to ignore + * + * @returns {string[]} The ignored routes names formalized. + */ +const initVueRouterGuard = function (Vue, vueRouter, ignoredViews) { + // Flatten routes name + if (ignoredViews) { + ignoredViews = ignoredViews.map(view => view.toLowerCase()) + } + + vueRouter.afterEach(to => { + // Ignore some routes + if (ignoredViews && ignoredViews.indexOf(to.name.toLowerCase()) !== -1) { + return + } + + // Dispatch vue event using meta analytics value if defined otherwise fallback to route name + Vue.analytics.trackView(to.meta.analytics || to.name) + }) + + return ignoredViews; +} + +// Export module +export default { install } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..3b9c2f0 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,30 @@ +import pluginConfig from './config' + +/** + * Console log depending on config debug mode + * @param {...*} message + */ +export const logDebug = function (message) { + if (pluginConfig.debug) { + console.log('VueAnalytics :', ...arguments) + } +} + +export const checkMandatoryParams = function (params) { + const mandatoryParams = [ 'trackingId', 'appName', 'appVersion' ]; + + mandatoryParams.forEach(el => { + if (!params[ el ]) throw new Error(`VueAnalytics : Please provide a "${el}" from the config.`) + }) +} + +/** + * Handle tools for cordova app workarounds + */ +export const cordovaApp = { + bootstrapWindows () { + // Disable activeX object to make Analytics.js use XHR, or something else + window.ActiveXObject = undefined; + ga('set', 'checkProtocolTask', null) + }, +} \ No newline at end of file diff --git a/vue-analytics.min.js b/vue-analytics.min.js deleted file mode 100644 index 49047af..0000000 --- a/vue-analytics.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var t=n();for(var o in t)("object"==typeof exports?exports:e)[o]=t[o]}}(this,function(){return function(e){function n(o){if(t[o])return t[o].exports;var i=t[o]={exports:{},id:o,loaded:!1};return e[o].call(i.exports,i,i.exports,n),i.loaded=!0,i.exports}var t={};return n.m=e,n.c=t,n.p="",n(0)}([function(e,n,t){e.exports=t(1)},function(e,n){"use strict";function t(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(n,"__esModule",{value:!0});var o=function(){function e(e,n){for(var t=0;t1&&void 0!==arguments[1]?arguments[1]:null,t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{};a("Dispatching event",{category:e,action:n,label:t,value:o,fieldsObject:i}),ga("send","event",e,n,t,o,i)}},{key:"trackException",value:function(e){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];ga("send","exception",{exDescription:e,exFatal:n})}},{key:"injectGlobalDimension",value:function(e,n){if(a("Trying dimension Injection...",{dimensionNumber:e,value:n}),this.conf.globalDimensions.find(function(n){return n.dimension===e}))throw new Error("VueAnalytics : Dimension already registered");var t={dimension:e,value:n};this.conf.globalDimensions.push(t),ga("set","dimension"+t.dimension,t.value),a("Dimension injected")}}]),e}(),s=function(e,n){if(n.debug=n.debug||!1,i=n.debug,!n.trackingId)throw new Error('VueAnalytics : Please provide a "trackingId" from the config');if(!n.appName)throw new Error('VueAnalytics : Please provide a "appName" from the config');if(!n.appVersion)throw new Error('VueAnalytics : Please provide a "appVersion" from the config');!function(e,n,t,o,i,a,r){e.GoogleAnalyticsObject=i,e[i]=e[i]||function(){(e[i].q=e[i].q||[]).push(arguments)},e[i].l=1*new Date,a=n.createElement(t),r=n.getElementsByTagName(t)[0],a.async=1,a.src=o,r.parentNode.insertBefore(a,r)}(window,document,"script","https://www.google-analytics.com/analytics.js","ga"),ga("create",n.trackingId,"auto"),ga("set","appName",n.appName),ga("set","appVersion",n.appVersion),!n.debug&&navigator.sendBeacon&&ga("set","transport","beacon"),n.globalDimensions&&n.globalDimensions.forEach(function(e){ga("set","dimension"+e.dimension,e.value)}),n.vueRouter&&(n.ignoredView&&(n.ignoredViews=n.ignoredViews.map(function(e){return e.toLowerCase()})),n.vueRouter.afterEach(function(t){var o=t.name;n.ignoredViews&&n.ignoredViews.indexOf(o.toLowerCase())!==-1||e.analytics.trackView(o)})),e.prototype.$analytics=e.prototype.$ua=e.analytics=new r(n)};n.default={install:s}}])}); \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 63a069c..5d3f509 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,11 +3,11 @@ var webpack = require('webpack') module.exports = { entry: [ - './analytics.js' + './src/index.js' ], output: { - filename: 'vue-analytics.min.js', - libraryTarget: 'umd' + filename: 'dist/vue-analytics.min.js', + libraryTarget: 'commonjs2' }, plugins: [ new webpack.optimize.UglifyJsPlugin({