Przeglądaj źródła

[frontend] capture advanced events with ga and backend (#3080)

* [frontend] capture advanced events with ga and backend

- Removed old GA universal code and added new for GA4
- Added global lister for clicks on elements with attribute data-hue-analytics and data-hue-analytics-prio
- Extended utils/hueAnalytics.ts to work with the new setup
- Updated secure_content_security_policy  unsafe-eval and connect-src to work with new GA address
- Updated backend log style
- Added window.hueDebugAnalytics to view GA params in the console
- Added hueAnalytics to react test component
- Added unit tests and global mock for hueAnalytics
Bjorn Alm 3 lat temu
rodzic
commit
09f9945580

+ 0 - 1
.eslintrc.js

@@ -33,7 +33,6 @@ const hueGlobals = [
   'MediumEditor',
   'moment',
   'Role',
-  'trackOnGA',
   '__webpack_public_path__',
 
   // jest

+ 0 - 15
apps/hbase/example/documents/gethue.com.html

@@ -464,20 +464,5 @@
   });
 </script>
 
-<script type="text/javascript">
-  var _gaq = _gaq || [];
-  _gaq.push(['_setAccount', 'UA-37637545-1']);
-  _gaq.push(['_trackPageview']);
-
-  (function () {
-    var ga = document.createElement('script');
-    ga.type = 'text/javascript';
-    ga.async = true;
-    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
-    var s = document.getElementsByTagName('script')[0];
-    s.parentNode.insertBefore(ga, s);
-  })();
-</script>
-
 
 <a id="jHueScrollUpAnchor" href="javascript:void(0)"><i class="icon-chevron-up"></i></a></body></html>

+ 2 - 1
desktop/conf.dist/hue.ini

@@ -119,7 +119,7 @@ http_500_debug_mode=false
 ## secure_browser_xss_filter=true
 
 # X-Content-Type-Options: nosniff This is a HTTP response header feature that helps prevent attacks based on MIME-type confusion.
-## secure_content_security_policy="script-src 'self' 'unsafe-inline' 'unsafe-eval' *.google-analytics.com *.doubleclick.net data:;img-src 'self' *.google-analytics.com *.doubleclick.net http://*.tile.osm.org *.tile.osm.org *.gstatic.com data:;style-src 'self' 'unsafe-inline' fonts.googleapis.com;connect-src 'self';frame-src *;child-src 'self' data: *.vimeo.com;object-src 'none'"
+## secure_content_security_policy="script-src 'self' 'unsafe-inline' 'unsafe-eval' *.googletagmanager.com *.doubleclick.net data:;img-src 'self' *.doubleclick.net http://*.tile.osm.org *.tile.osm.org *.gstatic.com data:;style-src 'self' 'unsafe-inline' fonts.googleapis.com;connect-src 'self' *.google-analytics.com;frame-src *;child-src 'self' data: *.vimeo.com;object-src 'none'"
 
 # Strict-Transport-Security HTTP Strict Transport Security(HSTS) is a policy which is communicated by the server to the user agent via HTTP response header field name "Strict-Transport-Security". HSTS policy specifies a period of time during which the user agent(browser) should only access the server in a secure fashion(https).
 ## secure_ssl_redirect=False
@@ -154,6 +154,7 @@ http_500_debug_mode=false
 # Help improve Hue with anonymous usage analytics.
 # Use Google Analytics to see how many times an application or specific section of an application is used, nothing more.
 ## collect_usage=true
+## gtag_id='G-25K7599S1Q'
 
 # Tile layer server URL for the Leaflet map charts
 # Read more on http://leafletjs.com/reference.html#tilelayer

+ 2 - 1
desktop/conf/pseudo-distributed.ini.tmpl

@@ -124,7 +124,7 @@
   ## secure_browser_xss_filter=true
 
   # X-Content-Type-Options: nosniff This is a HTTP response header feature that helps prevent attacks based on MIME-type confusion.
