From 09b8a61f65e5a07e4fdec90ff996396b4512a40e Mon Sep 17 00:00:00 2001 From: Justin DiRose Date: Fri, 6 Nov 2020 14:15:36 -0600 Subject: [PATCH] FEATURE: Add Google Universal Analytics v4 as an option (#11123) Per Google, sites are encouraged to upgrade from Universal Analytics v3 `analytics.js` to v4 `gtag.js` for Google Analytics tracking. We're giving admins the option to stay on the v3 API or migrate to v4. Admins can change the implementation they're using via the `ga_version` site setting. Eventually Google will deprecate v3, but our implementation gives admins the choice on what to use for now. We chose this implementation to make the change less error prone, as many site admins are using custom events via the v3 UA API. With the site stetting defaulted to `v3_analytics`, site analytics won't break until the admin is ready to make the migration. Additionally, in the v4 implementation, we do not enable automatic pageview tracking (on by default in the v4 API). Instead we rely on Discourse's page change API to report pageviews on transition to avoid double-tracking. --- .../app/initializers/page-tracking.js | 21 ++++++++++++--- ...cs.js => google-universal-analytics-v3.js} | 0 .../google-universal-analytics-v4.js | 26 +++++++++++++++++++ .../_google_universal_analytics.html.erb | 7 ++++- config/application.rb | 3 ++- config/locales/server.en.yml | 7 ++--- config/site_settings.yml | 6 +++++ lib/content_security_policy/default.rb | 2 ++ lib/discourse_js_processor.rb | 3 ++- spec/lib/content_security_policy_spec.rb | 24 ++++++++++++++--- 10 files changed, 87 insertions(+), 12 deletions(-) rename app/assets/javascripts/{google-universal-analytics.js => google-universal-analytics-v3.js} (100%) create mode 100644 app/assets/javascripts/google-universal-analytics-v4.js diff --git a/app/assets/javascripts/discourse/app/initializers/page-tracking.js b/app/assets/javascripts/discourse/app/initializers/page-tracking.js index 589f4b66cf..80a462d5e8 100644 --- a/app/assets/javascripts/discourse/app/initializers/page-tracking.js +++ b/app/assets/javascripts/discourse/app/initializers/page-tracking.js @@ -36,8 +36,11 @@ export default { return; } - // Also use Universal Analytics if it is present - if (typeof window.ga !== "undefined") { + // Use Universal Analytics v3 if it is present + if ( + typeof window.ga !== "undefined" && + typeof window.gtag === "undefined" + ) { appEvents.on("page:changed", (data) => { if (!data.replacedOnlyQueryParams) { window.ga("send", "pageview", { page: data.url, title: data.title }); @@ -45,7 +48,19 @@ export default { }); } - // And Google Tag Manager too + // And Universal Analytics v4 if we're upgraded + if (typeof window.gtag !== "undefined") { + appEvents.on("page:changed", (data) => { + if (!data.replacedOnlyQueryParams) { + window.gtag("event", "page_view", { + page_location: data.url, + page_title: data.title, + }); + } + }); + } + + // Google Tag Manager too if (typeof window.dataLayer !== "undefined") { appEvents.on("page:changed", (data) => { if (!data.replacedOnlyQueryParams) { diff --git a/app/assets/javascripts/google-universal-analytics.js b/app/assets/javascripts/google-universal-analytics-v3.js similarity index 100% rename from app/assets/javascripts/google-universal-analytics.js rename to app/assets/javascripts/google-universal-analytics-v3.js diff --git a/app/assets/javascripts/google-universal-analytics-v4.js b/app/assets/javascripts/google-universal-analytics-v4.js new file mode 100644 index 0000000000..d16f3de9eb --- /dev/null +++ b/app/assets/javascripts/google-universal-analytics-v4.js @@ -0,0 +1,26 @@ +// discourse-skip-module +(function () { + const gaDataElement = document.getElementById("data-ga-universal-analytics"); + window.dataLayer = window.dataLayer || []; + + window.gtag = function () { + window.dataLayer.push(arguments); + }; + window.gtag("js", new Date()); + + let autoLinkConfig = {}; + + if (gaDataElement.dataset.autoLinkDomains.length) { + const autoLinkDomains = gaDataElement.dataset.autoLinkDomains.split("|"); + autoLinkConfig = { + linker: { + accept_incoming: true, + domains: autoLinkDomains, + }, + }; + } + window.gtag("config", gaDataElement.dataset.trackingCode, { + send_page_view: false, + autoLinkConfig, + }); +})(); diff --git a/app/views/common/_google_universal_analytics.html.erb b/app/views/common/_google_universal_analytics.html.erb index 81d7a96db9..572c281768 100644 --- a/app/views/common/_google_universal_analytics.html.erb +++ b/app/views/common/_google_universal_analytics.html.erb @@ -4,4 +4,9 @@ auto_link_domains: SiteSetting.ga_universal_auto_link_domains } %> -<%= preload_script "google-universal-analytics" %> +<% if SiteSetting.ga_version == "v3_analytics" %> + <%= preload_script "google-universal-analytics-v3" %> +<% elsif SiteSetting.ga_version == "v4_gtag" %> + + <%= preload_script "google-universal-analytics-v4" %> +<% end %> diff --git a/config/application.rb b/config/application.rb index a55a24e27a..b4b9efeb8e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -159,7 +159,8 @@ module Discourse markdown-it-bundle.js service-worker.js google-tag-manager.js - google-universal-analytics.js + google-universal-analytics-v3.js + google-universal-analytics-v4.js start-discourse.js print-page.js omniauth-complete.js diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index f6ff02dba9..f5c3dbf817 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1555,9 +1555,10 @@ en: pending_users_reminder_delay: "Notify moderators if new users have been waiting for approval for longer than this many hours. Set to -1 to disable notifications." persistent_sessions: "Users will remain logged in when the web browser is closed" maximum_session_age: "User will remain logged in for n hours since last visit" - ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code ID, eg: UA-12345678-9; see https://google.com/analytics" - ga_universal_domain_name: "Google Universal Analytics (analytics.js) domain name, eg: mysite.com; see https://google.com/analytics" - ga_universal_auto_link_domains: "Enable Google Universal Analytics (analytics.js) cross-domain tracking. Outgoing links to these domains will have the client id added to them. See Google's Cross-Domain Tracking guide." + ga_version: "Version of Google Universal Analytics to use: v3 (analytics.js), v4 (gtag)" + ga_universal_tracking_code: "Google Universal Analytics tracking code ID, eg: UA-12345678-9; see https://google.com/analytics" + ga_universal_domain_name: "Google Universal Analytics domain name, eg: mysite.com; see https://google.com/analytics" + ga_universal_auto_link_domains: "Enable Google Universal Analytics cross-domain tracking. Outgoing links to these domains will have the client id added to them. See Google's Cross-Domain Tracking guide." gtm_container_id: "Google Tag Manager container id. eg: GTM-ABCDEF.
Note: Third-party scripts loaded by GTM may need to be allowlisted in 'content security policy script src'." enable_escaped_fragments: "Fall back to Google's Ajax-Crawling API if no webcrawler is detected. See https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" moderators_manage_categories_and_groups: "Allow moderators to manage categories and groups" diff --git a/config/site_settings.yml b/config/site_settings.yml index 0934a42be6..8f502a803a 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -139,6 +139,12 @@ basic: default: 365 min: 7 max: 36500 + ga_version: + type: enum + default: v3_analytics + choices: + - v3_analytics + - v4_gtag ga_universal_tracking_code: client: true default: "" diff --git a/lib/content_security_policy/default.rb b/lib/content_security_policy/default.rb index 29c59c44d9..1d5315709e 100644 --- a/lib/content_security_policy/default.rb +++ b/lib/content_security_policy/default.rb @@ -56,7 +56,9 @@ class ContentSecurityPolicy ].tap do |sources| sources << :report_sample if SiteSetting.content_security_policy_collect_reports sources << :unsafe_eval if Rails.env.development? # TODO remove this once we have proper source maps in dev + # we need analytics.js still as gtag/js is a script wrapper for it sources << 'https://www.google-analytics.com/analytics.js' if SiteSetting.ga_universal_tracking_code.present? + sources << 'https://www.googletagmanager.com/gtag/js' if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag" sources << 'https://www.googletagmanager.com/gtm.js' if SiteSetting.gtm_container_id.present? end end diff --git a/lib/discourse_js_processor.rb b/lib/discourse_js_processor.rb index e5893ce460..f2f601a5e9 100644 --- a/lib/discourse_js_processor.rb +++ b/lib/discourse_js_processor.rb @@ -52,7 +52,8 @@ class DiscourseJsProcessor wizard-start onpopstate-handler google-tag-manager - google-universal-analytics + google-universal-analytics-v3 + google-universal-analytics-v4 activate-account auto-redirect embed-application diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb index 04557e3900..14b5c8f658 100644 --- a/spec/lib/content_security_policy_spec.rb +++ b/spec/lib/content_security_policy_spec.rb @@ -69,12 +69,30 @@ describe ContentSecurityPolicy do expect(script_srcs).to include("'report-sample'") end - it 'allowlists Google Analytics and Tag Manager when integrated' do - SiteSetting.ga_universal_tracking_code = 'UA-12345678-9' + context 'for Google Analytics' do + before do + SiteSetting.ga_universal_tracking_code = 'UA-12345678-9' + end + + it 'allowlists Google Analytics v3 when integrated' do + script_srcs = parse(policy)['script-src'] + expect(script_srcs).to include('https://www.google-analytics.com/analytics.js') + expect(script_srcs).not_to include('https://www.googletagmanager.com/gtag/js') + end + + it 'allowlists Google Analytics v4 when integrated' do + SiteSetting.ga_version = 'v4_gtag' + + script_srcs = parse(policy)['script-src'] + expect(script_srcs).to include('https://www.google-analytics.com/analytics.js') + expect(script_srcs).to include('https://www.googletagmanager.com/gtag/js') + end + end + + it 'allowlists Google Tag Manager when integrated' do SiteSetting.gtm_container_id = 'GTM-ABCDEF' script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include('https://www.google-analytics.com/analytics.js') expect(script_srcs).to include('https://www.googletagmanager.com/gtm.js') end