From b756021a1f0a7f8f943c8ede789acf44be72b240 Mon Sep 17 00:00:00 2001 From: Ward Peeters Date: Mon, 31 May 2021 16:51:24 +0200 Subject: [PATCH 01/24] feat(gatsby-plugin-google-analytics): enable core webvitals tracking --- .../gatsby-plugin-google-analytics/.babelrc | 8 +- .../package.json | 4 +- .../src/__tests__/gatsby-browser.js | 78 ++++++++++++++++++- .../src/gatsby-browser.js | 41 +++++++++- .../src/gatsby-node.js | 1 + .../src/gatsby-ssr.js | 24 +++++- yarn.lock | 5 ++ 7 files changed, 151 insertions(+), 10 deletions(-) diff --git a/packages/gatsby-plugin-google-analytics/.babelrc b/packages/gatsby-plugin-google-analytics/.babelrc index 31043522b2321..7094b00b614c2 100644 --- a/packages/gatsby-plugin-google-analytics/.babelrc +++ b/packages/gatsby-plugin-google-analytics/.babelrc @@ -1,3 +1,9 @@ { - "presets": [["babel-preset-gatsby-package", { "browser": true }]] + "presets": [["babel-preset-gatsby-package"]], + "overrides": [ + { + "test": ["**/gatsby-browser.js"], + "presets": [["babel-preset-gatsby-package", { "browser": true, "esm": true }]] + } + ] } diff --git a/packages/gatsby-plugin-google-analytics/package.json b/packages/gatsby-plugin-google-analytics/package.json index adbd285110cec..e64d0ea39bf26 100644 --- a/packages/gatsby-plugin-google-analytics/package.json +++ b/packages/gatsby-plugin-google-analytics/package.json @@ -7,8 +7,8 @@ "url": "https://github.com/gatsbyjs/gatsby/issues" }, "dependencies": { - "@babel/runtime": "^7.12.5", - "minimatch": "3.0.4" + "minimatch": "3.0.4", + "web-vitals": "^1.1.2" }, "devDependencies": { "@babel/cli": "^7.12.1", 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 06d2267458d00..fe4c0734f3754 100644 --- a/packages/gatsby-plugin-google-analytics/src/__tests__/gatsby-browser.js +++ b/packages/gatsby-plugin-google-analytics/src/__tests__/gatsby-browser.js @@ -1,5 +1,24 @@ -import { onRouteUpdate } from "../gatsby-browser" +import { onClientEntry, onRouteUpdate } from "../gatsby-browser" import { Minimatch } from "minimatch" +import { getLCP, getFID, getCLS } from "web-vitals" + +jest.mock(`web-vitals`, () => { + function createEntry(type, id, delta) { + return { name: type, id, delta } + } + + return { + getLCP: jest.fn(report => { + report(createEntry(`LCP`, `1`, `300`)) + }), + getFID: jest.fn(report => { + report(createEntry(`FID`, `2`, `150`)) + }), + getCLS: jest.fn(report => { + report(createEntry(`CLS`, `3`, `0.10`)) + }), + } +}) describe(`gatsby-plugin-google-analytics`, () => { describe(`gatsby-browser`, () => { @@ -28,11 +47,12 @@ describe(`gatsby-plugin-google-analytics`, () => { beforeEach(() => { jest.useFakeTimers() + jest.clearAllMocks() window.ga = jest.fn() }) afterEach(() => { - jest.resetAllMocks() + jest.useRealTimers() }) it(`does not send page view when ga is undefined`, () => { @@ -85,6 +105,60 @@ describe(`gatsby-plugin-google-analytics`, () => { expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000) expect(window.ga).toHaveBeenCalledTimes(2) }) + + it(`sends core web vitals when enabled`, async () => { + jest.useRealTimers() + onClientEntry({}, { disableWebVitalsTracking: false }) + + // wait 2 ticks to wait for dynamic import to resolve + await Promise.resolve() + await Promise.resolve() + + expect(window.ga).toBeCalledTimes(3) + expect(window.ga).toBeCalledWith( + `send`, + `event`, + expect.objectContaining({ + eventAction: `LCP`, + eventCategory: `Web Vitals`, + eventLabel: `1`, + eventValue: 300, + }) + ) + expect(window.ga).toBeCalledWith( + `send`, + `event`, + expect.objectContaining({ + eventAction: `FID`, + eventCategory: `Web Vitals`, + eventLabel: `2`, + eventValue: 150, + }) + ) + expect(window.ga).toBeCalledWith( + `send`, + `event`, + expect.objectContaining({ + eventAction: `CLS`, + eventCategory: `Web Vitals`, + eventLabel: `3`, + eventValue: 100, + }) + ) + }) + + it(`sends nothing when web vitals tracking is disabled`, async () => { + jest.useRealTimers() + onClientEntry({}, { disableWebVitalsTracking: true }) + + // wait 2 ticks to wait for dynamic import to resolve + await Promise.resolve() + await Promise.resolve() + + expect(getLCP).not.toBeCalled() + expect(getFID).not.toBeCalled() + expect(getCLS).not.toBeCalled() + }) }) }) }) diff --git a/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js b/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js index 2b73f8e254bc4..d18ad1a3224db 100644 --- a/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js +++ b/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js @@ -1,4 +1,35 @@ +function sendWebVitals() { + return import(/* webpackMode: "lazy-once" */ `web-vitals`).then( + ({ getLCP, getFID, getCLS }) => { + getCLS(sendToGoogleAnalytics) + getFID(sendToGoogleAnalytics) + getLCP(sendToGoogleAnalytics) + } + ) +} + +function sendToGoogleAnalytics({ name, delta, id }) { + window.ga(`send`, `event`, { + eventCategory: `Web Vitals`, + eventAction: name, + // The `id` value will be unique to the current page load. When sending + // multiple values from the same page (e.g. for CLS), Google Analytics can + // compute a total by grouping on this ID (note: requires `eventLabel` to + // be a dimension in your report). + eventLabel: id, + // Google Analytics metrics must be integers, so the value is rounded. + // For CLS the value is first multiplied by 1000 for greater precision + // (note: increase the multiplier for greater precision if needed). + eventValue: Math.round(name === `CLS` ? delta * 1000 : delta), + // Use a non-interaction event to avoid affecting bounce rate. + nonInteraction: true, + // Use `sendBeacon()` if the browser supports it. + transport: `beacon`, + }) +} + export const onRouteUpdate = ({ location }, pluginOptions = {}) => { + const ga = window.ga if (process.env.NODE_ENV !== `production` || typeof ga !== `function`) { return null } @@ -16,8 +47,8 @@ export const onRouteUpdate = ({ location }, pluginOptions = {}) => { const pagePath = location ? location.pathname + location.search + location.hash : undefined - window.ga(`set`, `page`, pagePath) - window.ga(`send`, `pageview`) + ga(`set`, `page`, pagePath) + ga(`send`, `pageview`) } // Minimum delay for reactHelmet's requestAnimationFrame @@ -26,3 +57,9 @@ export const onRouteUpdate = ({ location }, pluginOptions = {}) => { return null } + +export function onClientEntry(_, pluginOptions) { + if (!pluginOptions.disableWebVitalsTracking) { + sendWebVitals() + } +} diff --git a/packages/gatsby-plugin-google-analytics/src/gatsby-node.js b/packages/gatsby-plugin-google-analytics/src/gatsby-node.js index bbc57071e63f6..24f89b6204cf6 100644 --- a/packages/gatsby-plugin-google-analytics/src/gatsby-node.js +++ b/packages/gatsby-plugin-google-analytics/src/gatsby-node.js @@ -54,4 +54,5 @@ exports.pluginOptionsSchema = ({ Joi }) => queueTime: Joi.number(), forceSSL: Joi.boolean(), transport: Joi.string(), + disableWebVitalsTracking: Joi.boolean().default(true), }) diff --git a/packages/gatsby-plugin-google-analytics/src/gatsby-ssr.js b/packages/gatsby-plugin-google-analytics/src/gatsby-ssr.js index 1afa395dc63b6..ccb65ba6b38ec 100644 --- a/packages/gatsby-plugin-google-analytics/src/gatsby-ssr.js +++ b/packages/gatsby-plugin-google-analytics/src/gatsby-ssr.js @@ -66,7 +66,23 @@ export const onRenderBody = ( const setComponents = pluginOptions.head ? setHeadComponents : setPostBodyComponents - return setComponents([ + + const inlineScripts = [] + if (!pluginOptions.disableWebVitalsTracking) { + inlineScripts.push( +