-  ## secure_content_security_policy="script-src 'self' 'unsafe-inline' 'unsafe-eval' *.google-analytics.com *.doubleclick.net data:;img-src 'self' *.google-analytics.com *.doubleclick.net http://*.tile.osm.org *.tile.osm.org *.gstatic.com data:;style-src 'self' 'unsafe-inline' fonts.googleapis.com;connect-src 'self';frame-src *;child-src 'self' data: *.vimeo.com;object-src 'none'"
+  ## secure_content_security_policy="script-src 'self' 'unsafe-inline' 'unsafe-eval' *.googletagmanager.com *.doubleclick.net data:;img-src 'self' *.doubleclick.net http://*.tile.osm.org *.tile.osm.org *.gstatic.com data:;style-src 'self' 'unsafe-inline' fonts.googleapis.com;connect-src 'self' *.google-analytics.com;frame-src *;child-src 'self' data: *.vimeo.com;object-src 'none'"
 
   # Strict-Transport-Security HTTP Strict Transport Security(HSTS) is a policy which is communicated by the server to the user agent via HTTP response header field name "Strict-Transport-Security". HSTS policy specifies a period of time during which the user agent(browser) should only access the server in a secure fashion(https).
   ## secure_ssl_redirect=False
@@ -159,6 +159,7 @@
   # Help improve Hue with anonymous usage analytics.
   # Use Google Analytics to see how many times an application or specific section of an application is used, nothing more.
   ## collect_usage=true
+  ## gtag_id='G-25K7599S1Q'
 
   # Tile layer server URL for the Leaflet map charts
   # Read more on http://leafletjs.com/reference.html#tilelayer

+ 10 - 4
desktop/core/src/desktop/conf.py

@@ -322,10 +322,10 @@ SECURE_CONTENT_SECURITY_POLICY = Config(
   help=_('X-Content-Type-Options: nosniff. This is a HTTP response header feature that helps prevent attacks '
     'based on MIME-type confusion.'),
   type=str,
-  default="script-src 'self' 'unsafe-inline' 'unsafe-eval' *.google-analytics.com *.doubleclick.net data:;"+
-          "img-src 'self' *.google-analytics.com *.doubleclick.net http://*.tile.osm.org *.tile.osm.org *.gstatic.com data:;"+
+  default="script-src 'self' 'unsafe-inline' 'unsafe-eval' *.googletagmanager.com *.doubleclick.net data:;"+
+          "img-src 'self' *.doubleclick.net http://*.tile.osm.org *.tile.osm.org *.gstatic.com data:;"+
           "style-src 'self' 'unsafe-inline' fonts.googleapis.com;"+
-          "connect-src 'self';"+
+          "connect-src 'self' *.google-analytics.com;"+
           "frame-src *;"+
           "child-src 'self' data: *.vimeo.com;"+
           "object-src 'none'")
@@ -447,10 +447,16 @@ USER_ACCESS_HISTORY_SIZE = Config(
 COLLECT_USAGE = Config(
   key="collect_usage",
   help=_("Help improve Hue with anonymous usage analytics."
-         "Use Google Analytics to see how many times an application or specific section of an application is used, nothing more."),
+         "Use Google Analytics 4 to see how many times an application or specific section of an application is used, nothing more."),
   type=coerce_bool,
   default=True)
 
