UNPKG

angulartics-google-analytics

Version:
632 lines (506 loc) 20.5 kB
(function(window, angular, undefined) {'use strict'; /** * @ngdoc overview * @name angulartics.google.analytics * Enables analytics support for Google Analytics (http://google.com/analytics) */ angular.module('angulartics.google.analytics', ['angulartics']) .config(['$analyticsProvider', '$windowProvider', function ($analyticsProvider, $windowProvider) { var $window = $windowProvider.$get(); // GA already supports buffered invocations so we don't need // to wrap these inside angulartics.waitForVendorApi $analyticsProvider.settings.pageTracking.trackRelativePath = true; // We cache the latest pageTrack path value here to ensure consistent // tracking of the page dimension with other hits var pagePathCache; // Set the default settings for this module $analyticsProvider.settings.ga = { additionalAccountNames: undefined, // Select hits to send to all additional accounts additionalAccountHitTypes: { pageview: true, event: true, exception: false, ecommerce: false, userTiming: false, setUserProperties: false, userId: false }, disableEventTracking: null, disablePageTracking: null, enhancedEcommerce: false, // GA supports transporting data via gif requests, XHRs, or sendBeacons // @link https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#transport transport: null, userId: null }; /** * Track Pageview in GA * @name pageTrack * * @param {string} path value of Page dimension stored with hit e.g. '/home' * @param {object} properties Object with optional addtional Custom Dimensions/Metrics * * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/pages * @link https://developers.google.com/analytics/devguides/collection/gajs/ */ $analyticsProvider.registerPageTrack(function (path, properties) { pagePathCache = path; properties = properties || {}; // Do nothing if page tracking is disabled if ($analyticsProvider.settings.ga.disablePageTracking) return; dispatchToGa('pageview', 'send', angular.extend(angular.copy(properties), { hitType: 'pageview', page: path })); }); /** * Track Event in GA * @name eventTrack * * @param {string} action Required 'action' (string) associated with the event * @param {object} properties Comprised of the mandatory field 'category' (string) and optional fields 'label' (string), 'value' (integer) and 'nonInteraction' (boolean) * * @link https://developers.google.com/analytics/devguides/collection/gajs/eventTrackerGuide#SettingUpEventTracking * * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/events */ $analyticsProvider.registerEventTrack(function(action, properties) { // Do nothing if event tracking is disabled if ($analyticsProvider.settings.ga.disableEventTracking) return; if (!action && action + '' !== '0') { return; } // Sets default properties properties = properties || {}; properties.category = properties.category || 'Event'; // GA requires that eventValue be an integer, see: // https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#eventValue // https://github.com/luisfarzati/angulartics/issues/81 if (properties.value) { var parsed = parseInt(properties.value, 10); properties.value = isNaN(parsed) ? 0 : parsed; } // GA requires that hitCallback be an function, see: // https://developers.google.com/analytics/devguides/collection/analyticsjs/sending-hits#hitcallback if (!angular.isFunction(properties.hitCallback)) { properties.hitCallback = null; } // Making nonInteraction parameter more intuitive, includes backwards compatibilty // https://github.com/angulartics/angulartics-google-analytics/issues/49 properties.nonInteraction = properties.nonInteraction || properties.noninteraction; dispatchToGa('event', 'send', angular.extend(angular.copy(properties), { hitType: 'event', eventCategory: properties.category, eventAction: action, eventLabel: properties.label, eventValue: properties.value, nonInteraction: properties.nonInteraction, page: getPage_(properties), hitCallback: properties.hitCallback, })); }); /** * Exception Track Event in GA * @name exceptionTrack * Sugar on top of the eventTrack method for easily handling errors * * @param {object} error An Error object to track: error.toString() used for event 'action', error.stack used for event 'label'. * @param {object} cause The cause of the error given from $exceptionHandler, not used. * * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/events */ $analyticsProvider.registerExceptionTrack(function (error, cause) { dispatchToGa('exception', 'send', { hitType: 'event', eventCategory: 'Exceptions', eventAction: error.toString(), eventLabel: error.stack, nonInteraction: true, page: getPage_({}), isException: true }); }); /** * Set Username * @name setUsername * * @param {string} userId Registers User ID of user for use with other hits * * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/cookies-user-id#user_id */ $analyticsProvider.registerSetUsername(function (userId) { $analyticsProvider.settings.ga.userId = userId; }); /** * Set User Properties * @name setUserProperties * * @param {object} properties Sets all properties with dimensionN or metricN to their respective values * * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#customs */ $analyticsProvider.registerSetUserProperties(function (properties) { if (properties) { dispatchToGa('setUserProperties', 'set', dimensionsAndMetrics(properties)); } }); /** * User Timings Event in GA * @name userTimings * * @param {object} properties Comprised of the mandatory fields: * 'timingCategory' (string), * 'timingVar' (string), * 'timingValue' (number) * Properties can also have the optional fields: * 'timingLabel' (string) * 'optSampleRate' (number) Classic Analytics only - determines % of users to collect data from, handled serverside by UA * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings#sampling_considerations * * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings */ $analyticsProvider.registerUserTimings(function (properties) { if (!angular.isObject(properties) || angular.isArray(properties)) { return console.log('Required argument properties is missing or not an object'); } angular.forEach(['timingCategory', 'timingVar', 'timingValue'], function(prop) { if (angular.isUndefined(properties[prop])) { return console.log('Argument properties missing required property ' + prop); } }); dispatchToGa('userTiming', 'send', { hitType: 'timing', timingCategory: properties.timingCategory, timingVar: properties.timingVar, timingValue: properties.timingValue, timingLabel: properties.timingLabel, optSampleRate: properties.optSampleRate, // Classic Analytics only page: getPage_(properties) }); }); /** * Ecommerce Tracking in GA * @name transactionTrack * * @param {object} transaction comprised of following fields: * 'id': 'T12345', // Transaction ID. Required for purchases and refunds. * 'affiliation': 'Online Store', * 'revenue': '35.43', // Total transaction value (incl. tax and shipping) * 'tax':'4.90', * 'shipping': '5.99', * 'coupon': 'SUMMER_SALE', // Enhanced Ecommerce Only * 'dimension1': 'Card ID #1234', // Hit, Session, or User-level Custom Dimension(s) * 'metric1': 1, // Custom Metric(s) * 'currencyCode': 'EUR', // Currency Code to track the transaction with. Recognized codes: https://support.google.com/analytics/answer/6205902?hl=en#supported-currencies * 'billingCity': 'San Francisco', // Classic Analytics only * 'billingRegion': 'California', // Classic Analytics only * 'billingCountry': 'USA', // Classic Analytics only * 'products': [{ // List of products * 'name': 'Triblend Android T-Shirt', // Name or ID is required. * 'id': '12345', // Product SKU * 'price': '15.25', * 'brand': 'Google', // Enhanced Ecommerce only * 'category': 'Apparel', * 'variant': 'Gray', // Enhanced Ecommerce only * 'quantity': 1, * 'coupon': '', // Enhanced Ecommerce only. * 'currencyCode': 'BRL', // Product-level currency code, Enhanced Ecommerce only * 'dimension2': 'Clearance', // Product-level Custom Dimension * 'metric2': 1 // Product-level Custom Metric * }, * ... * ] * * @param {object] properties comprised of custom dimensions and metrics to * send with the transaction hit * Utilizes traditional ecommerce tracking by default. To used Enhanced Ecommerce, * set the $analytics.settings.ga.enhancedEcommerce flag to true * * Docs on traditional ecommerce (UA): * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/ecommerce * * Docs on Enhanced Ecommerce * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce * * Docs on Classic Ecommerce (_gaq) * @link https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingEcommerce **/ $analyticsProvider.registerTransactionTrack(function(transaction) { var product; var i; // Universal Analytics splits off the ecommerce code into a separate // library we must include by using the "require" command dispatchToGa('ecommerce', 'require', 'ecommerce'); dispatchToGa('ecommerce', 'ecommerce:addTransaction', transaction); if (transaction.products) { for (i = 0; i < transaction.products.length; i++) { product = transaction.products[i]; // GA uses a SKU property and stores the transaction ID in the ID Field product.sku = product.id; product.id = transaction.id; dispatchToGa('ecommerce', 'ecommerce:addItem', transaction.products[i]); } } if (transaction.currencyCode) { dispatchToGa('ecommerce', '_set', transaction.currencyCode); // Classic Anayltics only - UA uses fieldsObj.currencyCode instead } dispatchToGa('ecommerce', 'ecommerce:send', angular.copy(transaction)); }); /** * Detects if Universal Analytics is installed * * @name detectUniversalAnalytics */ function detectUniversalAnalytics() { // Use the GoogleAnalyticsObject property set by the default GA snippet // to correctly determine the namespace of the GA global var gaNamespace = $window.GoogleAnalyticsObject; return gaNamespace && $window[gaNamespace]; } /** * Detects if Classic Analytics is installed * * @name detectClassicAnalytics */ function detectClassicAnalytics() { // If _gaq is undefined, we're trusting Classic Analytics to be there return !angular.isUndefined($window._gaq); } /** * Extract Custom Data for a hit * @name dimensionsAndMetrics * * @param {object} properties properties object from an API call that is filtered for Custom Dimensions & Metrics * * @returns {object} customData object with only Custom Dimensions/Metrics from properties argument */ function dimensionsAndMetrics(properties) { // add custom dimensions and metrics var customData = {}; var key; for (key in properties) { // Keys must be dimensionXX or metricXX, e.g. dimension1, metric155, so // if those strings aren't at zero (which evaluates to falsey), ignore // the key if (!key.indexOf('dimension') || !key.indexOf('metric')) { customData[key] = properties[key]; } } return customData; } /** * Handler for hits to GA. Dynamically adjusts syntax for * targeted version based on global detection. * * @name dispatchToGa * * @param {string} method Name of angulartics method for checking if hits should be duplicated * @param {string} command Standard Universal Analytics command (create, send, set) * @param {object} fieldsObj object with hit-specific fields. Fields are whitelisted in handler - non-supported fields are ignored. * */ var dispatchToGa = function(method, command, fieldsObj) { var handler; if (detectClassicAnalytics()) { handler = dispatchToClassic_; } if (detectUniversalAnalytics()) { handler = dispatchToUniversal_; } // If neither has been detected, GA is not above the angular code if (!handler) { return; } var shouldCopyHit = $analyticsProvider.settings.ga.additionalAccountHitTypes[method]; handler(command, fieldsObj, shouldCopyHit); }; /** * Dispatches a hit using Universal syntax * * @name dispatchToUniversal_ * @private * * @param {string} command Standard Universal Analytics command (create, send, set) * @param {object} fieldsObj object with hit-specific fields. Fields are whitelisted in handler - non-supported fields are ignored. * @param {boolean} shouldCopyHit should hit be propogated to all trackers */ function dispatchToUniversal_(command, fieldsObj, shouldCopyHit) { var userId = $analyticsProvider.settings.ga.userId; var uaCommand, pluginName; if (command === 'require' && fieldsObj === 'ecommerce') { pluginName = fieldsObj; if ($analyticsProvider.settings.ga.enhancedEcommerce) { pluginName = 'ec'; } // Exit here - require calls don't have fieldsObjs return applyUniversalCall_([command, pluginName], shouldCopyHit); } // If our User ID is set, set it on the hit if (userId && angular.isObject(fieldsObj)) fieldsObj.userId = userId; // If a transport preference is specified, set it on the hit if ($analyticsProvider.settings.ga.transport) { fieldsObj.transport = $analyticsProvider.settings.ga.transport; } if (command.indexOf('ecommerce:') > -1 && $analyticsProvider.settings.ga.enhancedEcommerce) { switch (command) { case 'ecommerce:addTransaction': command = ['ec:setAction', 'purchase']; break; case 'ecommerce:addItem': command = 'ec:addProduct'; // Enhanced Ecommerce reverts to using the ID property for the SKU, // so we swap them back here fieldsObj.id = fieldsObj.sku; break; case 'ecommerce:send': command = 'send'; fieldsObj.hitType = 'event'; fieldsObj.eventCategory = 'Angulartics Enhanced Ecommerce'; fieldsObj.eventAction = 'Purchase'; fieldsObj.nonInteraction = true; break; } } uaCommand = command instanceof Array ? command.concat(fieldsObj) : [command, fieldsObj]; applyUniversalCall_(uaCommand, shouldCopyHit); } /** * Handles applying a constructed call to the global Universal GA object * This exists primarily so calls within dispatchToUa_ can short circuit * out of the function to handle specific edge cases, e.g. require commands * @name applyUniversalCall_ * @private * * @param commandArray {array} command to be .apply()'d * @param shouldCopyHit {boolean} should the command be applied to all accts */ function applyUniversalCall_(commandArray, shouldCopyHit) { var userId = $analyticsProvider.settings.ga.userId; var gaNamespace = $window.GoogleAnalyticsObject; var commandClone; // Perform our initial call $window[gaNamespace].apply(this, commandArray); if (shouldCopyHit) { commandClone = angular.copy(commandArray); // If the userId shouldn't be duplicated, remove from the fieldsObj if (userId && !$analyticsProvider.settings.ga.additionalAccountHitTypes.userId) { if (commandClone[2] && typeof commandClone[2] === 'object') { delete commandClone[2].userId; } } angular.forEach($analyticsProvider.settings.ga.additionalAccountNames, function (accountName){ commandClone[0] = accountName + '.' + commandArray[0]; $window[gaNamespace].apply(this, commandClone); }); } } /** * Dispatches a hit using Classic syntax * Translates Universal Syntax to Classic syntax * * @name dispatchToClassic_ * @private * * @param {string} command Standard Universal Analytics command (create, send, set) * @param {object} fieldsObj object with hit-specific fields. Fields are whitelisted in handler - non-supported fields are ignored. * @param {boolean} shouldCopyHit should hit be propogated to all trackers */ function dispatchToClassic_(command, fieldsObj, shouldCopyHit) { if (command === 'set') { return console.log('Classic Analytics does not support the "set" command or Custom Dimensions. Command ignored.'); } var classicCommand; // Transpose our syntax from Universal Analytics to Classic Analytics // Currently we only support 'send' style commands if (command === 'send') { switch(fieldsObj.hitType) { case 'pageview': classicCommand = ['_trackPageview', fieldsObj.page]; break; case 'event': classicCommand = [ '_trackEvent', fieldsObj.eventCategory, fieldsObj.eventAction, fieldsObj.eventLabel, fieldsObj.eventValue, fieldsObj.nonInteraction ]; break; case 'timing': classicCommand = [ '_trackTiming', fieldsObj.timingCategory, fieldsObj.timingVar, fieldsObj.timingValue, fieldsObj.timingLabel, fieldsObj.optSampleRate ]; break; } } if (command === 'ecommerce:addTransaction') { classicCommand = [ '_addTrans', fieldsObj.id, fieldsObj.affiliation, fieldsObj.revenue, fieldsObj.tax, fieldsObj.shipping, fieldsObj.billingCity, fieldsObj.billingRegion, fieldsObj.billingCountry ]; } if (command === 'ecommerce:addItem') { classicCommand = [ '_addItem', fieldsObj.id, fieldsObj.sku, fieldsObj.name, fieldsObj.category, fieldsObj.price, fieldsObj.quantity ]; } if (command === '_set') { classicCommand = [ '_set', 'currencyCode', fieldsObj ]; } if (command === 'ecommerce:send') { classicCommand = [ '_trackTrans' ]; } if (!classicCommand) { return console.log('Unable to find command ' + command + ' or fieldsObj missing required properties. Command ignored.'); } // Issue our command to GA $window._gaq.push(classicCommand); if (shouldCopyHit) { angular.forEach($analyticsProvider.settings.ga.additionalAccountNames, function (accountName){ var classicCommandClone = [].slice.call(classicCommand); // Namespace the command as required classicCommandClone[0] = accountName + '.' + classicCommandClone[0]; $window._gaq.push(classicCommandClone); }); } } /** * Helper function for getting page value * * @param {Object} properties * * @returns {String} */ function getPage_(properties) { return properties.page || pagePathCache || $window.location.hash.substring(1) || window.location.pathname; } }]); })(window, window.angular);