Skip to content

Instantly share code, notes, and snippets.

@idarek
Last active February 28, 2024 08:40
  • Star 57 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save idarek/9ade69ac2a2ef00d98ab950426af5791 to your computer and use it in GitHub Desktop.
/* https://go.tacodewolff.nl/minify */
enScroll=!1,enFdl=!1,extCurrent=void 0,filename=void 0,targetText=void 0,splitOrigin=void 0;const lStor=localStorage,sStor=sessionStorage,doc=document,docEl=document.documentElement,docBody=document.body,docLoc=document.location,w=window,s=screen,nav=navigator||{},extensions=["pdf","xls","xlsx","doc","docx","txt","rtf","csv","exe","key","pps","ppt","pptx","7z","pkg","rar","gz","zip","avi","mov","mp4","mpe","mpeg","wmv","mid","midi","mp3","wav","wma"];function a(e,t,n,o){const j="G-XXXXXXXXXX",r=()=>Math.floor(Math.random()*1e9)+1,c=()=>Math.floor(Date.now()/1e3),F=()=>(sStor._p||(sStor._p=r()),sStor._p),E=()=>r()+"."+c(),_=()=>(lStor.cid_v4||(lStor.cid_v4=E()),lStor.cid_v4),m=lStor.getItem("cid_v4"),v=()=>m?void 0:enScroll==!0?void 0:"1",p=()=>(sStor.sid||(sStor.sid=c()),sStor.sid),O=()=>{if(!sStor._ss)return sStor._ss="1",sStor._ss;if(sStor.getItem("_ss")=="1")return void 0},a="1",g=()=>{if(sStor.sct)if(enScroll==!0)return sStor.sct;else x=+sStor.getItem("sct")+ +a,sStor.sct=x;else sStor.sct=a;return sStor.sct},i=docLoc.search,b=new URLSearchParams(i),h=["q","s","search","query","keyword"],y=h.some(e=>i.includes("&"+e+"=")||i.includes("?"+e+"=")),u=()=>y==!0?"view_search_results":enScroll==!0?"scroll":enFdl==!0?"file_download":"page_view",f=()=>enScroll==!0?"90":void 0,C=()=>{if(u()=="view_search_results"){for(let e of b)if(h.includes(e[0]))return e[1]}else return void 0},d=encodeURIComponent,k=e=>{let t=[];for(let n in e)e.hasOwnProperty(n)&&e[n]!==void 0&&t.push(d(n)+"="+d(e[n]));return t.join("&")},A=!1,S="https://www.google-analytics.com/g/collect",M=k({v:"2",tid:j,_p:F(),sr:(s.width*w.devicePixelRatio+"x"+s.height*w.devicePixelRatio).toString(),ul:(nav.language||void 0).toLowerCase(),cid:_(),_fv:v(),_s:"1",dl:docLoc.origin+docLoc.pathname+i,dt:doc.title||void 0,dr:doc.referrer||void 0,sid:p(),sct:g(),seg:"1",en:u(),"epn.percent_scrolled":f(),"ep.search_term":C(),"ep.file_extension":e||void 0,"ep.file_name":t||void 0,"ep.link_text":n||void 0,"ep.link_url":o||void 0,_ss:O(),_dbg:A?1:void 0}),l=S+"?"+M;if(nav.sendBeacon)nav.sendBeacon(l);else{let e=new XMLHttpRequest;e.open("POST",l,!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}),enScroll=!1}document.addEventListener("DOMContentLoaded",function(){let e=document.getElementsByTagName("a");for(let t=0;t<e.length;t++)if(e[t].getAttribute("href")!=null){const n=e[t].getAttribute("href"),s=n.substring(n.lastIndexOf("/")+1),o=s.split(".").pop();(e[t].hasAttribute("download")||extensions.includes(o))&&e[t].addEventListener("click",fDl,{passive:!0})}});function fDl(e){enFdl=!0;const t=e.currentTarget.getAttribute("href"),n=t.substring(t.lastIndexOf("/")+1),s=n.split(".").pop(),o=n.replace("."+s,""),i=e.currentTarget.text,r=t.replace(docLoc.origin,"");a(s,o,i,r),enFdl=!1}
// Version 1.10.200923
// Changelog:
// 1.10
// - added ability to track `<a href` links to files with specified extensions and all these links where there is a `download` attribute specified independently of the extension of the file.
//
// 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;
enFdl = false;
extCurrent = undefined;
filename = undefined;
targetText = undefined;
splitOrigin = undefined;
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 || {};
const extensions = ["pdf", "xls", "xlsx", "doc", "docx", "txt", "rtf", "csv", "exe", "key", "pps", "ppt", "pptx", "7z", "pkg", "rar", "gz", "zip", "avi", "mov", "mp4", "mpe", "mpeg", "wmv", "mid", "midi", "mp3", "wav", "wma"];
function a(extCurrent, filename, targetText, splitOrigin) {
// 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 if (enFdl == true) {
return "file_download";
}
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
"ep.file_extension": extCurrent || undefined,
"ep.file_name": filename || undefined,
"ep.link_text": targetText || undefined,
"ep.link_url": splitOrigin || undefined,
_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 });
enScroll = false;
}
// file download listener
document.addEventListener("DOMContentLoaded", function() {
let Anchors = document.getElementsByTagName("a");
for (let i = 0; i < Anchors.length; i++) {
if (Anchors[i].getAttribute("href") != null) {
const url = Anchors[i].getAttribute("href");
const file = url.substring(url.lastIndexOf("/") + 1);
const ext = file.split(".").pop();
/* if any anchor got download attribute, add event listener */
/* and if any anchor have acceptable extension */
if (Anchors[i].hasAttribute("download") || extensions.includes(ext)) {
Anchors[i].addEventListener("click", fDl, { passive: true });
}
}
}
});
// file download Event
function fDl(e) {
enFdl = true;
const urlCurrent = e.currentTarget.getAttribute("href");
const fileCurrent = urlCurrent.substring(urlCurrent.lastIndexOf("/") + 1);
const extCurrent = fileCurrent.split(".").pop();
const filename = fileCurrent.replace("." + extCurrent, "");
const targetText = e.currentTarget.text;
const splitOrigin = urlCurrent.replace(docLoc.origin, "");
// fire analytics script
a(extCurrent, filename, targetText, splitOrigin);
enFdl = false;
}
@jahilldev
Copy link

