Repository: vercel/next.js
Pull request: Eslint: Improve Google Tag manager third parties message

Note: new PR description written by @timneutkens 

## What?

Improves the detection of Google Tag Manager vs Google Analytics.
As indicated by the author of this PR it was confusing that when you have Google Tag Manager set up it gives you an error message saying you're adding Google Analytics even though it's Tag Manager.

I've updated the PR to have a separate message for Google Tag Manager and expanded the error message docs too.

It also mentioned `next/script` in a bunch of places even though the error doc recommends `@next/third-parties`. I've updated all cases to refer to `@next/third-parties` instead.

<details>
<summary>Previous PR description</summary>
Google has multiple products with similar names: the "Google Tag" and "Google Tag Manager". Google Tag Manager uses `gtm.js` whereas the "Google Tag" uses `gtag`. Google Analytics previously used `analytics.js` (<2017) and is now delivered via the "Google Tag".

Google Tag: `www.googletagmanager.com/gtag/js`
Google Tag Manager: `www.googletagmanager.com/gtm.js`

The `next-script-for-ga.ts` rule's matching list (`SUPPORTED_HTML_CONTENT_URLS`) includes `gtm.js` which means when a developer adds Google Tag Manager to their NextJS website, they'll receive the warning and be encouraged to swap from `gtm.js` to `gtag`. A developer asleep at the wheel (...me) may think...

> I'm using Google Analytics via Google Tag Manager, and NextJS is suggesting I swap `gtm.js` for `gtag` because it will improve performance. `gtag` sounds like it is part of "Google Tag Manager" so I am going to follow the instructions so that my website is faster.

