Skip to content

Commit

Permalink
ESLint Plugin: Prefer next script component when using the inline scr…
Browse files Browse the repository at this point in the history
…ipt for Google Analytics. (#25147)

* Add a lint rule for using next script component when using inline script for Google Analytics.

* Apply suggestions from code review

Co-authored-by: JJ Kasper <jj@jjsweb.site>

* Update errors/next-script-for-ga.md

Co-authored-by: JJ Kasper <jj@jjsweb.site>

* Apply suggestions from code review

Co-authored-by: JJ Kasper <jj@jjsweb.site>
  • Loading branch information
rgabs and ijjk committed Aug 16, 2021
1 parent 385ac94 commit da4d652
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 1 deletion.
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 &lt;title&gt; 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
98 changes: 98 additions & 0 deletions errors/next-script-for-ga.md
@@ -0,0 +1,98 @@
# 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) script to add analytics, use the `next/script` component with the right loading strategy to defer loading of the script until necessary.

```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) script to add analytics:

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

const Home = () => {
return (
<div class="container">
<Script>
{`
(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');
`}
</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', 'GOOGLE_ANALYTICS_ID', 'auto');
ga('send', 'pageview');
`}
</Script>
<Script
src="https://www.google-analytics.com/analytics.js"
strategy="lazyOnload"
></Script>
</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/)
- [next/script Documentation](https://nextjs.org/docs/basic-features/script)
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

0 comments on commit da4d652

Please sign in to comment.