1"use strict"; 2/* DokuWiki BotMon Plugin Script file */ 3/* 06.09.2025 - 0.2.0 - beta */ 4/* Author: Sascha Leib <ad@hominem.info> */ 5 6// enumeration of user types: 7const BM_USERTYPE = Object.freeze({ 8 'UNKNOWN': 'unknown', 9 'KNOWN_USER': 'user', 10 'HUMAN': 'human', 11 'LIKELY_BOT': 'likely_bot', 12 'KNOWN_BOT': 'known_bot' 13}); 14 15/* BotMon root object */ 16const BotMon = { 17 18 init: function() { 19 //console.info('BotMon.init()'); 20 21 // find the plugin basedir: 22 this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/')) 23 + '/plugins/botmon/'; 24 25 // read the page language from the DOM: 26 this._lang = document.getRootNode().documentElement.lang || this._lang; 27 28 // get the time offset: 29 this._timeDiff = BotMon.t._getTimeOffset(); 30 31 // init the sub-objects: 32 BotMon.t._callInit(this); 33 }, 34 35 _baseDir: null, 36 _lang: 'en', 37 _today: (new Date()).toISOString().slice(0, 10), 38 _timeDiff: '', 39 40 /* internal tools */ 41 t: { 42 /* helper function to call inits of sub-objects */ 43 _callInit: function(obj) { 44 //console.info('BotMon.t._callInit(obj=',obj,')'); 45 46 /* call init / _init on each sub-object: */ 47 Object.keys(obj).forEach( (key,i) => { 48 const sub = obj[key]; 49 let init = null; 50 if (typeof sub === 'object' && sub.init) { 51 init = sub.init; 52 } 53 54 // bind to object 55 if (typeof init == 'function') { 56 const init2 = init.bind(sub); 57 init2(obj); 58 } 59 }); 60 }, 61 62 /* helper function to calculate the time difference to UTC: */ 63 _getTimeOffset: function() { 64 const now = new Date(); 65 let offset = now.getTimezoneOffset(); // in minutes 66 const sign = Math.sign(offset); // +1 or -1 67 offset = Math.abs(offset); // always positive 68 69 let hours = 0; 70 while (offset >= 60) { 71 hours += 1; 72 offset -= 60; 73 } 74 return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : ''); 75 }, 76 77 /* helper function to create a new element with all attributes and text content */ 78 _makeElement: function(name, atlist = undefined, text = undefined) { 79 var r = null; 80 try { 81 r = document.createElement(name); 82 if (atlist) { 83 for (let attr in atlist) { 84 r.setAttribute(attr, atlist[attr]); 85 } 86 } 87 if (text) { 88 r.textContent = text.toString(); 89 } 90 } catch(e) { 91 console.error(e); 92 } 93 return r; 94 }, 95 96 /* helper to convert an ip address string to a normalised format: */ 97 _ip2Num: function(ip) { 98 if (ip.indexOf(':') > 0) { /* IP6 */ 99 return (ip.split(':').map(d => ('0000'+d).slice(-4) ).join('')); 100 } else { /* IP4 */ 101 return Number(ip.split('.').map(d => ('000'+d).slice(-3) ).join('')); 102 } 103 }, 104 105 /* helper function to format a Date object to show only the time. */ 106 /* returns String */ 107 _formatTime: function(date) { 108 109 if (date) { 110 return ('0'+date.getHours()).slice(-2) + ':' + ('0'+date.getMinutes()).slice(-2) + ':' + ('0'+date.getSeconds()).slice(-2); 111 } else { 112 return null; 113 } 114 115 }, 116 117 /* helper function to show a time difference in seconds or minutes */ 118 /* returns String */ 119 _formatTimeDiff: function(dateA, dateB) { 120 121 // if the second date is ealier, swap them: 122 if (dateA > dateB) dateB = [dateA, dateA = dateB][0]; 123 124 // get the difference in milliseconds: 125 let ms = dateB - dateA; 126 127 if (ms > 50) { /* ignore small time spans */ 128 const h = Math.floor((ms / (1000 * 60 * 60)) % 24); 129 const m = Math.floor((ms / (1000 * 60)) % 60); 130 const s = Math.floor((ms / 1000) % 60); 131 132 return ( h>0 ? h + 'h ': '') + ( m>0 ? m + 'm ': '') + ( s>0 ? s + 's': ''); 133 } 134 135 return null; 136 137 } 138 } 139}; 140 141/* everything specific to the "Today" tab is self-contained in the "live" object: */ 142BotMon.live = { 143 init: function() { 144 //console.info('BotMon.live.init()'); 145 146 // set the title: 147 const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (BotMon._timeDiff != '' ? `, ${BotMon._timeDiff}` : '' ) + ')'; 148 BotMon.live.gui.status.setTitle(`Data for <time datetime=${BotMon._today}>${BotMon._today}</time> ${tDiff}`); 149 150 // init sub-objects: 151 BotMon.t._callInit(this); 152 }, 153 154 data: { 155 init: function() { 156 //console.info('BotMon.live.data.init()'); 157 158 // call sub-inits: 159 BotMon.t._callInit(this); 160 }, 161 162 // this will be called when the known json files are done loading: 163 _dispatch: function(file) { 164 //console.info('BotMon.live.data._dispatch(,',file,')'); 165 166 // shortcut to make code more readable: 167 const data = BotMon.live.data; 168 169 // set the flags: 170 switch(file) { 171 case 'rules': 172 data._dispatchRulesLoaded = true; 173 break; 174 case 'bots': 175 data._dispatchBotsLoaded = true; 176 break; 177 case 'clients': 178 data._dispatchClientsLoaded = true; 179 break; 180 case 'platforms': 181 data._dispatchPlatformsLoaded = true; 182 break; 183 default: 184 // ignore 185 } 186 187 // are all the flags set? 188 if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded && data._dispatchRulesLoaded) { 189 // chain the log files loading: 190 BotMon.live.data.loadLogFile('srv', BotMon.live.data._onServerLogLoaded); 191 } 192 }, 193 // flags to track which data files have been loaded: 194 _dispatchBotsLoaded: false, 195 _dispatchClientsLoaded: false, 196 _dispatchPlatformsLoaded: false, 197 _dispatchRulesLoaded: false, 198 199 // event callback, after the server log has been loaded: 200 _onServerLogLoaded: function() { 201 //console.info('BotMon.live.data._onServerLogLoaded()'); 202 203 // chain the client log file to load: 204 BotMon.live.data.loadLogFile('log', BotMon.live.data._onClientLogLoaded); 205 }, 206 207 // event callback, after the client log has been loaded: 208 _onClientLogLoaded: function() { 209 //console.info('BotMon.live.data._onClientLogLoaded()'); 210 211 // chain the ticks file to load: 212 BotMon.live.data.loadLogFile('tck', BotMon.live.data._onTicksLogLoaded); 213 214 }, 215 216 // event callback, after the tiker log has been loaded: 217 _onTicksLogLoaded: function() { 218 //console.info('BotMon.live.data._onTicksLogLoaded()'); 219 220 // analyse the data: 221 BotMon.live.data.analytics.analyseAll(); 222 223 // sort the data: 224 // #TODO 225 226 // display the data: 227 BotMon.live.gui.overview.make(); 228 229 //console.log(BotMon.live.data.model._visitors); 230 231 }, 232 233 model: { 234 // visitors storage: 235 _visitors: [], 236 237 // find an already existing visitor record: 238 findVisitor: function(visitor) { 239 //console.info('BotMon.live.data.model.findVisitor()'); 240 //console.log(visitor); 241 242 // shortcut to make code more readable: 243 const model = BotMon.live.data.model; 244 245 const timeout = 60 * 60 * 1000; /* session timeout: One hour */ 246 247 // loop over all visitors already registered: 248 for (let i=0; i<model._visitors.length; i++) { 249 const v = model._visitors[i]; 250 251 if (visitor._type == BM_USERTYPE.KNOWN_BOT) { /* known bots */ 252 253 // bots match when their ID matches: 254 if (v._bot && v._bot.id == visitor._bot.id) { 255 return v; 256 } 257 258 } else if (visitor._type == BM_USERTYPE.KNOWN_USER) { /* registered users */ 259 260 // visitors match when their names match: 261 if ( v.usr == visitor.usr 262 && v.ip == visitor.ip 263 && v.agent == visitor.agent) { 264 return v; 265 } 266 } else { /* any other visitor */ 267 268 if (Math.abs(v._lastSeen - visitor.ts) < timeout) { /* ignore timed out visits */ 269 if ( v.id == visitor.id) { /* match the pre-defined IDs */ 270 return v; 271 } else if (v.ip == visitor.ip && v.agent == visitor.agent) { 272 if (v.typ !== 'ip') { 273 console.warn(`Visitor ID “${v.id}” not found, using matchin IP + User-Agent instead.`); 274 } 275 return v; 276 } 277 } 278 279 } 280 } 281 return null; // nothing found 282 }, 283 284 /* if there is already this visit registered, return the page view item */ 285 _getPageView: function(visit, view) { 286 287 // shortcut to make code more readable: 288 const model = BotMon.live.data.model; 289 290 for (let i=0; i<visit._pageViews.length; i++) { 291 const pv = visit._pageViews[i]; 292 if (pv.pg == view.pg) { 293 return pv; 294 } 295 } 296 return null; // not found 297 }, 298 299 // register a new visitor (or update if already exists) 300 registerVisit: function(nv, type) { 301 //console.info('registerVisit', nv, type); 302 303 // shortcut to make code more readable: 304 const model = BotMon.live.data.model; 305 306 // is it a known bot? 307 const bot = BotMon.live.data.bots.match(nv.agent); 308 309 // enrich new visitor with relevant data: 310 if (!nv._bot) nv._bot = bot ?? null; // bot info 311 nv._type = ( bot ? BM_USERTYPE.KNOWN_BOT : ( nv.usr && nv.usr !== '' ? BM_USERTYPE.KNOWN_USER : BM_USERTYPE.UNKNOWN ) ); 312 if (!nv._firstSeen) nv._firstSeen = nv.ts; 313 nv._lastSeen = nv.ts; 314 if (!nv.geo ||nv.geo === '') nv.geo = 'ZZ'; 315 316 // country name: 317 try { 318 nv._country = "Undefined"; 319 if (nv.geo && nv.geo !== '') { 320 const countryName = new Intl.DisplayNames(['en', BotMon._lang], {type: 'region'}); 321 nv._country = countryName.of(nv.geo) ?? "Unknown"; 322 } 323 } catch (err) { 324 console.error(err); 325 nv._country = 'Error'; 326 } 327 328 // check if it already exists: 329 let visitor = model.findVisitor(nv); 330 if (!visitor) { 331 visitor = nv; 332 visitor._seenBy = [type]; 333 visitor._pageViews = []; // array of page views 334 visitor._hasReferrer = false; // has at least one referrer 335 visitor._jsClient = false; // visitor has been seen logged by client js as well 336 visitor._client = BotMon.live.data.clients.match(nv.agent) ?? null; // client info 337 visitor._platform = BotMon.live.data.platforms.match(nv.agent); // platform info 338 model._visitors.push(visitor); 339 } else { // update existing 340 if (visitor._firstSeen > nv.ts) { 341 visitor._firstSeen = nv.ts; 342 } 343 } 344 345 // find browser 346 347 // is this visit already registered? 348 let prereg = model._getPageView(visitor, nv); 349 if (!prereg) { 350 // add new page view: 351 prereg = model._makePageView(nv, type); 352 visitor._pageViews.push(prereg); 353 } else { 354 // update last seen date 355 prereg._lastSeen = nv.ts; 356 // increase view count: 357 prereg._viewCount += 1; 358 prereg._tickCount += 1; 359 } 360 361 // update referrer state: 362 visitor._hasReferrer = visitor._hasReferrer || 363 (prereg.ref !== undefined && prereg.ref !== ''); 364 365 // update time stamp for last-seen: 366 if (visitor._lastSeen < nv.ts) { 367 visitor._lastSeen = nv.ts; 368 } 369 370 // if needed: 371 return visitor; 372 }, 373 374 // updating visit data from the client-side log: 375 updateVisit: function(dat) { 376 //console.info('updateVisit', dat); 377 378 // shortcut to make code more readable: 379 const model = BotMon.live.data.model; 380 381 const type = 'log'; 382 383 let visitor = BotMon.live.data.model.findVisitor(dat); 384 if (!visitor) { 385 visitor = model.registerVisit(dat, type); 386 } 387 if (visitor) { 388 389 if (visitor._lastSeen < dat.ts) { 390 visitor._lastSeen = dat.ts; 391 } 392 if (!visitor._seenBy.includes(type)) { 393 visitor._seenBy.push(type); 394 } 395 visitor._jsClient = true; // seen by client js 396 } 397 398 // find the page view: 399 let prereg = BotMon.live.data.model._getPageView(visitor, dat); 400 if (prereg) { 401 // update the page view: 402 prereg._lastSeen = dat.ts; 403 if (!prereg._seenBy.includes(type)) prereg._seenBy.push(type); 404 prereg._jsClient = true; // seen by client js 405 } else { 406 // add the page view to the visitor: 407 prereg = model._makePageView(dat, type); 408 visitor._pageViews.push(prereg); 409 } 410 prereg._tickCount += 1; 411 }, 412 413 // updating visit data from the ticker log: 414 updateTicks: function(dat) { 415 //console.info('updateTicks', dat); 416 417 // shortcut to make code more readable: 418 const model = BotMon.live.data.model; 419 420 const type = 'tck'; 421 422 // find the visit info: 423 let visitor = model.findVisitor(dat); 424 if (!visitor) { 425 console.info(`No visitor with ID “${dat.id}” found, registering as a new one.`); 426 visitor = model.registerVisit(dat, type); 427 } 428 if (visitor) { 429 // update visitor: 430 if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts; 431 if (!visitor._seenBy.includes(type)) visitor._seenBy.push(type); 432 433 // get the page view info: 434 let pv = model._getPageView(visitor, dat); 435 if (!pv) { 436 console.warn(`No page view for visit ID “${dat.id}”, page “${dat.pg}”, registering a new one.`); 437 pv = model._makePageView(dat, type); 438 visitor._pageViews.push(pv); 439 } 440 441 // update the page view info: 442 if (!pv._seenBy.includes(type)) pv._seenBy.push(type); 443 if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts; 444 pv._tickCount += 1; 445 446 } 447 }, 448 449 // helper function to create a new "page view" item: 450 _makePageView: function(data, type) { 451 452 // try to parse the referrer: 453 let rUrl = null; 454 try { 455 rUrl = ( data.ref && data.ref !== '' ? new URL(data.ref) : null ); 456 } catch (e) { 457 console.info(`Invalid referer: “${data.ref}”.`); 458 } 459 460 return { 461 _by: type, 462 ip: data.ip, 463 pg: data.pg, 464 _ref: rUrl, 465 _firstSeen: data.ts, 466 _lastSeen: data.ts, 467 _seenBy: [type], 468 _jsClient: ( type !== 'srv'), 469 _viewCount: 1, 470 _tickCount: 0 471 }; 472 } 473 }, 474 475 analytics: { 476 477 init: function() { 478 //console.info('BotMon.live.data.analytics.init()'); 479 }, 480 481 // data storage: 482 data: { 483 totalVisits: 0, 484 totalPageViews: 0, 485 humanPageViews: 0, 486 bots: { 487 known: 0, 488 suspected: 0, 489 human: 0, 490 users: 0 491 } 492 }, 493 494 // sort the visits by type: 495 groups: { 496 knownBots: [], 497 suspectedBots: [], 498 humans: [], 499 users: [] 500 }, 501 502 // all analytics 503 analyseAll: function() { 504 //console.info('BotMon.live.data.analytics.analyseAll()'); 505 506 // shortcut to make code more readable: 507 const model = BotMon.live.data.model; 508 const me = BotMon.live.data.analytics; 509 510 BotMon.live.gui.status.showBusy("Analysing data …"); 511 512 // loop over all visitors: 513 model._visitors.forEach( (v) => { 514 515 // count visits and page views: 516 this.data.totalVisits += 1; 517 this.data.totalPageViews += v._pageViews.length; 518 519 // check for typical bot aspects: 520 let botScore = 0; 521 522 if (v._type == BM_USERTYPE.KNOWN_BOT) { // known bots 523 524 this.data.bots.known += v._pageViews.length; 525 this.groups.knownBots.push(v); 526 527 } else if (v._type == BM_USERTYPE.KNOWN_USER) { // known users */ 528 529 this.data.bots.users += v._pageViews.length; 530 this.groups.users.push(v); 531 532 } else { 533 534 // get evaluation: 535 const e = BotMon.live.data.rules.evaluate(v); 536 v._eval = e.rules; 537 v._botVal = e.val; 538 539 if (e.isBot) { // likely bots 540 v._type = BM_USERTYPE.LIKELY_BOT; 541 this.data.bots.suspected += v._pageViews.length; 542 this.groups.suspectedBots.push(v); 543 } else { // probably humans 544 v._type = BM_USERTYPE.HUMAN; 545 this.data.bots.human += v._pageViews.length; 546 this.groups.humans.push(v); 547 } 548 } 549 550 // perform actions depending on the visitor type: 551 if (v._type == BM_USERTYPE.KNOWN_BOT || v._type == BM_USERTYPE.LIKELY_BOT) { /* bots only */ 552 553 // add bot views to IP range information: 554 v._pageViews.forEach( pv => { 555 me.addToIPRanges(pv.ip); 556 }); 557 558 // add to the country lists: 559 me.addToCountries(v.geo, v._country, v._type); 560 561 } else { /* humans only */ 562 563 // add browser and platform statistics: 564 me.addBrowserPlatform(v); 565 } 566 567 }); 568 569 BotMon.live.gui.status.hideBusy('Done.'); 570 }, 571 572 // visits from IP ranges: 573 _ipRange: { 574 ip4: [], 575 ip6: [] 576 }, 577 /** 578 * Adds a visit to the IP range statistics. 579 * 580 * This helps to identify IP ranges that are used by bots. 581 * 582 * @param {string} ip The IP address to add. 583 */ 584 addToIPRanges: function(ip) { 585 586 // #TODO: handle nestled ranges! 587 const me = BotMon.live.data.analytics; 588 const ipv = (ip.indexOf(':') > 0 ? 6 : 4); 589 590 const ipArr = ip.split( ipv == 6 ? ':' : '.'); 591 const maxSegments = (ipv == 6 ? 4 : 3); 592 593 let arr = (ipv == 6 ? me._ipRange.ip6 : me._ipRange.ip4); 594 595 // find any existing segment entry: 596 it = null; 597 for (let i=0; i < arr.length; i++) { 598 const sig = arr[i]; 599 if (sig.seg == ipArr[0]) { 600 it = sig; 601 break; 602 } 603 } 604 605 // create if not found: 606 if (!it) { 607 it = {seg: ipArr[0], count: 1}; 608 //if (i<maxSegments) it.sub = []; 609 arr.push(it); 610 611 } else { // increase count: 612 613 it.count += 1; 614 } 615 616 }, 617 getTopBotIPRanges: function(max) { 618 619 const me = BotMon.live.data.analytics; 620 621 const kMinHits = 2; 622 623 // combine the ip lists, removing all lower volume branches: 624 let ipTypes = [4,6]; 625 const tmpList = []; 626 for (let i=0; i<ipTypes.length; i++) { 627 const ipType = ipTypes[i]; 628 (ipType == 6 ? me._ipRange.ip6 : me._ipRange.ip4).forEach( it => { 629 if (it.count > kMinHits) { 630 it.type = ipType; 631 tmpList.push(it); 632 } 633 }); 634 tmpList.sort( (a,b) => b.count - a.count); 635 } 636 637 // reduce to only the top (max) items and create the target format: 638 // #TODO: handle nestled ranges! 639 let rList = []; 640 for (let j=0; Math.min(max, tmpList.length) > j; j++) { 641 const rangeInfo = tmpList[j]; 642 rList.push({ 643 'ip': rangeInfo.seg + ( rangeInfo.type == 4 ? '.x.x.x' : '::x'), 644 'typ': rangeInfo.type, 645 'num': rangeInfo.count 646 }); 647 } 648 649 return rList; 650 }, 651 652 /* countries of visits */ 653 _countries: { 654 'user': [], 655 'human': [], 656 'likelyBot': [], 657 'known_bot': [] 658 659 }, 660 /** 661 * Adds a country code to the statistics. 662 * 663 * @param {string} iso The ISO 3166-1 alpha-2 country code. 664 */ 665 addToCountries: function(iso, name, type) { 666 667 const me = BotMon.live.data.analytics; 668 669 // find the correct array: 670 let arr = null; 671 switch (type) { 672 673 case BM_USERTYPE.KNOWN_USER: 674 arr = me._countries.user; 675 break; 676 case BM_USERTYPE.HUMAN: 677 arr = me._countries.human; 678 break; 679 case BM_USERTYPE.LIKELY_BOT: 680 arr = me._countries.likelyBot; 681 break; 682 case BM_USERTYPE.KNOWN_BOT: 683 arr = me._countries.known_bot; 684 break; 685 default: 686 console.warn(`Unknown user type ${type} in function addToCountries.`); 687 } 688 689 if (arr) { 690 let cRec = arr.find( it => it.iso == iso); 691 if (!cRec) { 692 cRec = { 693 'iso': iso, 694 'name': name, 695 'count': 1 696 }; 697 arr.push(cRec); 698 } else { 699 cRec.count += 1; 700 } 701 } 702 }, 703 704 /** 705 * Returns a list of countries with visit counts, sorted by visit count in descending order. 706 * 707 * @param {BM_USERTYPE} type The type of visitors to return. 708 * @param {number} max The maximum number of entries to return. 709 * @return {Array} A list of objects with properties 'iso' (ISO 3166-1 alpha-2 country code) and 'count' (visit count). 710 */ 711 getCountryList: function(type, max) { 712 713 const me = BotMon.live.data.analytics; 714 715 // find the correct array: 716 let arr = null; 717 switch (type) { 718 719 case BM_USERTYPE.KNOWN_USER: 720 arr = me._countries.user; 721 break; 722 case BM_USERTYPE.HUMAN: 723 arr = me._countries.human; 724 break; 725 case BM_USERTYPE.LIKELY_BOT: 726 arr = me._countries.likelyBot; 727 break; 728 case BM_USERTYPE.KNOWN_BOT: 729 arr = me._countries.known_bot; 730 break; 731 default: 732 console.warn(`Unknown user type ${type} in function getCountryList.`); 733 } 734 735 if (arr) { 736 // sort by visit count: 737 arr.sort( (a,b) => b.count - a.count); 738 739 // reduce to only the top (max) items and create the target format: 740 let rList = []; 741 for (let i=0; Math.min(max, arr.length) > i; i++) { 742 const cRec = arr[i]; 743 rList.push({ 744 'iso': cRec.iso, 745 'name': cRec.name, 746 'count': cRec.count 747 }); 748 } 749 return rList; 750 } 751 return []; 752 }, 753 754 /* browser and platform of human visitors */ 755 _browsers: [], 756 _platforms: [], 757 758 addBrowserPlatform: function(visitor) { 759 //console.info('addBrowserPlatform', visitor); 760 761 const me = BotMon.live.data.analytics; 762 763 // add to browsers list: 764 let browserRec = ( visitor._client ? visitor._client : {'id': 'unknown'}); 765 if (visitor._client) { 766 let bRec = me._browsers.find( it => it.id == browserRec.id); 767 if (!bRec) { 768 bRec = { 769 'id': browserRec.id, 770 'count': 1 771 }; 772 me._browsers.push(bRec); 773 } else { 774 bRec.count += 1; 775 } 776 } 777 778 // add to platforms list: 779 let platformRec = ( visitor._platform ? visitor._platform : {'id': 'unknown'}); 780 if (visitor._platform) { 781 let pRec = me._platforms.find( it => it.id == platformRec.id); 782 if (!pRec) { 783 pRec = { 784 'id': platformRec.id, 785 'count': 1 786 }; 787 me._platforms.push(pRec); 788 } else { 789 pRec.count += 1; 790 } 791 } 792 793 }, 794 795 getTopBrowsers: function(max) { 796 797 const me = BotMon.live.data.analytics; 798 799 me._browsers.sort( (a,b) => b.count - a.count); 800 801 // how many browsers to show: 802 const max2 = ( me._browsers.length >= max ? max-1 : max ); 803 804 const rArr = []; // return array 805 let total = 0; 806 const others = { 807 'id': 'other', 808 'name': "Others", 809 'count': 0 810 } 811 for (let i=0; i < me._browsers.length; i++) { 812 if (i < max2) { 813 rArr.push({ 814 'id': me._browsers[i].id, 815 'name': BotMon.live.data.clients.getName(me._browsers[i].id), 816 'count': me._browsers[i].count 817 }); 818 total += me._browsers[i].count; 819 } else { 820 others.count += me._browsers[i].count; 821 total += me._browsers[i].count; 822 } 823 }; 824 825 if (me._browsers.length > (max-1)) { 826 rArr.push(others); 827 } 828 829 // update percentages: 830 rArr.forEach( it => { 831 it.pct = Math.round(it.count * 100 / total); 832 }) 833 834 return rArr; 835 }, 836 837 getTopPlatforms: function(max) { 838 839 const me = BotMon.live.data.analytics; 840 841 me._platforms.sort( (a,b) => b.count - a.count); 842 // how many browsers to show: 843 const max2 = ( me._platforms.length >= max ? max-1 : max ); 844 845 const rArr = []; // return array 846 let total = 0; 847 const others = { 848 'id': 'other', 849 'name': "Others", 850 'count': 0 851 } 852 for (let i=0; i < me._platforms.length; i++) { 853 if (i < max2) { 854 rArr.push({ 855 'id': me._platforms[i].id, 856 'name': BotMon.live.data.platforms.getName(me._platforms[i].id), 857 'count': me._platforms[i].count 858 }); 859 total += me._platforms[i].count; 860 } else { 861 others.count += me._platforms[i].count; 862 total += me._platforms[i].count; 863 } 864 }; 865 866 if (me._platforms.length > (max-1)) { 867 rArr.push(others); 868 } 869 870 // update percentages: 871 rArr.forEach( it => { 872 it.pct = Math.round(it.count * 100 / total); 873 }) 874 875 return rArr; 876 } 877 }, 878 879 bots: { 880 // loads the list of known bots from a JSON file: 881 init: async function() { 882 //console.info('BotMon.live.data.bots.init()'); 883 884 // Load the list of known bots: 885 BotMon.live.gui.status.showBusy("Loading known bots …"); 886 const url = BotMon._baseDir + 'config/known-bots.json'; 887 try { 888 const response = await fetch(url); 889 if (!response.ok) { 890 throw new Error(`${response.status} ${response.statusText}`); 891 } 892 893 this._list = await response.json(); 894 this._ready = true; 895 896 } catch (error) { 897 BotMon.live.gui.status.setError("Error while loading the ‘known bots’ file: " + error.message); 898 } finally { 899 BotMon.live.gui.status.hideBusy("Status: Done."); 900 BotMon.live.data._dispatch('bots') 901 } 902 }, 903 904 // returns bot info if the clientId matches a known bot, null otherwise: 905 match: function(agent) { 906 //console.info('BotMon.live.data.bots.match(',agent,')'); 907 908 const BotList = BotMon.live.data.bots._list; 909 910 // default is: not found! 911 let botInfo = null; 912 913 if (!agent) return null; 914 915 // check for known bots: 916 BotList.find(bot => { 917 let r = false; 918 for (let j=0; j<bot.rx.length; j++) { 919 const rxr = agent.match(new RegExp(bot.rx[j])); 920 if (rxr) { 921 botInfo = { 922 n : bot.n, 923 id: bot.id, 924 url: bot.url, 925 v: (rxr.length > 1 ? rxr[1] : -1) 926 }; 927 r = true; 928 break; 929 }; 930 }; 931 return r; 932 }); 933 934 // check for unknown bots: 935 if (!botInfo) { 936 const botmatch = agent.match(/([\s\d\w]*bot|[\s\d\w]*crawler|[\s\d\w]*spider)[\/\s;\),\\.$]/i); 937 if(botmatch) { 938 botInfo = {'id': ( botmatch[1] || "other_" ), 'n': "Other" + ( botmatch[1] ? " (" + botmatch[1] + ")" : "" ) , "bot": botmatch[1] }; 939 } 940 } 941 942 //console.log("botInfo:", botInfo); 943 return botInfo; 944 }, 945 946 947 // indicates if the list is loaded and ready to use: 948 _ready: false, 949 950 // the actual bot list is stored here: 951 _list: [] 952 }, 953 954 clients: { 955 // loads the list of known clients from a JSON file: 956 init: async function() { 957 //console.info('BotMon.live.data.clients.init()'); 958 959 // Load the list of known bots: 960 BotMon.live.gui.status.showBusy("Loading known clients"); 961 const url = BotMon._baseDir + 'config/known-clients.json'; 962 try { 963 const response = await fetch(url); 964 if (!response.ok) { 965 throw new Error(`${response.status} ${response.statusText}`); 966 } 967 968 BotMon.live.data.clients._list = await response.json(); 969 BotMon.live.data.clients._ready = true; 970 971 } catch (error) { 972 BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message); 973 } finally { 974 BotMon.live.gui.status.hideBusy("Status: Done."); 975 BotMon.live.data._dispatch('clients') 976 } 977 }, 978 979 // returns bot info if the user-agent matches a known bot, null otherwise: 980 match: function(agent) { 981 //console.info('BotMon.live.data.clients.match(',agent,')'); 982 983 let match = {"n": "Unknown", "v": -1, "id": null}; 984 985 if (agent) { 986 BotMon.live.data.clients._list.find(client => { 987 let r = false; 988 for (let j=0; j<client.rx.length; j++) { 989 const rxr = agent.match(new RegExp(client.rx[j])); 990 if (rxr) { 991 match.n = client.n; 992 match.v = (rxr.length > 1 ? rxr[1] : -1); 993 match.id = client.id || null; 994 r = true; 995 break; 996 } 997 } 998 return r; 999 }); 1000 } 1001 1002 //console.log(match) 1003 return match; 1004 }, 1005 1006 // return the browser name for a browser ID: 1007 getName: function(id) { 1008 const it = BotMon.live.data.clients._list.find(client => client.id == id); 1009 return it.n; 1010 }, 1011 1012 // indicates if the list is loaded and ready to use: 1013 _ready: false, 1014 1015 // the actual bot list is stored here: 1016 _list: [] 1017 1018 }, 1019 1020 platforms: { 1021 // loads the list of known platforms from a JSON file: 1022 init: async function() { 1023 //console.info('BotMon.live.data.platforms.init()'); 1024 1025 // Load the list of known bots: 1026 BotMon.live.gui.status.showBusy("Loading known platforms"); 1027 const url = BotMon._baseDir + 'config/known-platforms.json'; 1028 try { 1029 const response = await fetch(url); 1030 if (!response.ok) { 1031 throw new Error(`${response.status} ${response.statusText}`); 1032 } 1033 1034 BotMon.live.data.platforms._list = await response.json(); 1035 BotMon.live.data.platforms._ready = true; 1036 1037 } catch (error) { 1038 BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message); 1039 } finally { 1040 BotMon.live.gui.status.hideBusy("Status: Done."); 1041 BotMon.live.data._dispatch('platforms') 1042 } 1043 }, 1044 1045 // returns bot info if the browser id matches a known platform: 1046 match: function(cid) { 1047 //console.info('BotMon.live.data.platforms.match(',cid,')'); 1048 1049 let match = {"n": "Unknown", "id": null}; 1050 1051 if (cid) { 1052 BotMon.live.data.platforms._list.find(platform => { 1053 let r = false; 1054 for (let j=0; j<platform.rx.length; j++) { 1055 const rxr = cid.match(new RegExp(platform.rx[j])); 1056 if (rxr) { 1057 match.n = platform.n; 1058 match.v = (rxr.length > 1 ? rxr[1] : -1); 1059 match.id = platform.id || null; 1060 r = true; 1061 break; 1062 } 1063 } 1064 return r; 1065 }); 1066 } 1067 1068 return match; 1069 }, 1070 1071 // return the platform name for a given ID: 1072 getName: function(id) { 1073 const it = BotMon.live.data.platforms._list.find( pf => pf.id == id); 1074 console.log(it); 1075 return ( it ? it.n : 'Unknown' ); 1076 }, 1077 1078 1079 // indicates if the list is loaded and ready to use: 1080 _ready: false, 1081 1082 // the actual bot list is stored here: 1083 _list: [] 1084 1085 }, 1086 1087 rules: { 1088 // loads the list of rules and settings from a JSON file: 1089 init: async function() { 1090 //console.info('BotMon.live.data.rules.init()'); 1091 1092 // Load the list of known bots: 1093 BotMon.live.gui.status.showBusy("Loading list of rules …"); 1094 const url = BotMon._baseDir + 'config/botmon-config.json'; 1095 try { 1096 const response = await fetch(url); 1097 if (!response.ok) { 1098 throw new Error(`${response.status} ${response.statusText}`); 1099 } 1100 1101 const json = await response.json(); 1102 1103 if (json.rules) { 1104 this._rulesList = json.rules; 1105 } 1106 1107 if (json.threshold) { 1108 this._threshold = json.threshold; 1109 } 1110 1111 if (json.ipRanges) { 1112 // clean up the IPs first: 1113 let list = []; 1114 json.ipRanges.forEach( it => { 1115 let item = { 1116 'from': BotMon.t._ip2Num(it.from), 1117 'to': BotMon.t._ip2Num(it.to), 1118 'label': it.label 1119 }; 1120 list.push(item); 1121 }); 1122 1123 this._botIPs = list; 1124 } 1125 1126 this._ready = true; 1127 1128 } catch (error) { 1129 BotMon.live.gui.status.setError("Error while loading the ‘rules’ file: " + error.message); 1130 } finally { 1131 BotMon.live.gui.status.hideBusy("Status: Done."); 1132 BotMon.live.data._dispatch('rules') 1133 } 1134 }, 1135 1136 _rulesList: [], // list of rules to find out if a visitor is a bot 1137 _threshold: 100, // above this, it is considered a bot. 1138 1139 // returns a descriptive text for a rule id 1140 getRuleInfo: function(ruleId) { 1141 // console.info('getRuleInfo', ruleId); 1142 1143 // shortcut for neater code: 1144 const me = BotMon.live.data.rules; 1145 1146 for (let i=0; i<me._rulesList.length; i++) { 1147 const rule = me._rulesList[i]; 1148 if (rule.id == ruleId) { 1149 return rule; 1150 } 1151 } 1152 return null; 1153 1154 }, 1155 1156 // evaluate a visitor for lkikelihood of being a bot 1157 evaluate: function(visitor) { 1158 1159 // shortcut for neater code: 1160 const me = BotMon.live.data.rules; 1161 1162 let r = { // evaluation result 1163 'val': 0, 1164 'rules': [], 1165 'isBot': false 1166 }; 1167 1168 for (let i=0; i<me._rulesList.length; i++) { 1169 const rule = me._rulesList[i]; 1170 const params = ( rule.params ? rule.params : [] ); 1171 1172 if (rule.func) { // rule is calling a function 1173 if (me.func[rule.func]) { 1174 if(me.func[rule.func](visitor, ...params)) { 1175 r.val += rule.bot; 1176 r.rules.push(rule.id) 1177 } 1178 } else { 1179 //console.warn("Unknown rule function: “${rule.func}”. Ignoring rule.") 1180 } 1181 } 1182 } 1183 1184 // is a bot? 1185 r.isBot = (r.val >= me._threshold); 1186 1187 return r; 1188 }, 1189 1190 // list of functions that can be called by the rules list to evaluate a visitor: 1191 func: { 1192 1193 // check if client is on the list passed as parameter: 1194 matchesClient: function(visitor, ...clients) { 1195 1196 const clientId = ( visitor._client ? visitor._client.id : ''); 1197 return clients.includes(clientId); 1198 }, 1199 1200 // check if OS/Platform is one of the obsolete ones: 1201 matchesPlatform: function(visitor, ...platforms) { 1202 1203 const pId = ( visitor._platform ? visitor._platform.id : ''); 1204 return platforms.includes(pId); 1205 }, 1206 1207 // are there at lest num pages loaded? 1208 smallPageCount: function(visitor, num) { 1209 return (visitor._pageViews.length <= Number(num)); 1210 }, 1211 1212 // There was no entry in a specific log file for this visitor: 1213 // note that this will also trigger the "noJavaScript" rule: 1214 noRecord: function(visitor, type) { 1215 return !visitor._seenBy.includes(type); 1216 }, 1217 1218 // there are no referrers in any of the page visits: 1219 noReferrer: function(visitor) { 1220 1221 let r = false; // return value 1222 for (let i = 0; i < visitor._pageViews.length; i++) { 1223 if (!visitor._pageViews[i]._ref) { 1224 r = true; 1225 break; 1226 } 1227 } 1228 return r; 1229 }, 1230 1231 // test for specific client identifiers: 1232 /*matchesClients: function(visitor, ...list) { 1233 1234 for (let i=0; i<list.length; i++) { 1235 if (visitor._client.id == list[i]) { 1236 return true 1237 } 1238 }; 1239 return false; 1240 },*/ 1241 1242 // unusual combinations of Platform and Client: 1243 combinationTest: function(visitor, ...combinations) { 1244 1245 for (let i=0; i<combinations.length; i++) { 1246 1247 if (visitor._platform.id == combinations[i][0] 1248 && visitor._client.id == combinations[i][1]) { 1249 return true 1250 } 1251 }; 1252 1253 return false; 1254 }, 1255 1256 // is the IP address from a known bot network? 1257 fromKnownBotIP: function(visitor) { 1258 1259 const ipInfo = BotMon.live.data.rules.getBotIPInfo(visitor.ip); 1260 1261 if (ipInfo) { 1262 visitor._ipInKnownBotRange = true; 1263 } 1264 1265 return (ipInfo !== null); 1266 }, 1267 1268 // is the page language mentioned in the client's accepted languages? 1269 // the parameter holds an array of exceptions, i.e. page languages that should be ignored. 1270 matchLang: function(visitor, ...exceptions) { 1271 1272 if (visitor.lang && visitor.accept && exceptions.indexOf(visitor.lang) < 0) { 1273 return (visitor.accept.split(',').indexOf(visitor.lang) < 0); 1274 } 1275 return false; 1276 }, 1277 1278 // Is there an accept-language field defined at all? 1279 noAcceptLang: function(visitor) { 1280 1281 if (!visitor.accept || visitor.accept.length <= 0) { // no accept-languages header 1282 return true; 1283 } 1284 // TODO: parametrize this! 1285 return false; 1286 }, 1287 // At least x page views were recorded, but they come within less than y seconds 1288 loadSpeed: function(visitor, minItems, maxTime) { 1289 1290 if (visitor._pageViews.length >= minItems) { 1291 //console.log('loadSpeed', visitor._pageViews.length, minItems, maxTime); 1292 1293 const pvArr = visitor._pageViews.map(pv => pv._lastSeen).sort(); 1294 1295 let totalTime = 0; 1296 for (let i=1; i < pvArr.length; i++) { 1297 totalTime += (pvArr[i] - pvArr[i-1]); 1298 } 1299 1300 //console.log(' ', totalTime , Math.round(totalTime / (pvArr.length * 1000)), (( totalTime / pvArr.length ) <= maxTime * 1000), visitor.ip); 1301 1302 return (( totalTime / pvArr.length ) <= maxTime * 1000); 1303 } 1304 }, 1305 1306 // Country code matches one of those in the list: 1307 matchesCountry: function(visitor, ...countries) { 1308 1309 // ingore if geoloc is not set or unknown: 1310 if (visitor.geo && visitor.geo !== 'ZZ') { 1311 return (countries.indexOf(visitor.geo) >= 0); 1312 } 1313 return false; 1314 }, 1315 1316 // Country does not match one of the given codes. 1317 notFromCountry: function(visitor, ...countries) { 1318 1319 // ingore if geoloc is not set or unknown: 1320 if (visitor.geo && visitor.geo !== 'ZZ') { 1321 return (countries.indexOf(visitor.geo) < 0); 1322 } 1323 return false; 1324 } 1325 }, 1326 1327 /* known bot IP ranges: */ 1328 _botIPs: [], 1329 1330 // return information on a bot IP range: 1331 getBotIPInfo: function(ip) { 1332 1333 // shortcut to make code more readable: 1334 const me = BotMon.live.data.rules; 1335 1336 // convert IP address to easier comparable form: 1337 const ipNum = BotMon.t._ip2Num(ip); 1338 1339 for (let i=0; i < me._botIPs.length; i++) { 1340 const ipRange = me._botIPs[i]; 1341 1342 if (ipNum >= ipRange.from && ipNum <= ipRange.to) { 1343 return ipRange; 1344 } 1345 1346 }; 1347 return null; 1348 1349 } 1350 1351 }, 1352 1353 /** 1354 * Loads a log file (server, page load, or ticker) and parses it. 1355 * @param {String} type - the type of the log file to load (srv, log, or tck) 1356 * @param {Function} [onLoaded] - an optional callback function to call after loading is finished. 1357 */ 1358 loadLogFile: async function(type, onLoaded = undefined) { 1359 //console.info('BotMon.live.data.loadLogFile(',type,')'); 1360 1361 let typeName = ''; 1362 let columns = []; 1363 1364 switch (type) { 1365 case "srv": 1366 typeName = "Server"; 1367 columns = ['ts','ip','pg','id','typ','usr','agent','ref','lang','accept','geo']; 1368 break; 1369 case "log": 1370 typeName = "Page load"; 1371 columns = ['ts','ip','pg','id','usr','lt','ref','agent']; 1372 break; 1373 case "tck": 1374 typeName = "Ticker"; 1375 columns = ['ts','ip','pg','id','agent']; 1376 break; 1377 default: 1378 console.warn(`Unknown log type ${type}.`); 1379 return; 1380 } 1381 1382 // Show the busy indicator and set the visible status: 1383 BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`); 1384 1385 // compose the URL from which to load: 1386 const url = BotMon._baseDir + `logs/${BotMon._today}.${type}.txt`; 1387 //console.log("Loading:",url); 1388 1389 // fetch the data: 1390 try { 1391 const response = await fetch(url); 1392 if (!response.ok) { 1393 throw new Error(`${response.status} ${response.statusText}`); 1394 } 1395 1396 const logtxt = await response.text(); 1397 1398 logtxt.split('\n').forEach((line) => { 1399 if (line.trim() === '') return; // skip empty lines 1400 const cols = line.split('\t'); 1401 1402 // assign the columns to an object: 1403 const data = {}; 1404 cols.forEach( (colVal,i) => { 1405 colName = columns[i] || `col${i}`; 1406 const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim()); 1407 data[colName] = colValue; 1408 }); 1409 1410 // register the visit in the model: 1411 switch(type) { 1412 case 'srv': 1413 BotMon.live.data.model.registerVisit(data, type); 1414 break; 1415 case 'log': 1416 data.typ = 'js'; 1417 BotMon.live.data.model.updateVisit(data); 1418 break; 1419 case 'tck': 1420 data.typ = 'js'; 1421 BotMon.live.data.model.updateTicks(data); 1422 break; 1423 default: 1424 console.warn(`Unknown log type ${type}.`); 1425 return; 1426 } 1427 }); 1428 1429 if (onLoaded) { 1430 onLoaded(); // callback after loading is finished. 1431 } 1432 1433 } catch (error) { 1434 BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); 1435 } finally { 1436 BotMon.live.gui.status.hideBusy("Status: Done."); 1437 } 1438 } 1439 }, 1440 1441 gui: { 1442 init: function() { 1443 // init the lists view: 1444 this.lists.init(); 1445 }, 1446 1447 overview: { 1448 make: function() { 1449 1450 const data = BotMon.live.data.analytics.data; 1451 1452 // shortcut for neater code: 1453 const makeElement = BotMon.t._makeElement; 1454 1455 const botsVsHumans = document.getElementById('botmon__today__botsvshumans'); 1456 if (botsVsHumans) { 1457 botsVsHumans.appendChild(makeElement('dt', {}, "Bots vs. Humans")); 1458 1459 for (let i = 3; i >= 0; i--) { 1460 const dd = makeElement('dd'); 1461 let title = ''; 1462 let value = ''; 1463 switch(i) { 1464 case 0: 1465 title = "Registered users:"; 1466 value = data.bots.users; 1467 break; 1468 case 1: 1469 title = "Probably humans:"; 1470 value = data.bots.human; 1471 break; 1472 case 2: 1473 title = "Suspected bots:"; 1474 value = data.bots.suspected; 1475 break; 1476 case 3: 1477 title = "Known bots:"; 1478 value = data.bots.known; 1479 break; 1480 default: 1481 console.warn(`Unknown list type ${i}.`); 1482 } 1483 dd.appendChild(makeElement('span', {}, title)); 1484 dd.appendChild(makeElement('strong', {}, value)); 1485 botsVsHumans.appendChild(dd); 1486 } 1487 } 1488 1489 // update known bots list: 1490 const botlist = document.getElementById('botmon__botslist'); 1491 botlist.innerHTML = "<dt>Known bots (top 4)</dt>"; 1492 1493 let bots = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => { 1494 return b._pageViews.length - a._pageViews.length; 1495 }); 1496 1497 for (let i=0; i < Math.min(bots.length, 4); i++) { 1498 const dd = makeElement('dd'); 1499 dd.appendChild(makeElement('span', {'class': 'bot bot_' + bots[i]._bot.id }, bots[i]._bot.n)); 1500 dd.appendChild(makeElement('strong', undefined, bots[i]._pageViews.length)); 1501 botlist.appendChild(dd); 1502 } 1503 1504 // update the suspected bot IP ranges list: 1505 const botIps = document.getElementById('botmon__today__botips'); 1506 if (botIps) { 1507 botIps.appendChild(makeElement('dt', {}, "Bot IP ranges (top 4)")); 1508 1509 const ipList = BotMon.live.data.analytics.getTopBotIPRanges(4); 1510 ipList.forEach( (ipInfo) => { 1511 const li = makeElement('dd'); 1512 li.appendChild(makeElement('span', {'class': 'ip ip' + ipInfo.typ }, ipInfo.ip)); 1513 li.appendChild(makeElement('span', {'class': 'count' }, ipInfo.num)); 1514 botIps.append(li) 1515 }); 1516 } 1517 1518 // update the top bot countries list: 1519 const botCountries = document.getElementById('botmon__today__countries'); 1520 if (botCountries) { 1521 botCountries.appendChild(makeElement('dt', {}, "Bot Countries (top 4)")); 1522 const countryList = BotMon.live.data.analytics.getCountryList('likely_bot', 4); 1523 countryList.forEach( (cInfo) => { 1524 const cLi = makeElement('dd'); 1525 cLi.appendChild(makeElement('span', {'class': 'country ctry_' + cInfo.iso }, cInfo.name)); 1526 cLi.appendChild(makeElement('span', {'class': 'count' }, cInfo.count)); 1527 botCountries.appendChild(cLi); 1528 }); 1529 } 1530 1531 // update the webmetrics overview: 1532 const wmoverview = document.getElementById('botmon__today__wm_overview'); 1533 if (wmoverview) { 1534 const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 100); 1535 1536 wmoverview.appendChild(makeElement('dt', {}, "Overview")); 1537 for (let i = 0; i < 3; i++) { 1538 const dd = makeElement('dd'); 1539 let title = ''; 1540 let value = ''; 1541 switch(i) { 1542 case 0: 1543 title = "Total page views:"; 1544 value = data.totalPageViews; 1545 break; 1546 case 1: 1547 title = "Total visitors (est.):"; 1548 value = data.totalVisits; 1549 break; 1550 case 2: 1551 title = "Bounce rate (est.):"; 1552 value = bounceRate + '%'; 1553 break; 1554 default: 1555 console.warn(`Unknown list type ${i}.`); 1556 } 1557 dd.appendChild(makeElement('span', {}, title)); 1558 dd.appendChild(makeElement('strong', {}, value)); 1559 wmoverview.appendChild(dd); 1560 } 1561 } 1562 1563 // update the webmetrics clients list: 1564 const wmclients = document.getElementById('botmon__today__wm_clients'); 1565 if (wmclients) { 1566 1567 wmclients.appendChild(makeElement('dt', {}, "Browsers (humans only)")); 1568 1569 const clientList = BotMon.live.data.analytics.getTopBrowsers(5); 1570 if (clientList) { 1571 clientList.forEach( (cInfo) => { 1572 const cDd = makeElement('dd'); 1573 cDd.appendChild(makeElement('span', {'class': 'has_icon client_' + cInfo.id }, ( cInfo.name ? cInfo.name : cInfo.id))); 1574 cDd.appendChild(makeElement('span', { 1575 'class': 'count', 1576 'title': cInfo.count + " page views" 1577 }, Math.round(cInfo.pct) + '%')); 1578 wmclients.appendChild(cDd); 1579 }); 1580 } 1581 } 1582 1583 // update the webmetrics platforms list: 1584 const wmplatforms = document.getElementById('botmon__today__wm_platforms'); 1585 if (wmplatforms) { 1586 1587 wmplatforms.appendChild(makeElement('dt', {}, "Platforms (humans only)")); 1588 1589 const pfList = BotMon.live.data.analytics.getTopPlatforms(5); 1590 if (pfList) { 1591 pfList.forEach( (pInfo) => { 1592 const pDd = makeElement('dd'); 1593 pDd.appendChild(makeElement('span', {'class': 'has_icon client_' + pInfo.id }, ( pInfo.name ? pInfo.name : pInfo.id))); 1594 pDd.appendChild(makeElement('span', { 1595 'class': 'count', 1596 'title': pInfo.count + " page views" 1597 }, Math.round(pInfo.pct) + '%')); 1598 wmplatforms.appendChild(pDd); 1599 }); 1600 } 1601 } 1602 1603 } 1604 }, 1605 1606 status: { 1607 setText: function(txt) { 1608 const el = document.getElementById('botmon__today__status'); 1609 if (el && BotMon.live.gui.status._errorCount <= 0) { 1610 el.innerText = txt; 1611 } 1612 }, 1613 1614 setTitle: function(html) { 1615 const el = document.getElementById('botmon__today__title'); 1616 if (el) { 1617 el.innerHTML = html; 1618 } 1619 }, 1620 1621 setError: function(txt) { 1622 console.error(txt); 1623 BotMon.live.gui.status._errorCount += 1; 1624 const el = document.getElementById('botmon__today__status'); 1625 if (el) { 1626 el.innerText = "An error occured. See the browser log for details!"; 1627 el.classList.add('error'); 1628 } 1629 }, 1630 _errorCount: 0, 1631 1632 showBusy: function(txt = null) { 1633 BotMon.live.gui.status._busyCount += 1; 1634 const el = document.getElementById('botmon__today__busy'); 1635 if (el) { 1636 el.style.display = 'inline-block'; 1637 } 1638 if (txt) BotMon.live.gui.status.setText(txt); 1639 }, 1640 _busyCount: 0, 1641 1642 hideBusy: function(txt = null) { 1643 const el = document.getElementById('botmon__today__busy'); 1644 BotMon.live.gui.status._busyCount -= 1; 1645 if (BotMon.live.gui.status._busyCount <= 0) { 1646 if (el) el.style.display = 'none'; 1647 if (txt) BotMon.live.gui.status.setText(txt); 1648 } 1649 } 1650 }, 1651 1652 lists: { 1653 init: function() { 1654 1655 // function shortcut: 1656 const makeElement = BotMon.t._makeElement; 1657 1658 const parent = document.getElementById('botmon__today__visitorlists'); 1659 if (parent) { 1660 1661 for (let i=0; i < 4; i++) { 1662 1663 // change the id and title by number: 1664 let listTitle = ''; 1665 let listId = ''; 1666 switch (i) { 1667 case 0: 1668 listTitle = "Registered users"; 1669 listId = 'users'; 1670 break; 1671 case 1: 1672 listTitle = "Probably humans"; 1673 listId = 'humans'; 1674 break; 1675 case 2: 1676 listTitle = "Suspected bots"; 1677 listId = 'suspectedBots'; 1678 break; 1679 case 3: 1680 listTitle = "Known bots"; 1681 listId = 'knownBots'; 1682 break; 1683 default: 1684 console.warn('Unknown list number.'); 1685 } 1686 1687 const details = makeElement('details', { 1688 'data-group': listId, 1689 'data-loaded': false 1690 }); 1691 const title = details.appendChild(makeElement('summary')); 1692 title.appendChild(makeElement('span', {'class':'title'}, listTitle)); 1693 title.appendChild(makeElement('span', {'class':'counter'}, '–')); 1694 details.addEventListener("toggle", this._onDetailsToggle); 1695 1696 parent.appendChild(details); 1697 1698 } 1699 } 1700 }, 1701 1702 _onDetailsToggle: function(e) { 1703 //console.info('BotMon.live.gui.lists._onDetailsToggle()'); 1704 1705 const target = e.target; 1706 1707 if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet 1708 target.setAttribute('data-loaded', 'loading'); 1709 1710 const fillType = target.getAttribute('data-group'); 1711 const fillList = BotMon.live.data.analytics.groups[fillType]; 1712 if (fillList && fillList.length > 0) { 1713 1714 const ul = BotMon.t._makeElement('ul'); 1715 1716 fillList.forEach( (it) => { 1717 ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType)); 1718 }); 1719 1720 target.appendChild(ul); 1721 target.setAttribute('data-loaded', 'true'); 1722 } else { 1723 target.setAttribute('data-loaded', 'false'); 1724 } 1725 1726 } 1727 }, 1728 1729 _makeVisitorItem: function(data, type) { 1730 1731 // shortcut for neater code: 1732 const make = BotMon.t._makeElement; 1733 1734 let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); 1735 const platformName = (data._platform ? data._platform.n : 'Unknown'); 1736 const clientName = (data._client ? data._client.n: 'Unknown'); 1737 1738 const li = make('li'); // root list item 1739 const details = make('details'); 1740 const summary = make('summary'); 1741 details.appendChild(summary); 1742 1743 const span1 = make('span'); /* left-hand group */ 1744 1745 // country flag: 1746 if (data.geo && data.geo !=='') { 1747 span1.appendChild(make('span', { 1748 'class': 'icon country ctry_' + data.geo.toLowerCase(), 1749 'data-ctry': data.geo, 1750 'title': "Country: " + data._country 1751 }, data._country)); 1752 } 1753 1754 // identifier: 1755 if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */ 1756 1757 const botName = ( data._bot && data._bot.n ? data._bot.n : "Unknown"); 1758 span1.appendChild(make('span', { /* Bot */ 1759 'class': 'bot bot_' + (data._bot ? data._bot.id : 'unknown'), 1760 'title': "Bot: " + botName 1761 }, botName)); 1762 1763 } else if (data._type == BM_USERTYPE.KNOWN_USER) { /* User only */ 1764 1765 span1.appendChild(make('span', { /* User */ 1766 'class': 'user_known', 1767 'title': "User: " + data.usr 1768 }, data.usr)); 1769 1770 } else { /* others */ 1771 1772 if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; 1773 span1.appendChild(make('span', { /* IP-Address */ 1774 'class': 'ipaddr ip' + ipType, 1775 'title': "IP-Address: " + data.ip 1776 }, data.ip)); 1777 } 1778 1779 if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* Not for bots */ 1780 span1.appendChild(make('span', { /* Platform */ 1781 'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'), 1782 'title': "Platform: " + platformName 1783 }, platformName)); 1784 1785 span1.appendChild(make('span', { /* Client */ 1786 'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'), 1787 'title': "Client: " + clientName 1788 }, clientName)); 1789 } 1790 1791 summary.appendChild(span1); 1792 const span2 = make('span'); /* right-hand group */ 1793 1794 span2.appendChild(make('span', { /* page views */ 1795 'class': 'pageviews' 1796 }, data._pageViews.length)); 1797 1798 summary.appendChild(span2); 1799 1800 // add details expandable section: 1801 details.appendChild(BotMon.live.gui.lists._makeVisitorDetails(data, type)); 1802 1803 li.appendChild(details); 1804 return li; 1805 }, 1806 1807 _makeVisitorDetails: function(data, type) { 1808 1809 // shortcut for neater code: 1810 const make = BotMon.t._makeElement; 1811 1812 let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); 1813 if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; 1814 const platformName = (data._platform ? data._platform.n : 'Unknown'); 1815 const clientName = (data._client ? data._client.n: 'Unknown'); 1816 1817 const dl = make('dl', {'class': 'visitor_details'}); 1818 1819 if (data._type == BM_USERTYPE.KNOWN_BOT) { 1820 1821 dl.appendChild(make('dt', {}, "Bot name:")); /* bot info */ 1822 dl.appendChild(make('dd', {'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown')}, 1823 (data._bot ? data._bot.n : 'Unknown'))); 1824 1825 if (data._bot && data._bot.url) { 1826 dl.appendChild(make('dt', {}, "Bot info:")); /* bot info */ 1827 const botInfoDd = dl.appendChild(make('dd')); 1828 botInfoDd.appendChild(make('a', { 1829 'href': data._bot.url, 1830 'target': '_blank' 1831 }, data._bot.url)); /* bot info link*/ 1832 1833 } 1834 1835 } else { /* not for bots */ 1836 1837 dl.appendChild(make('dt', {}, "Client:")); /* client */ 1838 dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')}, 1839 clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) )); 1840 1841 dl.appendChild(make('dt', {}, "Platform:")); /* platform */ 1842 dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')}, 1843 platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) )); 1844 1845 dl.appendChild(make('dt', {}, "IP-Address:")); 1846 dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip)); 1847 1848 /*dl.appendChild(make('dt', {}, "ID:")); 1849 dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id));*/ 1850 } 1851 1852 if (Math.abs(data._lastSeen - data._firstSeen) < 100) { 1853 dl.appendChild(make('dt', {}, "Seen:")); 1854 dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString())); 1855 } else { 1856 dl.appendChild(make('dt', {}, "First seen:")); 1857 dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString())); 1858 dl.appendChild(make('dt', {}, "Last seen:")); 1859 dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString())); 1860 } 1861 1862 dl.appendChild(make('dt', {}, "User-Agent:")); 1863 dl.appendChild(make('dd', {'class': 'agent'}, data.agent)); 1864 1865 dl.appendChild(make('dt', {}, "Languages:")); 1866 dl.appendChild(make('dd', {'class': 'langs'}, "Client accepts: [" + data.accept + "]; Page: [" + data.lang + ']')); 1867 1868 if (data.geo && data.geo !=='') { 1869 dl.appendChild(make('dt', {}, "Location:")); 1870 dl.appendChild(make('dd', { 1871 'class': 'country ctry_' + data.geo.toLowerCase(), 1872 'data-ctry': data.geo, 1873 'title': "Country: " + data._country 1874 }, data._country + ' (' + data.geo + ')')); 1875 } 1876 1877 /*dl.appendChild(make('dt', {}, "Visitor Type:")); 1878 dl.appendChild(make('dd', undefined, data._type ));*/ 1879 1880 dl.appendChild(make('dt', {}, "Seen by:")); 1881 dl.appendChild(make('dd', undefined, data._seenBy.join(', ') )); 1882 1883 dl.appendChild(make('dt', {}, "Visited pages:")); 1884 const pagesDd = make('dd', {'class': 'pages'}); 1885 const pageList = make('ul'); 1886 1887 /* list all page views */ 1888 data._pageViews.forEach( (page) => { 1889 const pgLi = make('li'); 1890 1891 let visitTimeStr = "Bounce"; 1892 const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); 1893 if (visitDuration > 0) { 1894 visitTimeStr = Math.floor(visitDuration / 1000) + "s"; 1895 } 1896 1897 pgLi.appendChild(make('span', {}, page.pg)); /* DW Page ID */ 1898 if (page._ref) { 1899 pgLi.appendChild(make('span', { 1900 'data-ref': page._ref.host, 1901 'title': "Referrer: " + page._ref.full 1902 }, page._ref.site)); 1903 } else { 1904 pgLi.appendChild(make('span', { 1905 }, "No referer")); 1906 } 1907 pgLi.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount)); 1908 pgLi.appendChild(make('span', {}, BotMon.t._formatTime(page._firstSeen))); 1909 1910 // get the time difference: 1911 const tDiff = BotMon.t._formatTimeDiff(page._firstSeen, page._lastSeen); 1912 if (tDiff) { 1913 pgLi.appendChild(make('span', {'class': 'visit-length', 'title': 'Last seen: ' + page._lastSeen.toLocaleString()}, tDiff)); 1914 } else { 1915 pgLi.appendChild(make('span', { 1916 'class': 'bounce', 1917 'title': "Visitor bounced"}, "Bounce")); 1918 } 1919 1920 pageList.appendChild(pgLi); 1921 }); 1922 pagesDd.appendChild(pageList); 1923 dl.appendChild(pagesDd); 1924 1925 /* bot evaluation rating */ 1926 if (data._type !== BM_USERTYPE.KNOWN_BOT && data._type !== BM_USERTYPE.KNOWN_USER) { 1927 dl.appendChild(make('dt', undefined, "Bot rating:")); 1928 dl.appendChild(make('dd', {'class': 'bot-rating'}, ( data._botVal ? data._botVal : '–' ) + ' (of ' + BotMon.live.data.rules._threshold + ')')); 1929 1930 /* add bot evaluation details: */ 1931 if (data._eval) { 1932 dl.appendChild(make('dt', {}, "Bot evaluation details:")); 1933 const evalDd = make('dd'); 1934 const testList = make('ul',{ 1935 'class': 'eval' 1936 }); 1937 data._eval.forEach( test => { 1938 1939 const tObj = BotMon.live.data.rules.getRuleInfo(test); 1940 let tDesc = tObj ? tObj.desc : test; 1941 1942 // special case for Bot IP range test: 1943 if (tObj.func == 'fromKnownBotIP') { 1944 const rangeInfo = BotMon.live.data.rules.getBotIPInfo(data.ip); 1945 if (rangeInfo) { 1946 tDesc += ' (' + (rangeInfo.label ? rangeInfo.label : 'Unknown') + ')'; 1947 } 1948 } 1949 1950 // create the entry field 1951 const tstLi = make('li'); 1952 tstLi.appendChild(make('span', { 1953 'data-testid': test 1954 }, tDesc)); 1955 tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') )); 1956 testList.appendChild(tstLi); 1957 }); 1958 1959 // add total row 1960 const tst2Li = make('li', { 1961 'class': 'total' 1962 }); 1963 /*tst2Li.appendChild(make('span', {}, "Total:")); 1964 tst2Li.appendChild(make('span', {}, data._botVal)); 1965 testList.appendChild(tst2Li);*/ 1966 1967 evalDd.appendChild(testList); 1968 dl.appendChild(evalDd); 1969 } 1970 } 1971 // return the element to add to the UI: 1972 return dl; 1973 } 1974 1975 } 1976 } 1977}; 1978 1979/* launch only if the BotMon admin panel is open: */ 1980if (document.getElementById('botmon__admin')) { 1981 BotMon.init(); 1982}