Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby-plugin-google-analytics): enable core webvitals tracking #31665

Merged
merged 28 commits into from
Jun 15, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b756021
feat(gatsby-plugin-google-analytics): enable core webvitals tracking
wardpeet May 31, 2021
96a0a61
feat(gatsby-plugin-google-tagmanager): enable core webvitals tracking
wardpeet May 31, 2021
e738a1d
add readme
wardpeet May 31, 2021
8feae67
update tests
wardpeet Jun 2, 2021
f9f44b6
add disableWebVitalsTracking to options
wardpeet Jun 2, 2021
0701972
Merge branch 'master' into feat/web-vitals
wardpeet Jun 2, 2021
2451bcb
Merge branch 'master' into feat/web-vitals
wardpeet Jun 2, 2021
b341c28
Merge branch 'master' into feat/web-vitals
wardpeet Jun 3, 2021
d99a67d
change disable to enable
wardpeet Jun 14, 2021
e0d9126
adjust snapshot
pieh Jun 14, 2021
3191feb
Update packages/gatsby-plugin-google-analytics/README.md
wardpeet Jun 14, 2021
8d5a983
update review
wardpeet Jun 14, 2021
0c00415
Update README.md
wardpeet Jun 14, 2021
64ab5c1
Update README.md
LekoArts Jun 15, 2021
0e65489
sync readme
pieh Jun 15, 2021
d342d83
adjust tests after switching to base variant of web-vitals
pieh Jun 15, 2021
127e11c
always measure
wardpeet Jun 15, 2021
4f9f3f0
update tests
wardpeet Jun 15, 2021
06820fa
add tests for ssr
wardpeet Jun 15, 2021
a94bd69
Update packages/gatsby-plugin-google-analytics/src/gatsby-browser.js
wardpeet Jun 15, 2021
aa50b09
Update packages/gatsby-plugin-google-analytics/src/gatsby-browser.js
wardpeet Jun 15, 2021
af9f71e
Update packages/gatsby-plugin-google-tagmanager/src/__tests__/gatsby-…
wardpeet Jun 15, 2021
a1c95ef
Update packages/gatsby-plugin-google-tagmanager/src/gatsby-browser.js
wardpeet Jun 15, 2021
65b9c27
Update packages/gatsby-plugin-google-analytics/src/gatsby-browser.js
wardpeet Jun 15, 2021
69013e6
Update packages/gatsby-plugin-google-analytics/src/gatsby-browser.js
wardpeet Jun 15, 2021
285a2a3
fix lint
pieh Jun 15, 2021
1a15e08
don't load in dev
wardpeet Jun 15, 2021
d05937e
Merge branch 'master' into feat/web-vitals
wardpeet Jun 15, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/gatsby-plugin-google-analytics/.babelrc
Original file line number Diff line number Diff line change
@@ -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 }]]
}
]
}
6 changes: 6 additions & 0 deletions packages/gatsby-plugin-google-analytics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ module.exports = {
sampleRate: 5,
siteSpeedSampleRate: 10,
cookieDomain: "example.com",
// defaults to false
enableWebVitalsTracking: true,
},
},
],
Expand Down Expand Up @@ -133,6 +135,10 @@ If you need to set up SERVER_SIDE Google Optimize experiment, you can add the ex

Besides the experiment ID you also need the variation ID for SERVER_SIDE experiments in Google Optimize. Set 0 for original version.

### `enableWebVitalsTracking`

Optimizing for the quality of user experience is key to the long-term success of any site on the web. Capturing Real user metrics (RUM) helps you understand the experience of your user/customer. By setting `enableWebVitalsTracking` to `true`, Google Analytics will get "core-web-vitals" events with their values.

## Optional Fields

This plugin supports all optional Create Only Fields documented in [Google Analytics](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#create):
Expand Down
3 changes: 2 additions & 1 deletion packages/gatsby-plugin-google-analytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
},
"dependencies": {
"@babel/runtime": "^7.14.0",
"minimatch": "3.0.4"
"minimatch": "3.0.4",
"web-vitals": "^1.1.2"
},
"devDependencies": {
"@babel/cli": "^7.14.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -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/base"

jest.mock(`web-vitals/base`, () => {
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`, () => {
Expand Down Expand Up @@ -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`, () => {
Expand Down Expand Up @@ -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({}, { enableWebVitalsTracking: true })

// 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({}, { enableWebVitalsTracking: false })

// 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()
})
})
})
})
Expand Down
39 changes: 37 additions & 2 deletions packages/gatsby-plugin-google-analytics/src/gatsby-browser.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,33 @@
function sendWebVitals() {
return import(`web-vitals/base`).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
}
Expand All @@ -16,8 +45,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
Expand All @@ -26,3 +55,9 @@ export const onRouteUpdate = ({ location }, pluginOptions = {}) => {

return null
}