@idarek Hey, thanks for the effort thus far on this. Good work!

I've been integrating this into my own project, re-writing in TypeScript, and I noticed the script above is missing the 90% scroll event. I've got this working now with my modified script, but you basically do something like this:

document.addEventListener('scroll', debounce(() => {
  const scrollPercentage = 0; // calculate this

  if (percentage < 90) {
    return;
  }

  track({ type: 'scroll', event: { 'epn.percent_scrolled': 90 } });

  document.removeEventListener('scroll', scrollEvent);
});

I'm using a generic track function, but all that does above is set the params en=scroll and epn.percent_scrolled=90

@idarek
Copy link
Author

idarek commented May 11, 2022

Appreciate @jahilldev. Adding a listener would open options for some other aspects of this analytics solution. Not sure if this is what will be expected from Minimal Analytics. The idea is to track and forget approach, so the analytics is not tracking users in the background but I think it all depends on what the end-user need. JavaScript and TypeScrypt is something that I am natively good at as learning along when creating something, but goods thing to start further development :)

ps. the script is missing also engagement time and other default events that are present in the full script.

@jahilldev
Copy link

@idarek I'm thinking of creating a series of packages that provide minimal implementations of popular analytics libraries, starting with GA4. I'd really like for you to be a contributor given you put in the initial work on this: https://github.com/jahilldev/minimal-analytics

Let me know if that's something you want to do 👍

the script is missing also engagement time and other default events that are present in the full script.

Yeh I've noticed this, I think I'm going to make things like this opt-in so users can either keep it super basic (and small), or add things like scroll tracking and engagement time, etc.

@sifigi4335
Copy link

@jahilldev your code looks promising (though I haven't figured out yet how to use it). Do you intend to include engagement time?

@jahilldev
Copy link

@carerragt Thanks 👍

Yes, it's not 100% finished yet, but once it is I'm going to attempt to cover all basic data, engagement, scroll, etc.

Once ready, I'll write the README with instructions and publish to NPM.

@idarek
Copy link
Author

idarek commented May 15, 2022

Hi @jahilldev happy to help if I can. Let's see how this will be going.

@jahilldev
Copy link

jahilldev commented May 17, 2022

@carerragt I've just published the first version of GA4, it tracks page_view, scroll (90%) and user_engagement by default.

https://github.com/jahilldev/minimal-analytics/tree/main/packages/ga4#readme

Feel free to open issues if you find any 👍

@sifigi4335
Copy link

@carerragt I've just published the first version of GA4, it tracks page_view, scroll (90%) and user_engagement by default.

https://github.com/jahilldev/minimal-analytics/tree/main/packages/ga4#readme

Feel free to open issues if you find any 👍

Alright! Thanks.

@xerc
Copy link

xerc commented Jun 29, 2022

hey @idarek , thank for your GAv4 JS just want to comment on mixed usage of quotes ; best would be only double-quote @ min ref.

@idarek
Copy link
Author

idarek commented Jun 29, 2022

hey @idarek , thank for your GAv4 JS just want to comment on mixed usage of quotes ; best would be only double-quote @ min ref.

Thanks. I do not control minifying process and relay on this. Noticed that some minifyers are breaking the code so found this to be working. Would like to not amend manually it. If you can point changes to be made in unminified version I would greatly appreciate.

@xerc
Copy link

xerc commented Jun 29, 2022

just search for ' and replace with " ; s/'/"/g

@idarek
Copy link
Author

idarek commented Jun 29, 2022

just search for ' and replace with " ; s/'/"/g

Done, thank you.

@ChipMinder
Copy link

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.

@alfred-tuinman
Copy link

Thanks a ton Dariusz for posting this! I am delighted to use it.

@idarek
Copy link
Author

idarek commented May 9, 2023

Glad you like it. Still some work on this can be done but need to find time for that :)

@idarek
Copy link
Author

idarek commented Sep 21, 2023

To who is subscribing to the comments here. I just published version 1.10 which includes tracking of file downloads through specified extensions or download attributes in the link.

@xerc
Copy link

xerc commented Sep 27, 2023

@idarek thanks for the efford ; may think about a "wrapper" (like @ https://minimalanalytics.com) to prevent var leakage & params would be at the end for easier conf. at the user side

@idarek
Copy link
Author

idarek commented Sep 27, 2023

Thanks, @xerc I will have a look at that, however I need to learn about that. My knowledge is limited to what I am able to learn from examples over the internet. This is how I build this right now. Appreciate any links or pointing into some guides to help me with that.

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