diff --git a/img/bounce.svg b/img/bounce.svg
index 1f4d43a..7a8e919 100644
--- a/img/bounce.svg
+++ b/img/bounce.svg
@@ -1,56 +1 @@
-
-
+
\ No newline at end of file
diff --git a/img/info.svg b/img/info.svg
index f1daa8f..afd15a4 100644
--- a/img/info.svg
+++ b/img/info.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/img/more.svg b/img/more.svg
new file mode 100644
index 0000000..861d365
--- /dev/null
+++ b/img/more.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/img/page.svg b/img/page.svg
index 91f925b..a92a815 100644
--- a/img/page.svg
+++ b/img/page.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/img/referers.png b/img/referers.png
index 2fed645..ff68e5b 100644
Binary files a/img/referers.png and b/img/referers.png differ
diff --git a/plugin.info.txt b/plugin.info.txt
index 34ccea7..ec31480 100644
--- a/plugin.info.txt
+++ b/plugin.info.txt
@@ -1,7 +1,7 @@
base botmon
author Sascha Leib
email ad@hominem.com
-date 2025-09-15
+date 2025-09-16
name Bot Monitoring
desc A tool for monitoring and analysing bot traffic to your wiki (under development)
url https://www.dokuwiki.org/plugin:botmon
diff --git a/script.js b/script.js
index 39cd5fa..4b162af 100644
--- a/script.js
+++ b/script.js
@@ -578,9 +578,55 @@ BotMon.live = {
BotMon.live.gui.status.hideBusy('Done.');
},
+ // get a list of known bots:
+ getTopBots: function(max) {
+ //console.info('BotMon.live.data.analytics.getTopBots('+max+')');
+
+ //console.log(BotMon.live.data.analytics.groups.knownBots);
+
+ let botsList = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => {
+ return b._pageViews.length - a._pageViews.length;
+ });
+
+ const other = {
+ 'id': 'other',
+ 'name': "Others",
+ 'count': 0
+ };
+
+ const rList = [];
+ const max2 = ( botsList.length > max ? max-1 : botsList.length );
+ let total = 0; // adding up the items
+ for (let i=0; i
max2) {
+ rList.push(other);
+ };
+
+ rList.forEach( it => {
+ it.pct = (it.count * 100 / total);
+ });
+
+ return rList;
+ },
+
// Referer List:
- _refererList: [],
- _refererListCount: 0,
+ _refererList: [],
addToRefererList: function(ref) {
//console.log('BotMon.live.data.analytics.addToRefererList',ref);
@@ -592,18 +638,12 @@ BotMon.live = {
return;
}
- // find the referer ID:
- let refId = 'null';
- if (ref && ref.host) {
- const hArr = ref.host.split('.');
- const tld = hArr[hArr.length-1];
- refId = ( tld == 'localhost' ? tld : hArr[hArr.length-2] + ( tld.length > 3 ? '.' + tld : '' ) );
- }
+ const refInfo = me.getRefererInfo(ref);
// already exists?
let refObj = null;
for (let i = 0; i < me._refererList.length; i++) {
- if (me._refererList[i].id == refId) {
+ if (me._refererList[i].id == refInfo.id) {
refObj = me._refererList[i];
break;
}
@@ -611,16 +651,31 @@ BotMon.live = {
// if not exists, create it:
if (!refObj) {
- refObj = {
- id: refId,
- count: 0
- };
+ refObj = refInfo;
+ refObj.count = 1;
me._refererList.push(refObj);
} else {
refObj.count += 1;
}
- // add to total count:
- me._refererListCount += 1;
+ },
+
+ getRefererInfo: function(url) {
+ //console.log('BotMon.live.data.analytics.getRefererInfo',url);
+
+ // find the referer ID:
+ let refId = 'null';
+ let refName = 'No Referer';
+ if (url && url.host) {
+ const hArr = url.host.split('.');
+ const tld = hArr[hArr.length-1];
+ refId = ( tld == 'localhost' ? tld : hArr[hArr.length-2] + ( tld.length > 3 ? '.' + tld : '' ) );
+ refName = hArr[hArr.length-2] + '.' + tld;
+ }
+
+ return {
+ 'id': refId,
+ 'n': refName
+ };
},
getTopReferers: function(max) {
@@ -628,24 +683,53 @@ BotMon.live = {
const me = BotMon.live.data.analytics;
- const rList = []; // return array
+ return me._makeTopList(me._refererList, max);
+ },
+
+ _makeTopList: function(arr, max) {
+ //console.info(('BotMon.live.data.analytics._makeTopList(arr,' + max + ')'));
+
+ const me = BotMon.live.data.analytics;
// sort the list:
- me._refererList.sort( (a,b) => {
+ arr.sort( (a,b) => {
return b.count - a.count;
});
- // get the top:
- for (let i = 0; i < max; i++) {
- const it = me._refererList[i];
- const rIt = {
- id: it.id,
- count: it.count,
- pct: (it.count / me._refererListCount * 100).toFixed(0)
- };
- rList.push(rIt);
+ const rList = []; // return array
+ const max2 = ( arr.length >= max ? max-1 : arr.length );
+ const other = {
+ 'id': 'other',
+ 'name': "Others",
+ 'count': 0
+ };
+ let total = 0; // adding up the items
+ for (let i=0; Math.min(max, arr.length) > i; i++) {
+ const it = arr[i];
+ if (it) {
+ if (i < max2) {
+ const rIt = {
+ id: it.id,
+ name: (it.n ? it.n : it.id),
+ count: it.count
+ };
+ rList.push(rIt);
+ } else {
+ other.count += it.count;
+ }
+ total += it.count;
+ }
}
-
+
+ // add the "other" item, if needed:
+ if (arr.length > max2) {
+ rList.push(other);
+ };
+
+ rList.forEach( it => {
+ it.pct = (it.count * 100 / total);
+ });
+
return rList;
},
@@ -687,11 +771,11 @@ BotMon.live = {
}
if (arr) {
- let cRec = arr.find( it => it.iso == iso);
+ let cRec = arr.find( it => it.id == iso);
if (!cRec) {
cRec = {
- 'iso': iso,
- 'name': name,
+ 'id': iso,
+ 'n': name,
'count': 1
};
arr.push(cRec);
@@ -730,25 +814,10 @@ BotMon.live = {
break;
default:
console.warn(`Unknown user type ${type} in function getCountryList.`);
+ return;
}
- if (arr) {
- // sort by visit count:
- arr.sort( (a,b) => b.count - a.count);
-
- // reduce to only the top (max) items and create the target format:
- let rList = [];
- for (let i=0; Math.min(max, arr.length) > i; i++) {
- const cRec = arr[i];
- rList.push({
- 'iso': cRec.iso,
- 'name': cRec.name,
- 'count': cRec.count
- });
- }
- return rList;
- }
- return [];
+ return me._makeTopList(arr, max);
},
/* browser and platform of human visitors */
@@ -766,8 +835,9 @@ BotMon.live = {
let bRec = me._browsers.find( it => it.id == browserRec.id);
if (!bRec) {
bRec = {
- 'id': browserRec.id,
- 'count': 1
+ id: browserRec.id,
+ n: browserRec.n,
+ count: 1
};
me._browsers.push(bRec);
} else {
@@ -781,8 +851,9 @@ BotMon.live = {
let pRec = me._platforms.find( it => it.id == platformRec.id);
if (!pRec) {
pRec = {
- 'id': platformRec.id,
- 'count': 1
+ id: platformRec.id,
+ n: platformRec.n,
+ count: 1
};
me._platforms.push(pRec);
} else {
@@ -795,84 +866,15 @@ BotMon.live = {
getTopBrowsers: function(max) {
const me = BotMon.live.data.analytics;
-
- me._browsers.sort( (a,b) => b.count - a.count);
- // how many browsers to show:
- const max2 = ( me._browsers.length >= max ? max-1 : max );
-
- const rArr = []; // return array
- let total = 0;
- const others = {
- 'id': 'other',
- 'name': "Others",
- 'count': 0
- };
- for (let i=0; i < me._browsers.length; i++) {
- if (i < max2) {
- rArr.push({
- 'id': me._browsers[i].id,
- 'name': BotMon.live.data.clients.getName(me._browsers[i].id),
- 'count': me._browsers[i].count
- });
- total += me._browsers[i].count;
- } else {
- others.count += me._browsers[i].count;
- total += me._browsers[i].count;
- }
- };
-
- if (me._browsers.length > (max-1)) {
- rArr.push(others);
- };
-
- // update percentages:
- rArr.forEach( it => {
- it.pct = Math.round(it.count * 100 / total);
- });
-
- return rArr;
+ return me._makeTopList(me._browsers, max);
},
getTopPlatforms: function(max) {
const me = BotMon.live.data.analytics;
-
- me._platforms.sort( (a,b) => b.count - a.count);
- // how many browsers to show:
- const max2 = ( me._platforms.length >= max ? max-1 : max );
- const rArr = []; // return array
- let total = 0;
- const others = {
- 'id': 'other',
- 'name': "Others",
- 'count': 0
- };
- for (let i=0; i < me._platforms.length; i++) {
- if (i < max2) {
- rArr.push({
- 'id': me._platforms[i].id,
- 'name': BotMon.live.data.platforms.getName(me._platforms[i].id),
- 'count': me._platforms[i].count
- });
- total += me._platforms[i].count;
- } else {
- others.count += me._platforms[i].count;
- total += me._platforms[i].count;
- }
- };
-
- if (me._platforms.length > (max-1)) {
- rArr.push(others);
- };
-
- // update percentages:
- rArr.forEach( it => {
- it.pct = Math.round(it.count * 100 / total);
- });
-
- return rArr;
+ return me._makeTopList(me._platforms, max);
}
},
@@ -1490,14 +1492,16 @@ BotMon.live = {
const data = BotMon.live.data.analytics.data;
+ const maxItemsPerList = 5; // how many list items to show?
+
// shortcut for neater code:
const makeElement = BotMon.t._makeElement;
const botsVsHumans = document.getElementById('botmon__today__botsvshumans');
if (botsVsHumans) {
- botsVsHumans.appendChild(makeElement('dt', {}, "Bots vs. Humans"));
+ botsVsHumans.appendChild(makeElement('dt', {}, "Page views by category:"));
- for (let i = 3; i >= 0; i--) {
+ for (let i = 0; i <= 4; i++) {
const dd = makeElement('dd');
let title = '';
let value = '';
@@ -1518,6 +1522,10 @@ BotMon.live = {
title = "Known bots:";
value = data.bots.known;
break;
+ case 4:
+ title = "Total:";
+ value = data.totalPageViews;
+ break;
default:
console.warn(`Unknown list type ${i}.`);
}
@@ -1528,20 +1536,20 @@ BotMon.live = {
}
// update known bots list:
- const botlist = document.getElementById('botmon__botslist'); /* Known bots */
- botlist.innerHTML = "Known bots (top 5)";
+ const botElement = document.getElementById('botmon__botslist'); /* Known bots */
+ if (botElement) {
+ botElement.innerHTML = `Known bots (top ${maxItemsPerList})`;
- let bots = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => {
- return b._pageViews.length - a._pageViews.length;
- });
-
- for (let i=0; i < Math.min(bots.length, 5); i++) {
- const dd = makeElement('dd');
- dd.appendChild(makeElement('span', {'class': 'has_icon bot bot_' + bots[i]._bot.id }, bots[i]._bot.n));
- dd.appendChild(makeElement('span', undefined, bots[i]._pageViews.length));
- botlist.appendChild(dd);
+ let botList = BotMon.live.data.analytics.getTopBots(maxItemsPerList);
+ botList.forEach( (botInfo) => {
+ const bli = makeElement('dd');
+ bli.appendChild(makeElement('span', {'class': 'has_icon bot bot_' + botInfo.id }, botInfo.name));
+ bli.appendChild(makeElement('span', {'class': 'count' }, botInfo.count));
+ botElement.append(bli)
+ });
}
+
// update the suspected bot IP ranges list:
/*const botIps = document.getElementById('botmon__today__botips');
if (botIps) {
@@ -1559,11 +1567,11 @@ BotMon.live = {
// update the top bot countries list:
const botCountries = document.getElementById('botmon__today__countries');
if (botCountries) {
- botCountries.appendChild(makeElement('dt', {}, "Bot Countries (top 5)"));
+ botCountries.appendChild(makeElement('dt', {}, `Bot Countries (top ${maxItemsPerList})`));
const countryList = BotMon.live.data.analytics.getCountryList('likely_bot', 5);
countryList.forEach( (cInfo) => {
const cLi = makeElement('dd');
- cLi.appendChild(makeElement('span', {'class': 'has_icon country ctry_' + cInfo.iso.toLowerCase() }, cInfo.name));
+ cLi.appendChild(makeElement('span', {'class': 'has_icon country ctry_' + cInfo.id.toLowerCase() }, cInfo.name));
cLi.appendChild(makeElement('span', {'class': 'count' }, cInfo.count));
botCountries.appendChild(cLi);
});
@@ -1605,9 +1613,9 @@ BotMon.live = {
const wmclients = document.getElementById('botmon__today__wm_clients');
if (wmclients) {
- wmclients.appendChild(makeElement('dt', {}, "Top browsers"));
+ wmclients.appendChild(makeElement('dt', {}, "Browsers"));
- const clientList = BotMon.live.data.analytics.getTopBrowsers(5);
+ const clientList = BotMon.live.data.analytics.getTopBrowsers(maxItemsPerList);
if (clientList) {
clientList.forEach( (cInfo) => {
const cDd = makeElement('dd');
@@ -1615,7 +1623,7 @@ BotMon.live = {
cDd.appendChild(makeElement('span', {
'class': 'count',
'title': cInfo.count + " page views"
- }, Math.round(cInfo.pct) + '%'));
+ }, cInfo.pct.toFixed(1) + '%'));
wmclients.appendChild(cDd);
});
}
@@ -1625,9 +1633,9 @@ BotMon.live = {
const wmplatforms = document.getElementById('botmon__today__wm_platforms');
if (wmplatforms) {
- wmplatforms.appendChild(makeElement('dt', {}, "Top platforms"));
+ wmplatforms.appendChild(makeElement('dt', {}, "Platforms"));
- const pfList = BotMon.live.data.analytics.getTopPlatforms(5);
+ const pfList = BotMon.live.data.analytics.getTopPlatforms(maxItemsPerList);
if (pfList) {
pfList.forEach( (pInfo) => {
const pDd = makeElement('dd');
@@ -1635,7 +1643,7 @@ BotMon.live = {
pDd.appendChild(makeElement('span', {
'class': 'count',
'title': pInfo.count + " page views"
- }, Math.round(pInfo.pct) + '%'));
+ }, pInfo.pct.toFixed(1) + '%'));
wmplatforms.appendChild(pDd);
});
}
@@ -1645,17 +1653,17 @@ BotMon.live = {
const wmreferers = document.getElementById('botmon__today__wm_referers');
if (wmreferers) {
- wmreferers.appendChild(makeElement('dt', {}, "Top Referers"));
+ wmreferers.appendChild(makeElement('dt', {}, "Referers"));
- const refList = BotMon.live.data.analytics.getTopReferers(5);
+ const refList = BotMon.live.data.analytics.getTopReferers(maxItemsPerList);
if (refList) {
refList.forEach( (rInfo) => {
const rDd = makeElement('dd');
- rDd.appendChild(makeElement('span', {'class': 'has_icon referer ref_' + rInfo.id }, rInfo.id));
+ rDd.appendChild(makeElement('span', {'class': 'has_icon referer ref_' + rInfo.id }, rInfo.name));
rDd.appendChild(makeElement('span', {
'class': 'count',
'title': rInfo.count + " references"
- }, Math.round(rInfo.pct) + '%'));
+ }, rInfo.pct.toFixed(1) + '%'));
wmreferers.appendChild(rDd);
});
}
@@ -1859,6 +1867,15 @@ BotMon.live = {
}, ( data._country || "Unknown") ));
}
+ // referer icons:
+ if ((data._type == BM_USERTYPE.HUMAN || data._type == BM_USERTYPE.LIKELY_BOT) && data.ref) {
+ const refInfo = BotMon.live.data.analytics.getRefererInfo(new URL(data.ref));
+ span1.appendChild(make('span', {
+ 'class': 'icon_only referer ref_' + refInfo.id,
+ 'title': "Referer: " + data.ref
+ }, refInfo.n));
+ }
+
summary.appendChild(span1);
const span2 = make('span'); /* right-hand group */
@@ -1965,9 +1982,6 @@ BotMon.live = {
}, data._country + ' (' + data.geo + ')'));
}
- /*dl.appendChild(make('dt', {}, "Visitor Type:"));
- dl.appendChild(make('dd', undefined, data._type ));*/
-
dl.appendChild(make('dt', {}, "Session ID:"));
dl.appendChild(make('dd', {'class': 'has_icon session typ_' + data.typ}, data.id));
diff --git a/style.less b/style.less
index a112e57..55a86e5 100644
--- a/style.less
+++ b/style.less
@@ -51,6 +51,7 @@
&.bot_claude::before { background-position-y: -160px }
&.bot_applemsgs::before { background-position-y: -180px }
&.bot_reddit::before { background-position-y: -200px }
+ &.bot_other::before { background-image: url('img/more.svg') }
/* platform icons */
&.platform::before { background-image: url('img/platforms.png') }
@@ -68,6 +69,7 @@
&.pf_hmos::before { background-position-y: -220px }
&.pf_tizen::before { background-position-y: -240px }
&.pf_fire::before { background-position-y: -260px }
+ &.pf_other::before { background-image: url('img/more.svg') }
/* browser icons */
&.client::before { background-image: url('img/clients.png') }
@@ -89,6 +91,7 @@
&.cl_chromeold::before { background-position-y: -320px }
&.cl_ecosia::before { background-position-y: -340px }
&.cl_webkit::before { background-position-y: -360px }
+ &.cl_other::before { background-image: url('img/more.svg') }
/* Country flags */
/* Note: flag images and CSS adapted from: https://github.com/lafeber/world-flags-sprite/ */
@@ -353,6 +356,9 @@
&.ref_duckduckgo::before { background-position-y: -120px }
&.ref_ecosia::before { background-position-y: -140px }
&.ref_yandex::before { background-position-y: -160px }
+ &.ref_chatgpt::before { background-position-y: -180px }
+ &.ref_brave::before { background-position-y: -200px }
+ &.ref_other::before { background-image: url('img/more.svg') }
}
/* grid layout for the overview: */