Guide: How to update GA4 config variables on any event after the page view

A structural approach to configure GA4 tags in a flexible and scalable way, with examples for GTM and GTAG.js

In Google Analytics 4 there are two ways to set event scoped parameters:

  1. Include them in the GA4 configuration tag. This will persist values across all events on the page
  2. Add them to a custom event. In this manner the parameters only apply to the specific event being fired

Only having these two flavors is problematic when you want an event parameter to persist while the user is on the page, but when its value may change on a subsequent event after the page view. The situation can be especially complex for automatically measured events through enhanced measurement, as no custom parameters can be defined for these events. This guide describes two configurations to resolve this problem: 1 solution for GTM and 1 solution for the gtag.js library. But before diving into these solutions, let's first clarify the issue with an example.


Background

In Universal Analytics it is possible to 'set' variable values before (or after) any event. By doing so, the parameter persists across all subsequent events on the page. This option does not exist anymore in GA4. Only user property values can be set before (or after) any event, while event parameters can only be set in the configuration or in the event tag.

A relevant example for this website where this is problematic are theme switches from/to dark mode, using the sun or moon icon in the navigation. Let's say one would like to know if visitors in dark mode are more likely to scroll to the bottom of the page. To analyze this using the automatically measured 90% scrolled event, one would need to add the theme as a field in the GA4 configuration tag or config command. However, the theme defined in the configuration tag is not updated on a theme switch, as you can see in the scenario below:

  • On page load the theme parameter with the value 'light-theme' is added as a configuration field. This will make the parameter persist across subsequent events like 90% scrolled
  • light theme set at page view
  • When the visitor switches to the 'dark-theme', the new value is included as a parameter on the 'switch_theme' event. Here the parameter value does not persist beyond this event.
  • dark theme set after theme switch
  • As the user scrolls to the bottom of the page, the 90% scrolled event fires. No the theme parameter still has the value 'light-theme', as it was set in the configuration tag.
  • light theme is still included in the scroll event

Things are no better when the theme is added to only the custom event, even when page views are measured as custom events. Now the page theme is not added to the scroll event at all

  • scroll event contains no theme when this isn't set in the GA4 config

This may be a simple use case and I could have used a user property to work around this. However, there are scenarios where a user property does not make sense. Think for example about changes in search filters, which you probably want to include in automatically collected events like 'site search' or 'file download'. Or, alternatively, about updates to list of experiments the user is exposed to on a specific page. In that case you would like the list to be updated when a visitor is assigned to an additional experiment group after the page view event fired.

The next section will address how to resolve this issue in GTM implementations, before discussing the solution for gtag.js.


The solution for GA4 implementations in GTM

In Google Tag Manager you can configure a scalable solution with just two tags: one GA4 configuration tag and (at least) one GA4 event tag.

  • GTM GA4 Tag Overview

In the configuration tag you use the regular setup for the 'Measurement ID', 'Fields to set', and 'User Properties'. However, you need to use the following settings:

  • Disable the option 'Send a page view event when this configuration loads'
  • Send Page View flag is disabled in the GTM GA4 Configuration tag overview
  • Do NOT add any (firing) triggers. So the tag is saved without any triggers
  • GTM GA4 configuration tag overview

Next to the configuration tag, you need (at least) one custom event tag. If you designed your GA4 architecture in a scalable way, it should be possible to just have one event tag that handles both page views and all custom events, as is the case in this example. But you can use any number of custom event tags, as long as you use these settings:

  • Use the GA4 configuration tag as 'Configuration Tag'
  • Enable the option 'Fire a tag before <<tag name>> fires' under 'Advanced Settings' > 'Tag Sequencing'. Select the GA4 configuration tag as set-up tag
  • GTM GA4 event tag custom sequence configuration
  • Add your page-view and (relevant) event trigger(s) as triggers for this tag, so that it fires on page view and your event(s). Ideally you include the correct GA4 event names in your dataLayer (in the dataLayer.push event), but you can also use a lookup table variable
  • GTM GA4 event tag overview

As GTM persists the last observed variable value across all subsequent events on the page, this is a perfect solution. Only when a parameter is assigned a new value through a dataLayer.push event, it is updated in the GA4 configuration. Then the new value is used on all following events. For parameters not included in the last dataLayer.push event, GTM continues to use the last observed value.


The solution for GA4 implementations in gtag.js, which is also Single Page App compatible

For gtag.js the solution logic is comparable to that of GTM. For event parameters the gtag('set') command can't be used like in Universal Analytics, as in GA4 this call needs to be performed before the gtag('config') command. Therefore, we need to execute a gtag('config') command before each gtag('event') call. Page views aren't measured through config, but through the event command. To make this solution work, there are a few points of attention:

  • In the gtag('config') command you include all parameters you would like to persist on all subsequent events on the page, while in the gtag('event') call you only include event specific parameters. Note that this solution also allows you to set event parameters only on the 'page_view' event, which is not possible when sending page views through the gtag('config') command.
  • In the gtag('config') command you need to set the 'send_page_view' parameter to false (as boolean, not string), to prevent double page views.
  • You need a way to store and update variables set in the JSON variable used in the gtag('config') command. An easy solution is to store the JSON variable on the window object. Another option is to get variables or properties from the Tag Manager. Note: if you clear all variables in the config JSON variable on each 'page_view' event, you can also easily use this solution on Single Page Apps - see the demo script and validation events below for inspiration.
  • It is recommended to set the 'send_to' parameter to the GA4 Measurement ID, to prevent the event from being sent to other gtag instances.
  • It is possible to use the gtag('set', 'user_properties') command to set user properties, also after the gtag('config') command. Any properties set this way will have a user scope and can't be used as event parameters. The example script below also addresses this in a scalable way. See the GA4 documentation for more details.

