Category: ga4

  • How to track AMP Pages with Google Analytics 4

    One of the most notorious misses on Google Analytics 4. Is the lack of AMP (Accelerated Mobile Pages) Pages tracking support. While this may not be an issue for many sites, there’re some website types that really need this support (like media sites or magazines).

    That’s why I decided to investigate the possibilities of <amp-analytics> Component and APIs in order to try to build an AMP Native Tracking without needing to draw on some tricky methods like using the infamous iframes.

    I’ve been testing everything I could and everything seems to be working fine. I’m open to receiving feedback from people that may end up trying this solution, which may not end up being perfect, but still is more than what we actually have.

    The good news is that we’ve got all the needed pieces of information available to perform a fully working tracking with Google Analytics 4, including the session tracking and the needed switched to have the, first_visit, session_start, user_engagment events generated, and as an unexpected extra we’ll be able to set event parameters and user properties within our events

    If you feel it you could buy me some coffees to support my work, this time I’m even hosting a copy of the file myself to ease the work (which depending on the traffic may lead to some costs for me.

    Issues / Errors Reporting

    Please report any issues/improvements on the project page on GITHUB


    What features are supported

    While I may have missed something, I tried to cover all the basics for the tracking and event at some point going beyond it as you’ll notice in the next lines.

    The current version does enable the page_view tracking (will fire on the page load if not specified in another way) along with any other event you may want to send.

    This is a list of all supported features:

    • Sessions Tracking
    • Session Engagements (&seg)
    • Sessions Count (&sct)
    • First Visits Tracking (&_fv)
    • Session Starts Tracking (&_ss)
    • AMP Cross-Domain
    • User Properties (number and string)
    • Event Parameters (number and string)
    • Engagement Time Tracking (&_et)
    • Screen Resolution
    • User’s Browser Language
    • Document Title
    • Document URL
    • Document Referrer
    • Unique Pageview Id (&_p)

    Setting up the tracking

    Tracking Snippet Component

    Using the Session Data in AMP is forbidden when using Remote Configurations, this is why we are using the googleanalytics as a base, which will at the same time help in using and managing the reading/writing of the _ga cookie.

    The only thing we need to do is copy the following code in our AMP pages in order to have our Google Analytics 4 tracking in place

    <amp-analytics type="googleanalytics" config="https://amp.analytics-debugger.com/ga4.json" data-credentials="include">
    <script type="application/json">
    {
        "vars": {
                    "GA4_MEASUREMENT_ID": "G-THYNGSTER",
                    "GA4_ENDPOINT_HOSTNAME": "www.google-analytics.com",
                    "DEFAULT_PAGEVIEW_ENABLED": true,    
                    "GOOGLE_CONSENT_ENABLED": false,
                    "WEBVITALS_TRACKING": false,
                    "PERFORMANCE_TIMING_TRACKING": false,
                    "SEND_DOUBLECLICK_BEACON": false
        }
    }
    </script>
    </amp-analytics>

    As you may have noticed there’re many configuration switches, we’ll later explain them all in a deeper way, but for now, the only one you should care about is the “GA4_MEASUREMENT_ID” one. You need to add your MEASUREMENT_ID in there.

    You can grab that value from Admin > Properties > Data Streams > Web and then selecting your Stream

    As a small sneak-peak, this is the meaning for all the configuration switches.

    Feature NameDescription
    GA4_MEASUREMENT_IDYour Measurement ID, G-XXXXXXXX
    GA4_ENDPOINT_HOSTNAMEOverride the default endpoint domain. In case you want to send the hits to your own server or a Server Side GTM Instance.
    GOOGLE_CONSENT_ENABLEDa &gcs parameter will be added to the payloads with the current Consent Status
    WEBVITALS_TRACKINGIf you enable this a webvitals event will fire 5 seconds after the page is visible
    PERFORMANCE_TIMING_TRACKINGWhatever you want to push a performance_timing event including the current page load performance timings
    DEFAULT_PAGEVIEW_ENABLEDIf enabled a page_view event will fire on the page load
    SEND_DOUBLECLICK_BEACONSend a DC Hit
    Configuration Settings

    Important Note

    I’ve put an online copy of the configuration file for those who can’t host themselves, if you can get some collaboration, I suggest you to download the ga4.json from GitHub and hosting it yourself.

    config="https://yourdomain.com/ga4.json"

    Server Side GA4 Tracking (SGTM)

    If you prefer it you can have AMP send the hits to a Server Side GTM instance. for doing this set the current

    GA4_ENDPOINT_HOSTNAME: "sgtm.thyngster.com"

    If you prefer sending a copy of the hits to some internal database or any other tool, the logic is pretty straightforward so I don’t think it needs any explanation just, set your domain there and be sure that you enable an endpoint on the following path “/g/collect

    Consent Compliance

    This Google Analytics 4 tracking solution for AMP pages, supports the integration with the consent module from AMP and also with Google Consent Mode ( allowing you to attach the consent details to the hits )

    Keep reading this section if you are interested in having a Consent compliance setup in your AMP Pages.
    Google Consent Activation

    You can have the hits to hold the information about the current consent status. This will make the tracking compatible with the Google Consent Features. If you turn on this feature the current consent status will be reported within the current event hit, allowing Google Analytics to be more GDRP Compliant (#sigh) and at the same time allowing you to make use of the consent mode modeling when it became available in the future.

    To activate this you need to set the “ENABLE_CONSENT_TRACKING” switch to true, and then a a &gcs parameter will be added to all the hits, containing the actual consent status for the browsing user.

    It will hold 2 different values

    ValueMeaning
    G100Analytics Consent Non-Granted
    G101Analytics Consent Granted

    In case you want to block the tracking unless the user has implicitly given his consent you can make GA4 not to fire any hits in your AMP pages. This can be easily achieved by adding the following attribute to our amp-analytics block,

    data-block-on-consent

    Now our main snippet will looks closely to this:

    <amp-analytics
        type="googleanalytics"
        config="https://amp.analytics-debugger.com/ga4.json"
       data-credentials="include"
       data-block-on-consent
    >

    Two things will happen when you add this, first one if that if the current user didn’t give his explicit consent to be tracked, no hits will be sent to Google Analytics and when the user accepts the consent, AMP will fire hits that were blocked on a first instance.

    Please note, that this functionality relies on the <amp-consent> component, you need to load the right library and also setup the consent modal in the way you want. A simple example would be something like:

    <script async custom-element="amp-consent" src="https://cdn.ampproject.org/v0/amp-consent-0.1.js"></script>
    <script async custom-element="amp-geo" src="https://cdn.ampproject.org/v0/amp-geo-0.1.js"></script>
    <amp-consent layout="nodisplay" id="consent-element">
      <script type="application/json">
        {
          "consentInstanceId": "my-consent",
          "consentRequired": "remote",
          "checkConsentHref": "https://example.com/api/check-consent",
          "promptUI": "consent-ui",
          "onUpdateHref": "https://example.com/update-consent"
        }
      </script>
      <div id="consent-ui">
        <button on="tap:consent-element.accept">Accept</button>
        <button on="tap:consent-element.reject">Reject</button>
        <button on="tap:consent-element.dismiss">Dismiss</button>
      </div>
    </amp-consent>

    Events Tracking

    Pageviews Tracking

    By default a page_view event that will fire on the page load unless you set the DEFAULT_PAGEVIEW_ENABLED to false. There may be a case where you want to personalize the page_view event name, or maybe you need to add some custom parameters to it.

    If that’s your case, set the default page view to false, and then add a new trigger to fire a page_view event are your own into the init config:

    "custom_pageview": {
        "enabled": true,
        "on": "visible"
        "request": "ga4Event",
        "vars": {
            "ga4_event_name": "my_customized_page_view"
        },
        "extraUrlParams": {
            "event__str_real_url": "https://www.charlesfarina.com/?origin=thyngster",
            "event__str_param_2": "meh"
        }
    }

    Custom Events

    Remember in GA4everything is an event“, well, I tried to set up a configuration logic that allows you to track many user interactions using the currently provided functionality in AMP API.

    This is cool because we’re going even be able to pass User Properties and Event Parameters to our events

    AMP Events Types

    By default no other events than the “page_view” are tracked. But you can use any of the AMP event types to track your users interactions.

    There are some more that are not currently properly documented on the main docs, so we’re not covering them. You can read the original docs here:

    clickWhen there’s a click on an element
    hiddenWhen the page becomes hidden
    ini-loadWhen the initial contents of an AMP element or AMP document have been loaded.
    render-startWhen the rendering of an embedded component has started (ie : ads iframes)
    scrollWhen under certain conditions when the page is scrolled
    timerWhen on a regular time interval
    video-*When there’s a video interaction
    visibleWhen an element becomes visible
    blurWhen a specified element is no longer in focus
    changeWhen a specified element undergoes a state change
    user-errorWhen an error occurs that is attributable to the author of the page or to software that is used in publishing the page

    Event: click

    "triggers": {
      "mailtos": {
        "on": "click",
        "selector": "a[href^='mailto:']",
        "request": "ga4Event",
        "vars": {
          "ga4_event_name": "outgoing_click"
        },
        "extraUrlParams": {
             "event__str_outgoing_click_type": "mailto"
        }       
      }
    }

    This is the main one that we’ll be using, it triggers when an element is clicked, and we’ll use a CSS selector to define the conditional firing.


    Another typical example would be tracking the external links, we could achieve this with the following trigger:
    "triggers": {
      "mailtos": {
        "on": "outgoing",
        "selector": "a[href]:not(:where([href^='#'],[href^='/']:not([href^='//']), [href='thyngster.com'], [href='analytics-debugger.com'])",
        "request": "ga4Event",
        "vars": {
          "ga4_event_name": "outgoing_click"
        },
        "extraUrlParams": {
             "event__str_outgoing_click_type": "link"
        }       
      }
    }

    Event: hidden

    We can set a trigger when the current page is hidden ( ie: minimized, or the browser’s tab is switched )
    If we include the visibilitySpec , we can define some rules, for example firing it only if it has been hiding for 3 seconds, see the example below

    "triggers": {
      "pageHidden": {
        "on": "hidden",
        "request": "ga4Event",
        "vars": {
          "ga4_event_name": "page_is_hidden"
        },
        "visibilitySpec": {
          "selector": "body",
          "visiblePercentageMin": 20,
          "totalTimeMin": 3000
        }
      }
    }

    I won’t dig more on this, you can check all the visibilitySpec options on the following URL: https://github.com/ampproject/amphtml/blob/main/extensions/amp-analytics/amp-analytics.md

    Event: ini-load

    We can have AMP triggering an event when an AMP Element initial content has been loaded, this is done using a CSS Selector, if the selector is not specified this event will be attached to the current document.

    "triggers": {
     "pageLoaded": {
        "on": "ini-load",
        "request": "ga4Event",
        "vars": {
            "ga4_event_name": "page_is_loaded"
         }   
      } 
    }

    Event: render-start

    AMP will trigger this event when an element that embeds other documents in iframes for example the Ads Elements

    "adsLoaded": {
        "on": "render-start",
        "request": "ga4Event",
        "vars": {
            "ga4_event_name": "ads_loaded"
        },
        "selector": "#ads"
      }
    }

    Event: scroll

    When a page is scrolled AMP will trigger the scroll event. This trigger provides special vars that indicate the boundaries that triggered a request to be sent. In order to filter which scroll events we want to fire we’ll use the scrollSpec object.

    We can use the ${verticalScrollBoundary} variable to grab the current scrolling boundary. Here it goes a simple example that will trigger an event when the user scrolls to a 25, 50, 75, 90 of the current page.

    "scrollTracking": {
        "on": "scroll",
        "request": "ga4Event",
        "vars": {
            "ga4_event_name": "scroll"
        },
        "extraUrlParams": {
            "event__str_percent_scrolled": "${verticalScrollBoundary}%"
        },
        "scrollSpec": {
            "verticalBoundaries": [25, 50, 75, 90],
            "horizontalBoundaries": [],
            "useInitialPageSize": false
        }
    }

    Event: timer

    As the name suggests this will allow us to send and event based on a regular time interval to GA4. We can also use  timerSpec to control when this will fire.

    Please note it’s important to know that the timer will keep triggering until maxTimerLength has been reached. Another thing that you need to have in mind is that we can use startSpec to define then this trigger should fire. For example we may want to send a ping event each minute the page is active, but we want to step in if the document is hidden, we could do the following

    "triggers": {
      "pingEvents": {
        "on": "timer",
        "request": "ga4Event",
        "vars": {
            "ga4_event_name": "ping"
        },
        "timerSpec": {
          "interval": 60,
          "startSpec": {
            "on": "visible",
            "selector": ":root"
          },
          "stopSpec": {
            "on": "hidden",
            "selector": ":root"
          }
        },
      }
    }

    Refer to the AMP docs for full details about how to use the timer events.

    Event: video-*

    I feel this is the most complicated trigger available on AMP. It will allow us to track the video interactions happening on our site in Google Analytics 4.

    Multiple video providers are supported by AMP Analytics, these are defined in the following table:

    Video ProviderSupport level
    <amp-video>Full support
    <amp-3q-player>Partial support
    <amp-brid-player>Partial support
    <amp-brightcove>Full support
    <amp-dailymotion>Partial support
    <amp-ima-video>Partial support
    <amp-nexxtv-player>Partial support
    <amp-ooyala-player>Partial support
    <amp-youtube>Partial support
    <amp-viqeo-player>Full support
    Source: https://github.com/ampproject/amphtml/blob/main/extensions/amp-analytics/amp-video-analytics.md

    Partial support means that not all the variables may be available:

    VarTypeDescription
    autoplayBooleanIndicates whether the video began as an autoplay video.
    currentTimeNumberSpecifies the current playback time (in seconds) at the time of trigger.
    durationNumberSpecifies the total duration of the video (in seconds).
    heightNumberSpecifies the height of the video (in px).
    idStringSpecifies the ID of the video element.
    playedTotalNumberSpecifies the total amount of time the user has watched the video.
    stateStringIndicates the state, which can be one “playing_auto”, “playing_manual”, or “paused”.
    widthNumberSpecifies the width of video (in px).
    playedRangesJsonStringRepresents segments of time the user has watched the video (in JSON format). For example, [[1, 10], [5, 20]]
    Source: https://github.com/ampproject/amphtml/blob/main/extensions/amp-analytics/amp-video-analytics.md

    For now, we know which video players are supported and which data we will be able to use in our events, know it’s time to know which triggers/interactions we’ll be able to track, these are:

    Trigger NameDescription
    video-playVideo stats to play
    video-pauseVideo is paused
    video-endedVideo Completes (reach end of playback)
    video-sessionTriggers when a “video session” has ended. a video session starts when a video is played and finishes when the video is paused, ends, or became invisible
    video-seconds-playedThis will trigger each time the defined amount of time is played ( ie: every 10 seconds watched )
    video-percentage-playedSame as above we can define which % of the progress we want to trigger this
    video-ad-startVideo Ad Starts
    video-ad-endVideo Ads Ends

    As you can see, the possibilities are almost endless, so we won’t be adding examples for all of them, you can find some good examples on AMP documentation. In any case, let’s see one example.

    Let’s say that our AMP page has a YouTube Video embedded:

    <amp-youtube
        id="Take off - LGA-YYZ . DL4942 - CRJ-900"
        class="video"
        width="480"
        height="270"    
        data-videoid="Nx-JZ2-kEKU">
    </amp-youtube>

    #TIP Using the element “id” with the video name will allow us to use it for the video_title parameter for our event

    So, for tracking the video plays, we would add the following trigger:

    "triggers": {
        "videoPlayEvent": {
            "on": "video-play",
            "request": "ga4Event",
            "vars": {
                "ga4_event_name": "video_played"
            },
            "selector": ".video",
            "videoSpec": {},
            "extraUrlParams": {
                "event__str_video_title": "${id}",
                "event__num_video_duration": "${duration}"
            }
        }
    }

    Event: visible

    Using this event trigger we’ll be able to fire an event when the current element(s) defined with our CSS selector are visible within the current browser viewport.

    This trigger firing can be fine-tuned using the visibilitySpec , to define the amount of millisecond the element has to be on the screen, or what % of the element needs to be visible to trigger the event.

    Refer to the AMP official docs for all the configuration options 🙂

    "triggers": {
        "recomendationsViewed": {
            "on": "visible",
            "request": "ga4Event",
            "vars": {
                "ga4_event_name": "recomendations_block_viewed"
            },
            "selector": "#adobeTargetRecos",
            "visibilitySpec": {
                "waitFor": "ini-load",
                "reportWhen": "documentExit",
                "visiblePercentageMin": 20,
                "totalTimeMin": 500,
                "continuousTimeMin": 200
            }
        }
      }
    }

    In-Build Auto-Generated Events

    There is some data provided by AMP that is available and that can provide some cool events, in this first release I’ve added two events to track our page loading speed.

    WebVitals

    You can enable web vitals tracking events, which we expect to be the best ones on AMP ( don’t we’? )

    To enable this event just set the “ENABLE_WEBVITALS_TRACKING” to true in the main snippet settings, and that will make the tool launch an automatic event (web_vitals) 5 seconds after the page load, with the following available parameters:

    ParameterDescriptionExample Value
    epn.first_contentful_paintFirst Contentful Paint170.199951171875
    epn.first_viewport_readyFirst Viewport Ready164.59999990463257
    epn.make_body_visibleMake Body Visible163.40000009536743
    epn.largest_contenful_paintLargest Contentful Paint170.299072265625
    epn.cumulative_layout_shiftCumulative Layout Shift0.012389976353126643

    Performance Timing

    This is the same data that we are used to seeing in the Site Speed Reports in Universal Analytics, which includes the time (in ms) to the DomReady event or the time it took to do the DNS Resolution Time.

    To enable this event just set the PERFORMANCE_TIMING_TRACKING to true. Then on the page load, a performance_timing event will be fired containing the following parameters.

    epn.page_load_timeAmount of time (in seconds) it took that page to load
    epn.domain_lookup_timeThe average time (in seconds) spent in DNS lookup for this page
    epn.tcp_connect_timeProvides the time it took for HTTP connection to be set up. The duration includes connection handshake time and SOCKS authentication. The value is in milliseconds.
    epn.redirect_timeTime taken to complete all the redirects before the request for the current page is made (in ms)
    epn.server_response_timeTotal time taken by the server to start sending the response after it starts receiving the request (in ms)
    epn.page_download_timeProvides the time taken to load the whole page. The value is calculated from the time unload event handler on previous page ends to the time load event for the current page is fired. If there is no previous page, the duration starts from the time the user agent is ready to fetch the document using an HTTP request (in ms)
    epn.content_download_timeProvides the time the page takes to fire the DOMContentLoaded event from the time the previous page is unloaded (in ms)
    epn.dom_interactive_timeProvides the time the page to become interactive from the time the previous page is unloaded (in ms)

    User Properties

    To attach User Properties to our hits, we need to use the extraUrlParams key. We also need to have in mind that a User Property in GA4 can either be a number or a string and while the GTAG/GTM does take care of accordingly casting the values, on AMP we need to define the type.

    The way we should define the parameters is this:

    "user__str_user_id": "123456",    
    "user__num_lifetime_value": "147.34"

    As you can see is easy, the first part defines the current scope (which will be event for the Event Parameters in the next section ).

    If we add the User Properties to our main snippet, these will be added to all the subsequent events fired within the current page load. In the next example, we are setting 3 User Parameters in our init code snippet that will be persisted across all the events pushed during the current page load.,

    {
            "vars": {
                "GA4_MEASUREMENT_ID": "G-THYNGSTER",
                "ENABLE_CONSENT_TRACKING": false,
                "ENABLE_WEBVITALS_TRACKING": true
            },
            "extraUrlParams": {
                "user__str_user_id": "123456",
                "user__str_logged_in": "yes",
                "user__num_lifetime_value": "147.34"
            },
        }

    On the other site, we can attach some User Parameters ONLY to the current event, this is done by adding the extraUlrParameters to the current trigger.

            "triggers": {
                "demoClickEvent": {
                    "on": "click",
                    "request": "ga4Event",
                    "selector": "#upgradeMembership",
                    "vars": {
                        "ga4_event_name": "member_ship_upgraded",
                    },
                    "extraUrlParams": {
                        "user__str_last_membership": "premium"
                    }
                }
            }

    Event Parameters

    Same way as the User Properties above, we have two different ways of setting an Event Parameter, to all the current page events or just to the current event, this will be done, again, on the main init snippet, or adding it within the current trigger extraUrlParams.

    And of course, remember that they need to define the type, string, or number.

    The way we should define the parameter is the same as we did in the User Properties.

    "event__str_user_id": "123456",    
    "event__num_lifetime_value": "147.34"
  • Google Analytics 4 (GA4) Events Demystified

    At his point, many ( if not all ) have heard Google Analytics is moving to an “events” based tracking model with Google Analytics 4. But, what does it really imply? Do we have to worry about it?. To be honest, it’s not a big ( from the implementation side ) deal since we have been already using “events” all the time, we used to call them hit types. If we look at it from the reporting side it may lead to some “hard times” when trying to use the data, not because it’s better or worse, just because it’s different.

    This post will try to explain Google Analytics 4 Events from the technical perspective, trying to explain how to current event model works, where can the events come from, the limitations, etc.

    I’d say that one of the most important things when working with GA4, is realizing how important is going to be the data model definition we do at the start. Because this is going to condition the future of our implementation and data.

    But don’t worry about this for now. we’ll dig into this across the post 😊.

    How does Google Analytics 4 record the data

    Google Analytics 4 works much similarly to Universal Analytics.

    We’ll be sending hits (network requests) to a specific endpoint ( https://endpoint.url/collect ). This shouldn’t be anything new for anyone, that’s how all analytics tools and pixels work. And this is the way it works for the client-side tracking (gtag.js), server-side tracking ( measurement protocol ), and the app tracking ( Firebase Analytics SDK ).

    Tracking endpoints

    I found there are 5 different endpoints that we could use to send the data to Google Analytics 4, these are:

    • https://www.google-analytics.com/g/collect
    • https://analytics.google.com/g/collect
    • https://custom.domain/g/collect (this will really forward the hits to the first one on this list)
    • https://app-measurement.com
    • https://www.google-analytics.com/mp/collect

    Depending on where we are doing the tracking we’ll be using one of them.

    We could see hits flowing to 4 different endpoints for GA4 + 1 for Firebase

    The first two endpoints are the ones used by the client-side tracking but you may wonder why sometimes we see the hits coming through analytics.google.com, and some other times via the google-analytics.com domain. The reason is that if current GA4 property has “Enable Google signals data collection info” turned on, GA4 will use the *.google.com endpoint ( si Google would be able to use their cookies to identify the users, I guess )

    JavaScript Client Library

    The page tracking is done using a library provided by Google, the same way we used to have analytics.js , ga.js or urchin.js libraries in the past Google Analytics versions.

    The default code snippet will look like this:

    <!-- Global site tag (gtag.js) - Google Analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-THYNGSTER"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
    
      gtag('config', 'G-THYNGSTER');
    </script>

    If you have noticed it the snippet loads a JavaScript file from www.googletagmanager.com domain, and this is because all gtag.js snippets are in essence a predefined Google Tag Manager template. It’s not just a plain GTM container, since it does some internal stuff, but it works also based on tags, triggers, and variables.

    Previous tracking libraries were offering a public API to perform all the tracking at our end, ie: it was accepting some methods/calls and converting them to hits, doing the cross-domain tracking allowing us to use Tasks, while at the same time doing some logic for generating the cookies, reading the browser details, and this library was shared across all the users worldwide web.

    This is no longer working this way, now each Data Stream / Measurement ID will have its own snippet and it will load a separate js file. We may look at this as a performance penalty but it’s done this way for a reason.

    Each gtag.js container it’s now built dynamically at Google’s end and contains personalized code for the current property and also holds the settings for the current data Data Stream / Measurement ID. And that’s why the container sizes are different for each container we check. Don’t worry, this is normal and expected. The container size will vary depending on many things, like if we have the Enhanced measurement features we have enabled or the current settings we defined on the admin interface for our property.

    GA4 Containers Sizes

    One thing that has been confusing me since Google Analytics 4 arrived, was thinking that there were lots of things happening on the back that were hardly possible to debug, like the conversions, or the created / modified events.

    And well, that’s not the way it works, almost any setting or feature you enable on the admin it’s going to be translated into code and will be executed on the client-side. This means that when you add a new event on the interface that’s will add some code on the gtag.js container will send an event, and this will make that you “may” end seeing “ghost” events on the browser, don’t waste your time as me trying to see why your implementation was firing duplicated events :). Or for example when we define a conversion event when we configure our internal domains or the ignored referrals.

    While this approach may help some people in doing some common tracking tasks, on the other side it’s preventing to do some advanced implementation because some “loved” features like the “customTasks” are now missing. I’m ok with Google trying to control how things are done, but there will always be sites that will need custom /U personalized implementations, and I really feel that Google should provide some public/documented API methods to easily perform some of the most used common tasks like the cross-domain tracking in Google Analytics 4.

    Let’s see some examples, when you “create a new event” from the Admin Interface, this event won’t be created server-side, what’ is happening is that GA4 will add some code logic to send that hit client-side.

    Google Analytics 4 events creation modal

    Another example would be when you enable the Enhanced Measurement, this will turn on having some code added to your container. Remember that we mentioned that GA4 was in essence a Google Tag Manager container?, if you take a look at the current Measuring categories you’ll notice how they all match the current triggers available on GTM ( clicks tracking, scrolls tracking, youtube tracking )

    Enhanced measurement

    And that’s not all, when we change the session duration or the engagement time, some session_timeout variables will be updated internally (engagementSeconds, sessionMinutes, sessionHours)

    Session Timeout Adjust

    We could keep going on examples, or build a full list, but that’s likely going to get outdates sooner than later. The main idea you need to get from this part of the post is that GTAG is like a “predefined” GTM template and that all the tracking happens on the client’s browser.

    Firebase Analytics SDK

    Apps are usually tracked using the Firebase Analytics SDK . A good starting point would be visiting the following Url: https://firebase.google.com/docs/analytics/get-started?platform=android&hl=en

    The apps hits will use their own endpoint and format, the hits will go to https://app-measurement.com and the current payload will be sent in binary format, which makes it really difficult to debug, event if using Charles, Fiddles, or any other MITM proxy app.

    If you want to debug your Firebase implementation. I recommend you use my Android Debugger for Windows. Once you install the app, you’ll be able to request a free lifetime license.

    Android Debugger Splash Screen

    Google Analytics 4 Measurement Protocol

    Google Analytics finally offers a proper Measurement “Protocol“, which is at the time of writing this post it’s in Beta stage.

    This protocol will use the https://www.google-analytics.com/mp/collect endpoint, and rather than having the developers build the request payloads using some non-intuitive keys, now it accepts a POST request with a JSON string attached to the body using application/json Content-Type:

    fetch('https://www.google-analytics.com/mp/collect?measurement_id=G-THYNGSTER&api_secret=12zneF6DSDFSDFjJPgDAzzQ', {
      method: "POST",
      headers: {
         'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        "client_id": "12345678.87654321",
        "user_id": "RandomUserIdHash",
        "events": [{
          "name": "follow_me_at_twitter",
          "params": {
            "twitter_handle": "@thyng",
            "value": 7.77,
        },{
          "name": "follow_intent",
          "params": {
            "status": "success"
        }]
      })
    });
    KeyType
    client_idstrRequired. 
    user_idstrOptional.
    timestamp_microsintOptional. Hit offset. Up to 3 days ( 2,592e+11 microseconds ) before the current property’s defined timezone.
    user_properties{}Optional.
    non_personalized_adsboolOptional. ( whatever use this event for ads personalization )
    event[][]Required. ( Max 25 Events per request )
    event[].namestrRequired. 
    events[].params{}Optional.

    In any case, there are some things you need to have in mind, you should keep your API Secret not exposed, meaning that this endpoint should not be used client-side, because that would mean that your API Secret would need to be exposed. This endpoint is more likely to be used to track offline interactions, ( like refunds ), or for tracking our transactions server-side.

    At the time of writing this post ( Apr 2022 ), one of the biggest handicaps of this protocol is that it doesn’t support any sessionId parameter, meaning that you won’t be able to stitch the current server-side hits to the client-side session. This should be fixed over the next months,

    In the meanwhile, I’ve published a the GA4 Payload Parameters CheatSheet, which you could use to send some server-side hits in the old-school way ( like we used to do with the first Measurement Protocol for Universal Analytics ) and where you could attach the “&sid” parameter.

    There are of course some other points to have in mind, like that GA4 has some reserved event and parameters names, that you should not be using. We’ll cover this later in the “events” section.

    Events Model / Hit Types

    Let’s start by saying that everything on Google Analytics 4 is an “event“. I’m sure that it’s not the first time you hear that, and it’s totally right, but at the same time if we strictly look to Universal Analytics we were also sending “events“, but then we used to call them “hit types“.

    In a technical meaning, nothing has changed at all. We have networks requests to some endpoints. That is it!. If you want to learn a bit more about how the hits are built or sent from the web tracking library you can take a look at GA4: Google Analytics Measurement Protocol version 2 post to learn a bit more about how it works.

    The main difference on GA4 is that now Google does not offer a fixed tracking data model besides the page_views and the e-commerce. Meaning that the responsibility for building a proper data model falls on us. While working on our definition we need to have in mind that there are some predefined/reserved event and parameters names and that we have some limits we need to have in count (About total events, names, and values lengths).

    Universal Analytics Hit Types Model

    If we take a closer look, since Urchin times we’ve been using “events” for our tracking in Google Analytics. Yep, I’m not joking, we had, we just called them “hit types“.

    Just so you know, we could replicate the current Universal Analytics Data Model in Google Analytics 4 following the next table of events:

    Hit Type / EventParameters
    pageview– Location
    – Path
    – Title
    event– Category
    – Action
    – Label
    – Value
    – Non Interaction
    timing– Category
    – Variable
    – Label
    – Value
    social– Network
    – Action
    – Opt. Target
    exception– Description
    – Fatal
    screenview– Screen Name
    transaction ( Legacy Ecommerce )– Id
    – Affiliation
    – Revenue
    – Tax
    – Shipping
    – Coupon
    item ( Legacy Ecommerce )– Id
    – Name
    – Brand
    – Category
    – Variant
    – Price
    – Quantity

    Even Google offers a setting that will automatically convert all your ga() calls to some predefined events on GA4. From your Data Stream configuration you can enable this feature and all events, timing, and exception events will be converted to GA4 events ( they will add a listener to the ga('sent', 'event|exception|timing') calls for doing this,

    This tool wil map the data in the following way:

    Event NameParameters
    [event_name]This will take the current eventAction
    eventCategory > event_category 
    eventAction > event
    eventLabel > event_label
    eventValue > value
    timing_completetimingCategory > event_category
    timingLabel > event_label
    timingValue > value
    timingVar > name
    exceptionexDescription > description 
    exFatal > fatal 

    Beware because since its converting all Event Actions on “events“, depending on your current de events definition on Universal Analytics you have end up hitting the unique event names limit (500)

    Google Analytics 4 Events

    Event Sources

    The events on Google Analytics 4 can come from 4 different sources. These are:

    • Public Web/App endpoint.
    • Measurement Protocol ( Server Side )
    • Internal self-generated events
    • Admin defined events

    Public Web Endpoint

    The main actual origin for GA4 events we’ve already talked about them. These are the event that is being generated on our site coming from the GTAG.js container ( Check the GA4 Payload Parameters CheatSheet here ).

    Measurement Protocol ( Server Side )

    Another source for our events is the measurement protocol. This works similarly to the public endpoint. but the hits would be sent via server-side and we’ll need to use an API Secret within our requests.

    Internal self-generated Events

    This one can be a bit confusing, GA4 auto-generates some of the events we see in the reports. This means that we see some events in our reports that won’t be seen in our browser.

    This doesn’t mean that they’re being generated randomly or using some server-side logic. Most ( if not all ) of these events are created because a parameter was added to some event.

    Our events payloads may have some extra parameters attached to them sometimes that will make GA4 internally spawn a separate event. As far as I’ve been able to identify this is the list of the internally generated events and what’s the parameter that will trigger them.

    Event NameTrigger
    session_start&_ss
    first_visit&_fv
    user_engagement&seg

    For example, if the current event payload contains a &_ss parameter, a session_start will be generated, if it contains a $_fv then we should be able to see a first_visit events and so on. This list may grow in the future (and it may be missing some events that I’ve not been able to spot yet)

    If we’ve enabled the Enhanced Measurement, we may also see some events in our reports ( this time this event will be visible without the browser requests ), these are:

    Event NameParameters
    clicklink_id
    link_classes
    link_url
    link_domain
    outbound
    file_downloadlink_id
    link_text
    link_url
    file_name
    file_extension
    video_play
    video_pause
    video_seek
    video_buffering
    video_progress
    video_complete
    video_url
    video_title
    video_provider
    video_current_time
    video_duration
    video_percent
    visible
    view_search_resultssearch_term
    scrollpercent_scrolled
    page_viewpage_referrer ( URL and Title are Shared Parameters )


    On the other side, when working with the Firebase Analytics SDK, this one will automatically track a lot of events, without us needing to explicitly define them.

    Here is the current list of autogenerated event names by Firebase:

    ad_activeviewAPP
    ad_clickAPP
    ad_exposureAPP
    ad_impressionAPP
    ad_queryAPP
    adunit_exposureAPP
    app_clear_dataAPP
    app_installAPP
    app_updateAPP
    app_removeAPP
    errorAPP
    first_openAPP
    in_app_purchaseAPP
    notification_dismissAPP
    notification_foregroundAPP
    notification_openAPP
    notification_receiveAPP
    os_updateAPP
    screen_viewAPP
    user_engagementAPP,
    Note: These events will not count towards the unique events name limit

    Admin defined events

    We’ve already talked about these ones, when we create or modify an event within the admin section, these settings will be translated to the client-side tracking.

    This means the following:

    • You may see events being fired on the browser that you didn’t define on Google Tag Manager or GTAG. This is normal, don’t go crazy with it. If you see a duplicate event or a new event that you don’t know where it’s coming from take a look at the Data Stream Settings
    • You may have some unexpected parameters or event names if a “modify” rule is being used.

    Events Limitations

    Google Analytics 4 is full of limitations in many aspects, and it makes it a bit difficult to understand all of them, even more, when the limits keep constantly changing.

    We have limits for event names and values length, same for the event parameters and the user properties. At the same time, we have a limit on how many parameters and properties we can append to each event. And these limits may vary between the free and 360 versions.

    There are also, some exporting limitations (The free version it’s capped to 1M daily hit export to Big Query ) or the data retention settings wherein the free version will top at 14 months while the 360 will allow to hold up to 50 months on data.

    But this is not all the limits we’ll have … we will also have limits for the total conversions, audiences, insights, and funnels we can set. This is not directly related to the events, so if you’re interested you can visit the official Configuration Limits Information.

    Collecting and Names Limitations

    We can attach up to 25 event parameters ( 100 on GA4 360 ) to each event, and we can identify these values in our hits easily these are the ones starting with “^ep(|n).*“. Event Parameters are meant to add some metadata to our events.

    ep.event_origin: gtag

    Each of these parameters should have a name no longer than 40 characters and a value not bigger than 100 characters.

    At the same type, we have the “user properties“, We can attach up to 25 user properties to each hit these are attributes that will describe segments for our users. For example, we could think about recording the current user newsletter sign-up status, or the total purchases made by the current user. We can identify his data in our hits because they will start with “^up(|n).*“,

    up.newsletter_opt_in: yes
    upn.user_total_purchases: 43

    Each of these properties should have a name no longer than 24 characters and a value not bigger than 36 characters.

    Logged itemLimitFree360
    EventsEvent Name 40 chars
    Event parameter Name40 chars
    Event parameter Value100 chars
    Params per event25100
    User propertiesTotal per Property25
    Property Name24 chars
    Property Value36 chars
    User-ID256 characters
    Custom dimensionsEvent Scope50125
    Item Scope10
    User Scope25100
    Custom MetricsEvent Scope50125
    Events Offset3 days
    Full Limits Table

    Event Values Typing

    You may have noticed that some of the parameters start may start with up, ep, upn, epn . This is because an event parameter/user property can be either a string or a number, the good news is that we don’t need to define them since they’re automatically typed by GA4. Just take a look at the logic it’s used to define if a parameter is a string or a number.

    var value = 'something';
    if(typeof(value) === "number" && !isNaN(value)){
        console.log("is a number parameter");   
    }else{
        console.log("is a string parameter");
    }

    SGTM – Google Analytics 4 Hits

    The last thing I want to shout out is that GA4 hits sent via Server Side Google Tag Manager, are able of doing two things that we won’t see on the regular hits.

    First of these is that the hits sent server-side are able to set first-party cookies on the user browser, this is achieved using a Cookie-set header to the request:

    And the last one is that they may contain a response body, this is used to send back some pixels client-side. ie: SGTM builds up a pixel request and gets it back to the browser so it gets sent if for example, it was missing some third party cookie value (where sending it via server-side won’t be making any difference )

    More Questions

    How can I identify a conversion?

    If the current event has a &_c=1 parameter it will be counted as a conversion

    Are there any e-commerce limits?

    Yes, they’re, as far I’ve been able to deduct from the code.

    • A max of 200 items can be sent within a single event, any item above them will be skipped
    • A Max of 10 items scopes parameters, any parameter above this limit will be removed from the item

    It takes some seconds to see my hits

    Google Analytics 4, can delay up to 5 seconds the hits firing. This is because it uses an internal queue in order to batch the event and save some hits requests. At this time there is no way to “force” the queue dispatch, and there’re some situations where the queue is skipped and the events are sent right way. This is for example the first a visitor comes to your site (ie: when there’s no cookie present).

    Why can’t I use any of my parameters on the reports?

    You can send ANY parameters along with your events, but this doesn’t mean that you’ll be able to use them on your exploring reports. This can be confusing because while you’ll see the parameters on the Real-Time reports, you’ll need to set up them as dimensions on the admin in order to be able to use them. If you think about it, it makes sense, the real-time report is just some streaming report where no data is being parsed/processed at all, and we can not expect GA4 to process all the data coming with the events, so it will only process the parameters that we’ve configured. We need to setup then in the Custom Definitions section

    I’ve set-up my dimensions, but they show no data

    I’m not if this is only me, but it drove me crazy sometimes. I’d say that if you add a new event with some parameters and then you directly go to adding in the admin, they won’t show up, but you’ll be able to type the parameter name manually. All times I did this, I was not getting info for that dimension. My advice is to wait some hours before the custom definition and only do it if the dimension is being shown for being selected. ( rather than manually typing it ). If you did it wrong, the only solution that worked for me was archiving the dimension and re-creating it.