From 9273c096405ce3dd0ca69a0f21b78bfeb9445260 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 21 Apr 2022 20:09:21 +0300 Subject: [PATCH 1/6] Rename currentCanonicalUrl to reflect the content: currentCanonicalPath --- src/Routes.js | 4 ++-- src/analytics/analytics.js | 4 ++-- src/ducks/Routing.duck.js | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Routes.js b/src/Routes.js index 69b517e662..ae4f6e1b0d 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -64,8 +64,8 @@ const setPageScrollPosition = location => { const handleLocationChanged = (dispatch, location) => { setPageScrollPosition(location); - const url = canonicalRoutePath(routeConfiguration(), location); - dispatch(locationChanged(location, url)); + const path = canonicalRoutePath(routeConfiguration(), location); + dispatch(locationChanged(location, path)); }; /** diff --git a/src/analytics/analytics.js b/src/analytics/analytics.js index abfc75da4c..574955997a 100644 --- a/src/analytics/analytics.js +++ b/src/analytics/analytics.js @@ -8,9 +8,9 @@ export const createMiddleware = handlers => () => next => action => { const { type, payload } = action; if (type === LOCATION_CHANGED) { - const { canonicalUrl } = payload; + const { canonicalPath } = payload; handlers.forEach(handler => { - handler.trackPageView(canonicalUrl); + handler.trackPageView(canonicalPath); }); } diff --git a/src/ducks/Routing.duck.js b/src/ducks/Routing.duck.js index 0172b05e25..e6f22bf275 100644 --- a/src/ducks/Routing.duck.js +++ b/src/ducks/Routing.duck.js @@ -6,7 +6,7 @@ export const LOCATION_CHANGED = 'app/Routing/LOCATION_CHANGED'; const initialState = { currentLocation: null, - currentCanonicalUrl: null, + currentCanonicalPath: null, }; export default function routingReducer(state = initialState, action = {}) { @@ -16,7 +16,7 @@ export default function routingReducer(state = initialState, action = {}) { return { ...state, currentLocation: payload.location, - currentCanonicalUrl: payload.canonicalUrl, + currentCanonicalPath: payload.canonicalPath, }; default: @@ -26,7 +26,7 @@ export default function routingReducer(state = initialState, action = {}) { // ================ Action creators ================ // -export const locationChanged = (location, canonicalUrl) => ({ +export const locationChanged = (location, canonicalPath) => ({ type: LOCATION_CHANGED, - payload: { location, canonicalUrl }, + payload: { location, canonicalPath }, }); From b6ec6ffd4d46875024f3017c01d1998413c78c48 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 21 Apr 2022 20:22:58 +0300 Subject: [PATCH 2/6] Analytics handlers: include previous path when calling trackPageView --- src/analytics/analytics.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/analytics/analytics.js b/src/analytics/analytics.js index 574955997a..5d3b64d741 100644 --- a/src/analytics/analytics.js +++ b/src/analytics/analytics.js @@ -3,14 +3,15 @@ import { LOCATION_CHANGED } from '../ducks/Routing.duck'; // Create a Redux middleware from the given analytics handlers. Each // handler should have the following methods: // -// - trackPageView(url): called when the URL is changed -export const createMiddleware = handlers => () => next => action => { +// - trackPageView(canonicalPath, previousPath): called when the URL is changed +export const createMiddleware = handlers => store => next => action => { const { type, payload } = action; if (type === LOCATION_CHANGED) { + const previousPath = store?.getState()?.Routing?.currentCanonicalPath; const { canonicalPath } = payload; handlers.forEach(handler => { - handler.trackPageView(canonicalPath); + handler.trackPageView(canonicalPath, previousPath); }); } From 6bcfbef53638cdca99edf0a2996c1cd710c94530 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 21 Apr 2022 21:31:30 +0300 Subject: [PATCH 3/6] Google Analytics: use GA4 instead of deprecated Universal Analytics --- server/renderer.js | 35 +++++++++++++++++++++++------------ src/analytics/handlers.js | 30 ++++++++++++++++++++++-------- src/index.js | 13 +++++++++---- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/server/renderer.js b/server/renderer.js index db2528d5b8..88c4687f16 100644 --- a/server/renderer.js +++ b/server/renderer.js @@ -121,20 +121,31 @@ exports.render = function(requestUrl, context, data, renderApp, webExtractor) { // We want to precisely control where the analytics script is // injected in the HTML file so we can catch all events as early as - // possible. This is why we inject the GA script separately from - // react-helmet. This script also ensures that all the GA scripts + // possible. This script also ensures that all the GA scripts // are added only when the proper env var is present. + // NOTE: when dealing with cookie consents, it might make more sense to + // include this script through react-helmet. // - // See: https://developers.google.com/analytics/devguides/collection/analyticsjs/#alternative_async_tracking_snippet - const googleAnalyticsScript = process.env.REACT_APP_GOOGLE_ANALYTICS_ID - ? ` - - - ` - : ''; + // See: https://developers.google.com/analytics/devguides/collection/gtagjs + const googleAnalyticsId = process.env.REACT_APP_GOOGLE_ANALYTICS_ID; + // Add Google Analytics script if correct id exists (it should start with 'G-' prefix) + const hasGoogleAnalyticsv4Id = googleAnalyticsId.indexOf('G-') === 0; + + // Google Analytics: gtag.js + // NOTE: FTW is a single-page application (SPA). + // gtag.js sends initial page_view event after page load. + // but we need to handle subsequent events for in-app navigation. + const gtagScripts = ` + + + `; + const googleAnalyticsScript = hasGoogleAnalyticsv4Id ? gtagScripts : ''; return template({ htmlAttributes: head.htmlAttributes.toString(), diff --git a/src/analytics/handlers.js b/src/analytics/handlers.js index f96a92a529..03038e6043 100644 --- a/src/analytics/handlers.js +++ b/src/analytics/handlers.js @@ -4,16 +4,30 @@ export class LoggingAnalyticsHandler { } } +// Google Analytics 4 (GA4) using gtag.js script, which is included in server/rendered.js +// Note: the script is only available locally when running "yarn run dev-server" export class GoogleAnalyticsHandler { - constructor(ga) { - if (typeof ga !== 'function') { - throw new Error('Variable `ga` missing for Google Analytics'); + constructor(gtag) { + if (typeof gtag !== 'function') { + throw new Error('Variable `gtag` missing for Google Analytics'); } - this.ga = ga; + this.gtag = gtag; } - trackPageView(url) { - // https://developers.google.com/analytics/devguides/collection/analyticsjs/single-page-applications#tracking_virtual_pageviews - this.ga('set', 'page', url); - this.ga('send', 'pageview'); + trackPageView(canonicalPath, previousPath) { + // GA4 property. Manually send page_view events + // https://developers.google.com/analytics/devguides/collection/gtagjs/single-page-applications + // Note 1: You should turn "Enhanced measurement" off. + // It attaches own listeners to elements and that breaks in-app navigation. + // Note 2: If previousPath is null (just after page load), gtag script sends page_view event automatically. + // Only in-app navigation needs to be sent manually from SPA. + // Note 3: Timeout is needed because gtag script picks up , + // and location change event happens before initial rendering. + if (previousPath) { + window.setTimeout(() => { + this.gtag('event', 'page_view', { + page_path: canonicalPath, + }); + }, 300); + } } } diff --git a/src/index.js b/src/index.js index c2160a7a8e..f55ed1d673 100644 --- a/src/index.js +++ b/src/index.js @@ -81,14 +81,19 @@ const setupAnalyticsHandlers = () => { handlers.push(new LoggingAnalyticsHandler()); } - // Add Google Analytics handler if tracker ID is found + // Add Google Analytics 4 (GA4) handler if tracker ID is found if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { - if (window?.ga) { - handlers.push(new GoogleAnalyticsHandler(window.ga)); + if (window?.gtag) { + handlers.push(new GoogleAnalyticsHandler(window.gtag)); } else { // Some adblockers (e.g. Ghostery) might block the Google Analytics integration. console.warn( - 'Google Analytics (window.ga) is not available. It might be that your adblocker is blocking it.' + 'Google Analytics (window.gtag) is not available. It might be that your adblocker is blocking it.' + ); + } + if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID.indexOf('G-') !== 0) { + console.warn( + 'Google Analytics 4 (GA4) should have measurement id that starts with "G-" prefix' ); } } From af97b2782ebc5743f862a2ff094c90fd6851013c Mon Sep 17 00:00:00 2001 From: Vesa Luusua <vesa.luusua@gmail.com> Date: Fri, 22 Apr 2022 11:46:38 +0300 Subject: [PATCH 4/6] Update .env-template instructions --- .env-template | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.env-template b/.env-template index 179fc74d01..063022bb31 100644 --- a/.env-template +++ b/.env-template @@ -51,6 +51,10 @@ REACT_APP_CSP=report # REACT_APP_SENTRY_DSN=change-me # BASIC_AUTH_USERNAME=sharetribe # BASIC_AUTH_PASSWORD=secret + +# This is GA4 id, which should start with 'G-' prefix. +# You should also turn "Enhanced measurements" off from GA. +# https://support.google.com/analytics/answer/9216061 # REACT_APP_GOOGLE_ANALYTICS_ID=change-me From 512ddc94af6b1e8a97f1d8bbb7952d11db34acf2 Mon Sep 17 00:00:00 2001 From: Vesa Luusua <vesa.luusua@gmail.com> Date: Fri, 6 May 2022 13:07:50 +0300 Subject: [PATCH 5/6] Update CSP --- server/csp.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/csp.js b/server/csp.js index 5ae6345a3e..fbd0c1db67 100644 --- a/server/csp.js +++ b/server/csp.js @@ -32,6 +32,7 @@ const defaultDirectives = { 'events.mapbox.com', // Google Analytics + '​www.​googletagm­anager.​com', 'www.google-analytics.com', 'stats.g.doubleclick.net', @@ -59,6 +60,7 @@ const defaultDirectives = { '*.ggpht.com', // Google Analytics + 'www.googletagmanager.com', 'www.google.com', 'www.google-analytics.com', 'stats.g.doubleclick.net', @@ -72,6 +74,7 @@ const defaultDirectives = { data, 'maps.googleapis.com', 'api.mapbox.com', + '​www.​googletagm­anager.​com', '*.google-analytics.com', 'js.stripe.com', ], From df9cfa87b892ff3483d0e82973311bea9ac6d255 Mon Sep 17 00:00:00 2001 From: Vesa Luusua <vesa.luusua@gmail.com> Date: Fri, 22 Apr 2022 12:33:41 +0300 Subject: [PATCH 6/6] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b3c75f79..ca333df6ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2022-XX-XX +- [change] Google Analytics: remove Universal Analytics and start supporting GA4. + + NOTE: you need to update the Google Analytics id to GA4's id (starting with 'G-' prefix). + + [#1508](https://github.com/sharetribe/ftw-daily/pull/1508) + - [change] Update some outdated dependencies. [#1514](https://github.com/sharetribe/ftw-daily/pull/1514)