In the two following sections you can find an example script and some example JavaScript commands you can run in the browser console to test the script.


Example GA4 gtag.js implementation script

              
(function () {
   /*
    * --- NOTES ---
    * sendEventGa4 is here set at the window object,
    * but you can also use tag management variables/properties 
    * to store variable values.
    * -------------
    * If you want to test this code, make sure to update the
    * GA4 measurement ID in the getMeasurementId function.
    */

   /* Helper function:
    * return the GA4 measurement ID.
    */
  const getMeasurementId = () => {
    /* update this value with your own account */
    return "G-XXXXXXXX";
  };

   /* Helper function:
    * validate if a variable is a JSON object
    * with at least one key value pair.
    */
  const checkForJsonObject = (object) => {
    return (
      typeof object === "object" &&
      !Array.isArray(object) &&
      Object.keys(object).length > 0
    );
  };

   /* Helper function:
    * validate if a variable is a string
    * with characters in it.
    */
  const checkForString = (string) => {
    return typeof string === "string" && string.length > 0;
  };

   /* Function to load the global site tag (gtag.js)
    * if it is not yet available on the window object.
    */
  const loadGtag = () => {
    if (typeof window.gtag === "undefined") {
      /* inject the script */
      const account = getMeasurementId();
      const script = document.createElement("script");
      script.setAttribute("async", "");
      script.src = 
        "https://www.googletagmanager.com/gtag/js?id=" 
        + account;
      document.querySelector("head").appendChild(script);

      /* init the dataLayer object and gtag function */
      window.dataLayer = window.dataLayer || [];
      window.gtag = function () {
        dataLayer.push(arguments);
      };
      window.gtag("js", new Date());
    }
  };

   /* Function that returns the event parameters
    * that should be included in the GA4 config object.
    * If you want to add a new config variable,
    * It should be added to this list.
    */
  const getConfigFieldNames = () => {
    const fieldNames = [
      "page_name",
      "page_site_section",
      "page_url",
      "page_theme",
      "allow_google_signals",
      "allow_ad_personalization_signals",
      "traffic_type",
      "debug_mode",
    ];
    return fieldNames;
  };

   /* Function that returns the event parameters
    * that should be set as user properties.
    * If you want to add a new user property field,
    * It should be added to this list.
    */
  const getUserPropertyFieldNames = () => {
    const fieldNames = [
      "customer_status",
      "customer_segment"
    ];
    return fieldNames;
  };

   /* Function that returns the GA4 config parameters
    * as a JSON object.
    */
  const initGa4 = (eventName) => {
     /* Load the gtag.js library if it is not 
      * yet initialized on the page. 
      */
    loadGtag();

    if (eventName === "page_view") {
       /* Initialize the ga4 object on the window object
        * this will also reset it on virtual page views (SPAs)
        */
      window.ga4ConfigObject = {};
    } else {
      /* use the existing values */
      window.ga4ConfigObject = window.ga4ConfigObject || {};
    }
  };

  window.sendEventGa4 = (eventName, eventData) => {
    if (!checkForString(eventName)) {
       /* Stop the function if the eventName is
        * not a string, or has no text in it
        */
      return;
    } else {
       /* Perform the GA4 initialization function,
        * set the cookie flags param
        * and disable page view tracking through the 
        * gtag('config', ...) command
        */
      initGa4(eventName);
      /* window.ga4ConfigObject.cookie_flags = 
         "path=/;secure;samesite=none"; 
         // disabled for local machine testing 
       */
      window.ga4ConfigObject.send_page_view = false;
    }

    /* define a JSON object that can hold all 
     * user properties to be set 
     */
    const userProperties = {};

    if (checkForJsonObject(eventData)) {
       /* If eventData is a JSON object
        * with at least one key value pair:
        * then check for each key if it is a GA4 
        * config field or a user property field.
        * If it is a config field, 
        * then add the key value pair to the 
        * window.ga4ConfigObject variable.
        * If it is a user property, 
        * add it to the user property variable 
        * and remove it from eventData.
        * IMPORTANT: if you would like to add 
        * config variable keys (that persist),
        * add them to the getConfigFieldNames function!!!
        * IMPORTANT: if you would like to add user properties, 
        * add them to the getUserPropertyFieldNames 
        * function!!!
        */
      const configFieldNames = getConfigFieldNames();
      const userPropertyKeys = getUserPropertyFieldNames();
      Object.keys(eventData).forEach((param) => {
        if (configFieldNames.indexOf(param) > -1) {
          window.ga4ConfigObject[param] = eventData[param];
        } else if (userPropertyKeys.indexOf(param) > -1) {
          userProperties[param] = eventData[param];
          delete eventData[param];
        }
      });
    }

    /* perform privacy fallback, 
     * whereby values are set to false if they are not set 
     */
    window.ga4ConfigObject.allow_ad_personalization_signals =
      window.ga4ConfigObject.allow_ad_personalization_signals 
      || false;
    window.ga4ConfigObject.allow_google_signals =
      window.ga4ConfigObject.allow_google_signals 
      || false;

     /* set the page title and location */
    window.ga4ConfigObject.page_title =
      window.ga4ConfigObject.page_name; 
      /* update to your own preferences */
    window.ga4ConfigObject.page_location =
      window.ga4ConfigObject.page_url; 
      /* update to your own preferences */

     /* assign the measurement ID to a variable and 
      * add the send_to parameter to the event data 
      */
    const account = getMeasurementId();
    eventData.send_to = account;

    /* perform the GA4 config */
    gtag("config", account, window.ga4ConfigObject);

    /* set user properties */
    gtag("set", "user_properties", userProperties);

    /* send the event */
    gtag("event", eventName, eventData);
  };
})();    
              
            

