From 15f097338b77ebfbcab9077b463525910c3aba73 Mon Sep 17 00:00:00 2001
From: Michael Lee <lee.5151@osu.edu>
Date: Mon, 2 May 2022 11:25:24 -0400
Subject: [PATCH] Upgrading drupal/google_analytics (2.5.0 => 4.0.0)

---
 composer.json                                 |  32 +-
 composer.lock                                 |  34 +-
 vendor/composer/installed.json                |  36 +-
 vendor/composer/installed.php                 |  16 +-
 .../Iterator/MultiplePcreFilterIterator.php   |   8 +-
 web/modules/google_analytics/.eslintrc        |   2 +-
 web/modules/google_analytics/README.md        |   3 +-
 web/modules/google_analytics/composer.json    |   8 +-
 .../install/google_analytics.settings.yml     |   6 +-
 .../config/schema/google_analytics.schema.yml |  33 +-
 .../google_analytics.info.yml                 |   8 +-
 .../google_analytics/google_analytics.install |  79 +-
 .../google_analytics.links.menu.yml           |   2 +-
 .../google_analytics/google_analytics.module  | 525 +++-----------
 .../google_analytics.routing.yml              |   2 +-
 .../google_analytics.services.yml             |  47 ++
 .../js/google_analytics.admin.js              |   3 +
 .../js/google_analytics.debug.js              |  57 +-
 .../google_analytics/js/google_analytics.js   |  56 +-
 .../d6_google_analytics_settings.yml          |   2 +-
 .../d6_google_analytics_user_settings.yml     |   2 +-
 .../d7_google_analytics_settings.yml          |   2 +-
 .../d7_google_analytics_user_settings.yml     |   2 +-
 .../src/Constants/GoogleAnalyticsEvents.php   |  65 ++
 .../src/Constants/GoogleAnalyticsPatterns.php |  29 +
 .../src/Event/GoogleAnalyticsConfigEvent.php  |  84 +++
 .../src/Event/GoogleAnalyticsEventsEvent.php  |  47 ++
 .../src/Event/PagePathEvent.php               |  36 +
 .../GoogleAnalyticsConfig/CustomConfig.php    | 154 ++++
 .../GoogleAnalyticsConfig/DefaultConfig.php   | 142 ++++
 .../GoogleAnalyticsEvents/DrupalMessage.php   |  72 ++
 .../GoogleAnalyticsEventBase.php              |  85 +++
 .../PagePath/ContentTranslation.php           | 101 +++
 .../EventSubscriber/PagePath/HttpStatus.php   |  76 ++
 .../PagePath/InvalidUserLogin.php             |  68 ++
 .../src/EventSubscriber/PagePath/Search.php   |  92 +++
 .../Form/GoogleAnalyticsAdminSettingsForm.php | 685 ++++++++++++------
 .../google_analytics/src/GaAccount.php        |  45 ++
 .../src/GaJavascriptInterface.php             |  63 ++
 .../src/GaJavascriptObject.php                | 146 ++++
 .../src/GoogleAnalitycsInterface.php          |  15 -
 .../src/Helpers/GoogleAnalyticsAccounts.php   | 112 +++
 .../src/Helpers/VisiblityTracker.php          | 172 +++++
 .../src/JavascriptLocalCache.php              | 138 ++++
 .../GoogleAnalyticsVisibilityPages.php        |  25 +-
 .../GoogleAnalyticsVisibilityRoles.php        |   2 +-
 .../Tests}/GoogleAnalyticsJavaScriptTest.js   |   0
 .../google_analytics_test.info.yml            |   6 +-
 .../GoogleAnalyticsTestController.php         |   2 +-
 .../Functional/GoogleAnalyticsBasicTest.php   | 196 +++--
 ...nalyticsCustomDimensionsAndMetricsTest.php | 220 +++---
 .../Functional/GoogleAnalyticsCustomUrls.php  |  38 +-
 .../Functional/GoogleAnalyticsRolesTest.php   |  17 +-
 .../Functional/GoogleAnalyticsSearchTest.php  |  48 +-
 .../GoogleAnalyticsStatusMessagesTest.php     |  81 ++-
 .../GoogleAnalyticsUninstallTest.php          |  40 +-
 .../GoogleAnalyticsUserFieldsTest.php         |  32 +-
 .../GoogleAnalyticsFormValidationTest.php     | 147 ++++
 .../GoogleAnalyticsAdminSettingsFormTest.php  |  68 ++
 59 files changed, 3236 insertions(+), 1078 deletions(-)
 create mode 100644 web/modules/google_analytics/google_analytics.services.yml
 create mode 100644 web/modules/google_analytics/src/Constants/GoogleAnalyticsEvents.php
 create mode 100644 web/modules/google_analytics/src/Constants/GoogleAnalyticsPatterns.php
 create mode 100644 web/modules/google_analytics/src/Event/GoogleAnalyticsConfigEvent.php
 create mode 100644 web/modules/google_analytics/src/Event/GoogleAnalyticsEventsEvent.php
 create mode 100644 web/modules/google_analytics/src/Event/PagePathEvent.php
 create mode 100644 web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsConfig/CustomConfig.php
 create mode 100644 web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsConfig/DefaultConfig.php
 create mode 100644 web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsEvents/DrupalMessage.php
 create mode 100644 web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsEvents/GoogleAnalyticsEventBase.php
 create mode 100644 web/modules/google_analytics/src/EventSubscriber/PagePath/ContentTranslation.php
 create mode 100644 web/modules/google_analytics/src/EventSubscriber/PagePath/HttpStatus.php
 create mode 100644 web/modules/google_analytics/src/EventSubscriber/PagePath/InvalidUserLogin.php
 create mode 100644 web/modules/google_analytics/src/EventSubscriber/PagePath/Search.php
 create mode 100644 web/modules/google_analytics/src/GaAccount.php
 create mode 100644 web/modules/google_analytics/src/GaJavascriptInterface.php
 create mode 100644 web/modules/google_analytics/src/GaJavascriptObject.php
 delete mode 100644 web/modules/google_analytics/src/GoogleAnalitycsInterface.php
 create mode 100644 web/modules/google_analytics/src/Helpers/GoogleAnalyticsAccounts.php
 create mode 100644 web/modules/google_analytics/src/Helpers/VisiblityTracker.php
 create mode 100644 web/modules/google_analytics/src/JavascriptLocalCache.php
 rename web/modules/google_analytics/{tests/src/Functional => src/Tests}/GoogleAnalyticsJavaScriptTest.js (100%)
 create mode 100644 web/modules/google_analytics/tests/src/FunctionalJavascript/GoogleAnalyticsFormValidationTest.php
 create mode 100644 web/modules/google_analytics/tests/src/Kernel/Form/GoogleAnalyticsAdminSettingsFormTest.php

diff --git a/composer.json b/composer.json
index a7a456af7a..7b9f890c76 100644
--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,8 @@
     "description": "OSU ASC Pantheon custom upstream for Drupal 8",
     "type": "project",
     "license": "None",
