2025-08-29 23:14:10 +03:00
|
|
|
|
/* DokuWiki Monitor Plugin Script file */
|
2025-08-30 18:40:16 +03:00
|
|
|
|
/* 30.08.2025 - 0.1.5 - pre-release */
|
2025-08-29 23:14:10 +03:00
|
|
|
|
/* Authors: Sascha Leib <ad@hominem.info> */
|
|
|
|
|
|
|
|
|
|
|
|
const Monitor = {
|
|
|
|
|
|
|
|
|
|
|
|
init: function() {
|
|
|
|
|
|
//console.info('Monitor.init()');
|
|
|
|
|
|
|
|
|
|
|
|
// find the plugin basedir:
|
|
|
|
|
|
this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/'))
|
|
|
|
|
|
+ '/plugins/monitor/';
|
|
|
|
|
|
|
|
|
|
|
|
// read the page language from the DOM:
|
|
|
|
|
|
this._lang = document.getRootNode().documentElement.lang || this._lang;
|
|
|
|
|
|
|
|
|
|
|
|
// get the time offset:
|
|
|
|
|
|
this._timeDiff = Monitor.t._getTimeOffset();
|
|
|
|
|
|
|
|
|
|
|
|
// init the sub-objects:
|
|
|
|
|
|
Monitor.t._callInit(this);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
_baseDir: null,
|
|
|
|
|
|
_lang: 'en',
|
|
|
|
|
|
_today: (new Date()).toISOString().slice(0, 10),
|
|
|
|
|
|
_timeDiff: '',
|
|
|
|
|
|
|
|
|
|
|
|
/* internal tools */
|
|
|
|
|
|
t: {
|
|
|
|
|
|
|
|
|
|
|
|
/* helper function to call inits of sub-objects */
|
|
|
|
|
|
_callInit: function(obj) {
|
|
|
|
|
|
//console.info('Monitor.t._callInit(obj=',obj,')');
|
|
|
|
|
|
|
|
|
|
|
|
/* call init / _init on each sub-object: */
|
|
|
|
|
|
Object.keys(obj).forEach( (key,i) => {
|
|
|
|
|
|
const sub = obj[key];
|
|
|
|
|
|
let init = null;
|
|
|
|
|
|
if (typeof sub === 'object' && sub.init) {
|
|
|
|
|
|
init = sub.init;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// bind to object
|
|
|
|
|
|
if (typeof init == 'function') {
|
|
|
|
|
|
const init2 = init.bind(sub);
|
|
|
|
|
|
init2(obj);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* helper function to calculate the time difference to UTC: */
|
|
|
|
|
|
_getTimeOffset: function() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
let offset = now.getTimezoneOffset(); // in minutes
|
|
|
|
|
|
const sign = Math.sign(offset); // +1 or -1
|
|
|
|
|
|
offset = Math.abs(offset); // always positive
|
|
|
|
|
|
|
|
|
|
|
|
let hours = 0;
|
|
|
|
|
|
while (offset >= 60) {
|
|
|
|
|
|
hours += 1;
|
|
|
|
|
|
offset -= 60;
|
|
|
|
|
|
}
|
|
|
|
|
|
return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : '');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-29 23:28:19 +03:00
|
|
|
|
};
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
/* everything specific to the "Today" tab is self-contained in the "live" object: */
|
2025-08-30 13:01:50 +03:00
|
|
|
|
Monitor.live = {
|
2025-08-29 23:14:10 +03:00
|
|
|
|
init: function() {
|
2025-08-30 13:01:50 +03:00
|
|
|
|
//console.info('Monitor.live.init()');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// set the title:
|
2025-08-30 18:40:16 +03:00
|
|
|
|
const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (Monitor._timeDiff != '' ? `, ${Monitor._timeDiff}` : '' ) + ')';
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.setTitle(`Showing data for <time datetime=${Monitor._today}>${Monitor._today}</time> ${tDiff}`);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// init sub-objects:
|
|
|
|
|
|
Monitor.t._callInit(this);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
data: {
|
|
|
|
|
|
init: function() {
|
2025-08-30 13:01:50 +03:00
|
|
|
|
//console.info('Monitor.live.data.init()');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// call sub-inits:
|
|
|
|
|
|
Monitor.t._callInit(this);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// this will be called when the known json files are done loading:
|
|
|
|
|
|
_dispatch: function(file) {
|
|
|
|
|
|
//console.info('Monitor.live.data._dispatch(,',file,')');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// shortcut to make code more readable:
|
|
|
|
|
|
const data = Monitor.live.data;
|
|
|
|
|
|
|
|
|
|
|
|
// set the flags:
|
|
|
|
|
|
switch(file) {
|
|
|
|
|
|
case 'bots':
|
|
|
|
|
|
data._dispatchBotsLoaded = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'clients':
|
|
|
|
|
|
data._dispatchClientsLoaded = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'platforms':
|
|
|
|
|
|
data._dispatchPlatformsLoaded = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// are all the flags set?
|
|
|
|
|
|
if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded) {
|
|
|
|
|
|
// chain the log files loading:
|
|
|
|
|
|
Monitor.live.data.loadLogFile('srv', Monitor.live.data._onServerLogLoaded);
|
|
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
},
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// flags to track which data files have been loaded:
|
|
|
|
|
|
_dispatchBotsLoaded: false,
|
|
|
|
|
|
_dispatchClientsLoaded: false,
|
|
|
|
|
|
_dispatchPlatformsLoaded: false,
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// event callback, after the server log has been loaded:
|
2025-08-30 13:01:50 +03:00
|
|
|
|
_onServerLogLoaded: function() {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
//console.info('Monitor.live.data._onServerLogLoaded()');
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// chain the client log file to load:
|
2025-08-30 13:01:50 +03:00
|
|
|
|
Monitor.live.data.loadLogFile('log', Monitor.live.data._onClientLogLoaded);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// event callback, after the client log has been loaded:
|
2025-08-30 13:01:50 +03:00
|
|
|
|
_onClientLogLoaded: function() {
|
|
|
|
|
|
console.info('Monitor.live.data._onClientLogLoaded()');
|
|
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// chain the ticks file to load:
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.data.loadLogFile('tck', Monitor.live.data._onTicksLogLoaded);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// event callback, after the tiker log has been loaded:
|
|
|
|
|
|
_onTicksLogLoaded: function() {
|
|
|
|
|
|
console.info('Monitor.live.data._onTicksLogLoaded()');
|
|
|
|
|
|
|
|
|
|
|
|
// analyse the data:
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.data.analytics.analyseAll();
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// sort the data:
|
|
|
|
|
|
// #TODO
|
|
|
|
|
|
|
|
|
|
|
|
// display the data:
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.overview.make();
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
|
|
|
|
|
console.log(Monitor.live.data.model._visitors);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
model: {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// visitors storage:
|
2025-08-30 13:01:50 +03:00
|
|
|
|
_visitors: [],
|
|
|
|
|
|
|
|
|
|
|
|
// find an already existing visitor record:
|
|
|
|
|
|
findVisitor: function(id) {
|
|
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
|
|
|
|
|
const model = Monitor.live.data.model;
|
|
|
|
|
|
|
|
|
|
|
|
// loop over all visitors already registered:
|
|
|
|
|
|
for (let i=0; i<model._visitors.length; i++) {
|
|
|
|
|
|
const v = model._visitors[i];
|
|
|
|
|
|
if (v && v.id == id) return v;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null; // nothing found
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* if there is already this visit registered, return it (used for updates) */
|
|
|
|
|
|
_getVisit: function(visit, view) {
|
|
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
// shortcut to make code more readable:
|
|
|
|
|
|
const model = Monitor.live.data.model;
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
for (let i=0; i<visit._pageViews.length; i++) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
const pv = visit._pageViews[i];
|
|
|
|
|
|
if (pv.pg == view.pg && // same page id, and
|
|
|
|
|
|
view.ts.getTime() - pv._firstSeen.getTime() < 1200000) { // seen less than 20 minutes ago
|
|
|
|
|
|
return pv; // it is the same visit.
|
2025-08-30 13:01:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null; // not found
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// register a new visitor (or update if already exists)
|
|
|
|
|
|
registerVisit: function(dat) {
|
|
|
|
|
|
//console.info('registerVisit', dat);
|
|
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
|
|
|
|
|
const model = Monitor.live.data.model;
|
|
|
|
|
|
|
|
|
|
|
|
// check if it already exists:
|
|
|
|
|
|
let visitor = model.findVisitor(dat.id);
|
|
|
|
|
|
if (!visitor) {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
const bot = Monitor.live.data.bots.match(dat.client);
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
model._visitors.push(dat);
|
|
|
|
|
|
visitor = dat;
|
|
|
|
|
|
visitor._firstSeen = dat.ts;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
visitor._lastSeen = dat.ts;
|
|
|
|
|
|
visitor._isBot = ( bot ? 1.0 : 0.0 ); // likelihood of being a bot; primed to 0% or 100% in case of a known bot
|
2025-08-30 13:01:50 +03:00
|
|
|
|
visitor._pageViews = []; // array of page views
|
|
|
|
|
|
visitor._hasReferrer = false; // has at least one referrer
|
|
|
|
|
|
visitor._jsClient = false; // visitor has been seen logged by client js as well
|
2025-08-30 18:40:16 +03:00
|
|
|
|
visitor._client = bot ?? Monitor.live.data.clients.match(dat.client) ?? null; // client info (browser, bot, etc.)
|
|
|
|
|
|
visitor._platform = Monitor.live.data.platforms.match(dat.client); // platform info
|
|
|
|
|
|
|
|
|
|
|
|
// known bots get the bot ID as identifier:
|
|
|
|
|
|
if (bot) visitor.id = bot.id;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// find browser
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// is this visit already registered?
|
|
|
|
|
|
let prereg = model._getVisit(visitor, dat);
|
|
|
|
|
|
if (!prereg) {
|
|
|
|
|
|
// add the page view to the visitor:
|
|
|
|
|
|
prereg = {
|
|
|
|
|
|
_by: 'srv',
|
2025-08-30 18:40:16 +03:00
|
|
|
|
ip: dat.ip,
|
2025-08-30 13:01:50 +03:00
|
|
|
|
pg: dat.pg,
|
|
|
|
|
|
ref: dat.ref || '',
|
|
|
|
|
|
_firstSeen: dat.ts,
|
|
|
|
|
|
_lastSeen: dat.ts,
|
|
|
|
|
|
_jsClient: false
|
|
|
|
|
|
};
|
|
|
|
|
|
visitor._pageViews.push(prereg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// update referrer state:
|
|
|
|
|
|
visitor._hasReferrer = visitor._hasReferrer ||
|
|
|
|
|
|
(prereg.ref !== undefined && prereg.ref !== '');
|
|
|
|
|
|
|
|
|
|
|
|
// update time stamp for last-seen:
|
|
|
|
|
|
visitor._lastSeen = dat.ts;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// if needed:
|
|
|
|
|
|
return visitor;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// updating visit data from the client-side log:
|
|
|
|
|
|
updateVisit: function(dat) {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
//console.info('updateVisit', dat);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
|
|
|
|
|
const model = Monitor.live.data.model;
|
|
|
|
|
|
|
|
|
|
|
|
let visitor = model.findVisitor(dat.id);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
if (!visitor) {
|
|
|
|
|
|
visitor = model.registerVisit(dat);
|
|
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
if (visitor) {
|
|
|
|
|
|
visitor._lastSeen = dat.ts;
|
|
|
|
|
|
visitor._jsClient = true; // seen by client js
|
|
|
|
|
|
} else {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
console.warn(`No visit with ID ${dat.id}.`);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// find the page view:
|
|
|
|
|
|
let prereg = model._getVisit(visitor, dat);
|
|
|
|
|
|
if (prereg) {
|
|
|
|
|
|
// update the page view:
|
|
|
|
|
|
prereg._lastSeen = dat.ts;
|
|
|
|
|
|
prereg._jsClient = true; // seen by client js
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// add the page view to the visitor:
|
|
|
|
|
|
prereg = {
|
|
|
|
|
|
_by: 'log',
|
2025-08-30 18:40:16 +03:00
|
|
|
|
ip: dat.ip,
|
2025-08-30 13:01:50 +03:00
|
|
|
|
pg: dat.pg,
|
|
|
|
|
|
ref: dat.ref || '',
|
|
|
|
|
|
_firstSeen: dat.ts,
|
|
|
|
|
|
_lastSeen: dat.ts,
|
|
|
|
|
|
_jsClient: true
|
|
|
|
|
|
};
|
|
|
|
|
|
visitor._pageViews.push(prereg);
|
|
|
|
|
|
}
|
2025-08-30 18:40:16 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// updating visit data from the ticker log:
|
|
|
|
|
|
updateTicks: function(dat) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
//console.info('updateTicks', dat);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
|
|
|
|
|
const model = Monitor.live.data.model;
|
|
|
|
|
|
|
|
|
|
|
|
// find the visit info:
|
|
|
|
|
|
let visitor = model.findVisitor(dat.id);
|
2025-08-30 21:11:01 +03:00
|
|
|
|
if (!visitor) {
|
|
|
|
|
|
console.warn(`No visitor with ID ${dat.id}, registering a new one.`);
|
|
|
|
|
|
visitor = model.registerVisit(dat);
|
|
|
|
|
|
}
|
2025-08-30 18:40:16 +03:00
|
|
|
|
if (visitor) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
// update "last seen":
|
|
|
|
|
|
if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts;
|
|
|
|
|
|
|
|
|
|
|
|
// get the page view info:
|
|
|
|
|
|
const pv = model._getVisit(visitor, dat);
|
|
|
|
|
|
if (pv) {
|
|
|
|
|
|
// update the page view info:
|
|
|
|
|
|
if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn(`No page view for visit ID ${dat.id}, page ${dat.pg}, registering a new one.`);
|
|
|
|
|
|
|
|
|
|
|
|
// add a new page view to the visitor:
|
|
|
|
|
|
const newPv = {
|
|
|
|
|
|
_by: 'tck',
|
|
|
|
|
|
ip: dat.ip,
|
|
|
|
|
|
pg: dat.pg,
|
|
|
|
|
|
ref: '',
|
|
|
|
|
|
_firstSeen: dat.ts,
|
|
|
|
|
|
_lastSeen: dat.ts,
|
|
|
|
|
|
_jsClient: false
|
|
|
|
|
|
};
|
|
|
|
|
|
visitor._pageViews.push(newPv);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
} else {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
console.warn(`No visit with ID ${dat.id}.`);
|
|
|
|
|
|
return;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
analytics: {
|
|
|
|
|
|
|
|
|
|
|
|
init: function() {
|
|
|
|
|
|
console.info('Monitor.live.data.analytics.init()');
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// data storage:
|
|
|
|
|
|
data: {
|
|
|
|
|
|
totalVisits: 0,
|
|
|
|
|
|
totalPageViews: 0,
|
|
|
|
|
|
bots: {
|
|
|
|
|
|
known: 0,
|
2025-09-01 15:42:06 +02:00
|
|
|
|
likely: 0,
|
|
|
|
|
|
human: 0,
|
|
|
|
|
|
users: 0
|
2025-08-30 21:11:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// sort the visits by type:
|
|
|
|
|
|
groups: {
|
|
|
|
|
|
knownBots: [],
|
2025-09-01 15:42:06 +02:00
|
|
|
|
likelyBots: [],
|
2025-08-30 21:11:01 +03:00
|
|
|
|
humans: [],
|
|
|
|
|
|
users: []
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// all analytics
|
|
|
|
|
|
analyseAll: function() {
|
|
|
|
|
|
//console.info('Monitor.live.data.analytics.analyseAll()');
|
|
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
|
|
|
|
|
const model = Monitor.live.data.model;
|
|
|
|
|
|
|
|
|
|
|
|
// loop over all visitors:
|
|
|
|
|
|
model._visitors.forEach( (v) => {
|
|
|
|
|
|
|
|
|
|
|
|
// count visits and page views:
|
|
|
|
|
|
this.data.totalVisits += 1;
|
|
|
|
|
|
this.data.totalPageViews += v._pageViews.length;
|
|
|
|
|
|
|
|
|
|
|
|
// check for typical bot aspects:
|
|
|
|
|
|
let botScore = v._isBot; // start with the known bot score
|
|
|
|
|
|
|
|
|
|
|
|
if (v._isBot >= 1.0) { // known bots
|
|
|
|
|
|
|
|
|
|
|
|
this.data.bots.known += 1;
|
|
|
|
|
|
this.groups.knownBots.push(v);
|
|
|
|
|
|
|
|
|
|
|
|
} if (v.usr && v.usr != '') { // known users
|
|
|
|
|
|
this.groups.users.push(v);
|
2025-09-01 15:42:06 +02:00
|
|
|
|
this.data.bots.users += 1;
|
2025-08-30 21:11:01 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
// not a known bot, nor a known user; check other aspects:
|
|
|
|
|
|
|
|
|
|
|
|
// no referrer at all:
|
|
|
|
|
|
if (!v._hasReferrer) botScore += 0.2;
|
|
|
|
|
|
|
|
|
|
|
|
// no js client logging:
|
|
|
|
|
|
if (!v._jsClient) botScore += 0.2;
|
|
|
|
|
|
|
|
|
|
|
|
// average time between page views less than 30s:
|
|
|
|
|
|
if (v._pageViews.length > 1) {
|
|
|
|
|
|
botScore -= 0.2; // more than one view: good!
|
|
|
|
|
|
let totalDiff = 0;
|
|
|
|
|
|
for (let i=1; i<v._pageViews.length; i++) {
|
|
|
|
|
|
const diff = v._pageViews[i]._firstSeen.getTime() - v._pageViews[i-1]._lastSeen.getTime();
|
|
|
|
|
|
totalDiff += diff;
|
|
|
|
|
|
}
|
|
|
|
|
|
const avgDiff = totalDiff / (v._pageViews.length - 1);
|
|
|
|
|
|
if (avgDiff < 30000) botScore += 0.2;
|
|
|
|
|
|
else if (avgDiff < 60000) botScore += 0.1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// decide based on the score:
|
|
|
|
|
|
if (botScore >= 0.5) {
|
2025-09-01 15:42:06 +02:00
|
|
|
|
this.data.bots.likely += 1;
|
|
|
|
|
|
this.groups.likelyBots.push(v);
|
2025-08-30 21:11:01 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
this.data.bots.human += 1;
|
|
|
|
|
|
this.groups.humans.push(v);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log(this.data);
|
|
|
|
|
|
console.log(this.groups);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-29 23:14:10 +03:00
|
|
|
|
bots: {
|
|
|
|
|
|
// loads the list of known bots from a JSON file:
|
|
|
|
|
|
init: async function() {
|
2025-08-30 13:01:50 +03:00
|
|
|
|
//console.info('Monitor.live.data.bots.init()');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// Load the list of known bots:
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.showBusy("Loading known bots …");
|
2025-08-29 23:14:10 +03:00
|
|
|
|
const url = Monitor._baseDir + 'data/known-bots.json';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
Monitor.live.data.bots._list = await response.json();
|
|
|
|
|
|
Monitor.live.data.bots._ready = true;
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// TODO: allow using the bots list...
|
|
|
|
|
|
} catch (error) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.setError("Error while loading the ’known bots’ file: " + error.message);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
} finally {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.hideBusy("Done.");
|
2025-08-30 18:40:16 +03:00
|
|
|
|
Monitor.live.data._dispatch('bots')
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// returns bot info if the clientId matches a known bot, null otherwise:
|
2025-08-30 18:40:16 +03:00
|
|
|
|
match: function(client) {
|
|
|
|
|
|
//console.info('Monitor.live.data.bots.match(',client,')');
|
|
|
|
|
|
|
|
|
|
|
|
if (client) {
|
|
|
|
|
|
for (let i=0; i<Monitor.live.data.bots._list.length; i++) {
|
|
|
|
|
|
const bot = Monitor.live.data.bots._list[i];
|
|
|
|
|
|
for (let j=0; j<bot.rx.length; j++) {
|
|
|
|
|
|
if (client.match(new RegExp(bot.rx[j]))) {
|
|
|
|
|
|
return bot; // found a match
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null; // not found!
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// indicates if the list is loaded and ready to use:
|
|
|
|
|
|
_ready: false,
|
|
|
|
|
|
|
|
|
|
|
|
// the actual bot list is stored here:
|
|
|
|
|
|
_list: []
|
|
|
|
|
|
},
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
clients: {
|
|
|
|
|
|
// loads the list of known clients from a JSON file:
|
|
|
|
|
|
init: async function() {
|
|
|
|
|
|
//console.info('Monitor.live.data.clients.init()');
|
|
|
|
|
|
|
|
|
|
|
|
// Load the list of known bots:
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.showBusy("Loading known clients");
|
2025-08-30 18:40:16 +03:00
|
|
|
|
const url = Monitor._baseDir + 'data/known-clients.json';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Monitor.live.data.clients._list = await response.json();
|
|
|
|
|
|
Monitor.live.data.clients._ready = true;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.setError("Error while loading the known clients file: " + error.message);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
} finally {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.hideBusy("Done.");
|
2025-08-30 18:40:16 +03:00
|
|
|
|
Monitor.live.data._dispatch('clients')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// returns bot info if the clientId matches a known bot, null otherwise:
|
|
|
|
|
|
match: function(cid) {
|
|
|
|
|
|
//console.info('Monitor.live.data.clients.match(',cid,')');
|
|
|
|
|
|
|
|
|
|
|
|
let match = {"n": "Unknown", "v": -1, "id": null};
|
|
|
|
|
|
|
|
|
|
|
|
if (cid) {
|
|
|
|
|
|
Monitor.live.data.clients._list.find(client => {
|
|
|
|
|
|
let r = false;
|
|
|
|
|
|
for (let j=0; j<client.rx.length; j++) {
|
|
|
|
|
|
const rxr = cid.match(new RegExp(client.rx[j]));
|
|
|
|
|
|
if (rxr) {
|
|
|
|
|
|
match.n = client.n;
|
|
|
|
|
|
match.v = (rxr.length > 1 ? rxr[1] : -1);
|
|
|
|
|
|
match.id = client.id || null;
|
|
|
|
|
|
r = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return match;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// indicates if the list is loaded and ready to use:
|
|
|
|
|
|
_ready: false,
|
|
|
|
|
|
|
|
|
|
|
|
// the actual bot list is stored here:
|
|
|
|
|
|
_list: []
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
platforms: {
|
|
|
|
|
|
// loads the list of known platforms from a JSON file:
|
|
|
|
|
|
init: async function() {
|
|
|
|
|
|
//console.info('Monitor.live.data.platforms.init()');
|
|
|
|
|
|
|
|
|
|
|
|
// Load the list of known bots:
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.showBusy("Loading known platforms");
|
2025-08-30 18:40:16 +03:00
|
|
|
|
const url = Monitor._baseDir + 'data/known-platforms.json';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Monitor.live.data.platforms._list = await response.json();
|
|
|
|
|
|
Monitor.live.data.platforms._ready = true;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.setError("Error while loading the known platforms file: " + error.message);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
} finally {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.hideBusy("Done.");
|
2025-08-30 18:40:16 +03:00
|
|
|
|
Monitor.live.data._dispatch('platforms')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// returns bot info if the browser id matches a known platform:
|
|
|
|
|
|
match: function(cid) {
|
|
|
|
|
|
//console.info('Monitor.live.data.platforms.match(',cid,')');
|
|
|
|
|
|
|
|
|
|
|
|
let match = {"n": "Unknown", "id": null};
|
|
|
|
|
|
|
|
|
|
|
|
if (cid) {
|
|
|
|
|
|
Monitor.live.data.platforms._list.find(platform => {
|
|
|
|
|
|
let r = false;
|
|
|
|
|
|
for (let j=0; j<platform.rx.length; j++) {
|
|
|
|
|
|
const rxr = cid.match(new RegExp(platform.rx[j]));
|
|
|
|
|
|
if (rxr) {
|
|
|
|
|
|
match.n = platform.n;
|
|
|
|
|
|
match.v = (rxr.length > 1 ? rxr[1] : -1);
|
|
|
|
|
|
match.id = platform.id || null;
|
|
|
|
|
|
r = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return match;
|
2025-08-29 23:14:10 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// indicates if the list is loaded and ready to use:
|
|
|
|
|
|
_ready: false,
|
|
|
|
|
|
|
|
|
|
|
|
// the actual bot list is stored here:
|
|
|
|
|
|
_list: []
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-08-29 23:14:10 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
loadLogFile: async function(type, onLoaded = undefined) {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// console.info('Monitor.live.data.loadLogFile(',type,')');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
let typeName = '';
|
|
|
|
|
|
let columns = [];
|
|
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case "srv":
|
|
|
|
|
|
typeName = "Server";
|
2025-08-30 13:01:50 +03:00
|
|
|
|
columns = ['ts','ip','pg','id','typ','usr','client','ref'];
|
2025-08-29 23:14:10 +03:00
|
|
|
|
break;
|
|
|
|
|
|
case "log":
|
|
|
|
|
|
typeName = "Page load";
|
2025-08-30 18:40:16 +03:00
|
|
|
|
columns = ['ts','ip','pg','id','usr','lt','ref','client'];
|
2025-08-29 23:14:10 +03:00
|
|
|
|
break;
|
|
|
|
|
|
case "tck":
|
|
|
|
|
|
typeName = "Ticker";
|
2025-08-30 21:11:01 +03:00
|
|
|
|
columns = ['ts','ip','pg','id','client'];
|
2025-08-29 23:14:10 +03:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
console.warn(`Unknown log type ${type}.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// Show the busy indicator and set the visible status:
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.showBusy(`Loading ${typeName} log file …`);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// compose the URL from which to load:
|
2025-09-01 10:15:36 +02:00
|
|
|
|
const url = Monitor._baseDir + `logs/${Monitor._today}.${type}.txt`;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
//console.log("Loading:",url);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// fetch the data:
|
2025-08-29 23:14:10 +03:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
const logtxt = await response.text();
|
|
|
|
|
|
|
|
|
|
|
|
logtxt.split('\n').forEach((line) => {
|
|
|
|
|
|
if (line.trim() === '') return; // skip empty lines
|
|
|
|
|
|
const cols = line.split('\t');
|
|
|
|
|
|
|
|
|
|
|
|
// assign the columns to an object:
|
|
|
|
|
|
const data = {};
|
|
|
|
|
|
cols.forEach( (colVal,i) => {
|
|
|
|
|
|
colName = columns[i] || `col${i}`;
|
|
|
|
|
|
const colValue = (colName == 'ts' ? new Date(colVal) : colVal);
|
|
|
|
|
|
data[colName] = colValue;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// register the visit in the model:
|
|
|
|
|
|
switch(type) {
|
|
|
|
|
|
case 'srv':
|
|
|
|
|
|
Monitor.live.data.model.registerVisit(data);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'log':
|
|
|
|
|
|
Monitor.live.data.model.updateVisit(data);
|
|
|
|
|
|
break;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
case 'tck':
|
|
|
|
|
|
Monitor.live.data.model.updateTicks(data);
|
|
|
|
|
|
break;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
default:
|
|
|
|
|
|
console.warn(`Unknown log type ${type}.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (onLoaded) {
|
|
|
|
|
|
onLoaded(); // callback after loading is finished.
|
|
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
} finally {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
Monitor.live.gui.status.hideBusy("Done.");
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
gui: {
|
|
|
|
|
|
|
|
|
|
|
|
overview: {
|
|
|
|
|
|
make: function() {
|
|
|
|
|
|
const data = Monitor.live.data.analytics.data;
|
|
|
|
|
|
const parent = document.getElementById('monitor__today__content');
|
|
|
|
|
|
if (parent) {
|
|
|
|
|
|
jQuery(parent).prepend(jQuery(`
|
2025-09-01 15:42:06 +02:00
|
|
|
|
<details id="monitor__today__overview" open>
|
|
|
|
|
|
<summary>Overview</summary>
|
|
|
|
|
|
<div class="grid-3-columns">
|
|
|
|
|
|
<dl>
|
|
|
|
|
|
<dt>Web metrics</dt>
|
|
|
|
|
|
<dd><span>Total visits:</span><span>${data.totalVisits}</span></dd>
|
|
|
|
|
|
<dd><span>Total page views:</span><span>${data.totalPageViews}</span></dd>
|
|
|
|
|
|
<dd><span>Bounce rate:</span><span>(TBD)</span></dd>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
<dl>
|
|
|
|
|
|
<dt>Bots vs. Humans</dt>
|
|
|
|
|
|
<dd><span>Known bots:</span><span>${data.bots.known}</span></dd>
|
|
|
|
|
|
<dd><span>Likely bots:</span><span>${data.bots.likely}</span></dd>
|
|
|
|
|
|
<dd><span>Probably humans:</span><span>${data.bots.human}</span></dd>
|
|
|
|
|
|
<dd><span>Registered users:</span><span>${data.bots.users}</span></dd>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
<dl id="monitor__botslistWould be good for me, too">
|
|
|
|
|
|
<dt>Known bots</dt>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</details>
|
2025-08-30 21:11:01 +03:00
|
|
|
|
`));
|
|
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-08-30 21:11:01 +03:00
|
|
|
|
status: {
|
|
|
|
|
|
setText: function(txt) {
|
|
|
|
|
|
const el = document.getElementById('monitor__today__status');
|
|
|
|
|
|
if (el && Monitor.live.gui.status._errorCount <= 0) {
|
|
|
|
|
|
el.innerText = txt;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setTitle: function(html) {
|
|
|
|
|
|
const el = document.getElementById('monitor__today__title');
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setError: function(txt) {
|
|
|
|
|
|
console.error(txt);
|
|
|
|
|
|
Monitor.live.gui.status._errorCount += 1;
|
|
|
|
|
|
const el = document.getElementById('monitor__today__status');
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.innerText = "An error occured. See the browser log for details!";
|
|
|
|
|
|
el.classList.add('error');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
_errorCount: 0,
|
|
|
|
|
|
|
|
|
|
|
|
showBusy: function(txt = null) {
|
|
|
|
|
|
Monitor.live.gui.status._busyCount += 1;
|
|
|
|
|
|
const el = document.getElementById('monitor__today__busy');
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.style.display = 'inline-block';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (txt) Monitor.live.gui.status.setText(txt);
|
|
|
|
|
|
},
|
|
|
|
|
|
_busyCount: 0,
|
|
|
|
|
|
|
|
|
|
|
|
hideBusy: function(txt = null) {
|
|
|
|
|
|
const el = document.getElementById('monitor__today__busy');
|
|
|
|
|
|
Monitor.live.gui.status._busyCount -= 1;
|
|
|
|
|
|
if (Monitor.live.gui.status._busyCount <= 0) {
|
|
|
|
|
|
if (el) el.style.display = 'none';
|
|
|
|
|
|
if (txt) Monitor.live.gui.status.setText(txt);
|
|
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
2025-08-29 23:28:19 +03:00
|
|
|
|
};
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
/* launch only if the Monitor admin panel is open: */
|
2025-08-29 23:14:10 +03:00
|
|
|
|
if (document.getElementById('monitor__admin')) {
|
|
|
|
|
|
Monitor.init();
|
|
|
|
|
|
}
|