diff --git a/packages/gatsby-plugin-google-analytics/README.md b/packages/gatsby-plugin-google-analytics/README.md index e4e4c29ac1b9d..6b64c4c387bb7 100644 --- a/packages/gatsby-plugin-google-analytics/README.md +++ b/packages/gatsby-plugin-google-analytics/README.md @@ -24,6 +24,8 @@ module.exports = { respectDNT: true, // Avoids sending pageview hits from custom paths exclude: ["/preview/**", "/do-not-track/me/too/"], + // Delays sending pageview hits on route update (in milliseconds) + pageTransitionDelay: 0, // Enables Google Optimize using your container Id optimizeId: "YOUR_GOOGLE_OPTIMIZE_TRACKING_ID", // Enables Google Optimize Experiment ID @@ -102,6 +104,10 @@ If you enable this optional option, Google Analytics will not be loaded at all f If you need to exclude any path from the tracking system, you can add it (one or more) to this optional array as glob expressions. +### `pageTransitionDelay` + +If your site uses any custom transitions on route update (e.g. [`gatsby-plugin-transition-link`](https://www.gatsbyjs.org/blog/2018-12-04-per-link-gatsby-page-transitions-with-transitionlink/)), then you can delay processing the page view event until the new page is mounted. + ### `optimizeId` If you need to use Google Optimize for A/B testing, you can add this optional Optimize container id to allow Google Optimize to load the correct test parameters for your site. diff --git a/packages/gatsby-plugin-google-analytics/src/__tests__/gatsby-browser.js b/packages/gatsby-plugin-google-analytics/src/__tests__/gatsby-browser.js index 123e5b21ea698..06d2267458d00 100644 --- a/packages/gatsby-plugin-google-analytics/src/__tests__/gatsby-browser.js +++ b/packages/gatsby-plugin-google-analytics/src/__tests__/gatsby-browser.js @@ -1,8 +1,6 @@ import { onRouteUpdate } from "../gatsby-browser" import { Minimatch } from "minimatch" -jest.useFakeTimers() - describe(`gatsby-plugin-google-analytics`, () => { describe(`gatsby-browser`, () => { describe(`onRouteUpdate`, () => { @@ -29,10 +27,12 @@ describe(`gatsby-plugin-google-analytics`, () => { }) beforeEach(() => { + jest.useFakeTimers() window.ga = jest.fn() - window.requestAnimationFrame = jest.fn(cb => { - cb() - }) + }) + + afterEach(() => { + jest.resetAllMocks() }) it(`does not send page view when ga is undefined`, () => { @@ -40,7 +40,9 @@ describe(`gatsby-plugin-google-analytics`, () => { onRouteUpdate({}) - expect(window.requestAnimationFrame).not.toHaveBeenCalled() + jest.runAllTimers() + + expect(setTimeout).not.toHaveBeenCalled() }) it(`does not send page view when path is excluded`, () => { @@ -53,23 +55,34 @@ describe(`gatsby-plugin-google-analytics`, () => { }, }) + jest.runAllTimers() + expect(window.ga).not.toHaveBeenCalled() }) it(`sends page view`, () => { onRouteUpdate({}) + jest.runAllTimers() + expect(window.ga).toHaveBeenCalledTimes(2) }) - it(`uses setTimeout when requestAnimationFrame is undefined`, () => { - delete window.requestAnimationFrame - + it(`uses setTimeout with a minimum delay of 32ms`, () => { onRouteUpdate({}) jest.runAllTimers() - expect(setTimeout).toHaveBeenCalledTimes(1) + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 32) + expect(window.ga).toHaveBeenCalledTimes(2) + }) + + it(`uses setTimeout with the provided pageTransitionDelay value`, () => { + onRouteUpdate({}, { pageTransitionDelay: 1000 }) + + jest.runAllTimers() + + expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000) expect(window.ga).toHaveBeenCalledTimes(2) }) }) diff --git a/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js b/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js index 1808774c05171..2b73f8e254bc4 100644 --- a/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js +++ b/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js @@ -1,4 +1,4 @@ -export const onRouteUpdate = ({ location }) => { +export const onRouteUpdate = ({ location }, pluginOptions = {}) => { if (process.env.NODE_ENV !== `production` || typeof ga !== `function`) { return null } @@ -11,7 +11,7 @@ export const onRouteUpdate = ({ location }) => { if (pathIsExcluded) return null // wrap inside a timeout to make sure react-helmet is done with it's changes (https://github.com/gatsbyjs/gatsby/issues/9139) - // reactHelmet is using requestAnimationFrame so we should use it too: https://github.com/nfl/react-helmet/blob/5.2.0/src/HelmetUtils.js#L296-L299 + // reactHelmet is using requestAnimationFrame: https://github.com/nfl/react-helmet/blob/5.2.0/src/HelmetUtils.js#L296-L299 const sendPageView = () => { const pagePath = location ? location.pathname + location.search + location.hash @@ -20,14 +20,9 @@ export const onRouteUpdate = ({ location }) => { window.ga(`send`, `pageview`) } - if (`requestAnimationFrame` in window) { - requestAnimationFrame(() => { - requestAnimationFrame(sendPageView) - }) - } else { - // simulate 2 rAF calls - setTimeout(sendPageView, 32) - } + // Minimum delay for reactHelmet's requestAnimationFrame + const delay = Math.max(32, pluginOptions.pageTransitionDelay || 0) + setTimeout(sendPageView, delay) return null }