export function onClientEntry(_, pluginOptions) {
if (pluginOptions.enableWebVitalsTracking) {
sendWebVitals()
}
}
1 change: 1 addition & 0 deletions packages/gatsby-plugin-google-analytics/src/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ exports.pluginOptionsSchema = ({ Joi }) =>
queueTime: Joi.number(),
forceSSL: Joi.boolean(),
transport: Joi.string(),
enableWebVitalsTracking: Joi.boolean().default(false),
})
26 changes: 23 additions & 3 deletions packages/gatsby-plugin-google-analytics/src/gatsby-ssr.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,25 @@ export const onRenderBody = (
const setComponents = pluginOptions.head
? setHeadComponents
: setPostBodyComponents
return setComponents([

const inlineScripts = []
if (pluginOptions.enableWebVitalsTracking) {
// web-vitals/polyfill (necessary for non chromium browsers)
// @seehttps://www.npmjs.com/package/web-vitals#how-the-polyfill-works
inlineScripts.push(
pieh marked this conversation as resolved.
Show resolved Hide resolved
<script
key={`gatsby-plugin-google-analytics-web-vitals`}
data-gatsby="web-vitals-polyfill"
dangerouslySetInnerHTML={{
__html: `
!function(){var e,t,n,i,r={passive:!0,capture:!0},a=new Date,o=function(){i=[],t=-1,e=null,f(addEventListener)},c=function(i,r){e||(e=r,t=i,n=new Date,f(removeEventListener),u())},u=function(){if(t>=0&&t<n-a){var r={entryType:"first-input",name:e.type,target:e.target,cancelable:e.cancelable,startTime:e.timeStamp,processingStart:e.timeStamp+t};i.forEach((function(e){e(r)})),i=[]}},s=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){c(e,t),a()},i=function(){a()},a=function(){removeEventListener("pointerup",n,r),removeEventListener("pointercancel",i,r)};addEventListener("pointerup",n,r),addEventListener("pointercancel",i,r)}(t,e):c(t,e)}},f=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,s,r)}))},p="hidden"===document.visibilityState?0:1/0;addEventListener("visibilitychange",(function e(t){"hidden"===document.visibilityState&&(p=t.timeStamp,removeEventListener("visibilitychange",e,!0))}),!0);o(),self.webVitals={firstInputPolyfill:function(e){i.push(e),u()},resetFirstInputPolyfill:o,get firstHiddenTime(){return p}}}();
`,
wardpeet marked this conversation as resolved.
Show resolved Hide resolved
}}
/>
)
}

inlineScripts.push(
<script
key={`gatsby-plugin-google-analytics`}
dangerouslySetInnerHTML={{
Expand Down Expand Up @@ -134,6 +152,8 @@ export const onRenderBody = (
}, ``)}
}`,
}}
/>,
])
/>
)

return setComponents(inlineScripts)
}
8 changes: 8 additions & 0 deletions packages/gatsby-plugin-google-tagmanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ plugins: [
//
// Defaults to gatsby-route-change
routeChangeEventName: "YOUR_ROUTE_CHANGE_EVENT_NAME",
// Defaults to false
enableWebVitalsTracking: true,
},
},
]
Expand Down Expand Up @@ -80,6 +82,12 @@ This plugin will fire a new event called `gatsby-route-change` (or as in the `ga

This tag will now catch every route change in Gatsby, and you can add Google tag services as you wish to it.

#### Tracking Core Web Vitals

Optimizing for the quality of user experience is key to the long-term success of any site on the web. Capturing Real user metrics (RUM) helps you understand the experience of your user/customer. By setting `enableWebVitalsTracking` to `true`, GTM will get "core-web-vitals" events with their values.

You can save this data in Google Analytics or any database of your choosing.

#### Note

Out of the box this plugin will simply load Google Tag Manager on the initial page/app load. It's up to you to fire tags based on changes in your app. See the above "Tracking routes" section for an example.
3 changes: 2 additions & 1 deletion packages/gatsby-plugin-google-tagmanager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"url": "https://github.com/gatsbyjs/gatsby/issues"
},
"dependencies": {
"@babel/runtime": "^7.14.0"
"@babel/runtime": "^7.14.0",
"web-vitals": "^1.1.2"
},
"devDependencies": {
"@babel/cli": "^7.14.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
import { getLCP, getFID, getCLS } from "web-vitals/base"

jest.mock(`web-vitals/base`, () => {
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`))
}),
}
})

const getAPI = setup => {
if (setup) {
setup()
Expand All @@ -8,12 +28,19 @@ const getAPI = setup => {
return require(`../gatsby-browser`)
}

let currentNodeEnv
beforeEach(() => {
currentNodeEnv = process.env.NODE_ENV
window.dataLayer = []
jest.useFakeTimers()
process.env.NODE_ENV = undefined
})

afterEach(() => {
jest.useRealTimers()
process.env.NODE_ENV = currentNodeEnv
})

describe(`onRouteUpdate`, () => {
it(`does not register if NODE_ENV is not production`, () => {
const { onRouteUpdate } = getAPI(() => {
Expand Down Expand Up @@ -110,4 +137,76 @@ describe(`onRouteUpdate`, () => {

expect(window[dataLayerName]).toHaveLength(1)
})

it(`sends core web vitals when enabled`, async () => {
jest.useRealTimers()
const { onClientEntry } = getAPI(() => {
process.env.NODE_ENV = `production`
})
onClientEntry({}, { enableWebVitalsTracking: true })

// wait 2 ticks to wait for dynamic import to resolve
await Promise.resolve()
await Promise.resolve()

expect(window.dataLayer.length).toBe(3)
expect(window.dataLayer).toContainEqual({
event: `core-web-vitals`,
webVitalsMeasurement: {
name: `CLS`,
id: `3`,
value: 100,
},
})
expect(window.dataLayer).toContainEqual({
event: `core-web-vitals`,
webVitalsMeasurement: {
name: `FID`,
id: `2`,
value: 150,
},
})
expect(window.dataLayer).toEqual([
{
event: `core-web-vitals`,
webVitalsMeasurement: {
name: `CLS`,
id: `3`,
value: 100,
},
},
{
event: `core-web-vitals`,
webVitalsMeasurement: {
name: `FID`,
id: `2`,
value: 150,
},
},
{
event: `core-web-vitals`,
webVitalsMeasurement: {
name: `LCP`,
id: `1`,
value: 300,
},
},
])
})

it(`sends nothing when web vitals tracking is disabled`, async () => {
jest.useRealTimers()
const { onClientEntry } = getAPI(() => {
process.env.NODE_ENV = `production`
})
onClientEntry({}, { enableWebVitalsTracking: false })

// 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()
})
})
Loading