+GTAG_ID = Config(
+  key="gtag_id",
+  help=_("The gTag id used for anonymous analytics with Google Analytics 4."),
+  type=str,
+  default="G-25K7599S1Q")
+
 REST_RESPONSE_SIZE = Config(
   key="rest_response_size",
   help=_("Number of characters the rest api reponse calls to dump to the logs when debug is enabled.  Enter -1 for entire response."),

+ 2 - 3
desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.resultDownloadModal.js

@@ -21,6 +21,7 @@ import componentUtils from 'ko/components/componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 import UUID from 'utils/string/UUID';
+import hueAnalytics from 'utils/hueAnalytics';
 
 export const NAME = 'download-result-modal';
 
@@ -58,9 +59,7 @@ const TEMPLATE = `
 
 class DownloadResultModal {
   constructor(params, $downloadProgressModal) {
-    if (typeof trackOnGA == 'function') {
-      trackOnGA('notebook/download/' + params.format);
-    }
+    hueAnalytics.log('notebook', 'download' + params.format);
 
     const executable = params.executable;
     this.$downloadProgressModal = $downloadProgressModal;

+ 1 - 0
desktop/core/src/desktop/js/config/types.ts

@@ -81,6 +81,7 @@ export interface HueConfig extends GenericApiResponse {
   status: number;
   hue_config: {
     enable_sharing: boolean;
+    collect_usage: boolean;
   };
 }
 

+ 4 - 1
desktop/core/src/desktop/js/hue.js

@@ -43,7 +43,7 @@ import CancellableJqPromise from 'api/cancellableJqPromise';
 import { DOCUMENT_TYPE_I18n, DOCUMENT_TYPES } from 'doc/docSupport';
 import contextCatalog from 'catalog/contextCatalog';
 import dataCatalog from 'catalog/dataCatalog';
-import hueAnalytics from 'utils/hueAnalytics';
+import hueAnalytics, { setupGlobalListenersForAnalytics } from 'utils/hueAnalytics';
 import HueColors from 'utils/hueColors';
 import hueDebug from 'utils/hueDebug';
 import hueDrop from 'utils/hueDrop';
@@ -97,6 +97,7 @@ window.HdfsAutocompleter = HdfsAutocompleter;
 window.hueAnalytics = hueAnalytics;
 window.HueColors = HueColors;
 window.hueDebug = hueDebug;
+window.hueDebugAnalytics = false;
 window.HueDocument = HueDocument;
 window.hueDrop = hueDrop;
 window.HueFileEntry = HueFileEntry;
@@ -175,6 +176,8 @@ $(document).ready(async () => {
     }, 10);
   });
 
+  setupGlobalListenersForAnalytics();
+
   $('.page-content').jHueScrollUp();
 });
 

+ 1 - 0
desktop/core/src/desktop/js/jest/jest.init.js

@@ -122,3 +122,4 @@ process.on('unhandledRejection', err => {
 });
 
 jest.mock('../utils/i18nReact');
+jest.mock('../utils/hueAnalytics');

+ 2 - 0
desktop/core/src/desktop/js/reactComponents/ReactExampleGlobal/ReactExampleGlobal.tsx

@@ -3,6 +3,7 @@
 import React, { useState } from 'react';
 
 import { i18nReact } from '../../utils/i18nReact';
+import hueAnalytics from '../../utils/hueAnalytics';
 
 import './ReactExampleGlobal.scss';
 
@@ -35,6 +36,7 @@ const ReactExampleGlobal = ({
         onClick && onClick(e);
         setIsClicked(true);
         console.info(`ReactExampleGlobal clicked  ${version} ${myObj?.id}`);
+        hueAnalytics.log('test-area', 'button click', true);
       }}
     >
       ReactExampleGlobal - {children ?? t('Yes')}

+ 3 - 1
desktop/core/src/desktop/js/types/types.ts

@@ -48,6 +48,7 @@ export interface hueWindow {
   CACHEABLE_TTL?: { default?: number; sqlAnalyzer?: number };
   CLOSE_SESSIONS?: { [dialect: string]: boolean };
   CUSTOM_DASHBOARD_URL?: string;
+  DEV?: boolean;
   DISABLE_LOCAL_STORAGE?: boolean;
   ENABLE_PREDICT?: boolean;
   ENABLE_SQL_SYNTAX_CHECK?: boolean;
@@ -66,5 +67,6 @@ export interface hueWindow {
   WEB_SOCKETS_ENABLED?: boolean;
   WS_CHANNEL?: string;
   hueDebug?: HueDebug;
-  trackOnGA?(track: string): void;
+  hueDebugAnalytics?: boolean;
+  gtag?: (type: string, name: string, parameters: { action: string; version: string }) => void;
 }

+ 150 - 0
desktop/core/src/desktop/js/utils/hueAnalytics.test.ts

@@ -0,0 +1,150 @@
+import * as ApiUtils from '../api/utils';
+
+import * as HueConfig from '../config/hueConfig';
+
+// Since the our hueAnalytics is globally mocked in jest.init.js
+// we need to use requireActual when testing the actual hueAnalytics module
+const hueAnalyticsModule = jest.requireActual('./hueAnalytics');
+const { hueAnalytics, setupGlobalListenersForAnalytics } = hueAnalyticsModule;
+
+describe('hueAnalytics', () => {
+  let windowSpy;
+  const postSpy = jest.spyOn(ApiUtils, 'post').mockImplementation();
+  const getLastKnownConfigMock = jest.spyOn(HueConfig, 'getLastKnownConfig');
+
+  beforeEach(() => {
+    windowSpy = jest.spyOn(window, 'window', 'get');
+  });
+
+  afterEach(() => {
+    windowSpy.mockRestore();
+    postSpy.mockReset();
+    getLastKnownConfigMock.mockReset();
+  });
+
+  it('should log to console if dev mode, collect_usage and hueDebugAnalytics are true', () => {
+    const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
+    getLastKnownConfigMock.mockImplementation(() => {
+      return { hue_config: { collect_usage: true } };
+    });
+
+    windowSpy.mockImplementation(() => ({
+      DEV: true,
+      hueDebugAnalytics: true
+    }));
+
+    hueAnalytics.log('area1', 'action1');
+
+    expect(consoleSpy).toHaveBeenCalledWith(
+      'Analytics debug:',
+      'event',
+      'area1',
+      expect.objectContaining({
+        action: 'action1'
+      })
+    );
+  });
+
+  it('should always log to backend if prioritised', () => {
+    const isPrio = true;
+
+    windowSpy.mockImplementation(() => ({}));
+
+    hueAnalytics.log('area1', 'action1', isPrio);
+
+    expect(postSpy).toHaveBeenCalledWith('/desktop/log_analytics', {
+      action: 'action1',
+      area: 'area1'
+    });
+  });
+
+  it('should log to google analytics if COLLECT_USAGE is true but not in dev mode', () => {
+    const gtagSpy = jest.fn();
+    getLastKnownConfigMock.mockImplementation(() => {
+      return { hue_config: { collect_usage: true } };
+    });
+
+    windowSpy.mockImplementation(() => ({
+      DEV: false,
+      gtag: gtagSpy
+    }));
+
+    hueAnalytics.log('area1', 'action1');
+
+    expect(gtagSpy).toHaveBeenCalledWith(
+      'event',
+      'area1',
+      expect.objectContaining({
+        action: 'action1'
+      })
+    );
+  });
+
+  it('should always log to backend when convert is used', () => {
+    windowSpy.mockImplementation(() => ({}));
+
+    hueAnalytics.convert('area1', 'action1');
+
+    expect(postSpy).toHaveBeenCalledWith('/desktop/log_analytics', {
+      action: 'action1',
+      area: 'area1'
+    });
+  });
+
+  it('should use global listener to log clicks on element with attribute "data-hue-analytics"', () => {
+    const gtagSpy = jest.fn();
+    getLastKnownConfigMock.mockImplementation(() => {
+      return { hue_config: { collect_usage: true } };
+    });
+    windowSpy.mockImplementation(() => ({
+      DEV: false,
+      gtag: gtagSpy
+    }));
+
+    let globallyAddedEventListener!: EventListener;
+    jest.spyOn(document, 'addEventListener').mockImplementationOnce((eventType, listenerToTest) => {
+      globallyAddedEventListener = listenerToTest as EventListener;
+    });
+
+    setupGlobalListenersForAnalytics();
+
+    const button = document.createElement('button');
+    button.dataset.hueAnalytics = 'test-area:testbutton click';
+    const myEvent = new Event('click');
+    Object.defineProperty(myEvent, 'target', { writable: false, value: button });
+
+    globallyAddedEventListener(myEvent);
+
+    expect(gtagSpy).toHaveBeenCalledWith(
+      'event',
+      'test-area',
+      expect.objectContaining({
+        action: 'testbutton click'
+      })
+    );
+  });
+
+  it('should use global listener and backend API to log clicks on element with attribute "data-hue-analytics-prio"', () => {
+    windowSpy.mockImplementation(() => ({}));
+
+    let globallyAddedEventListener!: EventListener;
+    jest.spyOn(document, 'addEventListener').mockImplementationOnce((eventType, listenerToTest) => {
+      globallyAddedEventListener = listenerToTest as EventListener;
+    });
+
+    setupGlobalListenersForAnalytics();
+
+    const button = document.createElement('button');
+    button.dataset.hueAnalytics = 'test-area:testbutton click';
+    button.dataset.hueAnalyticsPrio = 'true';
+    const myEvent = new Event('click');
+    Object.defineProperty(myEvent, 'target', { writable: false, value: button });
+
+    globallyAddedEventListener(myEvent);
+
+    expect(postSpy).toHaveBeenCalledWith('/desktop/log_analytics', {
+      action: 'testbutton click',
+      area: 'test-area'
+    });
+  });
+});

+ 61 - 6
desktop/core/src/desktop/js/utils/hueAnalytics.ts

@@ -14,18 +14,73 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import { getLastKnownConfig } from '../config/hueConfig';
 import { post } from '../api/utils';
 import { hueWindow } from '../types/types';
 
-export default {
-  log(app: string, page: string): void {
-    if (typeof (<hueWindow>window).trackOnGA == 'function') {
-      (<hueWindow>window).trackOnGA!(app + '/' + page);
+// HOW TO TRACK EVENTS
+// a) Calling the hueAnalytics.log or hueAnalytics.convert manually
+// b) Using the attribute "data-hue-analytics" on a clickable HTML element
+// and set a string value containing the area followed by a colon and the action e.g.
+// <a href="#" data-hue-analytics="metastore:remove stuff">Remove</a>
+//
+// Setting the additional attribute data-hue-analytics-prio="true" will cause the interaction
+// to be logged in the Hue backend, independent of the collect_usage setting. This should be used
+// with caution to prevent flooding the log.
+
+const formatGaData = (
+  area: string,
+  action: string
+): { type: string; name: string; params: { action: string; version: string } } => ({
+  type: 'event',
+  name: area,
+  params: {
+    action,
+    version: (<hueWindow>window).HUE_VERSION || ''
+  }
+});
+
+export const hueAnalytics = {
+  log(area: string, action: string, isPrioritised?: boolean): void {
+    const config = getLastKnownConfig();
+    const typedWindow = <hueWindow>window;
+    if (isPrioritised) {
+      this.convert(area, action);
+    }
+
+    if (config?.hue_config?.collect_usage) {
+      const { type, name, params } = formatGaData(area, action);
+
+      // Quick debug mode to check that the analytics logging is working when developing new features
+      // Turn on by typing "window.hueDebugAnalytics = true;" in the browser JS console or setting
+      // "window.hueDebugAnalytics = true" in hue.js;
+      if (typedWindow.DEV && typedWindow.hueDebugAnalytics) {
+        console.info('Analytics debug:', type, name, params);
+      }
+
+      if (!typedWindow.DEV) {
+        typedWindow.gtag?.(type, name, params);
+      }
     }
   },
-  convert(app: string, page: string): void {
+  convert(area: string, action: string): void {
     post('/desktop/log_analytics', {
-      page: app + '/' + page
+      area,
+      action
     });
   }
 };
+
+export const setupGlobalListenersForAnalytics = (): void => {
+  document.addEventListener('click', event => {
+    const eventTarget = event?.target as HTMLElement;
+    const analyticsDataAttribute = eventTarget.dataset?.hueAnalytics;
+    if (analyticsDataAttribute) {
+      const [area, action] = analyticsDataAttribute.split(':');
+      const isPrioritised = !!eventTarget.dataset.hueAnalyticsPrio;
+      return hueAnalytics.log(area, action, isPrioritised);
+    }
+  });
+};
+
+export default hueAnalytics;

+ 3 - 2
desktop/core/src/desktop/models.py

@@ -51,7 +51,7 @@ from useradmin.organization import _fitered_queryset
 from desktop import appmanager
 from desktop.auth.backend import is_admin
 from desktop.conf import get_clusters, IS_MULTICLUSTER_ONLY, ENABLE_ORGANIZATIONS, ENABLE_PROMETHEUS, \
-    has_connectors, TASK_SERVER, APP_BLACKLIST, ENABLE_SHARING, ENABLE_CONNECTORS, ENABLE_UNIFIED_ANALYTICS, RAZ
+    has_connectors, TASK_SERVER, APP_BLACKLIST, COLLECT_USAGE, ENABLE_SHARING, ENABLE_CONNECTORS, ENABLE_UNIFIED_ANALYTICS, RAZ
 from desktop.lib import fsmanager
 from desktop.lib.connectors.api import _get_installed_connectors
 from desktop.lib.connectors.models import Connector
@@ -1785,7 +1785,8 @@ class ClusterConfig(object):
       'cluster_type': self.cluster_type,
       'has_computes': self.cluster_type in ('altus', 'snowball'), # or any grouped engine connectors
       'hue_config': {
-        'enable_sharing': ENABLE_SHARING.get()
+        'enable_sharing': ENABLE_SHARING.get(),
+        'collect_usage': COLLECT_USAGE.get()
       }
     }
 

+ 0 - 30
desktop/core/src/desktop/templates/common_footer_m.mako

@@ -57,36 +57,6 @@ else:
     nv.log = function() {};
   }
 
-  %if collect_usage:
-
-    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-    })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
-
-    ga('create', 'UA-40351920-1', 'auto');
-    ga('set', 'referrer', 'http://gethue.com'); // we force the referrer to prevent leaking sensitive information
-
-    // We collect only 2 path levels: not hostname, no IDs, no anchors...
-    var _pathName = location.pathname;
-    var _splits = _pathName.substr(1).split("/");
-    _pathName = _splits[0] + (_splits.length > 1 && $.trim(_splits[1]) != "" ? "/" + _splits[1] : "");
-
-    ga('send', 'pageview', {
-      'page': '/remote/${ version }/' + _pathName
-    });
-
-    function trackOnGA(path) {
-      if (typeof ga != "undefined" && ga != null) {
-        ga('set', 'referrer', 'http://gethue.com'); // we force the referrer to prevent leaking sensitive information
-        ga('send', 'pageview', {
-          'page': '/remote/${ version }/' + path
-        });
-      }
-    }
-
-  %endif
-
 </script>
   </body>
 </html>

+ 0 - 28
desktop/core/src/desktop/templates/common_header_footer_components.mako

@@ -579,33 +579,5 @@ else:
     };
   }
 
-  % if collect_usage:
-    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-    })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
-
-    ga('create', 'UA-40351920-1', 'auto');
-    ga('set', 'referrer', 'http://gethue.com'); // we force the referrer to prevent leaking sensitive information
-
-    // We collect only 2 path levels: not hostname, no IDs, no anchors...
-    var _pathName = location.pathname;
-    var _splits = _pathName.substr(1).split("/");
-    _pathName = _splits[0] + (_splits.length > 1 && $.trim(_splits[1]) != "" ? "/" + _splits[1] : "");
-
-    ga('send', 'pageview', {
-      'page': '/remote/${ version }/4/' + _pathName
-    });
-
-    function trackOnGA(path) {
-      if (typeof ga != "undefined" && ga != null) {
-        ga('set', 'referrer', 'http://gethue.com'); // we force the referrer to prevent leaking sensitive information
-        ga('send', 'pageview', {
-          'page': '/remote/${ version }/4/' + path
-        });
-      }
-    }
-  % endif
-
 </script>
 </%def>

+ 1 - 3
desktop/core/src/desktop/templates/common_notebook_ko_components.mako

@@ -486,9 +486,7 @@ else:
       }
 
       DownloadResultsViewModel.prototype.download = function (format) {
-        if (typeof trackOnGA == 'function') {
-          trackOnGA('notebook/download/' + format);
-        }
+        window.hueAnalytics.log('notebook', 'download' + format);
 
         var self = this;
         $.cookie('download-' + self.snippet.id(), null, { expires: -1, path: '/' })

+ 19 - 0
desktop/core/src/desktop/templates/hue.mako

@@ -45,6 +45,25 @@
 <!DOCTYPE html>
 <html lang="en" dir="ltr">
 <head>
+
+  % if conf.COLLECT_USAGE.get():
+    <!-- Google tag (gtag.js) -->
+    <script async src="https://www.googletagmanager.com/gtag/js?id=${conf.GTAG_ID.get()}"></script>
+    <script>
+      window.dataLayer = window.dataLayer || [];
+      function gtag(){dataLayer.push(arguments);}
+      gtag('js', new Date());
+
+      gtag('config', '${ conf.GTAG_ID.get()}', { 
+        // Prevent GA from accidentally passing client meta data present in urls
+        send_page_view: false, 
+        page_location: 'redacted',
+        page_referrer: 'redacted',
+        allow_google_signals: false
+        });
+    </script>
+  % endif
+
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta charset="utf-8">
   <title>Hue</title>

+ 1 - 1
desktop/core/src/desktop/views.py

@@ -220,7 +220,7 @@ def log_js_error(request):
 
 def log_analytics(request):
   ai = AccessInfo(request)
-  ai.log(level=logging.INFO, msg='PAGE: ' + request.POST.get('page'))
+  ai.log(level=logging.INFO, msg='UI INTERACTION: ' + request.POST.get('area') + ' > ' + request.POST.get('action'))
 
   return JsonResponse({'status': 0})