-    "repositories": [{
+    "repositories": [
+        {
             "type": "composer",
             "url": "https://packages.drupal.org/8"
         },
@@ -121,7 +122,7 @@
         "drupal/field_permissions": "1.1",
         "drupal/file_browser": "1.3",
         "drupal/focal_point": "1.5",
-        "drupal/google_analytics": "2.5",
+        "drupal/google_analytics": "^4.0",
         "drupal/google_tag": "1.4",
         "drupal/honeypot": "2.0.1",
         "drupal/inline_entity_form": "1.0-rc9",
@@ -225,12 +226,27 @@
     },
     "extra": {
         "installer-paths": {
-            "web/core": ["type:drupal-core"],
-            "web/libraries/{$name}": ["type:drupal-library", "enyo/dropzone", "desandro/masonry", "dimsemenov/magnific-popup"],
-            "web/modules/{$name}": ["type:drupal-module"],
-            "web/profiles/contrib/{$name}": ["type:drupal-profile"],
-            "web/themes/{$name}": ["type:drupal-theme"],
-            "drush/contrib/{$name}": ["type:drupal-drush"]
+            "web/core": [
+                "type:drupal-core"
+            ],
+            "web/libraries/{$name}": [
+                "type:drupal-library",
+                "enyo/dropzone",
+                "desandro/masonry",
+                "dimsemenov/magnific-popup"
+            ],
+            "web/modules/{$name}": [
+                "type:drupal-module"
+            ],
+            "web/profiles/contrib/{$name}": [
+                "type:drupal-profile"
+            ],
+            "web/themes/{$name}": [
+                "type:drupal-theme"
+            ],
+            "drush/contrib/{$name}": [
+                "type:drupal-drush"
+            ]
         },
         "build-env": {
             "install-cms": [
diff --git a/composer.lock b/composer.lock
index 8548e94276..a74df31f82 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "09537e7c3c425c2a37646d9578cdef43",
+    "content-hash": "ddbb6bfcc5492e80908375b84ecb45bb",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -4502,20 +4502,20 @@
         },
         {
             "name": "drupal/google_analytics",
-            "version": "2.5.0",
+            "version": "4.0.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/google_analytics.git",
-                "reference": "8.x-2.5"
+                "reference": "4.0.0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/google_analytics-8.x-2.5.zip",
-                "reference": "8.x-2.5",
-                "shasum": "9e0ff72cc313bf9295fe8bd73a68f5f7688513ab"
+                "url": "https://ftp.drupal.org/files/projects/google_analytics-4.0.0.zip",
+                "reference": "4.0.0",
+                "shasum": "4f761d4c852d11966f7289b0eb6431cc8db27240"
             },
             "require": {
-                "drupal/core": "^8.8.6|^9.0"
+                "drupal/core": "^8.9|^9.0"
             },
             "require-dev": {
                 "drupal/token": "^1.7"
@@ -4523,15 +4523,15 @@
             "type": "drupal-module",
             "extra": {
                 "drupal": {
-                    "version": "8.x-2.5",
-                    "datestamp": "1591298527",
+                    "version": "4.0.0",
+                    "datestamp": "1634230238",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
                     }
                 },
                 "branch-alias": {
-                    "dev-8.x-2.x": "2.x-dev"
+                    "dev-4.x": "4.x-dev"
                 }
             },
             "notification-url": "https://packages.drupal.org/8/downloads",
@@ -4567,7 +4567,7 @@
             "description": "Allows your site to be tracked by Google Analytics by adding a Javascript tracking code to every page.",
             "homepage": "https://www.drupal.org/project/google_analytics",
             "support": {
-                "source": "https://git.drupal.org/project/google_analytics.git",
+                "source": "https://git.drupalcode.org/project/google_analytics",
                 "issues": "https://www.drupal.org/project/issues/google_analytics"
             }
         },
@@ -13241,16 +13241,16 @@
         },
         {
             "name": "symfony/finder",
-            "version": "v4.4.37",
+            "version": "v4.4.41",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "b17d76d7ed179f017aad646e858c90a2771af15d"
+                "reference": "40790bdf293b462798882ef6da72bb49a4a6633a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/b17d76d7ed179f017aad646e858c90a2771af15d",
-                "reference": "b17d76d7ed179f017aad646e858c90a2771af15d",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/40790bdf293b462798882ef6da72bb49a4a6633a",
+                "reference": "40790bdf293b462798882ef6da72bb49a4a6633a",
                 "shasum": ""
             },
             "require": {
@@ -13283,7 +13283,7 @@
             "description": "Finds files and directories via an intuitive fluent interface",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/finder/tree/v4.4.37"
+                "source": "https://github.com/symfony/finder/tree/v4.4.41"
             },
             "funding": [
                 {
@@ -13299,7 +13299,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-01-02T09:41:36+00:00"
+            "time": "2022-04-14T15:36:10+00:00"
         },
         {
             "name": "symfony/http-client-contracts",
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index fa7044b23f..d763e090d0 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -4647,21 +4647,21 @@
         },
         {
             "name": "drupal/google_analytics",
-            "version": "2.5.0",
-            "version_normalized": "2.5.0.0",
+            "version": "4.0.0",
+            "version_normalized": "4.0.0.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/google_analytics.git",
-                "reference": "8.x-2.5"
+                "reference": "4.0.0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/google_analytics-8.x-2.5.zip",
-                "reference": "8.x-2.5",
-                "shasum": "9e0ff72cc313bf9295fe8bd73a68f5f7688513ab"
+                "url": "https://ftp.drupal.org/files/projects/google_analytics-4.0.0.zip",
+                "reference": "4.0.0",
+                "shasum": "4f761d4c852d11966f7289b0eb6431cc8db27240"
             },
             "require": {
-                "drupal/core": "^8.8.6|^9.0"
+                "drupal/core": "^8.9|^9.0"
             },
             "require-dev": {
                 "drupal/token": "^1.7"
@@ -4669,15 +4669,15 @@
             "type": "drupal-module",
             "extra": {
                 "drupal": {
-                    "version": "8.x-2.5",
-                    "datestamp": "1591298527",
+                    "version": "4.0.0",
+                    "datestamp": "1634230238",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
                     }
                 },
                 "branch-alias": {
-                    "dev-8.x-2.x": "2.x-dev"
+                    "dev-4.x": "4.x-dev"
                 }
             },
             "installation-source": "dist",
@@ -4714,7 +4714,7 @@
             "description": "Allows your site to be tracked by Google Analytics by adding a Javascript tracking code to every page.",
             "homepage": "https://www.drupal.org/project/google_analytics",
             "support": {
-                "source": "https://git.drupal.org/project/google_analytics.git",
+                "source": "https://git.drupalcode.org/project/google_analytics",
                 "issues": "https://www.drupal.org/project/issues/google_analytics"
             },
             "install-path": "../../web/modules/google_analytics"
@@ -13623,24 +13623,24 @@
         },
         {
             "name": "symfony/finder",
-            "version": "v4.4.37",
-            "version_normalized": "4.4.37.0",
+            "version": "v4.4.41",
+            "version_normalized": "4.4.41.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "b17d76d7ed179f017aad646e858c90a2771af15d"
+                "reference": "40790bdf293b462798882ef6da72bb49a4a6633a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/b17d76d7ed179f017aad646e858c90a2771af15d",
-                "reference": "b17d76d7ed179f017aad646e858c90a2771af15d",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/40790bdf293b462798882ef6da72bb49a4a6633a",
+                "reference": "40790bdf293b462798882ef6da72bb49a4a6633a",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.1.3",
                 "symfony/polyfill-php80": "^1.16"
             },
-            "time": "2022-01-02T09:41:36+00:00",
+            "time": "2022-04-14T15:36:10+00:00",
             "type": "library",
             "installation-source": "dist",
             "autoload": {
@@ -13668,7 +13668,7 @@
             "description": "Finds files and directories via an intuitive fluent interface",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/finder/tree/v4.4.37"
+                "source": "https://github.com/symfony/finder/tree/v4.4.41"
             },
             "funding": [
                 {
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index ee29eafea0..64e96a5dd3 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -5,7 +5,7 @@
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
-        'reference' => '002a337b01d47db573b437ed8bdc143bf2bb617b',
+        'reference' => '51f5173881510201397d977bee2cd820809999e7',
         'name' => 'osu-asc-webservices/d8-upstream',
         'dev' => true,
     ),
@@ -1016,12 +1016,12 @@
             ),
         ),
         'drupal/google_analytics' => array(
-            'pretty_version' => '2.5.0',
-            'version' => '2.5.0.0',
+            'pretty_version' => '4.0.0',
+            'version' => '4.0.0.0',
             'type' => 'drupal-module',
             'install_path' => __DIR__ . '/../../web/modules/google_analytics',
             'aliases' => array(),
-            'reference' => '8.x-2.5',
+            'reference' => '4.0.0',
             'dev_requirement' => false,
         ),
         'drupal/google_tag' => array(
@@ -2101,7 +2101,7 @@
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
-            'reference' => '002a337b01d47db573b437ed8bdc143bf2bb617b',
+            'reference' => '51f5173881510201397d977bee2cd820809999e7',
             'dev_requirement' => false,
         ),
         'pantheon-systems/quicksilver-pushback' => array(
@@ -2709,12 +2709,12 @@
             'dev_requirement' => false,
         ),
         'symfony/finder' => array(
-            'pretty_version' => 'v4.4.37',
-            'version' => '4.4.37.0',
+            'pretty_version' => 'v4.4.41',
+            'version' => '4.4.41.0',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/finder',
             'aliases' => array(),
-            'reference' => 'b17d76d7ed179f017aad646e858c90a2771af15d',
+            'reference' => '40790bdf293b462798882ef6da72bb49a4a6633a',
             'dev_requirement' => false,
         ),
         'symfony/http-client-contracts' => array(
diff --git a/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php b/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php
index 18b082ec0b..e185d13001 100644
--- a/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php
+++ b/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php
@@ -83,7 +83,13 @@ protected function isAccepted($string)
      */
     protected function isRegex($str)
     {
-        if (preg_match('/^(.{3,}?)[imsxuADU]*$/', $str, $m)) {
+        $availableModifiers = 'imsxuADU';
+
+        if (\PHP_VERSION_ID >= 80200) {
+            $availableModifiers .= 'n';
+        }
+
+        if (preg_match('/^(.{3,}?)['.$availableModifiers.']*$/', $str, $m)) {
             $start = substr($m[1], 0, 1);
             $end = substr($m[1], -1);
 
diff --git a/web/modules/google_analytics/.eslintrc b/web/modules/google_analytics/.eslintrc
index 9bff1793be..0000df370c 100644
--- a/web/modules/google_analytics/.eslintrc
+++ b/web/modules/google_analytics/.eslintrc
@@ -1,5 +1,5 @@
 {
   "globals": {
-    "ga": true
+    "gtag": true
   }
 }
diff --git a/web/modules/google_analytics/README.md b/web/modules/google_analytics/README.md
index 1042e58f8f..e19befaf34 100644
--- a/web/modules/google_analytics/README.md
+++ b/web/modules/google_analytics/README.md
@@ -77,7 +77,7 @@ One example for custom dimensions tracking is the "User roles" tracking.
        e.g. "User roles". This step is required. Do not miss it, please.
 
     2. Enter the below configuration data into the Drupal custom dimensions
-       settings form under admin/config/system/googleanalytics. You can also
+       settings form under admin/config/services/googleanalytics. You can also
        choose another index, but keep it always in sync with the index used in
        step #1.
 
@@ -111,6 +111,7 @@ Body:
 ```
 <ul>
   <li><a href="mailto:foo@example.com">Mailto</a></li>
+  <li><a href="tel:+1-303-499-7111">Tel</a></li>
   <li><a href="/files/test.txt">Download file</a></li>
   <li><a class="colorbox" href="#">Open colorbox</a></li>
   <li><a href="https://example.com/">External link</a></li>
diff --git a/web/modules/google_analytics/composer.json b/web/modules/google_analytics/composer.json
index 9896261191..2baa01c86e 100644
--- a/web/modules/google_analytics/composer.json
+++ b/web/modules/google_analytics/composer.json
@@ -11,18 +11,18 @@
   ],
   "support": {
     "issues": "https://www.drupal.org/project/issues/google_analytics",
-    "source": "https://git.drupal.org/project/google_analytics.git"
+    "source": "https://git.drupalcode.org/project/google_analytics"
   },
   "license": "GPL-2.0-or-later",
   "require": {
-    "drupal/core": "^8.8.6|^9.0"
+    "drupal/core": "^8.9|^9.0"
   },
   "require-dev": {
     "drupal/token": "^1.7"
   },
   "extra": {
     "branch-alias": {
-      "dev-8.x-2.x": "2.x-dev"
+      "dev-4.x": "4.x-dev"
     }
   }
-}
+}
\ No newline at end of file
diff --git a/web/modules/google_analytics/config/install/google_analytics.settings.yml b/web/modules/google_analytics/config/install/google_analytics.settings.yml
index fc9cfdffca..59397fbd38 100644
--- a/web/modules/google_analytics/config/install/google_analytics.settings.yml
+++ b/web/modules/google_analytics/config/install/google_analytics.settings.yml
@@ -11,6 +11,7 @@ visibility:
 track:
   outbound: true
   mailto: true
+  tel: true
   files: true
   files_extensions: '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip'
   colorbox: true
@@ -20,12 +21,11 @@ track:
   messages: {  }
   site_search: false
   adsense: false
-  displayfeatures: false
+  displayfeatures: true
 privacy:
   anonymizeip: true
 custom:
-  dimension: {  }
-  metric: {  }
+  parameters: {  }
 codesnippet:
   create: {  }
   before: ''
diff --git a/web/modules/google_analytics/config/schema/google_analytics.schema.yml b/web/modules/google_analytics/config/schema/google_analytics.schema.yml
index aa3aa732dd..cb15e3b116 100644
--- a/web/modules/google_analytics/config/schema/google_analytics.schema.yml
+++ b/web/modules/google_analytics/config/schema/google_analytics.schema.yml
@@ -48,6 +48,9 @@ google_analytics.settings:
         mailto:
           type: boolean
           label: 'Track clicks on mailto links'
+        tel:
+          type: boolean
+          label: 'Track clicks on tel links'
         files:
           type: boolean
           label: 'Track downloads'
@@ -92,29 +95,19 @@ google_analytics.settings:
       type: mapping
       label: 'Custom variables'
       mapping:
-        dimension:
+        parameters:
           type: sequence
-          label: 'Custom dimensions'
+          label: 'Custom Parameters'
           sequence:
             type: mapping
-            label: 'Dimension'
+            label: 'Parameter'
             mapping:
-              index:
-                type: integer
-                label: Index
-              value:
+              type:
                 type: string
-                label: Value
-        metric:
-          type: sequence
-          label: 'Custom metrics'
-          sequence:
-            type: mapping
-            label: 'Metric'
-            mapping:
-              index:
-                type: integer
-                label: Index
+                label: type
+              name:
+                type: string
+                label: Name
               value:
                 type: string
                 label: Value
@@ -124,10 +117,10 @@ google_analytics.settings:
       mapping:
         create:
           type: sequence
-          label: 'Create only fields'
+          label: 'Parameters'
           sequence:
             type: ignore
-            label: 'Create field'
+            label: 'Parameter'
         before:
           type: string
           label: 'Code snippet (before)'
diff --git a/web/modules/google_analytics/google_analytics.info.yml b/web/modules/google_analytics/google_analytics.info.yml
index 60bae23d9a..ae9d3fa3b6 100644
--- a/web/modules/google_analytics/google_analytics.info.yml
+++ b/web/modules/google_analytics/google_analytics.info.yml
@@ -2,10 +2,10 @@ name: 'Google Analytics'
 type: module
 description: 'Allows your site to be tracked by Google Analytics by adding a Javascript tracking code to every page.'
 package: Statistics
-core_version_requirement: ^8.8.6 || ^9
+core_version_requirement: ^8.9 || ^9
 configure: google_analytics.admin_settings_form
 
-# Information added by Drupal.org packaging script on 2020-06-04
-version: '8.x-2.5'
+# Information added by Drupal.org packaging script on 2021-10-14
+version: '4.0.0'
 project: 'google_analytics'
-datestamp: 1591298498
+datestamp: 1634230241
diff --git a/web/modules/google_analytics/google_analytics.install b/web/modules/google_analytics/google_analytics.install
index 13a558ac08..9ae7fbc310 100644
--- a/web/modules/google_analytics/google_analytics.install
+++ b/web/modules/google_analytics/google_analytics.install
@@ -28,7 +28,8 @@ function google_analytics_install() {
  * Remove cache directory if module is uninstalled.
  */
 function google_analytics_uninstall() {
-  google_analytics_clear_js_cache();
+  $javascript_service = \Drupal::service('google_analytics.javascript_cache');
+  $javascript_service->clearGoogleAnalyticsJsCache();
 }
 
 /**
@@ -39,9 +40,10 @@ function google_analytics_requirements($phase) {
 
   if ($phase == 'runtime') {
     $config = \Drupal::config('google_analytics.settings');
+    $ga_accounts = \Drupal::service('google_analytics.accounts');
 
     // Raise warning if Google user account has not been set yet.
-    if (!preg_match('/^UA-\d+-\d+$/', $config->get('account'))) {
+    if (!$ga_accounts->getDefaultMeasurementId()) {
       $requirements['google_analytics_account'] = [
         'title' => t('Google Analytics module'),
         'description' => t('Google Analytics module has not been configured yet. Please configure its settings from the <a href=":url">Google Analytics settings page</a>.', [':url' => Url::fromRoute('google_analytics.admin_settings_form')->toString()]),
@@ -72,3 +74,76 @@ function google_analytics_requirements($phase) {
 
   return $requirements;
 }
+
+/**
+ * Migrate create only fields to gtag.js parameters.
+ */
+function google_analytics_update_8300() {
+  $config = \Drupal::configFactory()->getEditable('google_analytics.settings');
+  $create_only_fields = $config->get('codesnippet.create');
+  $parameters = [
+    'client_id' => $create_only_fields['clientId'],
+    'cookie_name' => $create_only_fields['cookieName'],
+    'cookie_domain' => $create_only_fields['cookieDomain'],
+    'cookie_expires' => $create_only_fields['cookieExpires'],
+    'sample_rate' => $create_only_fields['sampleRate'],
+    'site_speed_sample_rate' => $create_only_fields['siteSpeedSampleRate'],
+    'use_amp_client_id' => $create_only_fields['useAmpClientId'],
+    'user_id' => $create_only_fields['userId'],
+  ];
+  $parameters = array_filter($parameters);
+
+  $config
+    ->set('codesnippet.create', $parameters)
+    ->save();
+
+  return t('Migrated create only fields to gtag.js parameters.');
+}
+
+/**
+ * Set default config for tel: link tracking.
+ */
+function google_analytics_update_8301() {
+  \Drupal::configFactory()
+    ->getEditable('google_analytics.settings')
+    ->set('track.tel', TRUE)
+    ->save();
+}
+
+/**
+ * Update existing custom dimensions and metrics to user parameters.
+ */
+function google_analytics_update_8400() {
+  $config = \Drupal::configFactory()->getEditable('google_analytics.settings');
+  $custom_parameters = [];
+  $custom_dimensions = $config->getOriginal('custom.dimension');
+  $custom_metrics = $config->getOriginal('custom.metric');
+
+  // Merge Dimensions
+  if (!empty($custom_dimensions)) {
+    foreach ($custom_dimensions as $key => $dimension) {
+      $custom_parameters['dimension' . $key]['type'] = 'dimension';
+      $custom_parameters['dimension' . $key]['name'] = $dimension['name'];
+      $custom_parameters['dimension' . $key]['value'] = $dimension['value'];
+    }
+  }
+
+  // Merge Metrics
+  if (!empty($custom_metrics)) {
+    foreach ($custom_metrics as $key => $metric) {
+      $custom_parameters['metric' . $key]['type'] = 'metric';
+      $custom_parameters['metric' . $key]['name'] = $metric['name'];
+      $custom_parameters['metric' . $key]['value'] = $metric['value'];
+    }
+  }
+
+  if (!empty($custom_parameters)) {
+    $config->set('custom.parameters', $custom_parameters);
+  }
+  // Remove the legacy settings.
+  $config->clear('custom.metric');
+  $config->clear('custom.dimension');
+
+  // Save the settings
+  $config->save();
+}
\ No newline at end of file
diff --git a/web/modules/google_analytics/google_analytics.links.menu.yml b/web/modules/google_analytics/google_analytics.links.menu.yml
index 276c39affd..bb6bc0c12d 100644
--- a/web/modules/google_analytics/google_analytics.links.menu.yml
+++ b/web/modules/google_analytics/google_analytics.links.menu.yml
@@ -1,5 +1,5 @@
 google_analytics.admin_settings_form:
   title: 'Google Analytics'
-  parent: system.admin_config_system
+  parent: system.admin_config_services
   description: 'Configure tracking behavior to get insights into your website traffic and marketing effectiveness.'
   route_name: google_analytics.admin_settings_form
diff --git a/web/modules/google_analytics/google_analytics.module b/web/modules/google_analytics/google_analytics.module
index df3be4bddf..35848b624b 100644
--- a/web/modules/google_analytics/google_analytics.module
+++ b/web/modules/google_analytics/google_analytics.module
@@ -7,27 +7,31 @@
  * Adds the required Javascript to all your Drupal pages to allow tracking by
  * the Google Analytics statistics package.
  *
- * @author: Alexander Hass <https://drupal.org/user/85918>
  */
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\Crypt;
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Site\Settings;
 use Drupal\Core\Url;
+use Drupal\google_analytics\Event\BuildGaJavascriptEvent;
+use Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent;
+use Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent;
+use Drupal\google_analytics\Event\PagePathEvent;
+use Drupal\google_analytics\GaJavascriptObject;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
 use Drupal\node\NodeInterface;
 use GuzzleHttp\Exception\RequestException;
 use Drupal\google_analytics\Component\Render\GoogleAnalyticsJavaScriptSnippet;
+use Drupal\Core\File\FileSystemInterface;
 
 /**
  * Advertise the supported google analytics api details.
  */
 function google_analytics_api() {
   return [
-    'api' => 'analytics.js',
+    'api' => 'gtag.js',
   ];
 }
 
@@ -59,9 +63,12 @@ function google_analytics_help($route_name, RouteMatchInterface $route_match) {
 function google_analytics_page_attachments(array &$page) {
   $account = \Drupal::currentUser();
   $config = \Drupal::config('google_analytics.settings');
-  $id = $config->get('account');
   $request = \Drupal::request();
-  $base_path = base_path();
+
+  /** @var \Drupal\google_analytics\Helpers\VisiblityTracker $visibilityTracker */
+  $visibilityTracker = \Drupal::service('google_analytics.visibility');
+  /** @var \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts $ga_accounts */
+  $ga_accounts = \Drupal::service('google_analytics.accounts');
 
   // Add module cache tags.
   $page['#cache']['tags'] = Cache::mergeTags(isset($page['#cache']['tags']) ? $page['#cache']['tags'] : [], $config->getCacheTags());
@@ -82,19 +89,26 @@ function google_analytics_page_attachments(array &$page) {
   // 2. Track page views based on visibility value.
   // 3. Check if we should track the currently active user's role.
   // 4. Ignore pages visibility filter for 404 or 403 status codes.
-  if (preg_match('/^UA-\d+-\d+$/', $id) && (_google_analytics_visibility_pages() || in_array($status, $trackable_status_codes)) && _google_analytics_visibility_user($account)) {
+  if ($ga_accounts->getDefaultMeasurementId() && ($visibilityTracker->getVisibilityPages() || in_array($status, $trackable_status_codes)) && $visibilityTracker->getUserVisibilty($account)) {
+    $default_id = $ga_accounts->getDefaultMeasurementId();
     // Init variables.
     $debug = $config->get('debug');
     $url_custom = '';
 
+    // Instantiate our event.
+    $javascript = new GaJavascriptObject($default_id);
+
     // Add link tracking.
-    $link_settings = [];
+    $link_settings = ['account' => $default_id];
     if ($track_outbound = $config->get('track.outbound')) {
       $link_settings['trackOutbound'] = $track_outbound;
     }
     if ($track_mailto = $config->get('track.mailto')) {
       $link_settings['trackMailto'] = $track_mailto;
     }
+    if ($track_tel = $config->get('track.tel')) {
+      $link_settings['trackTel'] = $track_tel;
+    }
     if (($track_download = $config->get('track.files')) && ($trackfiles_extensions = $config->get('track.files_extensions'))) {
       $link_settings['trackDownload'] = $track_download;
       $link_settings['trackDownloadExtensions'] = $trackfiles_extensions;
@@ -110,7 +124,6 @@ function google_analytics_page_attachments(array &$page) {
     }
     if ($track_url_fragments = $config->get('track.urlfragments')) {
       $link_settings['trackUrlFragments'] = $track_url_fragments;
-      $url_custom = 'location.pathname + location.search + location.hash';
     }
 
     if (!empty($link_settings)) {
@@ -129,222 +142,85 @@ function google_analytics_page_attachments(array &$page) {
       }
     }
 
-    // Add messages tracking.
-    $message_events = '';
-    if ($message_types = $config->get('track.messages')) {
-      $message_types = array_values(array_filter($message_types));
-      $status_heading = [
-        'status' => t('Status message'),
-        'warning' => t('Warning message'),
-        'error' => t('Error message'),
-      ];
-
-      foreach (\Drupal::messenger()->all(NULL, FALSE) as $type => $messages) {
-        // Track only the selected message types.
-        if (in_array($type, $message_types)) {
-          foreach ($messages as $message) {
-            // @todo: Track as exceptions?
-            $message_events .= 'ga("send", "event", ' . Json::encode(t('Messages')) . ', ' . Json::encode($status_heading[$type]) . ', ' . Json::encode(strip_tags((string) $message)) . ');';
-          }
-        }
-      }
-    }
-
-    // Site search tracking support.
-    if (\Drupal::moduleHandler()->moduleExists('search') && $config->get('track.site_search') && (strpos(\Drupal::routeMatch()->getRouteName(), 'search.view') === 0) && $keys = ($request->query->has('keys') ? trim($request->get('keys')) : '')) {
-      // hook_item_list__search_results() is not executed if search result is
-      // empty. Make sure the counter is set to 0 if there are no results.
-      $entity_id = \Drupal::routeMatch()->getParameter('entity')->id();
-      $url_custom = '(window.google_analytics_search_results) ? ' . Json::encode(Url::fromRoute('search.view_' . $entity_id, [], ['query' => ['search' => $keys]])->toString()) . ' : ' . Json::encode(Url::fromRoute('search.view_' . $entity_id, ['query' => ['search' => 'no-results:' . $keys, 'cat' => 'no-results']])->toString());
-    }
-
-    // If this node is a translation of another node, pass the original
-    // node instead.
-    if (\Drupal::moduleHandler()->moduleExists('content_translation') && $config->get('translation_set')) {
-      // Check if we have a node object, it has translation enabled, and its
-      // language code does not match its source language code.
-      if ($request->attributes->has('node')) {
-        $node = $request->attributes->get('node');
-        if ($node instanceof NodeInterface && \Drupal::service('entity.repository')->getTranslationFromContext($node) !== $node->getUntranslated()) {
-          $url_custom = Json::encode(Url::fromRoute('entity.node.canonical', ['node' => $node->id()], ['language' => $node->getUntranslated()->language()])->toString());
-        }
-      }
-    }
-
-    // Track access denied (403) and file not found (404) pages.
-    if ($status == '403') {
-      // See https://www.google.com/support/analytics/bin/answer.py?answer=86927
-      $url_custom = '"' . $base_path . '403.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer';
-    }
-    elseif ($status == '404') {
-      $url_custom = '"' . $base_path . '404.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer';
-    }
-
-    // #2693595: User has entered an invalid login and clicked on forgot
-    // password link. This link contains the username or email address and may
-    // get send to Google if we do not override it. Override only if 'name'
-    // query param exists. Last custom url condition, this need to win.
-    //
-    // URLs to protect are:
-    // - user/password?name=username
-    // - user/password?name=foo@example.com
-    if (\Drupal::routeMatch()->getRouteName() == 'user.pass' && $request->query->has('name')) {
-      $url_custom = '"' . $base_path . 'user/password"';
-    }
-
-    // Add custom dimensions and metrics.
-    $custom_var = '';
-    foreach (['dimension', 'metric'] as $google_analytics_custom_type) {
-      $google_analytics_custom_vars = $config->get('custom.' . $google_analytics_custom_type);
-      // Are there dimensions or metrics configured?
-      if (!empty($google_analytics_custom_vars)) {
-        // Add all the configured variables to the content.
-        foreach ($google_analytics_custom_vars as $google_analytics_custom_var) {
-          // Replace tokens in values.
-          $types = [];
-          if ($request->attributes->has('node')) {
-            $node = $request->attributes->get('node');
-            if ($node instanceof NodeInterface) {
-              $types += ['node' => $node];
-            }
-          }
-          $google_analytics_custom_var['value'] = \Drupal::token()->replace($google_analytics_custom_var['value'], $types, ['clear' => TRUE]);
-
-          // Suppress empty values.
-          if (!mb_strlen(trim($google_analytics_custom_var['value']))) {
-            continue;
-          }
-
-          // Per documentation the max length of a dimension is 150 bytes.
-          // A metric has no length limitation. It's not documented if this
-          // limit means 150 bytes after url encoding or before.
-          // See https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#customs
-          if ($google_analytics_custom_type == 'dimension' && mb_strlen($google_analytics_custom_var['value']) > 150) {
-            $google_analytics_custom_var['value'] = substr($google_analytics_custom_var['value'], 0, 150);
-          }
-
-          // Cast metric values for json_encode to data type numeric.
-          if ($google_analytics_custom_type == 'metric') {
-            settype($google_analytics_custom_var['value'], 'float');
-          };
-
-          // Add variables to tracker.
-          $custom_var .= 'ga("set", ' . Json::encode($google_analytics_custom_type . $google_analytics_custom_var['index']) . ', ' . Json::encode($google_analytics_custom_var['value']) . ');';
-        }
-      }
+    if ($config->get('track.adsense')) {
+      // Custom tracking. Prepend before all other JavaScript.
+      // @TODO: https://support.google.com/adsense/answer/98142
+      // sounds like it could be appended to $script.
+      $script = $javascript->getAdsenseScript();
     }
 
     // Build tracker code.
-    $script = '(function(i,s,o,g,r,a,m){';
-    $script .= 'i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){';
-    $script .= '(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),';
-    $script .= 'm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)';
-    $script .= '})(window,document,"script",';
-
-    // Which version of the tracking library should be used?
-    $library_tracker_url = 'https://www.google-analytics.com/' . ($debug ? 'analytics_debug.js' : 'analytics.js');
-
-    // Should a local cached copy of analytics.js be used?
-    if ($config->get('cache') && $url = _google_analytics_cache($library_tracker_url)) {
-      // A dummy query-string is added to filenames, to gain control over
-      // browser-caching. The string changes on every update or full cache
-      // flush, forcing browsers to load a new copy of the files, as the
-      // URL changed.
-      $query_string = '?' . (\Drupal::state()->get('system.css_js_query_string') ?: '0');
-
-      $script .= '"' . $url . $query_string . '"';
-    }
-    else {
-      $script .= '"' . $library_tracker_url . '"';
-    }
-    $script .= ',"ga");';
+    $script = 'window.dataLayer = window.dataLayer || [];';
+    $script .= 'function gtag(){dataLayer.push(arguments)};';
+    $script .= 'gtag("js", new Date());';
+    $script .= 'gtag("set", "developer_id.dMDhkMT", true);';
 
     // Add any custom code snippets if specified.
-    $codesnippet_create = $config->get('codesnippet.create');
     $codesnippet_before = $config->get('codesnippet.before');
     $codesnippet_after = $config->get('codesnippet.after');
 
-    // Build the create only fields list.
-    $create_only_fields = ['cookieDomain' => 'auto'];
-    $create_only_fields = array_merge($create_only_fields, $codesnippet_create);
-
-    // Domain tracking type.
-    global $cookie_domain;
-    $domain_mode = $config->get('domain_mode');
-    $googleanalytics_adsense_script = '';
-
-    // Per RFC 2109, cookie domains must contain at least one dot other than the
-    // first. For hosts such as 'localhost' or IP Addresses we don't set a
-    // cookie domain.
-    if ($domain_mode == 1 && count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
-      $create_only_fields = array_merge($create_only_fields, ['cookieDomain' => $cookie_domain]);
-      $googleanalytics_adsense_script .= 'window.google_analytics_domain_name = ' . Json::encode($cookie_domain) . ';';
-    }
-    elseif ($domain_mode == 2) {
-      // Cross Domain tracking. 'autoLinker' need to be enabled in 'create'.
-      $create_only_fields = array_merge($create_only_fields, ['allowLinker' => TRUE]);
-      $googleanalytics_adsense_script .= 'window.google_analytics_domain_name = "none";';
-    }
-
-    // Track logged in users across all devices.
-    if ($config->get('track.userid') && $account->isAuthenticated()) {
-      $create_only_fields['userId'] = google_analytics_user_id_hash($account->id());
-    }
-
     // Create a tracker.
-    $script .= 'ga("create", ' . Json::encode($id) . ', ' . Json::encode($create_only_fields) . ');';
+    if (!empty($codesnippet_before)) {
+      $script .= $codesnippet_before;
+    }
 
-    // Prepare Adsense tracking.
-    $googleanalytics_adsense_script .= 'window.google_analytics_uacct = ' . Json::encode($id) . ';';
+    // Create a config for each account.
+    foreach($ga_accounts->getAccounts() as $account) {
+      $ga_config = new GoogleAnalyticsConfigEvent($javascript, $account);
+
+      // Get the event_dispatcher service and dispatch the event.
+      $event_dispatcher = \Drupal::service('event_dispatcher');
+      $event_dispatcher->dispatch(GoogleAnalyticsEvents::ADD_CONFIG, $ga_config);
+
+      // Json::encode() cannot convert custom URLs properly.
+      $config_array = $ga_config->getConfig();
+      $path = '';
+      $path_type = '';
+      if (isset($config_array['page'])) {
+        $path_type = substr($config_array['page_placeholder'], strlen('PLACEHOLDER_'));
+        $path = $config_array['page'];
+        $config_array[$path_type] = $config_array['page_placeholder'];
+        unset($config_array['page']);
+        unset($config_array['page_placeholder']);
+      }
+      $arguments_json = Json::encode($config_array);
+      $arguments_json = str_replace('"PLACEHOLDER_'.$path_type.'"', $path, $arguments_json);
 
-    // Add enhanced link attribution after 'create', but before 'pageview' send.
-    // @see https://support.google.com/analytics/answer/2558867
-    if ($config->get('track.linkid')) {
-      $script .= 'ga("require", "linkid", "linkid.js");';
-    }
 
-    // Add display features after 'create', but before 'pageview' send.
-    // @see https://support.google.com/analytics/answer/2444872
-    if ($config->get('track.displayfeatures')) {
-      $script .= 'ga("require", "displayfeatures");';
+      $script .= 'gtag("config", ' . Json::encode((string)$account) . ', ' . $arguments_json . ');';
     }
 
-    // Domain tracking type.
-    if ($domain_mode == 2) {
-      // Cross Domain tracking
-      // https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#cross-domain
-      $script .= 'ga("require", "linker");';
-      $script .= 'ga("linker:autoLink", ' . Json::encode($link_settings['trackCrossDomains']) . ');';
-    }
 
-    if ($config->get('privacy.anonymizeip')) {
-      $script .= 'ga("set", "anonymizeIp", true);';
-    }
+    $ga_events = new GoogleAnalyticsEventsEvent($javascript);
 
-    if (!empty($custom_var)) {
-      $script .= $custom_var;
-    }
-    if (!empty($codesnippet_before)) {
-      $script .= $codesnippet_before;
-    }
-    if (!empty($url_custom)) {
-      $script .= 'ga("set", "page", ' . $url_custom . ');';
-    }
-    $script .= 'ga("send", "pageview");';
+    // Get the event_dispatcher service and dispatch the event.
+    $event_dispatcher = \Drupal::service('event_dispatcher');
+    $event_dispatcher->dispatch(GoogleAnalyticsEvents::ADD_EVENT, $ga_events);
 
-    if (!empty($message_events)) {
-      $script .= $message_events;
+    if (!empty($ga_events->getEvents())) {
+      foreach ($ga_events->getEvents() as $event) {
+        $event_name = array_key_first($event);
+        $event_parameters = $event[$event_name];
+        $script .= 'gtag("event", ' . Json::encode($event_name) . ', ' . Json::encode($event_parameters) . ');';
+      }
     }
     if (!empty($codesnippet_after)) {
       $script .= $codesnippet_after;
     }
 
-    if ($config->get('track.adsense')) {
-      // Custom tracking. Prepend before all other JavaScript.
-      // @TODO: https://support.google.com/adsense/answer/98142
-      // sounds like it could be appended to $script.
-      $script = $googleanalytics_adsense_script . $script;
-    }
+    // Prepend tracking library directly before script code.
+    $javascript_service = \Drupal::service('google_analytics.javascript_cache');
+
+    $page['#attached']['html_head'][] = [
+      [
+        '#tag' => 'script',
+        '#attributes' => [
+          'async' => TRUE,
+          'src' => $javascript_service->fetchGoogleAnalyticsJavascript($default_id),
+        ],
+      ],
+      'google_analytics_tracking_file',
+    ];
 
     $page['#attached']['html_head'][] = [
       [
@@ -356,23 +232,6 @@ function google_analytics_page_attachments(array &$page) {
   }
 }
 
-/**
- * Generate user id hash to implement USER_ID.
- *
- * The USER_ID value should be a unique, persistent, and non-personally
- * identifiable string identifier that represents a user or signed-in
- * account across devices.
- *
- * @param int $uid
- *   User id.
- *
- * @return string
- *   User id hash.
- */
-function google_analytics_user_id_hash($uid) {
-  return Crypt::hmacBase64($uid, \Drupal::service('private_key')->get() . Settings::getHashSalt());
-}
-
 /**
  * Implements hook_entity_extra_field_info().
  */
@@ -391,9 +250,14 @@ function google_analytics_entity_extra_field_info() {
  */
 function google_analytics_form_user_form_alter(&$form, FormStateInterface $form_state) {
   $config = \Drupal::config('google_analytics.settings');
-  $account = $form_state->getFormObject()->getEntity();
-
-  if ($account->hasPermission('opt-in or out of google analytics tracking') && ($visibility_user_account_mode = $config->get('visibility.user_account_mode')) != 0 && _google_analytics_visibility_roles($account)) {
+  /** @var Drupal\user\ProfileForm $profileForm */
+  $profileForm = $form_state->getFormObject();
+  /** @var Drupal\user\Entity\User $account */
+  $account = $profileForm->getEntity();
+  /** @var \Drupal\google_analytics\Helpers\VisiblityTracker $visibilityTracker */
+  $visibilityTracker = \Drupal::service('google_analytics.visibility');
+
+  if ($account->hasPermission('opt-in or out of google analytics tracking') && ($visibility_user_account_mode = $config->get('visibility.user_account_mode')) != 0 && $visibilityTracker->getVisibilityRoles($account)) {
     $account_data_google_analytics = \Drupal::service('user.data')->get('google_analytics', $account->id());
 
     $form['google_analytics'] = [
@@ -403,6 +267,7 @@ function google_analytics_form_user_form_alter(&$form, FormStateInterface $form_
       '#open' => TRUE,
     ];
 
+    $description = '';
     switch ($visibility_user_account_mode) {
       case 1:
         $description = t('Users are tracked by default, but you are able to opt out.');
@@ -429,7 +294,10 @@ function google_analytics_form_user_form_alter(&$form, FormStateInterface $form_
  * Submit callback for user profile form to save the Google Analytics setting.
  */
 function google_analytics_user_profile_form_submit($form, FormStateInterface $form_state) {
-  $account = $form_state->getFormObject()->getEntity();
+  /** @var Drupal\user\ProfileForm $profileForm */
+  $profileForm = $form_state->getFormObject();
+  /** @var Drupal\user\Entity\User $account */
+  $account = $profileForm->getEntity();
   if ($account->id() && $form_state->hasValue('user_account_users')) {
     \Drupal::service('user.data')->set('google_analytics', $account->id(), 'user_account_users', (int) $form_state->getValue('user_account_users'));
   }
@@ -441,10 +309,17 @@ function google_analytics_user_profile_form_submit($form, FormStateInterface $fo
 function google_analytics_cron() {
   $config = \Drupal::config('google_analytics.settings');
   $request_time = \Drupal::time()->getRequestTime();
+  $javascript_service = \Drupal::service('google_analytics.javascript_cache');
+  $ga_accounts = \Drupal::service('google_analytics.accounts');
+
+  // Return prematurely if no default measurement ID was found.
+  if (empty($ga_accounts->getDefaultMeasurementId())) {
+    return;
+  }
 
   // Regenerate the tracking code file every day.
   if ($request_time - \Drupal::state()->get('google_analytics.last_cache') >= 86400 && $config->get('cache')) {
-    _google_analytics_cache('https://www.google-analytics.com/analytics.js', TRUE);
+    $javascript_service->fetchGoogleAnalyticsJavascript($ga_accounts->getDefaultMeasurementId(), TRUE);
     \Drupal::state()->set('google_analytics.last_cache', $request_time);
   }
 }
@@ -477,204 +352,4 @@ function google_analytics_preprocess_item_list__search_results(&$variables) {
   }
 }
 
-/**
- * Download/Synchronize/Cache tracking code file locally.
- *
- * @param string $location
- *   The full URL to the external javascript file.
- * @param bool $synchronize
- *   Synchronize to local cache if remote file has changed.
- *
- * @return mixed
- *   The path to the local javascript file on success, boolean FALSE on failure.
- */
-function _google_analytics_cache($location, $synchronize = FALSE) {
-  $path = 'public://google_analytics';
-  $file_destination = $path . '/' . basename($location);
-  $filesystem = \Drupal::service('file_system');
-
-  if (!file_exists($file_destination) || $synchronize) {
-    // Download the latest tracking code.
-    try {
-      $data = (string) \Drupal::httpClient()
-        ->get($location)
-        ->getBody();
-
-      if (file_exists($file_destination)) {
-        // Synchronize tracking code and and replace local file if outdated.
-        $data_hash_local = Crypt::hashBase64(file_get_contents($file_destination));
-        $data_hash_remote = Crypt::hashBase64($data);
-        // Check that the files directory is writable.
-        if ($data_hash_local != $data_hash_remote &&  $filesystem->prepareDirectory($path)) {
-          // Save updated tracking code file to disk.
-          $filesystem->saveData($data, $file_destination, FileSystemInterface::EXISTS_REPLACE);
-          // Based on Drupal Core class AssetDumper.
-          if (extension_loaded('zlib') && \Drupal::config('system.performance')->get('js.gzip')) {
-            $filesystem->saveData(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FileSystemInterface::EXISTS_REPLACE);
-          }
-          \Drupal::logger('google_analytics')->info('Locally cached tracking code file has been updated.');
-
-          // Change query-strings on css/js files to enforce reload for all
-          // users.
-          _drupal_flush_css_js();
-        }
-      }
-      else {
-        // Check that the files directory is writable.
-        if ($filesystem->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY)) {
-          // There is no need to flush JS here as core refreshes JS caches
-          // automatically, if new files are added.
-          $filesystem->saveData($data, $file_destination, FileSystemInterface::EXISTS_REPLACE);
-          // Based on Drupal Core class AssetDumper.
-          if (extension_loaded('zlib') && \Drupal::config('system.performance')->get('js.gzip')) {
-            $filesystem->saveData(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FileSystemInterface::EXISTS_REPLACE);
-          }
-          \Drupal::logger('google_analytics')->info('Locally cached tracking code file has been saved.');
-
-          // Return the local JS file path.
-          return file_url_transform_relative(file_create_url($file_destination));
-        }
-      }
-    }
-    catch (RequestException $exception) {
-      watchdog_exception('google_analytics', $exception);
-    }
-  }
-  else {
-    // Return the local JS file path.
-    return file_url_transform_relative(file_create_url($file_destination));
-  }
-}
-
-/**
- * Delete cached files and directory.
- */
-function google_analytics_clear_js_cache() {
-  $path = 'public://google_analytics';
-  if (is_dir($path)) {
-    \Drupal::service('file_system')->deleteRecursive($path);
-
-    // Change query-strings on css/js files to enforce reload for all users.
-    _drupal_flush_css_js();
-
-    \Drupal::logger('google_analytics')->info('Local cache has been purged.');
-  }
-}
-
-/**
- * Tracking visibility check for an user object.
- *
- * @param object $account
- *   A user object containing an array of roles to check.
- *
- * @return bool
- *   TRUE if the current user is being tracked by Google Analytics,
- *   otherwise FALSE.
- */
-function _google_analytics_visibility_user($account) {
-  $config = \Drupal::config('google_analytics.settings');
-  $enabled = FALSE;
-
-  // Is current user a member of a role that should be tracked?
-  if (_google_analytics_visibility_roles($account)) {
 
-    // Use the user's block visibility setting, if necessary.
-    if (($visibility_user_account_mode = $config->get('visibility.user_account_mode')) != 0) {
-      $user_data_google_analytics = \Drupal::service('user.data')->get('google_analytics', $account->id());
-      if ($account->id() && isset($user_data_google_analytics['user_account_users'])) {
-        $enabled = $user_data_google_analytics['user_account_users'];
-      }
-      else {
-        $enabled = ($visibility_user_account_mode == 1);
-      }
-    }
-    else {
-      $enabled = TRUE;
-    }
-
-  }
-
-  return $enabled;
-}
-
-/**
- * Tracking visibility check for user roles.
- *
- * Based on visibility setting this function returns TRUE if JS code should
- * be added for the current role and otherwise FALSE.
- *
- * @param object $account
- *   A user object containing an array of roles to check.
- *
- * @return bool
- *   TRUE if JS code should be added for the current role and otherwise FALSE.
- */
-function _google_analytics_visibility_roles($account) {
-  $config = \Drupal::config('google_analytics.settings');
-  $enabled = $visibility_user_role_mode = $config->get('visibility.user_role_mode');
-  $visibility_user_role_roles = $config->get('visibility.user_role_roles');
-
-  if (count($visibility_user_role_roles) > 0) {
-    // One or more roles are selected.
-    foreach (array_values($account->getRoles()) as $user_role) {
-      // Is the current user a member of one of these roles?
-      if (in_array($user_role, $visibility_user_role_roles)) {
-        // Current user is a member of a role that should be tracked/excluded
-        // from tracking.
-        $enabled = !$visibility_user_role_mode;
-        break;
-      }
-    }
-  }
-  else {
-    // No role is selected for tracking, therefore all roles should be tracked.
-    $enabled = TRUE;
-  }
-
-  return $enabled;
-}
-
-/**
- * Tracking visibility check for pages.
- *
- * Based on visibility setting this function returns TRUE if JS code should
- * be added to the current page and otherwise FALSE.
- */
-function _google_analytics_visibility_pages() {
-  static $page_match;
-
-  // Cache visibility result if function is called more than once.
-  if (!isset($page_match)) {
-    $config = \Drupal::config('google_analytics.settings');
-    $visibility_request_path_mode = $config->get('visibility.request_path_mode');
-    $visibility_request_path_pages = $config->get('visibility.request_path_pages');
-
-    // Match path if necessary.
-    if (!empty($visibility_request_path_pages)) {
-      // Convert path to lowercase. This allows comparison of the same path
-      // with different case. Ex: /Page, /page, /PAGE.
-      $pages = mb_strtolower($visibility_request_path_pages);
-      if ($visibility_request_path_mode < 2) {
-        // Compare the lowercase path alias (if any) and internal path.
-        $path = \Drupal::service('path.current')->getPath();
-        $path_alias = mb_strtolower(\Drupal::service('path_alias.manager')->getAliasByPath($path));
-        $page_match = \Drupal::service('path.matcher')->matchPath($path_alias, $pages) || (($path != $path_alias) && \Drupal::service('path.matcher')->matchPath($path, $pages));
-        // When $visibility_request_path_mode has a value of 0, the tracking
-        // code is displayed on all pages except those listed in $pages. When
-        // set to 1, it is displayed only on those pages listed in $pages.
-        $page_match = !($visibility_request_path_mode xor $page_match);
-      }
-      elseif (\Drupal::moduleHandler()->moduleExists('php')) {
-        $page_match = php_eval($visibility_request_path_pages);
-      }
-      else {
-        $page_match = FALSE;
-      }
-    }
-    else {
-      $page_match = TRUE;
-    }
-
-  }
-  return $page_match;
-}
diff --git a/web/modules/google_analytics/google_analytics.routing.yml b/web/modules/google_analytics/google_analytics.routing.yml
index cea32df1ab..63b064754d 100644
--- a/web/modules/google_analytics/google_analytics.routing.yml
+++ b/web/modules/google_analytics/google_analytics.routing.yml
@@ -1,5 +1,5 @@
 google_analytics.admin_settings_form:
-  path: '/admin/config/system/google-analytics'
+  path: '/admin/config/services/google-analytics'
   defaults:
     _form: '\Drupal\google_analytics\Form\GoogleAnalyticsAdminSettingsForm'
     _title: 'Google Analytics'
diff --git a/web/modules/google_analytics/google_analytics.services.yml b/web/modules/google_analytics/google_analytics.services.yml
new file mode 100644
index 0000000000..f14dbd6f32
--- /dev/null
+++ b/web/modules/google_analytics/google_analytics.services.yml
@@ -0,0 +1,47 @@
+services:
+  google_analytics.visibility:
+    class: Drupal\google_analytics\Helpers\VisiblityTracker
+    arguments: [ '@config.factory', '@path_alias.manager', '@path.matcher', '@user.data', '@path.current' ]
+  google_analytics.accounts:
+    class: Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts
+    arguments: [ '@config.factory', '@private_key' ]
+  google_analytics.javascript_cache:
+    class: Drupal\google_analytics\JavascriptLocalCache
+    arguments: [ '@http_client', '@file_system', '@config.factory', '@logger.factory', '@state' ]
+
+  # Google Analytics Event Subscribers
+  google_analytics.events.messages:
+    class: '\Drupal\google_analytics\EventSubscriber\GoogleAnalyticsEvents\DrupalMessage'
+    arguments: [ '@config.factory', '@google_analytics.accounts', '@messenger' ]
+    tags:
+      - { name: 'event_subscriber' }
+  google_analytics.pagepath.content_translation:
+    class: '\Drupal\google_analytics\EventSubscriber\PagePath\ContentTranslation'
+    arguments: [ '@config.factory', '@request_stack', '@module_handler', '@entity.repository' ]
+    tags:
+      - { name: 'event_subscriber' }
+  google_analytics.pagepath.http_status:
+    class: '\Drupal\google_analytics\EventSubscriber\PagePath\HttpStatus'
+    arguments: [ '@config.factory', '@request_stack' ]
+    tags:
+      - { name: 'event_subscriber' }
+  google_analytics.pagepath.invalid_user_login:
+    class: '\Drupal\google_analytics\EventSubscriber\PagePath\InvalidUserLogin'
+    arguments: [ '@request_stack', '@current_route_match' ]
+    tags:
+      - { name: 'event_subscriber' }
+  google_analytics.pagepath.search:
+    class: '\Drupal\google_analytics\EventSubscriber\PagePath\Search'
+    arguments: [ '@config.factory', '@request_stack', '@module_handler', '@current_route_match' ]
+    tags:
+      - { name: 'event_subscriber' }
+  google_analytics.config.default_config:
+    class: '\Drupal\google_analytics\EventSubscriber\GoogleAnalyticsConfig\DefaultConfig'
+    arguments: [ '@config.factory', '@google_analytics.accounts', '@current_user' ]
+    tags:
+      - { name: 'event_subscriber' }
+  google_analytics.config.custom_config:
+    class: '\Drupal\google_analytics\EventSubscriber\GoogleAnalyticsConfig\CustomConfig'
+    arguments: [ '@config.factory', '@current_user', '@request_stack', '@token' ]
+    tags:
+      - { name: 'event_subscriber' }
\ No newline at end of file
diff --git a/web/modules/google_analytics/js/google_analytics.admin.js b/web/modules/google_analytics/js/google_analytics.admin.js
index c989f2d678..d7fc4bcafe 100644
--- a/web/modules/google_analytics/js/google_analytics.admin.js
+++ b/web/modules/google_analytics/js/google_analytics.admin.js
@@ -69,6 +69,9 @@
         if ($('input#edit-google-analytics-trackmailto', context).is(':checked')) {
           vals.push(Drupal.t('Mailto links'));
         }
+        if ($('input#edit-google-analytics-tracktel', context).is(':checked')) {
+          vals.push(Drupal.t('Tel links'));
+        }
         if ($('input#edit-google-analytics-trackfiles', context).is(':checked')) {
           vals.push(Drupal.t('Downloads'));
         }
diff --git a/web/modules/google_analytics/js/google_analytics.debug.js b/web/modules/google_analytics/js/google_analytics.debug.js
index 2df140a92b..dba84de365 100644
--- a/web/modules/google_analytics/js/google_analytics.debug.js
+++ b/web/modules/google_analytics/js/google_analytics.debug.js
@@ -35,21 +35,19 @@
           else if (drupalSettings.google_analytics.trackDownload && Drupal.google_analytics.isDownload(this.href)) {
             // Download link clicked.
             console.info("Download url '%s' has been found. Tracked download as extension '%s'.", Drupal.google_analytics.getPageUrl(this.href), Drupal.google_analytics.getDownloadExtension(this.href).toUpperCase());
-            ga('send', {
-              hitType: 'event',
-              eventCategory: 'Downloads',
-              eventAction: Drupal.google_analytics.getDownloadExtension(this.href).toUpperCase(),
-              eventLabel: Drupal.google_analytics.getPageUrl(this.href),
-              transport: 'beacon'
+            gtag('event', Drupal.google_analytics.getDownloadExtension(this.href).toUpperCase(), {
+              event_category: 'Downloads',
+              event_label: Drupal.google_analytics.getPageUrl(this.href),
+              transport_type: 'beacon'
             });
           }
           else if (Drupal.google_analytics.isInternalSpecial(this.href)) {
             // Keep the internal URL for Google Analytics website overlay intact.
             console.info("Click on internal special link '%s' has been tracked.", Drupal.google_analytics.getPageUrl(this.href));
-            ga('send', {
-              hitType: 'pageview',
-              page: Drupal.google_analytics.getPageUrl(this.href),
-              transport: 'beacon'
+            // @todo: May require tracking ID
+            gtag('config', drupalSettings.google_analytics.account, {
+              page_path: Drupal.google_analytics.getPageUrl(this.href),
+              transport_type: 'beacon'
             });
           }
           else {
@@ -61,24 +59,29 @@
           if (drupalSettings.google_analytics.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
             // Mailto link clicked.
             console.info("Click on e-mail '%s' has been tracked.", this.href.substring(7));
-            ga('send', {
-              hitType: 'event',
-              eventCategory: 'Mails',
-              eventAction: 'Click',
-              eventLabel: this.href.substring(7),
-              transport: 'beacon'
+            gtag('event', 'Click', {
+              event_category: 'Mails',
+              event_label: this.href.substring(7),
+              transport_type: 'beacon'
+            });
+          }
+          else if (drupalSettings.google_analytics.trackTel && $(this).is("a[href^='tel:'],area[href^='tel:']")) {
+            // Tel link clicked.
+            console.info("Click on telephone number '%s' has been tracked.", this.href.substring(4));
+            gtag('event', 'Click', {
+              event_category: 'Telephone calls',
+              event_label: this.href.substring(4),
+              transport_type: 'beacon'
             });
           }
           else if (drupalSettings.google_analytics.trackOutbound && this.href.match(/^\w+:\/\//i)) {
             if (drupalSettings.google_analytics.trackDomainMode !== 2 || (drupalSettings.google_analytics.trackDomainMode === 2 && !Drupal.google_analytics.isCrossDomain(this.hostname, drupalSettings.google_analytics.trackCrossDomains))) {
               // External link clicked / No top-level cross domain clicked.
               console.info("Outbound link '%s' has been tracked.", this.href);
-              ga('send', {
-                hitType: 'event',
-                eventCategory: 'Outbound links',
-                eventAction: 'Click',
-                eventLabel: this.href,
-                transport: 'beacon'
+              gtag('event', 'Click', {
+                event_category: 'Outbound links',
+                event_label: this.href,
+                transport_type: 'beacon'
               });
             }
             else {
@@ -95,9 +98,8 @@
     if (drupalSettings.google_analytics.trackUrlFragments) {
       window.onhashchange = function () {
         console.info("Track URL '%s' as pageview. Hash '%s' has changed.", location.pathname + location.search + location.hash, location.hash);
-        ga('send', {
-          hitType: 'pageview',
-          page: location.pathname + location.search + location.hash
+        gtag('config', drupalSettings.google_analytics.account, {
+          page_path: location.pathname + location.search + location.hash
         });
       };
     }
@@ -109,9 +111,8 @@
         var href = $.colorbox.element().attr('href');
         if (href) {
           console.info("Colorbox transition to url '%s' has been tracked.", Drupal.google_analytics.getPageUrl(href));
-          ga('send', {
-            hitType: 'pageview',
-            page: Drupal.google_analytics.getPageUrl(href)
+          gtag('config', drupalSettings.google_analytics.account, {
+            page_path: Drupal.google_analytics.getPageUrl(href)
           });
         }
       });
diff --git a/web/modules/google_analytics/js/google_analytics.js b/web/modules/google_analytics/js/google_analytics.js
index 82b46cf612..e00c5585b8 100644
--- a/web/modules/google_analytics/js/google_analytics.js
+++ b/web/modules/google_analytics/js/google_analytics.js
@@ -31,43 +31,45 @@
           // for download tracking?
           else if (drupalSettings.google_analytics.trackDownload && Drupal.google_analytics.isDownload(this.href)) {
             // Download link clicked.
-            ga('send', {
-              hitType: 'event',
-              eventCategory: 'Downloads',
-              eventAction: Drupal.google_analytics.getDownloadExtension(this.href).toUpperCase(),
-              eventLabel: Drupal.google_analytics.getPageUrl(this.href),
-              transport: 'beacon'
+            gtag('event', Drupal.google_analytics.getDownloadExtension(this.href).toUpperCase(), {
+              event_category: 'Downloads',
+              event_label: Drupal.google_analytics.getPageUrl(this.href),
+              transport_type: 'beacon'
             });
           }
           else if (Drupal.google_analytics.isInternalSpecial(this.href)) {
             // Keep the internal URL for Google Analytics website overlay intact.
-            ga('send', {
-              hitType: 'pageview',
-              page: Drupal.google_analytics.getPageUrl(this.href),
-              transport: 'beacon'
+            // @todo: May require tracking ID
+            gtag('config', drupalSettings.google_analytics.account, {
+              page_path: Drupal.google_analytics.getPageUrl(this.href),
+              transport_type: 'beacon'
             });
           }
         }
         else {
           if (drupalSettings.google_analytics.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
             // Mailto link clicked.
-            ga('send', {
-              hitType: 'event',
-              eventCategory: 'Mails',
-              eventAction: 'Click',
-              eventLabel: this.href.substring(7),
-              transport: 'beacon'
+            gtag('event', 'Click', {
+              event_category: 'Mails',
+              event_label: this.href.substring(7),
+              transport_type: 'beacon'
+            });
+          }
+          else if (drupalSettings.google_analytics.trackTel && $(this).is("a[href^='tel:'],area[href^='tel:']")) {
+            // Tel link clicked.
+            gtag('event', 'Click', {
+              event_category: 'Telephone calls',
+              event_label: this.href.substring(4),
+              transport_type: 'beacon'
             });
           }
           else if (drupalSettings.google_analytics.trackOutbound && this.href.match(/^\w+:\/\//i)) {
             if (drupalSettings.google_analytics.trackDomainMode !== 2 || (drupalSettings.google_analytics.trackDomainMode === 2 && !Drupal.google_analytics.isCrossDomain(this.hostname, drupalSettings.google_analytics.trackCrossDomains))) {
               // External link clicked / No top-level cross domain clicked.
-              ga('send', {
-                hitType: 'event',
-                eventCategory: 'Outbound links',
-                eventAction: 'Click',
-                eventLabel: this.href,
-                transport: 'beacon'
+              gtag('event', 'Click', {
+                event_category: 'Outbound links',
+                event_label: this.href,
+                transport_type: 'beacon'
               });
             }
           }
@@ -78,9 +80,8 @@
     // Track hash changes as unique pageviews, if this option has been enabled.
     if (drupalSettings.google_analytics.trackUrlFragments) {
       window.onhashchange = function () {
-        ga('send', {
-          hitType: 'pageview',
-          page: location.pathname + location.search + location.hash
+        gtag('config', drupalSettings.google_analytics.account, {
+          page_path: location.pathname + location.search + location.hash
         });
       };
     }
@@ -91,9 +92,8 @@
       $(document).on('cbox_complete', function () {
         var href = $.colorbox.element().attr('href');
         if (href) {
-          ga('send', {
-            hitType: 'pageview',
-            page: Drupal.google_analytics.getPageUrl(href)
+          gtag('config', drupalSettings.google_analytics.account, {
+            page_path: Drupal.google_analytics.getPageUrl(href)
           });
         }
       });
diff --git a/web/modules/google_analytics/migrations/d6_google_analytics_settings.yml b/web/modules/google_analytics/migrations/d6_google_analytics_settings.yml
index 70f6762b81..aa71bcbe77 100644
--- a/web/modules/google_analytics/migrations/d6_google_analytics_settings.yml
+++ b/web/modules/google_analytics/migrations/d6_google_analytics_settings.yml
@@ -33,7 +33,7 @@ source:
     - googleanalytics_translation_set
     - googleanalytics_visibility
     - googleanalytics_visibility_roles
-  source_module: googleanalytics    
+  source_module: googleanalytics
 process:
   account: googleanalytics_account
   cache: googleanalytics_cache
diff --git a/web/modules/google_analytics/migrations/d6_google_analytics_user_settings.yml b/web/modules/google_analytics/migrations/d6_google_analytics_user_settings.yml
index 98d3c23688..461c19a1f8 100644
--- a/web/modules/google_analytics/migrations/d6_google_analytics_user_settings.yml
+++ b/web/modules/google_analytics/migrations/d6_google_analytics_user_settings.yml
@@ -14,7 +14,7 @@ process:
   settings:
     plugin: skip_row_if_not_set
     index: 'custom'
-    source: data/google_analytics
+    source: data/googleanalytics
 destination:
   plugin: user_data
 migration_dependencies:
diff --git a/web/modules/google_analytics/migrations/d7_google_analytics_settings.yml b/web/modules/google_analytics/migrations/d7_google_analytics_settings.yml
index 30d18f827d..25f48ea5b3 100644
--- a/web/modules/google_analytics/migrations/d7_google_analytics_settings.yml
+++ b/web/modules/google_analytics/migrations/d7_google_analytics_settings.yml
@@ -35,7 +35,7 @@ source:
     - googleanalytics_translation_set
     - googleanalytics_visibility_pages
     - googleanalytics_visibility_roles
-  source_module: googleanalytics
+  source_module: googleanalytics    
 process:
   account: googleanalytics_account
   premium: googleanalytics_premium
diff --git a/web/modules/google_analytics/migrations/d7_google_analytics_user_settings.yml b/web/modules/google_analytics/migrations/d7_google_analytics_user_settings.yml
index 428c0d2725..a84278d3cc 100644
--- a/web/modules/google_analytics/migrations/d7_google_analytics_user_settings.yml
+++ b/web/modules/google_analytics/migrations/d7_google_analytics_user_settings.yml
@@ -14,7 +14,7 @@ process:
   settings:
     plugin: skip_row_if_not_set
     index: 'custom'
-    source: data/google_analytics
+    source: data/googleanalytics
 destination:
   plugin: user_data
 migration_dependencies:
diff --git a/web/modules/google_analytics/src/Constants/GoogleAnalyticsEvents.php b/web/modules/google_analytics/src/Constants/GoogleAnalyticsEvents.php
new file mode 100644
index 0000000000..aadcd86490
--- /dev/null
+++ b/web/modules/google_analytics/src/Constants/GoogleAnalyticsEvents.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\google_analytics\Constants;
+
+/**
+ * Defines events for the google_analytics module.
+ */
+final class GoogleAnalyticsEvents {
+
+  /**
+   * The event fired to build the Google Analytics javascript.
+   *
+   * Each action in Drupal that is tracked with google Analytics should have its
+   * own event subscriber to compile into the final javascript.
+   *
+   * @Event
+   *
+   * @see \Drupal\google_analytics\Event\BuildGaJavascriptEvent
+   *
+   * @var string
+   */
+  const BUILD_JAVASCRIPT = 'google_analytics_build_javascript';
+
+  /**
+   * The event fired to build the Google Analytics javascript.
+   *
+   * Each action in Drupal that is tracked with google Analytics should have its
+   * own event subscriber to compile into the final javascript.
+   *
+   * @Event
+   *
+   * @see \Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent
+   *
+   * @var string
+   */
+  const ADD_EVENT = 'google_analytics_add_event';
+
+  /**
+   * The event fired to build the Google Analytics javascript.
+   *
+   * Each action in Drupal that is tracked with google Analytics should have its
+   * own event subscriber to compile into the final javascript.
+   *
+   * @Event
+   *
+   * @see \Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent
+   *
+   * @var string
+   */
+  const ADD_CONFIG = 'google_analytics_add_config';
+
+  /**
+   * The event fired to set custom page paths.
+   *
+   * Each built in page path should stop propigation once it is found.
+   * This will then set the custom page path in analytics.
+   *
+   * @Event
+   *
+   * @see \Drupal\google_analytics\Event\PagePathEvent
+   *
+   * @var string
+   */
+  const PAGE_PATH = 'google_analytics_page_path';
+}
diff --git a/web/modules/google_analytics/src/Constants/GoogleAnalyticsPatterns.php b/web/modules/google_analytics/src/Constants/GoogleAnalyticsPatterns.php
new file mode 100644
index 0000000000..6eaef7b868
--- /dev/null
+++ b/web/modules/google_analytics/src/Constants/GoogleAnalyticsPatterns.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\google_analytics\Constants;
+
+/**
+ * Defines regex patterns for matching Google Analytics variables.
+ */
+final class GoogleAnalyticsPatterns {
+
+  /**
+   * Define the default file extension list that should be tracked as download.
+   */
+  const GOOGLE_ANALYTICS_TRACKFILES_EXTENSIONS = '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip';
+
+  /**
+   * Define the Acceptable GA ID Patterns
+   */
+  const GOOGLE_ANALYTICS_GTAG_MATCH = '/(?:UA|G|AW|DC)-[0-9a-zA-Z]{5,}(?:-[0-9]{1,})?/';
+
+  /**
+   * Define the Acceptable tracking ID patterns
+   */
+  const GOOGLE_ANALYTICS_TRACKING_MATCH = '/(?:UA|G)-[0-9a-zA-Z]{5,}(?:-[0-9]{1,})?/';
+
+  /**
+   * Define the pattern matching a universal analytics account.
+   */
+  const GOOGLE_ANALYTICS_UA_MATCH = '/(?:UA)-[0-9a-zA-Z]{5,}(?:-[0-9]{1,})?/';
+}
diff --git a/web/modules/google_analytics/src/Event/GoogleAnalyticsConfigEvent.php b/web/modules/google_analytics/src/Event/GoogleAnalyticsConfigEvent.php
new file mode 100644
index 0000000000..03be0c23e0
--- /dev/null
+++ b/web/modules/google_analytics/src/Event/GoogleAnalyticsConfigEvent.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\google_analytics\Event;
+
+use Drupal\google_analytics\GaAccount;
+use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
+use Symfony\Component\EventDispatcher\Event;
+use Drupal\google_analytics\GaJavascriptObject;
+
+/**
+ * Event that gathers all the config settings for a GA account.
+ */
+class GoogleAnalyticsConfigEvent extends Event {
+
+  /**
+   * The GA Javascript Object for which to store config.
+   *
+   * @var \Drupal\google_analytics\GaJavascriptObject
+   */
+  protected $javascript;
+
+  /**
+   * Array representing the config to pass to GA.
+   *
+   * @var array
+   */
+  protected $config;
+
+  /**
+   * Array representing the config to pass to GA.
+   *
+   * @var \Drupal\google_analytics\GaAccount
+   */
+  protected $gaAccount;
+
+  /**
+   * GoogleAnalyticsConfigEvent constructor.
+   *
+   * @param \Drupal\google_analytics\GaJavascriptObject $javascript
+   *   The GA Javascript Object.
+   */
+  public function __construct(GaJavascriptObject $javascript, GaAccount $ga_account) {
+    $this->javascript = $javascript;
+    $this->gaAccount = $ga_account;
+  }
+
+  /**
+   * Get the GA Javascript Object.
+   *
+   * @return \Drupal\google_analytics\GaJavascriptObject
+   *   The GA Javascript
+   */
+  public function getJavascript() {
+    return $this->javascript;
+  }
+
+  /**
+   * Get the specific Google Analytics account associated with this config.
+   *
+   * @return \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts
+   */
+  public function getGaAccount() {
+    return $this->gaAccount;
+  }
+
+  /**
+   * Get the GA Javascript Object being created.
+   *
+   * @return array
+   *   Config to be set in the GA javascript
+   */
+  public function getConfig() {
+    return $this->config;
+  }
+
+  /**
+   * Set a config key.
+   *
+   */
+  public function addConfig($config_key, $value) {
+    $this->config[$config_key] = $value;
+  }
+
+}
diff --git a/web/modules/google_analytics/src/Event/GoogleAnalyticsEventsEvent.php b/web/modules/google_analytics/src/Event/GoogleAnalyticsEventsEvent.php
new file mode 100644
index 0000000000..5de4e73d35
--- /dev/null
+++ b/web/modules/google_analytics/src/Event/GoogleAnalyticsEventsEvent.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\google_analytics\Event;
+
+use Symfony\Component\EventDispatcher\Event;
+use Drupal\google_analytics\GaJavascriptObject;
+
+/**
+ * Event that is fired when a user logs in.
+ */
+class GoogleAnalyticsEventsEvent extends Event {
+
+  /**
+   * The GA Javascript Object for which to create events.
+   *
+   * @var \Drupal\google_analytics\GaJavascriptObject
+   */
+  protected $javascript;
+
+  /**
+   * GoogleAnalyticsEventsEvent constructor.
+   *
+   * @param \Drupal\google_analytics\GaJavascriptObject $javascript
+   *   The GA Javascript object.
+   */
+  public function __construct(GaJavascriptObject $javascript) {
+    $this->javascript = $javascript;
+  }
+
+  /**
+   * Get the GA Javascript Object being created.
+   *
+   * @return array
+   *   Events in the javascript.
+   */
+  public function getEvents() {
+    return $this->javascript->getEvents();
+  }
+
+  /**
+   * Get the GA Javascript Object being created.
+   */
+  public function addEvent($event) {
+    $this->javascript->addEvent($event);
+  }
+
+}
diff --git a/web/modules/google_analytics/src/Event/PagePathEvent.php b/web/modules/google_analytics/src/Event/PagePathEvent.php
new file mode 100644
index 0000000000..ee1ac15ee8
--- /dev/null
+++ b/web/modules/google_analytics/src/Event/PagePathEvent.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\google_analytics\Event;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event that is fired when a user logs in.
+ */
+class PagePathEvent extends Event {
+
+  /**
+   * The Custom URL to be attached to the GA javascript.
+   *
+   * @var string
+   */
+  protected $page_path = '';
+
+  /**
+   * Get the current page path
+   *
+   * @return string
+   *   The currently set custom url in the javascript.
+   */
+  public function getPagePath() {
+    return $this->page_path;
+  }
+
+  /**
+   * Get the GA Javascript Object being created.
+   */
+  public function setPagePath($url) {
+    $this->page_path = $url;
+  }
+
+}
diff --git a/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsConfig/CustomConfig.php b/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsConfig/CustomConfig.php
new file mode 100644
index 0000000000..1ab7813c11
--- /dev/null
+++ b/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsConfig/CustomConfig.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Drupal\google_analytics\EventSubscriber\GoogleAnalyticsConfig;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\Utility\Token;
+use Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent;
+use Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
+use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
+use Drupal\node\NodeInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Adds custom Dimensions and Metrics to config and events.
+ */
+class CustomConfig implements EventSubscriberInterface {
+
+  /**
+   * Drupal Config Factory
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
+  /**
+   * Current Drupal User Account.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $currentAccount;
+
+  /**
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $token;
+
+  /**
+   * @var \Symfony\Component\HttpFoundation\Request|null
+   */
+  protected $request;
+
+  /**
+   * Custom Mapping of Vars.
+   *
+   * @var array
+   */
+  protected $custom_map = [];
+
+  /**
+   * Custom Variables passed to GA.
+   * @var array
+   */
+  protected $custom_vars = [];
+
+  /**
+   * DrupalMessage constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   Config Factory for Google Analytics Settings.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, AccountProxyInterface $account, RequestStack $request, Token $token) {
+    $this->config = $config_factory->get('google_analytics.settings');
+    $this->currentAccount = $account;
+    $this->request = $request->getCurrentRequest();
+    $this->token = $token;
+
+    // Populate custom map/vars
+    $this->populateCustomConfig();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[GoogleAnalyticsEvents::ADD_CONFIG][] = ['onAddConfig'];
+    $events[GoogleAnalyticsEvents::ADD_EVENT][] = ['onAddEvent'];
+    return $events;
+  }
+
+  /**
+   * Adds a new event to the Ga Javascript
+   *
+   * @param \Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent $event
+   *   The event being dispatched.
+   *
+   * @throws \Exception
+   */
+  public function onAddConfig(GoogleAnalyticsConfigEvent $event) {
+    // Don't execute event if there is nothing in the mapping fields.
+    if (empty($this->custom_map)) {
+      return;
+    }
+
+    // Only populate the config on UA accounts.
+    if ($event->getGaAccount()->isUniversalAnalyticsAccount()) {
+      $event->addConfig('custom_map', $this->custom_map['custom_map']);
+    }
+  }
+
+  public function onAddEvent(GoogleAnalyticsEventsEvent $event) {
+    // Don't execute event if there is nothing in the mapping fields.
+    if (empty($this->custom_vars)) {
+      return;
+    }
+    $event->addEvent(['custom' => $this->custom_vars]);
+  }
+
+  protected function populateCustomConfig() {
+    // Add custom dimensions and metrics.
+    $custom_parameters = $this->config->get('custom.parameters');
+    if (!empty($custom_parameters)) {
+      // Add all the configured variables to the content.
+    foreach ($custom_parameters as $index => $custom_parameter) {
+      // Replace tokens in values.
+      $types = [];
+      if ($this->request->attributes->has('node')) {
+        $node = $this->request->attributes->get('node');
+        if ($node instanceof NodeInterface) {
+          $types += ['node' => $node];
+        }
+      }
+      $custom_parameter['value'] = $this->token->replace($custom_parameter['value'], $types, ['clear' => TRUE]);
+
+      // Suppress empty values.
+      if ((isset($custom_parameter['name']) && !mb_strlen(trim($custom_parameter['name']))) || !mb_strlen(trim($custom_parameter['value']))) {
+        continue;
+      }
+
+        // Per documentation the max length of a dimension is 150 bytes.
+        // A metric has no length limitation. It's not documented if this
+        // limit means 150 bytes after url encoding or before.
+        // See https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#customs
+        if ($custom_parameter['type'] == 'dimension' && mb_strlen($custom_parameter['value']) > 150) {
+          $custom_parameter['value'] = substr($custom_parameter['value'], 0, 150);
+        }
+
+        // Cast metric values for json_encode to data type numeric.
+        if ($custom_parameter['type'] == 'metric') {
+          settype($custom_parameter['value'], 'float');
+        };
+
+        // Build the arrays of values.
+        $this->custom_map['custom_map'][$index] = ($custom_parameter['name'] ?? "");
+        if (isset($custom_parameter['name'])) {
+          $this->custom_vars[$custom_parameter['name']] = $custom_parameter['value'];
+        }
+      }
+    }
+  }
+
+}
diff --git a/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsConfig/DefaultConfig.php b/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsConfig/DefaultConfig.php
new file mode 100644
index 0000000000..dd94fab06c
--- /dev/null
+++ b/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsConfig/DefaultConfig.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Drupal\google_analytics\EventSubscriber\GoogleAnalyticsConfig;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent;
+use Drupal\google_analytics\Event\PagePathEvent;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
+use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Adds default config to Google Analytics.
+ */
+class DefaultConfig implements EventSubscriberInterface {
+
+  /**
+   * Drupal Config Factory
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
+  /**
+   * Current Drupal User Account.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $currentAccount;
+
+  /**
+   * The Global Google Analytics Accounts Service
+   *
+   * @var \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts
+   */
+  protected $gaAccounts;
+
+  /**
+   * DrupalMessage constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   Config Factory for Google Analytics Settings.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, GoogleAnalyticsAccounts $ga_accounts, AccountProxyInterface $account) {
+    $this->config = $config_factory->get('google_analytics.settings');
+    $this->gaAccounts = $ga_accounts;
+    $this->currentAccount = $account;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[GoogleAnalyticsEvents::ADD_CONFIG][] = ['onAddConfig'];
+    return $events;
+  }
+
+  /**
+   * Adds a new event to the Ga Javascript
+   *
+   * @param \Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent $event
+   *   The event being dispatched.
+   *
+   * @throws \Exception
+   */
+  public function onAddConfig(GoogleAnalyticsConfigEvent $event) {
+    $javascript = $event->getJavascript();
+    $ga_account = $event->getGaAccount();
+
+    // Custom Code Snippets that aren't created programmatically.
+    $codesnippet_parameters = $this->config->get('codesnippet.create') ?? [];
+
+    // Build the arguments fields list.
+    // https://developers.google.com/analytics/devguides/collection/gtagjs/sending-data
+    $arguments = ['groups' => 'default'];
+    $arguments = array_merge($arguments, $codesnippet_parameters);
+
+    // Domain tracking type.
+    global $cookie_domain;
+    $domain_mode = $this->config->get('domain_mode');
+
+    // Per RFC 2109, cookie domains must contain at least one dot other than the
+    // first. For hosts such as 'localhost' or IP Addresses we don't set a
+    // cookie domain.
+    if ($domain_mode == 1 && count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
+      $arguments = array_merge($arguments, ['cookie_domain' => $cookie_domain]);
+      $javascript->setAdsenseScript($cookie_domain);
+    }
+    elseif ($domain_mode == 2) {
+      // Cross Domain tracking
+      // https://developers.google.com/analytics/devguides/collection/gtagjs/cross-domain
+      $arguments['linker'] = [
+        'domains' => preg_split('/(\r\n?|\n)/', $this->config->get('cross_domains')),
+      ];
+      $javascript->setAdsenseScript();
+    }
+
+    // Track logged in users across all devices.
+    if ($this->currentAccount->isAuthenticated()) {
+      $arguments['user_id'] = $this->gaAccounts->getUserIdHash($this->currentAccount->id());
+    }
+
+    // Eliminate for GA 4.x
+    if ($this->config->get('privacy.anonymizeip') && $ga_account->isUniversalAnalyticsAccount()) {
+      $arguments['anonymize_ip'] = TRUE;
+    }
+
+    $page_path = new PagePathEvent();
+    // Get the event_dispatcher service and dispatch the event.
+    $event_dispatcher = \Drupal::service('event_dispatcher');
+    $event_dispatcher->dispatch(GoogleAnalyticsEvents::PAGE_PATH, $page_path);
+
+    $path_type = $ga_account->isUniversalAnalyticsAccount() ? 'page_path' : 'page_location';
+    $arguments['page_placeholder'] = 'PLACEHOLDER_' . $path_type;
+
+    // TODO: Rewrite this into the PagePath event that executes first.
+    if ($this->config->get('track.urlfragments')) {
+      $arguments['page'] = 'location.pathname + location.search + location.hash';
+    }
+
+    if (!empty($page_path->getPagePath())) {
+      $arguments['page'] = $page_path->getPagePath();
+    }
+
+    // Add enhanced link attribution after 'create', but before 'pageview' send.
+    // @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-link-attribution
+    if ($this->config->get('track.linkid')) {
+      $arguments['link_attribution'] = TRUE;
+    }
+
+    // Disabling display features.
+    // @see https://developers.google.com/analytics/devguides/collection/gtagjs/display-features
+    if (!$this->config->get('track.displayfeatures')) {
+      $arguments['allow_ad_personalization_signals'] = FALSE;
+    }
+
+    foreach ($arguments as $config_key => $value) {
+      $event->addConfig($config_key, $value);
+    }
+  }
+}
diff --git a/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsEvents/DrupalMessage.php b/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsEvents/DrupalMessage.php
new file mode 100644
index 0000000000..bd0b329193
--- /dev/null
+++ b/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsEvents/DrupalMessage.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\google_analytics\EventSubscriber\GoogleAnalyticsEvents;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
+use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
+
+/**
+ * Adds Drupal Messages to GA Javascript.
+ */
+class DrupalMessage extends GoogleAnalyticsEventBase {
+
+  /**
+   * Drupal Messenger Service.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * DrupalMessage constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   Config Factory for Google Analytics Settings.
+   * @param \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts $ga_accounts
+   *   The Google Analytics Account Service.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   Messenger Factory.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, GoogleAnalyticsAccounts $ga_accounts, MessengerInterface $messenger) {
+    parent::__construct($config_factory, $ga_accounts);
+    $this->messenger = $messenger;
+  }
+
+  public function addGaEvent(): array {
+    $events = [];
+    if ($message_types = $this->ga_config->get('track.messages')) {
+      $message_types = array_values(array_filter($message_types));
+      $status_heading = [
+        'status' => t('Status message'),
+        'warning' => t('Warning message'),
+        'error' => t('Error message'),
+      ];
+
+      foreach ($this->messenger->all() as $type => $messages) {
+        // Track only the selected message types.
+        if (in_array($type, $message_types)) {
+          foreach ($messages as $message) {
+            // Compatibility with 3.x and UA format.
+            if ($this->isLegacy) {
+              $events[] = [(string)$status_heading[$type] =>
+                ['event_category' => (string)t('Messages'),
+                  'event_label'    => strip_tags((string) $message)
+                ]
+              ];
+            }
+            else {
+              $events[] = [(string)$status_heading[$type] =>
+                ['value' => strip_tags((string) $message)]
+              ];
+            }
+          }
+        }
+      }
+    }
+    return $events;
+  }
+
+}
diff --git a/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsEvents/GoogleAnalyticsEventBase.php b/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsEvents/GoogleAnalyticsEventBase.php
new file mode 100644
index 0000000000..ef00cadbf0
--- /dev/null
+++ b/web/modules/google_analytics/src/EventSubscriber/GoogleAnalyticsEvents/GoogleAnalyticsEventBase.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\google_analytics\EventSubscriber\GoogleAnalyticsEvents;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
+use Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent;
+use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Class GoogleAnalyticsEventBase.
+ *
+ * Base class create events for Google Analytics.
+ *
+ * @package Drupal\google_analytics\EventSubscriber\GoogleAnalyticsEvents
+ */
+abstract class GoogleAnalyticsEventBase implements EventSubscriberInterface {
+
+  /**
+   * Google Analytics Config
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $ga_config;
+
+  /**
+   * Priority of the subscriber.
+   *
+   * @var int
+   */
+  public static $priority = 0;
+
+  /**
+   * Detect Legacy Universal Analytics Accounts
+   *
+   * @var bool
+   */
+  protected $isLegacy;
+
+  /**
+   * DrupalMessage constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   Config Factory for Google Analytics Settings.
+   * @param \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts $ga_accounts
+   *   The Google Analytics Account Service.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, GoogleAnalyticsAccounts $ga_accounts) {
+    $this->ga_config = $config_factory->get('google_analytics.settings');
+    $this->isLegacy = $ga_accounts->getDefaultMeasurementId()->isUniversalAnalyticsAccount();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[GoogleAnalyticsEvents::ADD_EVENT][] =
+      ['onAddEvent', self::$priority];
+    return $events;
+  }
+
+  /**
+   * Adds a new event in an array format for UA or GA4.
+   *
+   * @return array
+   */
+  abstract public function addGaEvent(): array;
+
+  /**
+   * Adds a new event to the Ga Javascript
+   *
+   * @param \Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent $event
+   *   The event being dispatched.
+   */
+  public function onAddEvent(GoogleAnalyticsEventsEvent $event) {
+    $ga_events = $this->addGaEvent();
+    if (!empty($ga_events)) {
+      foreach($ga_events AS $ga_event) {
+        $event->addEvent($ga_event);
+      }
+    }
+  }
+}
diff --git a/web/modules/google_analytics/src/EventSubscriber/PagePath/ContentTranslation.php b/web/modules/google_analytics/src/EventSubscriber/PagePath/ContentTranslation.php
new file mode 100644
index 0000000000..4cde9ecbb5
--- /dev/null
+++ b/web/modules/google_analytics/src/EventSubscriber/PagePath/ContentTranslation.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\google_analytics\EventSubscriber\PagePath;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Extension\ModuleHandler;
+use Drupal\Core\Url;
+use Drupal\google_analytics\Event\PagePathEvent;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
+use Drupal\node\NodeInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Adds Content Translation to custom URL
+ */
+class ContentTranslation implements EventSubscriberInterface {
+
+  /**
+   * Drupal Config Factory
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
+  /**
+   * Drupal Messenger Service.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * @var \GuzzleHttp\Psr7\Request
+   */
+  protected $request;
+
+  /**
+   * @var \Drupal\Core\Extension\ModuleHandler
+   */
+  protected $moduleHandler;
+
+  /**
+   * @var \Drupal\Core\Routing\CurrentRouteMatch
+   */
+  protected $currentRoute;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * DrupalMessage constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   Config Factory for Google Analytics Settings.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, RequestStack $request, ModuleHandler $module_handler, EntityRepositoryInterface $entity_repsoitory) {
+    $this->config = $config_factory->get('google_analytics.settings');
+    $this->request = $request->getCurrentRequest();
+    $this->moduleHandler = $module_handler;
+    $this->entityRepository = $entity_repsoitory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[GoogleAnalyticsEvents::PAGE_PATH][] = ['onPagePath'];
+    return $events;
+  }
+
+  /**
+   * Adds a new event to the Ga Javascript
+   *
+   * @param \Drupal\google_analytics\Event\PagePathEvent $event
+   *   The event being dispatched.
+   *
+   * @throws \Exception
+   */
+  public function onPagePath(PagePathEvent $event) {
+    // Site search tracking support.
+    // If this node is a translation of another node, pass the original
+    // node instead.
+    if ($this->moduleHandler->moduleExists('content_translation') && $this->config->get('translation_set')) {
+      // Check if we have a node object, it has translation enabled, and its
+      // language code does not match its source language code.
+      if ($this->request->attributes->has('node')) {
+        $node = $this->request->attributes->get('node');
+        if ($node instanceof NodeInterface && $this->entityRepository->getTranslationFromContext($node) !== $node->getUntranslated()) {
+          $url_custom = Json::encode(Url::fromRoute('entity.node.canonical', ['node' => $node->id()], ['language' => $node->getUntranslated()->language()])->toString());
+          $event->setPagePath($url_custom);
+          $event->stopPropagation();
+        }
+      }
+    }
+  }
+}
diff --git a/web/modules/google_analytics/src/EventSubscriber/PagePath/HttpStatus.php b/web/modules/google_analytics/src/EventSubscriber/PagePath/HttpStatus.php
new file mode 100644
index 0000000000..87c290ed87
--- /dev/null
+++ b/web/modules/google_analytics/src/EventSubscriber/PagePath/HttpStatus.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\google_analytics\EventSubscriber\PagePath;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\google_analytics\Event\PagePathEvent;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Adds Content Translation to custom URL
+ */
+class HttpStatus implements EventSubscriberInterface {
+
+  /**
+   * Drupal Config Factory
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
+  /**
+   * @var \GuzzleHttp\Psr7\Request
+   */
+  protected $request;
+
+  /**
+   * DrupalMessage constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   Config Factory for Google Analytics Settings.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, RequestStack $request) {
+    $this->config = $config_factory->get('google_analytics.settings');
+    $this->request = $request->getCurrentRequest();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[GoogleAnalyticsEvents::PAGE_PATH][] = ['onPagePath'];
+    return $events;
+  }
+
+  /**
+   * Adds error pages to the page path.
+   *
+   * @param \Drupal\google_analytics\Event\PagePathEvent $event
+   *   The event being dispatched.
+   *
+   * @throws \Exception
+   */
+  public function onPagePath(PagePathEvent $event) {
+    // Get page http status code for visibility filtering.
+    $status = NULL;
+    if ($exception = $this->request->attributes->get('exception')) {
+      $status = $exception->getStatusCode();
+    }
+    // TODO: Make configurable
+    $trackable_status_codes = [
+      // "Forbidden" status code.
+      '403',
+      // "Not Found" status code.
+      '404',
+    ];
+    if (in_array($status, $trackable_status_codes)) {
+      $base_path = base_path();
+
+      // Track access denied (403) and file not found (404) pages.
+      $event->setPagePath('"' . $base_path . $status . '.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer');
+      $event->stopPropagation();
+    }
+  }
+}
diff --git a/web/modules/google_analytics/src/EventSubscriber/PagePath/InvalidUserLogin.php b/web/modules/google_analytics/src/EventSubscriber/PagePath/InvalidUserLogin.php
new file mode 100644
index 0000000000..482b3ac2b6
--- /dev/null
+++ b/web/modules/google_analytics/src/EventSubscriber/PagePath/InvalidUserLogin.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\google_analytics\EventSubscriber\PagePath;
+
+use Drupal\Core\Routing\CurrentRouteMatch;
+use Drupal\google_analytics\Event\PagePathEvent;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Adds Content Translation to custom URL
+ */
+class InvalidUserLogin implements EventSubscriberInterface {
+
+  /**
+   * @var \GuzzleHttp\Psr7\Request
+   */
+  protected $request;
+
+  /**
+   * @var \Drupal\Core\Routing\CurrentRouteMatch
+   */
+  protected $currentRoute;
+
+  /**
+   * DrupalMessage constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   Config Factory for Google Analytics Settings.
+   */
+  public function __construct(RequestStack $request, CurrentRouteMatch $current_route) {
+    $this->request = $request->getCurrentRequest();
+    $this->currentRoute = $current_route;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[GoogleAnalyticsEvents::PAGE_PATH][] = ['onPagePath', 100];
+    return $events;
+  }
+
+  /**
+   * Adds error pages to the page path.
+   *
+   * @param \Drupal\google_analytics\Event\PagePathEvent $event
+   *   The event being dispatched.
+   *
+   * @throws \Exception
+   */
+  public function onPagePath(PagePathEvent $event) {
+    // #2693595: User has entered an invalid login and clicked on forgot
+    // password link. This link contains the username or email address and may
+    // get send to Google if we do not override it. Override only if 'name'
+    // query param exists. Last custom url condition, this need to win.
+    //
+    // URLs to protect are:
+    // - user/password?name=username
+    // - user/password?name=foo@example.com
+    $base_path = base_path();
+    if ($this->currentRoute->getRouteName() == 'user.pass' && $this->request->query->has('name')) {
+      $event->setPagePath('"' . $base_path . 'user/password"');
+      $event->stopPropagation();
+    }
+  }
+}
diff --git a/web/modules/google_analytics/src/EventSubscriber/PagePath/Search.php b/web/modules/google_analytics/src/EventSubscriber/PagePath/Search.php
new file mode 100644
index 0000000000..a6ce9b4196
--- /dev/null
+++ b/web/modules/google_analytics/src/EventSubscriber/PagePath/Search.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\google_analytics\EventSubscriber\PagePath;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandler;
+use Drupal\Core\Routing\CurrentRouteMatch;
+use Drupal\Core\Url;
+use Drupal\google_analytics\Event\PagePathEvent;
+use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Adds Drupal Messages to GA Javascript.
+ */
+class Search implements EventSubscriberInterface {
+
+  /**
+   * Drupal Config Factory
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
+  /**
+   * @var \GuzzleHttp\Psr7\Request
+   */
+  protected $request;
+
+  /**
+   * @var \Drupal\Core\Extension\ModuleHandler
+   */
+  protected $moduleHandler;
+
+  /**
+   * @var \Drupal\Core\Routing\CurrentRouteMatch
+   */
+  protected $currentRoute;
+
+  /**
+   * DrupalMessage constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   Config Factory for Google Analytics Settings.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, RequestStack $request, ModuleHandler $module_handler, CurrentRouteMatch $current_route) {
+    $this->config = $config_factory->get('google_analytics.settings');
+    $this->request = $request->getCurrentRequest();
+    $this->moduleHandler = $module_handler;
+    $this->currentRoute = $current_route;
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[GoogleAnalyticsEvents::PAGE_PATH][] = ['onCustomPagePath'];
+    return $events;
+  }
+
+  /**
+   * Adds a new event to the Ga Javascript
+   *
+   * @param \Drupal\google_analytics\Event\PagePathEvent $event
+   *   The event being dispatched.
+   *
+   * @throws \Exception
+   */
+  public function onCustomPagePath(PagePathEvent $event) {
+    // Site search tracking support.
+    if ($this->moduleHandler->moduleExists('search') && $this->config->get('track.site_search') && (strpos($this->currentRoute->getRouteName(), 'search.view') === 0) && $keys = ($this->request->query->has('keys') ? trim($this->request->get('keys')) : '')) {
+      // hook_item_list__search_results() is not executed if search result is
+      // empty. Make sure the counter is set to 0 if there are no results.
+      $entity = $this->currentRoute->getParameter('entity');
+      if (isset($entity)) {
+        $entity_id = $entity->id();
+        $url_custom = '(window.google_analytics_search_results) ? ' . Json::encode(Url::fromRoute('search.view_' . $entity_id, [], ['query' => ['search' => $keys]])
+            ->toString()) . ' : ' . Json::encode(Url::fromRoute('search.view_' . $entity_id, [], [
+              'query' => [
+                'search' => 'no-results:' . $keys,
+                'cat' => 'no-results'
+              ]
+          ])->toString());
+        $event->setPagePath($url_custom);
+        $event->stopPropagation();
+      }
+    }
+  }
+}
diff --git a/web/modules/google_analytics/src/Form/GoogleAnalyticsAdminSettingsForm.php b/web/modules/google_analytics/src/Form/GoogleAnalyticsAdminSettingsForm.php
index a892b3b0ec..c7810a74ad 100644
--- a/web/modules/google_analytics/src/Form/GoogleAnalyticsAdminSettingsForm.php
+++ b/web/modules/google_analytics/src/Form/GoogleAnalyticsAdminSettingsForm.php
@@ -2,14 +2,17 @@
 
 namespace Drupal\google_analytics\Form;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\google_analytics\Constants\GoogleAnalyticsPatterns;
+use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
+use Drupal\google_analytics\JavascriptLocalCache;
 use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\google_analytics\GoogleAnalitycsInterface;
 
 /**
  * Configure Google_Analytics settings for this site.
@@ -30,20 +33,54 @@ class GoogleAnalyticsAdminSettingsForm extends ConfigFormBase {
    */
   protected $currentUser;
 
+  /**
+   * The google analytics account manager.,
+   *
+   * @var \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts
+   */
+  protected $gaAccounts;
+
+  /**
+   * The google analytics local javascript cache manager.
+   *
+   * @var \Drupal\google_analytics\JavascriptLocalCache
+   */
+  protected $gaJavascript;
+
   /**
    * The constructor method.
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory.
-   * @param \Drupal\Core\Session\AccountInterface $currentUser
+   * @param \Drupal\Core\Session\AccountInterface $current_user
    *   The current user.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The manages modules.
+   * @param \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts $google_analytics_accounts
+   *   The google analytics accounts manager.
+   * @param \Drupal\google_analytics\JavascriptLocalCache $google_analytics_javascript
+   *   The JS Local Cache service.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $currentUser, ModuleHandlerInterface $moduleHandler) {
+  public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $current_user, ModuleHandlerInterface $module_handler, GoogleAnalyticsAccounts $google_analytics_accounts, JavascriptLocalCache $google_analytics_javascript) {
     parent::__construct($config_factory);
-    $this->currentUser = $currentUser;
-    $this->moduleHandler = $moduleHandler;
+    $this->currentUser = $current_user;
+    $this->moduleHandler = $module_handler;
+    $this->gaAccounts = $google_analytics_accounts;
+    $this->gaJavascript = $google_analytics_javascript;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+    // Load the service required to construct this class.
+      $container->get('config.factory'),
+      $container->get('current_user'),
+      $container->get('module_handler'),
+      $container->get('google_analytics.accounts'),
+      $container->get('google_analytics.javascript_cache')
+    );
   }
 
   /**
@@ -60,34 +97,95 @@ protected function getEditableConfigNames() {
     return ['google_analytics.settings'];
   }
 
-  /**
+  /**google_analytics.accounts
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
     $config = $this->config('google_analytics.settings');
 
+    $id_count = $form_state->get('id_count');
+    // If the id_count is null, we're loading for the first time, load in IDS.
+    if ($id_count === NULL) {
+      $accounts = $this->gaAccounts->getAccounts();
+      $id_count = empty($accounts) ? 1 : count($accounts);
+      $form_state->set('id_count', $id_count);
+    }
+    $id_prefix = implode('-', ['general', 'accounts']);
+    $wrapper_id = Html::getUniqueId($id_prefix . '-add-more-wrapper');
+
     $form['general'] = [
-      '#type' => 'details',
-      '#title' => $this->t('General settings'),
-      '#open' => TRUE,
+      '#type' => 'fieldset',
+      '#title' => $this->t('Web Property ID(s)'),
+      '#prefix' => '<div id="'. $wrapper_id .'">',
+      '#description' => $this->t('This ID is unique to each site you want to track separately, and is in the form of UA-xxxxx-yy, G-xxxxxxxx, AW-xxxxxxxxx, or DC-xxxxxxxx. To get a Web Property ID, <a href=":analytics">register your site with Google Analytics</a>, or if you already have registered your site, go to your Google Analytics Settings page to see the ID next to every site profile. <a href=":webpropertyid">Find more information in the documentation</a>.', [':analytics' => 'https://marketingplatform.google.com/about/analytics/', ':webpropertyid' => Url::fromUri('https://developers.google.com/analytics/resources/concepts/gaConceptsAccounts', ['fragment' => 'webProperty'])->toString()]),
+      '#suffix' => '</div>',
     ];
-
-    $form['general']['google_analytics_account'] = [
-      '#default_value' => $config->get('account'),
-      '#description' => $this->t('This ID is unique to each site you want to track separately, and is in the form of UA-xxxxxxx-yy. To get a Web Property ID, <a href=":analytics">register your site with Google Analytics</a>, or if you already have registered your site, go to your Google Analytics Settings page to see the ID next to every site profile. <a href=":webpropertyid">Find more information in the documentation</a>.', [':analytics' => 'https://marketingplatform.google.com/about/analytics/', ':webpropertyid' => Url::fromUri('https://developers.google.com/analytics/resources/concepts/gaConceptsAccounts', ['fragment' => 'webProperty'])->toString()]),
-      '#maxlength' => 20,
-      '#placeholder' => 'UA-',
-      '#required' => TRUE,
-      '#size' => 20,
-      '#title' => $this->t('Web Property ID'),
-      '#type' => 'textfield',
+    // Filter order (tabledrag).
+    $form['general']['accounts'] = [
+      '#type' => 'table',
+      '#tabledrag' => [
+        [
+          'action' => 'order',
+          'relationship' => 'sibling',
+          'group' => 'account-order-weight',
+        ],
+      ],
+      '#tree' => TRUE,
     ];
 
-    $form['general']['google_analytics_premium'] = [
-      '#default_value' => $config->get('premium'),
-      '#description' => $this->t('If you are a Google Analytics Premium customer, you can use up to 200 instead of 20 custom dimensions and metrics.'),
-      '#title' => $this->t('Premium account'),
-      '#type' => 'checkbox',
+    for ($i = 0; $i < $id_count; $i++) {
+      // This makes sure removed fields don't reappear in the form.
+      if ($i === $form_state->get('remove_ids')) {
+        continue;
+      }
+
+      $form['general']['accounts'][$i]['#attributes']['class'][] = 'draggable';
+      $form['general']['accounts'][$i]['#weight'] = $i;
+      $form['general']['accounts'][$i]['value'] = [
+        '#default_value' => (string)$accounts[$i] ?? '',
+        '#maxlength' => 20,
+        '#required' => ($i === 0),
+        '#size' => 20,
+        '#type' => 'textfield',
+        '#element_validate' => [[get_class($this), 'gtagElementValidate']],
+      ];
+
+      $form['general']['accounts'][$i]['weight'] = [
+        '#type' => 'weight',
+        '#title' => $this->t('Weight for @title', ['@title' => (string)$accounts[$i]]),
+        '#title_display' => 'invisible',
+        '#delta' => 50,
+        '#default_value' => $i,
+        '#parents' => ['accounts', $i, 'weight'],
+        '#attributes' => ['class' => ['account-order-weight']],
+      ];
+
+      // If there is more than one id, add the remove button.
+      if ($id_count > 1) {
+        $form['general']['accounts'][$i]['remove'] = [
+          '#type' => 'submit',
+          '#name' => 'remove_gtag_ids_'.$i,
+          '#value' => $this->t('Remove'),
+          '#submit' => ['::removeCallback'],
+          '#limit_validation_errors' => [],
+          '#ajax' => [
+            'callback' => '::gtagFieldCallback',
+            'wrapper' => $wrapper_id,
+          ],
+        ];
+      }
+    }
+
+    $form['general']['add_gtag_id'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Add another ID'),
+      '#name' => strtr($id_prefix, '-', '_') . '_add_gtag_id',
+      '#submit' => ['::addOne'],
+      '#ajax' => [
+        'callback' => '::gtagFieldCallback',
+        'wrapper' => $wrapper_id,
+        'effect' => 'fade',
+      ],
     ];
 
     // Visibility settings.
@@ -168,7 +266,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
     // Page specific visibility configurations.
     $account = $this->currentUser;
-    $php_access = $account->hasPermission('use PHP for google analytics tracking visibility');
     $visibility_request_path_pages = $config->get('visibility.request_path_pages');
 
     $form['tracking']['page_visibility_settings'] = [
@@ -177,7 +274,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#group' => 'tracking_scope',
     ];
 
-    if ($config->get('visibility.request_path_mode') == 2 && !$php_access) {
+    if ($config->get('visibility.request_path_mode') == 2) {
       $form['tracking']['page_visibility_settings'] = [];
       $form['tracking']['page_visibility_settings']['google_analytics_visibility_request_path_mode'] = ['#type' => 'value', '#value' => 2];
       $form['tracking']['page_visibility_settings']['google_analytics_visibility_request_path_pages'] = ['#type' => 'value', '#value' => $visibility_request_path_pages];
@@ -189,14 +286,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ];
       $description = $this->t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page.", ['%blog' => '/blog', '%blog-wildcard' => '/blog/*', '%front' => '<front>']);
 
-      if ($this->moduleHandler->moduleExists('php') && $php_access) {
-        $options[] = $this->t('Pages on which this PHP code returns <code>TRUE</code> (not supported in Drupal 9, experts only)');
-        $title = $this->t('Pages or PHP code');
-        $description .= ' ' . $this->t('If the PHP option is chosen, enter PHP code between %php. Note that executing incorrect PHP code can break your Drupal site.', ['%php' => '<?php ?>']);
-      }
-      else {
-        $title = $this->t('Pages');
-      }
       $form['tracking']['page_visibility_settings']['google_analytics_visibility_request_path_mode'] = [
         '#type' => 'radios',
         '#title' => $this->t('Add tracking to specific pages'),
@@ -205,7 +294,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ];
       $form['tracking']['page_visibility_settings']['google_analytics_visibility_request_path_pages'] = [
         '#type' => 'textarea',
-        '#title' => $title,
+        '#title' => $this->t('Pages'),
         '#title_display' => 'invisible',
         '#default_value' => !empty($visibility_request_path_pages) ? $visibility_request_path_pages : '',
         '#description' => $description,
@@ -258,12 +347,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ],
       '#default_value' => !empty($visibility_user_account_mode) ? $visibility_user_account_mode : 0,
     ];
-    $form['tracking']['user_visibility_settings']['google_analytics_trackuserid'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Track User ID'),
-      '#default_value' => $config->get('track.userid'),
-      '#description' => $this->t('User ID enables the analysis of groups of sessions, across devices, using a unique, persistent, and non-personally identifiable ID string representing a user. <a href=":url">Learn more about the benefits of using User ID</a>.', [':url' => 'https://support.google.com/analytics/answer/3123663']),
-    ];
 
     // Link specific configurations.
     $form['tracking']['linktracking'] = [
@@ -278,9 +361,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
     $form['tracking']['linktracking']['google_analytics_trackmailto'] = [
       '#type' => 'checkbox',
-      '#title' => $this->t('Track clicks on mailto links'),
+      '#title' => $this->t('Track clicks on mailto (email) links'),
       '#default_value' => $config->get('track.mailto'),
     ];
+    $form['tracking']['linktracking']['google_analytics_tracktel'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Track clicks on tel (telephone number) links'),
+      '#default_value' => $config->get('track.tel'),
+    ];
     $form['tracking']['linktracking']['google_analytics_trackfiles'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Track downloads (clicks on file links) for the following extensions'),
@@ -291,7 +379,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#title_display' => 'invisible',
       '#type' => 'textfield',
       '#default_value' => $config->get('track.files_extensions'),
-      '#description' => $this->t('A file extension list separated by the | character that will be tracked as download when clicked. Regular expressions are supported. For example: @extensions', ['@extensions' => GoogleAnalitycsInterface::GOOGLE_ANALYTICS_TRACKFILES_EXTENSIONS]),
+      '#description' => $this->t('A file extension list separated by the | character that will be tracked as download when clicked. Regular expressions are supported. For example: @extensions', ['@extensions' => GoogleAnalyticsPatterns::GOOGLE_ANALYTICS_TRACKFILES_EXTENSIONS]),
       '#maxlength' => 500,
       '#states' => [
         'enabled' => [
@@ -386,119 +474,152 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
     $form['tracking']['privacy']['google_analytics_tracker_anonymizeip'] = [
       '#type' => 'checkbox',
-      '#title' => $this->t('Anonymize visitors IP address'),
-      '#description' => $this->t('Tell Google Analytics to anonymize the information sent by the tracker objects by removing the last octet of the IP address prior to its storage. Note that this will slightly reduce the accuracy of geographic reporting. In some countries it is not allowed to collect personally identifying information for privacy reasons and this setting may help you to comply with the local laws.'),
+      '#title' => $this->t('Anonymize visitors IP address (UA Accounts only)'),
+      '#description' => $this->t('Tell Google Analytics to anonymize the information sent by the tracker objects by removing the last octet of the IP address prior to its storage. Note that this will slightly reduce the accuracy of geographic reporting. In some countries it is not allowed to collect personally identifying information for privacy reasons and this setting may help you to comply with the local laws. This option does nothing in GA4 as it Anonymizes IPs by default and not be configured.'),
       '#default_value' => $config->get('privacy.anonymizeip'),
     ];
 
-    // Custom Dimensions.
-    $form['google_analytics_custom_dimension'] = [
-      '#description' => $this->t('You can set values for Google Analytics <a href=":custom_var_documentation">Custom Dimensions</a> here. You must have already configured your custom dimensions in the <a href=":setup_documentation">Google Analytics Management Interface</a>. You may use tokens. Global and user tokens are always available; on node pages, node tokens are also available. A dimension <em>value</em> is allowed to have a maximum length of 150 bytes. Expect longer values to get trimmed.', [':custom_var_documentation' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/custom-dims-mets', ':setup_documentation' => 'https://support.google.com/analytics/answer/2709829']),
-      '#title' => $this->t('Custom dimensions'),
-      '#tree' => TRUE,
+    // If the param_count is null, we're loading for the first time, load in IDS.
+    $parameters = $this->getCustomParameters();
+    if ($form_state->get('param_count') === NULL) {
+      $form_state->set('param_count', count($parameters));
+    }
+    $param_id_prefix = implode('-', ['tracking', 'parameters']);
+    $param_wrapper_id = Html::getUniqueId($param_id_prefix . '-add-more-wrapper');
+
+    $form['tracking']['parameters'] = [
       '#type' => 'details',
+      '#title' => $this->t('Dimensions and Metrics'),
+      '#group' => 'tracking_scope',
+    ];
+
+    $form['tracking']['parameters']['indexes'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Custom dimensions and metrics'),
+      '#description' => $this->t('You can set values for Google Analytics <a href=":custom_var_documentation">Custom Dimensions</a> here. You must have already configured your custom dimensions in the <a href=":setup_documentation">Google Analytics Management Interface</a>. You may use tokens. Global and user tokens are always available; on node pages, node tokens are also available. A dimension <em>value</em> is allowed to have a maximum length of 150 bytes. Expect longer values to get trimmed.', [':custom_var_documentation' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/custom-dims-mets', ':setup_documentation' => 'https://support.google.com/analytics/answer/2709829']),
+      '#prefix' => '<div id="'. $param_wrapper_id .'">',
+      '#suffix' => '</div>',
     ];
 
-    $form['google_analytics_custom_dimension']['indexes'] = [
+    $form['tracking']['parameters']['indexes']['custom_parameters'] = [
       '#type' => 'table',
       '#header' => [
         ['data' => $this->t('Index')],
+        ['data' => $this->t('Type')],
+        ['data' => $this->t('Name')],
         ['data' => $this->t('Value')],
+        ['data' => $this->t('Operations')],
       ],
+      '#tree' => TRUE,
     ];
 
-    $google_analytics_custom_dimension = $config->get('custom.dimension');
+    for ($i = 0; $i < $form_state->get('param_count'); $i++) {
+      // This makes sure removed fields don't reappear in the form.
+      $remove_ids = $form_state->get('remove_parameter_ids');
+      if (isset($remove_ids[$i])) {
+        continue;
+      }
 
-    // Standard Google Analytics accounts support up to 20 custom dimensions,
-    // premium accounts support up to 200 custom dimensions.
-    $limit = ($config->get('premium')) ? 200 : 20;
-    for ($i = 1; $i <= $limit; $i++) {
-      $form['google_analytics_custom_dimension']['indexes'][$i]['index'] = [
-        '#default_value' => $i,
-        '#description' => $this->t('Index number'),
-        '#disabled' => TRUE,
-        '#size' => ($limit == 200) ? 3 : 2,
+      $form['tracking']['parameters']['indexes']['custom_parameters'][$i]['#attributes']['class'][] = 'draggable';
+      $form['tracking']['parameters']['indexes']['custom_parameters'][$i]['#weight'] = $i;
+
+      $form['tracking']['parameters']['indexes']['custom_parameters'][$i]['index'] = [
+        '#default_value' => $parameters[$i]['index'] ?? '',
+        '#description' => $this->t('Index (UA Only)'),
+        '#size' => 12,
         '#title' => $this->t('Custom dimension index #@index', ['@index' => $i]),
         '#title_display' => 'invisible',
         '#type' => 'textfield',
+        '#prefix' => '<div id="edit-index-'.$i.'">',
+        '#suffix' => '</div>',
+        '#attributes' => [
+          'readonly' => 'readonly',
+          'tabindex' => '-1'
+        ],
       ];
-      $form['google_analytics_custom_dimension']['indexes'][$i]['value'] = [
-        '#default_value' => isset($google_analytics_custom_dimension[$i]['value']) ? $google_analytics_custom_dimension[$i]['value'] : '',
-        '#description' => $this->t('The custom dimension value.'),
-        '#maxlength' => 255,
-        '#title' => $this->t('Custom dimension value #@index', ['@index' => $i]),
-        '#title_display' => 'invisible',
-        '#type' => 'textfield',
-        '#element_validate' => [[get_class($this), 'tokenElementValidate']],
-        '#token_types' => ['node'],
-      ];
-      if ($this->moduleHandler->moduleExists('token')) {
-        $form['google_analytics_custom_dimension']['indexes'][$i]['value']['#element_validate'][] = 'token_element_validate';
-      }
-    }
 
-    $form['google_analytics_custom_dimension']['google_analytics_description'] = [
-      '#type' => 'item',
-      '#description' => $this->t('You can supplement Google Analytics\' basic IP address tracking of visitors by segmenting users based on custom dimensions. Section 7 of the <a href=":ga_tos">Google Analytics terms of service</a> requires that You will not (and will not allow any third party to) use the Service to track, collect or upload any data that personally identifies an individual (such as a name, userid, email address or billing information), or other data which can be reasonably linked to such information by Google. You will have and abide by an appropriate Privacy Policy and will comply with all applicable laws and regulations relating to the collection of information from Visitors. You must post a Privacy Policy and that Privacy Policy must provide notice of Your use of cookies that are used to collect traffic data, and You must not circumvent any privacy features (e.g., an opt-out) that are part of the Service.', [':ga_tos' => 'https://www.google.com/analytics/terms/gb.html']),
-    ];
-    if ($this->moduleHandler->moduleExists('token')) {
-      $form['google_analytics_custom_dimension']['google_analytics_token_tree'] = [
-        '#theme' => 'token_tree_link',
-        '#token_types' => ['node'],
+      // GA doesn't use a '0' in its metric.
+      $index_count = $i+1;
+
+      $form['tracking']['parameters']['indexes']['custom_parameters'][$i]['type'] = [
+        '#type' => 'select',
+        '#default_value' => isset($parameters[$i]['type']) ? $parameters[$i]['type'] . '-' . $index_count : '-',
+        '#disabled' => isset($parameters[$i]['index']),
+        '#options' => [
+          '-'. $index_count => '',
+          'dimension-'. $index_count => $this->t('Dimension'),
+          'metric-'. $index_count => $this->t('Metric'),
+        ],
+        '#title' => $this->t('Parameter Type'),
+        '#title_display' => 'invisible',
+        '#description' => $this->t('Parameter Type'),
+        '#ajax' => array(
+          'callback' => '::parameterIndexCallback',
+          'disable-refocus' => FALSE, // Or TRUE to prevent re-focusing on the triggering element.
+          'event' => 'change',
+          'wrapper' => $param_wrapper_id, // This element is updated with this AJAX callback.
+          'progress' => [
+            'type' => 'throbber',
+            'message' => $this->t('Verifying entry...'),
+          ],
+        ),
       ];
-    }
-
-    // Custom Metrics.
-    $form['google_analytics_custom_metric'] = [
-      '#description' => $this->t('You can add Google Analytics <a href=":custom_var_documentation">Custom Metrics</a> here. You must have already configured your custom metrics in the <a href=":setup_documentation">Google Analytics Management Interface</a>. You may use tokens. Global and user tokens are always available; on node pages, node tokens are also available.', [':custom_var_documentation' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/custom-dims-mets', ':setup_documentation' => 'https://support.google.com/analytics/answer/2709829']),
-      '#title' => $this->t('Custom metrics'),
-      '#tree' => TRUE,
-      '#type' => 'details',
-    ];
-
-    $form['google_analytics_custom_metric']['indexes'] = [
-      '#type' => 'table',
-      '#header' => [
-        ['data' => $this->t('Index')],
-        ['data' => $this->t('Value')],
-      ],
-    ];
-
-    $google_analytics_custom_metric = $config->get('custom.metric');
-
-    // Standard Google Analytics accounts support up to 20 custom metrics,
-    // premium accounts support up to 200 custom metrics.
-    for ($i = 1; $i <= $limit; $i++) {
-      $form['google_analytics_custom_metric']['indexes'][$i]['index'] = [
-        '#default_value' => $i,
-        '#description' => $this->t('Index number'),
-        '#disabled' => TRUE,
-        '#size' => ($limit == 200) ? 3 : 2,
-        '#title' => $this->t('Custom metric index #@index', ['@index' => $i]),
+      $form['tracking']['parameters']['indexes']['custom_parameters'][$i]['name'] = [
+        '#default_value' => $parameters[$i]['name'] ?? '',
+        '#description' => $this->t('The custom parameter name.'),
+        '#maxlength' => 255,
+        '#title' => $this->t('Custom parameter name #@index', ['@index' => $parameters[$i]['index'] ?? $i]),
         '#title_display' => 'invisible',
         '#type' => 'textfield',
       ];
-      $form['google_analytics_custom_metric']['indexes'][$i]['value'] = [
-        '#default_value' => isset($google_analytics_custom_metric[$i]['value']) ? $google_analytics_custom_metric[$i]['value'] : '',
-        '#description' => $this->t('The custom metric value.'),
+      $form['tracking']['parameters']['indexes']['custom_parameters'][$i]['value'] = [
+        '#default_value' => $parameters[$i]['value'] ?? '',
+        '#description' => $this->t('The custom parameter value.'),
         '#maxlength' => 255,
-        '#title' => $this->t('Custom metric value #@index', ['@index' => $i]),
+        '#title' => $this->t('Custom parameter value #@index', ['@index' => $parameters[$i]['index'] ?? $i]),
         '#title_display' => 'invisible',
         '#type' => 'textfield',
         '#element_validate' => [[get_class($this), 'tokenElementValidate']],
         '#token_types' => ['node'],
       ];
+
+      // If there is more than one id, add the remove button.
+      if ($form_state->get('param_count') > 1) {
+        $form['tracking']['parameters']['indexes']['custom_parameters'][$i]['remove'] = [
+          '#type' => 'submit',
+          '#name' => 'remove_parameter_ids_'.$i,
+          '#value' => $this->t('Remove'),
+          '#submit' => ['::removeParametersCallback'],
+          '#limit_validation_errors' => [],
+          '#ajax' => [
+            'callback' => '::parametersFieldCallback',
+            'wrapper' => $param_wrapper_id,
+          ],
+        ];
+      }
+
       if ($this->moduleHandler->moduleExists('token')) {
-        $form['google_analytics_custom_metric']['indexes'][$i]['value']['#element_validate'][] = 'token_element_validate';
+        $form['tracking']['parameters']['indexes']['custom_parameters'][$i]['value']['#element_validate'][] = 'token_element_validate';
       }
     }
+    $form['tracking']['parameters']['indexes']['add_parameter'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Add another Parameter'),
+      '#name' => strtr($param_id_prefix, '-', '_') . '_add_parameter_id',
+      '#submit' => ['::addParameter'],
+      '#ajax' => [
+        'callback' => '::parametersFieldCallback',
+        'wrapper' => $param_wrapper_id,
+        'effect' => 'fade',
+      ],
+    ];
 
-    $form['google_analytics_custom_metric']['google_analytics_description'] = [
+    $form['tracking']['parameters']['google_analytics_description'] = [
       '#type' => 'item',
-      '#description' => $this->t('You can supplement Google Analytics\' basic IP address tracking of visitors by segmenting users based on custom metrics. Section 7 of the <a href=":ga_tos">Google Analytics terms of service</a> requires that You will not (and will not allow any third party to) use the Service to track, collect or upload any data that personally identifies an individual (such as a name, userid, email address or billing information), or other data which can be reasonably linked to such information by Google. You will have and abide by an appropriate Privacy Policy and will comply with all applicable laws and regulations relating to the collection of information from Visitors. You must post a Privacy Policy and that Privacy Policy must provide notice of Your use of cookies that are used to collect traffic data, and You must not circumvent any privacy features (e.g., an opt-out) that are part of the Service.', [':ga_tos' => 'https://www.google.com/analytics/terms/gb.html']),
+      '#description' => $this->t('You can supplement Google Analytics\' basic IP address tracking of visitors by segmenting users based on custom dimensions. Section 7 of the <a href=":ga_tos">Google Analytics terms of service</a> requires that You will not (and will not allow any third party to) use the Service to track, collect or upload any data that personally identifies an individual (such as a name, userid, email address or billing information), or other data which can be reasonably linked to such information by Google. You will have and abide by an appropriate Privacy Policy and will comply with all applicable laws and regulations relating to the collection of information from Visitors. You must post a Privacy Policy and that Privacy Policy must provide notice of Your use of cookies that are used to collect traffic data, and You must not circumvent any privacy features (e.g., an opt-out) that are part of the Service.', [':ga_tos' => 'https://www.google.com/analytics/terms/gb.html']),
     ];
     if ($this->moduleHandler->moduleExists('token')) {
-      $form['google_analytics_custom_metric']['google_analytics_token_tree'] = [
+      $form['tracking']['parameters']['google_analytics_token_tree'] = [
         '#theme' => 'token_tree_link',
         '#token_types' => ['node'],
       ];
@@ -538,11 +659,11 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
     $form['advanced']['codesnippet']['google_analytics_codesnippet_create'] = [
       '#type' => 'textarea',
-      '#title' => $this->t('Create only fields'),
-      '#default_value' => $this->getNameValueString($config->get('codesnippet.create')),
+      '#title' => $this->t('Parameters'),
+      '#default_value' => $this->getNameValueString($config->get('codesnippet.create') ?? []),
       '#rows' => 5,
-      '#description' => $this->t('Enter one value per line, in the format name|value. Settings in this textarea will be added to <code>ga("create", "UA-XXXX-Y", {"name":"value"});</code>. For more information, read <a href=":url">create only fields</a> documentation in the Analytics.js field reference.', [':url' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#create']),
-      '#element_validate' => [[get_class($this), 'validateCreateFieldValues']],
+      '#description' => $this->t('Enter one value per line, in the format name|value. Settings in this textarea will be added to <code>gtag("config", "UA-XXXX-Y", {"name":"value"});</code>. For more information, read <a href=":url">documentation</a> in the gtag.js reference.', [':url' => 'https://developers.google.com/analytics/devguides/collection/gtagjs/']),
+      '#element_validate' => [[get_class($this), 'validateParameterValues']],
     ];
     $form['advanced']['codesnippet']['google_analytics_codesnippet_before'] = [
       '#type' => 'textarea',
@@ -550,7 +671,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#default_value' => $config->get('codesnippet.before'),
       '#disabled' => $user_access_add_js_snippets,
       '#rows' => 5,
-      '#description' => $this->t('Code in this textarea will be added <strong>before</strong> <code>ga("send", "pageview");</code>.') . $user_access_add_js_snippets_permission_warning,
+      '#description' => $this->t('Code in this textarea will be added <strong>before</strong> <code>gtag("config", "UA-XXXX-Y");</code>.') . $user_access_add_js_snippets_permission_warning,
     ];
     $form['advanced']['codesnippet']['google_analytics_codesnippet_after'] = [
       '#type' => 'textarea',
@@ -558,7 +679,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#default_value' => $config->get('codesnippet.after'),
       '#disabled' => $user_access_add_js_snippets,
       '#rows' => 5,
-      '#description' => $this->t('Code in this textarea will be added <strong>after</strong> <code>ga("send", "pageview");</code>. This is useful if you\'d like to track a site in two accounts.') . $user_access_add_js_snippets_permission_warning,
+      '#description' => $this->t('Code in this textarea will be added <strong>after</strong> <code>gtag("config", "UA-XXXX-Y");</code>. This is useful if you\'d like to track a site in two accounts.') . $user_access_add_js_snippets_permission_warning,
     ];
 
     $form['advanced']['google_analytics_debug'] = [
@@ -577,40 +698,29 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   public function validateForm(array &$form, FormStateInterface $form_state) {
     parent::validateForm($form, $form_state);
 
-    // Trim custom dimensions and metrics.
-    foreach ($form_state->getValue(['google_analytics_custom_dimension', 'indexes']) as $dimension) {
-      $form_state->setValue(['google_analytics_custom_dimension', 'indexes', $dimension['index'], 'value'], trim($dimension['value']));
-      // Remove empty values from the array.
-      if (!mb_strlen($form_state->getValue(['google_analytics_custom_dimension', 'indexes', $dimension['index'], 'value']))) {
-        $form_state->unsetValue(['google_analytics_custom_dimension', 'indexes', $dimension['index']]);
-      }
-    }
-    $form_state->setValue('google_analytics_custom_dimension', $form_state->getValue(['google_analytics_custom_dimension', 'indexes']));
-
-    foreach ($form_state->getValue(['google_analytics_custom_metric', 'indexes']) as $metric) {
-      $form_state->setValue(['google_analytics_custom_metric', 'indexes', $metric['index'], 'value'], trim($metric['value']));
-      // Remove empty values from the array.
-      if (!mb_strlen($form_state->getValue(['google_analytics_custom_metric', 'indexes', $metric['index'], 'value']))) {
-        $form_state->unsetValue(['google_analytics_custom_metric', 'indexes', $metric['index']]);
+    // Trim custom dimensions and metrics, ensure indexes match row counts.
+    $custom_parameters = [];
+    if (!empty($form_state->getValue('custom_parameters'))) {
+      foreach ($form_state->getValue('custom_parameters') as $row => $parameter) {
+        if (!mb_strlen($parameter['value']) || !mb_strlen($parameter['name']) || empty($parameter['index'])) {
+          continue;
+        }
+        [$type] = explode('-', $parameter['type']);
+        $custom_parameters[$row]['index'] = $parameter['index'];
+        $custom_parameters[$row]['type'] = $type;
+        $custom_parameters[$row]['name'] = trim($parameter['name']);
+        $custom_parameters[$row]['value'] = trim($parameter['value']);
       }
+      $form_state->setValue('custom_parameters', $custom_parameters);
     }
-    $form_state->setValue('google_analytics_custom_metric', $form_state->getValue(['google_analytics_custom_metric', 'indexes']));
 
     // Trim some text values.
-    $form_state->setValue('google_analytics_account', trim($form_state->getValue('google_analytics_account')));
     $form_state->setValue('google_analytics_visibility_request_path_pages', trim($form_state->getValue('google_analytics_visibility_request_path_pages')));
     $form_state->setValue('google_analytics_cross_domains', trim($form_state->getValue('google_analytics_cross_domains')));
     $form_state->setValue('google_analytics_codesnippet_before', trim($form_state->getValue('google_analytics_codesnippet_before')));
     $form_state->setValue('google_analytics_codesnippet_after', trim($form_state->getValue('google_analytics_codesnippet_after')));
-    $form_state->setValue('google_analytics_visibility_user_role_roles', array_filter($form_state->getValue('google_analytics_visibility_user_role_roles')));
-    $form_state->setValue('google_analytics_trackmessages', array_filter($form_state->getValue('google_analytics_trackmessages')));
-
-    // Replace all type of dashes (n-dash, m-dash, minus) with normal dashes.
-    $form_state->setValue('google_analytics_account', str_replace(['–', '—', '−'], '-', $form_state->getValue('google_analytics_account')));
-
-    if (!preg_match('/^UA-\d+-\d+$/', $form_state->getValue('google_analytics_account'))) {
-      $form_state->setErrorByName('google_analytics_account', $this->t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxxxx-yy.'));
-    }
+    $form_state->setValue('google_analytics_visibility_user_role_roles', array_filter($form_state->getValue('google_analytics_visibility_user_role_roles') ?? []));
+    $form_state->setValue('google_analytics_trackmessages', array_filter($form_state->getValue('google_analytics_trackmessages') ?? []));
 
     // If multiple top-level domains has been selected, a domain names list is
     // required.
@@ -641,7 +751,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
     }
     // Clear obsolete local cache if cache has been disabled.
     if ($form_state->isValueEmpty('google_analytics_cache') && $form['advanced']['google_analytics_cache']['#default_value']) {
-      google_analytics_clear_js_cache();
+      $this->gaJavascript->clearGoogleAnalyticsJsCache();
     }
 
     // This is for the Newbie's who cannot read a text area description.
@@ -664,23 +774,38 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $config = $this->config('google_analytics.settings');
+
+    // Convert gtag ID values to accounts string
+    $accounts = $form_state->getValue('accounts');
+    $accounts = trim(implode(',', array_column($accounts, 'value')), ',');
+
+    // Convert custom parameter rows into GA indexes.
+    $custom_parameters = [];
+    if (!empty($form_state->getValue('custom_parameters'))) {
+      foreach ($form_state->getValue('custom_parameters') as $row) {
+        $custom_parameters[$row['index']]['type'] = $row['type'];
+        $custom_parameters[$row['index']]['name'] = $row['name'];
+        $custom_parameters[$row['index']]['value'] = $row['value'];
+      }
+      ksort($custom_parameters);
+    }
+
     $config
-      ->set('account', $form_state->getValue('google_analytics_account'))
-      ->set('premium', $form_state->getValue('google_analytics_premium'))
+      ->set('account', $accounts)
+      ->set('ua_legacy', $form_state->getValue('google_analytics_legacy'))
       ->set('cross_domains', $form_state->getValue('google_analytics_cross_domains'))
       ->set('codesnippet.create', $form_state->getValue('google_analytics_codesnippet_create'))
       ->set('codesnippet.before', $form_state->getValue('google_analytics_codesnippet_before'))
       ->set('codesnippet.after', $form_state->getValue('google_analytics_codesnippet_after'))
-      ->set('custom.dimension', $form_state->getValue('google_analytics_custom_dimension'))
-      ->set('custom.metric', $form_state->getValue('google_analytics_custom_metric'))
+      ->set('custom.parameters', $custom_parameters)
       ->set('domain_mode', $form_state->getValue('google_analytics_domain_mode'))
       ->set('track.files', $form_state->getValue('google_analytics_trackfiles'))
       ->set('track.files_extensions', $form_state->getValue('google_analytics_trackfiles_extensions'))
       ->set('track.colorbox', $form_state->getValue('google_analytics_trackcolorbox'))
       ->set('track.linkid', $form_state->getValue('google_analytics_tracklinkid'))
       ->set('track.urlfragments', $form_state->getValue('google_analytics_trackurlfragments'))
-      ->set('track.userid', $form_state->getValue('google_analytics_trackuserid'))
       ->set('track.mailto', $form_state->getValue('google_analytics_trackmailto'))
+      ->set('track.tel', $form_state->getValue('google_analytics_tracktel'))
       ->set('track.messages', $form_state->getValue('google_analytics_trackmessages'))
       ->set('track.outbound', $form_state->getValue('google_analytics_trackoutbound'))
       ->set('track.site_search', $form_state->getValue('google_analytics_site_search'))
@@ -703,6 +828,19 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     parent::submitForm($form, $form_state);
   }
 
+  public static function gtagElementValidate(&$element, FormStateInterface $form_state) {
+    // Get and Validate Analytics Account IDs
+    if (empty($element['#value'])) {
+      return;
+    }
+    $gtag_id = isset($element['#value']) ? $element['#value'] : $element['#default_value'];
+    $gtag_id = trim($gtag_id);
+    $gtag_id = str_replace(['–', '—', '−'], '-', $gtag_id);
+    if (!preg_match(GoogleAnalyticsPatterns::GOOGLE_ANALYTICS_GTAG_MATCH, $gtag_id)) {
+      $form_state->setError($element, t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxx-yy, G-xxxxxxxx, AW-xxxxxxxxx, or DC-xxxxxxxx.'));
+    }
+  }
+
   /**
    * Validate a form element that should have tokens in it.
    *
@@ -737,13 +875,13 @@ public static function tokenElementValidate(&$element, FormStateInterface $form_
   /**
    * Get an array of all forbidden tokens.
    *
-   * @param array $value
+   * @param array|string $value
    *   An array of token values.
    *
    * @return array
    *   A unique array of invalid tokens.
    */
-  protected static function getForbiddenTokens(array $value) {
+  protected static function getForbiddenTokens($value) {
     $invalid_tokens = [];
     $value_tokens = is_string($value) ? \Drupal::token()->scan($value) : $value;
 
@@ -753,8 +891,7 @@ protected static function getForbiddenTokens(array $value) {
       }
     }
 
-    array_unique($invalid_tokens);
-    return $invalid_tokens;
+    return array_unique($invalid_tokens);
   }
 
   /**
@@ -820,7 +957,7 @@ protected static function containsForbiddenToken($token_string) {
   }
 
   /**
-   * The #element_validate callback for create only fields.
+   * The #element_validate callback for parameters.
    *
    * @param array $element
    *   An associative array containing the properties and children of the
@@ -830,8 +967,8 @@ protected static function containsForbiddenToken($token_string) {
    *
    * @see form_process_pattern()
    */
-  public static function validateCreateFieldValues(array $element, FormStateInterface $form_state) {
-    $values = static::extractCreateFieldValues($element['#value']);
+  public static function validateParameterValues(array $element, FormStateInterface $form_state) {
+    $values = static::extractParameterValues($element['#value']);
 
     if (!is_array($values)) {
       $form_state->setError($element, t('The %element-title field contains invalid input.', ['%element-title' => $element['#title']]));
@@ -839,11 +976,11 @@ public static function validateCreateFieldValues(array $element, FormStateInterf
     else {
       // Check that name and value are valid for the field type.
       foreach ($values as $name => $value) {
-        if ($error = static::validateCreateFieldName($name)) {
+        if ($error = static::validateParameterName($name)) {
           $form_state->setError($element, $error);
           break;
         }
-        if ($error = static::validateCreateFieldValue($value)) {
+        if ($error = static::validateParameterValue($value)) {
           $form_state->setError($element, $error);
           break;
         }
@@ -864,7 +1001,7 @@ public static function validateCreateFieldValues(array $element, FormStateInterf
    *
    * @see \Drupal\options\Plugin\Field\FieldType\ListTextItem::allowedValuesString()
    */
-  protected static function extractCreateFieldValues($string) {
+  protected static function extractParameterValues($string) {
     $values = [];
 
     $list = explode("\n", $string);
@@ -886,11 +1023,11 @@ protected static function extractCreateFieldValues($string) {
       $values[$name] = $value;
     }
 
-    return static::convertFormValueDataTypes($values);
+    return self::convertFormValueDataTypes($values);
   }
 
   /**
-   * Checks whether a field name is valid.
+   * Checks whether a parameter name is valid.
    *
    * @param string $name
    *   The option value entered by the user.
@@ -898,54 +1035,61 @@ protected static function extractCreateFieldValues($string) {
    * @return string|null
    *   The error message if the specified value is invalid, NULL otherwise.
    */
-  protected static function validateCreateFieldName($name) {
+  protected static function validateParameterName($name) {
     // List of supported field names:
     // https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#create
-    $create_only_fields = [
-      'allowAnchor',
-      'alwaysSendReferrer',
-      'clientId',
-      'cookieName',
-      'cookieDomain',
-      'cookieExpires',
-      'legacyCookieDomain',
-      'legacyHistoryImport',
-      'sampleRate',
-      'siteSpeedSampleRate',
-      'storage',
-      'useAmpClientId',
+    $allowed_parameters = [
+      'client_id',
+      'currency',
+      'country',
+      'cookie_name',
+      'cookie_domain',
+      'cookie_expires',
+      'optimize_id',
+      'sample_rate',
+      'send_page_view',
+      'site_speed_sample_rate',
+      'use_amp_client_id',
     ];
 
-    if ($name == 'name') {
-      return t('Create only field name %name is a disallowed field name. Changing the <em>Tracker Name</em> is currently not supported.', ['%name' => $name]);
+    if ($name == 'allow_ad_personalization_signals') {
+      return t('Parameter name %name is disallowed. Please configure <em>Track display features</em> under <em>Tracking scope > Search and Advertising</em>.', ['%name' => $name]);
+    }
+    if ($name == 'anonymize_ip') {
+      return t('Parameter name %name is disallowed. Please configure <em>Anonymize visitors IP address</em> under <em>Tracking scope > Privacy</em>.', ['%name' => $name]);
+    }
+    if ($name == 'link_attribution') {
+      return t('Parameter name %name is disallowed. Please configure <em>Track enhanced link attribution</em> under <em>Tracking scope > Links and downloads</em>.', ['%name' => $name]);
     }
-    if ($name == 'allowLinker') {
-      return t('Create only field name %name is a disallowed field name. Please select <em>Multiple top-level domains</em> under <em>Tracking scope > Domains</em> to enable cross domain tracking.', ['%name' => $name]);
+    if ($name == 'linker') {
+      return t('Parameter name %name is disallowed. Please configure <em>Multiple top-level domains</em> under <em>Tracking scope > Domains</em> to enable cross domain tracking.', ['%name' => $name]);
     }
-    if ($name == 'userId') {
-      return t('Create only field name %name is a disallowed field name. Please enable <em>Track User ID</em> under <em>Tracking scope > Users</em>.', ['%name' => $name]);
+    if ($name == 'user_id') {
+      return t('Parameter name %name is disallowed. Please configure <em>Track User ID</em> under <em>Tracking scope > Users</em>.', ['%name' => $name]);
     }
-    if (!in_array($name, $create_only_fields)) {
-      return t('Create only field name %name is an unknown field name. Field names are case sensitive. Please see <a href=":url">create only fields</a> documentation for supported field names.', ['%name' => $name, ':url' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#create']);
+    if (!in_array($name, $allowed_parameters)) {
+      return t('Parameter name %name is unknown. Parameters are case sensitive. Please see <a href=":url">documentation</a> for supported parameters.', ['%name' => $name, ':url' => 'https://developers.google.com/analytics/devguides/collection/gtagjs/']);
     }
+    return NULL;
   }
 
   /**
    * Checks whether a candidate value is valid.
    *
-   * @param string $value
+   * @param string|bool $value
    *   The option value entered by the user.
    *
    * @return string|null
    *   The error message if the specified value is invalid, NULL otherwise.
    */
-  protected static function validateCreateFieldValue($value) {
+  protected static function validateParameterValue($value) {
     if (!is_bool($value) && !mb_strlen($value)) {
-      return t('A create only field requires a value.');
+      return t('A parameter requires a value.');
     }
     if (mb_strlen($value) > 255) {
-      return t('The value of a create only field must be a string at most 255 characters long.');
+      return t('The value of a parameter must be a string at most 255 characters long.');
     }
+    return NULL;
   }
 
   /**
@@ -962,7 +1106,7 @@ protected static function validateCreateFieldValue($value) {
    *    - Values are separated by a carriage return.
    *    - Each value is in the format "name|value" or "value".
    */
-  protected function getNameValueString(array $values) {
+  protected function getNameValueString(array $values = []) {
     $lines = [];
     foreach ($values as $name => $value) {
       // Convert data types.
@@ -981,7 +1125,7 @@ protected function getNameValueString(array $values) {
    * @param array $values
    *   Array of values.
    *
-   * @return string
+   * @return array
    *   Value with casted data type.
    */
   protected static function convertFormValueDataTypes(array $values) {
@@ -998,13 +1142,12 @@ protected static function convertFormValueDataTypes(array $values) {
 
       // Convert other known fields.
       switch ($name) {
-        case 'sampleRate':
+        case 'sample_rate':
           // Float types.
           settype($value, 'float');
           break;
 
-        case 'siteSpeedSampleRate':
-        case 'cookieExpires':
+        case 'cookie_expires':
           // Integer types.
           settype($value, 'integer');
           break;
@@ -1017,15 +1160,137 @@ protected static function convertFormValueDataTypes(array $values) {
   }
 
   /**
-   * {@inheritdoc}
+   * Callback for both ajax account buttons.
+   *
+   * Selects and returns the fieldset with the names in it.
    */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      // Load the service required to construct this class.
-      $container->get('config.factory'),
-      $container->get('current_user'),
-      $container->get('module_handler')
-    );
+  public function gtagFieldCallback(array &$form, FormStateInterface $form_state) {
+    return $form['general'];
+  }
+
+  /**
+   * Callback for both ajax custom parameters buttons.
+   *
+   * Selects and returns the fieldset with the names in it.
+   */
+  public function parametersFieldCallback(array &$form, FormStateInterface $form_state) {
+    return $form['tracking']['parameters']['indexes'];
+  }
+
+  /**
+   * Submit handler for the "add-one-more" button.
+   *
+   * Increments the max counter and causes a rebuild.
+   */
+  public function addOne(array &$form, FormStateInterface $form_state) {
+    $id_field = $form_state->get('id_count');
+    $add_button = $id_field + 1;
+    $form_state->set('id_count', $add_button);
+    // Since our buildForm() method relies on the value of 'num_names' to
+    // generate 'gtag_id' form elements, we have to tell the form to rebuild. If we
+    // don't do this, the form builder will not call buildForm().
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Submit handler for the "Add Parameter" button.
+   *
+   * Increments the max counter and causes a rebuild.
+   */
+  public function addParameter(array &$form, FormStateInterface $form_state) {
+    $id_field = $form_state->get('param_count');
+    $add_button = $id_field + 1;
+    $form_state->set('param_count', $add_button);
+    // Since our buildForm() method relies on the value of 'num_names' to
+    // generate 'gtag_id' form elements, we have to tell the form to rebuild. If we
+    // don't do this, the form builder will not call buildForm().
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Submit handler for the "remove one" button.
+   *
+   * Decrements the max counter and causes a form rebuild.
+   */
+  public function removeCallback(array &$form, FormStateInterface $form_state) {
+    $removed_trigger = $form_state->getTriggeringElement();
+    $gtag_id = (int)substr($removed_trigger['#name'], strlen('remove_gtags_ids'));
+    $form_state->set('remove_ids', $gtag_id);
+
+    // Since our buildForm() method relies on the value of 'num_names' to
+    // generate 'name' form elements, we have to tell the form to rebuild. If we
+    // don't do this, the form builder will not call buildForm().
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Submit handler for the "remove parameter" button.
+   *
+   * Decrements the max counter and causes a form rebuild.
+   */
+  public function removeParametersCallback(array &$form, FormStateInterface $form_state) {
+    $removed_trigger = $form_state->getTriggeringElement();
+    $parameter_id = (int)substr($removed_trigger['#name'], strlen('remove_parameter_ids_'));
+    $remove_ids = $form_state->get('remove_parameter_ids');
+    $remove_ids[$parameter_id] = TRUE;
+    $form_state->set('remove_parameter_ids', $remove_ids);
+
+    // Since our buildForm() method relies on the value of 'num_names' to
+    // generate 'name' form elements, we have to tell the form to rebuild. If we
+    // don't do this, the form builder will not call buildForm().
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Ajax callback to change the index ID depending on Type
+   */
+  public function parameterIndexCallback(array &$form, FormStateInterface $form_state) {
+    // Prepare our textfield. check if the example select field has a selected option.
+    if ($selectedValue = $form_state->getTriggeringElement()) {
+      // Get the index of the selected option.
+      // If the value is numeric it means the 'null' option was selected.
+      [$value, $current_row] = explode('-', $selectedValue['#value']);
+      if (!empty($value)) {
+        // Metric/Dimensions share index numbers. Re-order them
+        $parameter_count = ['dimension' => 0, 'metric' => 0];
+        $parameters = $form_state->getValue('custom_parameters');
+        foreach ($parameters as $row => $parameter) {
+          [$val, $ind] = explode('-', $parameter['type']);
+          $parameter_count[$val]++;
+          if($row == $current_row) {
+            $form['tracking']['parameters']['indexes']['custom_parameters'][$row]['index']['#value'] = $value.$parameter_count[$value];
+          }
+          else {
+            // Reset the counter for any other rows
+            $form['tracking']['parameters']['indexes']['custom_parameters'][$row]['index']['#value'] = !empty($val) ? $val.$parameter_count[$val] : $val;
+          }
+        }
+      }
+      // Return the prepared textfield.
+      return $form['tracking']['parameters']['indexes'];
+    }
+  }
+
+  /**
+   * Fetches and orders user defined (custom) parameters.
+   *
+   * @return array
+   *   The user defined parameters.
+   */
+  protected function getCustomParameters() {
+    if ($parameters = $this->config('google_analytics.settings')->get('custom.parameters')) {
+      $array = [];
+      foreach ($parameters as $row => $value) {
+        $array[] = [
+          'index' => $row,
+          'type' => $value['type'],
+          'name' => $value['name'],
+          'value' => $value['value'],
+        ];
+      }
+      return $array;
+    }
+    return [['value' => '']];
   }
 
 }
diff --git a/web/modules/google_analytics/src/GaAccount.php b/web/modules/google_analytics/src/GaAccount.php
new file mode 100644
index 0000000000..253bdbd937
--- /dev/null
+++ b/web/modules/google_analytics/src/GaAccount.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\google_analytics;
+
+use Drupal\google_analytics\Constants\GoogleAnalyticsPatterns;
+
+/**
+ * Decorator class for Google Analytics accounts
+ */
+class GaAccount {
+
+  /**
+   * The Google Analytics Account.
+   *
+   * @var string
+   */
+  protected $account;
+
+  public function __construct(string $account) {
+    $this->account = $account;
+  }
+
+  /**
+   * Return the account as a string.
+   *
+   * @return string
+   */
+  public function __toString() {
+    return $this->account;
+  }
+
+  /**
+   * Detects if there is a universal analytics account.
+   *
+   * If any account is UA, then this will return true.
+   *
+   * @return bool
+   */
+  public function isUniversalAnalyticsAccount() {
+    if (preg_match(GoogleAnalyticsPatterns::GOOGLE_ANALYTICS_UA_MATCH, $this->account)) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+}
\ No newline at end of file
diff --git a/web/modules/google_analytics/src/GaJavascriptInterface.php b/web/modules/google_analytics/src/GaJavascriptInterface.php
new file mode 100644
index 0000000000..5d554a382e
--- /dev/null
+++ b/web/modules/google_analytics/src/GaJavascriptInterface.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\google_analytics;
+
+/**
+ * Interface GaJavascriptInterface.
+ *
+ * @package Drupal\google_analytics
+ */
+interface GaJavascriptInterface {
+
+  /**
+   * Returns the Primary GA measurement ID.
+   *
+   * @return string
+   *   Object measurement ID.
+   */
+  public function getMeasurementId();
+
+  /**
+   * Returns object's config.
+   *
+   * @param string $measurement_id
+   *   The config's measurement_id.
+   *
+   * @return array
+   *   Object's config.
+   */
+  public function getConfig($measurement_id);
+
+  /**
+   * Metadata setter.
+   *
+   * @param array $config
+   *   Metadata array.
+   */
+  public function setConfig($measurement_id, array $config);
+
+  /**
+   * Returns all stored GA Events.
+   *
+   * @return array
+   *   Object's events.
+   */
+  public function getEvents();
+
+  /**
+   * Appends an event to the Javascript object.
+   *
+   * @param array $event
+   *   The event.
+   */
+  public function addEvent(array $event);
+
+  /**
+   * Converts object to array.
+   *
+   * @return mixed
+   *   Array representation.
+   */
+  public function toArray();
+
+}
diff --git a/web/modules/google_analytics/src/GaJavascriptObject.php b/web/modules/google_analytics/src/GaJavascriptObject.php
new file mode 100644
index 0000000000..48b414c539
--- /dev/null
+++ b/web/modules/google_analytics/src/GaJavascriptObject.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\google_analytics;
+
+use Drupal\Component\Serialization\Json;
+
+/**
+ * Class GaJavascript Object.
+ *
+ * @package Drupal\google_analytics
+ */
+class GaJavascriptObject implements GaJavascriptInterface {
+
+  /**
+   * Default measurement_id for the object.
+   *
+   * @var string
+   */
+  protected $measurement_id;
+
+  /**
+   * Object config.
+   *
+   * @var array
+   */
+  protected $config = [];
+
+  /**
+   * Events list.
+   *
+   * @var array
+   */
+  protected $events = [];
+
+  /**
+   * Custom URL.
+   */
+  protected $custom_url = '';
+
+  /**
+   * Adsense Script
+   */
+  protected $adsense = '';
+
+  /**
+   * GaJavascriptObject constructor.
+   *
+   * @param string $measurement_id
+   *   Object default measurement_id.
+   * @param array $config
+   *   Object config.
+   */
+  public function __construct($measurement_id, array $config = []) {
+    $this->measurement_id = $measurement_id;
+    $this->setConfig($measurement_id, $config);
+  }
+
+  /**
+   * Static Factory method to allow GaJavascript to interpret their own data.
+   *
+   * @param array $data
+   *   Initial data.
+   *
+   * @return \Drupal\google_analytics\GaJavascriptObject
+   *   GaJavascriptObject.
+   *
+   */
+  public static function fromArray(array $data) {
+    $object = new static($data['measurement_id'], $data['config']['measurement_id']);
+    return $object;
+  }
+
+  /**
+   * Static Factory method to format data from JSON into the Javascript Object.
+   *
+   * @param string $json
+   *   Data in JSON format.
+   *
+   * @return \Drupal\google_analytics\GaJavascriptObject
+   *   GA Javascript Object.
+   *
+   * @throws \ReflectionException
+   */
+  public static function fromJson(string $json) {
+    return self::fromArray(json_decode($json, TRUE));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function toArray() {
+    $output = [
+      'measurement_id' => $this->getMeasurementId(),
+      'config' => $this->config,
+      'events' => $this->events,
+    ];
+
+    return $output;
+  }
+
+  public function getMeasurementId() {
+    return $this->measurement_id;
+  }
+
+  public function getConfig($measurement_id = NULL) {
+    if (isset($this->config[$measurement_id ?? $this->measurement_id])) {
+      return $this->config[$measurement_id ?? $this->measurement_id];
+    }
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfig($measurement_id, array $config) {
+    $this->config[(string)$measurement_id] = $config;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEvents() {
+    return $this->events;
+  }
+
+  public function addEvent(array $event) {
+    $this->events[] = $event;
+  }
+
+  public function getCustomUrl() {
+    return $this->custom_url;
+  }
+
+  public function setCustomUrl($url) {
+    $this->custom_url = $url;
+  }
+
+  public function setAdsenseScript($domain = 'none') {
+    $this->adsense = 'window.google_analytics_domain_name = ' . Json::encode($domain) . ';
+                      window.google_analytics_uacct = ' . Json::encode($this->measurement_id) . ';';
+  }
+
+  public function getAdsenseScript() {
+    return $this->adsense;
+  }
+}
diff --git a/web/modules/google_analytics/src/GoogleAnalitycsInterface.php b/web/modules/google_analytics/src/GoogleAnalitycsInterface.php
deleted file mode 100644
index 0c98290328..0000000000
--- a/web/modules/google_analytics/src/GoogleAnalitycsInterface.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Drupal\google_analytics;
-
-/**
- * Provides an interface.
- */
-interface GoogleAnalitycsInterface {
-
-  /**
-   * Define the default file extension list that should be tracked as download.
-   */
-  const GOOGLE_ANALYTICS_TRACKFILES_EXTENSIONS = '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip';
-
-}
diff --git a/web/modules/google_analytics/src/Helpers/GoogleAnalyticsAccounts.php b/web/modules/google_analytics/src/Helpers/GoogleAnalyticsAccounts.php
new file mode 100644
index 0000000000..530dbccf9c
--- /dev/null
+++ b/web/modules/google_analytics/src/Helpers/GoogleAnalyticsAccounts.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\google_analytics\Helpers;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\PrivateKey;
+use Drupal\Core\Site\Settings;
+use Drupal\google_analytics\Constants\GoogleAnalyticsPatterns;
+use Drupal\google_analytics\GaAccount;
+
+class GoogleAnalyticsAccounts {
+
+  /**
+   * Private Key Service for generating user id hash.
+   *
+   * @var string
+   */
+  protected $privateKey;
+
+  /**
+   * The loaded config for the GA Module.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  private $config;
+
+  /**
+   * The Google Analytics Accounts storage array.
+   *
+   * @var array
+   */
+  private $accounts;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Drupal\Core\PrivateKey $private_key
+   *   The private key service.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, PrivateKey $private_key) {
+    $this->config = $config_factory->get('google_analytics.settings');
+
+    $accounts = $this->config->get('account');
+    // Create the accounts array from either a single gtag id or multiple ones.
+    if (strpos($accounts, ',') === FALSE) {
+      $this->accounts[] = new GaAccount($accounts);
+    }
+    else {
+      $accounts_array = explode(',', $accounts);
+      foreach($accounts_array as $account) {
+        $this->accounts[] = new GaAccount($account);
+      }
+    }
+
+    $this->privateKey = $private_key->get();
+  }
+
+  /**
+   * Generate user id hash to implement USER_ID.
+   *
+   * The USER_ID value should be a unique, persistent, and non-personally
+   * identifiable string identifier that represents a user or signed-in
+   * account across devices.
+   *
+   * @param int $uid
+   *   User id.
+   *
+   * @return string
+   *   User id hash.
+   */
+  public function getUserIdHash($uid) {
+    return Crypt::hmacBase64($uid, $this->privateKey . Settings::getHashSalt());
+  }
+
+  /**
+   * Get the default measurement ID. Defaults to the first account in config.
+   *
+   * @return false|mixed|string
+   */
+  public function getDefaultMeasurementId() {
+    // The top UA- or G- Account is the default measurement ID.
+    foreach ($this->accounts as $account) {
+      if (preg_match(GoogleAnalyticsPatterns::GOOGLE_ANALYTICS_TRACKING_MATCH, $account)) {
+        return $account;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Get accounts that aren't the default measurement ID.
+   *
+   * @return array|false|string[]
+   */
+  public function getAdditionalAccounts() {
+    return array_filter($this->accounts, function($v) {
+      return $v !== $this->getDefaultMeasurementId();
+    });
+  }
+
+  /**
+   * Return all the GA accounts stored.
+   *
+   * @return array|false|string[]
+   */
+  public function getAccounts() {
+    return $this->accounts;
+  }
+}
\ No newline at end of file
diff --git a/web/modules/google_analytics/src/Helpers/VisiblityTracker.php b/web/modules/google_analytics/src/Helpers/VisiblityTracker.php
new file mode 100644
index 0000000000..95f25f6c22
--- /dev/null
+++ b/web/modules/google_analytics/src/Helpers/VisiblityTracker.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Drupal\google_analytics\Helpers;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Path\CurrentPathStack;
+use Drupal\Core\Path\PathMatcherInterface;
+use Drupal\path_alias\AliasManagerInterface;
+use Drupal\user\UserDataInterface;
+
+/**
+ * Defines the Path Matcher class.
+ */
+class VisiblityTracker {
+
+  /**
+   * @var \Drupal\path_alias\AliasManagerInterface
+   */
+  private $aliasManager;
+
+  /**
+   * @var \Drupal\Core\Path\PathMatcherInterface
+   */
+  private $pathMatcher;
+
+  /**
+   * @var \Drupal\user\UserDataInterface
+   */
+  private $userData;
+
+  /**
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  private $config;
+
+  /**
+   * @var \Drupal\Core\Path\CurrentPathStack
+   */
+  private $currentPath;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Drupal\path_alias\AliasManagerInterface $alias_manager
+   *   The alias manager service.
+   * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
+   *   The path matcher service.
+   * @param \Drupal\user\UserDataInterface $user_data
+   *   The user data service.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, AliasManagerInterface $alias_manager, PathMatcherInterface $path_matcher, UserDataInterface $user_data, CurrentPathStack $current_path) {
+    $this->config = $config_factory->get('google_analytics.settings');
+    $this->aliasManager = $alias_manager;
+    $this->pathMatcher = $path_matcher;
+    $this->userData = $user_data;
+    $this->currentPath = $current_path;
+  }
+
+
+  /**
+   * Tracking visibility check for an user object.
+   *
+   * @param object $account
+   *   A user object containing an array of roles to check.
+   *
+   * @return bool
+   *   TRUE if the current user is being tracked by Google Analytics,
+   *   otherwise FALSE.
+   */
+  public function getUserVisibilty($account) {
+    $enabled = FALSE;
+
+    // Is current user a member of a role that should be tracked?
+    if ($this->getVisibilityRoles($account)) {
+
+      // Use the user's block visibility setting, if necessary.
+      if (($visibility_user_account_mode = $this->config->get('visibility.user_account_mode')) != 0) {
+        $user_data_google_analytics = $this->userData->get('google_analytics', $account->id());
+        if ($account->id() && isset($user_data_google_analytics['user_account_users'])) {
+          $enabled = $user_data_google_analytics['user_account_users'];
+        }
+        else {
+          $enabled = ($visibility_user_account_mode == 1);
+        }
+      }
+      else {
+        $enabled = TRUE;
+      }
+    }
+
+    return $enabled;
+  }
+
+  /**
+   * Tracking visibility check for user roles.
+   *
+   * Based on visibility setting this function returns TRUE if JS code should
+   * be added for the current role and otherwise FALSE.
+   *
+   * @param object $account
+   *   A user object containing an array of roles to check.
+   *
+   * @return bool
+   *   TRUE if JS code should be added for the current role and otherwise FALSE.
+   */
+  public function getVisibilityRoles($account) {
+    $enabled = $visibility_user_role_mode = $this->config->get('visibility.user_role_mode');
+    $visibility_user_role_roles = $this->config->get('visibility.user_role_roles');
+
+    if (count($visibility_user_role_roles) > 0) {
+      // One or more roles are selected.
+      foreach (array_values($account->getRoles()) as $user_role) {
+        // Is the current user a member of one of these roles?
+        if (in_array($user_role, $visibility_user_role_roles)) {
+          // Current user is a member of a role that should be tracked/excluded
+          // from tracking.
+          $enabled = !$visibility_user_role_mode;
+          break;
+        }
+      }
+    }
+    else {
+      // No role is selected for tracking, therefore all roles should be tracked.
+      $enabled = TRUE;
+    }
+
+    return $enabled;
+  }
+
+  /**
+   * Tracking visibility check for pages.
+   *
+   * Based on visibility setting this function returns TRUE if JS code should
+   * be added to the current page and otherwise FALSE.
+   */
+  public function getVisibilityPages() {
+    static $page_match;
+
+    // Cache visibility result if function is called more than once.
+    if (!isset($page_match)) {
+      $visibility_request_path_mode = $this->config->get('visibility.request_path_mode');
+      $visibility_request_path_pages = $this->config->get('visibility.request_path_pages');
+
+      // Match path if necessary.
+      if (!empty($visibility_request_path_pages)) {
+        // Convert path to lowercase. This allows comparison of the same path
+        // with different case. Ex: /Page, /page, /PAGE.
+        $pages = mb_strtolower($visibility_request_path_pages);
+        if ($visibility_request_path_mode < 2) {
+          // Compare the lowercase path alias (if any) and internal path.
+          $path = $this->currentPath->getPath();
+          $path_alias = mb_strtolower($this->aliasManager->getAliasByPath($path));
+          $page_match = $this->pathMatcher->matchPath($path_alias, $pages) || (($path != $path_alias) && $this->pathMatcher->matchPath($path, $pages));
+          // When $visibility_request_path_mode has a value of 0, the tracking
+          // code is displayed on all pages except those listed in $pages. When
+          // set to 1, it is displayed only on those pages listed in $pages.
+          $page_match = !($visibility_request_path_mode xor $page_match);
+        }
+        else {
+          $page_match = FALSE;
+        }
+      }
+      else {
+        $page_match = TRUE;
+      }
+
+    }
+    return $page_match;
+  }
+}
\ No newline at end of file
diff --git a/web/modules/google_analytics/src/JavascriptLocalCache.php b/web/modules/google_analytics/src/JavascriptLocalCache.php
new file mode 100644
index 0000000000..0d72efea41
--- /dev/null
+++ b/web/modules/google_analytics/src/JavascriptLocalCache.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Drupal\google_analytics;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\State\StateInterface;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\RequestException;
+
+class JavascriptLocalCache {
+
+  /**
+   * Google Analytics Javascript URL.
+   */
+  const GOOGLE_ANALYTICS_JAVASCRIPT_URL =  'https://www.googletagmanager.com/gtag/js';
+
+  /**
+   * @var \Drupal\Core\File\FileSystem
+   */
+  protected $fileSystem;
+
+  /**
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * @var \GuzzleHttp\ClientInterface
+   */
+  protected $httpClient;
+
+  /**
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  public function __construct(ClientInterface $http_client, FileSystemInterface $file_system, ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory, StateInterface $state) {
+    $this->httpClient = $http_client;
+    $this->fileSystem = $file_system;
+    $this->configFactory = $config_factory;
+    $this->state = $state;
+    $this->logger = $logger_factory->get('google_analytics');
+  }
+  /**
+   * Download/Synchronize/Cache tracking code file locally.
+   *
+   * @param string $tracking_id
+   *   The GA Tracking ID
+   * @param bool $synchronize
+   *   Synchronize to local cache if remote file has changed.
+   *
+   * @return string
+   *   The path to the local or remote tracking file.
+   */
+  public function fetchGoogleAnalyticsJavascript(string $tracking_id, bool $synchronize = FALSE) {
+    $path = 'public://google_analytics';
+    $remote_url = self::GOOGLE_ANALYTICS_JAVASCRIPT_URL . '?id=' . $tracking_id;
+    $file_destination = $path . '/gtag.js';
+
+    // If cache is disabled, just return the URL for GA
+    if (!$this->configFactory->get('google_analytics.settings')->get('cache')) {
+      return $remote_url;
+    }
+
+    if (!file_exists($file_destination) || $synchronize) {
+      // Download the latest tracking code.
+      try {
+        $data = (string) $this->httpClient
+          ->get($remote_url)
+          ->getBody();
+
+        if (file_exists($file_destination)) {
+          // Synchronize tracking code and replace local file if outdated.
+          $data_hash_local = Crypt::hashBase64(file_get_contents($file_destination));
+          $data_hash_remote = Crypt::hashBase64($data);
+          // Check that the files directory is writable.
+          if ($data_hash_local != $data_hash_remote && $this->fileSystem->prepareDirectory($path)) {
+            // Save updated tracking code file to disk.
+            $this->fileSystem->saveData($data, $file_destination, FileSystemInterface::EXISTS_REPLACE);
+            // Based on Drupal Core class AssetDumper.
+            if (extension_loaded('zlib') && $this->configFactory->get('system.performance')->get('js.gzip')) {
+              $this->fileSystem->saveData(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FileSystemInterface::EXISTS_REPLACE);
+            }
+            $this->logger->info('Locally cached tracking code file has been updated.');
+
+            // Change query-strings on css/js files to enforce reload for all
+            // users.
+            _drupal_flush_css_js();
+          }
+        }
+        else {
+          // Check that the files directory is writable.
+          if ($this->fileSystem->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY)) {
+            // There is no need to flush JS here as core refreshes JS caches
+            // automatically, if new files are added.
+            $this->fileSystem->saveData($data, $file_destination, FileSystemInterface::EXISTS_REPLACE);
+            // Based on Drupal Core class AssetDumper.
+            if (extension_loaded('zlib') && $this->configFactory->get('system.performance')->get('js.gzip')) {
+              $this->fileSystem->saveData(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FileSystemInterface::EXISTS_REPLACE);
+            }
+            $this->logger->info('Locally cached tracking code file has been saved.');
+          }
+        }
+      }
+      catch (RequestException $exception) {
+        watchdog_exception('google_analytics', $exception);
+        return $remote_url;
+      }
+    }
+    // Return the local JS file path.
+    $query_string = '?' . (\Drupal::state()->get('system.css_js_query_string') ?: '0');
+    return file_url_transform_relative(file_create_url($file_destination)) . $query_string;
+  }
+
+  /**
+   * Delete cached files and directory.
+   */
+  public function clearGoogleAnalyticsJsCache() {
+    $path = 'public://google_analytics';
+    if (is_dir($path)) {
+      $this->fileSystem->deleteRecursive($path);
+
+      // Change query-strings on css/js files to enforce reload for all users.
+      _drupal_flush_css_js();
+
+      $this->logger->info('Local Google Analytics file cache has been purged.');
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityPages.php b/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityPages.php
index 8374c5520d..23a26abca2 100644
--- a/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityPages.php
+++ b/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityPages.php
@@ -38,15 +38,6 @@ class GoogleAnalyticsVisibilityPages extends ProcessPluginBase implements Contai
    */
   protected $migrationPlugin;
 
-  /**
-   * Whether or not to skip Google Analytics that use PHP for visibility.
-   *
-   * Only applies if the PHP module is not enabled.
-   *
-   * @var bool
-   */
-  protected $skipPHP = FALSE;
-
   /**
    * {@inheritdoc}
    */
@@ -54,10 +45,6 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->moduleHandler = $module_handler;
     $this->migrationPlugin = $migration_plugin;
-
-    if (isset($configuration['skip_php'])) {
-      $this->skipPHP = $configuration['skip_php'];
-    }
   }
 
   /**
@@ -75,7 +62,7 @@ public static function create(ContainerInterface $container, array $configuratio
       $plugin_id,
       $plugin_definition,
       $container->get('module_handler'),
-      $container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_configuration, $migration)
+      $container->get('plugin.manager.migrate.process')->createInstance('migration_lookup', $migration_configuration, $migration)
     );
   }
 
@@ -90,16 +77,10 @@ public function transform($value, MigrateExecutableInterface $migrate_executable
     if ($pages) {
       // 2 == BLOCK_VISIBILITY_PHP in Drupal 6 and 7.
       if ($old_visibility == 2) {
-        // If the PHP module is present, migrate the visibility code unaltered.
-        if ($this->moduleHandler->moduleExists('php')) {
-          $request_path_pages = $pages;
-        }
         // Skip the row if we're configured to. If not, we don't need to do
         // anything else -- the block will simply have no PHP or request_path
-        // visibility configuration.
-        elseif ($this->skipPHP) {
-          throw new MigrateSkipRowException();
-        }
+        // visibility configuration. You will need to manually migrate PHP code.
+        throw new MigrateSkipRowException();
       }
       else {
         $paths = preg_split("(\r\n?|\n)", $pages);
diff --git a/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityRoles.php b/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityRoles.php
index 2fe62b405c..c6b4374e32 100644
--- a/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityRoles.php
+++ b/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityRoles.php
@@ -61,7 +61,7 @@ public static function create(ContainerInterface $container, array $configuratio
       $plugin_id,
       $plugin_definition,
       $container->get('module_handler'),
-      $container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_configuration, $migration)
+      $container->get('plugin.manager.migrate.process')->createInstance('migration_lookup', $migration_configuration, $migration)
     );
   }
 
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsJavaScriptTest.js b/web/modules/google_analytics/src/Tests/GoogleAnalyticsJavaScriptTest.js
similarity index 100%
rename from web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsJavaScriptTest.js
rename to web/modules/google_analytics/src/Tests/GoogleAnalyticsJavaScriptTest.js
diff --git a/web/modules/google_analytics/tests/modules/google_analytics_test/google_analytics_test.info.yml b/web/modules/google_analytics/tests/modules/google_analytics_test/google_analytics_test.info.yml
index eb378b90c7..a347aa514f 100644
--- a/web/modules/google_analytics/tests/modules/google_analytics_test/google_analytics_test.info.yml
+++ b/web/modules/google_analytics/tests/modules/google_analytics_test/google_analytics_test.info.yml
@@ -3,7 +3,7 @@ type: module
 description: 'Support module for Google Analytics testing.'
 package: Testing
 
-# Information added by Drupal.org packaging script on 2020-06-04
-version: '8.x-2.5'
+# Information added by Drupal.org packaging script on 2021-10-14
+version: '4.0.0'
 project: 'google_analytics'
-datestamp: 1591298498
+datestamp: 1634230241
diff --git a/web/modules/google_analytics/tests/modules/google_analytics_test/src/Controller/GoogleAnalyticsTestController.php b/web/modules/google_analytics/tests/modules/google_analytics_test/src/Controller/GoogleAnalyticsTestController.php
index 7a260d6bd6..9cfc9553db 100644
--- a/web/modules/google_analytics/tests/modules/google_analytics_test/src/Controller/GoogleAnalyticsTestController.php
+++ b/web/modules/google_analytics/tests/modules/google_analytics_test/src/Controller/GoogleAnalyticsTestController.php
@@ -20,7 +20,7 @@ public function drupalAddMessageTest() {
     $this->messenger()->addMessage($this->t('Example status message.'), 'status');
     $this->messenger()->addMessage($this->t('Example warning message.'), 'warning');
     $this->messenger()->addMessage($this->t('Example error message.'), 'error');
-    $this->messenger()->addMessage($this->t('Example error <em>message</em> with html tags and <a href="http://example.com/">link</a>.'), 'error');
+    $this->messenger()->addMessage($this->t('Example error <em>message</em> with html tags and <a href="https://example.com/">link</a>.'), 'error');
 
     return [];
   }
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsBasicTest.php b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsBasicTest.php
index 1054a8b89d..105f3630c3 100644
--- a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsBasicTest.php
+++ b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsBasicTest.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\Tests\google_analytics\Functional;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -13,6 +15,8 @@
  */
 class GoogleAnalyticsBasicTest extends BrowserTestBase {
 
+  use StringTranslationTrait;
+
   /**
    * User without permissions to use snippets.
    *
@@ -20,6 +24,13 @@ class GoogleAnalyticsBasicTest extends BrowserTestBase {
    */
   protected $noSnippetUser;
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
   /**
    * Modules to enable.
    *
@@ -32,14 +43,16 @@ class GoogleAnalyticsBasicTest extends BrowserTestBase {
   ];
 
   /**
-   * {@inheritdoc}
+   * Default theme.
+   *
+   * @var string
    */
   protected $defaultTheme = 'stark';
 
   /**
    * {@inheritdoc}
    */
-  protected function setUp() {
+  protected function setUp() :void {
     parent::setUp();
 
     $permissions = [
@@ -52,8 +65,8 @@ protected function setUp() {
     // User to set up google_analytics.
     $this->noSnippetUser = $this->drupalCreateUser($permissions);
     $permissions[] = 'add JS snippets for google analytics';
-    $this->admin_user = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($this->admin_user);
+    $this->adminUser = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->adminUser);
 
     // Place the block or the help is not shown.
     $this->drupalPlaceBlock('help_block', ['region' => 'help']);
@@ -66,40 +79,41 @@ public function testGoogleAnalyticsConfiguration() {
     // Check if Configure link is available on 'Extend' page.
     // Requires 'administer modules' permission.
     $this->drupalGet('admin/modules');
-    $this->assertSession()->responseContains('admin/config/system/google-analytics');
+    $this->assertSession()->responseContains('admin/config/services/google-analytics');
 
     // Check if Configure link is available on 'Status Reports' page.
     // NOTE: Link is only shown without UA code configured.
     // Requires 'administer site configuration' permission.
     $this->drupalGet('admin/reports/status');
-    $this->assertSession()->responseContains('admin/config/system/google-analytics');
+    $this->assertSession()->responseContains('admin/config/services/google-analytics');
 
     // Check for setting page's presence.
-    $this->drupalGet('admin/config/system/google-analytics');
-    $this->assertSession()->responseContains(t('Web Property ID'));
+    $this->drupalGet('admin/config/services/google-analytics');
+    $this->assertSession()->responseContains($this->t('Web Property ID(s)'));
 
     // Check for account code validation.
-    $edit['google_analytics_account'] = $this->randomMachineName(2);
-    $this->drupalPostForm('admin/config/system/google-analytics', $edit, t('Save configuration'));
-    $this->assertSession()->responseContains(t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxxxx-yy.'));
+    $edit['accounts[0][value]'] = $this->randomMachineName(2);
+    $this->drupalGet('admin/config/services/google-analytics');
+    $this->submitForm($edit, $this->t('Save configuration'));
+    $this->assertSession()->responseContains($this->t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxx-yy, G-xxxxxxxx, AW-xxxxxxxxx, or DC-xxxxxxxx.'));
 
     // User should have access to code snippets.
-    $this->assertFieldByName('google_analytics_codesnippet_create');
-    $this->assertFieldByName('google_analytics_codesnippet_before');
-    $this->assertFieldByName('google_analytics_codesnippet_after');
-    $this->assertNoFieldByXPath("//textarea[@name='google_analytics_codesnippet_create' and @disabled='disabled']", NULL, '"Create only fields" is enabled.');
+    $this->assertSession()->fieldExists('google_analytics_codesnippet_create');
+    $this->assertSession()->fieldExists('google_analytics_codesnippet_before');
+    $this->assertSession()->fieldExists('google_analytics_codesnippet_after');
+    $this->assertNoFieldByXPath("//textarea[@name='google_analytics_codesnippet_create' and @disabled='disabled']", NULL, '"Parameters" field is enabled.');
     $this->assertNoFieldByXPath("//textarea[@name='google_analytics_codesnippet_before' and @disabled='disabled']", NULL, '"Code snippet (before)" is enabled.');
     $this->assertNoFieldByXPath("//textarea[@name='google_analytics_codesnippet_after' and @disabled='disabled']", NULL, '"Code snippet (after)" is enabled.');
 
     // Login as user without JS permissions.
     $this->drupalLogin($this->noSnippetUser);
-    $this->drupalGet('admin/config/system/google-analytics');
+    $this->drupalGet('admin/config/services/google-analytics');
 
-    // User should *not* have access to snippets, but create fields.
-    $this->assertFieldByName('google_analytics_codesnippet_create');
-    $this->assertFieldByName('google_analytics_codesnippet_before');
-    $this->assertFieldByName('google_analytics_codesnippet_after');
-    $this->assertNoFieldByXPath("//textarea[@name='google_analytics_codesnippet_create' and @disabled='disabled']", NULL, '"Create only fields" is enabled.');
+    // User should *not* have access to snippets, but parameters field.
+    $this->assertSession()->fieldExists('google_analytics_codesnippet_create');
+    $this->assertSession()->fieldExists('google_analytics_codesnippet_before');
+    $this->assertSession()->fieldExists('google_analytics_codesnippet_after');
+    $this->assertNoFieldByXPath("//textarea[@name='google_analytics_codesnippet_create' and @disabled='disabled']", NULL, '"Parameters" field is enabled.');
     $this->assertFieldByXPath("//textarea[@name='google_analytics_codesnippet_before' and @disabled='disabled']", NULL, '"Code snippet (before)" is disabled.');
     $this->assertFieldByXPath("//textarea[@name='google_analytics_codesnippet_after' and @disabled='disabled']", NULL, '"Code snippet (after)" is disabled.');
   }
@@ -109,12 +123,12 @@ public function testGoogleAnalyticsConfiguration() {
    */
   public function testGoogleAnalyticsHelp() {
     // Requires help and block module and help block placement.
-    $this->drupalGet('admin/config/system/google-analytics');
-    $this->assertText('Google Analytics is a free (registration required) website traffic and marketing effectiveness service.');
+    $this->drupalGet('admin/config/services/google-analytics');
+    $this->assertSession()->pageTextContains('Google Analytics is a free (registration required) website traffic and marketing effectiveness service.');
 
     // Requires help.module.
     $this->drupalGet('admin/help/google_analytics');
-    $this->assertText('Google Analytics adds a web statistics tracking system to your website.');
+    $this->assertSession()->pageTextContains('Google Analytics adds a web statistics tracking system to your website.');
   }
 
   /**
@@ -124,7 +138,7 @@ public function testGoogleAnalyticsPageVisibility() {
     // Verify that no tracking code is embedded into the webpage; if there is
     // only the module installed, but UA code not configured. See #2246991.
     $this->drupalGet('');
-    $this->assertSession()->responseNotContains('https://www.google-analytics.com/analytics.js');
+    $this->assertSession()->responseNotContains('https://www.googletagmanager.com/gtag/js?id=');
 
     $ua_code = 'UA-123456-1';
     $this->config('google_analytics.settings')->set('account', $ua_code)->save();
@@ -138,22 +152,22 @@ public function testGoogleAnalyticsPageVisibility() {
 
     // Check tracking code visibility.
     $this->drupalGet('');
-    $this->assertSession()->responseContains($ua_code);
+    $this->assertSession()->responseContains('gtag("config", "' . $ua_code . '"');
 
     // Test whether tracking code is not included on pages to omit.
     $this->drupalGet('admin');
     $this->assertSession()->responseNotContains($ua_code);
-    $this->drupalGet('admin/config/system/google-analytics');
+    $this->drupalGet('admin/config/services/google-analytics');
     // Checking for tracking URI here, as $ua_code is displayed in the form.
-    $this->assertSession()->responseNotContains('https://www.google-analytics.com/analytics.js');
+    $this->assertSession()->responseNotContains('https://www.googletagmanager.com/gtag/js?id=');
 
     // Test whether tracking code display is properly flipped.
     $this->config('google_analytics.settings')->set('visibility.request_path_mode', 1)->save();
     $this->drupalGet('admin');
     $this->assertSession()->responseContains($ua_code);
-    $this->drupalGet('admin/config/system/google-analytics');
+    $this->drupalGet('admin/config/services/google-analytics');
     // Checking for tracking URI here, as $ua_code is displayed in the form.
-    $this->assertSession()->responseContains('https://www.google-analytics.com/analytics.js');
+    $this->assertSession()->responseContains('https://www.googletagmanager.com/gtag/js?id=');
     $this->drupalGet('');
     $this->assertSession()->responseNotContains($ua_code);
 
@@ -166,18 +180,6 @@ public function testGoogleAnalyticsPageVisibility() {
     $this->config('google_analytics.settings')->set('visibility.request_path_mode', 0)->save();
     // Enable tracking code for all user roles.
     $this->config('google_analytics.settings')->set('visibility.user_role_roles', [])->save();
-
-    $base_path = base_path();
-
-    // Test whether 403 forbidden tracking code is shown if user has no access.
-    $this->drupalGet('admin');
-    $this->assertSession()->statusCodeEquals(403);
-    $this->assertSession()->responseContains($base_path . '403.html');
-
-    // Test whether 404 not found tracking code is shown on non-existent pages.
-    $this->drupalGet($this->randomMachineName(64));
-    $this->assertSession()->statusCodeEquals(404);
-    $this->assertSession()->responseContains($base_path . '404.html');
   }
 
   /**
@@ -191,78 +193,71 @@ public function testGoogleAnalyticsTrackingCode() {
     $this->config('google_analytics.settings')->set('visibility.request_path_mode', 0)->save();
     // Enable tracking code for all user roles.
     $this->config('google_analytics.settings')->set('visibility.user_role_roles', [])->save();
+    // Disable Anonymous Tracking since its enabled by default.
+    $this->config('google_analytics.settings')->set('privacy.anonymizeip', 0)->save();
 
     /* Sample JS code as added to page:
     <script type="text/javascript" src="/sites/all/modules/google_analytics/google_analytics.js?w"></script>
+    <!-- Global Site Tag (gtag.js) - Google Analytics -->
+    <script async src="https://www.googletagmanager.com/gtag/js?id=UA-123456-7"></script>
     <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-123456-7');
-    ga('send', 'pageview');
+    window.dataLayer = window.dataLayer || [];
+    function gtag(){dataLayer.push(arguments)};
+    gtag('js', new Date());
+    gtag('config', 'UA-123456-7');
     </script>
-    <!-- End Google Analytics -->
      */
 
     // Test whether tracking code uses latest JS.
     $this->config('google_analytics.settings')->set('cache', 0)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseContains('https://www.google-analytics.com/analytics.js');
+    $this->assertSession()->responseContains('<script async src="https://www.googletagmanager.com/gtag/js?id=' . $ua_code . '"></script>');
+    $this->assertSession()->responseContains('window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments)};gtag("js", new Date());');
+    $this->assertSession()->responseContains('gtag("config", ' . Json::encode($ua_code));
 
-    // Test whether anonymize visitors IP address feature has been enabled.
-    $this->config('google_analytics.settings')->set('privacy.anonymizeip', 0)->save();
-    $this->drupalGet('');
-    $this->assertSession()->responseNotContains('ga("set", "anonymizeIp", true);');
     // Enable anonymizing of IP addresses.
     $this->config('google_analytics.settings')->set('privacy.anonymizeip', 1)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseContains('ga("set", "anonymizeIp", true);');
+    $this->assertSession()->responseContains('"anonymize_ip":true');
+
+    // Test whether anonymize visitors IP address feature has been enabled.
+    $this->config('google_analytics.settings')->set('privacy.anonymizeip', 0)->save();
+    $this->drupalGet('');
+    $this->assertSession()->responseNotContains('"anonymize_ip":true');
 
     // Test if track Enhanced Link Attribution is enabled.
     $this->config('google_analytics.settings')->set('track.linkid', 1)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseContains('ga("require", "linkid", "linkid.js");');
+    $this->assertSession()->responseContains('"link_attribution":true');
 
     // Test if track Enhanced Link Attribution is disabled.
     $this->config('google_analytics.settings')->set('track.linkid', 0)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseNotContains('ga("require", "linkid", "linkid.js");');
+    $this->assertSession()->responseNotContains('"link_attribution":true');
 
-    // Test if tracking of url fragments is enabled.
-    $this->config('google_analytics.settings')->set('track.urlfragments', 1)->save();
-    $this->drupalGet('');
-    $this->assertSession()->responseContains('ga("set", "page", location.pathname + location.search + location.hash);');
-
-    // Test if tracking of url fragments is disabled.
-    $this->config('google_analytics.settings')->set('track.urlfragments', 0)->save();
-    $this->drupalGet('');
-    $this->assertSession()->responseNotContains('ga("set", "page", location.pathname + location.search + location.hash);');
-
-    // Test if tracking of User ID is enabled.
-    $this->config('google_analytics.settings')->set('track.userid', 1)->save();
-    $this->drupalGet('');
-    $this->assertSession()->responseContains(', {"cookieDomain":"auto","userId":"');
-
-    // Test if tracking of User ID is disabled.
-    $this->config('google_analytics.settings')->set('track.userid', 0)->save();
+    // Test if track display features is disabled.
+    $this->config('google_analytics.settings')->set('track.displayfeatures', 0)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseNotContains(', {"cookieDomain":"auto","userId":"');
+    $this->assertSession()->responseContains('"allow_ad_personalization_signals":false');
 
     // Test if track display features is enabled.
     $this->config('google_analytics.settings')->set('track.displayfeatures', 1)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseContains('ga("require", "displayfeatures");');
+    $this->assertSession()->responseNotContains('"allow_ad_personalization_signals":false');
 
-    // Test if track display features is disabled.
-    $this->config('google_analytics.settings')->set('track.displayfeatures', 0)->save();
+    // Test if tracking of url fragments is enabled.
+    $this->config('google_analytics.settings')->set('track.urlfragments', 1)->save();
+    $this->drupalGet('');
+    $this->assertSession()->responseContains('"page_path":location.pathname + location.search + location.hash');
+
+    // Test if tracking of url fragments is disabled.
+    $this->config('google_analytics.settings')->set('track.urlfragments', 0)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseNotContains('ga("require", "displayfeatures");');
+    $this->assertSession()->responseNotContains('"page_path":location.pathname + location.search + location.hash');
 
     // Test whether single domain tracking is active.
     $this->drupalGet('');
-    $this->assertSession()->responseContains('{"cookieDomain":"auto"}');
+    $this->assertSession()->responseContains('"groups":"default"');
 
     // Enable "One domain with multiple subdomains".
     $this->config('google_analytics.settings')->set('domain_mode', 1)->save();
@@ -273,11 +268,11 @@ public function testGoogleAnalyticsTrackingCode() {
     // reliable.
     global $cookie_domain;
     if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
-      $this->assertSession()->responseContains('{"cookieDomain":"' . $cookie_domain . '"}');
+      $this->assertSession()->responseContains('"cookie_domain":"' . $cookie_domain . '"');
     }
     else {
       // Special cases, Localhost and IP addresses don't show 'cookieDomain'.
-      $this->assertSession()->responseNotContains('{"cookieDomain":"' . $cookie_domain . '"}');
+      $this->assertSession()->responseNotContains('"cookie_domain":"' . $cookie_domain . '"');
     }
 
     // Enable "Multiple top-level domains" tracking.
@@ -286,9 +281,8 @@ public function testGoogleAnalyticsTrackingCode() {
       ->set('cross_domains', "www.example.com\nwww.example.net")
       ->save();
     $this->drupalGet('');
-    $this->assertSession()->responseContains('ga("create", "' . $ua_code . '", {"cookieDomain":"auto","allowLinker":true');
-    $this->assertSession()->responseContains('ga("require", "linker");');
-    $this->assertSession()->responseContains('ga("linker:autoLink", ["www.example.com","www.example.net"]);');
+    $this->assertSession()->responseContains('"groups":"default","linker":');
+    $this->assertSession()->responseContains('"groups":"default","linker":{"domains":["www.example.com","www.example.net"]}');
     $this->assertSession()->responseContains('"trackDomainMode":2,');
     $this->assertSession()->responseContains('"trackCrossDomains":["www.example.com","www.example.net"]');
     $this->config('google_analytics.settings')->set('domain_mode', 0)->save();
@@ -296,36 +290,36 @@ public function testGoogleAnalyticsTrackingCode() {
     // Test whether debugging script has been enabled.
     $this->config('google_analytics.settings')->set('debug', 1)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseContains('https://www.google-analytics.com/analytics_debug.js');
+    // @FIXME
+    //$this->assertSession()->responseContains('https://www.google-analytics.com/analytics_debug.js');
 
     // Check if text and link is shown on 'Status Reports' page.
     // Requires 'administer site configuration' permission.
     $this->drupalGet('admin/reports/status');
-    $this->assertSession()->responseContains(t('Google Analytics module has debugging enabled. Please disable debugging setting in production sites from the <a href=":url">Google Analytics settings page</a>.', [':url' => Url::fromRoute('google_analytics.admin_settings_form')->toString()]));
+    $this->assertSession()->responseContains($this->t('Google Analytics module has debugging enabled. Please disable debugging setting in production sites from the <a href=":url">Google Analytics settings page</a>.', [':url' => Url::fromRoute('google_analytics.admin_settings_form')->toString()]));
 
     // Test whether debugging script has been disabled.
     $this->config('google_analytics.settings')->set('debug', 0)->save();
     $this->drupalGet('');
-    $this->assertSession()->responseContains('https://www.google-analytics.com/analytics.js');
+    $this->assertSession()->responseContains('https://www.googletagmanager.com/gtag/js?id=');
 
     // Test whether the CREATE and BEFORE and AFTER code is added to the
     // tracking code.
-    $codesnippet_create = [
-      'cookieDomain' => 'foo.example.com',
-      'cookieName' => 'myNewName',
-      'cookieExpires' => 20000,
-      'allowAnchor' => TRUE,
-      'sampleRate' => 4.3,
+    $codesnippet_parameters = [
+      'cookie_domain' => 'foo.example.com',
+      'cookie_name' => 'myNewName',
+      'cookie_expires' => "20000",
+      'sample_rate' => "4.3",
     ];
     $this->config('google_analytics.settings')
-      ->set('codesnippet.create', $codesnippet_create)
-      ->set('codesnippet.before', 'ga("set", "forceSSL", true);')
-      ->set('codesnippet.after', 'ga("create", "UA-123456-3", {"name": "newTracker"});if(1 == 1 && 2 < 3 && 2 > 1){console.log("Google Analytics: Custom condition works.");}ga("newTracker.send", "pageview");')
+      ->set('codesnippet.create', $codesnippet_parameters)
+      ->set('codesnippet.before', 'gtag("set", {"currency":"USD"});')
+      ->set('codesnippet.after', 'gtag("config", "UA-123456-3", {"groups":"default"});if(1 == 1 && 2 < 3 && 2 > 1){console.log("Google Analytics: Custom condition works.");}')
       ->save();
     $this->drupalGet('');
-    $this->assertSession()->responseContains('ga("create", "' . $ua_code . '", {"cookieDomain":"foo.example.com","cookieName":"myNewName","cookieExpires":20000,"allowAnchor":true,"sampleRate":4.3});');
-    $this->assertSession()->responseContains('ga("set", "forceSSL", true);');
-    $this->assertSession()->responseContains('ga("create", "UA-123456-3", {"name": "newTracker"});');
+    $this->assertSession()->responseContains('"groups":"default","cookie_domain":"foo.example.com","cookie_name":"myNewName","cookie_expires":"20000","sample_rate":"4.3"');
+    $this->assertSession()->responseContains('gtag("set", {"currency":"USD"});');
+    $this->assertSession()->responseContains('gtag("config", "UA-123456-3", {"groups":"default"});');
     $this->assertSession()->responseContains('if(1 == 1 && 2 < 3 && 2 > 1){console.log("Google Analytics: Custom condition works.");}');
   }
 
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsCustomDimensionsAndMetricsTest.php b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsCustomDimensionsAndMetricsTest.php
index 143f5c7143..3625a2d2c0 100644
--- a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsCustomDimensionsAndMetricsTest.php
+++ b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsCustomDimensionsAndMetricsTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\google_analytics\Functional;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -14,6 +15,8 @@
  */
 class GoogleAnalyticsCustomDimensionsAndMetricsTest extends BrowserTestBase {
 
+  use StringTranslationTrait;
+
   /**
    * Modules to enable.
    *
@@ -22,10 +25,19 @@ class GoogleAnalyticsCustomDimensionsAndMetricsTest extends BrowserTestBase {
   public static $modules = ['google_analytics', 'token', 'node'];
 
   /**
-   * {@inheritdoc}
+   * Default theme.
+   *
+   * @var string
    */
   protected $defaultTheme = 'stark';
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
   /**
    * {@inheritdoc}
    */
@@ -46,8 +58,8 @@ protected function setUp() {
     ]);
 
     // User to set up google_analytics.
-    $this->admin_user = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($this->admin_user);
+    $this->adminUser = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->adminUser);
   }
 
   /**
@@ -62,88 +74,116 @@ public function testGoogleAnalyticsCustomDimensions() {
 
     // Basic test if the feature works.
     $google_analytics_custom_dimension = [
-      1 => [
-        'index' => 1,
+      'dimension1' => [
+        'type' => 'dimension',
+        'name' => 'bar1',
         'value' => 'Bar 1',
       ],
-      2 => [
-        'index' => 2,
+      'dimension2' => [
+        'type' => 'dimension',
+        'name' => 'bar2',
         'value' => 'Bar 2',
       ],
-      3 => [
-        'index' => 3,
+      'dimension3' => [
+        'type' => 'dimension',
+        'name' => 'bar2',
         'value' => 'Bar 3',
       ],
-      4 => [
-        'index' => 4,
+      'dimension4' => [
+        'type' => 'dimension',
+        'name' => 'bar4',
         'value' => 'Bar 4',
       ],
-      5 => [
-        'index' => 5,
+      'dimension5' => [
+        'type' => 'dimension',
+        'name' => 'bar5',
         'value' => 'Bar 5',
       ],
     ];
-    $this->config('google_analytics.settings')->set('custom.dimension', $google_analytics_custom_dimension)->save();
+    $this->config('google_analytics.settings')->set('custom.parameters', $google_analytics_custom_dimension)->save();
     $this->drupalGet('');
 
-    foreach ($google_analytics_custom_dimension as $dimension) {
-      $this->assertSession()->responseContains('ga("set", ' . Json::encode('dimension' . $dimension['index']) . ', ' . Json::encode($dimension['value']) . ');');
+    $custom_map = [];
+    $custom_vars = [];
+    foreach ($google_analytics_custom_dimension as $index => $dimension) {
+      $custom_map['custom_map'][$index] = $dimension['name'];
+      $custom_vars[$dimension['name']] = $dimension['value'];
     }
+    // Verify the account ID exists in the config.
+    $this->assertSession()->responseContains('gtag("config", ' . Json::encode($ua_code));
+    // Check the dimensions.
+    $this->assertSession()->responseContains('"custom_map":' . Json::encode($custom_map['custom_map']));
+    $this->assertSession()->responseContains('gtag("event", "custom", ' . Json::encode($custom_vars) . ');');
 
     // Test whether tokens are replaced in custom dimension values.
     $site_slogan = $this->randomMachineName(16);
     $this->config('system.site')->set('slogan', $site_slogan)->save();
 
     $google_analytics_custom_dimension = [
-      1 => [
-        'index' => 1,
+      'dimension1' => [
+        'type' => 'dimension',
+        'name' => 'site_slogan',
         'value' => 'Value: [site:slogan]',
       ],
-      2 => [
-        'index' => 2,
+      'dimension2' => [
+        'type' => 'dimension',
+        'name' => 'machine_name',
         'value' => $this->randomMachineName(16),
       ],
-      3 => [
-        'index' => 3,
+      'dimension3' => [
+        'type' => 'dimension',
+        'name' => 'foo3',
         'value' => '',
       ],
       // #2300701: Custom dimensions and custom metrics not outputed on zero
       // value.
-      4 => [
-        'index' => 4,
+      'dimension4' => [
+        'type' => 'dimension',
+        'name' => 'bar4',
         'value' => '0',
       ],
-      5 => [
-        'index' => 5,
+      'dimension5' => [
+        'type' => 'dimension',
+        'name' => 'node_type',
         'value' => '[node:type]',
       ],
       // Test google_analytics_tokens().
-      6 => [
-        'index' => 6,
+      'dimension6' => [
+        'type' => 'dimension',
+        'name' => 'current_user_role_names',
         'value' => '[current-user:role-names]',
       ],
-      7 => [
-        'index' => 7,
+      'dimension7' => [
+        'type' => 'dimension',
+        'name' => 'current_user_role_ids',
         'value' => '[current-user:role-ids]',
       ],
     ];
-    $this->config('google_analytics.settings')->set('custom.dimension', $google_analytics_custom_dimension)->save();
+    $this->config('google_analytics.settings')->set('custom.parameters', $google_analytics_custom_dimension)->save();
     $this->verbose('<pre>' . print_r($google_analytics_custom_dimension, TRUE) . '</pre>');
 
     // Test on frontpage.
     $this->drupalGet('');
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('dimension1') . ', ' . Json::encode("Value: $site_slogan") . ');');
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('dimension2') . ', ' . Json::encode($google_analytics_custom_dimension['2']['value']) . ');');
-    $this->assertSession()->responseNotContains('ga("set", ' . Json::encode('dimension3') . ', ' . Json::encode('') . ');');
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('dimension4') . ', ' . Json::encode('0') . ');');
-    $this->assertSession()->responseNotContains('ga("set", ' . Json::encode('dimension5') . ', ' . Json::encode('article') . ');');
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('dimension6') . ', ' . Json::encode(implode(',', \Drupal::currentUser()->getRoles())) . ');');
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('dimension7') . ', ' . Json::encode(implode(',', array_keys(\Drupal::currentUser()->getRoles()))) . ');');
+    $this->assertSession()->responseContains(Json::encode('dimension1') . ':' . Json::encode($google_analytics_custom_dimension['dimension1']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_dimension['dimension1']['name']) . ':' . Json::encode("Value: $site_slogan"));
+    $this->assertSession()->responseContains(Json::encode('dimension2') . ':' . Json::encode($google_analytics_custom_dimension['dimension2']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_dimension['dimension2']['name']) . ':' . Json::encode($google_analytics_custom_dimension['dimension2']['value']));
+    $this->assertSession()->responseNotContains(Json::encode('dimension3') . ':' . Json::encode($google_analytics_custom_dimension['dimension3']['name']));
+    $this->assertSession()->responseNotContains(Json::encode($google_analytics_custom_dimension['dimension3']['name']) . ':' . Json::encode(''));
+    $this->assertSession()->responseContains(Json::encode('dimension4') . ':' . Json::encode($google_analytics_custom_dimension['dimension4']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_dimension['dimension4']['name']) . ':' . Json::encode('0'));
+    $this->assertSession()->responseNotContains(Json::encode('dimension5') . ':' . Json::encode($google_analytics_custom_dimension['dimension5']['name']));
+    $this->assertSession()->responseNotContains(Json::encode($google_analytics_custom_dimension['dimension5']['name']) . ':' . Json::encode('article'));
+    $this->assertSession()->responseContains(Json::encode('dimension6') . ':' . Json::encode($google_analytics_custom_dimension['dimension6']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_dimension['dimension6']['name']) . ':' . Json::encode(implode(',', \Drupal::currentUser()->getRoles())));
+    $this->assertSession()->responseContains(Json::encode('dimension7') . ':' . Json::encode($google_analytics_custom_dimension['dimension7']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_dimension['dimension7']['name']) . ':' . Json::encode(implode(',', array_keys(\Drupal::currentUser()->getRoles()))));
 
     // Test on a node.
     $this->drupalGet('node/' . $node->id());
-    $this->assertText($node->getTitle());
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('dimension5') . ', ' . Json::encode('article') . ');');
+    $this->assertSession()->pageTextContains($node->getTitle());
+    $this->assertSession()->responseContains(Json::encode('dimension5') . ':' . Json::encode($google_analytics_custom_dimension['dimension5']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_dimension['dimension5']['name']) . ':' . Json::encode('article'));
   }
 
   /**
@@ -155,88 +195,86 @@ public function testGoogleAnalyticsCustomMetrics() {
 
     // Basic test if the feature works.
     $google_analytics_custom_metric = [
-      1 => [
-        'index' => 1,
+      'metric1' => [
+        'type' => 'metric',
+        'name' => 'foo1',
         'value' => '6',
       ],
-      2 => [
-        'index' => 2,
+      'metric2' => [
+        'type' => 'metric',
+        'name' => 'foo2',
         'value' => '8000',
       ],
-      3 => [
-        'index' => 3,
+      'metric3' => [
+        'type' => 'metric',
+        'name' => 'foo3',
         'value' => '7.8654',
       ],
-      4 => [
-        'index' => 4,
+      'metric4' => [
+        'type' => 'metric',
+        'name' => 'foo4',
         'value' => '1123.4',
       ],
-      5 => [
-        'index' => 5,
+      'metric5' => [
+        'type' => 'metric',
+        'name' => 'foo5',
         'value' => '5,67',
       ],
     ];
 
-    $this->config('google_analytics.settings')->set('custom.metric', $google_analytics_custom_metric)->save();
+    $this->config('google_analytics.settings')->set('custom.parameters', $google_analytics_custom_metric)->save();
     $this->drupalGet('');
 
-    foreach ($google_analytics_custom_metric as $metric) {
-      $this->assertSession()->responseContains('ga("set", ' . Json::encode('metric' . $metric['index']) . ', ' . Json::encode((float) $metric['value']) . ');');
+    $custom_map = [];
+    $custom_vars = [];
+    foreach ($google_analytics_custom_metric as $index => $metric) {
+      $custom_map['custom_map'][$index] = $metric['name'];
+      $custom_vars[$metric['name']] = floatval($metric['value']);
     }
 
+    // Verify the account ID exists in the config.
+    $this->assertSession()->responseContains('gtag("config", ' . Json::encode($ua_code));
+    // Check the dimensions.
+    $this->assertSession()->responseContains('"custom_map":' . Json::encode($custom_map['custom_map']));
+    $this->assertSession()->responseContains('gtag("event", "custom", ' . Json::encode($custom_vars) . ');');
+
     // Test whether tokens are replaced in custom metric values.
     $google_analytics_custom_metric = [
-      1 => [
-        'index' => 1,
+      'metric1' => [
+        'type' => 'metric',
+        'name' => 'bar1',
         'value' => '[current-user:roles:count]',
       ],
-      2 => [
-        'index' => 2,
+      'metric2' => [
+        'type' => 'metric',
+        'name' => 'bar2',
         'value' => mt_rand(),
       ],
-      3 => [
-        'index' => 3,
+      'metric3' => [
+        'type' => 'metric',
+        'name' => 'bar3',
         'value' => '',
       ],
       // #2300701: Custom dimensions and custom metrics not outputed on zero
       // value.
-      4 => [
-        'index' => 4,
+      'metric4' => [
+        'type' => 'metric',
+        'name' => 'bar4',
         'value' => '0',
       ],
     ];
-    $this->config('google_analytics.settings')->set('custom.metric', $google_analytics_custom_metric)->save();
-    $this->verbose('<pre>' . print_r($google_analytics_custom_metric, TRUE) . '</pre>');
+    $this->config('google_analytics.settings')->set('custom.parameters', $google_analytics_custom_metric)->save();
+    //dump(print_r($google_analytics_custom_metric, TRUE));
 
     $this->drupalGet('');
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('metric1') . ', ');
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('metric2') . ', ' . Json::encode($google_analytics_custom_metric['2']['value']) . ');');
-    $this->assertSession()->responseNotContains('ga("set", ' . Json::encode('metric3') . ', ' . Json::encode('') . ');');
-    $this->assertSession()->responseContains('ga("set", ' . Json::encode('metric4') . ', ' . Json::encode(0) . ');');
-  }
-
-  /**
-   * Tests if Custom Dimensions token form validation works.
-   */
-  public function testGoogleAnalyticsCustomDimensionsTokenFormValidation() {
-    $ua_code = 'UA-123456-1';
-
-    // Check form validation.
-    $edit['google_analytics_account'] = $ua_code;
-    $edit['google_analytics_custom_dimension[indexes][1][value]'] = '[current-user:name]';
-    $edit['google_analytics_custom_dimension[indexes][2][value]'] = '[current-user:edit-url]';
-    $edit['google_analytics_custom_dimension[indexes][3][value]'] = '[user:name]';
-    $edit['google_analytics_custom_dimension[indexes][4][value]'] = '[term:name]';
-    $edit['google_analytics_custom_dimension[indexes][5][value]'] = '[term:tid]';
-
-    $this->drupalPostForm('admin/config/system/google-analytics', $edit, t('Save configuration'));
-
-    $this->assertSession()->responseContains(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 1]), '@invalid-tokens' => implode(', ', ['[current-user:name]'])]));
-    $this->assertSession()->responseContains(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 2]), '@invalid-tokens' => implode(', ', ['[current-user:edit-url]'])]));
-    $this->assertSession()->responseContains(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 3]), '@invalid-tokens' => implode(', ', ['[user:name]'])]));
-    // BUG #2037595
-    //$this->assertSession()->responseNotContains(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 4]), '@invalid-tokens' => implode(', ', ['[term:name]'])]));
-    //$this->assertSession()->responseNotContains(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 5]), '@invalid-tokens' => implode(', ', ['[term:tid]'])]));
+    $this->assertSession()->responseContains(Json::encode('metric1') . ':' . Json::encode($google_analytics_custom_metric['metric1']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_metric['metric1']['name']) . ':');
+    $this->assertSession()->responseContains(Json::encode('metric2') . ':' . Json::encode($google_analytics_custom_metric['metric2']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_metric['metric2']['name']) . ':' . Json::encode($google_analytics_custom_metric['metric2']['value']));
+    $this->assertSession()->responseNotContains(Json::encode('metric3') . ':' . Json::encode($google_analytics_custom_metric['metric3']['name']));
+    $this->assertSession()->responseNotContains(Json::encode($google_analytics_custom_metric['metric3']['name']) . ':' . Json::encode(''));
+    $this->assertSession()->responseContains(Json::encode('metric4') . ':' . Json::encode($google_analytics_custom_metric['metric4']['name']));
+    $this->assertSession()->responseContains(Json::encode($google_analytics_custom_metric['metric4']['name']) . ':' . Json::encode(0));
   }
 
 }
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsCustomUrls.php b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsCustomUrls.php
index 07faca58df..9d2d3ecb07 100644
--- a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsCustomUrls.php
+++ b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsCustomUrls.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\google_analytics\Functional;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -19,10 +20,19 @@ class GoogleAnalyticsCustomUrls extends BrowserTestBase {
   public static $modules = ['google_analytics'];
 
   /**
-   * {@inheritdoc}
+   * Default theme.
+   *
+   * @var string
    */
   protected $defaultTheme = 'stark';
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
   /**
    * {@inheritdoc}
    */
@@ -37,25 +47,39 @@ protected function setUp() {
     ];
 
     // User to set up google_analytics.
-    $this->admin_user = $this->drupalCreateUser($permissions);
+    $this->adminUser = $this->drupalCreateUser($permissions);
   }
 
   /**
    * Tests if user password page urls are overridden.
    */
-  public function testGoogleAnalyticsUserPasswordPage() {
+  public function testGoogleAnalyticsCustomUrls() {
     $base_path = base_path();
     $ua_code = 'UA-123456-1';
-    $this->config('google_analytics.settings')->set('account', $ua_code)->save();
+    $this->config('google_analytics.settings')
+      ->set('account', $ua_code)
+      ->set('privacy.anonymizeip', 0)
+      ->set('track.displayfeatures', 1)
+      ->save();
 
     $this->drupalGet('user/password', ['query' => ['name' => 'foo']]);
-    $this->assertSession()->responseContains('ga("set", "page", "' . $base_path . 'user/password"');
+    $this->assertSession()->responseContains('gtag("config", ' . Json::encode($ua_code) . ', {"groups":"default","page_path":"' . $base_path . 'user/password"});');
 
     $this->drupalGet('user/password', ['query' => ['name' => 'foo@example.com']]);
-    $this->assertSession()->responseContains('ga("set", "page", "' . $base_path . 'user/password"');
+    $this->assertSession()->responseContains('gtag("config", ' . Json::encode($ua_code) . ', {"groups":"default","page_path":"' . $base_path . 'user/password"});');
 
     $this->drupalGet('user/password');
-    $this->assertSession()->responseNotContains('ga("set", "page",');
+    $this->assertSession()->responseNotContains('"page_path":"' . $base_path . 'user/password"});');
+
+    // Test whether 403 forbidden tracking code is shown if user has no access.
+    $this->drupalGet('admin');
+    $this->assertSession()->statusCodeEquals(403);
+    $this->assertSession()->responseContains($base_path . '403.html');
+
+    // Test whether 404 not found tracking code is shown on non-existent pages.
+    $this->drupalGet($this->randomMachineName(64));
+    $this->assertSession()->statusCodeEquals(404);
+    $this->assertSession()->responseContains($base_path . '404.html');
   }
 
 }
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsRolesTest.php b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsRolesTest.php
index 4202b0f272..803375fdb4 100644
--- a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsRolesTest.php
+++ b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsRolesTest.php
@@ -20,10 +20,19 @@ class GoogleAnalyticsRolesTest extends BrowserTestBase {
   public static $modules = ['google_analytics'];
 
   /**
-   * {@inheritdoc}
+   * Default theme.
+   *
+   * @var string
    */
   protected $defaultTheme = 'stark';
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
   /**
    * {@inheritdoc}
    */
@@ -36,7 +45,7 @@ protected function setUp() {
     ];
 
     // User to set up google_analytics.
-    $this->admin_user = $this->drupalCreateUser($permissions);
+    $this->adminUser = $this->drupalCreateUser($permissions);
   }
 
   /**
@@ -59,7 +68,7 @@ public function testGoogleAnalyticsRolesTracking() {
     $this->assertSession()->statusCodeEquals(403);
     $this->assertSession()->responseContains('/403.html');
 
-    $this->drupalLogin($this->admin_user);
+    $this->drupalLogin($this->adminUser);
 
     $this->drupalGet('');
     $this->assertSession()->responseContains($ua_code);
@@ -89,7 +98,7 @@ public function testGoogleAnalyticsRolesTracking() {
     $this->assertSession()->statusCodeEquals(403);
     $this->assertSession()->responseContains('/403.html');
 
-    $this->drupalLogin($this->admin_user);
+    $this->drupalLogin($this->adminUser);
 
     $this->drupalGet('');
     $this->assertSession()->responseContains($ua_code);
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsSearchTest.php b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsSearchTest.php
index de4542d3e2..9d80cfbe2d 100644
--- a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsSearchTest.php
+++ b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsSearchTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\google_analytics\Functional;
 
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\search\SearchIndexInterface;
 use Drupal\Tests\BrowserTestBase;
 
@@ -12,6 +14,8 @@
  */
 class GoogleAnalyticsSearchTest extends BrowserTestBase {
 
+  use StringTranslationTrait;
+  
   /**
    * Modules to enable.
    *
@@ -20,7 +24,16 @@ class GoogleAnalyticsSearchTest extends BrowserTestBase {
   public static $modules = ['google_analytics', 'search', 'node'];
 
   /**
-   * {@inheritdoc}
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
+  /**
+   * Default theme.
+   *
+   * @var string
    */
   protected $defaultTheme = 'stark';
 
@@ -41,8 +54,8 @@ protected function setUp() {
     ];
 
     // User to set up google_analytics.
-    $this->admin_user = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($this->admin_user);
+    $this->adminUser = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->adminUser);
   }
 
   /**
@@ -50,14 +63,18 @@ protected function setUp() {
    */
   public function testGoogleAnalyticsSearchTracking() {
     $ua_code = 'UA-123456-1';
-    $this->config('google_analytics.settings')->set('account', $ua_code)->save();
+    $this->config('google_analytics.settings')
+      ->set('account', $ua_code)
+      ->set('privacy.anonymizeip', 0)
+      ->set('track.displayfeatures', 1)
+      ->save();
 
     // Check tracking code visibility.
     $this->drupalGet('');
-    $this->assertRaw($ua_code);
+    $this->assertSession()->responseContains($ua_code);
 
     $this->drupalGet('search/node');
-    $this->assertNoRaw('ga("set", "page",');
+    $this->assertSession()->responseNotContains('"page_path":(window.google_analytics_search_results) ?');
 
     // Enable site search support.
     $this->config('google_analytics.settings')->set('track.site_search', 1)->save();
@@ -66,20 +83,27 @@ public function testGoogleAnalyticsSearchTracking() {
     $search = ['keys' => $this->randomMachineName(8)];
 
     // Fire a search, it's expected to get 0 results.
-    $this->drupalPostForm('search/node', $search, t('Search'));
-    $this->assertSession()->responseContains('ga("set", "page", (window.google_analytics_search_results) ?');
+    $this->drupalGet('search/node');
+    $this->submitForm($search, $this->t('Search'));
+    $this->assertSession()->responseContains('"page_path":(window.google_analytics_search_results) ?');
+    // Check GA Site Search query param is 'search' when there are no results.
+    $this->assertSession()->responseMatches('/(.+search=' . urlencode("no-results:{$search['keys']}") . ')/');
     $this->assertSession()->responseContains('window.google_analytics_search_results = 0;');
 
     // Create a node and reindex.
     $this->createNodeAndIndex($search['keys']);
-    $this->drupalPostForm('search/node', $search, t('Search'));
-    $this->assertSession()->responseContains('ga("set", "page", (window.google_analytics_search_results) ?');
+    $this->drupalGet('search/node');
+    $this->submitForm($search, $this->t('Search'));
+    $this->assertSession()->responseContains('"page_path":(window.google_analytics_search_results) ?');
+    // Check the GA Site Search query param is 'search'.
+    $this->assertSession()->responseMatches('/(.+search=' . urlencode($search['keys']) . ')/');
     $this->assertSession()->responseContains('window.google_analytics_search_results = 1;');
 
     // Create a second node with same values and reindex.
     $this->createNodeAndIndex($search['keys']);
-    $this->drupalPostForm('search/node', $search, t('Search'));
-    $this->assertSession()->responseContains('ga("set", "page", (window.google_analytics_search_results) ?');
+    $this->drupalGet('search/node');
+    $this->submitForm($search, $this->t('Search'));
+    $this->assertSession()->responseContains('"page_path":(window.google_analytics_search_results) ?');
     $this->assertSession()->responseContains('window.google_analytics_search_results = 2;');
   }
 
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsStatusMessagesTest.php b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsStatusMessagesTest.php
index e1ac0cfd05..c00903783d 100644
--- a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsStatusMessagesTest.php
+++ b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsStatusMessagesTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\google_analytics\Functional;
 
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -11,6 +12,8 @@
  */
 class GoogleAnalyticsStatusMessagesTest extends BrowserTestBase {
 
+  use StringTranslationTrait;
+
   /**
    * Modules to enable.
    *
@@ -19,10 +22,19 @@ class GoogleAnalyticsStatusMessagesTest extends BrowserTestBase {
   public static $modules = ['google_analytics', 'google_analytics_test'];
 
   /**
-   * {@inheritdoc}
+   * Default theme.
+   *
+   * @var string
    */
   protected $defaultTheme = 'stark';
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
   /**
    * {@inheritdoc}
    */
@@ -35,29 +47,33 @@ protected function setUp() {
     ];
 
     // User to set up google_analytics.
-    $this->admin_user = $this->drupalCreateUser($permissions);
+    $this->adminUser = $this->drupalCreateUser($permissions);
   }
 
   /**
    * Tests if status messages tracking is properly added to the page.
+   *
+   * This is a legacy test for Universal Analytics tests.
    */
-  public function testGoogleAnalyticsStatusMessages() {
+  public function testGoogleAnalyticsUAStatusMessages() {
     $ua_code = 'UA-123456-4';
     $this->config('google_analytics.settings')->set('account', $ua_code)->save();
 
     // Enable logging of errors only.
     $this->config('google_analytics.settings')->set('track.messages', ['error' => 'error'])->save();
 
-    $this->drupalPostForm('user/login', [], t('Log in'));
-    $this->assertSession()->responseContains('ga("send", "event", "Messages", "Error message", "Username field is required.");');
-    $this->assertSession()->responseContains('ga("send", "event", "Messages", "Error message", "Password field is required.");');
+    $this->drupalGet('user/login');
+    $this->submitForm([], $this->t('Log in'));
+    // Username field isn't showing up anymore. Comment out for now.
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"event_category":"Messages","event_label":"Username field is required."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"event_category":"Messages","event_label":"Password field is required."});');
 
     // Testing this drupal_set_message() requires an extra test module.
     $this->drupalGet('google-analytics-test/drupal-messenger-add-message');
-    $this->assertSession()->responseNotContains('ga("send", "event", "Messages", "Status message", "Example status message.");');
-    $this->assertSession()->responseNotContains('ga("send", "event", "Messages", "Warning message", "Example warning message.");');
-    $this->assertSession()->responseContains('ga("send", "event", "Messages", "Error message", "Example error message.");');
-    $this->assertSession()->responseContains('ga("send", "event", "Messages", "Error message", "Example error message with html tags and link.");');
+    $this->assertSession()->responseNotContains('gtag("event", "Status message", {"event_category":"Messages","event_label":"Example status message."});');
+    $this->assertSession()->responseNotContains('gtag("event", "Warning message", {"event_category":"Messages","event_label":"Example warning message."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"event_category":"Messages","event_label":"Example error message."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"event_category":"Messages","event_label":"Example error message with html tags and link."});');
 
     // Enable logging of status, warnings and errors.
     $this->config('google_analytics.settings')->set('track.messages', [
@@ -67,10 +83,47 @@ public function testGoogleAnalyticsStatusMessages() {
     ])->save();
 
     $this->drupalGet('google-analytics-test/drupal-messenger-add-message');
-    $this->assertSession()->responseContains('ga("send", "event", "Messages", "Status message", "Example status message.");');
-    $this->assertSession()->responseContains('ga("send", "event", "Messages", "Warning message", "Example warning message.");');
-    $this->assertSession()->responseContains('ga("send", "event", "Messages", "Error message", "Example error message.");');
-    $this->assertSession()->responseContains('ga("send", "event", "Messages", "Error message", "Example error message with html tags and link.");');
+    $this->assertSession()->responseContains('gtag("event", "Status message", {"event_category":"Messages","event_label":"Example status message."});');
+    $this->assertSession()->responseContains('gtag("event", "Warning message", {"event_category":"Messages","event_label":"Example warning message."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"event_category":"Messages","event_label":"Example error message."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"event_category":"Messages","event_label":"Example error message with html tags and link."});');
   }
 
+  /**
+   * Tests if status messages tracking is properly added to the page.
+   *
+   * This test uses gtag 4.0 which uses a different event system.
+   */
+  public function testGoogleAnalyticsGA4StatusMessages() {
+    $ua_code = 'G-123456ABCD';
+    $this->config('google_analytics.settings')->set('account', $ua_code)->save();
+
+    // Enable logging of errors only.
+    $this->config('google_analytics.settings')->set('track.messages', ['error' => 'error'])->save();
+
+    $this->drupalGet('user/login');
+    $this->submitForm([], $this->t('Log in'));
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"value":"Username field is required."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"value":"Password field is required."});');
+
+    // Testing this drupal_set_message() requires an extra test module.
+    $this->drupalGet('google-analytics-test/drupal-messenger-add-message');
+    $this->assertSession()->responseNotContains('gtag("event", "Status message", {"value":"Example status message."});');
+    $this->assertSession()->responseNotContains('gtag("event", "Warning message", {"value":"Example warning message."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"value":"Example error message."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"value":"Example error message with html tags and link."});');
+
+    // Enable logging of status, warnings and errors.
+    $this->config('google_analytics.settings')->set('track.messages', [
+      'status' => 'status',
+      'warning' => 'warning',
+      'error' => 'error',
+    ])->save();
+
+    $this->drupalGet('google-analytics-test/drupal-messenger-add-message');
+    $this->assertSession()->responseContains('gtag("event", "Status message", {"value":"Example status message."});');
+    $this->assertSession()->responseContains('gtag("event", "Warning message", {"value":"Example warning message."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"value":"Example error message."});');
+    $this->assertSession()->responseContains('gtag("event", "Error message", {"value":"Example error message with html tags and link."});');
+  }
 }
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsUninstallTest.php b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsUninstallTest.php
index 47e34e1b6c..0e25299cc0 100644
--- a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsUninstallTest.php
+++ b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsUninstallTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\google_analytics\Functional;
 
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -11,6 +12,8 @@
  */
 class GoogleAnalyticsUninstallTest extends BrowserTestBase {
 
+  use StringTranslationTrait;
+
   /**
    * Modules to enable.
    *
@@ -19,10 +22,19 @@ class GoogleAnalyticsUninstallTest extends BrowserTestBase {
   public static $modules = ['google_analytics'];
 
   /**
-   * {@inheritdoc}
+   * Default theme.
+   *
+   * @var string
    */
   protected $defaultTheme = 'stark';
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
   /**
    * {@inheritdoc}
    */
@@ -36,8 +48,8 @@ protected function setUp() {
     ];
 
     // User to set up google_analytics.
-    $this->admin_user = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($this->admin_user);
+    $this->adminUser = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->adminUser);
   }
 
   /**
@@ -50,27 +62,29 @@ public function testGoogleAnalyticsUninstall() {
     // Show tracker in pages.
     $this->config('google_analytics.settings')->set('account', $ua_code)->save();
 
-    // Enable local caching of analytics.js.
+    // Enable local caching of gtag.js.
     $this->config('google_analytics.settings')->set('cache', 1)->save();
 
-    // Load page to get the analytics.js downloaded into local cache.
+    // Load page to get the gtag.js downloaded into local cache.
     $this->drupalGet('');
 
-    // Test if the directory and analytics.js exists.
-    $this->assertDirectoryExists($cache_path, 'Cache directory "public://google_analytics" has been found.');
-    $this->assertFileExists($cache_path . '/analytics.js', 'Cached analytics.js tracking file has been found.');
-    $this->assertFileExists($cache_path . '/analytics.js.gz', 'Cached analytics.js.gz tracking file has been found.');
+    $file_system = \Drupal::service('file_system');
+    // Test if the directory and gtag.js exists.
+    $this->assertTrue($file_system->prepareDirectory($cache_path), 'Cache directory "public://google_analytics" has been found.');
+    $this->assertTrue(file_exists($cache_path . '/gtag.js'), 'Cached analytics.js tracking file has been found.');
+    $this->assertTrue(file_exists($cache_path . '/gtag.js.gz'), 'Cached analytics.js.gz tracking file has been found.');
 
     // Uninstall the module.
     $edit = [];
     $edit['uninstall[google_analytics]'] = TRUE;
-    $this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
+    $this->drupalGet('admin/modules/uninstall');
+    $this->submitForm($edit, $this->t('Uninstall'));
     $this->assertSession()->pageTextNotContains(\Drupal::translation()->translate('Configuration deletions'));
-    $this->drupalPostForm(NULL, NULL, t('Uninstall'));
-    $this->assertSession()->pageTextContains(t('The selected modules have been uninstalled.'));
+    $this->submitForm([], $this->t('Uninstall'));
+    $this->assertSession()->pageTextContains($this->t('The selected modules have been uninstalled.'));
 
     // Test if the directory and all files have been removed.
-    $this->assertDirectoryNotExists($cache_path);
+    $this->assertFalse($file_system->prepareDirectory($cache_path), 'Cache directory "public://google_analytics" has been removed.');
   }
 
 }
diff --git a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsUserFieldsTest.php b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsUserFieldsTest.php
index 1ab12846d5..2c14661dbb 100644
--- a/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsUserFieldsTest.php
+++ b/web/modules/google_analytics/tests/src/Functional/GoogleAnalyticsUserFieldsTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\google_analytics\Functional;
 
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 
 /**
  * Test user fields functionality of Google Analytics module.
@@ -11,6 +12,8 @@
  */
 class GoogleAnalyticsUserFieldsTest extends BrowserTestBase {
 
+  use StringTranslationTrait;
+
   /**
    * Modules to enable.
    *
@@ -19,10 +22,19 @@ class GoogleAnalyticsUserFieldsTest extends BrowserTestBase {
   public static $modules = ['google_analytics', 'field_ui'];
 
   /**
-   * {@inheritdoc}
+   * Default theme.
+   *
+   * @var string
    */
   protected $defaultTheme = 'stark';
 
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
   /**
    * {@inheritdoc}
    */
@@ -36,8 +48,8 @@ protected function setUp() {
     ];
 
     // User to set up google_analytics.
-    $this->admin_user = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($this->admin_user);
+    $this->adminUser = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->adminUser);
   }
 
   /**
@@ -50,27 +62,27 @@ public function testGoogleAnalyticsUserFields() {
     // Check if the pseudo field is shown on account forms.
     $this->drupalGet('admin/config/people/accounts/form-display');
     $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->responseContains(t('Google Analytics settings'));
+    $this->assertSession()->responseContains($this->t('Google Analytics settings'));
 
     // No customization allowed.
     $this->config('google_analytics.settings')->set('visibility.user_account_mode', 0)->save();
-    $this->drupalGet('user/' . $this->admin_user->id() . '/edit');
+    $this->drupalGet('user/' . $this->adminUser->id() . '/edit');
     $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->responseNotContains(t('Google Analytics settings'));
+    $this->assertSession()->responseNotContains($this->t('Google Analytics settings'));
 
     // Tracking on by default, users with opt-in or out of tracking permission
     // can opt out.
     $this->config('google_analytics.settings')->set('visibility.user_account_mode', 1)->save();
-    $this->drupalGet('user/' . $this->admin_user->id() . '/edit');
+    $this->drupalGet('user/' . $this->adminUser->id() . '/edit');
     $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->responseContains(t('Users are tracked by default, but you are able to opt out.'));
+    $this->assertSession()->responseContains($this->t('Users are tracked by default, but you are able to opt out.'));
 
     // Tracking off by default, users with opt-in or out of tracking permission
     // can opt in.
     $this->config('google_analytics.settings')->set('visibility.user_account_mode', 2)->save();
-    $this->drupalGet('user/' . $this->admin_user->id() . '/edit');
+    $this->drupalGet('user/' . $this->adminUser->id() . '/edit');
     $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->responseContains(t('Users are <em>not</em> tracked by default, but you are able to opt in.'));
+    $this->assertSession()->responseContains($this->t('Users are <em>not</em> tracked by default, but you are able to opt in.'));
   }
 
 }
diff --git a/web/modules/google_analytics/tests/src/FunctionalJavascript/GoogleAnalyticsFormValidationTest.php b/web/modules/google_analytics/tests/src/FunctionalJavascript/GoogleAnalyticsFormValidationTest.php
new file mode 100644
index 0000000000..5765257ba8
--- /dev/null
+++ b/web/modules/google_analytics/tests/src/FunctionalJavascript/GoogleAnalyticsFormValidationTest.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\Tests\google_analytics\FunctionalJavascript;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests add more behavior for a multiple value field.
+ *
+ * @group google_analytics
+ */
+class GoogleAnalyticsFormValidationTest extends WebDriverTestBase {
+
+  use StringTranslationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['google_analytics', 'token', 'node'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Admin user.
+   *
+   * @var \Drupal\user\Entity\User|bool
+   */
+  protected $adminUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $permissions = [
+      'access administration pages',
+      'administer google analytics',
+      'administer nodes',
+      'create article content',
+    ];
+
+    // Create node type.
+    $this->drupalCreateContentType([
+      'type' => 'article',
+      'name' => 'Article',
+    ]);
+
+    // User to set up google_analytics.
+    $this->adminUser = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->adminUser);
+  }
+
+  /**
+   * Tests if Custom Dimensions token form validation works.
+   */
+  public function testGoogleAnalyticsCustomDimensionsTokenFormValidation() {
+    $this->drupalGet('admin/config/services/google-analytics');
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    // Set the UA Code
+    $user_name = $assert_session->waitForField('accounts[0][value]');
+    $account_field = $page->findField('accounts[0][value]');
+    $account_field->setValue('UA-123456-1');
+
+    $dms = $assert_session->waitForLink('Dimensions and Metrics');
+    $page->clickLink('Dimensions and Metrics');
+
+    // First set a value on the first input field.
+    $field_0_name = $page->findField('custom_parameters[0][name]');
+    $field_0_name->setValue('current_user_name');
+    $field_0_value = $page->findField('custom_parameters[0][value]');
+    $field_0_value->setValue('[current-user:name]');
+
+    // Validate the value of the first field exists.
+    $this->assertEquals('current_user_name', $field_0_name->getValue(), 'Name for the first item has not changed.');
+    $this->assertEquals('[current-user:name]', $field_0_value->getValue(), 'Value for the first item has not changed.');
+
+    /** TODO: Fix tests in Issue #3243622
+    $add_more_button = $page->findButton('Add another Parameter');
+    // Add another item
+    $add_more_button->click();
+    $field_1 = $assert_session->waitForField('custom_parameters[1][name]');
+    $this->assertNotEmpty($field_1, 'Successfully added another item.');
+
+    // Validate the value of the first field has not changed.
+    $this->assertEquals('current_user_name', $field_0_name->getValue(), 'Name for the first item has not changed.');
+    $this->assertEquals('[current-user:name]', $field_0_value->getValue(), 'Value for the first item has not changed.');
+
+    // Validate the value of the second item is empty.
+    $this->assertEmpty($field_1->getValue(), 'Value for the second item is currently empty.');
+
+    $field_1_name = $page->findField('custom_parameters[1][name]');
+    $field_1_name->setValue('current_user_edit_url');
+    $field_1_value = $page->findField('custom_parameters[1][value]');
+    $field_1_value->setValue('[current-user:edit-url]');
+
+    // Add third item
+    $add_more_button->click();
+    $field_2 = $assert_session->waitForField('custom_parameters[2][name]');
+    $this->assertNotEmpty($field_2, 'Successfully added another item.');
+
+    $field_2_name = $page->findField('custom_parameters[2][name]');
+    $field_2_name->setValue('user_name');
+    $field_2_value = $page->findField('custom_parameters[2][value]');
+    $field_2_value->setValue('[user:name]');
+
+    // Add forth item
+    $add_more_button->click();
+    $field_3 = $assert_session->waitForField('custom_parameters[3][name]');
+    $this->assertNotEmpty($field_3, 'Successfully added another item.');
+
+    $field_3_name = $page->findField('custom_parameters[3][name]');
+    $field_3_name->setValue('term_name');
+    $field_3_value = $page->findField('custom_parameters[3][value]');
+    $field_3_value->setValue('[term:name]');
+
+    // Add fifth item
+    $add_more_button->click();
+    $field_4 = $assert_session->waitForField('custom_parameters[4][name]');
+    $this->assertNotEmpty($field_4, 'Successfully added another item.');
+
+    $field_4_name = $page->findField('custom_parameters[4][name]');
+    $field_4_name->setValue('term_tid');
+    $field_4_value = $page->findField('custom_parameters[4][value]');
+    $field_4_value->setValue('[term:tid]');
+
+    $page->pressButton('op');
+
+    // Check form validation.
+    $this->assertSession()->responseContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => $this->t('Custom dimension value #@index', ['@index' => 0]), '@invalid-tokens' => implode(', ', ['[current-user:name]'])]));
+    $this->assertSession()->responseContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => $this->t('Custom dimension value #@index', ['@index' => 1]), '@invalid-tokens' => implode(', ', ['[current-user:edit-url]'])]));
+    $this->assertSession()->responseContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => $this->t('Custom dimension value #@index', ['@index' => 2]), '@invalid-tokens' => implode(', ', ['[user:name]'])]));
+    // BUG #2037595
+    //$this->assertSession()->responseNotContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 4]), '@invalid-tokens' => implode(', ', ['[term:name]'])]));
+    //$this->assertSession()->responseNotContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 5]), '@invalid-tokens' => implode(', ', ['[term:tid]'])]));
+  */
+  }
+
+}
diff --git a/web/modules/google_analytics/tests/src/Kernel/Form/GoogleAnalyticsAdminSettingsFormTest.php b/web/modules/google_analytics/tests/src/Kernel/Form/GoogleAnalyticsAdminSettingsFormTest.php
new file mode 100644
index 0000000000..57a835d953
--- /dev/null
+++ b/web/modules/google_analytics/tests/src/Kernel/Form/GoogleAnalyticsAdminSettingsFormTest.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\Tests\google_analytics\Kernel\Form;
+
+use Drupal\Core\Form\FormInterface;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\google_analytics\Form\GoogleAnalyticsAdminSettingsForm;
+
+/**
+ * Tests the google_analytics settings form.
+ *
+ * @group google_analytics
+ */
+class GoogleAnalyticsAdminSettingsFormTest extends KernelTestBase {
+
+  /**
+   * The google_analytics form object under test.
+   *
+   * @var \Drupal\google_analytics\Form\GoogleAnalyticsAdminSettingsForm
+   */
+  protected $googleAnalyticsSettingsForm;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'system',
+    'path_alias',
+    'user',
+    'google_analytics',
+  ];
+
+  /**
+   * {@inheritdoc}
+   *
+   * @covers ::__construct
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installConfig(static::$modules);
+    $this->googleAnalyticsSettingsForm = new GoogleAnalyticsAdminSettingsForm(
+      $this->container->get('config.factory'),
+      $this->container->get('current_user'),
+      $this->container->get('module_handler'),
+      $this->container->get('google_analytics.accounts'),
+      $this->container->get('google_analytics.javascript_cache')
+    );
+  }
+
+  /**
+   * Tests for \Drupal\google_analytics\Form\GoogleAnalyticsAdminSettingsForm.
+   */
+  public function testGoogleAnalyticsAdminSettingsForm() {
+    $this->assertInstanceOf(FormInterface::class, $this->googleAnalyticsSettingsForm);
+
+    $this->assertEquals('google_analytics_admin_settings', $this->googleAnalyticsSettingsForm->getFormId());
+
+    $method = new \ReflectionMethod(GoogleAnalyticsAdminSettingsForm::class, 'getEditableConfigNames');
+    $method->setAccessible(TRUE);
+
+    $name = $method->invoke($this->googleAnalyticsSettingsForm);
+    $this->assertEquals(['google_analytics.settings'], $name);
+  }
+
+}
-- 
GitLab