Skip to content

Commit

Permalink
Add a lint rule for using next script component when using inline scr…
Browse files Browse the repository at this point in the history
…ipt for Google Analytics.
  • Loading branch information
rgabs committed Jun 2, 2021
1 parent 288984b commit 47a7de7
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 1 deletion.
70 changes: 70 additions & 0 deletions errors/next-script-for-ga.md
@@ -0,0 +1,70 @@
# 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

#### Script component (experimental)

Use the Script component with the right loading strategy to defer loading of the script until necessary.

```jsx
import Script from 'next/experimental-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/experimental-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');
ga('send', 'pageview');
})
`}
</Script>
<Script
src="https://www.google-analytics.com/analytics.js"
strategy="lazyOnload"
></Script>
</div>
)
}

export default Home
```

Note: This is still an experimental feature and needs to be enabled via the `experimental.scriptLoader` flag in `next.config.js`.

### 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/)
60 changes: 60 additions & 0 deletions packages/eslint-plugin-next/lib/rules/next-script-for-ga.js
@@ -0,0 +1,60 @@
const NodeAttributes = require('../utils/nodeAttributes.js')

const GA_URL = 'www.google-analytics.com/analytics.js'
const ERROR_MSG =
'Use the Script component for loading third party scripts. See: https://nextjs.org/docs/messages/next-script-for-ga.'

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
if (
typeof attributes.value('src') === 'string' &&
attributes.value('src').includes(GA_URL)
) {
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
if (
attributes.has('dangerouslySetInnerHTML') &&
attributes.value('dangerouslySetInnerHTML')[0]
) {
const htmlContent = attributes.value('dangerouslySetInnerHTML')[0]
.value.quasis[0].value.raw

if (
htmlContent &&
htmlContent.includes('www.google-analytics.com/analytics.js')
) {
context.report({
node,
message: ERROR_MSG,
})
}
}
},
}
},
}

module.exports.schema = []
4 changes: 3 additions & 1 deletion packages/eslint-plugin-next/lib/utils/nodeAttributes.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
129 changes: 129 additions & 0 deletions test/eslint-plugin-next/next-script-for-ga.unit.test.js
@@ -0,0 +1,129 @@
const rule = require('@next/eslint-plugin-next/lib/rules/next-script-for-ga')

const RuleTester = require('eslint').RuleTester

const ERROR_MSG =
'Use the Script component for loading third party scripts. See: https://nextjs.org/docs/messages/next-script-for-ga.'

RuleTester.setDefaultConfig({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
modules: true,
jsx: true,
},
},
})

var ruleTester = new RuleTester()
ruleTester.run('sync-scripts', rule, {
valid: [
`import Script from 'next/experimental-script'
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<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>
);
}
}`,
`import Script from 'next/experimental-script'
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<Script>
{\`window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
})\`}
</Script>
</div>
);
}
}`,
`export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<script dangerouslySetInnerHTML={{}} />
</div>
);
}
}`,
],

invalid: [
{
code: `
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<script dangerouslySetInnerHTML={{
__html: \`
(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');
\`,
}}/>
</div>
);
}
}`,
errors: [
{
message: ERROR_MSG,
type: 'JSXOpeningElement',
},
],
},
{
code: `
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<script dangerouslySetInnerHTML={{
__html: \`
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
\`,
}}/>
<script async src='https://www.google-analytics.com/analytics.js'></script>
</div>
);
}
}`,
errors: [
{
message: ERROR_MSG,
type: 'JSXOpeningElement',
},
],
},
],
})

0 comments on commit 47a7de7

Please sign in to comment.