├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist ├── angular-google-analytics.js └── angular-google-analytics.min.js ├── index.js ├── nuget.nuspec ├── package.json └── test ├── karma.conf.js └── unit ├── classic-google-analytics.js ├── debug-mode.js ├── directives.js ├── disable-analytics.js ├── offline-mode.js ├── route-reading.js ├── scenarios.js ├── track-exception.js └── universal-google-analytics.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.log 3 | node_modules 4 | components 5 | bower_components 6 | .idea 7 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | module.exports = function(grunt) { 3 | 'use strict'; 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | pkg: grunt.file.readJSON('package.json'), 8 | meta: { 9 | banner: '/**\n' + 10 | ' * <%= pkg.description %>\n' + 11 | ' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 12 | ' * @link <%= pkg.homepage %>\n' + 13 | ' * @author <%= pkg.author %>\n' + 14 | ' * @contributors <%= pkg.contributors %>\n' + 15 | ' * @license MIT License, http://www.opensource.org/licenses/MIT\n' + 16 | ' */\n' 17 | }, 18 | dirs: { 19 | dest: 'dist' 20 | }, 21 | concat: { 22 | options: { 23 | banner: '<%= meta.banner %>' 24 | }, 25 | dist: { 26 | src: ['index.js'], 27 | dest: '<%= dirs.dest %>/<%= pkg.name %>.js' 28 | } 29 | }, 30 | uglify: { 31 | options: { 32 | banner: '<%= meta.banner %>' 33 | }, 34 | dist: { 35 | src: ['<%= concat.dist.dest %>'], 36 | dest: '<%= dirs.dest %>/<%= pkg.name %>.min.js' 37 | } 38 | }, 39 | stage: {}, 40 | release: { 41 | options: { 42 | commitMessage: '<%= version %>', 43 | tagName: '<%= version %>', 44 | file: 'package.json', 45 | push: false, 46 | tag: false, 47 | pushTags: false, 48 | npm: false 49 | } 50 | }, 51 | jshint: { 52 | files: ['Gruntfile.js', 'index.js', 'test/*.js', 'test/unit/*.js'], 53 | options: { 54 | curly: true, 55 | browser: true, 56 | eqeqeq: true, 57 | immed: true, 58 | latedef: true, 59 | newcap: true, 60 | noarg: true, 61 | sub: true, 62 | undef: true, 63 | boss: true, 64 | eqnull: true, 65 | expr: true, 66 | node: true, 67 | '-W018': true, 68 | globals: { 69 | exports: true, 70 | angular: false, 71 | $: false 72 | } 73 | } 74 | }, 75 | karma: { 76 | test: { 77 | options: { 78 | reporters: ['dots'], 79 | singleRun: true 80 | } 81 | }, 82 | server: { 83 | options: { 84 | singleRun: false 85 | } 86 | }, 87 | options: { 88 | configFile: __dirname + '/test/karma.conf.js' 89 | } 90 | } 91 | }); 92 | 93 | // Load the plugin that provides the "jshint" task. 94 | grunt.loadNpmTasks('grunt-contrib-jshint'); 95 | 96 | // Load the plugin that provides the "concat" task. 97 | grunt.loadNpmTasks('grunt-contrib-concat'); 98 | 99 | // Load the plugin that provides the "uglify" task. 100 | grunt.loadNpmTasks('grunt-contrib-uglify'); 101 | 102 | grunt.registerTask('stage', 'git add files before running the release task', function () { 103 | var files = this.options().files; 104 | grunt.util.spawn({ 105 | cmd: process.platform === 'win32' ? 'git.cmd' : 'git', 106 | args: ['add'].concat(files) 107 | }, grunt.task.current.async()); 108 | }); 109 | 110 | // Default task. 111 | grunt.registerTask('default', ['test']); 112 | 113 | // Static analysis 114 | grunt.registerTask('lint', ['jshint']); 115 | 116 | // Test tasks. 117 | grunt.registerTask('test', ['jshint', 'karma:test']); 118 | grunt.registerTask('test-server', ['karma:server']); 119 | 120 | // Build task. 121 | grunt.registerTask('build', ['test', 'concat', 'uglify']); 122 | 123 | // Release task. 124 | grunt.registerTask('release', ['build']); 125 | 126 | // Provides the "karma" task. 127 | grunt.registerMultiTask('karma', 'Starts up a karma server.', function() { 128 | var karma = require('karma'), 129 | done = this.async(); 130 | var server = new karma.Server(this.options(), function(code) { 131 | done(code === 0); 132 | }); 133 | server.start(); 134 | }); 135 | }; 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 "Julien Bouquillon " 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-google-analytics 2 | 3 | [![Bower Version](https://img.shields.io/bower/v/angular-google-analytics.svg)](https://github.com/revolunet/angular-google-analytics) 4 | [![NPM Version](https://img.shields.io/npm/v/angular-google-analytics.svg)](https://www.npmjs.com/package/angular-google-analytics) 5 | [![NuGet](https://img.shields.io/nuget/v/angular-google-analytics.svg)](https://www.nuget.org/packages/angular-google-analytics/) 6 | [![Master Build Status](https://codeship.com/projects/ba7a0af0-33fe-0133-927c-127922174191/status?branch=master)](https://codeship.com/projects) 7 | [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](https://github.com/revolunet/angular-google-analytics/blob/master/LICENSE) 8 | 9 | This service lets you integrate google analytics tracker in your AngularJS applications easily. 10 | 11 | You can use basic functions, `Analytics.trackEvent('video', 'play', 'django.mp4');` or more advanced e-commerce features like product tracking, promo codes, transactions... 12 | 13 | Proudly brought to you by [@revolunet](http://twitter.com/revolunet), [@deltaepsilon](https://github.com/deltaepsilon), [@justinsa](https://github.com/justinsa) and [contributors](https://github.com/revolunet/angular-google-analytics/graphs/contributors) 14 | 15 | ## Features 16 | 17 | - highly configurable 18 | - automatic page tracking 19 | - event tracking 20 | - e-commerce (ecommerce.js) support 21 | - enhanced e-commerce (ec.js) support 22 | - multiple-domains 23 | - ga.js (classic) and analytics.js (universal) support 24 | - cross-domain support 25 | - multiple tracking objects 26 | - hybrid mobile application support 27 | - Chrome extension support 28 | - offline mode 29 | - analytics.js advanced debugging support 30 | 31 | **IMPORTANT!** As of version 2.x, methods that were previously marked as deprecated have been removed from this library. The following methods are no longer available: `setCookieConfig`, `getCookieConfig`, `createAnalyticsScriptTag`, and `createScriptTag`. Each have been replaced with alternative implementations that can be found in this documentation. 32 | 33 | ## Installation and Quick Start 34 | The quick start is designed to give you a simple, working example for the most common usage scenario. There are numerous other ways to configure and use this library as explained in the documentation. 35 | 36 | ### 1- Installation: 37 | You can install the module from a package manger of your choice directly from the command line 38 | 39 | ```sh 40 | # Bower 41 | bower install angular-google-analytics 42 | 43 | # NPM 44 | npm i angular-google-analytics 45 | 46 | # Nuget 47 | nuget install angular-google-analytics 48 | ``` 49 | 50 | Or alternatively, grab the dist/angular-google-analytics.min.js and include it in your project 51 | 52 | In your application, declare the angular-google-analytics module dependency. 53 | 54 | ```html 55 | 56 | ``` 57 | 58 | ### 2- In your application, declare dependency injection: 59 | 60 | ```javascript 61 | var myApp = angular.module('myModule', ['angular-google-analytics']); 62 | ``` 63 | 64 | ### 3- Set your Google Analytics account and start tracking: 65 | 66 | ```JavaScript 67 | myApp.config(['AnalyticsProvider', function (AnalyticsProvider) { 68 | // Add configuration code as desired 69 | AnalyticsProvider.setAccount('UU-XXXXXXX-X'); //UU-XXXXXXX-X should be your tracking code 70 | }]).run(['Analytics', function(Analytics) { }]); 71 | ``` 72 | Congratulations! [angular-google-analytics](https://github.com/revolunet/angular-google-analytics) is ready and Google Analytics will track your page views once the application is run 73 | 74 | ## Configure Service 75 | ```js 76 | app.config(function (AnalyticsProvider) { 77 | // Add configuration code as desired - see below 78 | }); 79 | ``` 80 | 81 | ### Configuration Method Chaining 82 | ```js 83 | // All configuration methods return the provider object and can be chained to reduce typing. 84 | // For example: 85 | AnalyticsProvider 86 | .logAllCalls(true) 87 | .startOffline(true) 88 | .useECommerce(true, true); 89 | ``` 90 | 91 | ### Use Classic Analytics 92 | ```js 93 | // Use ga.js (classic) instead of analytics.js (universal) 94 | // By default, universal analytics is used, unless this is called with a falsey value. 95 | AnalyticsProvider.useAnalytics(false); 96 | ``` 97 | 98 | ### Set Google Analytics Accounts (Required) 99 | ```js 100 | // Set a single account 101 | AnalyticsProvider.setAccount('UA-XXXXX-xx'); 102 | ``` 103 | **Note:** the single account syntax is internally represented as an unnamed account object that will have all properties defined to defaults, except for name. 104 | 105 | ```js 106 | // Set multiple accounts 107 | // Universal Analytics only 108 | AnalyticsProvider.setAccount([ 109 | { tracker: 'UA-12345-12', name: "tracker1" }, 110 | { tracker: 'UA-12345-34', name: "tracker2" } 111 | ]); 112 | ``` 113 | **Note:** the above account objects will have all properties defined to defaults that are not defined. 114 | 115 | ```js 116 | // Set a single account with all properties defined 117 | // Universal Analytics only 118 | AnalyticsProvider.setAccount({ 119 | tracker: 'UA-12345-12', 120 | name: "tracker1", 121 | fields: { 122 | cookieDomain: 'foo.example.com', 123 | cookieName: 'myNewName', 124 | cookieExpires: 20000 125 | // See: [Analytics Field Reference](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference) for a list of all fields. 126 | }, 127 | crossDomainLinker: true, 128 | crossLinkDomains: ['domain-1.com', 'domain-2.com'], 129 | displayFeatures: true, 130 | enhancedLinkAttribution: true, 131 | select: function (args) { 132 | // This function is used to qualify or disqualify an account object to be run with commands. 133 | // If the function does not exist, is not a function, or returns true then the account object will qualify. 134 | // If the function exists and returns false then the account object will be disqualified. 135 | // The 'args' parameter is the set of arguments (which contains the command name) that will be sent to Universal Analytics. 136 | return true; 137 | }, 138 | set: { 139 | forceSSL: true 140 | // This is any set of `set` commands to make for the account immediately after the `create` command for the account. 141 | // The property key is the command and the property value is passed in as the argument, _e.g._, `ga('set', 'forceSSL', true)`. 142 | // Order of commands is not guaranteed as it is dependent on the implementation of the `for (property in object)` iterator. 143 | }, 144 | trackEvent: true, 145 | trackEcommerce: true 146 | }); 147 | ``` 148 | **Note:** the above properties are referenced and discussed in proceeding sections. 149 | 150 | ### Use Display Features 151 | ```js 152 | // Use display features module 153 | AnalyticsProvider.useDisplayFeatures(true); 154 | ``` 155 | 156 | If set to a truthy value then the display features module is loaded with Google Analytics. 157 | 158 | In the case of universal analytics, this value will be used as the default for any tracker that does not have the `displayFeatures` property defined. All trackers with `displayFeatures: true` will be registered for display features. 159 | 160 | ### Use Enhanced Link Attribution 161 | ```js 162 | // Enable enhanced link attribution module 163 | AnalyticsProvider.useEnhancedLinkAttribution(true); 164 | ``` 165 | 166 | If set to a truthy value then the enhanced link attribution module is loaded with Google Analytics. 167 | 168 | In the case of universal analytics, this value will be used as the default for any tracker that does not have the `enhancedLinkAttribution` property defined. All trackers with `enhancedLinkAttribution: true` will be registered for enhanced link attribution. 169 | 170 | ### Use Cross Domain Linking 171 | ```js 172 | // Use cross domain linking and set cross-linked domains 173 | AnalyticsProvider.useCrossDomainLinker(true); 174 | AnalyticsProvider.setCrossLinkDomains(['domain-1.com', 'domain-2.com']); 175 | ``` 176 | 177 | If set to a truthy value then the cross-linked domains are registered with Google Analytics. 178 | 179 | In the case of universal analytics, these values will be used as the default for any tracker that does not have the `crossDomainLinker` and `crossLinkDomains` properties defined. All trackers with `crossDomainLinker: true` will register the cross-linked domains. 180 | 181 | ### Track Events 182 | This property is defined for universal analytics account objects only and is false by default. 183 | 184 | If `trackEvent: true` for an account object then all `trackEvent` calls will be supported for that account object. 185 | 186 | Set `trackEvent: false` for an account object that is not tracking events. 187 | 188 | ### Track E-Commerce 189 | This property is defined for universal analytics account objects only. This property defaults to true if e-commerce is enabled (either classic or enhanced) and false otherwise. 190 | 191 | If `trackEcommerce: true` for an account object then all e-commerce calls will be supported for that account object. 192 | 193 | Set `trackEcommerce: false` for an account object that is not tracking e-commerce. 194 | 195 | ### Enable E-Commerce 196 | ```js 197 | // Enable e-commerce module (ecommerce.js) 198 | AnalyticsProvider.useECommerce(true, false); 199 | 200 | // Enable enhanced e-commerce module (ec.js) 201 | // Universal Analytics only 202 | AnalyticsProvider.useECommerce(true, true); 203 | 204 | // Set Currency 205 | // Default is 'USD'. Use ISO currency codes. 206 | AnalyticsProvider.setCurrency('CDN'); 207 | ``` 208 | **Note:** When enhanced e-commerce is enabled, the legacy e-commerce module is disabled and unsupported. This is a requirement of Google Analytics. 209 | 210 | ### Set Route Tracking Behaviors 211 | **Note:** In order to set route tracking behavior in the $routeProvider you need the ngRoute module in your application. Please refer 212 | to the official [angular ngRoute documentation](https://docs.angularjs.org/api/ngRoute) on how to install and use this service. 213 | ```js 214 | // Track all routes (default is true). 215 | AnalyticsProvider.trackPages(true); 216 | 217 | // Track all URL query params (default is false). 218 | AnalyticsProvider.trackUrlParams(true); 219 | 220 | // Ignore first page view (default is false). 221 | // Helpful when using hashes and whenever your bounce rate looks obscenely low. 222 | AnalyticsProvider.ignoreFirstPageLoad(true); 223 | 224 | // URL prefix (default is empty). 225 | // Helpful when the app doesn't run in the root directory. 226 | AnalyticsProvider.trackPrefix('my-application'); 227 | 228 | // Change the default page event name. 229 | // Helpful when using ui-router, which fires $stateChangeSuccess instead of $routeChangeSuccess. 230 | AnalyticsProvider.setPageEvent('$stateChangeSuccess'); 231 | 232 | // RegEx to scrub location before sending to analytics. 233 | // Internally replaces all matching segments with an empty string. 234 | AnalyticsProvider.setRemoveRegExp(/\/\d+?$/); 235 | 236 | // Activate reading custom tracking urls from $routeProvider config (default is false) 237 | // This is more flexible than using RegExp and easier to maintain for multiple parameters. 238 | // It also reduces tracked pages to routes (only those with a templateUrl) defined in the 239 | // $routeProvider and therefore reduces bounce rate created by redirects. 240 | // NOTE: The following option requires the ngRoute module 241 | AnalyticsProvider.readFromRoute(true); 242 | // Add custom routes to the $routeProvider like this. You can also exclude certain routes from tracking by 243 | // adding 'doNotTrack' property 244 | $routeProvider 245 | .when('/sessions', { 246 | templateUrl: 'list.html', 247 | controller: 'ListController' 248 | }) 249 | .when('/session/:id',{ 250 | templateUrl : 'master.html', 251 | controller: 'MasterController', 252 | pageTrack: '/session' // angular-google-analytics extension 253 | }) 254 | .when('/member/:sessionId/:memberId', { 255 | templateUrl : 'member.html', 256 | controller: 'CardController', 257 | pageTrack: '/member', // angular-google-analytics extension 258 | }) 259 | .otherwise({ 260 | templateUrl: '404.html', 261 | doNotTrack: true // angular-google-analytics extension 262 | }); 263 | ``` 264 | 265 | ### Set Domain Name 266 | ```js 267 | // Set the domain name 268 | AnalyticsProvider.setDomainName('XXX'); 269 | ``` 270 | **Note:** Use the string `'none'` for testing on localhost. 271 | 272 | ### Enable Experiment (universal analytics only) 273 | ```js 274 | // Enable analytics.js experiments 275 | AnalyticsProvider.setExperimentId('12345'); 276 | ``` 277 | **Note:** only a single experiment can be defined. 278 | 279 | ### Support Hybrid Mobile Applications (universal analytics only) 280 | ```js 281 | // Set hybrid mobile application support 282 | AnalyticsProvider.setHybridMobileSupport(true); 283 | ``` 284 | 285 | If set to a truthy value then each account object will disable protocol checking and all injected scripts will use the HTTPS protocol. 286 | 287 | ### Delay Script Tag Insertion and Tracker Setup 288 | ```js 289 | // Must manually call registerScriptTags method in order to insert the Google Analytics scripts on the page. 290 | // Analytics.registerScriptTags(); 291 | // Must manually call registerTrackers method in order to setup the trackers with Google Analytics. 292 | // Analytics.registerTrackers(); 293 | // Helpful when needing to do advanced configuration or user opt-out and wanting explicit control 294 | // over when the Google Analytics scripts get injected or tracker setup happens. 295 | AnalyticsProvider.delayScriptTag(true); 296 | ``` 297 | 298 | ### Offline Mode 299 | ```js 300 | // Start in offline mode if set to true. This also calls delayScriptTag(true) since the script cannot be 301 | // fetched if offline and must be manually called when the application goes online. 302 | AnalyticsProvider.startOffline(true); 303 | ``` 304 | 305 | ### Disable Analytics / User Opt-out 306 | ```js 307 | // Disable analytics data gathering via the user opt-out feature in Google Analytics. More information on this 308 | // is available here: https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced#optout. 309 | AnalyticsProvider.disableAnalytics(true); 310 | ``` 311 | 312 | **Note:** Using this configuration option requires that you already know the user wants to opt-out before the analytics script is injected on the page. This is somewhat unlikely for most use cases given the nature of a single page application. This module provides a better alternative with `Offline` mode since you can effectively opt the user out of tracking by enabling offline mode at any time during execution. 313 | 314 | ### Service Logging 315 | ```js 316 | // Log all outbound calls to an in-memory array accessible via ```Analytics.log``` (default is false). 317 | // This is useful for troubleshooting and seeing the order of calls with parameters. 318 | AnalyticsProvider.logAllCalls(true); 319 | ``` 320 | 321 | ### Test Mode 322 | ```js 323 | // This method is designed specifically for unit testing and entering test mode cannot be changed after 324 | // being called. Test mode skips the insertion of the Google Analytics script tags (both classic and universal) 325 | // and ensures there is a $window.ga() method available for calling by unit tests. This corrects transient 326 | // errors that were seen during unit tests due to the operation of the Google Analytics scripts. 327 | AnalyticsProvider.enterTestMode(); 328 | ``` 329 | 330 | ### Debug Mode 331 | ```js 332 | // Calling this method will enable debugging mode for Universal Analytics. Supplying a truthy value for the 333 | // optional parameter will further enable trace debugging for Universal Analytics. More information on this 334 | // is available here: https://developers.google.com/analytics/devguides/collection/analyticsjs/debugging. 335 | AnalyticsProvider.enterDebugMode(Boolean); 336 | ``` 337 | 338 | ### Anonymize IP 339 | ```js 340 | AnalyticsProvider.setAccount({ 341 | tracker: 'UA-12345-12', 342 | set: { 343 | anonymizeIp: true 344 | } 345 | } 346 | ``` 347 | 348 | ## Using the Analytics Service 349 | **IMPORTANT!** Due to how Google Analytics works, it is important to remember that you must always call `Analytics.pageView();` when you want to push setting changes and function calls to Google Analytics. 350 | 351 | ### Automatic Page View Tracking 352 | If you are relying on automatic page tracking, you need to inject Analytics at least once in your application. 353 | ```js 354 | // As an example, add the service to the run call: 355 | app.run(function(Analytics) {}); 356 | ``` 357 | 358 | ### Declaring a Controller 359 | ```js 360 | // As an example, a simple controller to make calls from: 361 | app.controller('SampleController', function (Analytics) { 362 | // Add calls as desired - see below 363 | }); 364 | ``` 365 | 366 | ### Accessing Configuration Settings 367 | The following configuration settings are intended to be immutable. While the values can be changed in this list by the user, this will not impact the behavior of the service as these values are not referenced internally; exceptions are noted below but are not intended to be utilized in such a way by the user. No guarantee will be made for future versions of this service supporting any functionality beyond reading values from this list. 368 | ```js 369 | // This is a mutable array. Changes to this list will impact service behaviors. 370 | Analytics.configuration.accounts; 371 | 372 | // If `true` then universal analytics is being used. 373 | // If `false` then classic analytics is being used. 374 | Analytics.configuration.universalAnalytics; 375 | 376 | Analytics.configuration.crossDomainLinker; 377 | Analytics.configuration.crossLinkDomains; 378 | Analytics.configuration.currency; 379 | Analytics.configuration.debugMode; 380 | 381 | Analytics.configuration.delayScriptTag; 382 | Analytics.configuration.disableAnalytics; 383 | Analytics.configuration.displayFeatures; 384 | Analytics.configuration.domainName; 385 | 386 | // ecommerce and enhancedEcommerce are mutually exclusive; either both will be false or one will be true. 387 | Analytics.configuration.ecommerce; 388 | Analytics.configuration.enhancedEcommerce; 389 | 390 | Analytics.configuration.enhancedLinkAttribution; 391 | Analytics.configuration.experimentId; 392 | Analytics.configuration.ignoreFirstPageLoad; 393 | Analytics.configuration.logAllCalls; 394 | Analytics.configuration.pageEvent; 395 | Analytics.configuration.removeRegExp; 396 | Analytics.configuration.traceDebuggingMode; 397 | Analytics.configuration.trackPrefix; 398 | Analytics.configuration.trackRoutes; 399 | Analytics.configuration.trackUrlParams; 400 | ``` 401 | 402 | ### Get URL 403 | ```js 404 | // Returns the current URL that would be sent if a `trackPage` call was made. 405 | // The returned value takes into account all configuration settings that modify the URL. 406 | Analytics.getUrl(); 407 | ``` 408 | 409 | ### Manual Script Tag Injection and Tracker Setup 410 | If `delayScriptTag(true)` was set during configuration then manual script tag injection and tracker setup is required. Otherwise, the script tag and trackers will be automatically injected and configured when the service is instantiated. 411 | ```js 412 | // Manually create either classic analytics (ga.js) or universal analytics (analytics.js) script tags 413 | Analytics.registerScriptTags(); 414 | 415 | // Manually setup the tracker object(s) 416 | Analytics.registerTrackers(); 417 | ``` 418 | 419 | ### Advanced Settings / Custom Dimensions 420 | The `set` call allows for advanced configuration and definitions in universal analytics only. This is a no-op when using classic analytics. 421 | ```js 422 | // Set the User Id 423 | Analytics.set('&uid', 1234); 424 | 425 | // Register a custom dimension for the default, unnamed account object 426 | // e.g., ga('set', 'dimension1', 'Paid'); 427 | Analytics.set('dimension1', 'Paid'); 428 | 429 | // Register a custom dimenstion for a named account object 430 | // e.g., ga('accountName.set', 'dimension2', 'Paid'); 431 | Analytics.set('dimension2', 'Paid', 'accountName'); 432 | ``` 433 | 434 | ### Page Tracking 435 | ```js 436 | // Create a new pageview event 437 | Analytics.trackPage('/video/detail/XXX'); 438 | 439 | // Create a new pageview event with page title 440 | Analytics.trackPage('/video/detail/XXX', 'Video XXX'); 441 | 442 | // Create a new pageview event with page title, custom dimension, and custom metric 443 | // Universal Analytics only 444 | Analytics.trackPage('/video/detail/XXX', 'Video XXX', { dimension15: 'My Custom Dimension', metric18: 8000 }); 445 | ``` 446 | 447 | ### Event Tracking 448 | ```js 449 | // Create a new tracking event 450 | Analytics.trackEvent('video', 'play', 'django.mp4'); 451 | 452 | // Create a new tracking event with a value 453 | Analytics.trackEvent('video', 'play', 'django.mp4', 4); 454 | 455 | // Create a new tracking event with a value and non-interaction flag 456 | Analytics.trackEvent('video', 'play', 'django.mp4', 4, true); 457 | 458 | // Create a new tracking event with a value, non-interaction flag, custom dimension, and custom metric 459 | // Universal Analytics only 460 | Analytics.trackEvent('video', 'play', 'django.mp4', 4, true, { dimension15: 'My Custom Dimension', metric18: 8000 }); 461 | 462 | // Track an event that is an outbound transport request 463 | Analytics.trackEvent( 464 | 'video', 465 | 'play', 466 | 'django.mp4', 467 | 4, 468 | true, 469 | { 470 | dimension15: 'My Custom Dimension', 471 | metric18: 8000, 472 | transport: 'beacon', 473 | hitCallback: function () { 474 | document.location = 'http://google.com'; 475 | } 476 | } 477 | ); 478 | ``` 479 | 480 | ### Track User Timings 481 | The `trackTimings` call is available for universal analytics only. This is a no-op when using classic analytics. 482 | ```js 483 | Analytics.trackTimings(timingCategory, timingVar, timingValue, timingLabel); 484 | 485 | // example: 486 | var endTime = new Date().getTime(), 487 | timeSpent = endTime - startTime; 488 | Analytics.trackTimings('Time to Checkout', 'User Timings', timeSpent); 489 | ``` 490 | 491 | ### Classic E-Commerce (ecommerce.js) 492 | Classic e-commerce and enhanced e-commerce are mutually exclusive. 493 | ```js 494 | // Create transaction 495 | Analytics.addTrans('1', '', '2.42', '0.42', '0', 'Amsterdam', '', 'Netherlands', 'EUR'); 496 | 497 | // Add items to transaction 498 | Analytics.addItem('1', 'sku-1', 'Test product 1', 'Testing', '1', '1'); 499 | Analytics.addItem('1', 'sku-2', 'Test product 2', 'Testing', '1', '1'); 500 | 501 | // Complete transaction 502 | Analytics.trackTrans(); 503 | 504 | // Clear transaction 505 | Analytics.clearTrans(); 506 | ``` 507 | 508 | ### Enhanced E-Commerce (ec.js) 509 | Enhanced e-commerce is only available for universal analytics. Enhanced e-commerce and classic e-commerce are mutually exclusive. 510 | 511 | #### Product Impression Tracking 512 | ```js 513 | Analytics.addImpression(productId, name, list, brand, category, variant, position, price); 514 | Analytics.pageView(); 515 | 516 | // example: 517 | Analytics.addImpression('sku-1', 'Test Product 1', 'Category List', 'Brand 1', 'Category-1', 'variant-1', '1', '24990'); 518 | Analytics.addImpression('sku-2', 'Test Product 2', 'Category List', 'Brand 2', 'Category-1', 'variant-3', '2', '2499'); 519 | Analytics.pageView(); 520 | ``` 521 | 522 | #### Product Click Tracking 523 | ```js 524 | Analytics.addProduct(productId, name, category, brand, variant, price, quantity, coupon, position, custom); 525 | Analytics.productClick(listName); 526 | 527 | // example: 528 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 529 | Analytics.productClick('Search Result'); 530 | 531 | // example with custom dimension and metric: 532 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1', { dimension4: 'strong', metric2: 5 }); 533 | Analytics.productClick('Search Result'); 534 | ``` 535 | 536 | #### Product Detail Tracking 537 | ```js 538 | Analytics.addProduct(productId, name, category, brand, variant, price, quantity, coupon, position); 539 | Analytics.trackDetail(); 540 | 541 | // example: 542 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 543 | Analytics.trackDetail(); 544 | ``` 545 | 546 | #### Add to Cart Tracking 547 | ```js 548 | Analytics.addProduct(productId, name, category, brand, variant, price, quantity, coupon, position); 549 | Analytics.trackCart('add', listName); // listname is optional 550 | 551 | // example: 552 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 553 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 554 | Analytics.trackCart('add', 'Search Result'); 555 | ``` 556 | 557 | #### Remove from Cart Tracking 558 | ```js 559 | Analytics.addProduct(productId, name, category, brand, variant, price, quantity, coupon, position); 560 | Analytics.trackCart('remove', listName); // listname is optional 561 | 562 | // example: 563 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 564 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 565 | Analytics.trackCart('remove', 'Search Result'); 566 | ``` 567 | 568 | #### Checkout Tracking 569 | ```js 570 | Analytics.addProduct(productId, name, category, brand, variant, price, quantity, coupon, position); 571 | Analytics.trackCheckout(checkoutStep, optionValue); 572 | 573 | // example: 574 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 575 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 576 | Analytics.trackCheckout(1, 'Visa'); 577 | ``` 578 | 579 | #### Transaction Tracking 580 | ```js 581 | Analytics.addProduct(productId, name, category, brand, variant, price, quantity, coupon, position); 582 | Analytics.trackTransaction(transactionId, affiliation, revenue, tax, shipping, coupon, list, step, option); 583 | 584 | // example: 585 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2222', '1', 'MEN10', '1'); 586 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '1111', '1', 'WOMEN10', '1'); 587 | Analytics.trackTransaction('T1234', 'Online Store - Web', '3333', '10', '200', 'FLAT10', '', '', ''); 588 | ``` 589 | 590 | #### Promotion Impressions 591 | ```js 592 | Analytics.addPromo(productId, name, creative, position); 593 | Analytics.pageView(); 594 | 595 | // example: 596 | Analytics.addPromo('PROMO_1234', 'Summer Sale', 'summer_banner2', 'banner_slot1'); 597 | Analytics.pageView(); 598 | ``` 599 | **Note:** Before tracking promotion clicks, call pageView, otherwise promotion impressions will be treated as promotion clicks. 600 | 601 | #### Promotion Clicks 602 | ```js 603 | Analytics.addPromo(promotionId, promotionName, creative, position); 604 | Analytics.promoClick(promotionName); 605 | 606 | // example: 607 | Analytics.addPromo('PROMO_1234', 'Summer Sale', 'summer_banner2', 'banner_slot1'); 608 | Analytics.promoClick('Summer Sale'); 609 | ``` 610 | 611 | ### Exception Tracking 612 | ```js 613 | Analytics.trackException(description, isFatal); 614 | 615 | // example: 616 | Analytics.trackException('Function "foo" is undefined on object "bar"', true); 617 | ``` 618 | 619 | ### Online / Offline Mode 620 | ```js 621 | // While in offline mode, no calls to the ga function or pushes to the gaq array are made. 622 | // This will queue all calls for later sending once offline mode is reset to false. 623 | Analytics.offline(true); 624 | 625 | // Reset offline mode to false 626 | Analytics.offline(false); 627 | ``` 628 | 629 | ### In-Memory Queues 630 | ```js 631 | // If logging is enabled then all outbound calls are accessible via an in-memory array. 632 | // This is useful for troubleshooting and seeing the order of outbound calls with parameters. 633 | Analytics.log; 634 | 635 | // If in offline mode then all calls are queued to an in-memory array for future processing. 636 | // All calls queued to the offlineQueue are not outbound calls yet and hence do not show up in the log. 637 | Analytics.offlineQueue; 638 | ``` 639 | 640 | ## Directive 641 | 642 | Alternatively, you can use a directive to avoid filling controllers with `Analytics.trackEvent();` statements. 643 | 644 | **Note:** the directive does not create an isolate scope. 645 | 646 | ```html 647 | 648 | 649 | 650 | 651 | 652 | ``` 653 | 654 | You can define the properties on your controller too, `$scope.event = ['video', 'play', 'django.mp4']` and reference them. 655 | 656 | ```html 657 | 658 | ``` 659 | 660 | `ga-track-event-if` is a conditional check. If the attribute value evaluates falsey, the event will **NOT** be fired. This is useful for user tracking opt-out, _etc._ 661 | 662 | ```html 663 | 664 | ``` 665 | 666 | ## Troubleshooting 667 | 668 | ### AdBlock EasyPrivacy 669 | 670 | AdBlock has a module named [EasyPrivacy](https://easylist-downloads.adblockplus.org/easyprivacy.txt) that is meant to block web tracking scripts. angular-google-analytics.js gets filtered out by the EasyPrivacy blacklist. 671 | 672 | Users who are already concatenating and minifying their scripts should not notice a problem as long as the new script name is not also on the EasyPrivacy blacklist. Alternatively, consider changing the file name manually. 673 | 674 | ### Debugging Resources 675 | 676 | Chrome Extension: [Google Analytics Debugger](https://chrome.google.com/webstore/detail/google-analytics-debugger/jnkmfdileelhofjcijamephohjechhna) 677 | Firefox Add-on: [Google Analytics Debugger](https://addons.mozilla.org/en-US/firefox/addon/gadebugger/) 678 | 679 | ## License 680 | 681 | As AngularJS itself, this module is released under the permissive [MIT License](http://revolunet.mit-license.org). Your contributions are always welcome. 682 | 683 | ## Development 684 | 685 | After forking you will need to run the following from a command line to get your environment setup: 686 | 687 | 1. ```npm install``` 688 | 689 | After install you have the following commands available to you from a command line: 690 | 691 | 1. ```npm run-script lint``` 692 | 2. ```npm test``` 693 | 3. ```npm run-script test-server``` 694 | 4. ```npm run-script build``` or ```npm run-script release``` 695 | 5. ```npm run-script stage``` 696 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Julien Bouquillon (https://github.com/revolunet)", 4 | "Justin Saunders (https://github.com/justinsa)", 5 | "Chris Esplin (https://github.com/deltaepsilon)", 6 | "Adam Misiorny (https://github.com/adam187)" 7 | ], 8 | "name": "angular-google-analytics", 9 | "description": "Easy Analytics for your AngularJS application", 10 | "keywords": [ 11 | "analytics", 12 | "angular", 13 | "angularjs", 14 | "angular-js", 15 | "google-analytics", 16 | "tracking", 17 | "visitor-tracking", 18 | "universal-analytics" 19 | ], 20 | "homepage": "https://github.com/revolunet/angular-google-analytics", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/revolunet/angular-google-analytics.git" 25 | }, 26 | "main": "./dist/angular-google-analytics.min.js", 27 | "dependencies": { 28 | "angular": ">=1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dist/angular-google-analytics.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular Google Analytics - Easy tracking for your AngularJS application 3 | * @version v1.1.8 - 2016-12-30 4 | * @link http://github.com/revolunet/angular-google-analytics 5 | * @author Julien Bouquillon (https://github.com/revolunet) 6 | * @contributors Julien Bouquillon (https://github.com/revolunet),Justin Saunders (https://github.com/justinsa),Chris Esplin (https://github.com/deltaepsilon),Adam Misiorny (https://github.com/adam187) 7 | * @license MIT License, http://www.opensource.org/licenses/MIT 8 | */ 9 | !function(a,b){"use strict";"undefined"!=typeof module&&module.exports?(b("undefined"==typeof angular?require("angular"):angular),module.exports="angular-google-analytics"):"function"==typeof define&&define.amd?define(["angular"],b):b(a.angular)}(this,function(a,b){"use strict";return a.module("angular-google-analytics",[]).provider("Analytics",function(){var c,d,e,f,g,h=!0,i="auto",j=!1,k=!1,l="USD",m=!1,n=!1,o=!1,p=!1,q=!1,r=!1,s=!1,t=!1,u=!1,v=!1,w=!1,x="$routeChangeSuccess",y=!1,z=!1,A=!1,B="",C=!0,D=!1;this.log=[],this.offlineQueue=[],this.setAccount=function(d){return c=a.isUndefined(d)||d===!1?b:a.isArray(d)?d:a.isObject(d)?[d]:[{tracker:d,trackEvent:!0}],this},this.trackPages=function(a){return C=!!a,this},this.trackPrefix=function(a){return B=a,this},this.setDomainName=function(a){return e=a,this},this.useDisplayFeatures=function(a){return o=!!a,this},this.useAnalytics=function(a){return h=!!a,this},this.useEnhancedLinkAttribution=function(a){return s=!!a,this},this.useCrossDomainLinker=function(a){return k=!!a,this},this.setCrossLinkDomains=function(a){return d=a,this},this.setPageEvent=function(a){return x=a,this},this.setCookieConfig=function(a){return i=a,this},this.useECommerce=function(a,b){return q=!!a,r=!!b,this},this.setCurrency=function(a){return l=a,this},this.setRemoveRegExp=function(a){return a instanceof RegExp&&(g=a),this},this.setExperimentId=function(a){return f=a,this},this.ignoreFirstPageLoad=function(a){return t=!!a,this},this.trackUrlParams=function(a){return D=!!a,this},this.disableAnalytics=function(a){return p=!!a,this},this.setHybridMobileSupport=function(a){return v=!!a,this},this.startOffline=function(a){return w=!!a,w===!0&&this.delayScriptTag(!0),this},this.delayScriptTag=function(a){return n=!!a,this},this.logAllCalls=function(a){return u=!!a,this},this.enterTestMode=function(){return z=!0,this},this.enterDebugMode=function(a){return m=!0,A=!!a,this},this.readFromRoute=function(a){return y=!!a,this},this.$get=["$document","$location","$log","$rootScope","$window","$injector",function(E,F,G,H,I,J){var K=this,L=function(b,c){return a.isObject(c)&&a.isDefined(c[b])},M=function(a,b,c){return L(a,b)&&b[a]===c},N=function(b,c){return a.isString(c)?c+"."+b:L("name",c)?c.name+"."+b:b},O={};y&&(J.has("$route")?O=J.get("$route"):G.warn("$route service is not available. Make sure you have included ng-route in your application dependencies."));var P=function(){if(y&&O.current&&"pageTrack"in O.current)return O.current.pageTrack;var a=D?F.url():F.path();return g?a.replace(g,""):a},Q=function(){var b={utm_source:"campaignSource",utm_medium:"campaignMedium",utm_term:"campaignTerm",utm_content:"campaignContent",utm_campaign:"campaignName"},c={};return a.forEach(F.search(),function(d,e){var f=b[e];a.isDefined(f)&&(c[f]=d)}),c},R=function(a,b,c,d,e,f,g,h,i){var j={};return a&&(j.id=a),b&&(j.affiliation=b),c&&(j.revenue=c),d&&(j.tax=d),e&&(j.shipping=e),f&&(j.coupon=f),g&&(j.list=g),h&&(j.step=h),i&&(j.option=i),j},S=function(b,c){var d="",e="https:"===document.location.protocol,f="chrome-extension:"===document.location.protocol,g=h===!0&&v===!0;return b=a.isString(b)?b:"",c=a.isString(c)?c:"",""!==b&&(d="http:"+b),(f||g||e&&""!==c)&&(d="https:"+c),d},T=function(a){!h&&I._gaq&&"function"==typeof a&&a()},U=function(){var a=Array.prototype.slice.call(arguments);return w===!0?void K.offlineQueue.push([U,a]):(I._gaq||(I._gaq=[]),u===!0&&K._log.apply(K,a),void I._gaq.push(a))},V=function(a){h&&I.ga&&"function"==typeof a&&a()},W=function(){var a=Array.prototype.slice.call(arguments);return w===!0?void K.offlineQueue.push([W,a]):"function"!=typeof I.ga?void K._log("warn","ga function not set on window"):(u===!0&&K._log.apply(K,a),void I.ga.apply(null,a))},X=function(a){var b=Array.prototype.slice.call(arguments,1),d=b[0],e=[];return"function"==typeof a?c.forEach(function(b){a(b)&&e.push(b)}):e=c,0===e.length?void W.apply(K,b):void e.forEach(function(a){L("select",a)&&"function"==typeof a.select&&!a.select(b)||(b[0]=N(d,a),W.apply(K,b))})};return this._log=function(){var a=Array.prototype.slice.call(arguments);if(a.length>0){if(a.length>1)switch(a[0]){case"debug":case"error":case"info":case"log":case"warn":G[a[0]](a.slice(1))}K.log.push(a)}},this._createScriptTag=function(){K._registerScriptTags(),K._registerTrackers()},this._createAnalyticsScriptTag=function(){K._registerScriptTags(),K._registerTrackers()},this._registerScriptTags=function(){var a,b=E[0],d=S();if(j===!0)return void K._log("warn","Script tags already created");if(p===!0&&c.forEach(function(a){K._log("info","Analytics disabled: "+a.tracker),I["ga-disable-"+a.tracker]=!0}),h===!0){if(a=d+"//www.google-analytics.com/"+(m?"analytics_debug.js":"analytics.js"),z!==!0?!function(a,b,c,d,e,f,g){a.GoogleAnalyticsObject=e,a[e]=a[e]||function(){(a[e].q=a[e].q||[]).push(arguments)},a[e].l=1*new Date,f=b.createElement(c),g=b.getElementsByTagName(c)[0],f.async=1,f.src=d,g.parentNode.insertBefore(f,g)}(window,b,"script",a,"ga"):("function"!=typeof I.ga&&(I.ga=function(){}),K._log("inject",a)),A&&(I.ga_debug={trace:!0}),f){var e=b.createElement("script"),g=b.getElementsByTagName("script")[0];e.src=d+"//www.google-analytics.com/cx/api.js?experiment="+f,g.parentNode.insertBefore(e,g)}}else a=S("//www","//ssl")+".google-analytics.com/ga.js",o===!0&&(a=d+"//stats.g.doubleclick.net/dc.js"),z!==!0?!function(){var c=b.createElement("script");c.type="text/javascript",c.async=!0,c.src=a;var d=b.getElementsByTagName("script")[0];d.parentNode.insertBefore(c,d)}():K._log("inject",a);return j=!0,!0},this._registerTrackers=function(){return!c||c.length<1?void K._log("warn","No accounts to register"):(h===!0?c.forEach(function(b){b.crossDomainLinker=L("crossDomainLinker",b)?b.crossDomainLinker:k,b.crossLinkDomains=L("crossLinkDomains",b)?b.crossLinkDomains:d,b.displayFeatures=L("displayFeatures",b)?b.displayFeatures:o,b.enhancedLinkAttribution=L("enhancedLinkAttribution",b)?b.enhancedLinkAttribution:s,b.set=L("set",b)?b.set:{},b.trackEcommerce=L("trackEcommerce",b)?b.trackEcommerce:q,b.trackEvent=!!L("trackEvent",b)&&b.trackEvent;var c={};L("fields",b)?c=b.fields:L("cookieConfig",b)?a.isString(b.cookieConfig)?c.cookieDomain=b.cookieConfig:c=b.cookieConfig:a.isString(i)?c.cookieDomain=i:i&&(c=i),b.crossDomainLinker===!0&&(c.allowLinker=!0),L("name",b)&&(c.name=b.name),b.fields=c,W("create",b.tracker,b.fields),v===!0&&W(N("set",b),"checkProtocolTask",null);for(var e in b.set)b.set.hasOwnProperty(e)&&W(N("set",b),e,b.set[e]);b.crossDomainLinker===!0&&(W(N("require",b),"linker"),a.isDefined(b.crossLinkDomains)&&W(N("linker:autoLink",b),b.crossLinkDomains)),b.displayFeatures&&W(N("require",b),"displayfeatures"),b.trackEcommerce&&(r?(W(N("require",b),"ec"),W(N("set",b),"&cu",l)):W(N("require",b),"ecommerce")),b.enhancedLinkAttribution&&W(N("require",b),"linkid"),C&&!t&&W(N("send",b),"pageview",B+P())}):(c.length>1&&(K._log("warn","Multiple trackers are not supported with ga.js. Using first tracker only"),c=c.slice(0,1)),U("_setAccount",c[0].tracker),e&&U("_setDomainName",e),s&&U("_require","inpage_linkid","//www.google-analytics.com/plugins/ga/inpage_linkid.js"),C&&!t&&(g?U("_trackPageview",P()):U("_trackPageview"))),!0)},this._ecommerceEnabled=function(a,b){var c=q&&!r;return a===!0&&c===!1&&(q&&r?K._log("warn",b+" is not available when Enhanced Ecommerce is enabled with analytics.js"):K._log("warn","Ecommerce must be enabled to use "+b+" with analytics.js")),c},this._enhancedEcommerceEnabled=function(a,b){var c=q&&r;return a===!0&&c===!1&&K._log("warn","Enhanced Ecommerce must be enabled to use "+b+" with analytics.js"),c},this._trackPage=function(c,d,e){c=c?c:P(),d=d?d:E[0].title,T(function(){U("_set","title",d),U("_trackPageview",B+c)}),V(function(){var f={page:B+c,title:d};a.extend(f,Q()),a.isObject(e)&&a.extend(f,e),X(b,"send","pageview",f)})},this._trackEvent=function(b,c,d,e,f,g){T(function(){U("_trackEvent",b,c,d,e,!!f)}),V(function(){var h={},i=function(a){return M("trackEvent",a,!0)};a.isDefined(f)&&(h.nonInteraction=!!f),a.isObject(g)&&a.extend(h,g),a.isDefined(h.page)||(h.page=P()),X(i,"send","event",b,c,d,e,h)})},this._addTrans=function(a,b,c,d,e,f,g,h,i){T(function(){U("_addTrans",a,b,c,d,e,f,g,h)}),V(function(){if(K._ecommerceEnabled(!0,"addTrans")){var f=function(a){return M("trackEcommerce",a,!0)};X(f,"ecommerce:addTransaction",{id:a,affiliation:b,revenue:c,tax:d,shipping:e,currency:i||"USD"})}})},this._addItem=function(a,b,c,d,e,f){T(function(){U("_addItem",a,b,c,d,e,f)}),V(function(){if(K._ecommerceEnabled(!0,"addItem")){var g=function(a){return M("trackEcommerce",a,!0)};X(g,"ecommerce:addItem",{id:a,name:c,sku:b,category:d,price:e,quantity:f})}})},this._trackTrans=function(){T(function(){U("_trackTrans")}),V(function(){if(K._ecommerceEnabled(!0,"trackTrans")){var a=function(a){return M("trackEcommerce",a,!0)};X(a,"ecommerce:send")}})},this._clearTrans=function(){V(function(){if(K._ecommerceEnabled(!0,"clearTrans")){var a=function(a){return M("trackEcommerce",a,!0)};X(a,"ecommerce:clear")}})},this._addProduct=function(b,c,d,e,f,g,h,i,j,k){T(function(){U("_addProduct",b,c,d,e,f,g,h,i,j)}),V(function(){if(K._enhancedEcommerceEnabled(!0,"addProduct")){var l=function(a){return M("trackEcommerce",a,!0)},m={id:b,name:c,category:d,brand:e,variant:f,price:g,quantity:h,coupon:i,position:j};a.isObject(k)&&a.extend(m,k),X(l,"ec:addProduct",m)}})},this._addImpression=function(a,b,c,d,e,f,g,h){T(function(){U("_addImpression",a,b,c,d,e,f,g,h)}),V(function(){if(K._enhancedEcommerceEnabled(!0,"addImpression")){var i=function(a){return M("trackEcommerce",a,!0)};X(i,"ec:addImpression",{id:a,name:b,category:e,brand:d,variant:f,list:c,position:g,price:h})}})},this._addPromo=function(a,b,c,d){T(function(){U("_addPromo",a,b,c,d)}),V(function(){if(K._enhancedEcommerceEnabled(!0,"addPromo")){var e=function(a){return M("trackEcommerce",a,!0)};X(e,"ec:addPromo",{id:a,name:b,creative:c,position:d})}})},this._setAction=function(a,b){T(function(){U("_setAction",a,b)}),V(function(){if(K._enhancedEcommerceEnabled(!0,"setAction")){var c=function(a){return M("trackEcommerce",a,!0)};X(c,"ec:setAction",a,b)}})},this._trackTransaction=function(a,b,c,d,e,f,g,h,i){this._setAction("purchase",R(a,b,c,d,e,f,g,h,i))},this._trackRefund=function(a){this._setAction("refund",R(a))},this._trackCheckOut=function(a,b){this._setAction("checkout",R(null,null,null,null,null,null,null,a,b))},this._trackDetail=function(){this._setAction("detail"),this._pageView()},this._trackCart=function(a,b){["add","remove"].indexOf(a)!==-1&&(this._setAction(a,{list:b}),this._trackEvent("UX","click",a+("add"===a?" to cart":" from cart")))},this._promoClick=function(a){this._setAction("promo_click"),this._trackEvent("Internal Promotions","click",a)},this._productClick=function(a){this._setAction("click",R(null,null,null,null,null,null,a,null,null)),this._trackEvent("UX","click",a)},this._pageView=function(a){V(function(){W(N("send",a),"pageview")})},this._send=function(){var a=Array.prototype.slice.call(arguments);a.unshift("send"),V(function(){W.apply(K,a)})},this._set=function(a,b,c){V(function(){W(N("set",c),a,b)})},this._trackTimings=function(a,c,d,e){V(function(){X(b,"send","timing",a,c,d,e)})},this._trackException=function(a,c){V(function(){X(b,"send","exception",{exDescription:a,exFatal:!!c})})},n||(this._registerScriptTags(),this._registerTrackers()),C&&H.$on(x,function(){(!y||O.current&&O.current.templateUrl&&!O.current.doNotTrack)&&K._trackPage()}),{log:K.log,offlineQueue:K.offlineQueue,configuration:{accounts:c,universalAnalytics:h,crossDomainLinker:k,crossLinkDomains:d,currency:l,debugMode:m,delayScriptTag:n,disableAnalytics:p,displayFeatures:o,domainName:e,ecommerce:K._ecommerceEnabled(),enhancedEcommerce:K._enhancedEcommerceEnabled(),enhancedLinkAttribution:s,experimentId:f,hybridMobileSupport:v,ignoreFirstPageLoad:t,logAllCalls:u,pageEvent:x,readFromRoute:y,removeRegExp:g,testMode:z,traceDebuggingMode:A,trackPrefix:B,trackRoutes:C,trackUrlParams:D},getUrl:P,setCookieConfig:function(a){return K._log("warn","DEPRECATION WARNING: setCookieConfig method is deprecated. Please use tracker fields instead."),K._setCookieConfig.apply(K,arguments)},getCookieConfig:function(){return K._log("warn","DEPRECATION WARNING: getCookieConfig method is deprecated. Please use tracker fields instead."),i},createAnalyticsScriptTag:function(a){return K._log("warn","DEPRECATION WARNING: createAnalyticsScriptTag method is deprecated. Please use registerScriptTags and registerTrackers methods instead."),a&&(i=a),K._createAnalyticsScriptTag()},createScriptTag:function(){return K._log("warn","DEPRECATION WARNING: createScriptTag method is deprecated. Please use registerScriptTags and registerTrackers methods instead."),K._createScriptTag()},registerScriptTags:function(){return K._registerScriptTags()},registerTrackers:function(){return K._registerTrackers()},offline:function(a){if(a===!0&&w===!1&&(w=!0),a===!1&&w===!0)for(w=!1;K.offlineQueue.length>0;){var b=K.offlineQueue.shift();b[0].apply(K,b[1])}return w},trackPage:function(a,b,c){K._trackPage.apply(K,arguments)},trackEvent:function(a,b,c,d,e,f){K._trackEvent.apply(K,arguments)},addTrans:function(a,b,c,d,e,f,g,h,i){K._addTrans.apply(K,arguments)},addItem:function(a,b,c,d,e,f){K._addItem.apply(K,arguments)},trackTrans:function(){K._trackTrans.apply(K,arguments)},clearTrans:function(){K._clearTrans.apply(K,arguments)},addProduct:function(a,b,c,d,e,f,g,h,i,j){K._addProduct.apply(K,arguments)},addPromo:function(a,b,c,d){K._addPromo.apply(K,arguments)},addImpression:function(a,b,c,d,e,f,g,h){K._addImpression.apply(K,arguments)},productClick:function(a){K._productClick.apply(K,arguments)},promoClick:function(a){K._promoClick.apply(K,arguments)},trackDetail:function(){K._trackDetail.apply(K,arguments)},trackCart:function(a,b){K._trackCart.apply(K,arguments)},trackCheckout:function(a,b){K._trackCheckOut.apply(K,arguments)},trackTimings:function(a,b,c,d){K._trackTimings.apply(K,arguments)},trackTransaction:function(a,b,c,d,e,f,g,h,i){K._trackTransaction.apply(K,arguments)},trackException:function(a,b){K._trackException.apply(K,arguments)},setAction:function(a,b){K._setAction.apply(K,arguments)},pageView:function(){K._pageView.apply(K,arguments)},send:function(a){K._send.apply(K,arguments)},set:function(a,b,c){K._set.apply(K,arguments)}}}]}).directive("gaTrackEvent",["Analytics","$parse",function(a,b){return{restrict:"A",link:function(c,d,e){var f=b(e.gaTrackEvent);d.bind("click",function(){e.gaTrackEventIf&&!c.$eval(e.gaTrackEventIf)||f.length>1&&a.trackEvent.apply(a,f(c))})}}}]),a.module("angular-google-analytics")}); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* globals define */ 2 | (function (root, factory) { 3 | 'use strict'; 4 | if (typeof module !== 'undefined' && module.exports) { 5 | if (typeof angular === 'undefined') { 6 | factory(require('angular')); 7 | } else { 8 | factory(angular); 9 | } 10 | module.exports = 'angular-google-analytics'; 11 | } else if (typeof define === 'function' && define.amd) { 12 | define(['angular'], factory); 13 | } else { 14 | factory(root.angular); 15 | } 16 | }(this, function (angular, undefined) { 17 | 'use strict'; 18 | angular.module('angular-google-analytics', []) 19 | .provider('Analytics', function () { 20 | var accounts, 21 | analyticsJS = true, 22 | created = false, 23 | crossDomainLinker = false, 24 | crossLinkDomains, 25 | currency = 'USD', 26 | debugMode = false, 27 | delayScriptTag = false, 28 | displayFeatures = false, 29 | disableAnalytics = false, 30 | domainName, 31 | ecommerce = false, 32 | enhancedEcommerce = false, 33 | enhancedLinkAttribution = false, 34 | experimentId, 35 | ignoreFirstPageLoad = false, 36 | logAllCalls = false, 37 | hybridMobileSupport = false, 38 | offlineMode = false, 39 | pageEvent = '$routeChangeSuccess', 40 | readFromRoute = false, 41 | removeRegExp, 42 | testMode = false, 43 | traceDebuggingMode = false, 44 | trackPrefix = '', 45 | trackRoutes = true, 46 | trackUrlParams = false; 47 | 48 | this.log = []; 49 | this.offlineQueue = []; 50 | 51 | /** 52 | * Configuration Methods 53 | **/ 54 | 55 | this.setAccount = function (tracker) { 56 | if (angular.isUndefined(tracker) || tracker === false) { 57 | accounts = undefined; 58 | } else if (angular.isArray(tracker)) { 59 | accounts = tracker; 60 | } else if (angular.isObject(tracker)) { 61 | accounts = [tracker]; 62 | } else { 63 | // In order to preserve an existing behavior with how the _trackEvent function works, 64 | // the trackEvent property must be set to true when there is only a single tracker. 65 | accounts = [{ tracker: tracker, trackEvent: true }]; 66 | } 67 | return this; 68 | }; 69 | 70 | this.trackPages = function (val) { 71 | trackRoutes = !!val; 72 | return this; 73 | }; 74 | 75 | this.trackPrefix = function (prefix) { 76 | trackPrefix = prefix; 77 | return this; 78 | }; 79 | 80 | this.setDomainName = function (domain) { 81 | domainName = domain; 82 | return this; 83 | }; 84 | 85 | this.useDisplayFeatures = function (val) { 86 | displayFeatures = !!val; 87 | return this; 88 | }; 89 | 90 | this.useAnalytics = function (val) { 91 | analyticsJS = !!val; 92 | return this; 93 | }; 94 | 95 | this.useEnhancedLinkAttribution = function (val) { 96 | enhancedLinkAttribution = !!val; 97 | return this; 98 | }; 99 | 100 | this.useCrossDomainLinker = function (val) { 101 | crossDomainLinker = !!val; 102 | return this; 103 | }; 104 | 105 | this.setCrossLinkDomains = function (domains) { 106 | crossLinkDomains = domains; 107 | return this; 108 | }; 109 | 110 | this.setPageEvent = function (name) { 111 | pageEvent = name; 112 | return this; 113 | }; 114 | 115 | this.useECommerce = function (val, enhanced) { 116 | ecommerce = !!val; 117 | enhancedEcommerce = !!enhanced; 118 | return this; 119 | }; 120 | 121 | this.setCurrency = function (currencyCode) { 122 | currency = currencyCode; 123 | return this; 124 | }; 125 | 126 | this.setRemoveRegExp = function (regex) { 127 | if (regex instanceof RegExp) { 128 | removeRegExp = regex; 129 | } 130 | return this; 131 | }; 132 | 133 | this.setExperimentId = function (id) { 134 | experimentId = id; 135 | return this; 136 | }; 137 | 138 | this.ignoreFirstPageLoad = function (val) { 139 | ignoreFirstPageLoad = !!val; 140 | return this; 141 | }; 142 | 143 | this.trackUrlParams = function (val) { 144 | trackUrlParams = !!val; 145 | return this; 146 | }; 147 | 148 | this.disableAnalytics = function (val) { 149 | disableAnalytics = !!val; 150 | return this; 151 | }; 152 | 153 | this.setHybridMobileSupport = function (val) { 154 | hybridMobileSupport = !!val; 155 | return this; 156 | }; 157 | 158 | this.startOffline = function (val) { 159 | offlineMode = !!val; 160 | if (offlineMode === true) { 161 | this.delayScriptTag(true); 162 | } 163 | return this; 164 | }; 165 | 166 | this.delayScriptTag = function (val) { 167 | delayScriptTag = !!val; 168 | return this; 169 | }; 170 | 171 | this.logAllCalls = function (val) { 172 | logAllCalls = !!val; 173 | return this; 174 | }; 175 | 176 | this.enterTestMode = function () { 177 | testMode = true; 178 | return this; 179 | }; 180 | 181 | this.enterDebugMode = function (enableTraceDebugging) { 182 | debugMode = true; 183 | traceDebuggingMode = !!enableTraceDebugging; 184 | return this; 185 | }; 186 | 187 | // Enable reading page url from route object 188 | this.readFromRoute = function(val) { 189 | readFromRoute = !!val; 190 | return this; 191 | }; 192 | 193 | /** 194 | * Public Service 195 | */ 196 | this.$get = ['$document', // To read page title 197 | '$location', // 198 | '$log', // 199 | '$rootScope',// 200 | '$window', // 201 | '$injector', // To access ngRoute module without declaring a fixed dependency 202 | function ($document, $location, $log, $rootScope, $window, $injector) { 203 | var that = this; 204 | 205 | /** 206 | * Side-effect Free Helper Methods 207 | **/ 208 | 209 | var isFunction = function (fn) { 210 | return typeof fn === 'function'; 211 | }; 212 | 213 | var isPropertyDefined = function (key, config) { 214 | return angular.isObject(config) && angular.isDefined(config[key]); 215 | }; 216 | 217 | var isPropertySetTo = function (key, config, value) { 218 | return isPropertyDefined(key, config) && config[key] === value; 219 | }; 220 | 221 | var generateCommandName = function (commandName, config) { 222 | if (angular.isString(config)) { 223 | return config + '.' + commandName; 224 | } 225 | return isPropertyDefined('name', config) ? (config.name + '.' + commandName) : commandName; 226 | }; 227 | 228 | // Try to read route configuration and log warning if not possible 229 | var $route = {}; 230 | if (readFromRoute) { 231 | if (!$injector.has('$route')) { 232 | $log.warn('$route service is not available. Make sure you have included ng-route in your application dependencies.'); 233 | } else { 234 | $route = $injector.get('$route'); 235 | } 236 | } 237 | 238 | // Get url for current page 239 | var getUrl = function () { 240 | // Using ngRoute provided tracking urls 241 | if (readFromRoute && $route.current && ('pageTrack' in $route.current)) { 242 | return $route.current.pageTrack; 243 | } 244 | 245 | // Otherwise go the old way 246 | var url = trackUrlParams ? $location.url() : $location.path(); 247 | return removeRegExp ? url.replace(removeRegExp, '') : url; 248 | }; 249 | 250 | var getUtmParams = function () { 251 | var utmToCampaignVar = { 252 | utm_source: 'campaignSource', 253 | utm_medium: 'campaignMedium', 254 | utm_term: 'campaignTerm', 255 | utm_content: 'campaignContent', 256 | utm_campaign: 'campaignName' 257 | }; 258 | var object = {}; 259 | 260 | angular.forEach($location.search(), function (value, key) { 261 | var campaignVar = utmToCampaignVar[key]; 262 | 263 | if (angular.isDefined(campaignVar)) { 264 | object[campaignVar] = value; 265 | } 266 | }); 267 | 268 | return object; 269 | }; 270 | 271 | /** 272 | * get ActionFieldObject 273 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#action-data 274 | * @param id 275 | * @param affliation 276 | * @param revenue 277 | * @param tax 278 | * @param shipping 279 | * @param coupon 280 | * @param list 281 | * @param step 282 | * @param option 283 | */ 284 | var getActionFieldObject = function (id, affiliation, revenue, tax, shipping, coupon, list, step, option) { 285 | var obj = {}; 286 | if (id) { obj.id = id; } 287 | if (affiliation) { obj.affiliation = affiliation; } 288 | if (revenue) { obj.revenue = revenue; } 289 | if (tax) { obj.tax = tax; } 290 | if (shipping) { obj.shipping = shipping; } 291 | if (coupon) { obj.coupon = coupon; } 292 | if (list) { obj.list = list; } 293 | if (step) { obj.step = step; } 294 | if (option) { obj.option = option; } 295 | return obj; 296 | }; 297 | 298 | /** 299 | * Private Methods 300 | */ 301 | 302 | var _getTrackPrefixUrl = function (url) { 303 | return trackPrefix + (url ? url : getUrl()); 304 | }; 305 | 306 | var _getProtocol = function (httpPostfix, httpsPostfix) { 307 | var protocol = '', 308 | isSslEnabled = document.location.protocol === 'https:', 309 | isChromeExtension = document.location.protocol === 'chrome-extension:', 310 | isHybridApplication = analyticsJS === true && hybridMobileSupport === true; 311 | httpPostfix = angular.isString(httpPostfix) ? httpPostfix : ''; 312 | httpsPostfix = angular.isString(httpsPostfix) ? httpsPostfix : ''; 313 | if (httpPostfix !== '') { 314 | protocol = 'http:' + httpPostfix; 315 | } 316 | if (isChromeExtension || isHybridApplication || (isSslEnabled && httpsPostfix !== '')) { 317 | protocol = 'https:' + httpsPostfix; 318 | } 319 | return protocol; 320 | }; 321 | 322 | var _gaJs = function (fn) { 323 | if (!analyticsJS && $window._gaq && isFunction(fn)) { 324 | fn(); 325 | } 326 | }; 327 | 328 | var _gaq = function () { 329 | var args = Array.prototype.slice.call(arguments); 330 | if (offlineMode === true) { 331 | that.offlineQueue.push([_gaq, args]); 332 | return; 333 | } 334 | if (!$window._gaq) { 335 | $window._gaq = []; 336 | } 337 | if (logAllCalls === true) { 338 | that._log.apply(that, args); 339 | } 340 | $window._gaq.push(args); 341 | }; 342 | 343 | var _analyticsJs = function (fn) { 344 | if (analyticsJS && $window.ga && isFunction(fn)) { 345 | fn(); 346 | } 347 | }; 348 | 349 | var _ga = function () { 350 | var args = Array.prototype.slice.call(arguments); 351 | if (offlineMode === true) { 352 | that.offlineQueue.push([_ga, args]); 353 | return; 354 | } 355 | if (!isFunction($window.ga)) { 356 | that._log('warn', 'ga function not set on window'); 357 | return; 358 | } 359 | if (logAllCalls === true) { 360 | that._log.apply(that, args); 361 | } 362 | $window.ga.apply(null, args); 363 | }; 364 | 365 | var _gaMultipleTrackers = function (includeFn) { 366 | // Drop the includeFn from the arguments and preserve the original command name 367 | var args = Array.prototype.slice.call(arguments, 1), 368 | commandName = args[0], 369 | trackers = []; 370 | if (isFunction(includeFn)) { 371 | accounts.forEach(function (account) { 372 | if (includeFn(account)) { 373 | trackers.push(account); 374 | } 375 | }); 376 | } else { 377 | // No include function indicates that all accounts are to be used 378 | trackers = accounts; 379 | } 380 | 381 | // To preserve backwards compatibility fallback to _ga method if no account 382 | // matches the specified includeFn. This preserves existing behaviors by 383 | // performing the single tracker operation. 384 | if (trackers.length === 0) { 385 | _ga.apply(that, args); 386 | return; 387 | } 388 | 389 | trackers.forEach(function (tracker) { 390 | // Check tracker 'select' function, if it exists, for whether the tracker should be used with the current command. 391 | // If the 'select' function returns false then the tracker will not be used with the current command. 392 | if (isPropertyDefined('select', tracker) && isFunction(tracker.select) && !tracker.select(args)) { 393 | return; 394 | } 395 | args[0] = generateCommandName(commandName, tracker); 396 | _ga.apply(that, args); 397 | }); 398 | }; 399 | 400 | this._log = function () { 401 | var args = Array.prototype.slice.call(arguments); 402 | if (args.length > 0) { 403 | if (args.length > 1) { 404 | switch (args[0]) { 405 | case 'debug': 406 | case 'error': 407 | case 'info': 408 | case 'log': 409 | case 'warn': 410 | $log[args[0]](args.slice(1)); 411 | break; 412 | } 413 | } 414 | that.log.push(args); 415 | } 416 | }; 417 | 418 | this._registerScriptTags = function () { 419 | var document = $document[0], 420 | protocol = _getProtocol(), 421 | scriptSource; 422 | 423 | if (created === true) { 424 | that._log('warn', 'Script tags already created'); 425 | return false; 426 | } 427 | 428 | if (disableAnalytics === true) { 429 | accounts.forEach(function (trackerObj) { 430 | that._log('info', 'Analytics disabled: ' + trackerObj.tracker); 431 | $window['ga-disable-' + trackerObj.tracker] = true; 432 | }); 433 | } 434 | 435 | // 436 | // Universal Analytics 437 | // 438 | if (analyticsJS === true) { 439 | scriptSource = protocol + '//www.google-analytics.com/' + (debugMode ? 'analytics_debug.js' : 'analytics.js'); 440 | if (testMode !== true) { 441 | // If not in test mode inject the Google Analytics tag 442 | (function (i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function (){ 443 | (i[r].q=i[r].q||[]).push(arguments);},i[r].l=1*new Date();a=s.createElement(o), 444 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m); 445 | })(window,document,'script',scriptSource,'ga'); 446 | } else { 447 | if (!isFunction($window.ga)) { 448 | // In test mode create a ga function if none exists that is a noop sink. 449 | $window.ga = function () {}; 450 | } 451 | // Log script injection. 452 | that._log('inject', scriptSource); 453 | } 454 | 455 | if (traceDebuggingMode) { 456 | $window.ga_debug = { trace: true }; 457 | } 458 | 459 | if (experimentId) { 460 | var expScript = document.createElement('script'), 461 | s = document.getElementsByTagName('script')[0]; 462 | expScript.src = protocol + '//www.google-analytics.com/cx/api.js?experiment=' + experimentId; 463 | s.parentNode.insertBefore(expScript, s); 464 | } 465 | // 466 | // Classic Analytics 467 | // 468 | } else { 469 | scriptSource = _getProtocol('//www', '//ssl') + '.google-analytics.com/ga.js'; 470 | if (displayFeatures === true) { 471 | scriptSource = protocol + '//stats.g.doubleclick.net/dc.js'; 472 | } 473 | 474 | if (testMode !== true) { 475 | // If not in test mode inject the Google Analytics tag 476 | (function () { 477 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 478 | ga.src = scriptSource; 479 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 480 | })(); 481 | } else { 482 | // Log the source location for validation 483 | that._log('inject', scriptSource); 484 | } 485 | } 486 | 487 | created = true; 488 | return true; 489 | }; 490 | 491 | this._registerTrackers = function () { 492 | if (!accounts || accounts.length < 1) { 493 | that._log('warn', 'No accounts to register'); 494 | return false; 495 | } 496 | 497 | // 498 | // Universal Analytics 499 | // 500 | if (analyticsJS === true) { 501 | accounts.forEach(function (trackerObj) { 502 | trackerObj.crossDomainLinker = isPropertyDefined('crossDomainLinker', trackerObj) ? trackerObj.crossDomainLinker : crossDomainLinker; 503 | trackerObj.crossLinkDomains = isPropertyDefined('crossLinkDomains', trackerObj) ? trackerObj.crossLinkDomains : crossLinkDomains; 504 | trackerObj.displayFeatures = isPropertyDefined('displayFeatures', trackerObj) ? trackerObj.displayFeatures : displayFeatures; 505 | trackerObj.enhancedLinkAttribution = isPropertyDefined('enhancedLinkAttribution', trackerObj) ? trackerObj.enhancedLinkAttribution : enhancedLinkAttribution; 506 | trackerObj.set = isPropertyDefined('set', trackerObj) ? trackerObj.set : {}; 507 | trackerObj.trackEcommerce = isPropertyDefined('trackEcommerce', trackerObj) ? trackerObj.trackEcommerce : ecommerce; 508 | trackerObj.trackEvent = isPropertyDefined('trackEvent', trackerObj) ? trackerObj.trackEvent : false; 509 | 510 | var fields = {}; 511 | if (isPropertyDefined('fields', trackerObj)) { 512 | fields = trackerObj.fields; 513 | } 514 | if (trackerObj.crossDomainLinker === true) { 515 | fields.allowLinker = true; 516 | } 517 | if (isPropertyDefined('name', trackerObj)) { 518 | fields.name = trackerObj.name; 519 | } 520 | trackerObj.fields = fields; 521 | 522 | _ga('create', trackerObj.tracker, trackerObj.fields); 523 | 524 | // Hybrid mobile application support 525 | // https://developers.google.com/analytics/devguides/collection/analyticsjs/tasks 526 | if (hybridMobileSupport === true) { 527 | _ga(generateCommandName('set', trackerObj), 'checkProtocolTask', null); 528 | } 529 | 530 | // Send all custom set commands from the trackerObj.set property 531 | for (var key in trackerObj.set) { 532 | if (trackerObj.set.hasOwnProperty(key)) { 533 | _ga(generateCommandName('set', trackerObj), key, trackerObj.set[key]); 534 | } 535 | } 536 | 537 | if (trackerObj.crossDomainLinker === true) { 538 | _ga(generateCommandName('require', trackerObj), 'linker'); 539 | if (angular.isDefined(trackerObj.crossLinkDomains)) { 540 | _ga(generateCommandName('linker:autoLink', trackerObj), trackerObj.crossLinkDomains); 541 | } 542 | } 543 | 544 | if (trackerObj.displayFeatures) { 545 | _ga(generateCommandName('require', trackerObj), 'displayfeatures'); 546 | } 547 | 548 | if (trackerObj.trackEcommerce) { 549 | if (!enhancedEcommerce) { 550 | _ga(generateCommandName('require', trackerObj), 'ecommerce'); 551 | } else { 552 | _ga(generateCommandName('require', trackerObj), 'ec'); 553 | _ga(generateCommandName('set', trackerObj), '&cu', currency); 554 | } 555 | } 556 | 557 | if (trackerObj.enhancedLinkAttribution) { 558 | _ga(generateCommandName('require', trackerObj), 'linkid'); 559 | } 560 | 561 | if (trackRoutes && !ignoreFirstPageLoad) { 562 | _ga(generateCommandName('send', trackerObj), 'pageview', _getTrackPrefixUrl()); 563 | } 564 | }); 565 | // 566 | // Classic Analytics 567 | // 568 | } else { 569 | if (accounts.length > 1) { 570 | that._log('warn', 'Multiple trackers are not supported with ga.js. Using first tracker only'); 571 | accounts = accounts.slice(0, 1); 572 | } 573 | 574 | _gaq('_setAccount', accounts[0].tracker); 575 | if(domainName) { 576 | _gaq('_setDomainName', domainName); 577 | } 578 | if (enhancedLinkAttribution) { 579 | _gaq('_require', 'inpage_linkid', '//www.google-analytics.com/plugins/ga/inpage_linkid.js'); 580 | } 581 | if (trackRoutes && !ignoreFirstPageLoad) { 582 | if (removeRegExp) { 583 | _gaq('_trackPageview', getUrl()); 584 | } else { 585 | _gaq('_trackPageview'); 586 | } 587 | } 588 | } 589 | 590 | return true; 591 | }; 592 | 593 | this._ecommerceEnabled = function (warn, command) { 594 | var result = ecommerce && !enhancedEcommerce; 595 | if (warn === true && result === false) { 596 | if (ecommerce && enhancedEcommerce) { 597 | that._log('warn', command + ' is not available when Enhanced Ecommerce is enabled with analytics.js'); 598 | } else { 599 | that._log('warn', 'Ecommerce must be enabled to use ' + command + ' with analytics.js'); 600 | } 601 | } 602 | return result; 603 | }; 604 | 605 | this._enhancedEcommerceEnabled = function (warn, command) { 606 | var result = ecommerce && enhancedEcommerce; 607 | if (warn === true && result === false) { 608 | that._log('warn', 'Enhanced Ecommerce must be enabled to use ' + command + ' with analytics.js'); 609 | } 610 | return result; 611 | }; 612 | 613 | /** 614 | * Track page 615 | https://developers.google.com/analytics/devguides/collection/gajs/ 616 | https://developers.google.com/analytics/devguides/collection/analyticsjs/pages 617 | * @param url 618 | * @param title 619 | * @param custom 620 | * @private 621 | */ 622 | this._trackPage = function (url, title, custom) { 623 | title = title ? title : $document[0].title; 624 | _gaJs(function () { 625 | // http://stackoverflow.com/questions/7322288/how-can-i-set-a-page-title-with-google-analytics 626 | _gaq('_set', 'title', title); 627 | _gaq('_trackPageview', _getTrackPrefixUrl(url)); 628 | }); 629 | _analyticsJs(function () { 630 | var opt_fieldObject = { 631 | 'page': _getTrackPrefixUrl(url), 632 | 'title': title 633 | }; 634 | angular.extend(opt_fieldObject, getUtmParams()); 635 | if (angular.isObject(custom)) { 636 | angular.extend(opt_fieldObject, custom); 637 | } 638 | _gaMultipleTrackers(undefined, 'send', 'pageview', opt_fieldObject); 639 | }); 640 | }; 641 | 642 | /** 643 | * Track event 644 | https://developers.google.com/analytics/devguides/collection/gajs/eventTrackerGuide 645 | https://developers.google.com/analytics/devguides/collection/analyticsjs/events 646 | * @param category 647 | * @param action 648 | * @param label 649 | * @param value 650 | * @param noninteraction 651 | * @param custom 652 | * @private 653 | */ 654 | this._trackEvent = function (category, action, label, value, noninteraction, custom) { 655 | _gaJs(function () { 656 | _gaq('_trackEvent', category, action, label, value, !!noninteraction); 657 | }); 658 | _analyticsJs(function () { 659 | var opt_fieldObject = {}; 660 | var includeFn = function (trackerObj) { 661 | return isPropertySetTo('trackEvent', trackerObj, true); 662 | }; 663 | 664 | if (angular.isDefined(noninteraction)) { 665 | opt_fieldObject.nonInteraction = !!noninteraction; 666 | } 667 | if (angular.isObject(custom)) { 668 | angular.extend(opt_fieldObject, custom); 669 | } 670 | if (!angular.isDefined(opt_fieldObject.page)) { 671 | opt_fieldObject.page = _getTrackPrefixUrl(); 672 | } 673 | _gaMultipleTrackers(includeFn, 'send', 'event', category, action, label, value, opt_fieldObject); 674 | }); 675 | }; 676 | 677 | /** 678 | * Add transaction 679 | * https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiEcommerce#_gat.GA_Tracker_._addTrans 680 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#addTrans 681 | * @param transactionId 682 | * @param affiliation 683 | * @param total 684 | * @param tax 685 | * @param shipping 686 | * @param city 687 | * @param state 688 | * @param country 689 | * @private 690 | */ 691 | this._addTrans = function (transactionId, affiliation, total, tax, shipping, city, state, country, currency) { 692 | _gaJs(function () { 693 | _gaq('_addTrans', transactionId, affiliation, total, tax, shipping, city, state, country); 694 | }); 695 | _analyticsJs(function () { 696 | if (that._ecommerceEnabled(true, 'addTrans')) { 697 | var includeFn = function (trackerObj) { 698 | return isPropertySetTo('trackEcommerce', trackerObj, true); 699 | }; 700 | 701 | _gaMultipleTrackers( 702 | includeFn, 703 | 'ecommerce:addTransaction', 704 | { 705 | id: transactionId, 706 | affiliation: affiliation, 707 | revenue: total, 708 | tax: tax, 709 | shipping: shipping, 710 | currency: currency || 'USD' 711 | }); 712 | } 713 | }); 714 | }; 715 | 716 | /** 717 | * Add item to transaction 718 | * https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiEcommerce#_gat.GA_Tracker_._addItem 719 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#addItem 720 | * @param transactionId 721 | * @param sku 722 | * @param name 723 | * @param category 724 | * @param price 725 | * @param quantity 726 | * @private 727 | */ 728 | this._addItem = function (transactionId, sku, name, category, price, quantity) { 729 | _gaJs(function () { 730 | _gaq('_addItem', transactionId, sku, name, category, price, quantity); 731 | }); 732 | _analyticsJs(function () { 733 | if (that._ecommerceEnabled(true, 'addItem')) { 734 | var includeFn = function (trackerObj) { 735 | return isPropertySetTo('trackEcommerce', trackerObj, true); 736 | }; 737 | 738 | _gaMultipleTrackers( 739 | includeFn, 740 | 'ecommerce:addItem', 741 | { 742 | id: transactionId, 743 | name: name, 744 | sku: sku, 745 | category: category, 746 | price: price, 747 | quantity: quantity 748 | }); 749 | } 750 | }); 751 | }; 752 | 753 | /** 754 | * Track transaction 755 | * https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiEcommerce#_gat.GA_Tracker_._trackTrans 756 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#sendingData 757 | * @private 758 | */ 759 | this._trackTrans = function () { 760 | _gaJs(function () { 761 | _gaq('_trackTrans'); 762 | }); 763 | _analyticsJs(function () { 764 | if (that._ecommerceEnabled(true, 'trackTrans')) { 765 | var includeFn = function (trackerObj) { 766 | return isPropertySetTo('trackEcommerce', trackerObj, true); 767 | }; 768 | 769 | _gaMultipleTrackers(includeFn, 'ecommerce:send'); 770 | } 771 | }); 772 | }; 773 | 774 | /** 775 | * Clear transaction 776 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce#clearingData 777 | * @private 778 | */ 779 | this._clearTrans = function () { 780 | _analyticsJs(function () { 781 | if (that._ecommerceEnabled(true, 'clearTrans')) { 782 | var includeFn = function (trackerObj) { 783 | return isPropertySetTo('trackEcommerce', trackerObj, true); 784 | }; 785 | 786 | _gaMultipleTrackers(includeFn, 'ecommerce:clear'); 787 | } 788 | }); 789 | }; 790 | 791 | /** 792 | * Enhanced Ecommerce 793 | */ 794 | 795 | /** 796 | * Add Product 797 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#product-data 798 | * @param productId 799 | * @param name 800 | * @param category 801 | * @param brand 802 | * @param variant 803 | * @param price 804 | * @param quantity 805 | * @param coupon 806 | * @param position 807 | * @param custom 808 | * @private 809 | */ 810 | this._addProduct = function (productId, name, category, brand, variant, price, quantity, coupon, position, custom) { 811 | _gaJs(function () { 812 | _gaq('_addProduct', productId, name, category, brand, variant, price, quantity, coupon, position); 813 | }); 814 | _analyticsJs(function () { 815 | if (that._enhancedEcommerceEnabled(true, 'addProduct')) { 816 | var includeFn = function (trackerObj) { 817 | return isPropertySetTo('trackEcommerce', trackerObj, true); 818 | }; 819 | var details = { 820 | id: productId, 821 | name: name, 822 | category: category, 823 | brand: brand, 824 | variant: variant, 825 | price: price, 826 | quantity: quantity, 827 | coupon: coupon, 828 | position: position 829 | }; 830 | if (angular.isObject(custom)) { 831 | angular.extend(details, custom); 832 | } 833 | _gaMultipleTrackers(includeFn, 'ec:addProduct', details); 834 | } 835 | }); 836 | }; 837 | 838 | /** 839 | * Add Impression 840 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#impression-data 841 | * @param id 842 | * @param name 843 | * @param list 844 | * @param brand 845 | * @param category 846 | * @param variant 847 | * @param position 848 | * @param price 849 | * @private 850 | */ 851 | this._addImpression = function (id, name, list, brand, category, variant, position, price){ 852 | _gaJs(function () { 853 | _gaq('_addImpression', id, name, list, brand, category, variant, position, price); 854 | }); 855 | _analyticsJs(function () { 856 | if (that._enhancedEcommerceEnabled(true, 'addImpression')) { 857 | var includeFn = function (trackerObj) { 858 | return isPropertySetTo('trackEcommerce', trackerObj, true); 859 | }; 860 | 861 | _gaMultipleTrackers( 862 | includeFn, 863 | 'ec:addImpression', 864 | { 865 | id: id, 866 | name: name, 867 | category: category, 868 | brand: brand, 869 | variant: variant, 870 | list: list, 871 | position: position, 872 | price: price 873 | }); 874 | } 875 | }); 876 | }; 877 | 878 | /** 879 | * Add Promo 880 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce 881 | * @param productId 882 | * @param name 883 | * @param creative 884 | * @param position 885 | * @private 886 | */ 887 | this._addPromo = function (productId, name, creative, position) { 888 | _gaJs(function () { 889 | _gaq('_addPromo', productId, name, creative, position); 890 | }); 891 | _analyticsJs(function () { 892 | if (that._enhancedEcommerceEnabled(true, 'addPromo')) { 893 | var includeFn = function (trackerObj) { 894 | return isPropertySetTo('trackEcommerce', trackerObj, true); 895 | }; 896 | 897 | _gaMultipleTrackers( 898 | includeFn, 899 | 'ec:addPromo', 900 | { 901 | id: productId, 902 | name: name, 903 | creative: creative, 904 | position: position 905 | }); 906 | } 907 | }); 908 | }; 909 | 910 | /** 911 | * Set Action 912 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#measuring-actions 913 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#action-types 914 | * @param action 915 | * @param obj 916 | * @private 917 | */ 918 | this._setAction = function (action, obj){ 919 | _gaJs(function () { 920 | _gaq('_setAction', action, obj); 921 | }); 922 | _analyticsJs(function () { 923 | if (that._enhancedEcommerceEnabled(true, 'setAction')) { 924 | var includeFn = function (trackerObj) { 925 | return isPropertySetTo('trackEcommerce', trackerObj, true); 926 | }; 927 | 928 | _gaMultipleTrackers(includeFn, 'ec:setAction', action, obj); 929 | } 930 | }); 931 | }; 932 | 933 | /** 934 | * Track Transaction 935 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#measuring-transactions 936 | * @param transactionId 937 | * @param affiliation 938 | * @param revenue 939 | * @param tax 940 | * @param shipping 941 | * @param coupon 942 | * @param list 943 | * @param step 944 | * @param option 945 | * @private 946 | */ 947 | this._trackTransaction = function (transactionId, affiliation, revenue, tax, shipping, coupon, list, step, option) { 948 | this._setAction('purchase', getActionFieldObject(transactionId, affiliation, revenue, tax, shipping, coupon, list, step, option)); 949 | }; 950 | 951 | /** 952 | * Track Refund 953 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#measuring-refunds 954 | * @param transactionId 955 | * @private 956 | */ 957 | this._trackRefund = function (transactionId) { 958 | this._setAction('refund', getActionFieldObject(transactionId)); 959 | }; 960 | 961 | /** 962 | * Track Checkout 963 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#measuring-checkout 964 | * @param step 965 | * @param option 966 | * @private 967 | */ 968 | this._trackCheckOut = function (step, option) { 969 | this._setAction('checkout', getActionFieldObject(null, null, null, null, null, null, null, step, option)); 970 | }; 971 | 972 | /** 973 | * Track detail 974 | * @private 975 | */ 976 | this._trackDetail = function () { 977 | this._setAction('detail'); 978 | this._pageView(); 979 | }; 980 | 981 | /** 982 | * Track add/remove to cart 983 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#add-remove-cart 984 | * @param action 985 | * @param list 986 | * @private 987 | */ 988 | this._trackCart = function (action, listName) { 989 | if (['add', 'remove'].indexOf(action) !== -1) { 990 | this._setAction(action, { list: listName }); 991 | this._trackEvent('UX', 'click', action + (action === 'add' ? ' to cart' : ' from cart')); 992 | } 993 | }; 994 | 995 | /** 996 | * Track promo click 997 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#measuring-promo-clicks 998 | * @param promotionName 999 | * @private 1000 | */ 1001 | this._promoClick = function (promotionName) { 1002 | this._setAction('promo_click'); 1003 | this._trackEvent('Internal Promotions', 'click', promotionName); 1004 | }; 1005 | 1006 | /** 1007 | * Track product click 1008 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce#measuring-promo-clicks 1009 | * @param promotionName 1010 | * @private 1011 | */ 1012 | this._productClick = function (listName) { 1013 | this._setAction('click', getActionFieldObject(null, null, null, null, null, null, listName, null, null)); 1014 | this._trackEvent('UX', 'click', listName); 1015 | }; 1016 | 1017 | /** 1018 | * Send page view 1019 | * @param trackerName 1020 | * @private 1021 | */ 1022 | this._pageView = function (trackerName) { 1023 | _analyticsJs(function () { 1024 | _ga(generateCommandName('send', trackerName), 'pageview'); 1025 | }); 1026 | }; 1027 | 1028 | /** 1029 | * Send custom events 1030 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings#implementation 1031 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/social-interactions#implementation 1032 | * @private 1033 | */ 1034 | this._send = function () { 1035 | var args = Array.prototype.slice.call(arguments); 1036 | args.unshift('send'); 1037 | _analyticsJs(function () { 1038 | _ga.apply(that, args); 1039 | }); 1040 | }; 1041 | 1042 | /** 1043 | * Set custom dimensions, metrics or experiment 1044 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/custom-dims-mets 1045 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#customs 1046 | * @param name (Required) 1047 | * @param value (Required) 1048 | * @param trackerName (Optional) 1049 | * @private 1050 | */ 1051 | this._set = function (name, value, trackerName) { 1052 | _analyticsJs(function () { 1053 | _ga(generateCommandName('set', trackerName), name, value); 1054 | }); 1055 | }; 1056 | 1057 | /** 1058 | * Track user timings 1059 | * @param timingCategory (Required): A string for categorizing all user timing variables into logical groups(e.g jQuery). 1060 | * @param timingVar (Required): A string to identify the variable being recorded(e.g. JavaScript Load). 1061 | * @param timingValue (Required): The number of milliseconds in elapsed time to report to Google Analytics(e.g. 20). 1062 | * @param timingLabel (Optional): A string that can be used to add flexibility in visualizing user timings in the reports(e.g. Google CDN). 1063 | * @private 1064 | */ 1065 | this._trackTimings = function (timingCategory, timingVar, timingValue, timingLabel) { 1066 | _analyticsJs(function () { 1067 | _gaMultipleTrackers(undefined, 'send', 'timing', timingCategory, timingVar, timingValue, timingLabel); 1068 | }); 1069 | }; 1070 | 1071 | /** 1072 | * Exception tracking 1073 | * https://developers.google.com/analytics/devguides/collection/analyticsjs/exceptions 1074 | * @param description (Optional): A description of the exception. 1075 | * @param isFatal (Optional): true if the exception was fatal, false otherwise. 1076 | * @private 1077 | */ 1078 | this._trackException = function (description, isFatal) { 1079 | _analyticsJs(function () { 1080 | _gaMultipleTrackers(undefined, 'send', 'exception', { exDescription: description, exFatal: !!isFatal}); 1081 | }); 1082 | }; 1083 | 1084 | // creates the Google Analytics tracker 1085 | if (!delayScriptTag) { 1086 | this._registerScriptTags(); 1087 | this._registerTrackers(); 1088 | } 1089 | 1090 | // activates page tracking 1091 | if (trackRoutes) { 1092 | $rootScope.$on(pageEvent, function () { 1093 | // Apply $route based filtering if configured 1094 | if (readFromRoute) { 1095 | // Avoid tracking undefined routes, routes without template (e.g. redirect routes) 1096 | // and those explicitly marked as 'do not track' 1097 | if (!$route.current || !$route.current.templateUrl || $route.current.doNotTrack) { 1098 | return; 1099 | } 1100 | } 1101 | 1102 | that._trackPage(); 1103 | }); 1104 | } 1105 | 1106 | return { 1107 | log: that.log, 1108 | offlineQueue: that.offlineQueue, 1109 | configuration: { 1110 | accounts: accounts, 1111 | universalAnalytics: analyticsJS, 1112 | crossDomainLinker: crossDomainLinker, 1113 | crossLinkDomains: crossLinkDomains, 1114 | currency: currency, 1115 | debugMode: debugMode, 1116 | delayScriptTag: delayScriptTag, 1117 | disableAnalytics: disableAnalytics, 1118 | displayFeatures: displayFeatures, 1119 | domainName: domainName, 1120 | ecommerce: that._ecommerceEnabled(), 1121 | enhancedEcommerce: that._enhancedEcommerceEnabled(), 1122 | enhancedLinkAttribution: enhancedLinkAttribution, 1123 | experimentId: experimentId, 1124 | hybridMobileSupport: hybridMobileSupport, 1125 | ignoreFirstPageLoad: ignoreFirstPageLoad, 1126 | logAllCalls: logAllCalls, 1127 | pageEvent: pageEvent, 1128 | readFromRoute: readFromRoute, 1129 | removeRegExp: removeRegExp, 1130 | testMode: testMode, 1131 | traceDebuggingMode: traceDebuggingMode, 1132 | trackPrefix: trackPrefix, 1133 | trackRoutes: trackRoutes, 1134 | trackUrlParams: trackUrlParams 1135 | }, 1136 | getUrl: getUrl, 1137 | registerScriptTags: function () { 1138 | return that._registerScriptTags(); 1139 | }, 1140 | registerTrackers: function () { 1141 | return that._registerTrackers(); 1142 | }, 1143 | offline: function (mode) { 1144 | if (mode === true && offlineMode === false) { 1145 | // Go to offline mode 1146 | offlineMode = true; 1147 | } 1148 | if (mode === false && offlineMode === true) { 1149 | // Go to online mode and process the offline queue 1150 | offlineMode = false; 1151 | while (that.offlineQueue.length > 0) { 1152 | var obj = that.offlineQueue.shift(); 1153 | obj[0].apply(that, obj[1]); 1154 | } 1155 | } 1156 | return offlineMode; 1157 | }, 1158 | trackPage: function (url, title, custom) { 1159 | that._trackPage.apply(that, arguments); 1160 | }, 1161 | trackEvent: function (category, action, label, value, noninteraction, custom) { 1162 | that._trackEvent.apply(that, arguments); 1163 | }, 1164 | addTrans: function (transactionId, affiliation, total, tax, shipping, city, state, country, currency) { 1165 | that._addTrans.apply(that, arguments); 1166 | }, 1167 | addItem: function (transactionId, sku, name, category, price, quantity) { 1168 | that._addItem.apply(that, arguments); 1169 | }, 1170 | trackTrans: function () { 1171 | that._trackTrans.apply(that, arguments); 1172 | }, 1173 | clearTrans: function () { 1174 | that._clearTrans.apply(that, arguments); 1175 | }, 1176 | addProduct: function (productId, name, category, brand, variant, price, quantity, coupon, position, custom) { 1177 | that._addProduct.apply(that, arguments); 1178 | }, 1179 | addPromo: function (productId, name, creative, position) { 1180 | that._addPromo.apply(that, arguments); 1181 | }, 1182 | addImpression: function (productId, name, list, brand, category, variant, position, price) { 1183 | that._addImpression.apply(that, arguments); 1184 | }, 1185 | productClick: function (listName) { 1186 | that._productClick.apply(that, arguments); 1187 | }, 1188 | promoClick : function (promotionName) { 1189 | that._promoClick.apply(that, arguments); 1190 | }, 1191 | trackDetail: function () { 1192 | that._trackDetail.apply(that, arguments); 1193 | }, 1194 | trackCart: function (action, list) { 1195 | that._trackCart.apply(that, arguments); 1196 | }, 1197 | trackCheckout: function (step, option) { 1198 | that._trackCheckOut.apply(that, arguments); 1199 | }, 1200 | trackTimings: function (timingCategory, timingVar, timingValue, timingLabel) { 1201 | that._trackTimings.apply(that, arguments); 1202 | }, 1203 | trackTransaction: function (transactionId, affiliation, revenue, tax, shipping, coupon, list, step, option) { 1204 | that._trackTransaction.apply(that, arguments); 1205 | }, 1206 | trackException: function (description, isFatal) { 1207 | that._trackException.apply(that, arguments); 1208 | }, 1209 | setAction: function (action, obj) { 1210 | that._setAction.apply(that, arguments); 1211 | }, 1212 | pageView: function () { 1213 | that._pageView.apply(that, arguments); 1214 | }, 1215 | send: function (obj) { 1216 | that._send.apply(that, arguments); 1217 | }, 1218 | set: function (name, value, trackerName) { 1219 | that._set.apply(that, arguments); 1220 | } 1221 | }; 1222 | }]; 1223 | }) 1224 | 1225 | .directive('gaTrackEvent', ['Analytics', '$parse', function (Analytics, $parse) { 1226 | return { 1227 | restrict: 'A', 1228 | link: function (scope, element, attrs) { 1229 | var options = $parse(attrs.gaTrackEvent); 1230 | element.bind('click', function () { 1231 | if(attrs.gaTrackEventIf){ 1232 | if(!scope.$eval(attrs.gaTrackEventIf)){ 1233 | return; // Cancel this event if we don't pass the ga-track-event-if condition 1234 | } 1235 | } 1236 | if (options.length > 1) { 1237 | Analytics.trackEvent.apply(Analytics, options(scope)); 1238 | } 1239 | }); 1240 | } 1241 | }; 1242 | }]); 1243 | return angular.module('angular-google-analytics'); 1244 | })); 1245 | -------------------------------------------------------------------------------- /nuget.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | angular-google-analytics 5 | 1.1.8 6 | Angular Google Analytics module 7 | Julien Bouquillon,Justin Saunders (packaged Thomas Fuchs) 8 | https://github.com/revolunet/angular-google-analytics/blob/master/LICENSE 9 | https://github.com/revolunet/angular-google-analytics 10 | false 11 | This service lets you integrate google analytics tracker in your AngularJS applications easily. 12 | 13 | Copyright (c) 2015 "Julien Bouquillon <julien@bouquillon.com>" 14 | en-US 15 | angular google-analytics analytics 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-google-analytics", 3 | "description": "Angular Google Analytics - Easy tracking for your AngularJS application", 4 | "version": "1.1.8", 5 | "homepage": "http://github.com/revolunet/angular-google-analytics", 6 | "author": "Julien Bouquillon (https://github.com/revolunet)", 7 | "contributors": [ 8 | "Julien Bouquillon (https://github.com/revolunet)", 9 | "Justin Saunders (https://github.com/justinsa)", 10 | "Chris Esplin (https://github.com/deltaepsilon)", 11 | "Adam Misiorny (https://github.com/adam187)" 12 | ], 13 | "main": "index.js", 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/revolunet/angular-google-analytics.git" 17 | }, 18 | "engines": { 19 | "node": "*", 20 | "npm": "*" 21 | }, 22 | "license": "MIT", 23 | "dependencies": { 24 | "angular": ">=1" 25 | }, 26 | "devDependencies": { 27 | "angular-mocks": ">=1", 28 | "angular-scenario": ">=1", 29 | "grunt": "1.0.4", 30 | "grunt-cli": "1.3.2", 31 | "grunt-contrib-concat": "1.0.1", 32 | "grunt-contrib-jshint": "2.1.0", 33 | "grunt-contrib-uglify": "4.0.1", 34 | "jasmine-core": "3.4.0", 35 | "karma": "4.0.1", 36 | "karma-chrome-launcher": "2.2.0", 37 | "karma-jasmine": "2.0.1" 38 | }, 39 | "scripts": { 40 | "build": "grunt build", 41 | "lint": "grunt lint", 42 | "release": "grunt release", 43 | "stage": "grunt stage", 44 | "test": "karma start test/karma.conf.js --single-run --browsers Chrome", 45 | "test-server": "karma start test/karma.conf.js --browsers Chrome" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma v0.13 http://karma-runner.github.io/0.13/config/configuration-file.html 2 | // See configuration documentation above for CLI arguments 3 | module.exports = function(config) { 4 | config.set({ 5 | basePath: './..', 6 | frameworks: ['jasmine'], 7 | autoWatch: true, 8 | colors: true, 9 | files: [ 10 | 'node_modules/angular/angular.js', 11 | 'node_modules/angular-mocks/angular-mocks.js', 12 | 'index.js', 13 | 'test/unit/*.js' 14 | ], 15 | reporters: ['progress'], 16 | browsers: ['Chrome'], 17 | logLevel: config.LOG_INFO, 18 | captureTimeout: 5000, 19 | singleRun: false, 20 | port: 9876, 21 | runnerPort: 9100, 22 | reportSlowerThan: 500 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /test/unit/classic-google-analytics.js: -------------------------------------------------------------------------------- 1 | /* global afterEach, before, beforeEach, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('classic analytics', function() { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .useAnalytics(false) 10 | .logAllCalls(true) 11 | .enterTestMode(); 12 | })); 13 | 14 | afterEach(inject(function (Analytics) { 15 | Analytics.log.length = 0; // clear log 16 | })); 17 | 18 | describe('required settings missing', function () { 19 | describe('for default ga script injection', function () { 20 | beforeEach(module(function (AnalyticsProvider) { 21 | AnalyticsProvider.setAccount(undefined); 22 | })); 23 | 24 | it('should inject a script tag', function () { 25 | inject(function (Analytics) { 26 | expect(Analytics.log.length).toBe(2); 27 | expect(Analytics.log[0]).toEqual(['inject', 'http://www.google-analytics.com/ga.js']); 28 | expect(document.querySelectorAll('script[src="http://www.google-analytics.com/ga.js"]').length).toBe(0); 29 | }); 30 | }); 31 | 32 | it('should issue a warning to the log', function () { 33 | inject(function ($log) { 34 | spyOn($log, 'warn'); 35 | inject(function (Analytics) { 36 | expect(Analytics.log.length).toBe(2); 37 | expect(Analytics.log[0]).toEqual(['inject', 'http://www.google-analytics.com/ga.js']); 38 | expect(Analytics.log[1]).toEqual(['warn', 'No accounts to register']); 39 | expect($log.warn).toHaveBeenCalledWith(['No accounts to register']); 40 | }); 41 | }); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('enabled delayed script tag', function () { 47 | beforeEach(module(function (AnalyticsProvider) { 48 | AnalyticsProvider.delayScriptTag(true); 49 | })); 50 | 51 | it('should have a truthy value for delayScriptTag', function () { 52 | inject(function (Analytics, $location) { 53 | expect(Analytics.configuration.delayScriptTag).toBe(true); 54 | }); 55 | }); 56 | 57 | it('should not inject a script tag', function () { 58 | inject(function (Analytics) { 59 | expect(Analytics.log.length).toBe(0); 60 | expect(document.querySelectorAll('script[src="http://www.google-analytics.com/ga.js"]').length).toBe(0); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('does not support multiple tracking objects', function () { 66 | var trackers = [ 67 | { tracker: 'UA-12345-12', name: 'tracker1' }, 68 | { tracker: 'UA-12345-45' } 69 | ]; 70 | 71 | beforeEach(module(function (AnalyticsProvider) { 72 | AnalyticsProvider.setAccount(trackers); 73 | })); 74 | 75 | it('should issue a warning to the log', function () { 76 | inject(function ($log) { 77 | spyOn($log, 'warn'); 78 | inject(function (Analytics) { 79 | expect(Analytics.log.length).toBe(4); 80 | expect(Analytics.log[0]).toEqual(['inject', 'http://www.google-analytics.com/ga.js']); 81 | expect(Analytics.log[1]).toEqual(['warn', 'Multiple trackers are not supported with ga.js. Using first tracker only']); 82 | expect($log.warn).toHaveBeenCalledWith(['Multiple trackers are not supported with ga.js. Using first tracker only']); 83 | }); 84 | }); 85 | }); 86 | }); 87 | 88 | describe('manually create script tag', function () { 89 | beforeEach(module(function (AnalyticsProvider) { 90 | AnalyticsProvider.delayScriptTag(true); 91 | })); 92 | 93 | it('should inject a script tag', function () { 94 | inject(function (Analytics) { 95 | Analytics.registerScriptTags(); 96 | expect(Analytics.log[0]).toEqual(['inject', 'http://www.google-analytics.com/ga.js']); 97 | expect(document.querySelectorAll('script[src="http://www.google-analytics.com/ga.js"]').length).toBe(0); 98 | }); 99 | }); 100 | 101 | it('should warn and prevent a second attempt to inject a script tag', function () { 102 | inject(function ($log) { 103 | spyOn($log, 'warn'); 104 | inject(function (Analytics) { 105 | Analytics.registerScriptTags(); 106 | expect(Analytics.log[0]).toEqual(['inject', 'http://www.google-analytics.com/ga.js']); 107 | Analytics.registerScriptTags(); 108 | expect(Analytics.log[1]).toEqual(['warn', 'Script tags already created']); 109 | expect($log.warn).toHaveBeenCalledWith(['Script tags already created']); 110 | }); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('automatic page tracking', function () { 116 | it('should inject the GA script', function () { 117 | inject(function (Analytics) { 118 | expect(Analytics.log[0]).toEqual(['inject', 'http://www.google-analytics.com/ga.js']); 119 | expect(document.querySelectorAll('script[src="http://www.google-analytics.com/ga.js"]').length).toBe(0); 120 | }); 121 | }); 122 | 123 | it('should generate trackPages', function () { 124 | inject(function (Analytics, $window) { 125 | $window._gaq.length = 0; // clear queue 126 | Analytics.trackPage('test'); 127 | expect($window._gaq.length).toBe(2); 128 | expect($window._gaq[0]).toEqual(['_set', 'title', '']); 129 | expect($window._gaq[1]).toEqual(['_trackPageview', 'test']); 130 | }); 131 | }); 132 | 133 | it('should generate a trackPage on routeChangeSuccess', function () { 134 | inject(function (Analytics, $rootScope, $window) { 135 | $window._gaq.length = 0; // clear queue 136 | $rootScope.$broadcast('$routeChangeSuccess'); 137 | expect($window._gaq.length).toBe(2); 138 | expect($window._gaq[0]).toEqual(['_set', 'title', '']); 139 | expect($window._gaq[1]).toEqual(['_trackPageview', '']); 140 | }); 141 | }); 142 | }); 143 | 144 | describe('NOT automatic page tracking', function () { 145 | beforeEach(module(function (AnalyticsProvider) { 146 | AnalyticsProvider.trackPages(false); 147 | })); 148 | 149 | it('should NOT generate a trackpage on routeChangeSuccess', function () { 150 | inject(function (Analytics, $rootScope, $window) { 151 | $window._gaq.length = 0; // clear queue 152 | $rootScope.$broadcast('$routeChangeSuccess'); 153 | expect($window._gaq.length).toBe(0); 154 | }); 155 | }); 156 | 157 | it('should generate a trackpage when explicitly called', function () { 158 | inject(function (Analytics, $window) { 159 | $window._gaq.length = 0; // clear queue 160 | Analytics.trackPage('/page/here'); 161 | expect($window._gaq.length).toBe(2); 162 | expect($window._gaq[0]).toEqual(['_set', 'title', '']); 163 | expect($window._gaq[1]).toEqual(['_trackPageview', '/page/here']); 164 | }); 165 | }); 166 | }); 167 | 168 | describe('event tracking', function () { 169 | beforeEach(module(function (AnalyticsProvider) { 170 | AnalyticsProvider.trackPages(false); 171 | })); 172 | 173 | it('should generate eventTracks', function () { 174 | inject(function (Analytics, $window) { 175 | $window._gaq.length = 0; // clear queue 176 | Analytics.trackEvent('test'); 177 | expect($window._gaq.length).toBe(1); 178 | expect($window._gaq[0]).toEqual(['_trackEvent', 'test', undefined, undefined, undefined, false]); 179 | }); 180 | }); 181 | 182 | it('should generate eventTracks with non-interactions', function () { 183 | inject(function (Analytics, $window) { 184 | $window._gaq.length = 0; // clear queue 185 | Analytics.trackEvent('test', 'action', 'label', 0, true); 186 | expect($window._gaq.length).toBe(1); 187 | expect($window._gaq[0]).toEqual(['_trackEvent', 'test', 'action', 'label', 0, true]); 188 | }); 189 | }); 190 | }); 191 | 192 | describe('supports dc.js', function () { 193 | beforeEach(module(function (AnalyticsProvider) { 194 | AnalyticsProvider.useDisplayFeatures(true); 195 | })); 196 | 197 | it('should inject the DC script and not the analytics script', function () { 198 | inject(function (Analytics) { 199 | expect(Analytics.log[0]).toEqual(['inject', '//stats.g.doubleclick.net/dc.js']); 200 | expect(document.querySelectorAll('script[src="//www.google-analytics.com/ga.js"]').length).toBe(0); 201 | expect(document.querySelectorAll('script[src="//stats.g.doubleclick.net/dc.js"]').length).toBe(0); 202 | }); 203 | }); 204 | }); 205 | 206 | describe('e-commerce transactions', function () { 207 | it('should add transcation', function () { 208 | inject(function (Analytics, $window) { 209 | $window._gaq.length = 0; // clear queue 210 | Analytics.addTrans('1', '', '2.42', '0.42', '0', 'Amsterdam', '', 'Netherlands'); 211 | expect($window._gaq.length).toBe(1); 212 | expect($window._gaq[0]).toEqual(['_addTrans', '1', '', '2.42', '0.42', '0', 'Amsterdam', '', 'Netherlands']); 213 | }); 214 | }); 215 | 216 | it('should add an item to transaction', function () { 217 | inject(function (Analytics, $window) { 218 | $window._gaq.length = 0; // clear queue 219 | Analytics.addItem('1', 'sku-1', 'Test product 1', 'Testing', '1', '1'); 220 | expect($window._gaq.length).toBe(1); 221 | expect($window._gaq[0]).toEqual(['_addItem', '1', 'sku-1', 'Test product 1', 'Testing', '1', '1']); 222 | Analytics.addItem('1', 'sku-2', 'Test product 2', 'Testing', '1', '1'); 223 | expect($window._gaq.length).toBe(2); 224 | expect($window._gaq[1]).toEqual(['_addItem', '1', 'sku-2', 'Test product 2', 'Testing', '1', '1']); 225 | }); 226 | }); 227 | 228 | it('should track the transaction', function () { 229 | inject(function (Analytics, $window) { 230 | $window._gaq.length = 0; // clear queue 231 | Analytics.trackTrans(); 232 | expect($window._gaq.length).toBe(1); 233 | expect($window._gaq[0]).toEqual(['_trackTrans']); 234 | }); 235 | }); 236 | }); 237 | 238 | describe('supports ignoreFirstPageLoad', function () { 239 | beforeEach(module(function (AnalyticsProvider) { 240 | AnalyticsProvider.ignoreFirstPageLoad(true); 241 | })); 242 | 243 | it('supports ignoreFirstPageLoad config', function () { 244 | inject(function (Analytics, $rootScope) { 245 | expect(Analytics.configuration.ignoreFirstPageLoad).toBe(true); 246 | }); 247 | }); 248 | }); 249 | 250 | describe('supports arbitrary page events', function () { 251 | beforeEach(module(function (AnalyticsProvider) { 252 | AnalyticsProvider.setPageEvent('$stateChangeSuccess'); 253 | })); 254 | 255 | it('should inject the Analytics script', function () { 256 | inject(function (Analytics, $rootScope, $window) { 257 | $window._gaq.length = 0; // clear queue 258 | $rootScope.$broadcast('$stateChangeSuccess'); 259 | expect($window._gaq.length).toBe(2); 260 | expect($window._gaq[0]).toEqual(['_set', 'title', '']); 261 | expect($window._gaq[1]).toEqual(['_trackPageview', '']); 262 | }); 263 | }); 264 | }); 265 | 266 | describe('supports RegExp path scrubbing', function () { 267 | beforeEach(module(function (AnalyticsProvider) { 268 | AnalyticsProvider.setRemoveRegExp(new RegExp(/\/\d+?$/)); 269 | })); 270 | 271 | it('should scrub urls', function () { 272 | inject(function (Analytics, $location) { 273 | $location.path('/some-crazy/page/with/numbers/123456'); 274 | expect(Analytics.getUrl()).toBe('/some-crazy/page/with/numbers'); 275 | }); 276 | }); 277 | }); 278 | 279 | describe('parameter defaulting on trackPage', function () { 280 | beforeEach(module(function (AnalyticsProvider) { 281 | AnalyticsProvider.trackPages(false); 282 | })); 283 | 284 | it('should set url and title when no parameters provided', function () { 285 | inject(function (Analytics, $document, $location, $window) { 286 | $window._gaq.length = 0; // clear queue 287 | $location.path('/page/here'); 288 | $document[0] = { title: 'title here' }; 289 | Analytics.trackPage(); 290 | expect($window._gaq.length).toBe(2); 291 | expect($window._gaq[0]).toEqual(['_set', 'title', 'title here']); 292 | expect($window._gaq[1]).toEqual(['_trackPageview', '/page/here']); 293 | }); 294 | }); 295 | 296 | it('should set title when no title provided', function () { 297 | inject(function (Analytics, $document, $window) { 298 | $window._gaq.length = 0; // clear queue 299 | $document[0] = { title: 'title here' }; 300 | Analytics.trackPage('/page/here'); 301 | expect($window._gaq.length).toBe(2); 302 | expect($window._gaq[0]).toEqual(['_set', 'title', 'title here']); 303 | expect($window._gaq[1]).toEqual(['_trackPageview', '/page/here']); 304 | }); 305 | }); 306 | }); 307 | 308 | describe('enabled url params tracking', function () { 309 | beforeEach(module(function (AnalyticsProvider) { 310 | AnalyticsProvider.trackUrlParams(true); 311 | })); 312 | 313 | it('should grab query params in the url', function () { 314 | inject(function (Analytics, $location) { 315 | $location.url('/some/page?with_params=foo&more_param=123'); 316 | expect(Analytics.getUrl()).toContain('?with_params=foo&more_param=123'); 317 | }); 318 | }); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /test/unit/debug-mode.js: -------------------------------------------------------------------------------- 1 | /* global afterEach, before, beforeEach, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('universal analytics debug mode', function () { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .useAnalytics(true) 10 | .logAllCalls(true) 11 | .enterTestMode(); 12 | })); 13 | 14 | afterEach(inject(function (Analytics) { 15 | Analytics.log.length = 0; // clear log 16 | })); 17 | 18 | it('should have debug mode disabled by default', function () { 19 | inject(function (Analytics) { 20 | expect(Analytics.configuration.debugMode).toBe(false); 21 | }); 22 | }); 23 | 24 | it('should have trace debugging mode disabled by default', function () { 25 | inject(function (Analytics) { 26 | expect(Analytics.configuration.traceDebuggingMode).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('without trace debugging mode', function () { 31 | beforeEach(module(function (AnalyticsProvider) { 32 | AnalyticsProvider.enterDebugMode(); 33 | })); 34 | 35 | it('should have debug mode enabled', function () { 36 | inject(function (Analytics) { 37 | expect(Analytics.configuration.debugMode).toBe(true); 38 | }); 39 | }); 40 | 41 | it('should have trace debugging disabled', function () { 42 | inject(function (Analytics) { 43 | expect(Analytics.configuration.traceDebuggingMode).toBe(false); 44 | }); 45 | }); 46 | 47 | it('should inject the analytics debug script', function () { 48 | inject(function (Analytics) { 49 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics_debug.js']); 50 | }); 51 | }); 52 | 53 | it('should not set the analytics trace debugging variable', function () { 54 | delete window.ga_debug; 55 | inject(function (Analytics, $window) { 56 | expect($window.ga_debug).toEqual(undefined); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('with trace debugging mode', function () { 62 | beforeEach(module(function (AnalyticsProvider) { 63 | AnalyticsProvider.enterDebugMode(true); 64 | })); 65 | 66 | it('should have debug mode enabled', function () { 67 | inject(function (Analytics) { 68 | expect(Analytics.configuration.debugMode).toBe(true); 69 | }); 70 | }); 71 | 72 | it('should have trace debugging enabled', function () { 73 | inject(function (Analytics) { 74 | expect(Analytics.configuration.traceDebuggingMode).toBe(true); 75 | }); 76 | }); 77 | 78 | it('should inject the analytics debug script', function () { 79 | inject(function (Analytics) { 80 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics_debug.js']); 81 | }); 82 | }); 83 | 84 | it('should set the analytics trace debugging variable', function () { 85 | delete window.ga_debug; 86 | inject(function (Analytics, $window) { 87 | expect($window.ga_debug).toEqual({ trace: true }); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/unit/directives.js: -------------------------------------------------------------------------------- 1 | /* global before, beforeEach, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('directives', function() { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .logAllCalls(true) 10 | .enterTestMode(); 11 | })); 12 | 13 | describe('directives', function () { 14 | describe('gaTrackEvent', function () { 15 | it('should evaluate scope params', function () { 16 | inject(function (Analytics, $rootScope, $compile) { 17 | spyOn(Analytics, 'trackEvent'); 18 | var scope = $rootScope.$new(), 19 | element = '
test
', 20 | compiled = $compile(element)(scope); 21 | 22 | scope.event = 'button'; 23 | scope.action = 'click'; 24 | scope.label = 'Some Button'; 25 | 26 | scope.$digest(); 27 | compiled.triggerHandler('click'); 28 | expect(Analytics.trackEvent).toHaveBeenCalledWith('button', 'click', 'Some Button'); 29 | }); 30 | }); 31 | 32 | it('should track an event when clicked', function () { 33 | inject(function (Analytics, $rootScope, $compile) { 34 | spyOn(Analytics, 'trackEvent'); 35 | var scope = $rootScope.$new(), 36 | element = '
test
', 37 | compiled = $compile(element)(scope); 38 | scope.$digest(); 39 | compiled.triggerHandler('click'); 40 | expect(Analytics.trackEvent).toHaveBeenCalledWith('button', 'click', 'Some Button'); 41 | }); 42 | }); 43 | 44 | it('should inherit parent scope', function () { 45 | inject(function (Analytics, $rootScope, $compile) { 46 | spyOn(Analytics, 'trackEvent'); 47 | var scope = $rootScope.$new(), element, compiled; 48 | scope.event = ['button', 'click', 'Some Button']; 49 | element = '
test
'; 50 | compiled = $compile(element)(scope); 51 | scope.$digest(); 52 | compiled.triggerHandler('click'); 53 | expect(Analytics.trackEvent).toHaveBeenCalledWith('button', 'click', 'Some Button'); 54 | }); 55 | }); 56 | 57 | it('should abort if gaTrackEventIf is false', function () { 58 | inject(function (Analytics, $rootScope, $compile) { 59 | spyOn(Analytics, 'trackEvent'); 60 | var scope = $rootScope.$new(), 61 | element = '
test
', 62 | compiled = $compile(element)(scope); 63 | scope.$digest(); 64 | compiled.triggerHandler('click'); 65 | expect(Analytics.trackEvent.calls.count()).toBe(0); 66 | 67 | element = '
test
'; 68 | compiled = $compile(element)(scope); 69 | scope.$digest(); 70 | compiled.triggerHandler('click'); 71 | expect(Analytics.trackEvent.calls.count()).toBe(1); 72 | }); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/unit/disable-analytics.js: -------------------------------------------------------------------------------- 1 | /* global afterEach, before, beforeEach, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('disable analytics / user opt-out', function () { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .logAllCalls(true) 10 | .enterTestMode(); 11 | })); 12 | 13 | afterEach(inject(function (Analytics) { 14 | Analytics.log.length = 0; // clear log 15 | })); 16 | 17 | describe('with universal analytics', function () { 18 | beforeEach(module(function (AnalyticsProvider) { 19 | AnalyticsProvider.useAnalytics(true); 20 | })); 21 | 22 | it('should be enabled by default', function () { 23 | inject(function (Analytics) { 24 | expect(Analytics.configuration.disableAnalytics).toBe(false); 25 | }); 26 | }); 27 | 28 | describe('when disabled', function () { 29 | beforeEach(module(function (AnalyticsProvider) { 30 | AnalyticsProvider.disableAnalytics(true); 31 | })); 32 | 33 | it('should be disabled', function () { 34 | inject(function (Analytics) { 35 | expect(Analytics.configuration.disableAnalytics).toBe(true); 36 | }); 37 | }); 38 | 39 | it('should log an info message about the account being disabled', function () { 40 | inject(function (Analytics, $window) { 41 | expect(Analytics.log[0]).toEqual([ 'info', 'Analytics disabled: UA-XXXXXX-xx' ]); 42 | expect($window['ga-disable-UA-XXXXXX-xx']).toBe(true); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('with classic analytics', function () { 49 | beforeEach(module(function (AnalyticsProvider) { 50 | AnalyticsProvider.useAnalytics(false); 51 | })); 52 | 53 | it('should be enabled by default', function () { 54 | inject(function (Analytics) { 55 | expect(Analytics.configuration.disableAnalytics).toBe(false); 56 | }); 57 | }); 58 | 59 | describe('when disabled', function () { 60 | beforeEach(module(function (AnalyticsProvider) { 61 | AnalyticsProvider.disableAnalytics(true); 62 | })); 63 | 64 | it('should be disabled', function () { 65 | inject(function (Analytics) { 66 | expect(Analytics.configuration.disableAnalytics).toBe(true); 67 | }); 68 | }); 69 | 70 | it('should log an info message about the account being disabled', function () { 71 | inject(function (Analytics, $window) { 72 | expect(Analytics.log[0]).toEqual([ 'info', 'Analytics disabled: UA-XXXXXX-xx' ]); 73 | expect($window['ga-disable-UA-XXXXXX-xx']).toBe(true); 74 | }); 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/unit/offline-mode.js: -------------------------------------------------------------------------------- 1 | /* global afterEach, before, beforeEach, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('offline mode', function () { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .logAllCalls(true) 10 | .enterTestMode(); 11 | })); 12 | 13 | afterEach(inject(function (Analytics) { 14 | Analytics.log.length = 0; // clear log 15 | })); 16 | 17 | describe('with universal analytics', function () { 18 | beforeEach(module(function (AnalyticsProvider) { 19 | AnalyticsProvider.useAnalytics(true); 20 | })); 21 | 22 | describe('at startup', function () { 23 | beforeEach(module(function (AnalyticsProvider) { 24 | AnalyticsProvider.startOffline(true); 25 | })); 26 | 27 | it('should have offline set to true', function () { 28 | inject(function (Analytics) { 29 | expect(Analytics.offline()).toBe(true); 30 | }); 31 | }); 32 | 33 | it('should have delay script tag set to true', function () { 34 | inject(function (Analytics) { 35 | expect(Analytics.configuration.delayScriptTag).toBe(true); 36 | }); 37 | }); 38 | 39 | it('should not have sent any commands while offline', function () { 40 | inject(function (Analytics) { 41 | Analytics.trackPage('/page/here'); 42 | expect(Analytics.log.length).toBe(0); 43 | }); 44 | }); 45 | 46 | it('should send everything when script is added and reset to online', function () { 47 | inject(function (Analytics, $window) { 48 | Analytics.registerScriptTags(); 49 | Analytics.registerTrackers(); 50 | Analytics.offline(false); 51 | expect(Analytics.log.length).toBe(3); 52 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics.js']); 53 | expect(Analytics.log[1]).toEqual(['create', 'UA-XXXXXX-xx', {}]); 54 | expect(Analytics.log[2]).toEqual(['send', 'pageview', '']); 55 | }); 56 | }); 57 | }); 58 | 59 | it('should be online by default', function () { 60 | inject(function (Analytics) { 61 | expect(Analytics.offline()).toBe(false); 62 | }); 63 | }); 64 | 65 | it('should respect being set to offline', function () { 66 | inject(function (Analytics) { 67 | expect(Analytics.offline()).toBe(false); 68 | Analytics.offline(true); 69 | expect(Analytics.offline()).toBe(true); 70 | }); 71 | }); 72 | 73 | it('should respect being reset to online', function () { 74 | inject(function (Analytics) { 75 | expect(Analytics.offline()).toBe(false); 76 | Analytics.offline(true); 77 | expect(Analytics.offline()).toBe(true); 78 | Analytics.offline(false); 79 | expect(Analytics.offline()).toBe(false); 80 | }); 81 | }); 82 | 83 | it('should not send any commands while offline', function () { 84 | inject(function (Analytics) { 85 | Analytics.log.length = 0; // clear log 86 | Analytics.offline(true); 87 | Analytics.trackPage('/page/here'); 88 | expect(Analytics.log.length).toBe(0); 89 | }); 90 | }); 91 | 92 | it('should send all queued commands when reset to online', function () { 93 | inject(function (Analytics) { 94 | Analytics.log.length = 0; // clear log 95 | Analytics.offline(true); 96 | Analytics.trackPage('/page/here'); 97 | expect(Analytics.log.length).toBe(0); 98 | Analytics.offline(false); 99 | expect(Analytics.log.length).toBe(1); 100 | expect(Analytics.log[0]).toEqual(['send', 'pageview', { page : '/page/here', title : '' }]); 101 | }); 102 | }); 103 | }); 104 | 105 | describe('with classic analytics', function () { 106 | beforeEach(module(function (AnalyticsProvider) { 107 | AnalyticsProvider.useAnalytics(false); 108 | })); 109 | 110 | describe('at startup', function () { 111 | beforeEach(module(function (AnalyticsProvider) { 112 | AnalyticsProvider.startOffline(true); 113 | })); 114 | 115 | it('should have offline set to true', function () { 116 | inject(function (Analytics) { 117 | expect(Analytics.offline()).toBe(true); 118 | }); 119 | }); 120 | 121 | it('should have delay script tag set to true', function () { 122 | inject(function (Analytics) { 123 | expect(Analytics.configuration.delayScriptTag).toBe(true); 124 | }); 125 | }); 126 | 127 | it('should not have sent any commands while offline', function () { 128 | inject(function (Analytics, $window) { 129 | $window._gaq.length = 0; // clear queue 130 | Analytics.trackPage('/page/here'); 131 | expect($window._gaq.length).toBe(0); 132 | }); 133 | }); 134 | 135 | it('should send everything when script is added and reset to online', function () { 136 | inject(function (Analytics, $window) { 137 | $window._gaq.length = 0; // clear queue 138 | Analytics.registerScriptTags(); 139 | Analytics.registerTrackers(); 140 | Analytics.offline(false); 141 | expect(Analytics.log.length).toBe(3); 142 | expect(Analytics.log[0]).toEqual(['inject', 'http://www.google-analytics.com/ga.js']); 143 | expect(Analytics.log[1]).toEqual(['_setAccount', 'UA-XXXXXX-xx']); 144 | expect(Analytics.log[2]).toEqual(['_trackPageview']); 145 | expect($window._gaq.length).toBe(Analytics.log.length - 1); 146 | expect($window._gaq[0]).toEqual(Analytics.log[1]); 147 | expect($window._gaq[1]).toEqual(Analytics.log[2]); 148 | }); 149 | }); 150 | }); 151 | 152 | it('should be online by default', function () { 153 | inject(function (Analytics) { 154 | expect(Analytics.offline()).toBe(false); 155 | }); 156 | }); 157 | 158 | it('should respect being set to offline', function () { 159 | inject(function (Analytics) { 160 | expect(Analytics.offline()).toBe(false); 161 | Analytics.offline(true); 162 | expect(Analytics.offline()).toBe(true); 163 | }); 164 | }); 165 | 166 | it('should respect being reset to online', function () { 167 | inject(function (Analytics) { 168 | expect(Analytics.offline()).toBe(false); 169 | Analytics.offline(true); 170 | expect(Analytics.offline()).toBe(true); 171 | Analytics.offline(false); 172 | expect(Analytics.offline()).toBe(false); 173 | }); 174 | }); 175 | 176 | it('should not send any commands while offline', function () { 177 | inject(function (Analytics, $window) { 178 | $window._gaq.length = 0; // clear queue 179 | Analytics.offline(true); 180 | Analytics.trackPage('/page/here'); 181 | expect($window._gaq.length).toBe(0); 182 | }); 183 | }); 184 | 185 | it('should send all queued commands when reset to online', function () { 186 | inject(function (Analytics, $window) { 187 | Analytics.log.length = 0; // clear log 188 | $window._gaq.length = 0; // clear queue 189 | Analytics.offline(true); 190 | Analytics.trackPage('/page/here'); 191 | expect(Analytics.log.length).toBe(0); 192 | expect($window._gaq.length).toBe(0); 193 | Analytics.offline(false); 194 | expect(Analytics.log.length).toBe(2); 195 | expect(Analytics.log[0]).toEqual(['_set', 'title', '']); 196 | expect(Analytics.log[1]).toEqual(['_trackPageview', '/page/here']); 197 | expect($window._gaq.length).toBe(Analytics.log.length); 198 | expect($window._gaq[0]).toEqual(Analytics.log[0]); 199 | expect($window._gaq[1]).toEqual(Analytics.log[1]); 200 | }); 201 | }); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /test/unit/route-reading.js: -------------------------------------------------------------------------------- 1 | /* global afterEach, before, beforeEach, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('Reading from $route service', function() { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .logAllCalls(true) 10 | .enterTestMode(); 11 | })); 12 | 13 | afterEach(inject(function (Analytics) { 14 | Analytics.log.length = 0; // clear log 15 | })); 16 | 17 | it('should not activate $route reading', function() { 18 | inject(function(Analytics) { 19 | expect(Analytics.configuration.readFromRoute).toBe(false); 20 | }); 21 | }); 22 | 23 | describe('without $route service available', function() { 24 | beforeEach(module(function(AnalyticsProvider){ 25 | AnalyticsProvider.readFromRoute(true); 26 | })); 27 | 28 | it('should log warning if service is missing', function() { 29 | inject(function($log) { 30 | spyOn($log, 'warn'); 31 | inject(function(Analytics) { 32 | expect($log.warn).toHaveBeenCalledWith('$route service is not available. Make sure you have included ng-route in your application dependencies.'); 33 | }); 34 | }); 35 | }); 36 | }); 37 | 38 | describe('after setting readFromRoute', function() { 39 | beforeEach(module(function(AnalyticsProvider, $provide){ 40 | AnalyticsProvider.readFromRoute(true); 41 | $provide.service('$route', function () { 42 | this.routes = { 43 | someroute: { pageTrack: '/some' }, 44 | otherroute: { } 45 | }; 46 | }); 47 | })); 48 | 49 | it('should activate $route reading', function(){ 50 | inject(function(Analytics){ 51 | expect(Analytics.configuration.readFromRoute).toBe(true); 52 | }); 53 | }); 54 | 55 | it('should read \'/someroute\' from routes object', function(){ 56 | inject(function(Analytics, $route){ 57 | $route.current = $route.routes.someroute; 58 | expect(Analytics.getUrl()).toBe('/some'); 59 | }); 60 | }); 61 | 62 | it('should fallback to url for \'/otherroute\' without \'pageTrack\' property', function(){ 63 | inject(function(Analytics, $route, $location){ 64 | $route.current = $route.routes.otherroute; 65 | $location.url('/otherroute'); 66 | expect(Analytics.getUrl()).toBe('/otherroute'); 67 | }); 68 | }); 69 | 70 | it('should fallback to url for \'/undefinedroute\' which is not present in $route config', function(){ 71 | inject(function(Analytics, $location) { 72 | $location.url('/undefinedroute'); 73 | expect(Analytics.getUrl()).toBe('/undefinedroute'); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('after setting readFromRoute for classic analytics', function() { 79 | beforeEach(module(function(AnalyticsProvider, $provide){ 80 | AnalyticsProvider.readFromRoute(true) 81 | .useAnalytics(false); 82 | $provide.service('$route', function () { }); 83 | })); 84 | 85 | it('should not track undefined routes', function() { 86 | inject(function(Analytics, $window, $rootScope) { 87 | $window._gaq.length = 0; // clear queue 88 | $rootScope.$broadcast('$routeChangeSuccess'); 89 | expect($window._gaq.length).toBe(0); 90 | }); 91 | }); 92 | 93 | it('should not track routes without template', function() { 94 | inject(function(Analytics, $window, $rootScope, $route) { 95 | $route.current = { }; 96 | $window._gaq.length = 0; // clear queue 97 | $rootScope.$broadcast('$routeChangeSuccess'); 98 | expect($window._gaq.length).toBe(0); 99 | }); 100 | }); 101 | 102 | it('should not track routes with \'doNotTrack\' attribute', function() { 103 | inject(function(Analytics, $window, $rootScope, $route) { 104 | $route.current = { templateUrl: '/myTemplate', doNotTrack: true }; 105 | $window._gaq.length = 0; // clear queue 106 | $rootScope.$broadcast('$routeChangeSuccess'); 107 | expect($window._gaq.length).toBe(0); 108 | }); 109 | }); 110 | 111 | it('should track routes with a defined template (no redirect)', function() { 112 | inject(function(Analytics, $window, $rootScope, $route) { 113 | $route.current = { templateUrl: '/myTemplate', pageTrack: '/myTrack' }; 114 | $window._gaq.length = 0; // clear queue 115 | $rootScope.$broadcast('$routeChangeSuccess'); 116 | expect($window._gaq.length).toBe(2); 117 | expect($window._gaq[0]).toEqual(['_set', 'title', '']); 118 | expect($window._gaq[1]).toEqual(['_trackPageview', '/myTrack']); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('after setting readFromRoute for universal analytics', function() { 124 | beforeEach(module(function(AnalyticsProvider, $provide){ 125 | AnalyticsProvider.readFromRoute(true) 126 | .useAnalytics(true); 127 | $provide.service('$route', function () { }); 128 | })); 129 | 130 | it('should not track undefined routes', function() { 131 | inject(function(Analytics, $rootScope) { 132 | Analytics.log.length = 0; // clear queue 133 | $rootScope.$broadcast('$routeChangeSuccess'); 134 | expect(Analytics.log.length).toBe(0); 135 | }); 136 | }); 137 | 138 | it('should not track routes without template', function() { 139 | inject(function(Analytics, $rootScope, $route) { 140 | $route.current = { }; 141 | Analytics.log.length = 0; // clear queue 142 | $rootScope.$broadcast('$routeChangeSuccess'); 143 | expect(Analytics.log.length).toBe(0); 144 | }); 145 | }); 146 | 147 | it('should not track routes with \'doNotTrack\' attribute', function() { 148 | inject(function(Analytics, $rootScope, $route) { 149 | $route.current = { templateUrl: '/myTemplate', doNotTrack: true }; 150 | Analytics.log.length = 0; // clear queue 151 | $rootScope.$broadcast('$routeChangeSuccess'); 152 | expect(Analytics.log.length).toBe(0); 153 | }); 154 | }); 155 | 156 | it('should track routes with a defined template (no redirect)', function() { 157 | inject(function(Analytics, $window, $rootScope, $route) { 158 | $route.current = { templateUrl: '/myTemplate', pageTrack: '/myTrack' }; 159 | Analytics.log.length = 0; // clear queue 160 | $rootScope.$broadcast('$routeChangeSuccess'); 161 | expect(Analytics.log.length).toBe(1); 162 | expect(Analytics.log[0]).toEqual(['send', 'pageview', { page: '/myTrack', title: '' }]); 163 | }); 164 | }); 165 | }); 166 | }); -------------------------------------------------------------------------------- /test/unit/scenarios.js: -------------------------------------------------------------------------------- 1 | /* global afterEach, before, beforeEach, console, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('universal analytics scenarios', function () { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .useECommerce(true, true) 10 | .setCurrency('EUR') 11 | .logAllCalls(true) 12 | .enterTestMode(); 13 | })); 14 | 15 | afterEach(inject(function (Analytics) { 16 | Analytics.log.length = 0; // clear log 17 | })); 18 | 19 | it('should handle e-commerce scenario #1', function () { 20 | inject(function ($window) { 21 | spyOn($window, 'ga'); 22 | inject(function (Analytics) { 23 | var i, count, expected = [ 24 | ['inject', '//www.google-analytics.com/analytics.js'], // This entry is in the log only due to test mode 25 | ['create', 'UA-XXXXXX-xx', {}], 26 | ['require', 'ec'], 27 | ['set', '&cu', 'EUR'], 28 | ['send', 'pageview', ''], 29 | ['send', 'pageview', {page: '/foobar', title: ''}], 30 | ['ec:addProduct', {id: 'sku-2', name: 'Test Product 2', category: 'Category-1', brand: 'Brand 2', variant: 'variant-3', price: '2499', quantity: '1', coupon: 'FLAT10', position: '1'}], 31 | ['ec:setAction', 'checkout', {step: 1, option: 'Visa'}], 32 | ['ec:addProduct', {id: 'sku-2', name: 'Test Product 2', category: 'Category-1', brand: 'Brand 2', variant: 'variant-3', price: '1111', quantity: '1', coupon: 'WOMEN10', position: '1'}], 33 | ['ec:setAction', 'purchase', {id: 'T1234', affiliation: 'Online Store - Web', revenue: '3333', tax: '10', shipping: '200', coupon: 'FLAT10'}] 34 | ]; 35 | Analytics.trackPage('/foobar'); 36 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 37 | Analytics.trackCheckout(1, 'Visa'); 38 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '1111', '1', 'WOMEN10', '1'); 39 | Analytics.trackTransaction('T1234', 'Online Store - Web', '3333', '10', '200', 'FLAT10', '', '', ''); 40 | count = $window.ga.calls.count(); 41 | expect(count).toBe(expected.length - 1); 42 | expect(Analytics.log.length).toBe(expected.length); 43 | expect(Analytics.log[0]).toEqual(expected[0]); 44 | for (i = 0; i < count; ++i) { 45 | expect(Analytics.log[i]).toEqual(expected[i]); 46 | expect(Analytics.log[i + 1]).toEqual($window.ga.calls.argsFor(i)); 47 | } 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/track-exception.js: -------------------------------------------------------------------------------- 1 | /* global afterEach, before, beforeEach, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('universal analytics exception tracking', function () { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .useAnalytics(true) 10 | .logAllCalls(true) 11 | .enterTestMode(); 12 | })); 13 | 14 | afterEach(inject(function (Analytics) { 15 | Analytics.log.length = 0; // clear log 16 | })); 17 | 18 | it('should have a trackException method', function () { 19 | inject(function (Analytics) { 20 | expect(typeof Analytics.trackException === 'function').toBe(true); 21 | }); 22 | }); 23 | 24 | it('should allow for tracking an exception with no parameters provided', function () { 25 | inject(function (Analytics) { 26 | Analytics.log.length = 0; // clear log 27 | Analytics.trackException(); 28 | expect(Analytics.log[0]).toEqual(['send', 'exception', { exDescription: undefined, exFatal: false }]); 29 | }); 30 | }); 31 | 32 | it('should allow for tracking an exception with all parameters provided', function () { 33 | inject(function (Analytics) { 34 | Analytics.log.length = 0; // clear log 35 | Analytics.trackException('Something fatal happened!', true); 36 | expect(Analytics.log[0]).toEqual(['send', 'exception', { exDescription: 'Something fatal happened!', exFatal: true }]); 37 | }); 38 | }); 39 | 40 | describe('supports tracking for multiple tracking objects', function () { 41 | var trackers = [ 42 | { tracker: 'UA-12345-12', name: 'tracker1', trackEvent: true }, 43 | { tracker: 'UA-12345-34', name: 'tracker2' }, 44 | { tracker: 'UA-12345-45', trackEvent: true } 45 | ]; 46 | 47 | beforeEach(module(function (AnalyticsProvider) { 48 | AnalyticsProvider.setAccount(trackers); 49 | })); 50 | 51 | it('should track exceptions for all objects', function () { 52 | inject(function ($window) { 53 | spyOn($window, 'ga'); 54 | inject(function (Analytics) { 55 | Analytics.trackException('Something fatal happened!', true); 56 | expect($window.ga).toHaveBeenCalledWith('tracker1.send', 'exception', { exDescription: 'Something fatal happened!', exFatal: true }); 57 | expect($window.ga).toHaveBeenCalledWith('tracker2.send', 'exception', { exDescription: 'Something fatal happened!', exFatal: true }); 58 | expect($window.ga).toHaveBeenCalledWith('send', 'exception', { exDescription: 'Something fatal happened!', exFatal: true }); 59 | }); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/unit/universal-google-analytics.js: -------------------------------------------------------------------------------- 1 | /* global afterEach, before, beforeEach, describe, document, expect, inject, it, module, spyOn */ 2 | 'use strict'; 3 | 4 | describe('universal analytics', function () { 5 | beforeEach(module('angular-google-analytics')); 6 | beforeEach(module(function (AnalyticsProvider) { 7 | AnalyticsProvider 8 | .setAccount('UA-XXXXXX-xx') 9 | .useAnalytics(true) 10 | .logAllCalls(true) 11 | .enterTestMode(); 12 | })); 13 | 14 | afterEach(inject(function (Analytics) { 15 | Analytics.log.length = 0; // clear log 16 | })); 17 | 18 | describe('required settings missing', function () { 19 | describe('for analytics script injection', function () { 20 | beforeEach(module(function (AnalyticsProvider) { 21 | AnalyticsProvider.setAccount(undefined); 22 | })); 23 | 24 | it('should inject a script tag', function () { 25 | inject(function (Analytics) { 26 | expect(Analytics.log.length).toBe(2); 27 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics.js']); 28 | expect(document.querySelectorAll('script[src="//www.google-analytics.com/analytics.js"]').length).toBe(0); 29 | }); 30 | }); 31 | 32 | it('should issue a warning to the log', function () { 33 | inject(function ($log) { 34 | spyOn($log, 'warn'); 35 | inject(function (Analytics) { 36 | expect(Analytics.log.length).toBe(2); 37 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics.js']); 38 | expect(Analytics.log[1]).toEqual(['warn', 'No accounts to register']); 39 | expect($log.warn).toHaveBeenCalledWith(['No accounts to register']); 40 | }); 41 | }); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('delay script tag', function () { 47 | beforeEach(module(function (AnalyticsProvider) { 48 | AnalyticsProvider.delayScriptTag(true); 49 | })); 50 | 51 | it('should have a truthy value for Analytics.delayScriptTag', function () { 52 | inject(function (Analytics, $location) { 53 | expect(Analytics.configuration.delayScriptTag).toBe(true); 54 | }); 55 | }); 56 | 57 | it('should not inject a script tag', function () { 58 | inject(function (Analytics) { 59 | expect(Analytics.log.length).toBe(0); 60 | expect(document.querySelectorAll('script[src="//www.google-analytics.com/analytics.js"]').length).toBe(0); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('automatically create analytics script tag', function () { 66 | it('should inject the script tag', function () { 67 | inject(function (Analytics) { 68 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics.js']); 69 | expect(document.querySelectorAll('script[src="//www.google-analytics.com/analytics.js"]').length).toBe(0); 70 | }); 71 | }); 72 | 73 | it('should warn and prevent a second attempt to inject a script tag', function () { 74 | inject(function ($log) { 75 | spyOn($log, 'warn'); 76 | inject(function (Analytics) { 77 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics.js']); 78 | Analytics.registerScriptTags(); 79 | expect($log.warn).toHaveBeenCalledWith(['Script tags already created']); 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | describe('manually create analytics script tag', function () { 86 | beforeEach(module(function (AnalyticsProvider) { 87 | AnalyticsProvider.delayScriptTag(true); 88 | })); 89 | 90 | it('should inject the script tag', function () { 91 | inject(function (Analytics, $location) { 92 | Analytics.log.length = 0; // clear log 93 | Analytics.registerScriptTags(); 94 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics.js']); 95 | }); 96 | }); 97 | 98 | it('should warn and prevent a second attempt to inject a script tag', function () { 99 | inject(function ($log) { 100 | spyOn($log, 'warn'); 101 | inject(function (Analytics) { 102 | Analytics.log.length = 0; // clear log 103 | Analytics.registerScriptTags(); 104 | expect(Analytics.log[0]).toEqual(['inject', '//www.google-analytics.com/analytics.js']); 105 | Analytics.registerScriptTags(); 106 | expect($log.warn).toHaveBeenCalledWith(['Script tags already created']); 107 | }); 108 | }); 109 | }); 110 | 111 | describe('with a prefix set', function(){ 112 | beforeEach(module(function (AnalyticsProvider){ 113 | AnalyticsProvider 114 | .trackPrefix("test-prefix"); 115 | })); 116 | 117 | it('should send the url, including the prefix', function(){ 118 | inject(function (Analytics) { 119 | Analytics.log.length = 0; // clear log 120 | Analytics.registerScriptTags(); 121 | Analytics.registerTrackers(); 122 | expect(Analytics.log[2]).toEqual(['send', 'pageview', 'test-prefix']); 123 | }); 124 | }); 125 | 126 | it('should send the url, including the prefix with each event', function() { 127 | inject(function ($window) { 128 | spyOn($window, 'ga'); 129 | inject(function (Analytics) { 130 | Analytics.log.length = 0; // clear log 131 | Analytics.trackEvent('test', 'action', 'label', 0); 132 | expect(Analytics.log.length).toBe(1); 133 | expect($window.ga).toHaveBeenCalledWith('send', 'event', 'test', 'action', 'label', 0, { page: 'test-prefix' }); 134 | }); 135 | }); 136 | }); 137 | }); 138 | }); 139 | 140 | describe('hybrid mobile application support', function () { 141 | beforeEach(module(function (AnalyticsProvider) { 142 | AnalyticsProvider 143 | .setHybridMobileSupport(true) 144 | .delayScriptTag(true); 145 | })); 146 | 147 | it('should support hybridMobileSupport', function () { 148 | inject(function (Analytics) { 149 | expect(Analytics.configuration.hybridMobileSupport).toBe(true); 150 | }); 151 | }); 152 | 153 | it('should inject a script tag with the HTTPS protocol and set checkProtocolTask to null', function () { 154 | inject(function (Analytics) { 155 | Analytics.log.length = 0; // clear log 156 | Analytics.registerScriptTags(); 157 | Analytics.registerTrackers(); 158 | expect(Analytics.log[0]).toEqual(['inject', 'https://www.google-analytics.com/analytics.js']); 159 | expect(Analytics.log[1]).toEqual(['create', 'UA-XXXXXX-xx', {}]); 160 | expect(Analytics.log[2]).toEqual(['set', 'checkProtocolTask', null]); 161 | }); 162 | }); 163 | }); 164 | 165 | describe('account custom set commands support', function () { 166 | beforeEach(module(function (AnalyticsProvider) { 167 | AnalyticsProvider 168 | .setAccount({ 169 | tracker: 'UA-XXXXXX-xx', 170 | set: { 171 | forceSSL: true 172 | } 173 | }) 174 | .setHybridMobileSupport(true) 175 | .delayScriptTag(true); 176 | })); 177 | 178 | it('should set the account object to use forceSSL', function () { 179 | inject(function (Analytics) { 180 | Analytics.log.length = 0; // clear log 181 | Analytics.registerScriptTags(); 182 | Analytics.registerTrackers(); 183 | expect(Analytics.log[0]).toEqual(['inject', 'https://www.google-analytics.com/analytics.js']); 184 | expect(Analytics.log[1]).toEqual(['create', 'UA-XXXXXX-xx', {}]); 185 | expect(Analytics.log[2]).toEqual(['set', 'checkProtocolTask', null]); 186 | expect(Analytics.log[3]).toEqual(['set', 'forceSSL', true]); 187 | }); 188 | }); 189 | }); 190 | 191 | describe('account select support', function () { 192 | var account; 193 | 194 | beforeEach(module(function (AnalyticsProvider) { 195 | account = { 196 | tracker: 'UA-XXXXXX-xx', 197 | select: function () { 198 | return false; 199 | } 200 | }; 201 | spyOn(account, 'select'); 202 | AnalyticsProvider.setAccount(account); 203 | })); 204 | 205 | it('should not run with commands after configuration when select returns false', function () { 206 | inject(function (Analytics) { 207 | Analytics.log.length = 0; // clear log 208 | Analytics.trackPage('/path/to', 'title'); 209 | expect(Analytics.log.length).toEqual(0); 210 | expect(account.select).toHaveBeenCalledWith(['send', 'pageview', { page: '/path/to', title: 'title' }]); 211 | }); 212 | }); 213 | }); 214 | 215 | describe('ignoreFirstPageLoad configuration support', function () { 216 | beforeEach(module(function (AnalyticsProvider) { 217 | AnalyticsProvider.ignoreFirstPageLoad(true); 218 | })); 219 | 220 | it('should support ignoreFirstPageLoad', function () { 221 | inject(function (Analytics) { 222 | expect(Analytics.configuration.ignoreFirstPageLoad).toBe(true); 223 | }); 224 | }); 225 | }); 226 | 227 | describe('displayFeature configuration support', function () { 228 | beforeEach(module(function (AnalyticsProvider) { 229 | AnalyticsProvider.useDisplayFeatures(true); 230 | })); 231 | 232 | it('should support displayFeatures config', function () { 233 | inject(function (Analytics) { 234 | expect(Analytics.configuration.displayFeatures).toBe(true); 235 | }); 236 | }); 237 | }); 238 | 239 | describe('enhancedLinkAttribution configuration support', function () { 240 | beforeEach(module(function (AnalyticsProvider) { 241 | AnalyticsProvider.useEnhancedLinkAttribution(true); 242 | })); 243 | 244 | it('should support enhancedLinkAttribution config', function () { 245 | inject(function (Analytics) { 246 | expect(Analytics.configuration.enhancedLinkAttribution).toBe(true); 247 | }); 248 | }); 249 | }); 250 | 251 | describe('experiment configuration support', function () { 252 | beforeEach(module(function (AnalyticsProvider) { 253 | AnalyticsProvider.setExperimentId('12345'); 254 | })); 255 | 256 | it('should support experimentId config', function () { 257 | inject(function (Analytics) { 258 | expect(Analytics.configuration.experimentId).toBe('12345'); 259 | }); 260 | }); 261 | }); 262 | 263 | describe('supports custom events, dimensions, and metrics', function () { 264 | it('should allow sending custom events', function () { 265 | inject(function (Analytics) { 266 | var social = { 267 | hitType: 'social', 268 | socialNetwork: 'facebook', 269 | socialAction: 'like', 270 | socialTarget: 'http://mycoolpage.com', 271 | page: '/my-new-page' 272 | }; 273 | Analytics.log.length = 0; // clear log 274 | Analytics.send(social); 275 | expect(Analytics.log.length).toBe(1); 276 | expect(Analytics.log[0]).toEqual(['send', social]); 277 | }); 278 | }); 279 | 280 | it('should allow setting custom dimensions, metrics or experiment', function () { 281 | inject(function (Analytics) { 282 | var data = { 283 | name: 'dimension1', 284 | value: 'value1' 285 | }; 286 | Analytics.log.length = 0; // clear log 287 | Analytics.set(data.name, data.value); 288 | expect(Analytics.log.length).toBe(1); 289 | expect(Analytics.log[0]).toEqual(['set', data.name, data.value]); 290 | }); 291 | }); 292 | 293 | describe('with eventTracks', function () { 294 | beforeEach(module(function (AnalyticsProvider) { 295 | AnalyticsProvider.trackPages(false); 296 | })); 297 | 298 | it('should generate eventTracks', function () { 299 | inject(function ($window) { 300 | spyOn($window, 'ga'); 301 | inject(function (Analytics) { 302 | Analytics.log.length = 0; // clear log 303 | Analytics.trackEvent('test'); 304 | expect(Analytics.log.length).toBe(1); 305 | expect($window.ga).toHaveBeenCalledWith('send', 'event', 'test', undefined, undefined, undefined, { page: '' }); 306 | }); 307 | }); 308 | }); 309 | 310 | it('should generate eventTracks and honor non-interactions', function () { 311 | inject(function ($window) { 312 | spyOn($window, 'ga'); 313 | inject(function (Analytics) { 314 | Analytics.log.length = 0; // clear log 315 | Analytics.trackEvent('test', 'action', 'label', 0, true); 316 | expect(Analytics.log.length).toBe(1); 317 | expect($window.ga).toHaveBeenCalledWith('send', 'event', 'test', 'action', 'label', 0, { nonInteraction: true, page: '' }); 318 | }); 319 | }); 320 | }); 321 | }); 322 | }); 323 | 324 | describe('e-commerce transactions with analytics.js', function () { 325 | beforeEach(module(function (AnalyticsProvider) { 326 | AnalyticsProvider.useECommerce(true); 327 | })); 328 | 329 | it('should have e-commerce enabled', function () { 330 | inject(function (Analytics) { 331 | expect(Analytics.configuration.ecommerce).toBe(true); 332 | }); 333 | }); 334 | 335 | it('should have enhanced e-commerce disabled', function () { 336 | inject(function (Analytics) { 337 | expect(Analytics.configuration.enhancedEcommerce).toBe(false); 338 | }); 339 | }); 340 | 341 | it('should add transcation', function () { 342 | inject(function (Analytics) { 343 | Analytics.log.length = 0; // clear log 344 | Analytics.addTrans('1', '', '2.42', '0.42', '0', 'Amsterdam', '', 'Netherlands'); 345 | expect(Analytics.log.length).toBe(1); 346 | expect(Analytics.log[0][0]).toEqual('ecommerce:addTransaction'); 347 | }); 348 | }); 349 | 350 | it('should add an item to transaction', function () { 351 | inject(function (Analytics) { 352 | Analytics.log.length = 0; // clear log 353 | Analytics.addItem('1', 'sku-1', 'Test product 1', 'Testing', '1', '1'); 354 | expect(Analytics.log.length).toBe(1); 355 | expect(Analytics.log[0][0]).toEqual('ecommerce:addItem'); 356 | }); 357 | }); 358 | 359 | it('should track the transaction', function () { 360 | inject(function (Analytics) { 361 | Analytics.log.length = 0; // clear log 362 | Analytics.trackTrans(); 363 | expect(Analytics.log.length).toBe(1); 364 | expect(Analytics.log[0]).toEqual(['ecommerce:send']); 365 | }); 366 | }); 367 | 368 | it('should allow transaction clearing', function () { 369 | inject(function (Analytics) { 370 | Analytics.log.length = 0; // clear log 371 | Analytics.clearTrans(); 372 | expect(Analytics.log.length).toBe(1); 373 | expect(Analytics.log[0]).toEqual(['ecommerce:clear']); 374 | }); 375 | }); 376 | 377 | it('should not support enhanced e-commerce commands', function () { 378 | var commands = [ 379 | 'addImpression', 380 | 'addProduct', 381 | 'addPromo', 382 | 'setAction' 383 | ]; 384 | 385 | inject(function ($log) { 386 | spyOn($log, 'warn'); 387 | inject(function (Analytics) { 388 | commands.forEach(function (command) { 389 | Analytics[command](); 390 | expect($log.warn).toHaveBeenCalledWith(['Enhanced Ecommerce must be enabled to use ' + command + ' with analytics.js']); 391 | }); 392 | }); 393 | }); 394 | }); 395 | 396 | describe('supports multiple tracking objects', function () { 397 | var trackers = [ 398 | { tracker: 'UA-12345-12', name: 'tracker1', trackEcommerce: true }, 399 | { tracker: 'UA-12345-34', name: 'tracker2', trackEcommerce: false }, 400 | { tracker: 'UA-12345-45', trackEcommerce: true } 401 | ]; 402 | 403 | beforeEach(module(function (AnalyticsProvider) { 404 | AnalyticsProvider.setAccount(trackers); 405 | })); 406 | 407 | it('should track transactions for configured tracking objects only', function () { 408 | inject(function ($window) { 409 | spyOn($window, 'ga'); 410 | inject(function (Analytics) { 411 | Analytics.log.length = 0; // clear log 412 | Analytics.trackTrans(); 413 | expect(Analytics.log.length).toBe(2); 414 | expect($window.ga).toHaveBeenCalledWith('tracker1.ecommerce:send'); 415 | expect($window.ga).toHaveBeenCalledWith('ecommerce:send'); 416 | }); 417 | }); 418 | }); 419 | }); 420 | }); 421 | 422 | describe('enhanced e-commerce transactions with analytics.js', function () { 423 | beforeEach(module(function (AnalyticsProvider) { 424 | AnalyticsProvider.useECommerce(true, true); 425 | })); 426 | 427 | it('should have ecommerce disabled', function () { 428 | inject(function (Analytics) { 429 | expect(Analytics.configuration.ecommerce).toBe(false); 430 | }); 431 | }); 432 | 433 | it('should have enhanced ecommerce enabled', function () { 434 | inject(function (Analytics) { 435 | expect(Analytics.configuration.enhancedEcommerce).toBe(true); 436 | }); 437 | }); 438 | 439 | it('should add product impression', function () { 440 | inject(function (Analytics) { 441 | Analytics.log.length = 0; // clear log 442 | Analytics.addImpression('sku-1', 'Test Product 1', 'Category List', 'Brand 1', 'Category-1', 'variant-1', '1', '24990'); 443 | expect(Analytics.log.length).toBe(1); 444 | expect(Analytics.log[0][0]).toBe('ec:addImpression'); 445 | }); 446 | }); 447 | 448 | it('should add product data', function () { 449 | inject(function ($window) { 450 | spyOn($window, 'ga'); 451 | inject(function (Analytics) { 452 | Analytics.log.length = 0; // clear log 453 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 454 | expect(Analytics.log.length).toBe(1); 455 | expect($window.ga).toHaveBeenCalledWith( 456 | 'ec:addProduct', 457 | { 458 | id: 'sku-2', 459 | name: 'Test Product 2', 460 | category: 'Category-1', 461 | brand: 'Brand 2', 462 | variant: 'variant-3', 463 | price: '2499', 464 | quantity: '1', 465 | coupon: 'FLAT10', 466 | position: '1' 467 | }); 468 | }); 469 | }); 470 | }); 471 | 472 | it('should add product data with custom properties', function () { 473 | inject(function ($window) { 474 | spyOn($window, 'ga'); 475 | inject(function (Analytics) { 476 | Analytics.log.length = 0; // clear log 477 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', undefined, undefined, { dimension1: '1' }); 478 | expect(Analytics.log.length).toBe(1); 479 | expect($window.ga).toHaveBeenCalledWith( 480 | 'ec:addProduct', 481 | { 482 | id: 'sku-2', 483 | name: 'Test Product 2', 484 | category: 'Category-1', 485 | brand: 'Brand 2', 486 | variant: 'variant-3', 487 | price: '2499', 488 | quantity: '1', 489 | coupon: undefined, 490 | position: undefined, 491 | dimension1: '1' 492 | }); 493 | }); 494 | }); 495 | }); 496 | 497 | it('should add promo data', function () { 498 | inject(function (Analytics) { 499 | Analytics.log.length = 0; // clear log 500 | Analytics.addPromo('PROMO_1234', 'Summer Sale', 'summer_banner2', 'banner_slot1'); 501 | expect(Analytics.log.length).toBe(1); 502 | expect(Analytics.log[0][0]).toBe('ec:addPromo'); 503 | }); 504 | }); 505 | 506 | it('should set action', function () { 507 | inject(function (Analytics) { 508 | Analytics.log.length = 0; // clear log 509 | Analytics.setAction('dummy'); 510 | expect(Analytics.log.length).toBe(1); 511 | expect(Analytics.log[0]).toEqual(['ec:setAction', 'dummy', undefined]); 512 | }); 513 | }); 514 | 515 | it('should track product click', function () { 516 | inject(function (Analytics) { 517 | Analytics.log.length = 0; // clear log 518 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 519 | Analytics.productClick('dummy list'); 520 | expect(Analytics.log.length).toBe(3); 521 | expect(Analytics.log[0][0]).toBe('ec:addProduct'); 522 | expect(Analytics.log[1]).toEqual([ 'ec:setAction', 'click', { list: 'dummy list' } ]); 523 | expect(Analytics.log[2]).toEqual([ 'send', 'event', 'UX', 'click', 'dummy list', undefined, { page: '' } ]); 524 | }); 525 | }); 526 | 527 | it('should track product detail', function () { 528 | inject(function (Analytics) { 529 | Analytics.log.length = 0; // clear log 530 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 531 | Analytics.trackDetail(); 532 | expect(Analytics.log.length).toBe(3); 533 | expect(Analytics.log[0][0]).toBe('ec:addProduct'); 534 | expect(Analytics.log[1]).toEqual([ 'ec:setAction', 'detail', undefined ]); 535 | expect(Analytics.log[2]).toEqual(['send', 'pageview']); 536 | }); 537 | }); 538 | 539 | it('should track add to cart event', function () { 540 | inject(function (Analytics) { 541 | Analytics.log.length = 0; // clear log 542 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 543 | Analytics.trackCart('add'); 544 | expect(Analytics.log.length).toBe(3); 545 | expect(Analytics.log[0][0]).toBe('ec:addProduct'); 546 | expect(Analytics.log[1]).toEqual([ 'ec:setAction', 'add', { list: undefined } ]); 547 | expect(Analytics.log[2]).toEqual([ 'send', 'event', 'UX', 'click', 'add to cart', undefined, { page: '' } ]); 548 | }); 549 | }); 550 | 551 | it('should track add to cart event with product list', function () { 552 | inject(function (Analytics) { 553 | Analytics.log.length = 0; // clear log 554 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 555 | Analytics.trackCart('add', 'product-list'); 556 | expect(Analytics.log.length).toBe(3); 557 | expect(Analytics.log[0][0]).toBe('ec:addProduct'); 558 | expect(Analytics.log[1]).toEqual([ 'ec:setAction', 'add', { list: 'product-list' } ]); 559 | expect(Analytics.log[2]).toEqual([ 'send', 'event', 'UX', 'click', 'add to cart', undefined, { page: '' } ]); 560 | }); 561 | }); 562 | 563 | it('should track remove from cart event', function () { 564 | inject(function (Analytics) { 565 | Analytics.log.length = 0; // clear log 566 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 567 | Analytics.trackCart('remove'); 568 | expect(Analytics.log.length).toBe(3); 569 | expect(Analytics.log[0][0]).toBe('ec:addProduct'); 570 | expect(Analytics.log[1]).toEqual([ 'ec:setAction', 'remove', { list: undefined } ]); 571 | expect(Analytics.log[2]).toEqual([ 'send', 'event', 'UX', 'click', 'remove from cart', undefined, { page: '' } ]); 572 | }); 573 | }); 574 | 575 | it('should track remove from cart event with product list', function () { 576 | inject(function (Analytics) { 577 | Analytics.log.length = 0; // clear log 578 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 579 | Analytics.trackCart('remove', 'product-list'); 580 | expect(Analytics.log.length).toBe(3); 581 | expect(Analytics.log[0][0]).toBe('ec:addProduct'); 582 | expect(Analytics.log[1]).toEqual([ 'ec:setAction', 'remove', {list: 'product-list'} ]); 583 | expect(Analytics.log[2]).toEqual([ 'send', 'event', 'UX', 'click', 'remove from cart', undefined, { page: '' } ]); 584 | }); 585 | }); 586 | 587 | it('should track checkout', function () { 588 | inject(function (Analytics) { 589 | Analytics.log.length = 0; // clear log 590 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 591 | Analytics.trackCheckout(); 592 | expect(Analytics.log.length).toBe(2); 593 | expect(Analytics.log[0][0]).toBe('ec:addProduct'); 594 | expect(Analytics.log[1][0]).toBe('ec:setAction'); 595 | expect(Analytics.log[1][1]).toBe('checkout'); 596 | }); 597 | }); 598 | 599 | it('should track transaction', function () { 600 | inject(function (Analytics) { 601 | Analytics.log.length = 0; // clear log 602 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 603 | Analytics.addProduct('sku-3', 'Test Product 3', 'Category-1', 'Brand 2', 'variant-5', '299', '1', 'FLAT10', '1'); 604 | Analytics.trackTransaction(); 605 | expect(Analytics.log.length).toBe(3); 606 | expect(Analytics.log[0][0]).toBe('ec:addProduct'); 607 | expect(Analytics.log[1][0]).toBe('ec:addProduct'); 608 | expect(Analytics.log[2][0]).toBe('ec:setAction'); 609 | expect(Analytics.log[2][1]).toBe('purchase'); 610 | }); 611 | }); 612 | 613 | it('should track promo click', function () { 614 | inject(function (Analytics) { 615 | Analytics.log.length = 0; // clear log 616 | Analytics.addPromo('PROMO_1234', 'Summer Sale', 'summer_banner2', 'banner_slot1'); 617 | Analytics.promoClick('Summer Sale'); 618 | expect(Analytics.log.length).toBe(3); 619 | expect(Analytics.log[0][0]).toBe('ec:addPromo'); 620 | expect(Analytics.log[1][0]).toBe('ec:setAction'); 621 | expect(Analytics.log[1][1]).toBe('promo_click'); 622 | expect(Analytics.log[2]).toEqual([ 'send', 'event', 'Internal Promotions', 'click', 'Summer Sale', undefined, { page: '' } ]); 623 | }); 624 | }); 625 | 626 | it('should not support ecommerce commands', function () { 627 | var commands = [ 628 | 'addItem', 629 | 'addTrans', 630 | 'clearTrans', 631 | 'trackTrans' 632 | ]; 633 | 634 | inject(function ($log) { 635 | spyOn($log, 'warn'); 636 | inject(function (Analytics) { 637 | commands.forEach(function (command) { 638 | Analytics[command](); 639 | expect($log.warn).toHaveBeenCalledWith([command + ' is not available when Enhanced Ecommerce is enabled with analytics.js']); 640 | }); 641 | }); 642 | }); 643 | }); 644 | 645 | describe('supports multiple tracking objects', function () { 646 | var trackers = [ 647 | { tracker: 'UA-12345-12', name: 'tracker1', trackEcommerce: false }, 648 | { tracker: 'UA-12345-34', name: 'tracker2', trackEcommerce: true }, 649 | { tracker: 'UA-12345-45', trackEcommerce: true } 650 | ]; 651 | 652 | beforeEach(module(function (AnalyticsProvider) { 653 | AnalyticsProvider.setAccount(trackers); 654 | })); 655 | 656 | it('should add product for configured tracking objects only', function () { 657 | inject(function ($window) { 658 | spyOn($window, 'ga'); 659 | inject(function (Analytics) { 660 | Analytics.log.length = 0; // clear log 661 | Analytics.addProduct('sku-2', 'Test Product 2', 'Category-1', 'Brand 2', 'variant-3', '2499', '1', 'FLAT10', '1'); 662 | expect(Analytics.log.length).toBe(2); 663 | expect($window.ga).toHaveBeenCalledWith( 664 | 'ec:addProduct', 665 | { 666 | id: 'sku-2', 667 | name: 'Test Product 2', 668 | category: 'Category-1', 669 | brand: 'Brand 2', 670 | variant: 'variant-3', 671 | price: '2499', 672 | quantity: '1', 673 | coupon: 'FLAT10', 674 | position: '1' 675 | }); 676 | expect($window.ga).toHaveBeenCalledWith( 677 | 'tracker2.ec:addProduct', 678 | { 679 | id: 'sku-2', 680 | name: 'Test Product 2', 681 | category: 'Category-1', 682 | brand: 'Brand 2', 683 | variant: 'variant-3', 684 | price: '2499', 685 | quantity: '1', 686 | coupon: 'FLAT10', 687 | position: '1' 688 | }); 689 | }); 690 | }); 691 | }); 692 | }); 693 | }); 694 | 695 | describe('supports arbitrary page events', function () { 696 | beforeEach(module(function (AnalyticsProvider) { 697 | AnalyticsProvider.setPageEvent('$stateChangeSuccess'); 698 | })); 699 | 700 | it('should respond to non-default page event', function () { 701 | inject(function (Analytics, $rootScope) { 702 | Analytics.log.length = 0; // clear log 703 | $rootScope.$broadcast('$stateChangeSuccess'); 704 | expect(Analytics.log.length).toBe(1); 705 | expect(Analytics.log[0]).toEqual([ 'send', 'pageview', { page: '', title: '' } ]); 706 | }); 707 | }); 708 | }); 709 | 710 | describe('supports RegExp path scrubbing', function () { 711 | beforeEach(module(function (AnalyticsProvider) { 712 | AnalyticsProvider.setRemoveRegExp(new RegExp(/\/\d+?$/)); 713 | })); 714 | 715 | it('should scrub urls', function () { 716 | inject(function (Analytics, $location) { 717 | $location.path('/some-crazy/page/with/numbers/123456'); 718 | expect(Analytics.getUrl()).toBe('/some-crazy/page/with/numbers'); 719 | }); 720 | }); 721 | }); 722 | 723 | describe('parameter defaulting on trackPage', function () { 724 | beforeEach(module(function (AnalyticsProvider) { 725 | AnalyticsProvider.trackPages(false); 726 | })); 727 | 728 | it('should set url and title when no parameters provided', function () { 729 | inject(function (Analytics, $document, $location) { 730 | $location.path('/page/here'); 731 | $document[0] = { title: 'title here' }; 732 | Analytics.log.length = 0; // clear log 733 | Analytics.trackPage(); 734 | expect(Analytics.log.length).toBe(1); 735 | expect(Analytics.log[0]).toEqual([ 'send', 'pageview', { page: '/page/here', title: 'title here' } ]); 736 | }); 737 | }); 738 | 739 | it('should set title when no title provided', function () { 740 | inject(function (Analytics, $document) { 741 | $document[0] = { title: 'title here' }; 742 | Analytics.log.length = 0; // clear log 743 | Analytics.trackPage('/page/here'); 744 | expect(Analytics.log.length).toBe(1); 745 | expect(Analytics.log[0]).toEqual([ 'send', 'pageview', { page: '/page/here', title: 'title here' } ]); 746 | }); 747 | }); 748 | }); 749 | 750 | describe('supports multiple tracking objects', function () { 751 | var trackers = [ 752 | { tracker: 'UA-12345-12', name: 'tracker1' }, 753 | { tracker: 'UA-12345-34', name: 'tracker2' }, 754 | { tracker: 'UA-12345-45' } 755 | ]; 756 | 757 | beforeEach(module(function (AnalyticsProvider) { 758 | AnalyticsProvider.setAccount(trackers); 759 | })); 760 | 761 | it('should call create event for each tracker', function () { 762 | inject(function ($window) { 763 | spyOn($window, 'ga'); 764 | inject(function (Analytics) { 765 | expect($window.ga).toHaveBeenCalledWith('create', trackers[0].tracker, { name: trackers[0].name }); 766 | expect($window.ga).toHaveBeenCalledWith('create', trackers[1].tracker, { name: trackers[1].name }); 767 | expect($window.ga).toHaveBeenCalledWith('create', trackers[2].tracker, {}); 768 | }); 769 | }); 770 | }); 771 | 772 | it('should call send pageview event for each tracker', function () { 773 | inject(function ($window) { 774 | spyOn($window, 'ga'); 775 | inject(function (Analytics) { 776 | Analytics.trackPage('/mypage', 'My Page'); 777 | expect($window.ga).toHaveBeenCalledWith(trackers[0].name + '.send', 'pageview', { page: '/mypage', title: 'My Page' }); 778 | expect($window.ga).toHaveBeenCalledWith(trackers[1].name + '.send', 'pageview', { page: '/mypage', title: 'My Page' }); 779 | expect($window.ga).toHaveBeenCalledWith('send', 'pageview', { page: '/mypage', title: 'My Page' }); 780 | }); 781 | }); 782 | }); 783 | }); 784 | 785 | describe('supports advanced options for multiple tracking objects', function () { 786 | var trackers = [ 787 | { tracker: 'UA-12345-12', name: 'tracker1', crossDomainLinker: true }, 788 | { tracker: 'UA-12345-34', name: 'tracker2', crossDomainLinker: true, crossLinkDomains: ['domain-1.com'] }, 789 | { tracker: 'UA-12345-45', crossDomainLinker: true, crossLinkDomains: ['domain-2.com'] } 790 | ]; 791 | 792 | beforeEach(module(function (AnalyticsProvider) { 793 | AnalyticsProvider.setAccount(trackers); 794 | })); 795 | 796 | it('should call require for each tracker', function () { 797 | inject(function ($window) { 798 | spyOn($window, 'ga'); 799 | inject(function (Analytics) { 800 | expect($window.ga).toHaveBeenCalledWith('tracker1.require', 'linker'); 801 | expect($window.ga).toHaveBeenCalledWith('tracker2.require', 'linker'); 802 | expect($window.ga).toHaveBeenCalledWith('require', 'linker'); 803 | }); 804 | }); 805 | }); 806 | 807 | it('should call linker autoLink for configured tracking objects only', function () { 808 | inject(function ($window) { 809 | spyOn($window, 'ga'); 810 | inject(function (Analytics) { 811 | expect($window.ga).not.toHaveBeenCalledWith('tracker1.linker:autoLink'); 812 | expect($window.ga).toHaveBeenCalledWith('tracker2.linker:autoLink', ['domain-1.com']); 813 | expect($window.ga).toHaveBeenCalledWith('linker:autoLink', ['domain-2.com']); 814 | }); 815 | }); 816 | }); 817 | }); 818 | 819 | describe('supports advanced tracking for multiple tracking objects', function () { 820 | var trackers = [ 821 | { tracker: 'UA-12345-12', name: 'tracker1', trackEvent: true }, 822 | { tracker: 'UA-12345-34', name: 'tracker2' }, 823 | { tracker: 'UA-12345-45', trackEvent: true } 824 | ]; 825 | 826 | beforeEach(module(function (AnalyticsProvider) { 827 | AnalyticsProvider.setAccount(trackers); 828 | })); 829 | 830 | it('should track events for configured tracking objects only', function () { 831 | inject(function ($window) { 832 | spyOn($window, 'ga'); 833 | inject(function (Analytics) { 834 | Analytics.trackEvent('category', 'action', 'label', 'value'); 835 | expect($window.ga).toHaveBeenCalledWith('tracker1.send', 'event', 'category', 'action', 'label', 'value', { page: '' }); 836 | expect($window.ga).not.toHaveBeenCalledWith('tracker2.send', 'event', 'category', 'action', 'label', 'value', { page: '' }); 837 | expect($window.ga).toHaveBeenCalledWith('send', 'event', 'category', 'action', 'label', 'value', { page: '' }); 838 | }); 839 | }); 840 | }); 841 | 842 | it('should track user timings for all objects', function () { 843 | inject(function ($window) { 844 | spyOn($window, 'ga'); 845 | inject(function (Analytics) { 846 | Analytics.trackTimings('Time to Checkout', 'User Timings', '32', 'My Timings'); 847 | expect($window.ga).toHaveBeenCalledWith('tracker1.send', 'timing', 'Time to Checkout', 'User Timings', '32', 'My Timings'); 848 | expect($window.ga).toHaveBeenCalledWith('tracker2.send', 'timing', 'Time to Checkout', 'User Timings', '32', 'My Timings'); 849 | expect($window.ga).toHaveBeenCalledWith('send', 'timing', 'Time to Checkout', 'User Timings', '32', 'My Timings'); 850 | }); 851 | }); 852 | }); 853 | 854 | it('should set value for default tracker if no trackerName provided', function () { 855 | inject(function ($window) { 856 | spyOn($window, 'ga'); 857 | inject(function (Analytics) { 858 | Analytics.set('dimension1', 'metric1'); 859 | expect($window.ga).toHaveBeenCalledWith('set', 'dimension1', 'metric1'); 860 | }); 861 | }); 862 | }); 863 | 864 | it('should set value for named tracker if a trackerName provided', function () { 865 | inject(function ($window) { 866 | spyOn($window, 'ga'); 867 | inject(function (Analytics) { 868 | Analytics.set('dimension2', 'metric2', 'tracker1'); 869 | expect($window.ga).toHaveBeenCalledWith('tracker1.set', 'dimension2', 'metric2'); 870 | }); 871 | }); 872 | }); 873 | }); 874 | 875 | describe('enabled url params tracking', function () { 876 | beforeEach(module(function (AnalyticsProvider) { 877 | AnalyticsProvider.trackUrlParams(true); 878 | })); 879 | 880 | it('should grab query params in the url', function () { 881 | inject(function (Analytics, $location) { 882 | $location.url('/some/page?with_params=foo&more_param=123'); 883 | expect(Analytics.getUrl()).toContain('?with_params=foo&more_param=123'); 884 | }); 885 | }); 886 | }); 887 | }); 888 | --------------------------------------------------------------------------------