From 80450514ae4b7ae03e53018f6a9d3dc4d19a4487 Mon Sep 17 00:00:00 2001
From: Brian Canini <canini.16@osu.edu>
Date: Tue, 18 Feb 2020 11:47:51 -0500
Subject: [PATCH] Updating drupal/google_analytics (2.2.0 => 3.0.0)

---
 composer.json                                 |   2 +-
 composer.lock                                 |  22 +-
 vendor/composer/installed.json                |  22 +-
 web/modules/google_analytics/.eslintrc        |   2 +-
 web/modules/google_analytics/README.txt       |  14 +-
 web/modules/google_analytics/composer.json    |   6 +-
 .../install/google_analytics.settings.yml     |   2 +-
 .../config/schema/google_analytics.schema.yml |  10 +-
 .../google_analytics.info.yml                 |   8 +-
 .../google_analytics/google_analytics.install |  40 ++++
 .../google_analytics/google_analytics.module  | 175 +++++++-------
 .../js/google_analytics.admin.js              |   4 +-
 .../js/google_analytics.debug.js              |  52 ++---
 .../google_analytics/js/google_analytics.js   |  52 ++---
 .../d6_google_analytics_settings.yml          |   2 +
 .../d6_google_analytics_user_settings.yml     |   8 +-
 .../d7_google_analytics_settings.yml          |   2 +
 .../d7_google_analytics_user_settings.yml     |   8 +-
 .../Form/GoogleAnalyticsAdminSettingsForm.php | 216 +++++++++++-------
 .../src/GoogleAnalitycsInterface.php          |  15 ++
 .../GoogleAnalyticsSkipRowIfNotSet.php        |  30 ---
 .../GoogleAnalyticsVisibilityPages.php        |   2 +
 .../GoogleAnalyticsVisibilityRoles.php        |   2 +
 .../src/Tests/GoogleAnalyticsBasicTest.php    | 130 +++++------
 ...nalyticsCustomDimensionsAndMetricsTest.php |  76 ++++--
 .../src/Tests/GoogleAnalyticsCustomUrls.php   |  26 ++-
 .../Tests/GoogleAnalyticsJavaScriptTest.js    |  14 +-
 .../Tests/GoogleAnalyticsPhpFilterTest.php    |   6 +-
 .../src/Tests/GoogleAnalyticsSearchTest.php   |  16 +-
 .../GoogleAnalyticsStatusMessagesTest.php     |  30 +--
 .../Tests/GoogleAnalyticsUninstallTest.php    |  10 +-
 .../google_analytics_test.info.yml            |   6 +-
 .../google_analytics_test.routing.yml         |   8 +-
 .../GoogleAnalyticsTestController.php         |  14 +-
 34 files changed, 592 insertions(+), 440 deletions(-)
 rename web/modules/google_analytics/{migration_templates => migrations}/d6_google_analytics_settings.yml (98%)
 rename web/modules/google_analytics/{migration_templates => migrations}/d6_google_analytics_user_settings.yml (71%)
 rename web/modules/google_analytics/{migration_templates => migrations}/d7_google_analytics_settings.yml (98%)
 rename web/modules/google_analytics/{migration_templates => migrations}/d7_google_analytics_user_settings.yml (71%)
 create mode 100644 web/modules/google_analytics/src/GoogleAnalitycsInterface.php
 delete mode 100644 web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsSkipRowIfNotSet.php

diff --git a/composer.json b/composer.json
index e48d28bf44..5d6d84cec4 100644
--- a/composer.json
+++ b/composer.json
@@ -128,7 +128,7 @@
         "drupal/file_browser": "1.1",
         "drupal/focal_point": "1.0-beta6",
         "drupal/geolocation": "1.10",
-        "drupal/google_analytics": "2.2",
+        "drupal/google_analytics": "3.0",
         "drupal/google_tag": "^1.1",
         "drupal/honeypot": "^1.28",
         "drupal/image_popup": "1.1",
diff --git a/composer.lock b/composer.lock
index 112fa313e6..e7f3237073 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": "860bbfe67c7dcc55be713de1dfd94b59",
+    "content-hash": "9c3077b4cca6c29f375aa02405c58ae2",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -5056,20 +5056,20 @@
         },
         {
             "name": "drupal/google_analytics",
-            "version": "2.2.0",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/google_analytics.git",
-                "reference": "8.x-2.2"
+                "reference": "8.x-3.0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/google_analytics-8.x-2.2.zip",
-                "reference": "8.x-2.2",
-                "shasum": "15cd0642ad99f3776a46b702e5012713405a0733"
+                "url": "https://ftp.drupal.org/files/projects/google_analytics-8.x-3.0.zip",
+                "reference": "8.x-3.0",
+                "shasum": "18179854152cae44be2a5ed5de5550bc6e131c70"
             },
             "require": {
-                "drupal/core": "~8.0"
+                "drupal/core": "~8.5"
             },
             "require-dev": {
                 "drupal/php": "*",
@@ -5078,11 +5078,11 @@
             "type": "drupal-module",
             "extra": {
                 "branch-alias": {
-                    "dev-2.x": "2.x-dev"
+                    "dev-3.x": "3.x-dev"
                 },
                 "drupal": {
-                    "version": "8.x-2.2",
-                    "datestamp": "1506372845",
+                    "version": "8.x-3.0",
+                    "datestamp": "1548968580",
                     "security-coverage": {
                         "status": "covered",
                         "message": "Covered by Drupal's security advisory policy"
@@ -5106,7 +5106,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": "http://git.drupal.org/project/google_analytics.git",
+                "source": "https://git.drupal.org/project/google_analytics.git",
                 "issues": "https://www.drupal.org/project/issues/google_analytics"
             }
         },
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index a8e23e0a60..9747907e8a 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -5209,21 +5209,21 @@
     },
     {
         "name": "drupal/google_analytics",
-        "version": "2.2.0",
-        "version_normalized": "2.2.0.0",
+        "version": "3.0.0",
+        "version_normalized": "3.0.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/google_analytics.git",
-            "reference": "8.x-2.2"
+            "reference": "8.x-3.0"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/google_analytics-8.x-2.2.zip",
-            "reference": "8.x-2.2",
-            "shasum": "15cd0642ad99f3776a46b702e5012713405a0733"
+            "url": "https://ftp.drupal.org/files/projects/google_analytics-8.x-3.0.zip",
+            "reference": "8.x-3.0",
+            "shasum": "18179854152cae44be2a5ed5de5550bc6e131c70"
         },
         "require": {
-            "drupal/core": "~8.0"
+            "drupal/core": "~8.5"
         },
         "require-dev": {
             "drupal/php": "*",
@@ -5232,11 +5232,11 @@
         "type": "drupal-module",
         "extra": {
             "branch-alias": {
-                "dev-2.x": "2.x-dev"
+                "dev-3.x": "3.x-dev"
             },
             "drupal": {
-                "version": "8.x-2.2",
-                "datestamp": "1506372845",
+                "version": "8.x-3.0",
+                "datestamp": "1548968580",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
@@ -5261,7 +5261,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": "http://git.drupal.org/project/google_analytics.git",
+            "source": "https://git.drupal.org/project/google_analytics.git",
             "issues": "https://www.drupal.org/project/issues/google_analytics"
         }
     },
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.txt b/web/modules/google_analytics/README.txt
index d424de51cb..4d8765ef67 100644
--- a/web/modules/google_analytics/README.txt
+++ b/web/modules/google_analytics/README.txt
@@ -1,6 +1,6 @@
 
 Module: Google Analytics
-Author: Alexander Hass <http://drupal.org/user/85918>
+Author: Alexander Hass <https://drupal.org/user/85918>
 
 
 Description
@@ -45,15 +45,15 @@ user with 'Administer Google Analytics' permission.
 Like the blocks visibility settings in Drupal core, there is a choice for
 "Add if the following PHP code returns TRUE." Sample PHP snippets that can be
 used in this textarea can be found on the handbook page "Overview-approach to
-block visibility" at http://drupal.org/node/64135.
+block visibility" at https://drupal.org/node/64135.
 
 Custom dimensions and metrics
 =============================
 One example for custom dimensions tracking is the "User roles" tracking.
 
-1. In the Google Analytics (http://www.google.com/analytics/) Management 
-   Interface you need to setup Dimension #1 with name e.g. "User roles". This
-   step is required. Do not miss it, please.
+1. In the Google Analytics (https://marketingplatform.google.com/about/analytics/)
+   Management Interface you need to setup Dimension #1 with name 
+   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 choose another
@@ -69,7 +69,7 @@ Advanced Settings
 =================
 You can include additional JavaScript snippets in the custom javascript
 code textarea. These can be found on the official Google Analytics pages
-and a few examples at http://drupal.org/node/248699. Support is not
+and a few examples at https://drupal.org/node/248699. Support is not
 provided for any customisations you include.
 
 To speed up page loading you may also cache the Google Analytics "analytics.js"
@@ -89,7 +89,7 @@ Body:
   <li><a href="mailto:foo@example.com">Mailto</a></li>
   <li><a href="/files/test.txt">Download file</a></li>
   <li><a class="colorbox" href="#">Open colorbox</a></li>
-  <li><a href="http://example.com/">External link</a></li>
+  <li><a href="https://example.com/">External link</a></li>
   <li><a href="/go/test">Go link</a></li>
 </ul>
 
diff --git a/web/modules/google_analytics/composer.json b/web/modules/google_analytics/composer.json
index fcb5776898..2204e9a4be 100644
--- a/web/modules/google_analytics/composer.json
+++ b/web/modules/google_analytics/composer.json
@@ -15,8 +15,10 @@
   ],
   "support": {
     "issues": "https://www.drupal.org/project/issues/google_analytics",
-    "source": "http://git.drupal.org/project/google_analytics.git"
+    "source": "https://git.drupal.org/project/google_analytics.git"
   },
   "license": "GPL-2.0+",
-  "require": {}
+  "require": {
+    "drupal/core": "~8.5"  
+  }
 }
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 615cb36032..f14f25d55a 100644
--- a/web/modules/google_analytics/config/install/google_analytics.settings.yml
+++ b/web/modules/google_analytics/config/install/google_analytics.settings.yml
@@ -20,7 +20,7 @@ track:
   messages: {  }
   site_search: false
   adsense: false
-  displayfeatures: false
+  displayfeatures: true
 privacy:
   anonymizeip: true
 custom:
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..dbaa929a74 100644
--- a/web/modules/google_analytics/config/schema/google_analytics.schema.yml
+++ b/web/modules/google_analytics/config/schema/google_analytics.schema.yml
@@ -102,6 +102,9 @@ google_analytics.settings:
               index:
                 type: integer
                 label: Index
+              name:
+                type: string
+                label: Name
               value:
                 type: string
                 label: Value
@@ -115,6 +118,9 @@ google_analytics.settings:
               index:
                 type: integer
                 label: Index
+              name:
+                type: string
+                label: Name
               value:
                 type: string
                 label: Value
@@ -124,10 +130,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 a1d32b1057..a102b60e81 100644
--- a/web/modules/google_analytics/google_analytics.info.yml
+++ b/web/modules/google_analytics/google_analytics.info.yml
@@ -4,12 +4,14 @@ description: 'Allows your site to be tracked by Google Analytics by adding a Jav
 package: Statistics
 # core: 8.x
 configure: google_analytics.admin_settings_form
+dependencies:
+  - drupal:system (>= 8.5)
 test_dependencies:
   - php:php
   - token:token
 
-# Information added by Drupal.org packaging script on 2017-09-25
-version: '8.x-2.2'
+# Information added by Drupal.org packaging script on 2019-01-31
+version: '8.x-3.0'
 core: '8.x'
 project: 'google_analytics'
-datestamp: 1506372866
+datestamp: 1548968586
diff --git a/web/modules/google_analytics/google_analytics.install b/web/modules/google_analytics/google_analytics.install
index 259bf2c196..a780c784ac 100644
--- a/web/modules/google_analytics/google_analytics.install
+++ b/web/modules/google_analytics/google_analytics.install
@@ -6,6 +6,21 @@
  */
 
 use Drupal\Core\Url;
+use Drupal\user\Entity\Role;
+
+/**
+ * Implements hook_install().
+ */
+function google_analytics_install() {
+  // Make the default install more user and GDPR friendly.
+  $role = Role::load('authenticated');
+  $role->grantPermission('opt-in or out of google analytics tracking');
+  $success = $role->save();
+  if ($success) {
+    $messenger = \Drupal::messenger();
+    $messenger->addMessage(t('Module %module granted %permission permission to authenticated users.', ['%module' => 'Google Analytics', '%permission' => t('Opt-in or out of tracking')]), 'status');
+  }
+}
 
 /**
  * Implements hook_uninstall().
@@ -47,3 +62,28 @@ 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.');
+}
diff --git a/web/modules/google_analytics/google_analytics.module b/web/modules/google_analytics/google_analytics.module
index addd4ec27e..6a8b775360 100644
--- a/web/modules/google_analytics/google_analytics.module
+++ b/web/modules/google_analytics/google_analytics.module
@@ -7,7 +7,7 @@
  * Adds the required Javascript to all your Drupal pages to allow tracking by
  * the Google Analytics statistics package.
  *
- * @author: Alexander Hass <http://drupal.org/user/85918>
+ * @author: Alexander Hass <https://drupal.org/user/85918>
  */
 
 use Drupal\Component\Serialization\Json;
@@ -22,17 +22,12 @@
 use GuzzleHttp\Exception\RequestException;
 use Drupal\google_analytics\Component\Render\GoogleAnalyticsJavaScriptSnippet;
 
-/**
- * Define the default file extension list that should be tracked as download.
- */
-define('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');
-
 /**
  * Advertise the supported google analytics api details.
  */
 function google_analytics_api() {
   return [
-    'api' => 'analytics.js',
+    'api' => 'gtag.js',
   ];
 }
 
@@ -52,7 +47,7 @@ function google_analytics_help($route_name, RouteMatchInterface $route_match) {
       return $output;
 
     case 'google_analytics.admin_settings_form':
-      return t('<a href=":ga_url">Google Analytics</a> is a free (registration required) website traffic and marketing effectiveness service.', [':ga_url' => 'http://www.google.com/analytics/']);
+      return t('<a href=":ga_url">Google Analytics</a> is a free (registration required) website traffic and marketing effectiveness service.', [':ga_url' => 'https://marketingplatform.google.com/about/analytics/']);
   }
 }
 
@@ -93,7 +88,7 @@ function google_analytics_page_attachments(array &$page) {
     $url_custom = '';
 
     // Add link tracking.
-    $link_settings = [];
+    $link_settings = ['account' => $id];
     if ($track_outbound = $config->get('track.outbound')) {
       $link_settings['trackOutbound'] = $track_outbound;
     }
@@ -142,12 +137,15 @@ function google_analytics_page_attachments(array &$page) {
         'error' => t('Error message'),
       ];
 
-      foreach (drupal_get_messages(NULL, FALSE) as $type => $messages) {
+      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($message)) . ');';
+            $event = [];
+            $event['event_category'] = t('Messages');
+            $event['event_label'] = strip_tags((string) $message);
+            $message_events .= 'gtag("event", ' . Json::encode($status_heading[$type]) . ', ' . Json::encode($event) . ');';
           }
         }
       }
@@ -176,7 +174,7 @@ function google_analytics_page_attachments(array &$page) {
 
     // Track access denied (403) and file not found (404) pages.
     if ($status == '403') {
-      // See http://www.google.com/support/analytics/bin/answer.py?answer=86927
+      // 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') {
@@ -196,7 +194,8 @@ function google_analytics_page_attachments(array &$page) {
     }
 
     // Add custom dimensions and metrics.
-    $custom_var = '';
+    $custom_map = [];
+    $custom_vars = [];
     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?
@@ -214,7 +213,7 @@ function google_analytics_page_attachments(array &$page) {
           $google_analytics_custom_var['value'] = \Drupal::token()->replace($google_analytics_custom_var['value'], $types, ['clear' => TRUE]);
 
           // Suppress empty values.
-          if (!Unicode::strlen(trim($google_analytics_custom_var['value']))) {
+          if (!Unicode::strlen(trim($google_analytics_custom_var['name'])) || !Unicode::strlen(trim($google_analytics_custom_var['value']))) {
             continue;
           }
 
@@ -231,45 +230,34 @@ function google_analytics_page_attachments(array &$page) {
             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']) . ');';
+          // Build the arrays of values.
+          $custom_map['custom_map'][$google_analytics_custom_type . $google_analytics_custom_var['index']] = $google_analytics_custom_var['name'];
+          $custom_vars[$google_analytics_custom_var['name']] = $google_analytics_custom_var['value'];
         }
       }
     }
 
-    // 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');
+    $custom_var = '';
+    if (!empty($custom_map)) {
+      // Add custom variables to tracker.
+      $custom_var .= 'gtag("config", ' . Json::encode($id) . ', ' . Json::encode($custom_map) . ');';
+      $custom_var .= 'gtag("event", "custom", ' . Json::encode($custom_vars) . ');';
+    };
 
-      $script .= '"' . $url . $query_string . '"';
-    }
-    else {
-      $script .= '"' . $library_tracker_url . '"';
-    }
-    $script .= ',"ga");';
+    // Build tracker code.
+    $script = 'window.dataLayer = window.dataLayer || [];';
+    $script .= 'function gtag(){dataLayer.push(arguments)};';
+    $script .= 'gtag("js", new Date());';
 
     // Add any custom code snippets if specified.
-    $codesnippet_create = $config->get('codesnippet.create');
+    $codesnippet_parameters = $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);
+    // 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;
@@ -280,61 +268,60 @@ function google_analytics_page_attachments(array &$page) {
     // 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]);
+      $arguments = array_merge($arguments, ['cookie_domain' => $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]);
+      // Cross Domain tracking
+      // https://developers.google.com/analytics/devguides/collection/gtagjs/cross-domain
+      $arguments['linker'] = [
+        'domains' => $link_settings['trackCrossDomains'],
+      ];
       $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());
+      $arguments['user_id'] = google_analytics_user_id_hash($account->id());
     }
 
-    // Create a tracker.
-    $script .= 'ga("create", ' . Json::encode($id) . ', ' . Json::encode($create_only_fields) . ');';
+    if ($config->get('privacy.anonymizeip')) {
+      $arguments['anonymize_ip'] = TRUE;
+    }
 
-    // Prepare Adsense tracking.
-    $googleanalytics_adsense_script .= 'window.google_analytics_uacct = ' . Json::encode($id) . ';';
+    if (!empty($url_custom)) {
+      $arguments['page_path'] = 'PLACEHOLDER_URL_CUSTOM';
+    }
 
     // Add enhanced link attribution after 'create', but before 'pageview' send.
-    // @see https://support.google.com/analytics/answer/2558867
+    // @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-link-attribution
     if ($config->get('track.linkid')) {
-      $script .= 'ga("require", "linkid", "linkid.js");';
+      $arguments['link_attribution'] = TRUE;
     }
 
-    // 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");';
+    // Disabling display features.
+    // @see https://developers.google.com/analytics/devguides/collection/gtagjs/display-features
+    if (!$config->get('track.displayfeatures')) {
+      $arguments['allow_ad_personalization_signals'] = FALSE;
     }
 
-    // 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']) . ');';
-    }
+    // Convert array to JSON format.
+    $arguments_json = Json::encode($arguments);
+    // Json::encode() cannot convert every data type properly.
+    $arguments_json = str_replace('"PLACEHOLDER_URL_CUSTOM"', $url_custom, $arguments_json);
 
-    if ($config->get('privacy.anonymizeip')) {
-      $script .= 'ga("set", "anonymizeIp", true);';
+    // Create a tracker.
+    if (!empty($codesnippet_before)) {
+      $script .= $codesnippet_before;
     }
+    $script .= 'gtag("config", ' . Json::encode($id) . ', ' . $arguments_json . ');';
+
+    // Prepare Adsense tracking.
+    $googleanalytics_adsense_script .= 'window.google_analytics_uacct = ' . Json::encode($id) . ';';
 
     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");';
-
     if (!empty($message_events)) {
       $script .= $message_events;
     }
@@ -349,6 +336,33 @@ function google_analytics_page_attachments(array &$page) {
       $script = $googleanalytics_adsense_script . $script;
     }
 
+    // Prepend tracking library directly before script code.
+    if ($debug) {
+      // Debug script has highest priority to load.
+      // @FIXME: Cannot find the debug URL!???
+      $library = 'https://www.googletagmanager.com/gtag/js?id=' . $id;
+    }
+    elseif ($config->get('cache') && _google_analytics_cache('https://www.googletagmanager.com/gtag/js')) {
+      // Should a local cached copy of gtag.js be used?
+      $query_string = '?' . (\Drupal::state()->get('system.css_js_query_string') ?: '0');
+      $library = file_url_transform_relative(file_create_url('public://google_analytics/gtag.js')) . $query_string;
+    }
+    else {
+      // Fallback to default.
+      $library = 'https://www.googletagmanager.com/gtag/js?id=' . $id;
+    }
+
+    $page['#attached']['html_head'][] = [
+      [
+        '#tag' => 'script',
+        '#attributes' => [
+          'async' => TRUE,
+          'src' => $library,
+        ],
+      ],
+      'google_analytics_tracking_file',
+    ];
+
     $page['#attached']['html_head'][] = [
       [
         '#tag' => 'script',
@@ -443,11 +457,12 @@ 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();
 
   // 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);
-    \Drupal::state()->set('google_analytics.last_cache', REQUEST_TIME);
+  if ($request_time - \Drupal::state()->get('google_analytics.last_cache') >= 86400 && $config->get('cache')) {
+    _google_analytics_cache('https://www.googletagmanager.com/gtag/js', TRUE);
+    \Drupal::state()->set('google_analytics.last_cache', $request_time);
   }
 }
 
@@ -490,14 +505,14 @@ function google_analytics_preprocess_item_list__search_results(&$variables) {
  */
 function _google_analytics_cache($location, $synchronize = FALSE) {
   $path = 'public://google_analytics';
-  $file_destination = $path . '/' . basename($location);
+  $file_destination = $path . '/gtag.js';
 
   if (!file_exists($file_destination) || $synchronize) {
     // Download the latest tracking code.
     try {
-      $data = \Drupal::httpClient()
+      $data = (string) \Drupal::httpClient()
         ->get($location)
-        ->getBody(TRUE);
+        ->getBody();
 
       if (file_exists($file_destination)) {
         // Synchronize tracking code and and replace local file if outdated.
diff --git a/web/modules/google_analytics/js/google_analytics.admin.js b/web/modules/google_analytics/js/google_analytics.admin.js
index 8ee9b94cf2..c989f2d678 100644
--- a/web/modules/google_analytics/js/google_analytics.admin.js
+++ b/web/modules/google_analytics/js/google_analytics.admin.js
@@ -3,7 +3,7 @@
  * Google Analytics admin behaviors.
  */
 
-(function ($, window) {
+(function ($) {
 
   'use strict';
 
@@ -141,4 +141,4 @@
     }
   };
 
-})(jQuery, window);
+})(jQuery);
diff --git a/web/modules/google_analytics/js/google_analytics.debug.js b/web/modules/google_analytics/js/google_analytics.debug.js
index d60eca3559..4895836be4 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,20 @@
           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.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 +89,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 +102,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)
           });
         }
       });
@@ -179,8 +171,8 @@
    * Extract the relative internal URL from an absolute internal URL.
    *
    * Examples:
-   * - http://mydomain.com/node/1 -> /node/1
-   * - http://example.com/foo/bar -> http://example.com/foo/bar
+   * - https://mydomain.com/node/1 -> /node/1
+   * - https://example.com/foo/bar -> https://example.com/foo/bar
    *
    * @param {string} url
    *   The web url to check.
diff --git a/web/modules/google_analytics/js/google_analytics.js b/web/modules/google_analytics/js/google_analytics.js
index 0490c2b944..46bb0fc85f 100644
--- a/web/modules/google_analytics/js/google_analytics.js
+++ b/web/modules/google_analytics/js/google_analytics.js
@@ -31,43 +31,37 @@
           // 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.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 +72,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 +84,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)
           });
         }
       });
@@ -161,8 +153,8 @@
    * Extract the relative internal URL from an absolute internal URL.
    *
    * Examples:
-   * - http://mydomain.com/node/1 -> /node/1
-   * - http://example.com/foo/bar -> http://example.com/foo/bar
+   * - https://mydomain.com/node/1 -> /node/1
+   * - https://example.com/foo/bar -> https://example.com/foo/bar
    *
    * @param {string} url
    *   The web url to check.
diff --git a/web/modules/google_analytics/migration_templates/d6_google_analytics_settings.yml b/web/modules/google_analytics/migrations/d6_google_analytics_settings.yml
similarity index 98%
rename from web/modules/google_analytics/migration_templates/d6_google_analytics_settings.yml
rename to web/modules/google_analytics/migrations/d6_google_analytics_settings.yml
index ad2be93ff2..aa71bcbe77 100644
--- a/web/modules/google_analytics/migration_templates/d6_google_analytics_settings.yml
+++ b/web/modules/google_analytics/migrations/d6_google_analytics_settings.yml
@@ -2,6 +2,7 @@ id: d6_google_analytics_settings
 label: Google Analytics 6 configuration
 migration_tags:
   - Drupal 6
+  - Configuration
 source:
   plugin: variable
   variables:
@@ -32,6 +33,7 @@ source:
     - googleanalytics_translation_set
     - googleanalytics_visibility
     - googleanalytics_visibility_roles
+  source_module: googleanalytics
 process:
   account: googleanalytics_account
   cache: googleanalytics_cache
diff --git a/web/modules/google_analytics/migration_templates/d6_google_analytics_user_settings.yml b/web/modules/google_analytics/migrations/d6_google_analytics_user_settings.yml
similarity index 71%
rename from web/modules/google_analytics/migration_templates/d6_google_analytics_user_settings.yml
rename to web/modules/google_analytics/migrations/d6_google_analytics_user_settings.yml
index a6dfa76004..461c19a1f8 100644
--- a/web/modules/google_analytics/migration_templates/d6_google_analytics_user_settings.yml
+++ b/web/modules/google_analytics/migrations/d6_google_analytics_user_settings.yml
@@ -12,11 +12,9 @@ process:
   key: 'constants/key'
   module: 'constants/module'
   settings:
-    # Based on skip_row_if_not_set
-    plugin: google_analytics_skip_row_if_not_set
-    key: custom
-    module: googleanalytics
-    source: data
+    plugin: skip_row_if_not_set
+    index: 'custom'
+    source: data/googleanalytics
 destination:
   plugin: user_data
 migration_dependencies:
diff --git a/web/modules/google_analytics/migration_templates/d7_google_analytics_settings.yml b/web/modules/google_analytics/migrations/d7_google_analytics_settings.yml
similarity index 98%
rename from web/modules/google_analytics/migration_templates/d7_google_analytics_settings.yml
rename to web/modules/google_analytics/migrations/d7_google_analytics_settings.yml
index ab3beece9f..25f48ea5b3 100644
--- a/web/modules/google_analytics/migration_templates/d7_google_analytics_settings.yml
+++ b/web/modules/google_analytics/migrations/d7_google_analytics_settings.yml
@@ -2,6 +2,7 @@ id: d7_google_analytics_settings
 label: Google Analytics 7 configuration
 migration_tags:
   - Drupal 7
+  - Configuration
 source:
   plugin: variable
   variables:
@@ -34,6 +35,7 @@ source:
     - googleanalytics_translation_set
     - googleanalytics_visibility_pages
     - googleanalytics_visibility_roles
+  source_module: googleanalytics    
 process:
   account: googleanalytics_account
   premium: googleanalytics_premium
diff --git a/web/modules/google_analytics/migration_templates/d7_google_analytics_user_settings.yml b/web/modules/google_analytics/migrations/d7_google_analytics_user_settings.yml
similarity index 71%
rename from web/modules/google_analytics/migration_templates/d7_google_analytics_user_settings.yml
rename to web/modules/google_analytics/migrations/d7_google_analytics_user_settings.yml
index 6d4e21719e..a84278d3cc 100644
--- a/web/modules/google_analytics/migration_templates/d7_google_analytics_user_settings.yml
+++ b/web/modules/google_analytics/migrations/d7_google_analytics_user_settings.yml
@@ -12,11 +12,9 @@ process:
   key: 'constants/key'
   module: 'constants/module'
   settings:
-    # Based on skip_row_if_not_set
-    plugin: google_analytics_skip_row_if_not_set
-    key: custom
-    module: googleanalytics
-    source: data
+    plugin: skip_row_if_not_set
+    index: 'custom'
+    source: data/googleanalytics
 destination:
   plugin: user_data
 migration_dependencies:
