diff --git a/.env_sample b/.env_sample index a59de0cf7..628d06270 100644 --- a/.env_sample +++ b/.env_sample @@ -115,11 +115,16 @@ FASTLY_SERVICE_ID="" # (https://www.honeycomb.io/) HONEYCOMB_API_KEY="" -# Google analytics +# Google analytics 3/Universal Analytics # (https://developers.google.com/analytics/devguides/reporting/core/v4) # When blank don't render Google Analytics. GA_TRACKING_ID="" +# Google analytics 4/Tag Manager +# (https://developers.google.com/analytics/devguides/reporting/data/v1) +# When blank don't render Google Analytics 4. +GA_ANALYTICS_4_ID="" + # Mailchimp for mails # (https://mailchimp.com/developer/) MAILCHIMP_API_KEY="Optional-valid" diff --git a/app/assets/javascripts/.eslintrc.js b/app/assets/javascripts/.eslintrc.js index d3f1296e5..0ea391c19 100644 --- a/app/assets/javascripts/.eslintrc.js +++ b/app/assets/javascripts/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { preventDefaultAction: false, userData: false, ga: false, // Google Analytics + gtag: false, // Google Analytics 4 handleOptimisticButtRender: false, handleFollowButtPress: false, browserStoreCache: false, diff --git a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb index f27c15747..113f36acc 100644 --- a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb +++ b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb @@ -1,4 +1,10 @@ function initializeBaseTracking() { + trackGoogleAnalytics3(); + trackGoogleAnalytics4(); + trackCustomImpressions(); +} + +function trackGoogleAnalytics3() { var wait = 0; var addedGA = false; var gaTrackingCode = document.body.dataset.gaTracking; @@ -25,9 +31,48 @@ function initializeBaseTracking() { }, 25); eventListening(); } - trackCustomImpressions(); } +function trackGoogleAnalytics4() { + var wait = 0; + var addedGA4 = false; + var ga4MeasurementCode = document.body.dataset.ga4TrackingId; + if (ga4MeasurementCode) { + var waitingOnGA4 = setInterval(function() { + if (!addedGA4) { + <%# Dynamically add the Google Analytics 4 script tag %> + var script = document.createElement('script'); + script.src = "http://www.googletagmanager.com/gtag/js?id=" + ga4MeasurementCode; + script.async = true; + document.head.appendChild(script); + } + addedGA4 = true; + wait++; + if (window.google_tag_manager) { + <%# Define the gtag function and call it. Adapted from https://stackoverflow.com/questions/22716542/google-analytics-code-explanation %> + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + + window['gtag'] = window['gtag'] || function () { + dataLayer.push(arguments) + } + + gtag('js', new Date()); + gtag('config', ga4MeasurementCode, { 'anonymize_ip': true }); + clearInterval(waitingOnGA4); + } + if (wait > 85) { + clearInterval(waitingOnGA4); + <%# The gem we're using server-side (Staccato) is not yet compatible with the Google Analytics 4 tracking code. + More details: https://github.com/tpitale/staccato/issues/97 %> + <%# fallbackActivityRecording(); %> + } + }, 25); + eventListening(); + } +} + + function fallbackActivityRecording() { var tokenMeta = document.querySelector("meta[name='csrf-token']") if (!tokenMeta) { @@ -69,6 +114,15 @@ function eventListening(){ } } +function ga4eventListening(){ + var registerNowButt = document.getElementById("cta-comment-register-now-link"); + if (registerNowButt) { + registerNowButt.onclick = function(){ + gtag('event', 'register-now-click' ); + } + } +} + function trackCustomImpressions() { setTimeout(function(){ var ArticleElement = document.getElementById('article-body') || document.getElementById('comment-article-indicator'); diff --git a/app/assets/javascripts/initializers/initializeBodyData.js b/app/assets/javascripts/initializers/initializeBodyData.js index c68cc2e27..945c7f28e 100644 --- a/app/assets/javascripts/initializers/initializeBodyData.js +++ b/app/assets/javascripts/initializers/initializeBodyData.js @@ -41,6 +41,9 @@ function fetchBaseData() { if (typeof ga === 'function') { ga('set', 'userId', JSON.parse(user).id); } + if (typeof gtag === 'function') { + gtag('set', 'user_Id', JSON.parse(user).id); + } }, 400); } else { // Ensure user data is not exposed if no one is logged in diff --git a/app/assets/javascripts/initializers/initializeSponsorshipVisibility.js b/app/assets/javascripts/initializers/initializeSponsorshipVisibility.js index e55aaa545..b3c799d8b 100644 --- a/app/assets/javascripts/initializers/initializeSponsorshipVisibility.js +++ b/app/assets/javascripts/initializers/initializeSponsorshipVisibility.js @@ -13,13 +13,25 @@ function sponsorClickHandler(event) { ); } +function sponsorClickHandlerGA4(event) { + gtag('event', 'click sponsor link', { + event_category: 'click', + event_label: event.target.dataset.details, + }); +} + function listenForSponsorClick() { setTimeout(() => { - if (window.ga) { + if (window.ga || window.gtag) { var links = document.getElementsByClassName('partner-link'); // eslint-disable-next-line no-plusplus for (var i = 0; i < links.length; i++) { - links[i].onclick = sponsorClickHandler; + if (window.ga) { + links[i].onclick = sponsorClickHandler; + } + if (window.gtag) { + links[i].onclick = sponsorClickHandlerGA4; + } } } }, 400); @@ -32,8 +44,8 @@ function initializeSponsorshipVisibility() { var user = userData(); if (el) { setTimeout(() => { - if (window.ga) { - if (document.querySelectorAll('[data-partner-seen]').length === 0) { + if (document.querySelectorAll('[data-partner-seen]').length === 0) { + if (window.ga) { ga( 'send', 'event', @@ -42,8 +54,14 @@ function initializeSponsorshipVisibility() { el.dataset.details, null, ); - el.dataset.partnerSeen = 'true'; } + if (window.gtag) { + gtag('event', 'sponsor displayed on page', { + event_category: 'view', + event_label: el.dataset.details, + }); + } + el.dataset.partnerSeen = 'true'; } }, 400); } diff --git a/app/javascript/.eslintrc.js b/app/javascript/.eslintrc.js index de1780faa..39b7a3236 100644 --- a/app/javascript/.eslintrc.js +++ b/app/javascript/.eslintrc.js @@ -79,6 +79,7 @@ module.exports = { InstantClick: false, filterXSS: false, ga: false, + gtag: false, Honeybadger: false, AndroidBridge: false, }, diff --git a/app/javascript/packs/homePageFeed.jsx b/app/javascript/packs/homePageFeed.jsx index 86e64826c..30326ecc2 100644 --- a/app/javascript/packs/homePageFeed.jsx +++ b/app/javascript/packs/homePageFeed.jsx @@ -11,10 +11,10 @@ import { getUserDataAndCsrfToken } from '@utilities/getUserDataAndCsrfToken'; * * @param {number} articleId */ -function sendFeaturedArticleAnalytics(articleId) { - (function logFeaturedArticleImpression() { +function sendFeaturedArticleGoogleAnalytics(articleId) { + (function logFeaturedArticleImpressionGA() { if (!window.ga || !ga.create) { - setTimeout(logFeaturedArticleImpression, 20); + setTimeout(logFeaturedArticleImpressionGA, 20); return; } @@ -29,6 +29,20 @@ function sendFeaturedArticleAnalytics(articleId) { })(); } +function sendFeaturedArticleAnalyticsGA4(articleId) { + (function logFeaturedArticleImpressionGA4() { + if (!window.gtag) { + setTimeout(logFeaturedArticleImpressionGA4, 20); + return; + } + + gtag('event', 'featured-feed-impression', { + event_category: 'view', + event_label: `articles-${articleId}`, + }); + })(); +} + const FeedLoading = () => (
@@ -89,7 +103,8 @@ export const renderFeed = async (timeFrame) => { const [featuredStory, ...subStories] = feedItems; if (featuredStory) { - sendFeaturedArticleAnalytics(featuredStory.id); + sendFeaturedArticleGoogleAnalytics(featuredStory.id); + sendFeaturedArticleAnalyticsGA4(featuredStory.id); } // 1. Show the pinned article first diff --git a/app/lib/constants/settings/general.rb b/app/lib/constants/settings/general.rb index e2d41c541..6a74eeccf 100644 --- a/app/lib/constants/settings/general.rb +++ b/app/lib/constants/settings/general.rb @@ -35,6 +35,10 @@ module Constants description: I18n.t("lib.constants.settings.general.ga_tracking.description"), placeholder: "" }, + ga_analytics_4_id: { + description: I18n.t("lib.constants.settings.general.ga_analytics_4.description"), + placeholder: "" + }, health_check_token: { description: I18n.t("lib.constants.settings.general.health.description"), placeholder: I18n.t("lib.constants.settings.general.health.placeholder") diff --git a/app/models/settings/general.rb b/app/models/settings/general.rb index eb435ad45..2416f584e 100644 --- a/app/models/settings/general.rb +++ b/app/models/settings/general.rb @@ -23,6 +23,7 @@ module Settings # Google Analytics Tracking ID, e.g. UA-71991000-1 setting :ga_tracking_id, type: :string, default: ApplicationConfig["GA_TRACKING_ID"] + setting :ga_analytics_4_id, type: :string, default: ApplicationConfig["GA_ANALYTICS_4_ID"] # Images setting :main_social_image, diff --git a/app/views/admin/settings/forms/_google_analytics.html.erb b/app/views/admin/settings/forms/_google_analytics.html.erb index da931c4eb..5b81f7c07 100644 --- a/app/views/admin/settings/forms/_google_analytics.html.erb +++ b/app/views/admin/settings/forms/_google_analytics.html.erb @@ -6,12 +6,19 @@
- <%= admin_config_label :ga_tracking_id, "View ID" %> + <%= admin_config_label :ga_tracking_id, "Google Universal Analytics View ID" %> <%= admin_config_description Constants::Settings::General.details[:ga_tracking_id][:description] %> <%= f.text_field :ga_tracking_id, class: "crayons-textfield", value: Settings::General.ga_tracking_id %>
+
+ <%= admin_config_label :ga_analytics_4_id, "Google Analytics 4 Measurement ID" %> + <%= admin_config_description Constants::Settings::General.details[:ga_analytics_4_id][:description] %> + <%= f.text_field :ga_analytics_4_id, + class: "crayons-textfield", + value: Settings::General.ga_analytics_4_id %> +
<%= render "update_setting_button", f: f %>
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index ad3c1f51e..d489d9e27 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -27,6 +27,7 @@ data-deployed-at="<%= j(ForemInstance.deployed_at) %>" data-latest-commit-id="<%= j(ForemInstance.latest_commit_id) %>" data-ga-tracking="<%= j(Settings::General.ga_tracking_id) %>" + data-ga4-tracking-id="<%= j(Settings::General.ga_analytics_4_id) %>" data-controller="snackbar" data-action="snackbar:add@document->snackbar#addItem">
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e4b1ea738..55ebc17da 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -54,7 +54,9 @@ data-honeybadger-key="<%= j(ApplicationConfig["HONEYBADGER_JS_API_KEY"]) %>" data-deployed-at="<%= j(ForemInstance.deployed_at) %>" data-latest-commit-id="<%= j(ForemInstance.latest_commit_id) %>" - data-ga-tracking="<%= j(Settings::General.ga_tracking_id) %>"> + data-ga-tracking="<%= j(Settings::General.ga_tracking_id) %>" + data-ga4-tracking-id="<%= j(Settings::General.ga_analytics_4_id) %>" + > <%# Repeat of stylesheets in to fix rendering glitch: https://github.com/forem/forem/issues/12377 %> <%= render "layouts/styles", qualifier: "secondary" %>
diff --git a/config/locales/lib/en.yml b/config/locales/lib/en.yml index 5b2c0fd84..86ec1d749 100644 --- a/config/locales/lib/en.yml +++ b/config/locales/lib/en.yml @@ -108,7 +108,9 @@ en: favicon: description: Used as the site favicon ga_tracking: - description: Google Analytics Tracking ID, e.g. UA-71991000-1 + description: Google Universal Analytics Tracking ID, e.g. UA-XXXXXXXX-1 + ga_analytics_4: + description: Google Analytics 4 Measurement ID, e.g. G-XXXXXXXXX hashtag: description: Used as the twitter hashtag of the community placeholder: '#DEVCommunity' @@ -205,4 +207,4 @@ en: primary_hex: description: Determines background/border of buttons etc. Must be dark enough to contrast with white text. tag_feed: - description: Minimum score needed for a post to show up on default tag page. \ No newline at end of file + description: Minimum score needed for a post to show up on default tag page. diff --git a/config/locales/lib/fr.yml b/config/locales/lib/fr.yml index 8efdf942d..320c7c774 100644 --- a/config/locales/lib/fr.yml +++ b/config/locales/lib/fr.yml @@ -108,7 +108,9 @@ fr: favicon: description: Used as the site favicon ga_tracking: - description: Google Analytics Tracking ID, e.g. UA-71991000-1 + description: Google Universal Analytics Tracking ID, e.g. UA-XXXXXXXX-1 + ga_analytics_4: + description: Google Analytics 4 Measurement ID, e.g. G-XXXXXXXXX hashtag: description: Used as the twitter hashtag of the community placeholder: '#DEVCommunity' diff --git a/spec/requests/admin/configs_spec.rb b/spec/requests/admin/configs_spec.rb index fb27201e3..c8ef3f81a 100644 --- a/spec/requests/admin/configs_spec.rb +++ b/spec/requests/admin/configs_spec.rb @@ -256,7 +256,7 @@ RSpec.describe "/admin/customization/config", type: :request do end end - describe "Google Analytics Reporting API v4" do + describe "Google Universal Analytics Reporting" do it "updates ga_tracking_id" do post admin_settings_general_settings_path, params: { settings_general: { ga_tracking_id: "abc" } @@ -265,6 +265,15 @@ RSpec.describe "/admin/customization/config", type: :request do end end + describe "Google Analytics 4 Reporting" do + it "updates ga_analytics_4_id" do + post admin_settings_general_settings_path, params: { + settings_general: { ga_analytics_4_id: "abc" } + } + expect(Settings::General.ga_analytics_4_id).to eq("abc") + end + end + describe "Images" do it "updates main_social_image" do expected_default_image_url = URL.local_image("social-media-cover.png") diff --git a/spec/requests/stories_index_spec.rb b/spec/requests/stories_index_spec.rb index 724a87e0c..cd7676322 100644 --- a/spec/requests/stories_index_spec.rb +++ b/spec/requests/stories_index_spec.rb @@ -20,7 +20,7 @@ RSpec.describe "StoriesIndex", type: :request do get "/" expect(response.body).to include(CGI.escapeHTML(article.title)) - renders_ga_tracking_data + renders_ga_tracking_fields renders_proper_description renders_min_read_time renders_proper_sidebar(navigation_link) @@ -44,8 +44,9 @@ RSpec.describe "StoriesIndex", type: :request do expect(response.body).to include(CGI.escapeHTML(navigation_link.name)) end - def renders_ga_tracking_data + def renders_ga_tracking_fields expect(response.body).to include("data-ga-tracking=\"#{Settings::General.ga_tracking_id}\"") + expect(response.body).to include("data-ga4-tracking-id=\"#{Settings::General.ga_analytics_4_id}\"") end it "renders registration page if the Forem instance is private" do