Validation events to run on the gtag.js example script

In the code block below, you can find a number of test events that you can run yourself, to see how the above gtag.js script behaves in different scenarios. If you want to walk through all the steps, it is recommended to also test some automatic collected events from enhanced measurement (e.g. scroll or click). If you use a browser that does not block Google Analytics, you can validate the data in the browser and in the GA4 debug view.

              
/* page view */
let eventName = "page_view";
let eventData = {
  page_name: "regular_page",
  page_site_section: "html",
  page_url: "https://michel.local/home",
  test_param: "test_value", 
  /* 'test_param' is an event parameter, 
   * so it does not persist */
  test_param2: "test_value2", 
  /* 'test_param2' is an event parameter, 
   * so it does not persist */
  debug_mode: true 
  /* enable debug mode */
};
window.sendEventGa4(eventName, eventData);

/* tutorial begin, incl. reset of page_site_section */
let eventName = "tutorial_begin";
let eventData = {
    element_content: "my first tutorial",
    element_type: "instruction",
    page_site_section: undefined,
    customer_status: 'prospect'
    /* 'customer_status' is a user property */
};
window.sendEventGa4(eventName, eventData);

/* theme change, in which a persistent config var is added */
let eventName = "switch_theme";
let eventData = {
  element_content: "switch to dark mode",
  element_type: "icon",
  page_theme: "dark" 
  /* 'page_theme' a config var and will 
   * persist for the next event(s) */
};
window.sendEventGa4(eventName, eventData);

/* tutorial end, in which the page_site_section 
 * and page_theme parameters can be validated */
let eventName = "tutorial_complete";
let eventData = {
    element_content: "my first tutorial",
    element_type: "instruction",
    customer_segment: "green"
    /* 'customer_segment' is a user property */
};
window.sendEventGa4(eventName, eventData);

/* page view 2 (virtual). 
 * Do not refresh te page to see 
 * that all config vars are reset 
 */
let eventName = "page_view";
let eventData = {
  page_name: "virtual_page",
  page_site_section: "spa",
  page_url: "https://michel.local/home#1",
  page_theme: "dark",
  debug_mode: true 
  /* enable debug mode */
};
window.sendEventGa4(eventName, eventData);

/* click */
let eventName = "click";
let eventData = {
  element_content: "my first button",
  element_type: "button"
};
window.sendEventGa4(eventName, eventData);

/* theme change */
let eventName = "switch_theme";
let eventData = {
  element_content: "switch to light mode",
  element_type: "icon",
  page_theme: "light" 
  /* change of value for the 
   * 'page_theme' config variable */
};
window.sendEventGa4(eventName, eventData);

/* test an automatic collected 'enhanced measurement' event, 
 * e.g. by scrolling down to the bottom of the page
 * or clicking an outbound link
 * if enabled in GA4 
 */

/* thumbs rating */
let eventName = "share_feedback";
let eventData = {
  element_content: "thumbs-up",
  element_type: "thumbs-rating"
};
window.sendEventGa4(eventName, eventData);

/* page view 3 (virtual).
 * Do not refresh te page to see 
 * that all config vars are reset 
 */
let eventName = "page_view";
let eventData = {
  page_name: "virtual_page_2",
  page_url: "https://michel.local/home#2",
  note: "you see: it works", 
  /* 'note' is an event parameter, 
   * so it does not persist */
  debug_mode: true 
  /* enable debug mode */
};
window.sendEventGa4(eventName, eventData);
              
            

As discussed in this guide the suggested default GA4 implementation does not offer much flexibility when it comes to updating config variables for all subsequent events on the page. However, with some minor tweaks you can implement GA4 in a more scalable way that offers this possibility.

In case you have questions or comments with regards to this article, feel free to reach out on LinkedIn

  • Was this article helpful?