Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
/* https://go.tacodewolff.nl/minify */
enScroll=!1;const lStor=localStorage,sStor=sessionStorage,doc=document,docEl=document.documentElement,docBody=document.body,docLoc=document.location,w=window,s=screen,nav=navigator||{};function a(){const k="G-XXXXXXXXXX",t=()=>Math.floor(Math.random()*1e9)+1,n=()=>Math.floor(Date.now()/1e3),y=()=>(sStor._p||(sStor._p=t()),sStor._p),v=()=>t()+"."+n(),p=()=>(lStor.cid_v4||(lStor.cid_v4=v()),lStor.cid_v4),m=lStor.getItem("cid_v4"),u=()=>m?void 0:enScroll==!0?void 0:"1",l=()=>(sStor.sid||(sStor.sid=n()),sStor.sid),d=()=>{if(!sStor._ss)return sStor._ss="1",sStor._ss;if(sStor.getItem("_ss")=="1")return void 0},r="1",h=()=>{if(sStor.sct)if(enScroll==!0)return sStor.sct;else x=+sStor.getItem("sct")+ +r,sStor.sct=x;else sStor.sct=r;return sStor.sct},e=docLoc.search,f=new URLSearchParams(e),a=["q","s","search","query","keyword"],g=a.some(t=>e.includes("&"+t+"=")||e.includes("?"+t+"=")),i=()=>g==!0?"view_search_results":enScroll==!0?"scroll":"page_view",b=()=>enScroll==!0?"90":void 0,j=()=>{if(i()=="view_search_results"){for(let e of f)if(a.includes(e[0]))return e[1]}else return void 0},o=encodeURIComponent,_=e=>{let t=[];for(let n in e)e.hasOwnProperty(n)&&e[n]!==void 0&&t.push(o(n)+"="+o(e[n]));return t.join("&")},O=!1,C="https://www.google-analytics.com/g/collect",E=_({v:"2",tid:k,_p:y(),sr:(s.width*w.devicePixelRatio+"x"+s.height*w.devicePixelRatio).toString(),ul:(nav.language||void 0).toLowerCase(),cid:p(),_fv:u(),_s:"1",dl:docLoc.origin+docLoc.pathname+e,dt:doc.title||void 0,dr:doc.referrer||void 0,sid:l(),sct:h(),seg:"1",en:i(),"epn.percent_scrolled":b(),"ep.search_term":j(),_ss:d(),_dbg:O?1:void 0}),c=C+"?"+E;if(nav.sendBeacon)nav.sendBeacon(c);else{let e=new XMLHttpRequest;e.open("POST",c,!0)}}a();function sPr(){return(docEl.scrollTop||docBody.scrollTop)/((docEl.scrollHeight||docBody.scrollHeight)-docEl.clientHeight)*100}doc.addEventListener("scroll",sEv,{passive:!0});function sEv(){const e=sPr();if(e<90)return;enScroll=!0,a(),doc.removeEventListener("scroll",sEv,{passive:!0})}
// Version 1.09.1.21062022
// Changelog:
// 1.09.1
// - minor changes of single quote in code (') to double quote (").
// 1.09
// - add listener for 90% scroll event. When user scrol to 90%+ of the page then script is fired again with scroll even and then (listener) terminates.
// - Specified global parameters and small tweak in search event.
// 1.08
// - replaced VAR with LET and moved new URLSearchParams, as caused issues when minified
// - changed Minified version from minify-js.com to go.tacodewolff.nl/minify as caused issues with higher number of new users than users
// 1.07
// - added event parameter (search_term) when tracking search via view_search_results event
// -- commented gtmId as currently not in use, hidden from minified version
// 1.06
// - added event identification when page view is a search to set as view_search_results, in other case page_view
// 1.05
// - Added _fv (first_visit indicator) based on cid_v4 in local storage for identification of returning users
// - Corrected (sr) screen resolution retection to include device Pixel ration (like Retina)
// - gtm - value convert to lower case
// - gtm - 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
// 1.04
// - start new session (_ss) only when it is real new session (store session in sessionStorage); revert cid generation to previous method and store under cid_v4 to do not cause an issue when using minimal UA(v3) with this code.
// 1.03
// - split cid generation into two parts and combine at later stage
// 1.02
// - corrected generation of cid
// 1.01
// - changed method for generating _p, cid & sid
// 1.00
// - first release
enScroll = false;
const lStor = localStorage;
const sStor = sessionStorage;
const doc = document;
const docEl = document.documentElement;
const docBody = document.body;
const docLoc = document.location;
const w = window;
const s = screen;
const nav = navigator || {};
function a() {
// debug options to clean cache
// localStorage.clear();
// sessionStorage.clear();
const trackingId = "G-XXXXXXXXXX"; // set here your Measurement ID for GA4 / Stream ID
// gmt > If the current hit is coming was generated from GTM, it will contain a hash of current GTM/GTAG config, example: 2oear0
// Currently not in use, leave XXXXXX , under investigation
// let gtmId = "XXXXXX";
// if (gtmId == "XXXXXX") {
// let gtmId = undefined;
// }
// else {
// let gtmId = gtmId.toLowerCase();
// }
// 10-ish digit number generator
const generateId = () => Math.floor(Math.random() * 1000000000) + 1;
// UNIX datetime generator
const dategenId = () => Math.floor(Date.now() / 1000);
const _pId = () => {
if (!sStor._p) {
sStor._p = generateId();
}
return sStor._p;
};
const generatecidId = () => generateId() + "." + dategenId();
const cidId = () => {
if (!lStor.cid_v4) {
lStor.cid_v4 = generatecidId();
}
return lStor.cid_v4;
};
const cidCheck = lStor.getItem("cid_v4");
const _fvId = () => {
if(cidCheck) {
return undefined;
}
else if(enScroll==true) {
return undefined;
}
else {
return "1";
}
};
const sidId = () => {
if (!sStor.sid) {
sStor.sid = dategenId();
}
return sStor.sid;
};
const _ssId = () => {
if (!sStor._ss) {
sStor._ss = "1";
return sStor._ss;
}
else if(sStor.getItem("_ss") == "1") {
return undefined;
}
};
const generatesctId = "1";
const sctId = () => {
if (!sStor.sct) {
sStor.sct = generatesctId;
}
else if(enScroll==true) {
return sStor.sct;
}
else {
x = +sStor.getItem("sct") + +generatesctId;
sStor.sct = x;
}
return sStor.sct;
};
// Default GA4 Search Term Query Parameter: q,s,search,query,keyword
const searchString = docLoc.search;
const searchParams = new URLSearchParams(searchString);
//const searchString = "?search1=test&query1=1234&s=dsf"; // test search string
const sT = ["q", "s", "search", "query", "keyword"];
const sR = sT.some(si => searchString.includes("&"+si+"=") || searchString.includes("?"+si+"="));
const eventId = () => {
if (sR==true) {
return "view_search_results";
}
else if(enScroll==true) {
return "scroll";
}
else {
return "page_view";
}
};
const eventParaId = () => {
if(enScroll==true) {
return "90";
}
else {
return undefined;
}
};
// get search_term
const searchId = () => {
if (eventId() == "view_search_results") {
//Iterate the search parameters.
for (let p of searchParams) {
//console.log(p); // for debuging
if (sT.includes(p[0])) {
return p[1];
}
}
}
else {
return undefined;
}
};
const encode = encodeURIComponent;
const serialize = (obj) => {
let str = [];
for (let p in obj) {
if (obj.hasOwnProperty(p)) {
if(obj[p] !== undefined) {
str.push(encode(p) + "=" + encode(obj[p]));
}
}
}
return str.join("&");
};
const debug = false; // enable analytics debuging
// url
const url = "https://www.google-analytics.com/g/collect";
// payload
const data = serialize({
v: "2", // Measurement Protocol Version 2 for GA4
tid: trackingId, // Measurement ID for GA4 or Stream ID
//gtm: gtmId, // 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 (not in use, currently under investigation)
_p: _pId(), // random number, hold in sessionStorage, unknown use
sr: (s.width * w.devicePixelRatio+"x"+s.height * w.devicePixelRatio).toString(), // Screen Resolution
ul: (nav.language || undefined).toLowerCase(), // User Language
cid: cidId(), // client ID, hold in localStorage
_fv: _fvId(), // first_visit, identify returning users based on existance of client ID in localStorage
_s: "1", // session hits counter
dl: docLoc.origin + docLoc.pathname + searchString, // Document location
dt: doc.title || undefined, // document title
dr: doc.referrer || undefined, // document referrer
sid: sidId(), // session ID random generated, hold in sessionStorage
sct: sctId(), // session count for a user, increase +1 in new interaction
seg: "1", // session engaged (interacted for at least 10 seconds), assume yes
en: eventId(), // event like page_view, view_search_results or scroll
"epn.percent_scrolled": eventParaId(),// event parameter, used for scroll event
"ep.search_term": searchId(), // search_term reported for view_search_results from search parameter
_ss: _ssId(), // session_start, new session start
_dbg: debug ? 1 : undefined, // console debug
});
const fullurl = (url+"?"+data);
// for debug purposes
// console.log(data);
// console.log(url, data);
// console.log(fullurl);
if(nav.sendBeacon) {
nav.sendBeacon(fullurl);
} else {
let xhr = new XMLHttpRequest();
xhr.open("POST", (fullurl), true);
}
}
a();
// Scroll Percent
function sPr() {
return (docEl.scrollTop||docBody.scrollTop) / ((docEl.scrollHeight||docBody.scrollHeight) - docEl.clientHeight) * 100;
}
// add scroll listener
doc.addEventListener("scroll", sEv, { passive: true });
// scroll Event
function sEv() {
const percentage = sPr();
if (percentage < 90) {
return;
}
enScroll = true;
// fire analytics script
a();
// remove scroll listener
doc.removeEventListener("scroll", sEv, { passive: true });
}
@kumarec
Copy link

kumarec commented Sep 10, 2022

Hi @idarek, could you please add the average engagement time?

@idarek
Copy link
Author

idarek commented Sep 10, 2022

Hi @idarek, could you please add the average engagement time?

It will be at some point, but that depend on my time availability. Next on my list is to add tracking for downloads using <a href with download> attribute before I look on engagement time.

I been looking into that for some time and found that it’s not as simple as you can imagine as there is more than one scenario to be considered like found by @jahilldev on his TypeScript GA4 equivalent.

It’s on my list for sure, just need to see when I will be able to take time on that in a pure JavaScript approach and simplified form.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment