Skip to content

Commit

Permalink
Merge pull request #1508 from sharetribe/remove-ga-add-gtag-script
Browse files Browse the repository at this point in the history
Remove GA  analytics.js add gtag.js script
  • Loading branch information
Gnito authored May 19, 2022
2 parents 29520a9 + df9cfa8 commit 9b556fa
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 34 deletions.
4 changes: 4 additions & 0 deletions .env-template
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions server/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const defaultDirectives = {
'events.mapbox.com',

// Google Analytics
'​www.​googletagm­anager.​com',
'www.google-analytics.com',
'stats.g.doubleclick.net',

Expand Down Expand Up @@ -59,6 +60,7 @@ const defaultDirectives = {
'*.ggpht.com',

// Google Analytics
'www.googletagmanager.com',
'www.google.com',
'www.google-analytics.com',
'stats.g.doubleclick.net',
Expand All @@ -72,6 +74,7 @@ const defaultDirectives = {
data,
'maps.googleapis.com',
'api.mapbox.com',
'​www.​googletagm­anager.​com',
'*.google-analytics.com',
'js.stripe.com',
],
Expand Down
35 changes: 23 additions & 12 deletions server/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
? `
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', '${process.env.REACT_APP_GOOGLE_ANALYTICS_ID}', 'auto');
</script>
<script async src="https://www.google-analytics.com/analytics.js"></script>
`
: '';
// 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 = `
<script async src="https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${googleAnalyticsId}');
</script>
`;
const googleAnalyticsScript = hasGoogleAnalyticsv4Id ? gtagScripts : '';

return template({
htmlAttributes: head.htmlAttributes.toString(),
Expand Down
4 changes: 2 additions & 2 deletions src/Routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};

/**
Expand Down
9 changes: 5 additions & 4 deletions src/analytics/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { canonicalUrl } = payload;
const previousPath = store?.getState()?.Routing?.currentCanonicalPath;
const { canonicalPath } = payload;
handlers.forEach(handler => {
handler.trackPageView(canonicalUrl);
handler.trackPageView(canonicalPath, previousPath);
});
}

Expand Down
30 changes: 22 additions & 8 deletions src/analytics/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <title>,
// and location change event happens before initial rendering.
if (previousPath) {
window.setTimeout(() => {
this.gtag('event', 'page_view', {
page_path: canonicalPath,
});
}, 300);
}
}
}
8 changes: 4 additions & 4 deletions src/ducks/Routing.duck.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}) {
Expand All @@ -16,7 +16,7 @@ export default function routingReducer(state = initialState, action = {}) {
return {
...state,
currentLocation: payload.location,
currentCanonicalUrl: payload.canonicalUrl,
currentCanonicalPath: payload.canonicalPath,
};

default:
Expand All @@ -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 },
});
13 changes: 9 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
}
}
Expand Down

0 comments on commit 9b556fa

Please sign in to comment.