A very Minimal Google Analytics 4 Snippet

A very minimal google analytics 4 snippet allowing you to record page view and site search on your website without loading official, heavy tracking library
Contents

Due to Google’s announcement that they will force us to move away from Universal Analytics to Google Analytics 4 I wasn’t happy with that. Due to a lack of alternatives in minimal analytics, loading official (bloated) tracking code that weights 171kB (in my instance), that is liable for blocking by various AdBlockers, wasn’t something that I had been looking forward.

I started searching for a solution. Due to lack of it, I decided, by hit-and-miss approach, to create my own, and I think I did it. It weighs slightly over 1kB minified. Its main purpose is to track page views (page_view, session_start and first_visit) on our website in Google Analytics 4 property. Since version 1.06 it also detects and tracks site searches (view_search_results) and from 1.07 search query (search_term).

Minimal Analytics 4 - the code #

Version 1.08.27042022 (1.08 from 27/04/2022)

<script>
function a(){const n=localStorage,e=sessionStorage,t=document,o=navigator||{},w="G-XXXXXXXXXX",a=()=>Math.floor(Math.random()*1e9)+1,r=()=>Math.floor(Date.now()/1e3),y=()=>(e._p||(e._p=a()),e._p),b=()=>a()+"."+r(),g=()=>(n.cid_v4||(n.cid_v4=b()),n.cid_v4),f=n.getItem("cid_v4"),A=()=>f?void 0:"1",j=()=>(e.sid||(e.sid=r()),e.sid),m=()=>{if(!e._ss)return e._ss="1",e._ss;if(e.getItem("_ss")=="1")return void 0},d="1",p=()=>(e.sct?(x=+e.getItem("sct")+ +d,e.sct=x):e.sct=d,e.sct),s=t.location.search,v=new URLSearchParams(s),l=["q","s","search","query","keyword"],h=l.some(e=>s.includes("&"+e+"=")||s.includes("?"+e+"=")),c=()=>h==!0?"view_search_results":"page_view",_=()=>{if(c()=="view_search_results"){for(let e of v)if(l.includes(e[0]))return e[1]}else return void 0},i=encodeURIComponent,O=e=>{let t=[];for(let n in e)e.hasOwnProperty(n)&&e[n]!==void 0&&t.push(i(n)+"="+i(e[n]));return t.join("&")},C=!1,E="https://www.google-analytics.com/g/collect",k=O({v:"2",tid:w,_p:y(),sr:(screen.width*window.devicePixelRatio+"x"+screen.height*window.devicePixelRatio).toString(),ul:(o.language||void 0).toLowerCase(),cid:g(),_fv:A(),_s:"1",dl:t.location.origin+t.location.pathname+s,dt:t.title||void 0,dr:t.referrer||void 0,sid:j(),sct:p(),seg:"1",en:c(),'ep.search_term':_(),_ss:m(),_dbg:C?1:void 0}),u=E+"?"+k;if(o.sendBeacon)o.sendBeacon(u);else{let e=new XMLHttpRequest;e.open("POST",u,!0)}}a()
</script>

See the full, unminified code and changelog on Gist/GitHub.

Official Google Analytics 4, Global Site Tag (gtag.js) = 171kB
Snippet (minified) over 1kB without script tag (1,393 bytes)

The snippet sends page views (and site searches since version 1.06) directly to Google Analytics 4 (using /g/collect) and stores some metrics in localStorage and sessionStorage in our browser to prevent generating dummy entries by that same user.

Thanks to that, we don’t need to load an official script and the loading speeds of our website will not be affected.

