Skip to content

Commit

Permalink
feat(google-analytics): migrate to gtag.js with dual tagging (#10687)
Browse files Browse the repository at this point in the history
We have been using `analytics.js`, which is being deprecated, and
we want to use UA and GA4 in parallel, which `gtag.js` supports.
  • Loading branch information
caugner authored Mar 20, 2024
1 parent 8ac8c6f commit 56dbe78
Show file tree
Hide file tree
Showing 17 changed files with 88 additions and 179 deletions.
2 changes: 1 addition & 1 deletion .env.testing
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ REACT_APP_AUTOCOMPLETE_SEARCH_WIDGET=true

# These are set to fake values just to make sure the paths that
# injects the relevant script tags actually run in end-to-end testing.
BUILD_GOOGLE_ANALYTICS_ACCOUNT=UA-00000000-0
BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID=G-XXXXXXXX

# The functional tests are done in a production'y way as if it had
# to go into full production mode.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dev-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ jobs:

# This just makes sure the Google Analytics script gets used even if
# it goes nowhere.
BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-00000000-0
BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID: G-XXXXXXXX

# This removes the ability to sign in
REACT_APP_DISABLE_AUTH: true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:

# Make sure it's set to something so that the build uses the
# Google Analytics tag which is most realistic.
BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-00000000-0
BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID: G-XXXXXXXX
run: |
yarn build:prepare
# BUILD_FOLDERSEARCH=mdn/kitchensink yarn build
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/prod-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,11 @@ jobs:
# Now is not the time to worry about flaws.
BUILD_FLAW_LEVELS: "*:ignore"

# This is the Google Analytics account ID for developer.mozilla.org
# If it's used on other domains (e.g. stage or dev builds), it's OK
# because ultimately Google Analytics will filter it out since the
# origin domain isn't what that account expects.
BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5
# These are the Google Analytics measurement IDs for:
# - developer.mozilla.org (UA)
# - developer.mozilla.org (GA4)
# Using measurement ids on other domains is okay, as GA will filter these events.
BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID: UA-36116321-5,G-PWTK27XVWP

# This enables the MDN Plus
REACT_APP_ENABLE_PLUS: true
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/stage-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,11 @@ jobs:
# Now is not the time to worry about flaws.
BUILD_FLAW_LEVELS: "*:ignore"

# This is the Google Analytics account ID for developer.mozilla.org
# If it's used on other domains (e.g. stage or dev builds), it's OK
# because ultimately Google Analytics will filter it out since the
# origin domain isn't what that account expects.
BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5
# These are the Google Analytics measurement IDs for:
# - developer.mozilla.org (UA)
# - developer.allizom.org (GA4)
# Using measurement ids on other domains is okay, as GA will filter these events.
BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID: UA-36116321-5,G-ZG5HNVZRY0

# This enables the Plus call-to-action banner and the Plus landing page
REACT_APP_ENABLE_PLUS: true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/xyz-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ jobs:
# If it's used on other domains (e.g. stage or dev builds), it's OK
# because ultimately Google Analytics will filter it out since the
# origin domain isn't what that account expects.
BUILD_GOOGLE_ANALYTICS_ACCOUNT: UA-36116321-5
BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID: UA-36116321-5

# This enables the Plus call-to-action banner and the Plus landing page
REACT_APP_ENABLE_PLUS: true
Expand Down
11 changes: 5 additions & 6 deletions client/src/document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class HTTPError extends Error {
}

export function Document(props /* TODO: define a TS interface for this */) {
const ga = useGA();
const { gtag } = useGA();
const gleanClick = useGleanClick();
const isServer = useIsServer();

Expand Down Expand Up @@ -147,10 +147,9 @@ export function Document(props /* TODO: define a TS interface for this */) {
// I.e. not the initial load but the location has now changed.
// Note that in local development, where you use `localhost:3000`
// this will always be true because it's always client-side navigation.
ga("set", "dimension19", "Yes");
ga("send", {
hitType: "pageview",
location,
gtag("event", "pageview", {
dimension19: "Yes",
page_location: location,
});
gleanClick(`${CLIENT_SIDE_NAVIGATION}: ${location}`);
}
Expand All @@ -159,7 +158,7 @@ export function Document(props /* TODO: define a TS interface for this */) {
// a client-side navigation happened.
mountCounter.current++;
}
}, [ga, gleanClick, doc, error]);
}, [gtag, gleanClick, doc, error]);