However, `gtag` is **not** compatible with `gtm.js` (they're different products with similar names and similar purposes) and so making this change will cause very confusing Google Tag Manager behaviour. I am suggesting that `gtm.js` is removed completely from the rule, because anyone using `gtm.js` should not be following these instructions. A separate rule could be added specifically for Google Tag Manager, that does not refer to `gtag`.
</details>

Changed files:
filename: errors/next-script-for-ga.mdx
patch: @@ -1,8 +1,8 @@
 ---
-title: Using Google Analytics with Next.js (through `next/script`)
+title: Using Google Analytics with Next.js (through `@next/third-parties/google`)
 ---
 
-> Prefer `next/script` component when using the inline script for Google Analytics.
+> Prefer `@next/third-parties/google` when using the inline script for Google Analytics and Tag Manager.
 
 ## Why This Error Occurred
 
@@ -63,11 +63,42 @@ export default function Page() {
 }
 ```
 
-> **Good to know:**
->
-> - If you are using the Pages Router, please refer to the [`pages/` documentation](/docs/pages/guides/third-party-libraries).
-> - `@next/third-parties` also supports [Google Tag Manager](/docs/app/guides/third-party-libraries#google-tag-manager) and other third parties.
-> - Using `@next/third-parties` is not required. You can also use the `next/script` component directly. Refer to the [`next/script` documentation](/docs/app/guides/scripts) to learn more.
+### Use `@next/third-parties` to add Google Tag Manager
+
+The `GoogleTagManager` component can be used to add [Google Tag Manager](https://developers.google.com/tag-manager/quickstart) to your page.
+
+```tsx filename="app/layout.tsx" switcher
+import { GoogleTagManager } from '@next/third-parties/google'
+
+export default function RootLayout({
+  children,
+}: {
+  children: React.ReactNode
+}) {
+  return (
+    <html lang="en">
+      <GoogleTagManager gtmId="GTM-XYZ" />
+      <body>{children}</body>
+    </html>
+  )
+}
+```
+
+To load Google Tag Manager for a single route, include the component in your page file:
+
+```jsx filename="app/page.js"
+import { GoogleTagManager } from '@next/third-parties/google'
+
+export default function Page() {
+  return <GoogleTagManager gtmId="GTM-XYZ" />
+}
+```
+
+## Good to know
+
+- If you are using the Pages Router, please refer to the [`pages/` documentation](/docs/pages/guides/third-party-libraries).
+- `@next/third-parties` also supports [other third parties](/docs/app/guides/third-party-libraries#google-tag-manager).
+- Using `@next/third-parties` is not required. You can also use the `next/script` component directly. Refer to the [`next/script` documentation](/docs/app/guides/scripts) to learn more.
 
 ## Useful Links
 

filename: packages/eslint-plugin-next/src/rules/next-script-for-ga.ts
patch: @@ -1,23 +1,17 @@
 import { defineRule } from '../utils/define-rule'
 import NodeAttributes from '../utils/node-attributes'
 
-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 GOOGLE_ANALYTICS_URL = 'www.google-analytics.com/analytics.js'
+const GOOGLE_TAG_MANAGER_URL = 'www.googletagmanager.com/gtag/js'
+
+const GOOGLE_ANALYTICS_SRC = GOOGLE_ANALYTICS_URL
+const GOOGLE_TAG_MANAGER_SRC = 'www.googletagmanager.com/gtm.js'
+
 const description =
-  'Prefer `next/script` component when using the inline script for Google Analytics.'
+  'Prefer `@next/third-parties/google` when using the inline script for Google Analytics and Tag Manager.'
 const url = 'https://nextjs.org/docs/messages/next-script-for-ga'
-const ERROR_MSG = `${description} See: ${url}`
-
-// 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))
-}
+const ERROR_MSG_GOOGLE_ANALYTICS = `Prefer \`GoogleAnalytics\` component from \`@next/third-parties/google\` when using the inline script for Google Analytics. See: ${url}`
+const ERROR_MSG_GOOGLE_TAG_MANAGER = `Prefer \`GoogleTagManager\` component from \`@next/third-parties/google\` when using the inline script for Google Tag Manager. See: ${url}`
 
 export default defineRule({
   meta: {
@@ -40,37 +34,46 @@ export default defineRule({
         }
         const attributes = new NodeAttributes(node)
 
+        const src = attributes.value('src')
         // 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)
+        if (typeof src === 'string' && src.includes(GOOGLE_ANALYTICS_URL)) {
+          return context.report({
+            node,
+            message: ERROR_MSG_GOOGLE_ANALYTICS,
+          })
+        } else if (
+          typeof src === 'string' &&
+          src.includes(GOOGLE_TAG_MANAGER_URL)
         ) {
           return context.report({
             node,
-            message: ERROR_MSG,
+            message: ERROR_MSG_GOOGLE_TAG_MANAGER,
           })
         }
 
+        const dangerouslySetInnerHTML = attributes.value(
+          'dangerouslySetInnerHTML'
+        )
         // 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.value('dangerouslySetInnerHTML') &&
-          attributes.value('dangerouslySetInnerHTML').length > 0
-        ) {
-          const htmlContent =
-            attributes.value('dangerouslySetInnerHTML')[0].value.quasis &&
-            attributes.value('dangerouslySetInnerHTML')[0].value.quasis[0].value
-              .raw
-          if (
+        if (dangerouslySetInnerHTML && dangerouslySetInnerHTML.length > 0) {
+          const quasis = dangerouslySetInnerHTML[0].value.quasis
+          const htmlContent = quasis?.[0]?.value?.raw
+          if (htmlContent && htmlContent.includes(GOOGLE_ANALYTICS_SRC)) {
+            context.report({
+              node,
+              message: ERROR_MSG_GOOGLE_ANALYTICS,
+            })
+          } else if (
             htmlContent &&
-            containsStr(htmlContent, SUPPORTED_HTML_CONTENT_URLS)
+            htmlContent.includes(GOOGLE_TAG_MANAGER_SRC)
           ) {
             context.report({
               node,
-              message: ERROR_MSG,
+              message: ERROR_MSG_GOOGLE_TAG_MANAGER,
             })
           }
         }

filename: test/unit/eslint-plugin-next/next-script-for-ga.test.ts
patch: @@ -3,8 +3,9 @@ import { rules } from '@next/eslint-plugin-next'
 
 const NextESLintRule = rules['next-script-for-ga']
 
-const ERROR_MSG =
-  'Prefer `next/script` component when using the inline script for Google Analytics. See: https://nextjs.org/docs/messages/next-script-for-ga'
+const url = 'https://nextjs.org/docs/messages/next-script-for-ga'
+const ERROR_MSG_GOOGLE_ANALYTICS = `Prefer \`GoogleAnalytics\` component from \`@next/third-parties/google\` when using the inline script for Google Analytics. See: ${url}`
+const ERROR_MSG_GOOGLE_TAG_MANAGER = `Prefer \`GoogleTagManager\` component from \`@next/third-parties/google\` when using the inline script for Google Tag Manager. See: ${url}`
 
 const tests = {
   valid: [
@@ -108,36 +109,7 @@ const tests = {
       }`,
       errors: [
         {
-          message: ERROR_MSG,
-          type: 'JSXOpeningElement',
-        },
-      ],
-    },
-    {
-      code: `
-        export class Blah extends Head {
-          render() {
-            return (
-              <div>
-                <h1>Hello title</h1> qqq
-                {/* Google Tag Manager - Global base code */}
-                <script
-                dangerouslySetInnerHTML={{
-                  __html: \`
-                    (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
-                    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
-                    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
-                    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
-                    })(window,document,'script','dataLayer', '\${GTM_ID}');
-                  \`,
-                }}/>
-              </div>
-            );
-          }
-      }`,
-      errors: [
-        {
-          message: ERROR_MSG,
+          message: ERROR_MSG_GOOGLE_TAG_MANAGER,
           type: 'JSXOpeningElement',
         },
       ],
@@ -166,7 +138,7 @@ const tests = {
       }`,
       errors: [
         {
-          message: ERROR_MSG,
+          message: ERROR_MSG_GOOGLE_ANALYTICS,
           type: 'JSXOpeningElement',
         },
       ],
@@ -192,7 +164,7 @@ const tests = {
       }`,
       errors: [
         {
-          message: ERROR_MSG,
+          message: ERROR_MSG_GOOGLE_ANALYTICS,
           type: 'JSXOpeningElement',
         },
       ],
@@ -222,7 +194,7 @@ const tests = {
       }`,
       errors: [
         {
-          message: ERROR_MSG,
+          message: ERROR_MSG_GOOGLE_ANALYTICS,
           type: 'JSXOpeningElement',
         },
       ],