From 985e49bdb5e22fa197ba267aa41d7a8ba7e50550 Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Mon, 4 Jul 2022 11:59:27 +0000 Subject: [PATCH] [FIX] website: remove Google Analytics dashboard (deprecated) ==== Short version ==== Google is deprecating Universal Analytics in July 2023 and Google Sign-In in March 2023. Google Analytics Embed API is based on Sign-In, meaning it won't work anymore. It actually already doesn't work anymore for accounts created somewhere after mid-2020 apparently. There is no plan for now for Google to allow Analytics 4 dashboard to be embed in external website. We therefore can't do anything to keep the Google Analytics dashboard in Odoo. In previous stable version, it was kept but is displaying a warning about it (as until mid 2023 old accounts can still embed it). All this is about the embed dashboard, not the tracking in itself for which Odoo is already adapted in Odoo 15.0 for Analytics 4. ==== Detailed version (following short version, read it first) ==== - Universal Analytics EOL July 2023, see [1]. - It will be replaced by Analytics 4 for which Odoo is already ready and actually using it since version 15.0 with [2]. - Google Sign-In EOL March 2023, see [3]. Analytics Embed API was based on it, it won't work anymore. - There is no plan (for now) for Google to allow Analytics 4 to be able to be embed in external websites. They seem to just have dropped the "feature". This was confirmed by Google here [4] and indirectly here [5] in the DOC: `Note: This API does not support Google Analytics 4 (GA4) properties` - While the EOL is planed for 2023, the dashboard integration is already not working anymore for new accounts. - Old projects/keys/accounts can still embed their analytics dashboard. The threshold seems to be somewhere mid-2020, according to [6]. It seems to be accurate as my own key from 2018 still works, while my keys from 2021 do not. ==== Fix ==== - In stable, warn user about it in their Odoo Analytics dashboard (this PR) and also add a warning about that on the doc. - In master, simply drop the whole google analytics dashboard integration and remove the doc about it, see [7]. ==== Useful links ==== [1]: https://support.google.com/analytics/answer/11583528?hl=en [2]: https://github.com/odoo/odoo/commit/78bc86cbeccfc5df16218aee2b0d7c501e5c05b5 [3]: https://developers.googleblog.com/2022/03/gis-jsweb-authz-migration.html [4]: https://issuetracker.google.com/issues/233738709?pli=1 [5]: https://developers.google.com/analytics/devguides/reporting/embed/v1 [6]: https://support.google.com/analytics/answer/11583832 [7]: https://www.odoo.com/documentation/15.0/applications/websites/website/optimize/google_analytics_dashboard.html Finally, note that it means that from July 2023 to Octobre 2023, while Odoo 14.0 is still supported, Google Analytics won't work anymore in that version as it will still be designed for Universal Analytics and not Analytics 4. opw-2710910 opw-2855405 opw-2881515 opw-2892370 task-2790245 task-2820890 closes odoo/odoo#96280 X-original-commit: d065595f77790fb5ab9480f6de5b88549352324b Related: odoo/enterprise#29666 Related: odoo/upgrade#3698 Related: odoo/documentation#2499 Signed-off-by: Romain Derie (rde) --- addons/website/controllers/backend.py | 36 +- addons/website/models/res_config_settings.py | 25 -- addons/website/models/website.py | 2 - .../static/src/js/backend/dashboard.js | 358 ------------------ .../static/src/scss/website.backend.scss | 59 --- .../static/src/xml/website.backend.xml | 56 +-- .../website/static/tests/dashboard_tests.js | 1 - .../views/res_config_settings_views.xml | 28 -- 8 files changed, 14 insertions(+), 551 deletions(-) diff --git a/addons/website/controllers/backend.py b/addons/website/controllers/backend.py index c5cf4822c06..34245145c5b 100644 --- a/addons/website/controllers/backend.py +++ b/addons/website/controllers/backend.py @@ -3,7 +3,6 @@ from odoo import http from odoo.http import request -from odoo.tools.translate import _ class WebsiteBackend(http.Controller): @@ -19,9 +18,7 @@ class WebsiteBackend(http.Controller): 'website_designer': has_group_designer }, 'currency': request.env.company.currency_id.id, - 'dashboards': { - 'visits': {}, - } + 'dashboards': {} } current_website = website_id and Website.browse(website_id) or Website.get_current_website() @@ -33,40 +30,9 @@ class WebsiteBackend(http.Controller): website['selected'] = True if has_group_designer: - if current_website.google_management_client_id and current_website.google_analytics_key: - dashboard_data['dashboards']['visits'] = dict( - ga_client_id=current_website.google_management_client_id or '', - ga_analytics_key=current_website.google_analytics_key or '', - ) dashboard_data['dashboards']['plausible_share_url'] = current_website._get_plausible_share_url() return dashboard_data @http.route('/website/iframefallback', type="http", auth='user', website=True) def get_iframe_fallback(self): return request.render('website.iframefallback') - - @http.route('/website/dashboard/set_ga_data', type='json', auth='user') - def website_set_ga_data(self, website_id, ga_client_id, ga_analytics_key): - if not request.env.user.has_group('base.group_system'): - return { - 'error': { - 'title': _('Access Error'), - 'message': _('You do not have sufficient rights to perform that action.'), - } - } - if not ga_analytics_key or not ga_client_id.endswith('.apps.googleusercontent.com'): - return { - 'error': { - 'title': _('Incorrect Client ID / Key'), - 'message': _('The Google Analytics Client ID or Key you entered seems incorrect.'), - } - } - Website = request.env['website'] - current_website = website_id and Website.browse(website_id) or Website.get_current_website() - - request.env['res.config.settings'].create({ - 'google_management_client_id': ga_client_id, - 'google_analytics_key': ga_analytics_key, - 'website_id': current_website.id, - }).execute() - return True diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py index 0f510bffe8f..1b4bed33d55 100644 --- a/addons/website/models/res_config_settings.py +++ b/addons/website/models/res_config_settings.py @@ -64,14 +64,6 @@ class ResConfigSettings(models.TransientModel): 'Google Analytics Key', related='website_id.google_analytics_key', readonly=False) - google_management_client_id = fields.Char( - 'Google Client ID', - related='website_id.google_management_client_id', - readonly=False) - google_management_client_secret = fields.Char( - 'Google Client Secret', - related='website_id.google_management_client_secret', - readonly=False) google_search_console = fields.Char( 'Google Search Console', related='website_id.google_search_console', @@ -115,10 +107,6 @@ class ResConfigSettings(models.TransientModel): "Google Analytics", compute='_compute_has_google_analytics', inverse='_inverse_has_google_analytics') - has_google_analytics_dashboard = fields.Boolean( - "Google Analytics Dashboard", - compute='_compute_has_google_analytics_dashboard', - inverse='_inverse_has_google_analytics_dashboard') has_google_search_console = fields.Boolean( "Console Google Search", compute='_compute_has_google_search_console', @@ -187,21 +175,8 @@ class ResConfigSettings(models.TransientModel): for config in self: if config.has_google_analytics: continue - config.has_google_analytics_dashboard = False config.google_analytics_key = False - @api.depends('website_id') - def _compute_has_google_analytics_dashboard(self): - for config in self: - config.has_google_analytics_dashboard = bool(config.google_management_client_id) - - def _inverse_has_google_analytics_dashboard(self): - for config in self: - if config.has_google_analytics_dashboard: - continue - config.google_management_client_id = False - config.google_management_client_secret = False - @api.depends('website_id') def _compute_has_google_search_console(self): for config in self: diff --git a/addons/website/models/website.py b/addons/website/models/website.py index f3267065e4e..85808bd844f 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -108,8 +108,6 @@ class Website(models.Model): has_social_default_image = fields.Boolean(compute='_compute_has_social_default_image', store=True) google_analytics_key = fields.Char('Google Analytics Key') - google_management_client_id = fields.Char('Google Client ID') - google_management_client_secret = fields.Char('Google Client Secret') google_search_console = fields.Char(help='Google key, or Enable to access first reply') google_maps_api_key = fields.Char('Google Maps API Key') diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js index 8610a1374fa..b9b51aad3f1 100644 --- a/addons/website/static/src/js/backend/dashboard.js +++ b/addons/website/static/src/js/backend/dashboard.js @@ -1,18 +1,15 @@ -/* global google, gapi */ odoo.define('website.backend.dashboard', function (require) { 'use strict'; var AbstractAction = require('web.AbstractAction'); var ajax = require('web.ajax'); var core = require('web.core'); -var Dialog = require('web.Dialog'); var field_utils = require('web.field_utils'); var pyUtils = require('web.py_utils'); var session = require('web.session'); var time = require('web.time'); var web_client = require('web.web_client'); -var _t = core._t; var QWeb = core.qweb; var COLORS = ["#1f77b4", "#aec7e8"]; @@ -37,7 +34,6 @@ var Dashboard = AbstractAction.extend({ '/web/static/lib/Chart/Chart.js', ], events: { - 'click .js_link_analytics_settings': 'on_link_analytics_settings', 'click .o_dashboard_action': 'on_dashboard_action', 'click .o_dashboard_action_form': 'on_dashboard_action_form', }, @@ -105,59 +101,12 @@ var Dashboard = AbstractAction.extend({ return prom; }, - on_link_analytics_settings: function(ev) { - ev.preventDefault(); - - var self = this; - var dialog = new Dialog(this, { - size: 'medium', - title: _t('Connect Google Analytics'), - $content: QWeb.render('website.ga_dialog_content', { - ga_key: this.dashboards_data.visits.ga_client_id, - ga_analytics_key: this.dashboards_data.visits.ga_analytics_key, - }), - buttons: [ - { - text: _t("Save"), - classes: 'btn-primary', - close: true, - click: function() { - var ga_client_id = dialog.$el.find('input[name="ga_client_id"]').val(); - var ga_analytics_key = dialog.$el.find('input[name="ga_analytics_key"]').val(); - self.on_save_ga_client_id(ga_client_id, ga_analytics_key); - }, - }, - { - text: _t("Cancel"), - close: true, - }, - ], - }).open(); - }, - on_go_to_website: function (ev) { ev.preventDefault(); var website = _.findWhere(this.websites, {selected: true}); window.location.replace(`/web#action=website.website_preview&website_id=${website.id}`); }, - on_save_ga_client_id: function(ga_client_id, ga_analytics_key) { - var self = this; - return this._rpc({ - route: '/website/dashboard/set_ga_data', - params: { - 'website_id': self.website_id, - 'ga_client_id': ga_client_id, - 'ga_analytics_key': ga_analytics_key, - }, - }).then(function (result) { - if (result.error) { - self.displayNotification({ title: result.error.title, message: result.error.message, type: 'danger' }); - return; - } - self.on_date_range_button('week'); - }); - }, render_dashboards: function() { var self = this; @@ -268,46 +217,9 @@ var Dashboard = AbstractAction.extend({ self.render_graph('.o_graph_' + e.name, self.dashboards_data[e.name].graph, chart_id); } }); - this.render_graph_analytics(this.dashboards_data.visits.ga_client_id); } }, - render_graph_analytics: function(client_id) { - if (!this.dashboards_data.visits || !this.dashboards_data.visits.ga_client_id) { - return; - } - - this.load_analytics_api(); - - var $analytics_components = this.$('.js_analytics_components'); - this.addLoader($analytics_components); - - var self = this; - gapi.analytics.ready(function() { - - $analytics_components.empty(); - // 1. Authorize component - var $analytics_auth = $('
').addClass('col-lg-12'); - window.onOriginError = function () { - $analytics_components.find('.js_unauthorized_message').remove(); - self.display_unauthorized_message($analytics_components, 'not_initialized'); - }; - gapi.analytics.auth.authorize({ - container: $analytics_auth[0], - clientid: client_id - }); - - $analytics_auth.appendTo($analytics_components); - - self.handle_analytics_auth($analytics_components); - gapi.analytics.auth.on('signIn', function() { - delete window.onOriginError; - self.handle_analytics_auth($analytics_components); - }); - - }); - }, - on_date_range_button: function(date_range) { if (date_range === 'week') { this.date_range = 'week'; @@ -417,277 +329,7 @@ var Dashboard = AbstractAction.extend({ this.controlPanelProps.cp_content = { $searchview, $buttons }; }, - // Loads Analytics API - load_analytics_api: function() { - var self = this; - if (!("gapi" in window)) { - (function(w,d,s,g,js,fjs){ - g=w.gapi||(w.gapi={});g.analytics={q:[],ready:function(cb){this.q.push(cb);}}; - js=d.createElement(s);fjs=d.getElementsByTagName(s)[0]; - js.src='https://apis.google.com/js/platform.js'; - fjs.parentNode.insertBefore(js,fjs);js.onload=function(){g.load('analytics');}; - }(window,document,'script')); - gapi.analytics.ready(function() { - self.analytics_create_components(); - }); - } - }, - - handle_analytics_auth: function($analytics_components) { - $analytics_components.find('.js_unauthorized_message').remove(); - - // Check if the user is authenticated and has the right to make API calls - if (!gapi.analytics.auth.getAuthResponse()) { - this.display_unauthorized_message($analytics_components, 'not_connected'); - } else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics') === -1) { - this.display_unauthorized_message($analytics_components, 'no_right'); - } else { - this.make_analytics_calls($analytics_components); - } - }, - - display_unauthorized_message: function($analytics_components, reason) { - $analytics_components.prepend($(QWeb.render('website.unauthorized_analytics', {reason: reason}))); - }, - - make_analytics_calls: function($analytics_components) { - // 2. ActiveUsers component - var $analytics_users = $('
'); - var activeUsers = new gapi.analytics.ext.ActiveUsers({ - container: $analytics_users[0], - pollingInterval: 10, - }); - $analytics_users.appendTo($analytics_components); - - // 3. View Selector - var $analytics_view_selector = $('
').addClass('col-lg-12 o_properties_selection'); - var viewSelector = new gapi.analytics.ViewSelector({ - container: $analytics_view_selector[0], - }); - viewSelector.execute(); - $analytics_view_selector.appendTo($analytics_components); - - // 4. Chart graph - var start_date = '7daysAgo'; - if (this.date_range === 'month') { - start_date = '30daysAgo'; - } else if (this.date_range === 'year') { - start_date = '365daysAgo'; - } - var $analytics_chart_2 = $('
').addClass('col-lg-6 col-12'); - var breakdownChart = new gapi.analytics.googleCharts.DataChart({ - query: { - 'dimensions': 'ga:date', - 'metrics': 'ga:sessions', - 'start-date': start_date, - 'end-date': 'yesterday' - }, - chart: { - type: 'LINE', - container: $analytics_chart_2[0], - options: { - title: 'All', - width: '100%', - tooltip: {isHtml: true}, - } - } - }); - $analytics_chart_2.appendTo($analytics_components); - - // 5. Chart table - var $analytics_chart_1 = $('
').addClass('col-lg-6 col-12'); - var mainChart = new gapi.analytics.googleCharts.DataChart({ - query: { - 'dimensions': 'ga:medium', - 'metrics': 'ga:sessions', - 'sort': '-ga:sessions', - 'max-results': '6' - }, - chart: { - type: 'TABLE', - container: $analytics_chart_1[0], - options: { - width: '100%' - } - } - }); - $analytics_chart_1.appendTo($analytics_components); - - // Events handling & animations - - var table_row_listener; - - viewSelector.on('change', function(ids) { - var options = {query: {ids: ids}}; - activeUsers.set({ids: ids}).execute(); - mainChart.set(options).execute(); - breakdownChart.set(options).execute(); - - if (table_row_listener) { google.visualization.events.removeListener(table_row_listener); } - }); - - mainChart.on('success', function(response) { - var chart = response.chart; - var dataTable = response.dataTable; - - table_row_listener = google.visualization.events.addListener(chart, 'select', function() { - var options; - if (chart.getSelection().length) { - var row = chart.getSelection()[0].row; - var medium = dataTable.getValue(row, 0); - options = { - query: { - filters: 'ga:medium==' + medium, - }, - chart: { - options: { - title: medium, - } - } - }; - } else { - options = { - chart: { - options: { - title: 'All', - } - } - }; - delete breakdownChart.get().query.filters; - } - breakdownChart.set(options).execute(); - }); - }); - - // Add CSS animation to visually show the when users come and go. - activeUsers.once('success', function() { - var element = this.container.firstChild; - var timeout; - - this.on('change', function(data) { - element = this.container.firstChild; - var animationClass = data.delta > 0 ? 'is-increasing' : 'is-decreasing'; - element.className += (' ' + animationClass); - - clearTimeout(timeout); - timeout = setTimeout(function() { - element.className = element.className.replace(/ is-(increasing|decreasing)/g, ''); - }, 3000); - }); - }); - }, - - /* - * Credits to https://github.com/googleanalytics/ga-dev-tools - * This is the Active Users component that polls - * the number of active users on Analytics each 5 secs - */ - analytics_create_components: function() { - - gapi.analytics.createComponent('ActiveUsers', { - - initialize: function() { - this.activeUsers = 0; - gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this)); - }, - - execute: function() { - // Stop any polling currently going on. - if (this.polling_) { - this.stop(); - } - - this.render_(); - - // Wait until the user is authorized. - if (gapi.analytics.auth.isAuthorized()) { - this.pollActiveUsers_(); - } else { - gapi.analytics.auth.once('signIn', this.pollActiveUsers_.bind(this)); - } - }, - - stop: function() { - clearTimeout(this.timeout_); - this.polling_ = false; - this.emit('stop', {activeUsers: this.activeUsers}); - }, - - render_: function() { - var opts = this.get(); - - // Render the component inside the container. - this.container = typeof opts.container === 'string' ? - document.getElementById(opts.container) : opts.container; - - this.container.innerHTML = opts.template || this.template; - this.container.querySelector('b').innerHTML = this.activeUsers; - }, - - pollActiveUsers_: function() { - var options = this.get(); - var pollingInterval = (options.pollingInterval || 5) * 1000; - - if (isNaN(pollingInterval) || pollingInterval < 5000) { - throw new Error('Frequency must be 5 seconds or more.'); - } - - this.polling_ = true; - gapi.client.analytics.data.realtime - .get({ids:options.ids, metrics:'rt:activeUsers'}) - .then(function(response) { - var result = response.result; - var newValue = result.totalResults ? +result.rows[0][0] : 0; - var oldValue = this.activeUsers; - - this.emit('success', {activeUsers: this.activeUsers}); - - if (newValue !== oldValue) { - this.activeUsers = newValue; - this.onChange_(newValue - oldValue); - } - - if (this.polling_) { - this.timeout_ = setTimeout(this.pollActiveUsers_.bind(this), pollingInterval); - } - }.bind(this)); - }, - - onChange_: function(delta) { - var valueContainer = this.container.querySelector('b'); - if (valueContainer) { valueContainer.innerHTML = this.activeUsers; } - - this.emit('change', {activeUsers: this.activeUsers, delta: delta}); - if (delta > 0) { - this.emit('increase', {activeUsers: this.activeUsers, delta: delta}); - } else { - this.emit('decrease', {activeUsers: this.activeUsers, delta: delta}); - } - }, - - handleSignOut_: function() { - this.stop(); - gapi.analytics.auth.once('signIn', this.handleSignIn_.bind(this)); - }, - - handleSignIn_: function() { - this.pollActiveUsers_(); - gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this)); - }, - - template: - '
' + - 'Active Users: ' + - '
' - - }); - }, - // Utility functions - addLoader: function(selector) { - var loader = ''; - selector.html("
" + loader + "
"); - }, getValue: function(d) { return d[1]; }, format_number: function(value, type, digits, symbol) { if (type === 'currency') { diff --git a/addons/website/static/src/scss/website.backend.scss b/addons/website/static/src/scss/website.backend.scss index 55db5025d0e..86bc5b2b953 100644 --- a/addons/website/static/src/scss/website.backend.scss +++ b/addons/website/static/src/scss/website.backend.scss @@ -132,10 +132,6 @@ .o_buttons { position: relative; - - > button { - display: block; - } } .o_layer { @@ -147,61 +143,6 @@ height: 100%; } } - - .o_properties_selection { - - @include media-breakpoint-up(lg) { - display: flex; - } - - margin-top: 10px; - margin-bottom: 10px; - } - - .gapi-analytics-auth-styles-signinbutton { - cursor: pointer; - } - - .ActiveUsers { - position: relative; - float: right; - right: 10px; - border: 1px solid map-get($grays, '200'); - font-weight: 300; - white-space: nowrap; - padding: .5em 1.5em; - margin: .5em; - text-transform: uppercase; - } - - .ActiveUsers-value { - font-weight: 300; - margin-right: -.25em; - } - - .ActiveUsers.is-increasing { - animation: increase 3s; - } - - .ActiveUsers.is-decreasing { - animation: decrease 3s; - } - - @keyframes increase { - 10% { - background-color: lighten($o-brand-primary, 30%); - border-color: $o-brand-primary; - color: $o-brand-primary; - } - } - - @keyframes decrease { - 10% { - background-color: lighten($o-brand-primary, 30%); - border-color: $o-brand-primary; - color: $o-brand-primary; - } - } } tr.o_product_template { diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml index b5453c35409..aeff11e7e0b 100644 --- a/addons/website/static/src/xml/website.backend.xml +++ b/addons/website/static/src/xml/website.backend.xml @@ -30,64 +30,34 @@
-
- + +
-

Visits

-
-
- Edit my Analytics Client ID +

Analytics

+
+
+