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

ESLint Plugin: Prefer next script component when using the inline script for Google Analytics. #25147

Merged
merged 8 commits into from Aug 16, 2021
1 change: 1 addition & 0 deletions docs/basic-features/eslint.md
Expand Up @@ -95,6 +95,7 @@ Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/
| ✔️ | [next/no-title-in-document-head](https://nextjs.org/docs/messages/no-title-in-document-head) | Disallow using <title> with Head from next/document |
| ✔️ | [next/no-unwanted-polyfillio](https://nextjs.org/docs/messages/no-unwanted-polyfillio) | Prevent duplicate polyfills from Polyfill.io |
| ✔️ | next/no-typos | Ensure no typos were made declaring [Next.js's data fetching function](https://nextjs.org/docs/basic-features/data-fetching) |
| ✔️ | [next/next-script-for-ga](https://nextjs.org/docs/messages/next-script-for-ga) | Use the Script component to defer loading of the script until necessary. |

- ✔: Enabled in the recommended configuration

Expand Down
99 changes: 99 additions & 0 deletions errors/next-script-for-ga.md
@@ -0,0 +1,99 @@
# Next Script for Google Analytics

### Why This Error Occurred

An inline script was used for Google analytics which might impact your webpage's performance.

### Possible Ways to Fix It

#### Using gtag.js

If you are using the [gtag.js](https://developers.google.com/analytics/devguides/collection/gtagjs) to add analytics, use the `next/script` component with the right loading strategy to defer loading of the script until necessary.
ijjk marked this conversation as resolved.
Show resolved Hide resolved

```jsx
import Script from 'next/script'

const Home = () => {
return (
<div class="container">
<!-- Global site tag (gtag.js) - Google Analytics -->
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
strategy="lazyOnload"
></Script>
<Script>
{`
window.dataLayer = window.dataLayer || [];
function gtag(){window.dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', 'GA_MEASUREMENT_ID');
`}
</Script>
</div>
)
}

export default Home
```

#### Using analytics.js

If you are using the [analytics.js](https://developers.google.com/analytics/devguides/collection/analyticsjs) to add analytics:
ijjk marked this conversation as resolved.
Show resolved Hide resolved

```jsx
import Script from 'next/script'

const Home = () => {
return (
<div class="container">
<Script>
{`
ijjk marked this conversation as resolved.
Show resolved Hide resolved
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
})
rgabs marked this conversation as resolved.
Show resolved Hide resolved
`}
</Script>
</div>
)
}

export default Home
```

If you are using the [alternative async variant](https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag):

```jsx
import Script from 'next/script'

const Home = () => {
return (
<div class="container">
<Script>
{`
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-XXXXX-Y', 'auto');
rgabs marked this conversation as resolved.
Show resolved Hide resolved
ga('send', 'pageview');
})
rgabs marked this conversation as resolved.
Show resolved Hide resolved
`}
</Script>
<Script
src="https://www.google-analytics.com/analytics.js"
strategy="lazyOnload"
></Script>
rgabs marked this conversation as resolved.
Show resolved Hide resolved
</div>
)
}

export default Home
```

### Useful Links

- [Add analytics.js to Your Site](https://developers.google.com/analytics/devguides/collection/analyticsjs)
- [Efficiently load third-party JavaScript](https://web.dev/efficiently-load-third-party-javascript/)
rgabs marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions packages/eslint-plugin-next/lib/index.js
Expand Up @@ -16,6 +16,7 @@ module.exports = {
'no-script-in-head': require('./rules/no-script-in-head'),
'no-typos': require('./rules/no-typos'),
'no-duplicate-head': require('./rules/no-duplicate-head'),
'next-script-for-ga': require('./rules/next-script-for-ga'),
},
configs: {
recommended: {
Expand All @@ -31,6 +32,7 @@ module.exports = {
'@next/next/google-font-display': 1,
'@next/next/google-font-preconnect': 1,
'@next/next/link-passhref': 1,
'@next/next/next-script-for-ga': 1,
'@next/next/no-document-import-in-page': 2,
'@next/next/no-head-import-in-document': 2,
'@next/next/no-script-in-document': 2,
Expand Down
77 changes: 77 additions & 0 deletions packages/eslint-plugin-next/lib/rules/next-script-for-ga.js
@@ -0,0 +1,77 @@
const NodeAttributes = require('../utils/node-attributes.js')

const SUPPORTED_SRCS = [
'www.google-analytics.com/analytics.js',
'www.googletagmanager.com/gtag/js',
]
const SUPPORTED_HTML_CONTENT_URLS = [
'www.google-analytics.com/analytics.js',
'www.googletagmanager.com/gtm.js',
]
const ERROR_MSG =
'Use the `next/script` component for loading third party scripts. See: https://nextjs.org/docs/messages/next-script-for-ga.'

// Check if one of the items in the list is a substring of the passed string
const containsStr = (str, strList) => {
return strList.some((s) => str.includes(s))
}

module.exports = {
meta: {
docs: {
description:
'Prefer next script component when using the inline script for Google Analytics',
recommended: true,
},
},
create: function (context) {
return {
JSXOpeningElement(node) {
if (node.name.name !== 'script') {
return
}
if (node.attributes.length === 0) {
return
}
const attributes = new NodeAttributes(node)

// Check if the Alternative async tag is being used to add GA.
// https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag
// https://developers.google.com/analytics/devguides/collection/gtagjs
if (
typeof attributes.value('src') === 'string' &&
containsStr(attributes.value('src'), SUPPORTED_SRCS)
) {
return context.report({
node,
message: ERROR_MSG,
})
}

// Check if inline script is being used to add GA.
// https://developers.google.com/analytics/devguides/collection/analyticsjs#the_google_analytics_tag
// https://developers.google.com/tag-manager/quickstart
if (
attributes.has('dangerouslySetInnerHTML') &&
attributes.value('dangerouslySetInnerHTML')[0]
) {
const htmlContent =
attributes.value('dangerouslySetInnerHTML')[0].value.quasis &&
attributes.value('dangerouslySetInnerHTML')[0].value.quasis[0].value
.raw
if (
htmlContent &&
containsStr(htmlContent, SUPPORTED_HTML_CONTENT_URLS)
) {
context.report({
node,
message: ERROR_MSG,
})
}
}
},
}
},
}

module.exports.schema = []
4 changes: 3 additions & 1 deletion packages/eslint-plugin-next/lib/utils/node-attributes.js
Expand Up @@ -26,7 +26,9 @@ class NodeAttributes {
this.attributes[attribute.name.name].value = attribute.value.value
} else if (attribute.value.expression) {
this.attributes[attribute.name.name].value =
attribute.value.expression.value
typeof attribute.value.expression.value !== 'undefined'
? attribute.value.expression.value
: attribute.value.expression.properties
}
}
})
Expand Down