React.useEffect(() => {
const location = document.location;
Expand Down
84 changes: 15 additions & 69 deletions client/src/ga-context.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,42 @@
import * as React from "react";
import { useContext, useEffect, useState } from "react";
import { useContext } from "react";

export type GAFunction = (...any) => void;

const GA_SESSION_STORAGE_KEY = "ga";

function getPostponedEvents() {
let value;
try {
value = sessionStorage.getItem(GA_SESSION_STORAGE_KEY);
} catch (e) {
// No sessionStorage support
return [];
}
return JSON.parse(value || JSON.stringify([]));
}

/**
* Saves given events into sessionStorage so that they are sent once the next
* page has loaded. This should be used for events that need to be sent without
* delaying navigation to a new page (which would cancel pending network
* requests).
*/
export function gaSendOnNextPage(newEvents: any[]) {
const events = getPostponedEvents();
const value = JSON.stringify(events.concat(newEvents));
try {
sessionStorage.setItem(GA_SESSION_STORAGE_KEY, value);
} catch (e) {
// No sessionStorage support
}
export interface GAData {
gtag: GAFunction;
}
export type GAFunction = (...any) => void;

declare global {
interface Window {
ga?: Function;
gtag?: Function;
}
}

function ga(...args) {
if (typeof window === "object" && typeof window.ga === "function") {
window.ga(...args);
function gtag(...args) {
if (typeof window === "object" && typeof window.gtag === "function") {
window.gtag(...args);
}
}

const GAContext = React.createContext<GAFunction>(ga);
const GAContext = React.createContext<GAData>({ gtag });

/**
* If we're running in the browser (not being server-side rendered)
* and if the HTML document includes the Google Analytics snippet that
* defines the ga() function, then this provider component makes that
* ga() function available to any component via:
* defines the gtag() function, then this provider component makes that
* gtag() function available to any component via:
*
* let ga = useContext(GAProvider.context)
* const { gtag } = useContext(GAProvider.context)
*
* If we're not in a browser or if google analytics is not enabled,
* then we provide a dummy function that ignores its arguments and
* does nothing. The idea is that components can always safely call
* the function provided by this component.
*/
export function GAProvider(props: { children: React.ReactNode }) {
/**
* Checks for the existence of postponed analytics events, which we store
* in sessionStorage. It also clears them so that they aren't sent again.
*/
useEffect(() => {
const events = getPostponedEvents();
try {
sessionStorage.removeItem(GA_SESSION_STORAGE_KEY);
} catch (e) {
// No sessionStorage support
}
for (const event of events) {
ga("send", event);
}
}, []);

return <GAContext.Provider value={ga}>{props.children}</GAContext.Provider>;
}

// This is a custom hook to return the GA client id. It returns the
// empty string until (and unless) it can determine that id from the GA object.
export function useClientId() {
const [clientId, setClientId] = useState<string>("");
const ga = useContext(GAContext);
useEffect(() => {
ga((tracker) => {
setClientId(tracker.get("clientId"));
});
}, [ga]);

return clientId;
return (
<GAContext.Provider value={{ gtag }}>{props.children}</GAContext.Provider>
);
}

export function useGA() {
Expand Down
13 changes: 6 additions & 7 deletions client/src/site-search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const SearchResults = React.lazy(() => import("./search-results"));

export function SiteSearch() {
const isServer = useIsServer();
const ga = useGA();
const { gtag } = useGA();
const gleanClick = useGleanClick();
const [searchParams] = useSearchParams();

Expand All @@ -34,25 +34,24 @@ export function SiteSearch() {

const mountCounter = React.useRef(0);
React.useEffect(() => {
if (ga) {
if (gtag) {
if (mountCounter.current > 0) {
const location = window.location.toString();
// 'dimension19' means it's a client-side navigation.
// I.e. not the initial load but the location has now changed.
// Note that in local development, where you use `localhost:3000`
// this will always be true because it's always client-side navigation.
ga("set", "dimension19", "Yes");
ga("send", {
hitType: "pageview",
location,
gtag("event", "pageview", {
dimension19: "Yes",
page_location: location,
});
gleanClick(`${CLIENT_SIDE_NAVIGATION}: ${location}`);
}
// By counting every time a document is mounted, we can use this to know if
// a client-side navigation happened.
mountCounter.current++;
}
}, [query, page, ga, gleanClick]);
}, [query, page, gtag, gleanClick]);

return (
<div className="main-wrapper site-search">
Expand Down
18 changes: 7 additions & 11 deletions docs/envvars.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,23 +143,19 @@ Used to serve legacy lives samples that do not support playground rendering.

The base URL used in the Interactive Example iframes.

### `BUILD_GOOGLE_ANALYTICS_ACCOUNT`
### `BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID`

**Default: `''`**

If set, the rendered HTML will have a Google Analytics snippet. For example, to
test use: `export BUILD_GOOGLE_ANALYTICS_ACCOUNT=UA-00000000-0`. By default it's
disabled (empty string).
test use: `export BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID=G-XXXXXXXX`. By default
it's disabled (empty string).

### `BUILD_GOOGLE_ANALYTICS_DEBUG`
For dual tagging (UA + GA4), multiple IDs can be separated by a comma:

**Default: `false`**

If true, and when `BUILD_GOOGLE_ANALYTICS_ACCOUNT` is truthy, when it injects
the Google Analytics script tag it will use
`<script src="https://www.google-analytics.com/analytics_debug.js"></script>`
instead which triggers additional console logging which is useful for
developers.
```env
export BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID=UA-00000000-0,G-XXXXXXXX
```

### `BUILD_ALWAYS_ALLOW_ROBOTS`

Expand Down
35 changes: 11 additions & 24 deletions docs/google-analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,34 @@ from the client-side code. The way it works is that you have to send an
environment variable, like:

```bash
BUILD_GOOGLE_ANALYTICS_ACCOUNT=UA-1234678-0
BUILD_GOOGLE_ANALYTICS_MEASUREMENT_ID=G-XXXXXXXX
```

and that gets included in the build by more or less code-generating the snippet
we use to set up Google Analytics.

By default, it's not set and that means no Google Analytics JavaScript code
inside the rendered final HTML. By setting the environment variable, a
build-step will generate a `/static/js/ga.js` file that configures how we enable
Google Analytics. And the server-side rendering will inject a
`<script defer src=/static/js/ga.js>` in the HTML of every page, including home
page, site-search, `404.html` and article pages.
build-step will generate a `/static/js/gtag.js` file that configures how we
enable Google Analytics. And the server-side rendering will inject a
`<script defer src=/static/js/gtag.js>` in the HTML of every page, including
home page, site-search, `404.html` and article pages.

## Debugging

The best way to debugging it is to set two environment variables in your `.env`.

```bash
BUILD_GOOGLE_ANALYTICS_ACCOUNT=UA-00000000-0
BUILD_GOOGLE_ANALYTICS_DEBUG=true
```

That will ensure that the `https://www.google-analytics.com/analytics_debug.js`
file is used which uses `console.log()` to print out all sorts of information
about what it's sending to Google Analytics.

But note, when you use the `webpack` server (from `create-react-app`) that runs
on <http://localhost:3000> this will not be present. It's only present on pages
that are fully server-side rendered. So to test out what Google Analytics does,
make sure you use <http://localhost:5042>.
See:
[Troubleshooting with Tag Assistant](https://support.google.com/tagassistant/answer/10039345)

## Sending events

You can send individual arbitrary events in the client-side code. The best way
to describe how this works is to look a existing code.

Look for code that uses the `const ga = useGA()` hook and things that start
with...:
Look for code that uses the `const { gtag } = useGA()` hook and things that
start with...:

```javascript
ga("send", {
gtag("event", {
...
```
Expand All @@ -55,7 +42,7 @@ to wrap you send events with a conditional statement.
## Client-side navigation
By default, we send a `pageview` event as soon as the `ga.js` and the
By default, we send a `pageview` event as soon as the `gtag.js` and the
`https://www.google-analytics.com/analytics.js` code have both loaded. This
should happen as early as possible. Even before the `DOMContentLoaded` DOM
event. But we do use some client-side navigation and that will trigger a new
Expand Down
7 changes: 0 additions & 7 deletions libs/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ export const CSP_SCRIPT_SRC_VALUES = [
"'report-sample'",
"'self'",

// UA.
"www.google-analytics.com/analytics.js",
// GA4.
"https://www.googletagmanager.com/gtag/js",

Expand Down Expand Up @@ -107,8 +105,6 @@ export const CSP_DIRECTIVES = {
"updates.developer.allizom.org",
"updates.developer.mozilla.org",

// UA.
"www.google-analytics.com",
// GA4.
"https://*.google-analytics.com",
"https://*.analytics.google.com",
Expand Down Expand Up @@ -155,9 +151,6 @@ export const CSP_DIRECTIVES = {
"wikipedia.org",
"upload.wikimedia.org",

// UA.
"www.google-analytics.com",

// GA4.
"https://*.google-analytics.com",
"https://*.googletagmanager.com",
Expand Down
3 changes: 1 addition & 2 deletions libs/env/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ export const DEFAULT_FLAW_LEVELS: string;
export const BASE_URL: string;
export const FILES: string;
export const FOLDERSEARCH: string;
export const GOOGLE_ANALYTICS_ACCOUNT: string;
export const GOOGLE_ANALYTICS_DEBUG: boolean;
export const GOOGLE_ANALYTICS_MEASUREMENT_ID: string;
export const NO_PROGRESSBAR: boolean;
export const FIX_FLAWS: boolean;
export const FIX_FLAWS_DRY_RUN: boolean;
Expand Down
Loading

0 comments on commit 56dbe78

Please sign in to comment.