If you need to load the official script (taking a risk that it will be blocked by AdBlockers, you can still do this together with the JavaScript delay approach described here Google Universal Analytics property is shutting down. Here is what you need to know..

Snippet will identify searches on the site based on search query in URL like q, s, search, query and keyword. Once detected it sending and event view_search_results (instead page_view) and search_term as even parameter (since 1.07)

In Universal Analytics Google automatically strip search query from URL and extract search term from it. This is not the case in Google Analytics 4 and along event, need to sent even parameters.

Snippet is also working on principle but can be easily modified to add any custom search queries (remember to add this into configuration on Google Analytics Data Streams as well, not only on snippet).

Minimal Analytics 4 - the setup #

Setup you Google Analytics 4 property and Web stream. In Enhanced measurement select only Page views and Site Search. Copy the above snippet and paste it in your website <head>. Replace G-XXXXXXXXXX with your measurement ID (from Web Stream), and you are ready to go.

Make sure, to paste your code in <head> of the document after the <title> tag.


Despite that snipped weight a tonne less than official script, still is liable for blocking by various AdBlockers. But there is solution for that (if yours hosting provider, like Netlify, allows to set Redirects 200). See masking solution described below.

Minimal Analytics 4 - masking (hiding) requests #

My site is hosted on Netlify and they allow, through _redirects file, to set not typical redirect with code 200 (OK), working as a proxy URL.

200 Code informs about successful processing of the request.

My rule looks as follow

/g/collect https://www.google-analytics.com/g/collect 200

Then I am replacing the following part in the code

https://www.google-analytics.com/g/collect

With my site address

https://dariusz.wieckiewicz.org/g/collect

The call happens on the website from the same address as the main site, so it’s not blocked by AdBlocker. In the background, on the server-side (on Netlify CDN in my instance) my URL is converted to an official one and the request is sent together with payload /g/collect?v=2&tid=G... (all after ? is a payload).

There is another option to pass the payload of the URL through more advanced redirect as explained on Masking (hiding) Google Analytics 4 code

This solution has its downsides as described in my other post Prevent Google Analytics from being blocked by AdBlocker – The Downside.


JavaScript is not something that I am using every day. I used Minimal Analytics snipped by David Künnen as a guide to build mine and it works as expected.

The code is free, and you can call it “the base” for further development. Appreciate it if any of you, with much more JavaScript knowledge, can help in its further development.


If you are more technical, and you want to know how this was built and what each element mean, read the below technical specification.

Minimal Analytics 4 - technical specification #

A bit of technical description of the code.

Page view or Site search tracking is sent to Google together with gathered data (payload) using https://www.google-analytics.com/g/collect URL.

This URL is likely to be blocked by AdBlockers or DuckDuckGo extension to Internet Browser resulting that many visits (in my calculations around 40%) are not measured. This is why, despite the downside, a masking solution can help with that.

Payload (data) is added to it in serialised form and contain

  • v=2, Measurement Protocol Version 2 for GA4;
  • tid=G-XXXXXXXXXX, Measurement ID for GA4 or Stream ID;
  • gtm=XXXXXX (removed from minified since version 1.07 as currently not in use) Google Tag Manager (GTM) Hash Info. If the current hit is coming was generated from GTM, it will contain a hash of current GTM/GTAG config. In one of my tracking it looks like 2oe3n1.
  • _p=4723978523, another value sent with payload with an unknown purpose. Due to lack of official documentation, I assumed that it as random generated a n-digit number. In case it needs to be unique per session it’s stored in browser sessionStorage;
  • sr=1920x1080, Screen Resolution;
  • ul=en, visiting user language;
  • cid=1659736851.1648993429, client ID hold in browser localStorage. Identify new users from returning users. Random generated, set of two n-digit numbers;
  • _fv, first view, identify returning users based on existance of client ID in localStorage;
  • _s=1, session hits count;
  • dl=https://..., URL of tracked page;
  • dt=Title, Title of the tracked page;
  • dr=https://..., if the user comes to tracked page from a different page or site, this will hold the referrer URL;
  • sid=1648993317, holds a unique session (page visit) ID and is storing it in browser sessionStorage. Generated based on date function;
  • sct=1, its session engagement factor. Officially shall be set if the user interacts with the website for at least 10 seconds (assume that interaction happened). This value will increase +1 when the user will be visiting other pages on our website in the current session;
  • en=page_view or en=view_search_results, counted event, page_view for views and view_search_results for site searches. This code can be further modified and triggered with different events;
  • ep.search_term=microsoft, its a tracked search term (what user typed into search form)
  • _ss=1, count visit as a new session
  • _dbg=1, enable analytics to debug that is recorded on google analytics website, in debug section.

Based on url and payload data the URL is combined, creating fullurl and in such form is sent.

As you will notice, the request from the script is not sent through the expected solution navigator.sendBeacon(url, data) but more rather (url+"?"+data). Not sure why, but somehow on (url, data) approach something is lost and the page view is not recorded, whereas on the other solution it is.

There are some commented parts for cleaning sessionStorage, localStorage and displaying the output of URL with payload in the browser console. Uncomment this if you want to analyse how it all works.

Hope this code will provide us with a solution for the following years with Google Analytics 4, in the same manner as David’s minimal analytics provided before with Universal Analytics (GA3).

Privacy and other aspects #

This snipped, as opposed to Google’s original script, not tracking your activity for all the time you are on the website. Once the page is loaded, the script is fired up, appropriate data are sent and user interaction is finished. End of story.

Because of that, some reports that rely on the original script on the Google Analytics interface will not show any data or the data will look very strange.

The snippet is run (fired) again if you refresh the page or go to a different page on the website. After that, once again, the interaction is finished. You need to remember that.

Because of that, it’s not possible to show how long a user has been on one page or his engagement time with it. It’s not a fault, it’s the way how this is designed and run (fired).

Like David Kuennen mentioned in his minimal analytics for UA:

“You should not use this if you want to use advanced features like tracking AdWords”.

There is no difference with this one. You need to think about what you need and decide what solution is good for you.

For example, once you land on the website, the snippet automatically assumed that you are engaged with it. It’s not working on the principle of waiting 10 or 30 seconds before assuming that the user is engaged or not.

Spam bots generating fake visits rarely use JavaScript hence they will not fire up this snippet, hence a major of trackings is for the real users.

Delayng snippet until the user interacts #

If you look into my post Implementing JavaScript Delay for Cookie Consent Banner, you can implement it for this solution.

JavaScript Delay waiting by default for user interaction through the movement of the cursor, keypress, scroll or touch before its firing it. If a user loads the page but there is no interaction, as a failsafe, the script will fire up after 5 seconds delay (can be adjusted). This can act as a delay to engagement (Engaged Session) if needed.

Apart from that, I don’t see a way of measuring time (Engagement Time) without constantly (at the beginning) tracking the user and having the script working in the background until it’s assumed that the user is engaged and time has been measured. This is against the ideology of this approach.

We want to measure how many users visit our website and what parts of the website are most popular. This is the simplest approach for all web enthusiasts, bloggers, and small company websites.

Remember, however, that despite that our tracking ability is simplified, it’s still tracking. For some websites, you need to consider having a relevant policy (GDPR) and consent banner for your users, especially in recent revelations where some countries EU trying to convince you that Google Analytics is illegal and violating GDPR.


Credits to:

Buy me a coffee
Comments