Hi, tomorrow 1st of July is the day when Universal Analytics is being sunsetted for all free account users, 360 accounts will enjoy of an extra year for dealing with the full migration .
On 26th June, Google luckily announced the official Google Analytics 4 support for AMP pages. Which makes me really happy since in some time from now I won’t need to worried to keep my solution and CDN servers online, (sorry it feels like a lot of responsability to keep something for almost 1B unique users and +3B monthly hits, and also there will be some more money in my pocket )
I’ve been following the coments on social networks and forums, and it seems there’s a lot of people that were waiting for some AMP/GTM support. But as now 24h to the deadline, there’s no a template for GTM AMP containers.
I worked on this the past summer, but I didn’t publish it since not having a template on GTM wasn’t making much sense, in any case, I feel this may end help some people (or not…). But I’m showing you how you can do the GA4 tracking on AMP using GTM.
First of all we need to modify our main <amp-analytics> tag to include the type="googleanalytics" , this is needed for having access to the session information variables.
Oki doki, we have the first step, now, current GTM containers for AMP doesn’t have the predefined variables to build a proper Google Analytics 4 hit, for example it’s missing the session_id, session_count, client_hits, and some other details. And here’s were we’re going to make use of the current AMP variables to accordingly calculate this information so we can later or using it in our hits. For this we need to add some vars to our Google Tag Manager configuration, I took care of building everything you, so you just need to copy and paste. ( just change the GTM container ID )
How are we going for now?, easy, isn’t it?. At this point, we have all the necessary data points to properly build a Google Analytics 4 hit payload.
Now we just need to create some Custom Image tags in GTM for our page_views and events . Just to keep making your life easy, I’m attaching the full main payload you should be using in ALL of our hits. Yuu need to take this and then add your custom details based on the data you want to sent to GA4.
At this point i still advice you to update your setup to GTAG tag type on AMP, since it’s not clear at this point if GTM for AMP will be supported in the future, hopefully there will be some announements about this at some point.
It took almost 9 months, but it’s finally here. The new Analytics Debugger Extension (Formerly GTM/GA Debugger) hit the 2.0.0 milestone.
It’s been a full rewrite of the code, mainly to make it fully compliance with the new MV3, but at the same time it’s internal functionality has been improved. Now all the components are loaded asyncronouly, making the extension to use less resources and working fast, and it also make much more easier to add new features.
The UI has been kept faithful to the original one, but there’re a lot of slight differences that make the reports more clean :). The report that has got the most changes ir the Google Analytics 4 one.
While debugging your Google Analytics 4 hits/events, you will be able to:
See the hit event batches The server-side generated events (session_start, first_visit, etc ) On the main report you’ll see the curretn event engagement time, if it’s counting as a conversion, if it has ecommerce.
See the events batches grouped but the holding hit request
The server-side generated events (session_start, first_visit, etc )
On the main report you’ll see the curretn event engagement time, if it’s counting as a conversion, if it has ecommerce.
The current used endpoint
If it’s a sent to a SGTM server
The SGTM response and header ( pixels, and cookies set server side )
All the data is presented on a friendly way ( not only the parameters keys )
Still you can see the raw payload details
The current hit consent mode
The session Id and Sessions coount
If Google Signals is being used.
You can filter the events by the Measurement ID or the event name
Event parameters and User properties
And some much more new features coming in the future stay tuned.
Real Time Notifications
The new version, is able to show real-time news/notifications about the current viewed vendor. This will alow me to notify users about service outages or breaking changes on the tools. Of course all this is not automated and I’ll need to be up to date with the news in order to have them showing up on time.
Amplitude Support
Another big news, it now supports Amplitude. It support seeing the current hit batches , as usual in a really friendly manner. Clicking the project ID will show up the current tracker/project configuration
Matomo/Piwik Support
I also added support for Matomo/Piwik. This report will allow you to see the hits coming in real time, it support the pings, link cliks, purchases. And you can even parse the current hits payload to exactly now what does each payload key means.
IU Updates
I really took some time to make the tool configurable, just because not everyone has the same needs. Now you can define which tools to show in your debugging session, and in which order you want the tabs to be shown on the reports:
The internalization has finally arrived, the extension is now available in English , Spanish and Japanese (kind). Which some more languages coming in the future.
You can choose if the hit payloads are shown in the way they are send or if you want the payload key sorted out alphabetically
You can choose the current deep level you want the object to be opened, some people just want the first level to be opened by default, other want to see a deeper level
Finally you may like to see more data in your screen, or you may use a 4K monitor that makes all very small, you can know setup the zoom level to make your debugging sessions more confortable.
Support / Bug Reports / Features Proposals
I’ve opened a Github repository, to track the bugs, you can find it here:
Please have in mind I rewrite it from scratch, so while I feel it’s more accurate that the previous version, some people may hit bugs on some sites. Just reach me so I can work on them.
Some more vendors are coming in the next months, just to mention some of them
Adobe Analytics
Adobe Launch
Adobe Target
Tealium IQ
Yandex Metrika
Chartbeat
And some more debugging features are also coming that will make some of the most complicated debugging tasks a breeze.
It’s been half a year since I released my Android Debugger during the Super Week Punchcard Prize contest. Today it’s a big day for the family, and most logic next step on this travel, I’m adding support for iOS devices.
This tool is offered “as is” and for free. While you won’t have to pay for it, you’ll be getting some nag screens to invite you to support the development, either grabbing me a coffee or even better becoming a monthly sponsor (๐).
The Android version supports debugging your Firebase ( GA4 ), Universal Analytics ( GA3 ) and Google Tag Manager ( basic data ) implementations out of the box for any app on your device. There’s no need for modifying the manifest file, there’s no need to install any certificates, it just works.
It supports USB and Wireless debugging sessions ( meaning that you won’t need to mess around with the USB Drivers or anything else )
iOS
The iOS version works in a totally different way and it uses a proxy to intercept the requests being sent by the device.
It currently supports debugging features for Firebase( GA4 ), Universal Analytics ( GA3 ) and Adobe Analytics ( sorry this is not currently available for Android )
Features
Firebase ( GA4 )
You’ll be able to see the current events being sent by your app in a very nicely and intuitive way. It event reports the current batches, if hits contains e-commerce details, if the current event is a conversion, the engagment times, all this data is available within the main report.
Click on an event will give you all the details about the current hit payload, including event the internal data used by firebase in addition to the event name, event parameters and user properties.
iOS/Android platforms has been synched and the data is reported with the name key names, making even easier to do a cross-platform audits.
Lastly you’ll be able to copy the events details to the clipboard within a single click.
Universal Analytics ( GA3 )
I know, this is legacy stuff, and we’ll should have moved already to Firebase, but that’s not always the case, so we’re keeping this for these who still need to keep an eye on their Universal Analytics implementations.
This report will show the hits coming from the device to any Universal Analytics Properties in a friendly way, allowing you to copy these hits to the clipboard.
Google Tag Manager ( only Android )
This report will show you the current containers, events, and variables being evaluated on your device, since this report relies on the Google Play Services logging service it only works for Android devices.
While it’s not a full report, it may give you an overall idea of what is happening on Google Tag Manager on your device.
Adobe Analytics ( only for iOS )
Since I was not able to offer GTM support, and I wanted to play around with some other tools, I’ve added support for debugging Adobe Analytics implementations.
This will reports the current Adobe Analytics hits being sent by your devices and will show all the detail in a friendly format. It will report the current event/page name, the current account, and the endpoint being used, along with full payload details.
As in the other tools, you’ll be able to copy the hits information to your clipboard.
Splash Screen
I noticed that I was using a background image for the splash screen and since I was not able to find the author or if it had any copyright. I’ve asked my friend @kroniksan to make some AI generated backgrounds for me, and they look awesome.
Back in time, I remember relying on jQuery for all the custom event tracking on my client websites, it was really great since it was covering the support for IE8, IE9. Since jQuery dropped the support for the older browser versions, the point of using it for the clicks / interactions tracking has lost most of the sense for me.
While jQuery is still a really good library it includes a ton of features that us the Web Analysts don’t really need, since a 99% of the times we’ll just be using the CSS selectors engine from jQuery. Meaning that we are loading a lot of JS code that we won’t be using. And you may be right that maybe the site is already using it and that it won’t have any effect of the page loading, but with all the new tecnlogies, React, Vue, Angular, SolidJS, etc, the usage and insterest has been dropping a lot.
Latest jQuery version weights around 90KB, what about if I tell you that you could mostly replate it by a 300bytes-ish snippet that will cover most of your needs when talking about interactions tracking.
I’ve been using it for years in some clients without no major issues, it relies on the narive AddEventListener from the browser and takes care of the Event Delegation . If you’re coming from jQuery this equals to using the $.on .
_slimedEventListener(document.body, 'mousedown', 'a', function(evt, matched) {
console.log("clicked element href", matched.getAttribute('href'))
console.log("clicked element data attributes", matched.dataset)
console.log("clicked element text", matched.innerText)
});
You can grab the current clicked element and it’s data, you could use matched.parentNode , matched.closest, to get and navigate the parent elements or even using the matched.querySelector and matched.querySelectorAll to find some other element withing the returned element.
As you can see we replace the need of using an external 90KB library dependency, with a 300bytes snippet that will make use of the native browsers functionality. It may take some time to get used to how to grab some elements details ( innerText, dataset, href, getAttribute, hasClass ) instead of the jQuery funciontions names, but I really think that this will boost your code quality and make it more future proof ( sorry, to many clients dropping jQuery at their ends over the last years and breaking everything at my side : ) )
I’m really obsessed with performance, while I may not be the best on that task I’m a real tryhard on that topic, at least up to where my knowledge allows me. And this said, I really miss the Site Speed Report on Google Analytics 4.
This is why I replicated the current metrics and logic from Universal Analytics, and I sharing it with you on this post. If you end implementing this tracking you’ll have the following metrics avaiable in for your use, for example in data studio.
The current provided code, will even allow you to set a siteSpeedSampleRate as Universal Analytics.
Code Snippet
<script>
(function() {
var siteSpeedSampleRate = 100;
var gaCookiename = '_ga';
var dataLayerName = 'dataLayer';
// No need to edit anything after this line
var shouldItBeTracked = function(siteSpeedSampleRate) {
// If we don't pass a sample rate, default value is 1
if (!siteSpeedSampleRate)
siteSpeedSampleRate = 1;
// Generate a hashId from a String
var hashId = function(a) {
var b = 1, c;
if (a)
for (b = 0,
c = a.length - 1; 0 <= c; c--) {
var d = a.charCodeAt(c);
b = (b << 6 & 268435455) + d + (d << 14);
d = b & 266338304;
b = 0 != d ? b ^ d >> 21 : b
}
return b
}
var clientId = ('; ' + document.cookie).split('; '+gaCookiename+'=').pop().split(';').shift().split(/GA1\.[0-9]\./)[1];
if(!clientId) return !1;
// If, for any reason the sample speed rate is higher than 100, let's keep it to a 100 max value
var b = Math.min(siteSpeedSampleRate, 100);
return hashId(clientId) % 100 >= b ? !1 : !0
}
if (shouldItBeTracked(siteSpeedSampleRate)) {
var pt = window.performance || window.webkitPerformance;
pt = pt && pt.timing;
if (!pt)
return;
if (pt.navigationStart === 0 || pt.loadEventStart === 0)
return;
var timingData = {
"page_load_time": pt.loadEventStart - pt.navigationStart,
"page_download_time": pt.responseEnd - pt.responseStart,
"dns_time": pt.domainLookupEnd - pt.domainLookupStart,
"redirect_response_time": pt.fetchStart - pt.navigationStart,
"server_response_time": pt.responseStart - pt.requestStart,
"tcp_connect_time": pt.connectEnd - pt.connectStart,
"dom_interactive_time": pt.domInteractive - pt.navigationStart,
"content_load_time": pt.domContentLoadedEventStart - pt.navigationStart
};
// Sanity Checks if any value is negative abort
if (Object.values(timingData).filter(function(e) {
if (e < 0)
return e;
}).length > 0)
return;
window[dataLayerName] && window[dataLayerName].push({
"event": "performance_timing",
"timing": timingData
})
}
}
)()
</script>
Setting up GTM
The first thing we need to do it adding the following code snippet above in a Custom HTML tag in Google Tag Manager, that fired on the Window Load event.
Outcome
The code we added above will kindly push all the performance timing data in a nicely formated dataLayer push, that we later use to pass the data to any tag/vendor we want. In our case we’ll pushing the data to a GA4 event tag.
Now that we have all the data being push to the dataLayer, we’ll need to create a new GA4 event Tag and maps all the data accordingly, please refer to the following screenshot for all the details.
Custom Metrics definition in Google Analytics 4
As you already know, sending a parameter within an event doesn’t mean anything until we map it to a dimension within our property.
For this we need to go to the Custom Definitions > Custom Metrics, and add all this new parameters with the event scope and Milliseconds as the Unit of Measurement.
How to view the data
Now that we started collecting the data, we have some different ways to view it, we could use the GA4 reports ( which will be limiting the reporting possibilities a lot )
Using the Google Analytics Exploration
Using Data Studio
This is my prefered option, you can create AVG metrics in an easy way, AVG metrics in Seconds to match the reports on Universal Analytics.
For example, I tried to quickly replicate the Universal Site Speed reports using Data Studio without almost no time.
Using Big Query
Lastly, if you’re one of the brave analysts around the world, the good news is that the possibilities are now almost enless, is just up to you to play around with this and paint it anywhere you want.
As a simple example, let’s find out how many and which pages took more than 1 second to load.
SELECT * ,
FROM `thyngster.*********.events_20220712`
WHERE event_name = "performance_timing" AND
(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'page_load_time') > 1000
Metrics List
Here it goes the list of metrics you can configure and use to replicate the old Universal Analytics reports:
Site Speed Events Count
Page Load Time (ms) and Avg. Page Load Time (sec)
Domain Lookup Time (ms) and Avg. Domain Lookup Time (sec)
Page Download Time (ms) and Avg. Page Download Time (sec)
Redirection Time (ms) and Avg. Redirection Time (sec)
Server Connection Time (ms) and Avg. Server Connection Time (sec)
Server Response Time (ms) and Avg. Server Response Time (sec)
Document Interactive Time (ms) and Avg. Document Interactive Time
Document Content Loaded Time (ms) Avg. Document Content Loaded Time (sec)
(Extra) How siteSpeedSample Works
I must confess I’ve never looked at this and I found pretty curious about how Universal Analytics was doing the Site Speed Sampling. I was expecting having it doing a sampling based on hit, but currently the sampling is done on users ( devices ).
This means that rather than getting details about a 5% of the pageviews, you’re getting the details about the 5% of your visitors instead.
Universal Analytics has been relying on the clientId value to determine if the current user should be sending the timing details.
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
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 Name
Description
GA4_MEASUREMENT_ID
Your Measurement ID, G-XXXXXXXX
GA4_ENDPOINT_HOSTNAME
Override the default endpoint domain. In case you want to send the hits to your own server or a Server Side GTM Instance.
GOOGLE_CONSENT_ENABLED
a &gcs parameter will be added to the payloads with the current Consent Status
WEBVITALS_TRACKING
If you enable this a webvitals event will fire 5 seconds after the page is visible
PERFORMANCE_TIMING_TRACKING
Whatever you want to push a performance_timing event including the current page load performance timings
DEFAULT_PAGEVIEW_ENABLED
If enabled a page_view event will fire on the page load
SEND_DOUBLECLICK_BEACON
Send 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
Value
Meaning
G100
Analytics Consent Non-Granted
G101
Analytics Consent Granted
Blocking Google Analytics 4 – No Consent
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,
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:
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:
Remember in GA4 “everything 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:
click
When there’s a click on an element
hidden
When the page becomes hidden
ini-load
When the initial contents of an AMP element or AMP document have been loaded.
render-start
When the rendering of an embedded component has started (ie : ads iframes)
scroll
When under certain conditions when the page is scrolled
timer
When on a regular time interval
video-*
When thereโs a video interaction
visible
When an element becomes visible
blur
When a specified element is no longer in focus
change
When a specified element undergoes a state change
user-error
When an error occurs that is attributable to the author of the page or to software that is used in publishing the page
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
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.
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.
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
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:
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 Name
Description
video-play
Video stats to play
video-pause
Video is paused
video-ended
Video Completes (reach end of playback)
video-session
Triggers 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-played
This will trigger each time the defined amount of time is played ( ie: every 10 seconds watched )
video-percentage-played
Same as above we can define which % of the progress we want to trigger this
video-ad-start
Video Ad Starts
video-ad-end
Video 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:
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 ๐
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:
Parameter
Description
Example Value
epn.first_contentful_paint
First Contentful Paint
170.199951171875
epn.first_viewport_ready
First Viewport Ready
164.59999990463257
epn.make_body_visible
Make Body Visible
163.40000009536743
epn.largest_contenful_paint
Largest Contentful Paint
170.299072265625
epn.cumulative_layout_shift
Cumulative Layout Shift
0.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_time
Amount of time (in seconds) it took that page to load
epn.domain_lookup_time
The average time (in seconds) spent in DNS lookup for this page
epn.tcp_connect_time
Provides 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_time
Time taken to complete all the redirects before the request for the current page is made (in ms)
epn.server_response_time
Total time taken by the server to start sending the response after it starts receiving the request (in ms)
epn.page_download_time
Provides 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_time
Provides the time the page takes to fire the DOMContentLoaded event from the time the previous page is unloaded (in ms)
epn.dom_interactive_time
Provides 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.
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.,
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.
When Google Announced Google Analytics 4, I started to work on this library, with the main aim to be used as a customTask.
Now the current deadline is 1 year-ish for all free Universal Analytics users and 3 extra months ( thanks? ) for paid users. In my very humble opinion, this is very short notice for such a big work. In my experience, a big implementation will take at least 6 months on average, for defining everything, write up the specs, have it team implement it, the GTM( or any other TMS ) setup and then doing the QAing a dozen times until finally all the data is coming as expected and with the proper values.
The current help Google is offering in this situation is allowing us to turn on a switch on the interface that will autogenerate some GA4 events and pageviews from the Universal Analytics hits. If you wonder how they’re doing this, they’re adding a listener for the window.ga('send') calls.
At a first look, it may look like good help, but I really feel that’s all against how Google Analytics 4 is supposed to be implemented. I really see a lot of issues coming from this:
No data model will be implemented on GA4, if you have messy events tracking setup, you’re new data on GA4 will be at least as messy as your events were on Universal Analytics
It won’t pass the custom dimensions/metrics/content grouping, this is likely going to be insufficient for anyone having at least 1 dimension.
Timing, Exceptions, Legacy Transaction Events, and Social hits, won’t be migrated.
Another issue with the automatic migration tool Google offers is that an Event Action seems to be converted to an Event Name. That’s a terrible thing, most of the clients I worked on, will end up having thousands of different events. ( I wonder if this is the reason for having a “non-written” no limit for the events coming from the website ).
In any case, if you’re migrating your setup to Google Analytics 4, there's a no better chance to fix whatever is not right with your setup. One of the most important implementation steps in GA4 is the data model definition, that’s gonna define your current data quality and it’s gonna limit you in the future ( event parameter limits, user properties limits, etc ). Don’t take the lazy path and regret it later.
Universal Analytics Payload Parser
Now, the main post point, I’ve released a Universal Analytics Payload Parser library. This library is able of taking a Univeral Analytics Hit and converting them into some properly modeled dataLayer pushes.
This library can be used as a customTask in Universal Analytics and it will push the hit contained data into a Google Tag Manager (dataLayer.push), Tealium IQ (utag.link) or Adobe Launch (_satellite.track) pushes.
Instead of having someone send some predefined data into your GA4 property, this tool will push the data to your TMS so you can send the data to GA4 in the format, you want or pass the data to ANY other tool you want if at some point you want to migrate to any other analytics solution ( Snowplow, Matomo, Amplitude, Adobe Analytics, etc ), or even to any internal data solution you have built in your company.
Features
This library accepts a Universal Analytics Payloadand parses it to return a formated and standardized object that can be used to pass the current info to any Tag Management System. At the time of writing this post, the library supports the output for Google Tag Manager, Tealium IQ, or Adobe Launch, adding a callback of support for sending the data to any external source should be pretty straightforward.
This library will take all the UA hit types, and will create an object with all data contained on that hit.
It generates individual pushes for each UA hit.
It’ll attach all event-related implicit parameters ( like the page title or page URL for the pageview event, or the event category, action, label for the events ). Check the table below for more details.
It will also map all the custom dimensions, metrics, and content groupings related to the current hit, and will allow you to define friendly names for them ( it: have the event to hold “client_id” rather than “cd1”
This library will also extract/parse all the e-commerce data from the payloads and generate a standalone Ecommerce Event with the data, also including the custom dimensions/metrics for the products.
Supported Hit Types / Parameters
Next, you can find the list of supported hit types and all the data that will be shipped with the generated object push.
As we mentioned before the Universal Analytics Payload Parser the library will also take care of parsing the Enhanced Ecommerce data contained within the hits and will generate the following standalone Ecommerce Pushes.
Whether or not to parse the e-commerce data and push it
skipTransportEvent
If we set this to true, the current hit/event will be skipped and only the EEC event will be generated
mapping.keys
Our dimensions, metrics, and content_grouping definition, this will convert the โcd1โ payload key into something more readable like โclient_idโ in the pushes.
mapping.events.checkout
GA4 not longer accepts checkout_steps, this will let you define which current Checkout Steps should be considered the new โbegin_checkoutโ and โview_cartโ events
So, that’s all, as you can see rather than sending data to Google Analytics 4, this library allows sending the data to the dataLayer ( or anywhere else ) instead so you can work with the data in any way you want, you could define different event names, you could sanitize the values, you can block/skip the events you don’t need.
The best point is that you’ll have full control over how the data ends in your Google Analytics 4 account, and this will work even if you’re using GTAG, or even if you have a hardcoded implementation, just adding the custom task in your main snippet should allow you to have the data anywhere else.
If you’re using Google Tag Manager for setting up Google Analytics 4 tracking you may have noticed the need of creating a separate tag for each event we want to track. While this gives a really nice and easy-to-understand photo of the current used events, it may add a ton of work for maintaining them, for example, if at some point we end up having hundreds of them.
In Universal Analytics many implementations were using a single event tag for tracking all their events, using a unique dataLayer event , and then mapping the event category, event action, event label and event value.
This cannot be done in Google Analytics 4, because we have an open specter of parameter / dimensions names which we have to individually map to each of the events we configure.
In this post, we’ll be learning today how to do all our events tracking using just a single GA4Event Tag, while keeping all the features and functionality we’d have if we were setting up individual event tags.
Defining our dataLayer pushes
The first thing we need to do is to define how are we going to push the data to our dataLayer, and this is the way we’ll need to do it:
There’s a reason for this, Google Tag Manager dataLayer is cumulative, which means that all new pushed data keeps being added to the internal data model. If we push a param1 key it will not only be available for the current event, but it will be present on the subsequent ones. Since we want to use a single tag, this means that this data would be added to the next events.
In order to deal with this, we’ll be using a v1 dataLayer variable to read the event_params and user_properties. Because this way all the data contained within these keys will be overridden with all the pushes.
If you are wondering which are the differences between the v1 and v2 dataLayer variables:
v1
Doesn’t support nested notation
Keys are overwritten rather than merged
v2
Support for nested objects notation ( one.two.three )
Key are merged
Let’s see this with an example so we can understand this better. We have these 2 dataLayer pushes, each of them having a different event_parameter attached to them:
Then in Google Tag Manager we’d be defining two variables. let’s name them {{dl - clicked_url}} and {{dl - tab_id}} . Let’s see how they would look depending on how we read them
{{dl - clicked_url}}
{{dl - tab_id}}
First Push
v1: Simo Blog v2: Simo Blog
v1: undefined v2: undefined
Second Push
v1: undefined v2: Simo Blog
v1: 1 v2: 1
If you check the table above if we were using the v2 version, the second event would end attaching the clicked_url value to the event, which is something that we don’t want.
It’s true that if you’re creating individual tags for each event, it’s gonna be hard this would be affecting you all (but it’s not impossible), in any case, let’s learn how to prevent this situation, and also let’s set up GTM to handle all of our GA4 events within a single tag.
Google Tag Manager Setup
The core Event Tag
First thing first, we need a new GA4 Event tag in our container. For now, the only thing we’ll configure here is the Event Name to be the in-build {{Event}} variable which will read the actual dataLayer push event value and pass it back to our GA4 tag event name.
For now, that’s all we’ll be doing, before mapping up the event parameters and user properties , we will learn how would need to set up the variables to relay on the v1 version.
The variables
This may be a bit confusing for some people, but as we explained before the need to use a version 1 parameter, but we also mentioned this dataLayer variable type does not support nested values. The workaround for this is using a variable that grabs the main key ( user_properties, event_params ) and then having some Custom JS variables returning the values we need.
Let’s start it, create these two variables and remember to set them as Version 1
The next step is creating a variable for each of the values we want to have access to, in our case, we’ll have 2 of them.
As you can see instead of reading them from the dataLayer, we are reading them from the v1 variable created. This will cause the event_params to be reset each time, and the values will be undefined for the parameters NOT being added to the current push. This functionally is like all the events/user data will be “reset” on each event push.
Mapping the variables
Now that we have our GA4 Event Tag and also our dataLayer variables, it’s time to map these last ones to the first one.
In your personal case, you’ll need to map as many parameters/variables as different values you’ve defined across all your events.
Setting up the triggers
Wait for a second, you said that we’ll be keeping all the features as doing the tracking with individual tags, with the setup we won’t be able to check which events are configured or we won’t be able to pause and specific event.
That’s where our trigger comes into scene, we’ll define a firing trigger that will trigger on .* the events, but only if the current event name is within an events lookup table.
Create a lookup table type variable in Google Tag Manager, on this table we’ll define an output of 1 for all the events name we want to enable ( ie: that will fire our GA4 event tag ), and then a default value of 0
As you may have already guessed this variable will be used as a condition in our trigger this way:
Recap
All this may look more convoluted than it really is, but it leads to some really interesting benefits.
We only have to set up one single tag
We still have the lookup table to control and review which events are defined and firing
Even if we end up going with the path of adding dozens or hundreds of event tags, the issue of having the dataLayer adding some parameters to some event that it should just because it was pushed before may be present on your setup. So I recommend using the combination of v1 and v2 variables to get rid of this problem.
At some point, if you have many events you could group them and use an event/trigger/lookup for each group ๐
Google Updated their GTAG library to relay on fetch instead of sendBeacon, making the old script to stop working. In any case sendBeacon uses fetch API under the hoods ( it’s a fetch request that won’t expected any return response from Google Analytics Endpoint ). We may expect at some poing Google returning some data back to the browser and that’s being the reason behind this update.
In any case there’s a new code snippet for Monkey Patching the Fetch function allowing you to send duplicate hits to secondary accounts, and not only this, it’s an improved code that will allow you to control which measurementId you want to duplicate (in case you are using more than one)
This is way now you can define the Measure IDs mapping:
// All requests to G-DEBUGEMALL will get a copy to G-REDIRECT-1 and G-REDIRECT-2varmeasurementIdsMapping={"G-DEBUGEMALL": ["G-REDIRECT-1","G-REDIRECT-2"]};
Post Start I must admit it, I miss the flexibility the customTasks give to Universal Analytics, and I really hope someone takes a step forward at some point by adding that feature to Google Analytics 4.
In the meanwhile, I was in the need of doing parallel tracking, ie: sending the data to two differentMeasurement IDs and that would mean duplicatingall the event tags on the client account. If it only was this I’d even accept it, but having the setup splitter un duplicated event would mean needing to have the setup synced in the future ( which all of us know how that would likely end )
How are we doing it
While this is in any case is a recommended practice, we can do a trick to forward a copy of the GA4 beacons with a modified Measurement ID . It’s based on a technique named “Monkey Patching“, we already used this one for our Google Analytics 4 PII Redacting post, but this time we change the logic slightly.
In case you don’t know “Monkey Patching” it’s a technique that will modify/update the behavior of the previously defined function/method at runtime, without needing the change the original code.
Google Analytics 4, relies on the navigator.sendBeacon browser’s API for sending the data, and we’re going to intercept that calls to that API in order to be able to capture the current GA4 Hits Payloads and sending a copy.
Setting up Google Tag Manager
There’s one thing we need to have in mind and is that this code MUST be run before Google Analytics 4 Config Tag, and for achieving this we’ll use the Tag Sequencing on Google Tag Manager.
But before anything, we need to create a new CUSTOM HTML tag. This is where ALL the stuff happens.
<script>(function() {// David Vallejo 2024// Only defined measurementId will be identified and a clone hit will be sent to the defined// Secondary measurementIdsvarmeasurementIdsMapping={"G-DEBUGEMALL": ["G-REDIRECT-1","G-REDIRECT-2"]};// We should not run this twice, if the sendBeacon has been already modified, abortif (window.fetch&&window.fetch.toString().indexOf('native code') !==-1) { var _window=window,originalFetch=_window.fetch; window.fetch=function(){varresource=arguments[0];varoptions=arguments[1];try{if (resource&&measurementIdsMapping&&Object.keys(measurementIdsMapping).length >0) {varpayload=Object.fromEntries(newURLSearchParams(newURL(resource).search));if (Object.keys(measurementIdsMapping).includes(payload.tid) &&payload.cid) {measurementIdsMapping[payload.tid].forEach(function(measurementId){varbeaconBaseUrl=newURL(resource);beaconBaseUrl.searchParams.set('tid',measurementId);originalFetch(beaconBaseUrl.toString(),options).catch(function(error){console.error('Error in clone hit fetch:',error);});});}}}catch(e){}returnoriginalFetch.apply(this,arguments);};}})();</script>
<script>(function() {// Add your secondary measurement ID(s) herevarmeasurementIds= ["G-THYNGSTER-2"];// We should not run this twice, if the fetch has been already modified, abort, jicif(navigator.sendBeacon&&navigator.sendBeacon.toString().indexOf('native code') !==-1){// Helper Convert QueryString to an Object var queryString2Object=functionqueryString2Object(str){return (str||document.location.search).replace(/(^\?)/,"").split("&").map(function(n){returnn=n.split("="),this[n[0]] =decodeURIComponent(n[1]),this;}.bind({}))[0];};// Helper Convert an Object to a QueryString var Object2QueryString=functionObject2QueryString(obj){returnObject.keys(obj).map(function(key){returnkey+'='+encodeURIComponent(obj[key]);}).join('&');}; try {// Monkey Patch, sendBeacon varproxied=window.navigator.sendBeacon;window.navigator.sendBeacon=function(){// Make an arguments copy and modify the Measurement IDvarargs=Array.prototype.slice.call(arguments);var_this=this;if (args&&args[0].match(/analytics\.google\.com|google-analytics\.com.*v\=2\&/)) {varpayload=queryString2Object(args[0]);measurementIds.forEach(function(id){payload.tid=id;args[0] =Object2QueryString(payload);proxied.apply(_this,args);});}returnproxied.apply(this,arguments);};} catch (e) {// In case something goes wrong, let's apply back the arguments to the original function return proxied.apply(this,arguments);}}})();</script>
The trigger
We could be using an “All Pages” trigger, but since Tags are injected asynchronously by GTM into the page, it’s safer to use the Tag Sequencing. We only have to link the previously create tag within our GA4 Configuration Tag
Disclaimer
Before going further on this post, I want to say again, while this is a working workaround but a specific need is not, actually, covered by GTAG / Google Analytics 4 . You should run this at your own risk, and I recommend you to follow some people like Simo or Charles which are both on top of all new GA4/GTM related features to be notified if at some point some official functionality comes to GTAG.
How the code works
This snippet code is pretty straightforward, there’s only one thing we need to configure, and it’s the first variable with the Measurement IDs to where we want to send the data.
I’m trying a new blogging approach for this blog, and on this post and doing a deep walkthrough over the code, I feel this can be of interest to people wanting to learn rather than just wanting to copy and paste the code. At this point, if you’re in the last one’s group you can skip the rest of the post otherwise I hope I’m doing any good work explaining the code ๐
Note: You should only add the secondary account, the main Measurement ID on the GA4 Config that doesn’t need to be added here.
var measurementIds = ["G-MEASUREMENTID-1", "G-MEASUREMENTID-2"];
One problem with Monkey Patching functions is that they may have been already modified by some other scripts… So in order to be safe on our side, we’re aborting the patching if the current navigator.sendBeacon has been already modified.
Next in line are 2 helper functions, queryString2Object and Object2QueryString , these are not needed since we could use a replace or a regex to do the work, but this way everything is cleaner. First5 one takes a query string:
v=2&tid=G-THYNGSTER
And converts it to an Object
{
v: "1",
tid: "G-THYNGSTER"
}
Now we can easily update any payload values with no risk of writing a wrong regex or doing a bad text replace. The second function does the inverse task, converts the object back to a QueryString
Now, we’ll be wrapping everything between a try && catch statement, if for ANY reason anything fails, we’ll send the hit back to the original function. We really want to have the original request to be sent despite the duplicate ones that may fail at some point.
Let’s now check how the Monkey Patching is done, first of all, since we’re going to modify the original function, we need to save a reference to the original function:
var proxied = window.navigator.sendBeacon;
In the first place, we want to keep the current call arguments intact, that’s why we’re doing a copy of them, and then we’ll use this copy rather than the original ones.
window.navigator.sendBeacon = function() {
var args = Array.prototype.slice.call(arguments);
var _this = this;
Our next check is verifying that the current beacon is for GA4, we don’t really want or need to mess around with other hits (again, let’s stay in the safe place ๐ )
if (args && args[0].match(/analytics\.google\.com|google-analytics\.com.*v\=2\&/)) {
Once, we know that the current hit is a GA4 Hit, we’ll convert the payload to an object
var payload = queryString2Object(args[0]);
And the last thing we’re doing is looping across our secondary measurement IDs while updating the &tid parameter, then finally we send the hit to Google Analytics 4 Endpoint using for that the “original” reference we saved at the start.
The last line will take care of sending the original hit ( this is why we don’t need to add the main Measurement ID into the configuration )
return proxied.apply(this, arguments);
Well, there’s still a final one, the one within the catch statement, as we mentioned before if ANYTHING goes wrong we’ll still send back the original hit, this assures that despite the code fails, we’ll have our main configured id recording the data.
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 explainGoogle 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.jscontainer 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.
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.
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 )
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)
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.
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/jsonContent-Type:
Optional. Hit offset. Up to 3 days ( 2,592e+11 microseconds ) before the current property’s defined timezone.
user_properties
{}
Optional.
non_personalized_ads
bool
Optional. ( whatever use this event for ads personalization )
event[]
[]
Required. ( Max 25 Events per request )
event[].name
str
Required.
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 / Event
Parameters
pageview
– Location – Path – Title
event
– Category – Action – Label – Value – Non Interaction
– 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 Name
Parameters
[event_name]
This will take the current eventAction eventCategory > event_category eventAction > event eventLabel > event_label eventValue > value
timing_complete
timingCategory > event_category timingLabel > event_label timingValue > value timingVar > name
exception
exDescription > 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 Name
Trigger
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:
page_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_activeview
APP
ad_click
APP
ad_exposure
APP
ad_impression
APP
ad_query
APP
adunit_exposure
APP
app_clear_data
APP
app_install
APP
app_update
APP
app_remove
APP
error
APP
first_open
APP
in_app_purchase
APP
notification_dismiss
APP
notification_foreground
APP
notification_open
APP
notification_receive
APP
os_update
APP
screen_view
APP
user_engagement
APP,
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).*“,
Each of these properties should have a name no longer than 24 characters and a value not bigger than 36 characters.
Logged item
Limit
Free
360
Events
Event Name
40 chars
Event parameter Name
40 chars
Event parameter Value
100 chars
Params per event
25
100
User properties
Total per Property
25
Property Name
24 chars
Property Value
36 chars
User-ID
256 characters
Custom dimensions
Event Scope
50
125
Item Scope
10
User Scope
25
100
Custom Metrics
Event Scope
50
125
Events Offset
3 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.