1/** 2 * Provides utility functions for publishing analytics in Confluence, including support for adding 3 * and removing the 'src' parameter from URLs once an equivalent analytics event has been published. 4 * 5 * NOTE - there are plugins that access these utility functions on the global AJS.Confluence variable 6 * at the moment, once this is no longer true this code should be refactored to stop exposing functionality 7 * globally. 8 */ 9define('confluence/analytics-support', [ 10 'jquery', 11 'ajs', 12 'confluence/meta', 13 'window', 14 'document', 15 'confluence/api/url' 16], function( 17 $, 18 AJS, 19 Meta, 20 window, 21 document, 22 url 23) { 24 'use strict'; 25 26 /** 27 * @exports confluence/analytics-support 28 */ 29 var Analytics = Object.create(null); 30 31 var hasAnalyticsHandler; 32 33 /* eslint-disable vars-on-top, valid-jsdoc */ 34 function srcAppendedUrl(urlString, analyticsSource) { 35 var parsedUrl = url.parseUrl(urlString); 36 37 if (parsedUrl.size !== 0) { 38 parsedUrl.get('queryParams').set('src', [analyticsSource]); 39 } 40 return url.formatUrl(parsedUrl); 41 } 42 43 function analyticsHandlerExists() { 44 if (typeof hasAnalyticsHandler === 'undefined') { 45 var eventsData = $._data(window, 'events'); 46 hasAnalyticsHandler = eventsData && eventsData.analytics && eventsData.analytics.length > 0; 47 } 48 return hasAnalyticsHandler; 49 } 50 51 function replaceStateAfterCleaningUpAnalyticsParameters() { 52 var cleanUrl = Analytics.srcRemovedUrl(document.URL); 53 if (document.URL !== cleanUrl) { 54 window.history.replaceState(null, '', cleanUrl); 55 } 56 } 57 58 /** 59 * Returns true if the provided name (components[0].components[1].components[2]) is a valid analytics attribute. 60 * This is true if the attribute name matches the pattern /src\.[^.]+\.[^.]+/ 61 * @param {Array.<String>} components 62 */ 63 function isAnalyticsAttribute(components) { 64 return components.length === 3 && components[0] === 'src'; 65 } 66 67 /** 68 * Updates the href of the array of links to include a src parameter. The value of the src parameter is specified in analyticsSource 69 * @param {jQuery} links 70 * @param {string} analyticsSource 71 */ 72 Analytics.setAnalyticsSource = function(links, analyticsSource) { 73 if (analyticsHandlerExists()) { 74 links.attr('href', function(i, href) { 75 return srcAppendedUrl(href, encodeURIComponent(analyticsSource)); 76 }); 77 } 78 }; 79 80 Analytics.srcRemovedUrl = function(urlString) { 81 var parsedUrl = url.parseUrl(urlString); 82 parsedUrl.get('queryParams').delete('src'); 83 84 var keys = Array.from(parsedUrl.get('queryParams').keys()); 85 for (var i = 0; i < keys.length; i++) { 86 var property = keys[i]; 87 var components = property.split('.'); 88 if (isAnalyticsAttribute(components)) { 89 parsedUrl.get('queryParams').delete(property); 90 } 91 var isJwtTokenParam = property === 'jwt'; 92 if (isJwtTokenParam) { 93 parsedUrl.get('queryParams').delete(property); 94 } 95 } 96 return url.formatUrl(parsedUrl); 97 }; 98 99 /** 100 * Returns values of the src parameter from the specified URL as an array. 101 * @param urlString String that represents a URL to parse. 102 * @returns {src|*|string|Array} Array containing all 'src' parameter values. 103 */ 104 Analytics.srcParamValues = function(urlString) { 105 var params = url.parseUrl(urlString).get('queryParams'); 106 return params && params.get('src') ? params.get('src') : []; 107 }; 108 109 /** 110 * Decodes the analytics attributes from the url 111 * Analytics attributes should be of the form <code>src.SOURCE.ATTRIBUTE_NAME=ATTRIBUTE_VALUE</code> 112 * 113 * We only take the first occurrence of a particular <code>src.SOURCE.ATTRIBUTE_NAME=ATTRIBUTE_VALUE</code> query param. 114 * 115 * @param {String} urlString String that represents a URL to parse. 116 * @returns {object} Map containing all 'src' attributes. 117 */ 118 Analytics.srcAttrParamValues = function(urlString) {
119 var params = url.parseUrl(urlString).get('queryParams'); 120 121 var sourceAttributes = Object.create(null); 122 123 var keys = Array.from(params.keys()); 124 for (var i = 0; i < keys.length; i++) { 125 var property = keys[i]; 126 127 var components = property.split('.'); 128 if (isAnalyticsAttribute(components)) { 129 var source = components[1]; 130 var attributeName = components[2]; 131 132 // If this source object does not already exist we add it, and then put in the value 133 sourceAttributes[source] = sourceAttributes[source] || Object.create(null); 134 sourceAttributes[source][attributeName] = decodeURIComponent(params.get(property)[0]); 135 } 136 } 137 138 return sourceAttributes; 139 }; 140 141 /** 142 * Extract analytics data from a url 143 * @param urlString String that represents the url to parse 144 * @returns {Array.<{src:String,attr:Object}>} 145 */ 146 Analytics.extractAnalyticsData = function(urlString) { 147 var analyticsData = []; 148 149 var values = Analytics.srcParamValues(urlString); 150 var srcAttributes = Analytics.srcAttrParamValues(urlString); 151 152 for (var i = 0; i < values.length; i++) { 153 var source = values[i]; 154 var attributes = srcAttributes[source] || {}; 155 analyticsData.push({ src: source, attr: attributes }); 156 } 157 158 return analyticsData; 159 }; 160 161 /** 162 * Publishes a client-side analytics event. The event being published must adhere to our privacy policy, and also 163 * needs to be added to a plugin or product whitelist as per the instructions here: 164 * https://extranet.atlassian.com/display/AA/The+Developer%27s+Guide+to+Atlassian+Analytics 165 * @param name Name of the analytics event 166 * @param data Additional attributes 167 */ 168 Analytics.publish = function(name, data) { 169 AJS.trigger('analytics', { name: name, data: data || {} }); 170 }; 171 172 Analytics.init = function() { 173 var analyticsData = Analytics.extractAnalyticsData(document.URL); 174 175 var defaultData = { 176 userKey: Meta.get('remote-user-key'), 177 pageID: Meta.get('page-id'), 178 draftID: Meta.get('draft-id') 179 }; 180 181 if (analyticsData.length > 0) { 182 for (var i = 0; i < analyticsData.length; i++) { 183 // Events are published with a set of attributes, however individual 184 // attributes need to be added to the white list to be of any use 185 var analyticsEvent = analyticsData[i]; 186 var data = $.extend({}, defaultData, analyticsEvent.attr); 187 Analytics.publish('confluence.viewpage.src.' + analyticsEvent.src, data); 188 } 189 190 // Check if browser supports HTML5 replaceState() 191 if (window.history && window.history.replaceState) { 192 replaceStateAfterCleaningUpAnalyticsParameters(); 193 } 194 } else { 195 // CONFDEV-33536 - Confluence simplify journeys 196 // Adding generic event when src isn't available 197 Analytics.publish('confluence.viewpage.src.empty', defaultData); 198 } 199 }; 200 201 return Analytics; 202}); 203 204require('confluence/module-exporter').exportModuleAsGlobal('confluence/analytics-support', 'AJS.Confluence.Analytics', function(AnalyticsSupport) { 205 'use strict'; 206 207 var AJS = require('ajs'); 208 209 AJS.toInit(AnalyticsSupport.init); 210});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.