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.
This commit is contained in:
Justin DiRose 2020-11-06 14:15:36 -06:00 committed by GitHub
parent 8f7e4f87ec
commit 09b8a61f65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 87 additions and 12 deletions

View File

@ -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) {

View File

@ -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,
});
})();

View File

@ -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" %>
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= SiteSetting.ga_universal_tracking_code %>"></script>
<%= preload_script "google-universal-analytics-v4" %>
<% end %>

View File

@ -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

View File

@ -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 <a href='https://google.com/analytics' target='_blank'>https://google.com/analytics</a>"
ga_universal_domain_name: "Google Universal Analytics (analytics.js) domain name, eg: mysite.com; see <a href='https://google.com/analytics' target='_blank'>https://google.com/analytics</a>"
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 <a href='https://support.google.com/analytics/answer/1034342?hl=en' target='_blank'>Google's Cross-Domain Tracking guide.</a>"
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 <a href='https://google.com/analytics' target='_blank'>https://google.com/analytics</a>"
ga_universal_domain_name: "Google Universal Analytics domain name, eg: mysite.com; see <a href='https://google.com/analytics' target='_blank'>https://google.com/analytics</a>"
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 <a href='https://support.google.com/analytics/answer/1034342?hl=en' target='_blank'>Google's Cross-Domain Tracking guide.</a>"
gtm_container_id: "Google Tag Manager container id. eg: GTM-ABCDEF. <br/>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 <a href='https://developers.google.com/webmasters/ajax-crawling/docs/learn-more' target='_blank'>https://developers.google.com/webmasters/ajax-crawling/docs/learn-more</a>"
moderators_manage_categories_and_groups: "Allow moderators to manage categories and groups"

View File

@ -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: ""

View File

@ -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

View File

@ -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

View File

@ -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
context 'for Google Analytics' do
before do
SiteSetting.ga_universal_tracking_code = 'UA-12345678-9'
SiteSetting.gtm_container_id = 'GTM-ABCDEF'
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.googletagmanager.com/gtm.js')
end