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