diff --git a/web/modules/google_analytics/src/Form/GoogleAnalyticsAdminSettingsForm.php b/web/modules/google_analytics/src/Form/GoogleAnalyticsAdminSettingsForm.php
index 197f0275c6..bd39c33b3c 100644
--- a/web/modules/google_analytics/src/Form/GoogleAnalyticsAdminSettingsForm.php
+++ b/web/modules/google_analytics/src/Form/GoogleAnalyticsAdminSettingsForm.php
@@ -3,15 +3,33 @@
 namespace Drupal\google_analytics\Form;
 
 use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandler;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\google_analytics\GoogleAnalitycsInterface;
 
 /**
  * Configure Google_Analytics settings for this site.
  */
 class GoogleAnalyticsAdminSettingsForm extends ConfigFormBase {
 
+  protected $moduleHandler;
+
+  protected $currentUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $currentUser, ModuleHandler $moduleHandler) {
+    parent::__construct($config_factory);
+    $this->currentUser = $currentUser;
+    $this->moduleHandler = $moduleHandler;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -40,7 +58,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
     $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' => 'http://www.google.com/analytics/', ':webpropertyid' => Url::fromUri('https://developers.google.com/analytics/resources/concepts/gaConceptsAccounts', ['fragment' => 'webProperty'])->toString()]),
+      '#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,
@@ -133,7 +151,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
 
     // Page specific visibility configurations.
-    $account = \Drupal::currentUser();
+    $account = $this->currentUser;
     $php_access = $account->hasPermission('use PHP for google analytics tracking visibility');
     $visibility_request_path_pages = $config->get('visibility.request_path_pages');
 
@@ -150,18 +168,18 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     }
     else {
       $options = [
-        t('Every page except the listed pages'),
-        t('The listed pages only'),
+        $this->t('Every page except the listed pages'),
+        $this->t('The listed pages only'),
       ];
-      $description = 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>']);
+      $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 (\Drupal::moduleHandler()->moduleExists('php') && $php_access) {
-        $options[] = t('Pages on which this PHP code returns <code>TRUE</code> (experts only)');
-        $title = t('Pages or PHP code');
-        $description .= ' ' . 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 ?>']);
+      if ($this->moduleHandler->moduleExists('php') && $php_access) {
+        $options[] = $this->t('Pages on which this PHP code returns <code>TRUE</code> (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 = t('Pages');
+        $title = $this->t('Pages');
       }
       $form['tracking']['page_visibility_settings']['google_analytics_visibility_request_path_mode'] = [
         '#type' => 'radios',
@@ -192,8 +210,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#type' => 'radios',
       '#title' => $this->t('Add tracking for specific roles'),
       '#options' => [
-        t('Add to the selected roles only'),
-        t('Add to every role except the selected ones'),
+        $this->t('Add to the selected roles only'),
+        $this->t('Add to every role except the selected ones'),
       ],
       '#default_value' => $config->get('visibility.user_role_mode'),
     ];
@@ -213,14 +231,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Users'),
       '#group' => 'tracking_scope',
     ];
-    $t_permission = ['%permission' => $this->t('opt-in or out of tracking')];
+    $t_permission = ['%permission' => $this->t('Opt-in or out of tracking')];
     $form['tracking']['user_visibility_settings']['google_analytics_visibility_user_account_mode'] = [
       '#type' => 'radios',
       '#title' => $this->t('Allow users to customize tracking on their account page'),
       '#options' => [
-        t('No customization allowed'),
-        t('Tracking on by default, users with %permission permission can opt out', $t_permission),
-        t('Tracking off by default, users with %permission permission can opt in', $t_permission),
+        $this->t('No customization allowed'),
+        $this->t('Tracking on by default, users with %permission permission can opt out', $t_permission),
+        $this->t('Tracking off by default, users with %permission permission can opt in', $t_permission),
       ],
       '#default_value' => !empty($visibility_user_account_mode) ? $visibility_user_account_mode : 0,
     ];
@@ -257,7 +275,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' => 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' => GoogleAnalitycsInterface::GOOGLE_ANALYTICS_TRACKFILES_EXTENSIONS]),
       '#maxlength' => 500,
       '#states' => [
         'enabled' => [
@@ -271,7 +289,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
 
     $colorbox_dependencies = '<div class="admin-requirements">';
-    $colorbox_dependencies .= t('Requires: @module-list', ['@module-list' => (\Drupal::moduleHandler()->moduleExists('colorbox') ? t('@module (<span class="admin-enabled">enabled</span>)', ['@module' => 'Colorbox']) : t('@module (<span class="admin-missing">disabled</span>)', ['@module' => 'Colorbox']))]);
+    $colorbox_dependencies .= $this->t('Requires: @module-list', ['@module-list' => ($this->moduleHandler->moduleExists('colorbox') ? $this->t('@module (<span class="admin-enabled">enabled</span>)', ['@module' => 'Colorbox']) : $this->t('@module (<span class="admin-missing">disabled</span>)', ['@module' => 'Colorbox']))]);
     $colorbox_dependencies .= '</div>';
 
     $form['tracking']['linktracking']['google_analytics_trackcolorbox'] = [
@@ -279,7 +297,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Track content in colorbox modal dialogs'),
       '#description' => $this->t('Enable to track the content shown in colorbox modal windows.') . $colorbox_dependencies,
       '#default_value' => $config->get('track.colorbox'),
-      '#disabled' => (\Drupal::moduleHandler()->moduleExists('colorbox') ? FALSE : TRUE),
+      '#disabled' => ($this->moduleHandler->moduleExists('colorbox') ? FALSE : TRUE),
     ];
 
     $form['tracking']['linktracking']['google_analytics_tracklinkid'] = [
@@ -321,7 +339,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
 
     $site_search_dependencies = '<div class="admin-requirements">';
-    $site_search_dependencies .= t('Requires: @module-list', ['@module-list' => (\Drupal::moduleHandler()->moduleExists('search') ? t('@module (<span class="admin-enabled">enabled</span>)', ['@module' => 'Search']) : t('@module (<span class="admin-missing">disabled</span>)', ['@module' => 'Search']))]);
+    $site_search_dependencies .= $this->t('Requires: @module-list', ['@module-list' => ($this->moduleHandler->moduleExists('search') ? $this->t('@module (<span class="admin-enabled">enabled</span>)', ['@module' => 'Search']) : $this->t('@module (<span class="admin-missing">disabled</span>)', ['@module' => 'Search']))]);
     $site_search_dependencies .= '</div>';
 
     $form['tracking']['search_and_advertising']['google_analytics_site_search'] = [
@@ -329,7 +347,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Track internal search'),
       '#description' => $this->t('If checked, internal search keywords are tracked. You must configure your Google account to use the internal query parameter <strong>search</strong>. For more information see <a href=":url">Setting Up Site Search for a Profile</a>.', [':url' => 'https://support.google.com/analytics/answer/1012264']) . $site_search_dependencies,
       '#default_value' => $config->get('track.site_search'),
-      '#disabled' => (\Drupal::moduleHandler()->moduleExists('search') ? FALSE : TRUE),
+      '#disabled' => ($this->moduleHandler->moduleExists('search') ? FALSE : TRUE),
     ];
     $form['tracking']['search_and_advertising']['google_analytics_trackadsense'] = [
       '#type' => 'checkbox',
@@ -369,6 +387,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#type' => 'table',
       '#header' => [
         ['data' => $this->t('Index')],
+        ['data' => $this->t('Name')],
         ['data' => $this->t('Value')],
       ],
     ];
@@ -388,6 +407,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         '#title_display' => 'invisible',
         '#type' => 'textfield',
       ];
+      $form['google_analytics_custom_dimension']['indexes'][$i]['name'] = [
+        '#default_value' => isset($google_analytics_custom_dimension[$i]['name']) ? $google_analytics_custom_dimension[$i]['name'] : '',
+        '#description' => $this->t('The custom dimension name.'),
+        '#maxlength' => 255,
+        '#title' => $this->t('Custom dimension name #@index', ['@index' => $i]),
+        '#title_display' => 'invisible',
+        '#type' => 'textfield',
+      ];
       $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.'),
@@ -398,16 +425,16 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         '#element_validate' => [[get_class($this), 'tokenElementValidate']],
         '#token_types' => ['node'],
       ];
-      if (\Drupal::moduleHandler()->moduleExists('token')) {
+      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' => 'http://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 (\Drupal::moduleHandler()->moduleExists('token')) {
+    if ($this->moduleHandler->moduleExists('token')) {
       $form['google_analytics_custom_dimension']['google_analytics_token_tree'] = [
         '#theme' => 'token_tree_link',
         '#token_types' => ['node'],
@@ -426,6 +453,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#type' => 'table',
       '#header' => [
         ['data' => $this->t('Index')],
+        ['data' => $this->t('Name')],
         ['data' => $this->t('Value')],
       ],
     ];
@@ -444,6 +472,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         '#title_display' => 'invisible',
         '#type' => 'textfield',
       ];
+      $form['google_analytics_custom_metric']['indexes'][$i]['name'] = [
+        '#default_value' => isset($google_analytics_custom_metric[$i]['name']) ? $google_analytics_custom_metric[$i]['name'] : '',
+        '#description' => $this->t('The custom metric name.'),
+        '#maxlength' => 255,
+        '#title' => $this->t('Custom metric name #@index', ['@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.'),
@@ -454,16 +490,16 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         '#element_validate' => [[get_class($this), 'tokenElementValidate']],
         '#token_types' => ['node'],
       ];
-      if (\Drupal::moduleHandler()->moduleExists('token')) {
+      if ($this->moduleHandler->moduleExists('token')) {
         $form['google_analytics_custom_metric']['indexes'][$i]['value']['#element_validate'][] = 'token_element_validate';
       }
     }
 
     $form['google_analytics_custom_metric']['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' => 'http://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 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']),
     ];
-    if (\Drupal::moduleHandler()->moduleExists('token')) {
+    if ($this->moduleHandler->moduleExists('token')) {
       $form['google_analytics_custom_metric']['google_analytics_token_tree'] = [
         '#theme' => 'token_tree_link',
         '#token_types' => ['node'],
@@ -485,7 +521,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
 
     // Allow for tracking of the originating node when viewing translation sets.
-    if (\Drupal::moduleHandler()->moduleExists('content_translation')) {
+    if ($this->moduleHandler->moduleExists('content_translation')) {
       $form['advanced']['google_analytics_translation_set'] = [
         '#type' => 'checkbox',
         '#title' => $this->t('Track translation sets as one unit'),
@@ -500,15 +536,15 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#type' => 'details',
       '#title' => $this->t('Custom JavaScript code'),
       '#open' => TRUE,
-      '#description' => $this->t('You can add custom Google Analytics <a href=":snippets">code snippets</a> here. These will be added every time tracking is in effect. Before you add your custom code, you should read the <a href=":ga_concepts_overview">Google Analytics Tracking Code - Functional Overview</a> and the <a href=":ga_js_api">Google Analytics Tracking API</a> documentation. <strong>Do not include the &lt;script&gt; tags</strong>, and always end your code with a semicolon (;).', [':snippets' => 'http://drupal.org/node/248699', ':ga_concepts_overview' => 'https://developers.google.com/analytics/resources/concepts/gaConceptsTrackingOverview', ':ga_js_api' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference']),
+      '#description' => $this->t('You can add custom Google Analytics <a href=":snippets">code snippets</a> here. These will be added every time tracking is in effect. Before you add your custom code, you should read the <a href=":ga_concepts_overview">Google Analytics Tracking Code - Functional Overview</a> and the <a href=":ga_js_api">Google Analytics Tracking API</a> documentation. <strong>Do not include the &lt;script&gt; tags</strong>, and always end your code with a semicolon (;).', [':snippets' => 'https://drupal.org/node/248699', ':ga_concepts_overview' => 'https://developers.google.com/analytics/resources/concepts/gaConceptsTrackingOverview', ':ga_js_api' => 'https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference']),
     ];
     $form['advanced']['codesnippet']['google_analytics_codesnippet_create'] = [
       '#type' => 'textarea',
-      '#title' => $this->t('Create only fields'),
+      '#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',
@@ -516,7 +552,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',
@@ -524,7 +560,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'] = [
@@ -575,13 +611,13 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
     $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', t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxxxx-yy.'));
+      $form_state->setErrorByName('google_analytics_account', $this->t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxxxx-yy.'));
     }
 
     // If multiple top-level domains has been selected, a domain names list is
     // required.
     if ($form_state->getValue('google_analytics_domain_mode') == 2 && $form_state->isValueEmpty('google_analytics_cross_domains')) {
-      $form_state->setErrorByName('google_analytics_cross_domains', t('A list of top-level domains is required if <em>Multiple top-level domains</em> has been selected.'));
+      $form_state->setErrorByName('google_analytics_cross_domains', $this->t('A list of top-level domains is required if <em>Multiple top-level domains</em> has been selected.'));
     }
     // Clear the domain list if cross domains are disabled.
     if ($form_state->getValue('google_analytics_domain_mode') != 2) {
@@ -589,12 +625,12 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
     }
 
     // Verify that every path is prefixed with a slash, but don't check PHP
-    // code snippets.
-    if ($form_state->getValue('google_analytics_visibility_request_path_mode') != 2) {
+    // code snippets and do not check for slashes if no paths configured.
+    if ($form_state->getValue('google_analytics_visibility_request_path_mode') != 2 && !empty($form_state->getValue('google_analytics_visibility_request_path_pages'))) {
       $pages = preg_split('/(\r\n?|\n)/', $form_state->getValue('google_analytics_visibility_request_path_pages'));
       foreach ($pages as $page) {
         if (strpos($page, '/') !== 0 && $page !== '<front>') {
-          $form_state->setErrorByName('google_analytics_visibility_request_path_pages', t('Path "@page" not prefixed with slash.', ['@page' => $page]));
+          $form_state->setErrorByName('google_analytics_visibility_request_path_pages', $this->t('Path "@page" not prefixed with slash.', ['@page' => $page]));
           // Drupal forms show one error only.
           break;
         }
@@ -603,7 +639,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
 
     // Disallow empty list of download file extensions.
     if ($form_state->getValue('google_analytics_trackfiles') && $form_state->isValueEmpty('google_analytics_trackfiles_extensions')) {
-      $form_state->setErrorByName('google_analytics_trackfiles_extensions', t('List of download file extensions cannot empty.'));
+      $form_state->setErrorByName('google_analytics_trackfiles_extensions', $this->t('List of download file extensions cannot empty.'));
     }
     // Clear obsolete local cache if cache has been disabled.
     if ($form_state->isValueEmpty('google_analytics_cache') && $form['advanced']['google_analytics_cache']['#default_value']) {
@@ -612,16 +648,16 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
 
     // This is for the Newbie's who cannot read a text area description.
     if (stristr($form_state->getValue('google_analytics_codesnippet_before'), 'google-analytics.com/analytics.js')) {
-      $form_state->setErrorByName('google_analytics_codesnippet_before', t('Do not add the tracker code provided by Google into the javascript code snippets! This module already builds the tracker code based on your Google Analytics account number and settings.'));
+      $form_state->setErrorByName('google_analytics_codesnippet_before', $this->t('Do not add the tracker code provided by Google into the javascript code snippets! This module already builds the tracker code based on your Google Analytics account number and settings.'));
     }
     if (stristr($form_state->getValue('google_analytics_codesnippet_after'), 'google-analytics.com/analytics.js')) {
-      $form_state->setErrorByName('google_analytics_codesnippet_after', t('Do not add the tracker code provided by Google into the javascript code snippets! This module already builds the tracker code based on your Google Analytics account number and settings.'));
+      $form_state->setErrorByName('google_analytics_codesnippet_after', $this->t('Do not add the tracker code provided by Google into the javascript code snippets! This module already builds the tracker code based on your Google Analytics account number and settings.'));
     }
     if (preg_match('/(.*)<\/?script(.*)>(.*)/i', $form_state->getValue('google_analytics_codesnippet_before'))) {
-      $form_state->setErrorByName('google_analytics_codesnippet_before', t('Do not include the &lt;script&gt; tags in the javascript code snippets.'));
+      $form_state->setErrorByName('google_analytics_codesnippet_before', $this->t('Do not include the &lt;script&gt; tags in the javascript code snippets.'));
     }
     if (preg_match('/(.*)<\/?script(.*)>(.*)/i', $form_state->getValue('google_analytics_codesnippet_after'))) {
-      $form_state->setErrorByName('google_analytics_codesnippet_after', t('Do not include the &lt;script&gt; tags in the javascript code snippets.'));
+      $form_state->setErrorByName('google_analytics_codesnippet_after', $this->t('Do not include the &lt;script&gt; tags in the javascript code snippets.'));
     }
   }
 
@@ -786,7 +822,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
@@ -796,8 +832,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']]));
@@ -805,11 +841,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;
         }
@@ -830,7 +866,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);
@@ -856,7 +892,7 @@ protected static function extractCreateFieldValues($string) {
   }
 
   /**
-   * Checks whether a field name is valid.
+   * Checks whether a parameter name is valid.
    *
    * @param string $name
    *   The option value entered by the user.
@@ -864,32 +900,39 @@ 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',
-      'userId',
-    ];
-
-    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 == 'allowLinker') {
-      return t('Create only field name %name is a disallowed field name. Please select <em>Multiple top-level domains</em> under <em>What are you tracking</em> to enable cross domain tracking.', ['%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']);
+    $allowed_parameters = [
+      'client_id',
+      'currency',
+      'country',
+      'cookie_name',
+      'cookie_domain',
+      'cookie_expires',
+      'sample_rate',
+      'send_page_view',
+      'site_speed_sample_rate',
+      'use_amp_client_id',
+    ];
+
+    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 == '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 == '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, $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/']);
     }
   }
 
@@ -902,12 +945,12 @@ protected static function validateCreateFieldName($name) {
    * @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) && !Unicode::strlen($value)) {
-      return t('A create only field requires a value.');
+      return t('A parameter requires a value.');
     }
     if (Unicode::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.');
     }
   }
 
@@ -961,13 +1004,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;
@@ -979,4 +1021,16 @@ protected static function convertFormValueDataTypes(array $values) {
     return $values;
   }
 
+  /**
+   * {@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')
+    );
+  }
+
 }
diff --git a/web/modules/google_analytics/src/GoogleAnalitycsInterface.php b/web/modules/google_analytics/src/GoogleAnalitycsInterface.php
new file mode 100644
index 0000000000..0c98290328
--- /dev/null
+++ b/web/modules/google_analytics/src/GoogleAnalitycsInterface.php
@@ -0,0 +1,15 @@
+<?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/Plugin/migrate/process/GoogleAnalyticsSkipRowIfNotSet.php b/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsSkipRowIfNotSet.php
deleted file mode 100644
index 089d68519e..0000000000
--- a/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsSkipRowIfNotSet.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-namespace Drupal\google_analytics\Plugin\migrate\process;
-
-use Drupal\migrate\ProcessPluginBase;
-use Drupal\migrate\MigrateExecutableInterface;
-use Drupal\migrate\Row;
-use Drupal\migrate\MigrateSkipRowException;
-
-/**
- * If the source evaluates to empty, we skip the current row.
- *
- * @MigrateProcessPlugin(
- *   id = "google_analytics_skip_row_if_not_set",
- *   handle_multiples = TRUE
- * )
- */
-class GoogleAnalyticsSkipRowIfNotSet extends ProcessPluginBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
-    if (!isset($value[$this->configuration['module']][$this->configuration['key']])) {
-      throw new MigrateSkipRowException();
-    }
-    return $value[$this->configuration['module']][$this->configuration['key']];
-  }
-
-}
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 f7986db9ce..8374c5520d 100644
--- a/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityPages.php
+++ b/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityPages.php
@@ -29,6 +29,8 @@ class GoogleAnalyticsVisibilityPages extends ProcessPluginBase implements Contai
   protected $moduleHandler;
 
   /**
+   * The migration process plugin.
+   *
    * The migration process plugin, configured for lookups in the d6_user_role
    * and d7_user_role migrations.
    *
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 6d6dab7f4f..2fe62b405c 100644
--- a/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityRoles.php
+++ b/web/modules/google_analytics/src/Plugin/migrate/process/GoogleAnalyticsVisibilityRoles.php
@@ -28,6 +28,8 @@ class GoogleAnalyticsVisibilityRoles extends ProcessPluginBase implements Contai
   protected $moduleHandler;
 
   /**
+   * The migration process plugin.
+   *
    * The migration process plugin, configured for lookups in the d6_user_role
    * and d7_user_role migrations.
    *
diff --git a/web/modules/google_analytics/src/Tests/GoogleAnalyticsBasicTest.php b/web/modules/google_analytics/src/Tests/GoogleAnalyticsBasicTest.php
index 04926f183d..c5875e2063 100644
--- a/web/modules/google_analytics/src/Tests/GoogleAnalyticsBasicTest.php
+++ b/web/modules/google_analytics/src/Tests/GoogleAnalyticsBasicTest.php
@@ -82,7 +82,7 @@ public function testGoogleAnalyticsConfiguration() {
     $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->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.');
 
@@ -90,11 +90,11 @@ public function testGoogleAnalyticsConfiguration() {
     $this->drupalLogin($this->noSnippetUser);
     $this->drupalGet('admin/config/system/google-analytics');
 
-    // User should *not* have access to snippets, but create fields.
+    // User should *not* have access to snippets, but parameters field.
     $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->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.');
   }
@@ -119,7 +119,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->assertNoRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed without UA code configured.');
+    $this->assertNoRaw('https://www.googletagmanager.com/gtag/js?id=', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed without UA code configured.');
 
     $ua_code = 'UA-123456-1';
     $this->config('google_analytics.settings')->set('account', $ua_code)->save();
@@ -133,14 +133,14 @@ public function testGoogleAnalyticsPageVisibility() {
 
     // Check tracking code visibility.
     $this->drupalGet('');
-    $this->assertRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed for authenticated users.');
+    $this->assertRaw('gtag("config", "' . $ua_code . '"', '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed for authenticated users.');
 
     // Test whether tracking code is not included on pages to omit.
     $this->drupalGet('admin');
     $this->assertNoRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin page.');
     $this->drupalGet('admin/config/system/google-analytics');
     // Checking for tracking URI here, as $ua_code is displayed in the form.
-    $this->assertNoRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin subpage.');
+    $this->assertNoRaw('https://www.googletagmanager.com/gtag/js?id=', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin subpage.');
 
     // Test whether tracking code display is properly flipped.
     $this->config('google_analytics.settings')->set('visibility.request_path_mode', 1)->save();
@@ -148,7 +148,7 @@ public function testGoogleAnalyticsPageVisibility() {
     $this->assertRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin page.');
     $this->drupalGet('admin/config/system/google-analytics');
     // Checking for tracking URI here, as $ua_code is displayed in the form.
-    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin subpage.');
+    $this->assertRaw('https://www.googletagmanager.com/gtag/js?id=', '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin subpage.');
     $this->drupalGet('');
     $this->assertNoRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is NOT displayed on front page.');
 
@@ -161,18 +161,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->assertResponse(403);
-    $this->assertRaw($base_path . '403.html', '[testGoogleAnalyticsPageVisibility]: 403 Forbidden tracking code shown if user has no access.');
-
-    // Test whether 404 not found tracking code is shown on non-existent pages.
-    $this->drupalGet($this->randomMachineName(64));
-    $this->assertResponse(404);
-    $this->assertRaw($base_path . '404.html', '[testGoogleAnalyticsPageVisibility]: 404 Not Found tracking code shown on non-existent page.');
   }
 
   /**
@@ -189,75 +177,76 @@ public function testGoogleAnalyticsTrackingCode() {
 
     /* 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->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsTrackingCode]: Latest tracking code used.');
+    $this->assertRaw('<script async src="https://www.googletagmanager.com/gtag/js?id=' . $ua_code . '"></script>');
+    $this->assertRaw('window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments)};gtag("js", new Date());');
+    $this->assertRaw('"google_analytics":{"account":"' . $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->assertNoRaw('ga("set", "anonymizeIp", true);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address not found on frontpage.');
     // Enable anonymizing of IP addresses.
     $this->config('google_analytics.settings')->set('privacy.anonymizeip', 1)->save();
     $this->drupalGet('');
-    $this->assertRaw('ga("set", "anonymizeIp", true);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address found on frontpage.');
+    $this->assertRaw('"anonymize_ip":true', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address found on frontpage.');
+
+    // Test whether anonymize visitors IP address feature has been enabled.
+    $this->config('google_analytics.settings')->set('privacy.anonymizeip', 0)->save();
+    $this->drupalGet('');
+    $this->assertNoRaw('"anonymize_ip":true', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address not found on frontpage.');
 
     // Test if track Enhanced Link Attribution is enabled.
     $this->config('google_analytics.settings')->set('track.linkid', 1)->save();
     $this->drupalGet('');
-    $this->assertRaw('ga("require", "linkid", "linkid.js");', '[testGoogleAnalyticsTrackingCode]: Tracking code for Enhanced Link Attribution is enabled.');
+    $this->assertRaw('"link_attribution":true', '[testGoogleAnalyticsTrackingCode]: Tracking code for Enhanced Link Attribution is enabled.');
 
     // Test if track Enhanced Link Attribution is disabled.
     $this->config('google_analytics.settings')->set('track.linkid', 0)->save();
     $this->drupalGet('');
-    $this->assertNoRaw('ga("require", "linkid", "linkid.js");', '[testGoogleAnalyticsTrackingCode]: Tracking code for Enhanced Link Attribution is not enabled.');
-
-    // Test if tracking of url fragments is enabled.
-    $this->config('google_analytics.settings')->set('track.urlfragments', 1)->save();
-    $this->drupalGet('');
-    $this->assertRaw('ga("set", "page", location.pathname + location.search + location.hash);', '[testGoogleAnalyticsTrackingCode]: Tracking code for url fragments is enabled.');
-
-    // Test if tracking of url fragments is disabled.
-    $this->config('google_analytics.settings')->set('track.urlfragments', 0)->save();
-    $this->drupalGet('');
-    $this->assertNoRaw('ga("set", "page", location.pathname + location.search + location.hash);', '[testGoogleAnalyticsTrackingCode]: Tracking code for url fragments is not enabled.');
+    $this->assertNoRaw('"link_attribution":true', '[testGoogleAnalyticsTrackingCode]: Tracking code for Enhanced Link Attribution is not enabled.');
 
     // Test if tracking of User ID is enabled.
     $this->config('google_analytics.settings')->set('track.userid', 1)->save();
     $this->drupalGet('');
-    $this->assertRaw(', {"cookieDomain":"auto","userId":"', '[testGoogleAnalyticsTrackingCode]: Tracking code for User ID is enabled.');
+    $this->assertRaw('"user_id":"', '[testGoogleAnalyticsTrackingCode]: Tracking code for User ID is enabled.');
 
     // Test if tracking of User ID is disabled.
     $this->config('google_analytics.settings')->set('track.userid', 0)->save();
     $this->drupalGet('');
-    $this->assertNoRaw(', {"cookieDomain":"auto","userId":"', '[testGoogleAnalyticsTrackingCode]: Tracking code for User ID is disabled.');
+    $this->assertNoRaw('"user_id":"', '[testGoogleAnalyticsTrackingCode]: Tracking code for User ID is disabled.');
+
+    // Test if track display features is disabled.
+    $this->config('google_analytics.settings')->set('track.displayfeatures', 0)->save();
+    $this->drupalGet('');
+    $this->assertRaw('"allow_ad_personalization_signals":false', '[testGoogleAnalyticsTrackingCode]: Tracking code for display features is not enabled.');
 
     // Test if track display features is enabled.
     $this->config('google_analytics.settings')->set('track.displayfeatures', 1)->save();
     $this->drupalGet('');
-    $this->assertRaw('ga("require", "displayfeatures");', '[testGoogleAnalyticsTrackingCode]: Tracking code for display features is enabled.');
+    $this->assertNoRaw('"allow_ad_personalization_signals":false', '[testGoogleAnalyticsTrackingCode]: Tracking code for display features is enabled.');
 
-    // 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->assertRaw('"page_path":location.pathname + location.search + location.hash});', '[testGoogleAnalyticsTrackingCode]: Tracking code for url fragments is enabled.');
+
+    // Test if tracking of url fragments is disabled.
+    $this->config('google_analytics.settings')->set('track.urlfragments', 0)->save();
     $this->drupalGet('');
-    $this->assertNoRaw('ga("require", "displayfeatures");', '[testGoogleAnalyticsTrackingCode]: Tracking code for display features is not enabled.');
+    $this->assertNoRaw('"page_path":location.pathname + location.search + location.hash});', '[testGoogleAnalyticsTrackingCode]: Tracking code for url fragments is not enabled.');
 
     // Test whether single domain tracking is active.
     $this->drupalGet('');
-    $this->assertRaw('{"cookieDomain":"auto"}', '[testGoogleAnalyticsTrackingCode]: Single domain tracking is active.');
+    $this->assertRaw('{"groups":"default"}', '[testGoogleAnalyticsTrackingCode]: Single domain tracking is active.');
 
     // Enable "One domain with multiple subdomains".
     $this->config('google_analytics.settings')->set('domain_mode', 1)->save();
@@ -268,11 +257,11 @@ public function testGoogleAnalyticsTrackingCode() {
     // reliable.
     global $cookie_domain;
     if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
-      $this->assertRaw('{"cookieDomain":"' . $cookie_domain . '"}', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains is active on real host.');
+      $this->assertRaw('"cookie_domain":"' . $cookie_domain . '"', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains is active on real host.');
     }
     else {
       // Special cases, Localhost and IP addresses don't show 'cookieDomain'.
-      $this->assertNoRaw('{"cookieDomain":"' . $cookie_domain . '"}', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains may be active on localhost (test result is not reliable).');
+      $this->assertNoRaw('"cookie_domain":"' . $cookie_domain . '"', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains may be active on localhost (test result is not reliable).');
     }
 
     // Enable "Multiple top-level domains" tracking.
@@ -281,9 +270,8 @@ public function testGoogleAnalyticsTrackingCode() {
       ->set('cross_domains', "www.example.com\nwww.example.net")
       ->save();
     $this->drupalGet('');
-    $this->assertRaw('ga("create", "' . $ua_code . '", {"cookieDomain":"auto","allowLinker":true', '[testGoogleAnalyticsTrackingCode]: "allowLinker" has been found. Cross domain tracking is active.');
-    $this->assertRaw('ga("require", "linker");', '[testGoogleAnalyticsTrackingCode]: Require linker has been found. Cross domain tracking is active.');
-    $this->assertRaw('ga("linker:autoLink", ["www.example.com","www.example.net"]);', '[testGoogleAnalyticsTrackingCode]: "linker:autoLink" has been found. Cross domain tracking is active.');
+    $this->assertRaw('gtag("config", "' . $ua_code . '", {"groups":"default","linker":', '[testGoogleAnalyticsTrackingCode]: "linker" has been found. Cross domain tracking is active.');
+    $this->assertRaw('gtag("config", "' . $ua_code . '", {"groups":"default","linker":{"domains":["www.example.com","www.example.net"]}});', '[testGoogleAnalyticsTrackingCode]: "domains" has been found. Cross domain tracking is active.');
     $this->assertRaw('"trackDomainMode":2,', '[testGoogleAnalyticsTrackingCode]: Domain mode value is of type integer.');
     $this->assertRaw('"trackCrossDomains":["www.example.com","www.example.net"]', '[testGoogleAnalyticsTrackingCode]: Cross domain tracking with www.example.com and www.example.net is active.');
     $this->config('google_analytics.settings')->set('domain_mode', 0)->save();
@@ -291,7 +279,8 @@ public function testGoogleAnalyticsTrackingCode() {
     // Test whether debugging script has been enabled.
     $this->config('google_analytics.settings')->set('debug', 1)->save();
     $this->drupalGet('');
-    $this->assertRaw('https://www.google-analytics.com/analytics_debug.js', '[testGoogleAnalyticsTrackingCode]: Google debugging script has been enabled.');
+    // @FIXME
+    //$this->assertRaw('https://www.google-analytics.com/analytics_debug.js', '[testGoogleAnalyticsTrackingCode]: Google debugging script has been enabled.');
 
     // Check if text and link is shown on 'Status Reports' page.
     // Requires 'administer site configuration' permission.
@@ -301,26 +290,25 @@ public function testGoogleAnalyticsTrackingCode() {
     // Test whether debugging script has been disabled.
     $this->config('google_analytics.settings')->set('debug', 0)->save();
     $this->drupalGet('');
-    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsTrackingCode]: Google debugging script has been disabled.');
+    $this->assertRaw('https://www.googletagmanager.com/gtag/js?id=', '[testGoogleAnalyticsTrackingCode]: Google debugging script has been disabled.');
 
     // 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->assertRaw('ga("create", "' . $ua_code . '", {"cookieDomain":"foo.example.com","cookieName":"myNewName","cookieExpires":20000,"allowAnchor":true,"sampleRate":4.3});', '[testGoogleAnalyticsTrackingCode]: Create only fields have been found.');
-    $this->assertRaw('ga("set", "forceSSL", true);', '[testGoogleAnalyticsTrackingCode]: Before codesnippet will force http pages to also send all beacons using https.');
-    $this->assertRaw('ga("create", "UA-123456-3", {"name": "newTracker"});', '[testGoogleAnalyticsTrackingCode]: After codesnippet with "newTracker" tracker has been found.');
+    $this->assertRaw('gtag("config", "' . $ua_code . '", {"groups":"default","cookie_domain":"foo.example.com","cookie_name":"myNewName","cookie_expires":20000,"sample_rate":4.3});', '[testGoogleAnalyticsTrackingCode]: Config parameters have been found.');
+    $this->assertRaw('gtag("set", {"currency":"USD"});', '[testGoogleAnalyticsTrackingCode]: Before codesnippet has been found.');
+    $this->assertRaw('gtag("config", "UA-123456-3", {"groups":"default"});', '[testGoogleAnalyticsTrackingCode]: After codesnippet custom UA code has been found.');
     $this->assertRaw('if(1 == 1 && 2 < 3 && 2 > 1){console.log("Google Analytics: Custom condition works.");}', '[testGoogleAnalyticsTrackingCode]: JavaScript code is not HTML escaped.');
   }
 
diff --git a/web/modules/google_analytics/src/Tests/GoogleAnalyticsCustomDimensionsAndMetricsTest.php b/web/modules/google_analytics/src/Tests/GoogleAnalyticsCustomDimensionsAndMetricsTest.php
index 9404debc01..db7b5b9376 100644
--- a/web/modules/google_analytics/src/Tests/GoogleAnalyticsCustomDimensionsAndMetricsTest.php
+++ b/web/modules/google_analytics/src/Tests/GoogleAnalyticsCustomDimensionsAndMetricsTest.php
@@ -59,31 +59,41 @@ public function testGoogleAnalyticsCustomDimensions() {
     $google_analytics_custom_dimension = [
       1 => [
         'index' => 1,
+        'name' => 'bar1',
         'value' => 'Bar 1',
       ],
       2 => [
         'index' => 2,
+        'name' => 'bar2',
         'value' => 'Bar 2',
       ],
       3 => [
         'index' => 3,
+        'name' => 'bar2',
         'value' => 'Bar 3',
       ],
       4 => [
         'index' => 4,
+        'name' => 'bar4',
         'value' => 'Bar 4',
       ],
       5 => [
         'index' => 5,
+        'name' => 'bar5',
         'value' => 'Bar 5',
       ],
     ];
     $this->config('google_analytics.settings')->set('custom.dimension', $google_analytics_custom_dimension)->save();
     $this->drupalGet('');
 
+    $custom_map = [];
+    $custom_vars = [];
     foreach ($google_analytics_custom_dimension as $dimension) {
-      $this->assertRaw('ga("set", ' . Json::encode('dimension' . $dimension['index']) . ', ' . Json::encode($dimension['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Dimension #' . $dimension['index'] . ' is shown.');
+      $custom_map['custom_map']['dimension' . $dimension['index']] = $dimension['name'];
+      $custom_vars[$dimension['name']] = $dimension['value'];
     }
+    $this->assertRaw('gtag("config", ' . Json::encode($ua_code) . ', ' . Json::encode($custom_map) . ');');
+    $this->assertRaw('gtag("event", "custom", ' . Json::encode($custom_vars) . ');');
 
     // Test whether tokens are replaced in custom dimension values.
     $site_slogan = $this->randomMachineName(16);
@@ -92,33 +102,40 @@ public function testGoogleAnalyticsCustomDimensions() {
     $google_analytics_custom_dimension = [
       1 => [
         'index' => 1,
+        'name' => 'site_slogan',
         'value' => 'Value: [site:slogan]',
       ],
       2 => [
         'index' => 2,
+        'name' => 'machine_name',
         'value' => $this->randomMachineName(16),
       ],
       3 => [
         'index' => 3,
+        'name' => 'foo3',
         'value' => '',
       ],
       // #2300701: Custom dimensions and custom metrics not outputed on zero
       // value.
       4 => [
         'index' => 4,
+        'name' => 'bar4',
         'value' => '0',
       ],
       5 => [
         'index' => 5,
+        'name' => 'node_type',
         'value' => '[node:type]',
       ],
       // Test google_analytics_tokens().
       6 => [
         'index' => 6,
+        'name' => 'current_user_role_names',
         'value' => '[current-user:role-names]',
       ],
       7 => [
         'index' => 7,
+        'name' => 'current_user_role_ids',
         'value' => '[current-user:role-ids]',
       ],
     ];
@@ -127,18 +144,26 @@ public function testGoogleAnalyticsCustomDimensions() {
 
     // Test on frontpage.
     $this->drupalGet('');
-    $this->assertRaw('ga("set", ' . Json::encode('dimension1') . ', ' . Json::encode("Value: $site_slogan") . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Tokens have been replaced in dimension value.');
-    $this->assertRaw('ga("set", ' . Json::encode('dimension2') . ', ' . Json::encode($google_analytics_custom_dimension['2']['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Random value is shown.');
-    $this->assertNoRaw('ga("set", ' . Json::encode('dimension3') . ', ' . Json::encode('') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Empty value is not shown.');
-    $this->assertRaw('ga("set", ' . Json::encode('dimension4') . ', ' . Json::encode('0') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Value 0 is shown.');
-    $this->assertNoRaw('ga("set", ' . Json::encode('dimension5') . ', ' . Json::encode('article') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Node tokens are shown.');
-    $this->assertRaw('ga("set", ' . Json::encode('dimension6') . ', ' . Json::encode(implode(',', \Drupal::currentUser()->getRoles())) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: List of roles shown.');
-    $this->assertRaw('ga("set", ' . Json::encode('dimension7') . ', ' . Json::encode(implode(',', array_keys(\Drupal::currentUser()->getRoles()))) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: List of role IDs shown.');
+    $this->assertRaw(Json::encode('dimension1') . ':' . Json::encode($google_analytics_custom_dimension['1']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_dimension['1']['name']) . ':' . Json::encode("Value: $site_slogan"));
+    $this->assertRaw(Json::encode('dimension2') . ':' . Json::encode($google_analytics_custom_dimension['2']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_dimension['2']['name']) . ':' . Json::encode($google_analytics_custom_dimension['2']['value']));
+    $this->assertNoRaw(Json::encode('dimension3') . ':' . Json::encode($google_analytics_custom_dimension['3']['name']));
+    $this->assertNoRaw(Json::encode($google_analytics_custom_dimension['3']['name']) . ':' . Json::encode(''));
+    $this->assertRaw(Json::encode('dimension4') . ':' . Json::encode($google_analytics_custom_dimension['4']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_dimension['4']['name']) . ':' . Json::encode('0'));
+    $this->assertNoRaw(Json::encode('dimension5') . ':' . Json::encode($google_analytics_custom_dimension['5']['name']));
+    $this->assertNoRaw(Json::encode($google_analytics_custom_dimension['5']['name']) . ':' . Json::encode('article'));
+    $this->assertRaw(Json::encode('dimension6') . ':' . Json::encode($google_analytics_custom_dimension['6']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_dimension['6']['name']) . ':' . Json::encode(implode(',', \Drupal::currentUser()->getRoles())));
+    $this->assertRaw(Json::encode('dimension7') . ':' . Json::encode($google_analytics_custom_dimension['7']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_dimension['7']['name']) . ':' . Json::encode(implode(',', array_keys(\Drupal::currentUser()->getRoles()))));
 
     // Test on a node.
     $this->drupalGet('node/' . $node->id());
     $this->assertText($node->getTitle());
-    $this->assertRaw('ga("set", ' . Json::encode('dimension5') . ', ' . Json::encode('article') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Node tokens are shown.');
+    $this->assertRaw(Json::encode('dimension5') . ':' . Json::encode($google_analytics_custom_dimension['5']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_dimension['5']['name']) . ':' . Json::encode('article'));
   }
 
   /**
@@ -152,22 +177,27 @@ public function testGoogleAnalyticsCustomMetrics() {
     $google_analytics_custom_metric = [
       1 => [
         'index' => 1,
+        'name' => 'foo1',
         'value' => '6',
       ],
       2 => [
         'index' => 2,
+        'name' => 'foo2',
         'value' => '8000',
       ],
       3 => [
         'index' => 3,
+        'name' => 'foo3',
         'value' => '7.8654',
       ],
       4 => [
         'index' => 4,
+        'name' => 'foo4',
         'value' => '1123.4',
       ],
       5 => [
         'index' => 5,
+        'name' => 'foo5',
         'value' => '5,67',
       ],
     ];
@@ -175,28 +205,37 @@ public function testGoogleAnalyticsCustomMetrics() {
     $this->config('google_analytics.settings')->set('custom.metric', $google_analytics_custom_metric)->save();
     $this->drupalGet('');
 
+    $custom_map = [];
+    $custom_vars = [];
     foreach ($google_analytics_custom_metric as $metric) {
-      $this->assertRaw('ga("set", ' . Json::encode('metric' . $metric['index']) . ', ' . Json::encode((float) $metric['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Metric #' . $metric['index'] . ' is shown.');
+      $custom_map['custom_map']['metric' . $metric['index']] = $metric['name'];
+      $custom_vars[$metric['name']] = (float) $metric['value'];
     }
+    $this->assertRaw('gtag("config", ' . Json::encode($ua_code) . ', ' . Json::encode($custom_map) . ');');
+    $this->assertRaw('gtag("event", "custom", ' . Json::encode($custom_vars) . ');');
 
     // Test whether tokens are replaced in custom metric values.
     $google_analytics_custom_metric = [
       1 => [
         'index' => 1,
+        'name' => 'bar1',
         'value' => '[current-user:roles:count]',
       ],
       2 => [
         'index' => 2,
+        'name' => 'bar2',
         'value' => mt_rand(),
       ],
       3 => [
         'index' => 3,
+        'name' => 'bar3',
         'value' => '',
       ],
       // #2300701: Custom dimensions and custom metrics not outputed on zero
       // value.
       4 => [
         'index' => 4,
+        'name' => 'bar4',
         'value' => '0',
       ],
     ];
@@ -204,10 +243,14 @@ public function testGoogleAnalyticsCustomMetrics() {
     $this->verbose('<pre>' . print_r($google_analytics_custom_metric, TRUE) . '</pre>');
 
     $this->drupalGet('');
-    $this->assertRaw('ga("set", ' . Json::encode('metric1') . ', ', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Tokens have been replaced in metric value.');
-    $this->assertRaw('ga("set", ' . Json::encode('metric2') . ', ' . Json::encode($google_analytics_custom_metric['2']['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Random value is shown.');
-    $this->assertNoRaw('ga("set", ' . Json::encode('metric3') . ', ' . Json::encode('') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Empty value is not shown.');
-    $this->assertRaw('ga("set", ' . Json::encode('metric4') . ', ' . Json::encode(0) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Value 0 is shown.');
+    $this->assertRaw(Json::encode('metric1') . ':' . Json::encode($google_analytics_custom_metric['1']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_metric['1']['name']) . ':');
+    $this->assertRaw(Json::encode('metric2') . ':' . Json::encode($google_analytics_custom_metric['2']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_metric['2']['name']) . ':' . Json::encode($google_analytics_custom_metric['2']['value']));
+    $this->assertNoRaw(Json::encode('metric3') . ':' . Json::encode($google_analytics_custom_metric['3']['name']));
+    $this->assertNoRaw(Json::encode($google_analytics_custom_metric['3']['name']) . ':' . Json::encode(''));
+    $this->assertRaw(Json::encode('metric4') . ':' . Json::encode($google_analytics_custom_metric['4']['name']));
+    $this->assertRaw(Json::encode($google_analytics_custom_metric['4']['name']) . ':' . Json::encode(0));
   }
 
   /**
@@ -218,10 +261,15 @@ public function testGoogleAnalyticsCustomDimensionsTokenFormValidation() {
 
     // Check form validation.
     $edit['google_analytics_account'] = $ua_code;
+    $edit['google_analytics_custom_dimension[indexes][1][name]'] = 'current_user_name';
     $edit['google_analytics_custom_dimension[indexes][1][value]'] = '[current-user:name]';
+    $edit['google_analytics_custom_dimension[indexes][2][name]'] = 'current_user_edit_url';
     $edit['google_analytics_custom_dimension[indexes][2][value]'] = '[current-user:edit-url]';
+    $edit['google_analytics_custom_dimension[indexes][3][name]'] = 'user_name';
     $edit['google_analytics_custom_dimension[indexes][3][value]'] = '[user:name]';
+    $edit['google_analytics_custom_dimension[indexes][4][name]'] = 'term_name';
     $edit['google_analytics_custom_dimension[indexes][4][value]'] = '[term:name]';
+    $edit['google_analytics_custom_dimension[indexes][5][name]'] = 'term_tid';
     $edit['google_analytics_custom_dimension[indexes][5][value]'] = '[term:tid]';
 
     $this->drupalPostForm('admin/config/system/google-analytics', $edit, t('Save configuration'));
diff --git a/web/modules/google_analytics/src/Tests/GoogleAnalyticsCustomUrls.php b/web/modules/google_analytics/src/Tests/GoogleAnalyticsCustomUrls.php
index cbf14a7841..139a23031c 100644
--- a/web/modules/google_analytics/src/Tests/GoogleAnalyticsCustomUrls.php
+++ b/web/modules/google_analytics/src/Tests/GoogleAnalyticsCustomUrls.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\google_analytics\Tests;
 
-use Drupal\Core\Session\AccountInterface;
+use Drupal\Component\Serialization\Json;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -39,19 +39,33 @@ protected function setUp() {
   /**
    * 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->assertRaw('ga("set", "page", "' . $base_path . 'user/password"');
+    $this->assertRaw('gtag("config", ' . Json::encode($ua_code) . ', {"groups":"default","page_path":"' . $base_path . 'user/password"});');
 
     $this->drupalGet('user/password', ['query' => ['name' => 'foo@example.com']]);
-    $this->assertRaw('ga("set", "page", "' . $base_path . 'user/password"');
+    $this->assertRaw('gtag("config", ' . Json::encode($ua_code) . ', {"groups":"default","page_path":"' . $base_path . 'user/password"});');
 
     $this->drupalGet('user/password');
-    $this->assertNoRaw('ga("set", "page",', '[testGoogleAnalyticsCustomUrls]: Custom url not set.');
+    $this->assertNoRaw('"page_path":"' . $base_path . 'user/password"});', '[testGoogleAnalyticsCustomUrls]: Custom url not set.');
+
+    // Test whether 403 forbidden tracking code is shown if user has no access.
+    $this->drupalGet('admin');
+    $this->assertResponse(403);
+    $this->assertRaw($base_path . '403.html', '[testGoogleAnalyticsCustomUrls]: 403 Forbidden tracking code shown if user has no access.');
+
+    // Test whether 404 not found tracking code is shown on non-existent pages.
+    $this->drupalGet($this->randomMachineName(64));
+    $this->assertResponse(404);
+    $this->assertRaw($base_path . '404.html', '[testGoogleAnalyticsCustomUrls]: 404 Not Found tracking code shown on non-existent page.');
   }
 
 }
diff --git a/web/modules/google_analytics/src/Tests/GoogleAnalyticsJavaScriptTest.js b/web/modules/google_analytics/src/Tests/GoogleAnalyticsJavaScriptTest.js
index ded59bf5c9..45f8d52a47 100644
--- a/web/modules/google_analytics/src/Tests/GoogleAnalyticsJavaScriptTest.js
+++ b/web/modules/google_analytics/src/Tests/GoogleAnalyticsJavaScriptTest.js
@@ -81,7 +81,7 @@
   Drupal.google_analytics.test.assertTrue(Drupal.google_analytics.isInternal(base_url + drupalSettings.path.baseUrl + 'node/1?foo=bar'), "Link '" + base_url + drupalSettings.path.baseUrl + "node/1?foo=bar' has been detected as internal link.");
   Drupal.google_analytics.test.assertTrue(Drupal.google_analytics.isInternal(base_url + drupalSettings.path.baseUrl + 'node/1?foo=bar#foo'), "Link '" + base_url + drupalSettings.path.baseUrl + "node/1?foo=bar#foo' has been detected as internal link.");
   Drupal.google_analytics.test.assertTrue(Drupal.google_analytics.isInternal(base_url + drupalSettings.path.baseUrl + 'go/foo'), "Link '" + base_url + drupalSettings.path.baseUrl + "go/foo' has been detected as internal link.");
-  Drupal.google_analytics.test.assertFalse(Drupal.google_analytics.isInternal('http://example.com/node/3'), "Link 'http://example.com/node/3' has been detected as external link.");
+  Drupal.google_analytics.test.assertFalse(Drupal.google_analytics.isInternal('https://example.com/node/3'), "Link 'https://example.com/node/3' has been detected as external link.");
   console.groupEnd();
 
   console.group("Test 'isInternalSpecial':");
@@ -90,9 +90,11 @@
   console.groupEnd();
 
   console.group("Test 'getPageUrl':");
-  Drupal.google_analytics.test.assertSame(base_path, Drupal.google_analytics.getPageUrl(base_url + drupalSettings.path.baseUrl + 'node/1'), "Absolute internal URL '" + drupalSettings.path.baseUrl + "node/1' has been extracted from full qualified url '" + base_url + base_path + "'.");
-  Drupal.google_analytics.test.assertSame(base_path, Drupal.google_analytics.getPageUrl(drupalSettings.path.baseUrl + 'node/1'), "Absolute internal URL '" + drupalSettings.path.baseUrl + "node/1' has been extracted from absolute url '" + base_path + "'.");
-  Drupal.google_analytics.test.assertSame('http://example.com/node/2', Drupal.google_analytics.getPageUrl('http://example.com/node/2'), "Full qualified external url 'http://example.com/node/2' has been extracted.");
+  Drupal.google_analytics.test.assertSame(base_path, Drupal.google_analytics.getPageUrl(window.location.href), "Absolute internal URL '" + base_path + "' has been extracted from full qualified url '" + window.location.href + "'.");
+  Drupal.google_analytics.test.assertSame(base_path, Drupal.google_analytics.getPageUrl(base_path), "Absolute internal URL '" + base_path + "' has been extracted from absolute url '" + base_path + "'.");
+  //Drupal.google_analytics.test.assertSame(base_path, Drupal.google_analytics.getPageUrl(base_url + drupalSettings.path.baseUrl + 'node/1'), "Absolute internal URL '" + drupalSettings.path.baseUrl + "node/1' has been extracted from full qualified url '" + base_url + base_path + "'.");
+  //Drupal.google_analytics.test.assertSame(base_path, Drupal.google_analytics.getPageUrl(drupalSettings.path.baseUrl + 'node/1'), "Absolute internal URL '" + drupalSettings.path.baseUrl + "node/1' has been extracted from absolute url '" + base_path + "'.");
+  Drupal.google_analytics.test.assertSame('https://example.com/node/2', Drupal.google_analytics.getPageUrl('https://example.com/node/2'), "Full qualified external url 'https://example.com/node/2' has been extracted.");
   Drupal.google_analytics.test.assertSame('//example.com/node/2', Drupal.google_analytics.getPageUrl('//example.com/node/2'), "Full qualified external url '//example.com/node/2' has been extracted.");
   console.groupEnd();
 
@@ -109,9 +111,9 @@
   if (drupalSettings.google_analytics.trackCrossDomains) {
     console.dir(drupalSettings.google_analytics.trackCrossDomains);
     Drupal.google_analytics.test.assertTrue(Drupal.google_analytics.isCrossDomain('example.com', drupalSettings.google_analytics.trackCrossDomains), "URL 'example.com' has been found in cross domain list.");
-    Drupal.google_analytics.test.assertTrue(Drupal.google_analytics.isCrossDomain('example.net', drupalSettings.google_analytics.trackCrossDomains), "URL 'example.com' has been found in cross domain list.");
+    Drupal.google_analytics.test.assertTrue(Drupal.google_analytics.isCrossDomain('example.net', drupalSettings.google_analytics.trackCrossDomains), "URL 'example.net' has been found in cross domain list.");
     Drupal.google_analytics.test.assertFalse(Drupal.google_analytics.isCrossDomain('www.example.com', drupalSettings.google_analytics.trackCrossDomains), "URL 'www.example.com' not found in cross domain list.");
-    Drupal.google_analytics.test.assertFalse(Drupal.google_analytics.isCrossDomain('www.example.net', drupalSettings.google_analytics.trackCrossDomains), "URL 'www.example.com' not found in cross domain list.");
+    Drupal.google_analytics.test.assertFalse(Drupal.google_analytics.isCrossDomain('www.example.net', drupalSettings.google_analytics.trackCrossDomains), "URL 'www.example.net' not found in cross domain list.");
   }
   else {
     console.warn('Cross domain tracking is not enabled. Tests skipped.');
diff --git a/web/modules/google_analytics/src/Tests/GoogleAnalyticsPhpFilterTest.php b/web/modules/google_analytics/src/Tests/GoogleAnalyticsPhpFilterTest.php
index df4af7617e..ffdcf3f581 100644
--- a/web/modules/google_analytics/src/Tests/GoogleAnalyticsPhpFilterTest.php
+++ b/web/modules/google_analytics/src/Tests/GoogleAnalyticsPhpFilterTest.php
@@ -63,13 +63,13 @@ public function testGoogleAnalyticsPhpFilter() {
     // Check tracking code visibility.
     $this->config('google_analytics.settings')->set('visibility.request_path_pages', '<?php return TRUE; ?>')->save();
     $this->drupalGet('');
-    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is displayed on frontpage page.');
+    $this->assertRaw('https://www.googletagmanager.com/gtag/js?id=', '[testGoogleAnalyticsPhpFilter]: Tracking is displayed on frontpage page.');
     $this->drupalGet('admin');
-    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is displayed on admin page.');
+    $this->assertRaw('https://www.googletagmanager.com/gtag/js?id=', '[testGoogleAnalyticsPhpFilter]: Tracking is displayed on admin page.');
 
     $this->config('google_analytics.settings')->set('visibility.request_path_pages', '<?php return FALSE; ?>')->save();
     $this->drupalGet('');
-    $this->assertNoRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is not displayed on frontpage page.');
+    $this->assertNoRaw('https://www.googletagmanager.com/gtag/js?id=', '[testGoogleAnalyticsPhpFilter]: Tracking is not displayed on frontpage page.');
 
     // Test administration form.
     $this->config('google_analytics.settings')->set('visibility.request_path_pages', '<?php return TRUE; ?>')->save();
diff --git a/web/modules/google_analytics/src/Tests/GoogleAnalyticsSearchTest.php b/web/modules/google_analytics/src/Tests/GoogleAnalyticsSearchTest.php
index ec6af115b4..8e30bc5c97 100644
--- a/web/modules/google_analytics/src/Tests/GoogleAnalyticsSearchTest.php
+++ b/web/modules/google_analytics/src/Tests/GoogleAnalyticsSearchTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\google_analytics\Tests;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -44,14 +45,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, '[testGoogleAnalyticsSearch]: Tracking code is displayed for authenticated users.');
 
     $this->drupalGet('search/node');
-    $this->assertNoRaw('ga("set", "page",', '[testGoogleAnalyticsSearch]: Custom url not set.');
+    $this->assertNoRaw('gtag("config", ' . Json::encode($ua_code) . ', {"groups":"default","page_path":"', '[testGoogleAnalyticsSearch]: Custom url not set.');
 
     // Enable site search support.
     $this->config('google_analytics.settings')->set('track.site_search', 1)->save();
@@ -61,14 +66,13 @@ public function testGoogleAnalyticsSearchTracking() {
     $search['keys'] = $this->randomMachineName(8);
 
     // Create a node to search for.
-    // Create a node.
     $edit = [];
     $edit['title[0][value]'] = 'This is a test title';
     $edit['body[0][value]'] = 'This test content contains ' . $search['keys'] . ' string.';
 
     // Fire a search, it's expected to get 0 results.
     $this->drupalPostForm('search/node', $search, t('Search'));
-    $this->assertRaw('ga("set", "page", (window.google_analytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
+    $this->assertRaw('gtag("config", ' . Json::encode($ua_code) . ', {"groups":"default","page_path":(window.google_analytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
     $this->assertRaw('window.google_analytics_search_results = 0;', '[testGoogleAnalyticsSearch]: Search yielded no results.');
 
     // Save the node.
@@ -79,7 +83,7 @@ public function testGoogleAnalyticsSearchTracking() {
     $this->cronRun();
 
     $this->drupalPostForm('search/node', $search, t('Search'));
-    $this->assertRaw('ga("set", "page", (window.google_analytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
+    $this->assertRaw('gtag("config", ' . Json::encode($ua_code) . ', {"groups":"default","page_path":(window.google_analytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
     $this->assertRaw('window.google_analytics_search_results = 1;', '[testGoogleAnalyticsSearch]: One search result found.');
 
     $this->drupalPostForm('node/add/page', $edit, t('Save'));
@@ -89,7 +93,7 @@ public function testGoogleAnalyticsSearchTracking() {
     $this->cronRun();
 
     $this->drupalPostForm('search/node', $search, t('Search'));
-    $this->assertRaw('ga("set", "page", (window.google_analytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
+    $this->assertRaw('gtag("config", ' . Json::encode($ua_code) . ', {"groups":"default","page_path":(window.google_analytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
     $this->assertRaw('window.google_analytics_search_results = 2;', '[testGoogleAnalyticsSearch]: Two search results found.');
   }
 
diff --git a/web/modules/google_analytics/src/Tests/GoogleAnalyticsStatusMessagesTest.php b/web/modules/google_analytics/src/Tests/GoogleAnalyticsStatusMessagesTest.php
index 2082fccc32..25a5771692 100644
--- a/web/modules/google_analytics/src/Tests/GoogleAnalyticsStatusMessagesTest.php
+++ b/web/modules/google_analytics/src/Tests/GoogleAnalyticsStatusMessagesTest.php
@@ -44,24 +44,28 @@ public function testGoogleAnalyticsStatusMessages() {
     $this->config('google_analytics.settings')->set('track.messages', ['error' => 'error'])->save();
 
     $this->drupalPostForm('user/login', [], t('Log in'));
-    $this->assertRaw('ga("send", "event", "Messages", "Error message", "Username field is required.");', '[testGoogleAnalyticsStatusMessages]: Event message "Username field is required." is shown.');
-    $this->assertRaw('ga("send", "event", "Messages", "Error message", "Password field is required.");', '[testGoogleAnalyticsStatusMessages]: Event message "Password field is required." is shown.');
+    $this->assertRaw('gtag("event", "Error message", {"event_category":"Messages","event_label":"Username field is required."});', '[testGoogleAnalyticsStatusMessages]: Event message "Username field is required." is shown.');
+    $this->assertRaw('gtag("event", "Error message", {"event_category":"Messages","event_label":"Password field is required."});', '[testGoogleAnalyticsStatusMessages]: Event message "Password field is required." is shown.');
 
     // Testing this drupal_set_message() requires an extra test module.
-    $this->drupalGet('google-analytics-test/drupal-set-message');
-    $this->assertNoRaw('ga("send", "event", "Messages", "Status message", "Example status message.");', '[testGoogleAnalyticsStatusMessages]: Example status message is not enabled for tracking.');
-    $this->assertNoRaw('ga("send", "event", "Messages", "Warning message", "Example warning message.");', '[testGoogleAnalyticsStatusMessages]: Example warning message is not enabled for tracking.');
-    $this->assertRaw('ga("send", "event", "Messages", "Error message", "Example error message.");', '[testGoogleAnalyticsStatusMessages]: Example error message is shown.');
-    $this->assertRaw('ga("send", "event", "Messages", "Error message", "Example error message with html tags and link.");', '[testGoogleAnalyticsStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.');
+    $this->drupalGet('google-analytics-test/drupal-messenger-add-message');
+    $this->assertNoRaw('gtag("event", "Status message", {"event_category":"Messages","event_label":"Example status message."});', '[testGoogleAnalyticsStatusMessages]: Example status message is not enabled for tracking.');
+    $this->assertNoRaw('gtag("event", "Warning message", {"event_category":"Messages","event_label":"Example warning message."});', '[testGoogleAnalyticsStatusMessages]: Example warning message is not enabled for tracking.');
+    $this->assertRaw('gtag("event", "Error message", {"event_category":"Messages","event_label":"Example error message."});', '[testGoogleAnalyticsStatusMessages]: Example error message is shown.');
+    $this->assertRaw('gtag("event", "Error message", {"event_category":"Messages","event_label":"Example error message with html tags and link."});', '[testGoogleAnalyticsStatusMessages]: HTML has been stripped successful from 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->config('google_analytics.settings')->set('track.messages', [
+      'status' => 'status',
+      'warning' => 'warning',
+      'error' => 'error',
+    ])->save();
 
-    $this->drupalGet('google-analytics-test/drupal-set-message');
-    $this->assertRaw('ga("send", "event", "Messages", "Status message", "Example status message.");', '[testGoogleAnalyticsStatusMessages]: Example status message is enabled for tracking.');
-    $this->assertRaw('ga("send", "event", "Messages", "Warning message", "Example warning message.");', '[testGoogleAnalyticsStatusMessages]: Example warning message is enabled for tracking.');
-    $this->assertRaw('ga("send", "event", "Messages", "Error message", "Example error message.");', '[testGoogleAnalyticsStatusMessages]: Example error message is shown.');
-    $this->assertRaw('ga("send", "event", "Messages", "Error message", "Example error message with html tags and link.");', '[testGoogleAnalyticsStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.');
+    $this->drupalGet('google-analytics-test/drupal-messenger-add-message');
+    $this->assertRaw('gtag("event", "Status message", {"event_category":"Messages","event_label":"Example status message."});', '[testGoogleAnalyticsStatusMessages]: Example status message is enabled for tracking.');
+    $this->assertRaw('gtag("event", "Warning message", {"event_category":"Messages","event_label":"Example warning message."});', '[testGoogleAnalyticsStatusMessages]: Example warning message is enabled for tracking.');
+    $this->assertRaw('gtag("event", "Error message", {"event_category":"Messages","event_label":"Example error message."});', '[testGoogleAnalyticsStatusMessages]: Example error message is shown.');
+    $this->assertRaw('gtag("event", "Error message", {"event_category":"Messages","event_label":"Example error message with html tags and link."});', '[testGoogleAnalyticsStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.');
   }
 
 }
diff --git a/web/modules/google_analytics/src/Tests/GoogleAnalyticsUninstallTest.php b/web/modules/google_analytics/src/Tests/GoogleAnalyticsUninstallTest.php
index f73f0a7c42..bcffc740b1 100644
--- a/web/modules/google_analytics/src/Tests/GoogleAnalyticsUninstallTest.php
+++ b/web/modules/google_analytics/src/Tests/GoogleAnalyticsUninstallTest.php
@@ -45,16 +45,16 @@ 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.
+    // Test if the directory and gtag.js exists.
     $this->assertTrue(file_prepare_directory($cache_path), 'Cache directory "public://google_analytics" has been found.');
-    $this->assertTrue(file_exists($cache_path . '/analytics.js'), 'Cached analytics.js tracking file has been found.');
-    $this->assertTrue(file_exists($cache_path . '/analytics.js.gz'), 'Cached analytics.js.gz tracking file 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 = [];
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 7399b11b6a..1bb1f5c8ca 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
@@ -4,8 +4,8 @@ description: 'Support module for Google Analytics testing.'
 package: Testing
 # core: 8.x
 
-# Information added by Drupal.org packaging script on 2017-09-25
-version: '8.x-2.2'
+# Information added by Drupal.org packaging script on 2019-01-31
+version: '8.x-3.0'
 core: '8.x'
 project: 'google_analytics'
-datestamp: 1506372866
+datestamp: 1548968586
diff --git a/web/modules/google_analytics/tests/modules/google_analytics_test/google_analytics_test.routing.yml b/web/modules/google_analytics/tests/modules/google_analytics_test/google_analytics_test.routing.yml
index 8b7b092da3..dd12ce3fd6 100644
--- a/web/modules/google_analytics/tests/modules/google_analytics_test/google_analytics_test.routing.yml
+++ b/web/modules/google_analytics/tests/modules/google_analytics_test/google_analytics_test.routing.yml
@@ -1,7 +1,7 @@
-google_analytics_test.drupal_set_message:
-  path: '/google-analytics-test/drupal-set-message'
+google_analytics_test.drupal_messenger_add_message:
+  path: '/google-analytics-test/drupal-messenger-add-message'
   defaults:
-    _title: 'Set messages with drupal_set_message()'
-    _controller: '\Drupal\google_analytics_test\Controller\GoogleAnalyticsTestController::drupalSetMessageTest'
+    _title: 'Set messages with Drupal::messenger()'
+    _controller: '\Drupal\google_analytics_test\Controller\GoogleAnalyticsTestController::drupalAddMessageTest'
   requirements:
     _access: 'TRUE'
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 074049cb97..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
@@ -12,15 +12,15 @@ class GoogleAnalyticsTestController extends ControllerBase {
   /**
    * Tests setting messages and removing one before it is displayed.
    *
-   * @return string
-   *   Empty string, we just test the setting of messages.
+   * @return array
+   *   Empty array, we just test the setting of messages.
    */
-  public function drupalSetMessageTest() {
+  public function drupalAddMessageTest() {
     // Set some messages.
-    drupal_set_message('Example status message.', 'status');
-    drupal_set_message('Example warning message.', 'warning');
-    drupal_set_message('Example error message.', 'error');
-    drupal_set_message('Example error <em>message</em> with html tags and <a href="http://example.com/">link</a>.', 'error');
+    $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="https://example.com/">link</a>.'), 'error');
 
     return [];
   }
-- 
GitLab