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 315 // check if it already exists: 316 let visitor = model.findVisitor(nv); 317 if (!visitor) { 318 visitor = nv; 319 visitor._seenBy = [type]; 320 visitor._pageViews = []; // array of page views 321 visitor._hasReferrer = false; // has at least one referrer 322 visitor._jsClient = false; // visitor has been seen logged by client js as well 323 visitor._client = BotMon.live.data.clients.match(nv.agent) ?? null; // client info 324 visitor._platform = BotMon.live.data.platforms.match(nv.agent); // platform info 325 model._visitors.push(visitor); 326 } else { // update existing 327 if (visitor._firstSeen > nv.ts) { 328 visitor._firstSeen = nv.ts; 329 } 330 } 331 332 // find browser 333 334 // is this visit already registered? 335 let prereg = model._getPageView(visitor, nv); 336 if (!prereg) { 337 // add new page view: 338 prereg = model._makePageView(nv, type); 339 visitor._pageViews.push(prereg); 340 } else { 341 // update last seen date 342 prereg._lastSeen = nv.ts; 343 // increase view count: 344 prereg._viewCount += 1; 345 prereg._tickCount += 1; 346 } 347 348 // update referrer state: 349 visitor._hasReferrer = visitor._hasReferrer || 350 (prereg.ref !== undefined && prereg.ref !== ''); 351 352 // update time stamp for last-seen: 353 if (visitor._lastSeen < nv.ts) { 354 visitor._lastSeen = nv.ts; 355 } 356 357 // if needed: 358 return visitor; 359 }, 360 361 // updating visit data from the client-side log: 362 updateVisit: function(dat) { 363 //console.info('updateVisit', dat); 364 365 // shortcut to make code more readable: 366 const model = BotMon.live.data.model; 367 368 const type = 'log'; 369 370 let visitor = BotMon.live.data.model.findVisitor(dat); 371 if (!visitor) { 372 visitor = model.registerVisit(dat, type); 373 } 374 if (visitor) { 375 376 if (visitor._lastSeen < dat.ts) { 377 visitor._lastSeen = dat.ts; 378 } 379 if (!visitor._seenBy.includes(type)) { 380 visitor._seenBy.push(type); 381 } 382 visitor._jsClient = true; // seen by client js 383 } 384 385 // find the page view: 386 let prereg = BotMon.live.data.model._getPageView(visitor, dat); 387 if (prereg) { 388 // update the page view: 389 prereg._lastSeen = dat.ts; 390 if (!prereg._seenBy.includes(type)) prereg._seenBy.push(type); 391 prereg._jsClient = true; // seen by client js 392 } else { 393 // add the page view to the visitor: 394 prereg = model._makePageView(dat, type); 395 visitor._pageViews.push(prereg); 396 } 397 prereg._tickCount += 1; 398 }, 399 400 // updating visit data from the ticker log: 401 updateTicks: function(dat) { 402 //console.info('updateTicks', dat); 403 404 // shortcut to make code more readable: 405 const model = BotMon.live.data.model; 406 407 const type = 'tck'; 408 409 // find the visit info: 410 let visitor = model.findVisitor(dat); 411 if (!visitor) { 412 console.info(`No visitor with ID “${dat.id}” found, registering as a new one.`); 413 visitor = model.registerVisit(dat, type); 414 } 415 if (visitor) { 416 // update visitor: 417 if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts; 418 if (!visitor._seenBy.includes(type)) visitor._seenBy.push(type); 419 420 // get the page view info: 421 let pv = model._getPageView(visitor, dat); 422 if (!pv) { 423 console.warn(`No page view for visit ID “${dat.id}”, page “${dat.pg}”, registering a new one.`); 424 pv = model._makePageView(dat, type); 425 visitor._pageViews.push(pv); 426 } 427 428 // update the page view info: 429 if (!pv._seenBy.includes(type)) pv._seenBy.push(type); 430 if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts; 431 pv._tickCount += 1; 432 433 } 434 }, 435 436 // helper function to create a new "page view" item: 437 _makePageView: function(data, type) { 438 439 // try to parse the referrer: 440 let rUrl = null; 441 try { 442 rUrl = ( data.ref && data.ref !== '' ? new URL(data.ref) : null ); 443 } catch (e) { 444 console.info(`Invalid referer: “${data.ref}”.`); 445 } 446 447 return { 448 _by: type, 449 ip: data.ip, 450 pg: data.pg, 451 _ref: rUrl, 452 _firstSeen: data.ts, 453 _lastSeen: data.ts, 454 _seenBy: [type], 455 _jsClient: ( type !== 'srv'), 456 _viewCount: 1, 457 _tickCount: 0 458 }; 459 } 460 }, 461 462 analytics: { 463 464 init: function() { 465 //console.info('BotMon.live.data.analytics.init()'); 466 }, 467 468 // data storage: 469 data: { 470 totalVisits: 0, 471 totalPageViews: 0, 472 bots: { 473 known: 0, 474 suspected: 0, 475 human: 0, 476 users: 0 477 } 478 }, 479 480 // sort the visits by type: 481 groups: { 482 knownBots: [], 483 suspectedBots: [], 484 humans: [], 485 users: [] 486 }, 487 488 // all analytics 489 analyseAll: function() { 490 //console.info('BotMon.live.data.analytics.analyseAll()'); 491 492 // shortcut to make code more readable: 493 const model = BotMon.live.data.model; 494 const me = BotMon.live.data.analytics; 495 496 BotMon.live.gui.status.showBusy("Analysing data …"); 497 498 // loop over all visitors: 499 model._visitors.forEach( (v) => { 500 501 // count visits and page views: 502 this.data.totalVisits += 1; 503 this.data.totalPageViews += v._pageViews.length; 504 505 // check for typical bot aspects: 506 let botScore = 0; 507 508 if (v._type == BM_USERTYPE.KNOWN_BOT) { // known bots 509 510 this.data.bots.known += v._pageViews.length; 511 this.groups.knownBots.push(v); 512 513 } else if (v._type == BM_USERTYPE.KNOWN_USER) { // known users */ 514 515 this.data.bots.users += v._pageViews.length; 516 this.groups.users.push(v); 517 518 } else { 519 520 // get evaluation: 521 const e = BotMon.live.data.rules.evaluate(v); 522 v._eval = e.rules; 523 v._botVal = e.val; 524 525 // add each page view to IP range information (unless it is already from a known bot IP range): 526 v._pageViews.forEach( pv => { 527 me._addToIPRanges(pv.ip); 528 }); 529 530 if (e.isBot) { // likely bots 531 v._type = BM_USERTYPE.LIKELY_BOT; 532 this.data.bots.suspected += v._pageViews.length; 533 this.groups.suspectedBots.push(v); 534 } else { // probably humans 535 v._type = BM_USERTYPE.HUMAN; 536 this.data.bots.human += v._pageViews.length; 537 this.groups.humans.push(v); 538 } 539 // TODO: find suspected bots 540 541 } 542 }); 543 544 BotMon.live.gui.status.hideBusy('Done.'); 545 }, 546 547 // visits from IP ranges: 548 _ipRange: { 549 ip4: [], 550 ip6: [] 551 }, 552 /** 553 * Adds a visit to the IP range statistics. 554 * 555 * This helps to identify IP ranges that are used by bots. 556 * 557 * @param {string} ip The IP address to add. 558 */ 559 _addToIPRanges: function(ip) { 560 561 // #TODO: handle nestled ranges! 562 const me = BotMon.live.data.analytics; 563 const ipv = (ip.indexOf(':') > 0 ? 6 : 4); 564 565 const ipArr = ip.split( ipv == 6 ? ':' : '.'); 566 const maxSegments = (ipv == 6 ? 4 : 3); 567 568 let arr = (ipv == 6 ? me._ipRange.ip6 : me._ipRange.ip4); 569 570 // find any existing segment entry: 571 it = null; 572 for (let i=0; i < arr.length; i++) { 573 const sig = arr[i]; 574 if (sig.seg == ipArr[0]) { 575 it = sig; 576 break; 577 } 578 } 579 580 // create if not found: 581 if (!it) { 582 it = {seg: ipArr[0], count: 1}; 583 //if (i<maxSegments) it.sub = []; 584 arr.push(it); 585 586 } else { // increase count: 587 588 it.count += 1; 589 } 590 591 }, 592 getTopBotIPRanges: function(max) { 593 594 const me = BotMon.live.data.analytics; 595 596 const kMinHits = 2; 597 598 // combine the ip lists, removing all lower volume branches: 599 let ipTypes = [4,6]; 600 const tmpList = []; 601 for (let i=0; i<ipTypes.length; i++) { 602 const ipType = ipTypes[i]; 603 (ipType == 6 ? me._ipRange.ip6 : me._ipRange.ip4).forEach( it => { 604 if (it.count > kMinHits) { 605 it.type = ipType; 606 tmpList.push(it); 607 } 608 }); 609 tmpList.sort( (a,b) => b.count - a.count); 610 } 611 612 // reduce to only the top (max) items and create the target format: 613 // #TODO: handle nestled ranges! 614 let rList = []; 615 for (let j=0; Math.min(max, tmpList.length) > j; j++) { 616 const rangeInfo = tmpList[j]; 617 rList.push({ 618 'ip': rangeInfo.seg + ( rangeInfo.type == 4 ? '.x.x.x' : '::x'), 619 'typ': rangeInfo.type, 620 'num': rangeInfo.count 621 }); 622 } 623 624 return rList; 625 } 626 }, 627 628 bots: { 629 // loads the list of known bots from a JSON file: 630 init: async function() { 631 //console.info('BotMon.live.data.bots.init()'); 632 633 // Load the list of known bots: 634 BotMon.live.gui.status.showBusy("Loading known bots …"); 635 const url = BotMon._baseDir + 'config/known-bots.json'; 636 try { 637 const response = await fetch(url); 638 if (!response.ok) { 639 throw new Error(`${response.status} ${response.statusText}`); 640 } 641 642 this._list = await response.json(); 643 this._ready = true; 644 645 } catch (error) { 646 BotMon.live.gui.status.setError("Error while loading the ‘known bots’ file: " + error.message); 647 } finally { 648 BotMon.live.gui.status.hideBusy("Status: Done."); 649 BotMon.live.data._dispatch('bots') 650 } 651 }, 652 653 // returns bot info if the clientId matches a known bot, null otherwise: 654 match: function(agent) { 655 //console.info('BotMon.live.data.bots.match(',agent,')'); 656 657 const BotList = BotMon.live.data.bots._list; 658 659 // default is: not found! 660 let botInfo = null; 661 662 if (!agent) return null; 663 664 // check for known bots: 665 BotList.find(bot => { 666 let r = false; 667 for (let j=0; j<bot.rx.length; j++) { 668 const rxr = agent.match(new RegExp(bot.rx[j])); 669 if (rxr) { 670 botInfo = { 671 n : bot.n, 672 id: bot.id, 673 url: bot.url, 674 v: (rxr.length > 1 ? rxr[1] : -1) 675 }; 676 r = true; 677 break; 678 }; 679 }; 680 return r; 681 }); 682 683 // check for unknown bots: 684 if (!botInfo) { 685 const botmatch = agent.match(/[^\s](\w*bot)[\/\s;\),$]/i); 686 if(botmatch) { 687 botInfo = {'id': "other", 'n': "Other", "bot": botmatch[0] }; 688 } 689 } 690 691 //console.log("botInfo:", botInfo); 692 return botInfo; 693 }, 694 695 696 // indicates if the list is loaded and ready to use: 697 _ready: false, 698 699 // the actual bot list is stored here: 700 _list: [] 701 }, 702 703 clients: { 704 // loads the list of known clients from a JSON file: 705 init: async function() { 706 //console.info('BotMon.live.data.clients.init()'); 707 708 // Load the list of known bots: 709 BotMon.live.gui.status.showBusy("Loading known clients"); 710 const url = BotMon._baseDir + 'config/known-clients.json'; 711 try { 712 const response = await fetch(url); 713 if (!response.ok) { 714 throw new Error(`${response.status} ${response.statusText}`); 715 } 716 717 BotMon.live.data.clients._list = await response.json(); 718 BotMon.live.data.clients._ready = true; 719 720 } catch (error) { 721 BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message); 722 } finally { 723 BotMon.live.gui.status.hideBusy("Status: Done."); 724 BotMon.live.data._dispatch('clients') 725 } 726 }, 727 728 // returns bot info if the user-agent matches a known bot, null otherwise: 729 match: function(agent) { 730 //console.info('BotMon.live.data.clients.match(',agent,')'); 731 732 let match = {"n": "Unknown", "v": -1, "id": null}; 733 734 if (agent) { 735 BotMon.live.data.clients._list.find(client => { 736 let r = false; 737 for (let j=0; j<client.rx.length; j++) { 738 const rxr = agent.match(new RegExp(client.rx[j])); 739 if (rxr) { 740 match.n = client.n; 741 match.v = (rxr.length > 1 ? rxr[1] : -1); 742 match.id = client.id || null; 743 r = true; 744 break; 745 } 746 } 747 return r; 748 }); 749 } 750 751 //console.log(match) 752 return match; 753 }, 754 755 // indicates if the list is loaded and ready to use: 756 _ready: false, 757 758 // the actual bot list is stored here: 759 _list: [] 760 761 }, 762 763 platforms: { 764 // loads the list of known platforms from a JSON file: 765 init: async function() { 766 //console.info('BotMon.live.data.platforms.init()'); 767 768 // Load the list of known bots: 769 BotMon.live.gui.status.showBusy("Loading known platforms"); 770 const url = BotMon._baseDir + 'config/known-platforms.json'; 771 try { 772 const response = await fetch(url); 773 if (!response.ok) { 774 throw new Error(`${response.status} ${response.statusText}`); 775 } 776 777 BotMon.live.data.platforms._list = await response.json(); 778 BotMon.live.data.platforms._ready = true; 779 780 } catch (error) { 781 BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message); 782 } finally { 783 BotMon.live.gui.status.hideBusy("Status: Done."); 784 BotMon.live.data._dispatch('platforms') 785 } 786 }, 787 788 // returns bot info if the browser id matches a known platform: 789 match: function(cid) { 790 //console.info('BotMon.live.data.platforms.match(',cid,')'); 791 792 let match = {"n": "Unknown", "id": null}; 793 794 if (cid) { 795 BotMon.live.data.platforms._list.find(platform => { 796 let r = false; 797 for (let j=0; j<platform.rx.length; j++) { 798 const rxr = cid.match(new RegExp(platform.rx[j])); 799 if (rxr) { 800 match.n = platform.n; 801 match.v = (rxr.length > 1 ? rxr[1] : -1); 802 match.id = platform.id || null; 803 r = true; 804 break; 805 } 806 } 807 return r; 808 }); 809 } 810 811 return match; 812 }, 813 814 // indicates if the list is loaded and ready to use: 815 _ready: false, 816 817 // the actual bot list is stored here: 818 _list: [] 819 820 }, 821 822 rules: { 823 // loads the list of rules and settings from a JSON file: 824 init: async function() { 825 //console.info('BotMon.live.data.rules.init()'); 826 827 // Load the list of known bots: 828 BotMon.live.gui.status.showBusy("Loading list of rules …"); 829 const url = BotMon._baseDir + 'config/rules.json'; 830 try { 831 const response = await fetch(url); 832 if (!response.ok) { 833 throw new Error(`${response.status} ${response.statusText}`); 834 } 835 836 const json = await response.json(); 837 838 if (json.rules) { 839 this._rulesList = json.rules; 840 } 841 842 if (json.threshold) { 843 this._threshold = json.threshold; 844 } 845 846 if (json.ipRanges) { 847 // clean up the IPs first: 848 let list = []; 849 json.ipRanges.forEach( it => { 850 let item = { 851 'from': BotMon.t._ip2Num(it.from), 852 'to': BotMon.t._ip2Num(it.to), 853 'label': it.label 854 }; 855 list.push(item); 856 }); 857 858 this._botIPs = list; 859 } 860 861 this._ready = true; 862 863 } catch (error) { 864 BotMon.live.gui.status.setError("Error while loading the ‘rules’ file: " + error.message); 865 } finally { 866 BotMon.live.gui.status.hideBusy("Status: Done."); 867 BotMon.live.data._dispatch('rules') 868 } 869 }, 870 871 _rulesList: [], // list of rules to find out if a visitor is a bot 872 _threshold: 100, // above this, it is considered a bot. 873 874 // returns a descriptive text for a rule id 875 getRuleInfo: function(ruleId) { 876 // console.info('getRuleInfo', ruleId); 877 878 // shortcut for neater code: 879 const me = BotMon.live.data.rules; 880 881 for (let i=0; i<me._rulesList.length; i++) { 882 const rule = me._rulesList[i]; 883 if (rule.id == ruleId) { 884 return rule; 885 } 886 } 887 return null; 888 889 }, 890 891 // evaluate a visitor for lkikelihood of being a bot 892 evaluate: function(visitor) { 893 894 // shortcut for neater code: 895 const me = BotMon.live.data.rules; 896 897 let r = { // evaluation result 898 'val': 0, 899 'rules': [], 900 'isBot': false 901 }; 902 903 for (let i=0; i<me._rulesList.length; i++) { 904 const rule = me._rulesList[i]; 905 const params = ( rule.params ? rule.params : [] ); 906 907 if (rule.func) { // rule is calling a function 908 if (me.func[rule.func]) { 909 if(me.func[rule.func](visitor, ...params)) { 910 r.val += rule.bot; 911 r.rules.push(rule.id) 912 } 913 } else { 914 //console.warn("Unknown rule function: “${rule.func}”. Ignoring rule.") 915 } 916 } 917 } 918 919 // is a bot? 920 r.isBot = (r.val >= me._threshold); 921 922 return r; 923 }, 924 925 // list of functions that can be called by the rules list to evaluate a visitor: 926 func: { 927 928 // check if client is on the list passed as parameter: 929 matchesClient: function(visitor, ...clients) { 930 931 const clientId = ( visitor._client ? visitor._client.id : ''); 932 return clients.includes(clientId); 933 }, 934 935 // check if OS/Platform is one of the obsolete ones: 936 matchesPlatform: function(visitor, ...platforms) { 937 938 const pId = ( visitor._platform ? visitor._platform.id : ''); 939 return platforms.includes(pId); 940 }, 941 942 // are there at lest num pages loaded? 943 smallPageCount: function(visitor, num) { 944 return (visitor._pageViews.length <= Number(num)); 945 }, 946 947 // There was no entry in a specific log file for this visitor: 948 // note that this will also trigger the "noJavaScript" rule: 949 noRecord: function(visitor, type) { 950 return !visitor._seenBy.includes(type); 951 }, 952 953 // there are no referrers in any of the page visits: 954 noReferrer: function(visitor) { 955 956 let r = false; // return value 957 for (let i = 0; i < visitor._pageViews.length; i++) { 958 if (!visitor._pageViews[i]._ref) { 959 r = true; 960 break; 961 } 962 } 963 return r; 964 }, 965 966 // test for specific client identifiers: 967 /*matchesClients: function(visitor, ...list) { 968 969 for (let i=0; i<list.length; i++) { 970 if (visitor._client.id == list[i]) { 971 return true 972 } 973 }; 974 return false; 975 },*/ 976 977 // unusual combinations of Platform and Client: 978 combinationTest: function(visitor, ...combinations) { 979 980 for (let i=0; i<combinations.length; i++) { 981 982 if (visitor._platform.id == combinations[i][0] 983 && visitor._client.id == combinations[i][1]) { 984 return true 985 } 986 }; 987 988 return false; 989 }, 990 991 // is the IP address from a known bot network? 992 fromKnownBotIP: function(visitor) { 993 994 const ipInfo = BotMon.live.data.rules.getBotIPInfo(visitor.ip); 995 996 if (ipInfo) { 997 visitor._ipInKnownBotRange = true; 998 } 999 1000 return (ipInfo !== null); 1001 }, 1002 1003 // is the page language mentioned in the client's accepted languages? 1004 // the parameter holds an array of exceptions, i.e. page languages that should be ignored. 1005 matchLang: function(visitor, ...exceptions) { 1006 1007 if (visitor.lang && visitor.accept && exceptions.indexOf(visitor.lang) < 0) { 1008 return (visitor.accept.split(',').indexOf(visitor.lang) < 0); 1009 } 1010 return false; 1011 }, 1012 1013 // Is there an accept-language field defined at all? 1014 noAcceptLang: function(visitor) { 1015 1016 if (!visitor.accept || visitor.accept.length <= 0) { // no accept-languages header 1017 return true; 1018 } 1019 // TODO: parametrize this! 1020 return false; 1021 }, 1022 // At least x page views were recorded, but they come within less than y seconds 1023 loadSpeed: function(visitor, minItems, maxTime) { 1024 1025 if (visitor._pageViews.length >= minItems) { 1026 //console.log('loadSpeed', visitor._pageViews.length, minItems, maxTime); 1027 1028 const pvArr = visitor._pageViews.map(pv => pv._lastSeen).sort(); 1029 1030 let totalTime = 0; 1031 for (let i=1; i < pvArr.length; i++) { 1032 totalTime += (pvArr[i] - pvArr[i-1]); 1033 } 1034 1035 //console.log(' ', totalTime , Math.round(totalTime / (pvArr.length * 1000)), (( totalTime / pvArr.length ) <= maxTime * 1000), visitor.ip); 1036 1037 return (( totalTime / pvArr.length ) <= maxTime * 1000); 1038 } 1039 } 1040 }, 1041 1042 /* known bot IP ranges: */ 1043 _botIPs: [], 1044 1045 // return information on a bot IP range: 1046 getBotIPInfo: function(ip) { 1047 1048 // shortcut to make code more readable: 1049 const me = BotMon.live.data.rules; 1050 1051 // convert IP address to easier comparable form: 1052 const ipNum = BotMon.t._ip2Num(ip); 1053 1054 for (let i=0; i < me._botIPs.length; i++) { 1055 const ipRange = me._botIPs[i]; 1056 1057 if (ipNum >= ipRange.from && ipNum <= ipRange.to) { 1058 return ipRange; 1059 } 1060 1061 }; 1062 return null; 1063 1064 } 1065 1066 }, 1067 1068 loadLogFile: async function(type, onLoaded = undefined) { 1069 //console.info('BotMon.live.data.loadLogFile(',type,')'); 1070 1071 let typeName = ''; 1072 let columns = []; 1073 1074 switch (type) { 1075 case "srv": 1076 typeName = "Server"; 1077 columns = ['ts','ip','pg','id','typ','usr','agent','ref','lang','accept']; 1078 break; 1079 case "log": 1080 typeName = "Page load"; 1081 columns = ['ts','ip','pg','id','usr','lt','ref','agent']; 1082 break; 1083 case "tck": 1084 typeName = "Ticker"; 1085 columns = ['ts','ip','pg','id','agent']; 1086 break; 1087 default: 1088 console.warn(`Unknown log type ${type}.`); 1089 return; 1090 } 1091 1092 // Show the busy indicator and set the visible status: 1093 BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`); 1094 1095 // compose the URL from which to load: 1096 const url = BotMon._baseDir + `logs/${BotMon._today}.${type}.txt`; 1097 //console.log("Loading:",url); 1098 1099 // fetch the data: 1100 try { 1101 const response = await fetch(url); 1102 if (!response.ok) { 1103 throw new Error(`${response.status} ${response.statusText}`); 1104 } 1105 1106 const logtxt = await response.text(); 1107 1108 logtxt.split('\n').forEach((line) => { 1109 if (line.trim() === '') return; // skip empty lines 1110 const cols = line.split('\t'); 1111 1112 // assign the columns to an object: 1113 const data = {}; 1114 cols.forEach( (colVal,i) => { 1115 colName = columns[i] || `col${i}`; 1116 const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim()); 1117 data[colName] = colValue; 1118 }); 1119 1120 // register the visit in the model: 1121 switch(type) { 1122 case 'srv': 1123 BotMon.live.data.model.registerVisit(data, type); 1124 break; 1125 case 'log': 1126 data.typ = 'js'; 1127 BotMon.live.data.model.updateVisit(data); 1128 break; 1129 case 'tck': 1130 data.typ = 'js'; 1131 BotMon.live.data.model.updateTicks(data); 1132 break; 1133 default: 1134 console.warn(`Unknown log type ${type}.`); 1135 return; 1136 } 1137 }); 1138 1139 if (onLoaded) { 1140 onLoaded(); // callback after loading is finished. 1141 } 1142 1143 } catch (error) { 1144 BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); 1145 } finally { 1146 BotMon.live.gui.status.hideBusy("Status: Done."); 1147 } 1148 } 1149 }, 1150 1151 gui: { 1152 init: function() { 1153 // init the lists view: 1154 this.lists.init(); 1155 }, 1156 1157 overview: { 1158 make: function() { 1159 1160 const data = BotMon.live.data.analytics.data; 1161 1162 // shortcut for neater code: 1163 const makeElement = BotMon.t._makeElement; 1164 1165 const botsVsHumans = document.getElementById('botmon__today__botsvshumans'); 1166 if (botsVsHumans) { 1167 botsVsHumans.appendChild(makeElement('dt', {}, "Bots vs. Humans (page views)")); 1168 1169 for (let i = 3; i >= 0; i--) { 1170 const dd = makeElement('dd'); 1171 let title = ''; 1172 let value = ''; 1173 switch(i) { 1174 case 0: 1175 title = "Registered users:"; 1176 value = data.bots.users; 1177 break; 1178 case 1: 1179 title = "Probably humans:"; 1180 value = data.bots.human; 1181 break; 1182 case 2: 1183 title = "Suspected bots:"; 1184 value = data.bots.suspected; 1185 break; 1186 case 3: 1187 title = "Known bots:"; 1188 value = data.bots.known; 1189 break; 1190 default: 1191 console.warn(`Unknown list type ${i}.`); 1192 } 1193 dd.appendChild(makeElement('span', {}, title)); 1194 dd.appendChild(makeElement('strong', {}, value)); 1195 botsVsHumans.appendChild(dd); 1196 } 1197 } 1198 1199 // update known bots list: 1200 const botlist = document.getElementById('botmon__botslist'); 1201 botlist.innerHTML = "<dt>Top 5 known bots (page views)</dt>"; 1202 1203 let bots = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => { 1204 return b._pageViews.length - a._pageViews.length; 1205 }); 1206 1207 for (let i=0; i < Math.min(bots.length, 5); i++) { 1208 const dd = makeElement('dd'); 1209 dd.appendChild(makeElement('span', {'class': 'bot bot_' + bots[i]._bot.id }, bots[i]._bot.n)); 1210 dd.appendChild(makeElement('strong', undefined, bots[i]._pageViews.length)); 1211 botlist.appendChild(dd); 1212 } 1213 1214 // update the suspected bot IP ranges list: 1215 const botIps = document.getElementById('botmon__today__botips'); 1216 if (botIps) { 1217 botIps.appendChild(makeElement('dt', {}, "Top 5 suspected bots’ IP ranges")); 1218 1219 const ipList = BotMon.live.data.analytics.getTopBotIPRanges(5); 1220 ipList.forEach( (ipInfo) => { 1221 const li = makeElement('dd'); 1222 li.appendChild(makeElement('span', {'class': 'ip ip' + ipInfo.typ }, ipInfo.ip)); 1223 li.appendChild(makeElement('span', {'class': 'count' }, ipInfo.num)); 1224 botIps.append(li) 1225 }) 1226 } 1227 1228 // update the webmetrics overview: 1229 const wmoverview = document.getElementById('botmon__today__wm_overview'); 1230 if (wmoverview) { 1231 const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 100); 1232 1233 wmoverview.appendChild(makeElement('dt', {}, "Overview")); 1234 for (let i = 0; i < 3; i++) { 1235 const dd = makeElement('dd'); 1236 let title = ''; 1237 let value = ''; 1238 switch(i) { 1239 case 0: 1240 title = "Total page views:"; 1241 value = data.totalPageViews; 1242 break; 1243 case 1: 1244 title = "Total visitors (est.):"; 1245 value = data.totalVisits; 1246 break; 1247 case 2: 1248 title = "Bounce rate (est.):"; 1249 value = bounceRate + '%'; 1250 break; 1251 default: 1252 console.warn(`Unknown list type ${i}.`); 1253 } 1254 dd.appendChild(makeElement('span', {}, title)); 1255 dd.appendChild(makeElement('strong', {}, value)); 1256 wmoverview.appendChild(dd); 1257 } 1258 } 1259 1260 } 1261 }, 1262 1263 status: { 1264 setText: function(txt) { 1265 const el = document.getElementById('botmon__today__status'); 1266 if (el && BotMon.live.gui.status._errorCount <= 0) { 1267 el.innerText = txt; 1268 } 1269 }, 1270 1271 setTitle: function(html) { 1272 const el = document.getElementById('botmon__today__title'); 1273 if (el) { 1274 el.innerHTML = html; 1275 } 1276 }, 1277 1278 setError: function(txt) { 1279 console.error(txt); 1280 BotMon.live.gui.status._errorCount += 1; 1281 const el = document.getElementById('botmon__today__status'); 1282 if (el) { 1283 el.innerText = "An error occured. See the browser log for details!"; 1284 el.classList.add('error'); 1285 } 1286 }, 1287 _errorCount: 0, 1288 1289 showBusy: function(txt = null) { 1290 BotMon.live.gui.status._busyCount += 1; 1291 const el = document.getElementById('botmon__today__busy'); 1292 if (el) { 1293 el.style.display = 'inline-block'; 1294 } 1295 if (txt) BotMon.live.gui.status.setText(txt); 1296 }, 1297 _busyCount: 0, 1298 1299 hideBusy: function(txt = null) { 1300 const el = document.getElementById('botmon__today__busy'); 1301 BotMon.live.gui.status._busyCount -= 1; 1302 if (BotMon.live.gui.status._busyCount <= 0) { 1303 if (el) el.style.display = 'none'; 1304 if (txt) BotMon.live.gui.status.setText(txt); 1305 } 1306 } 1307 }, 1308 1309 lists: { 1310 init: function() { 1311 1312 // function shortcut: 1313 const makeElement = BotMon.t._makeElement; 1314 1315 const parent = document.getElementById('botmon__today__visitorlists'); 1316 if (parent) { 1317 1318 for (let i=0; i < 4; i++) { 1319 1320 // change the id and title by number: 1321 let listTitle = ''; 1322 let listId = ''; 1323 switch (i) { 1324 case 0: 1325 listTitle = "Registered users"; 1326 listId = 'users'; 1327 break; 1328 case 1: 1329 listTitle = "Probably humans"; 1330 listId = 'humans'; 1331 break; 1332 case 2: 1333 listTitle = "Suspected bots"; 1334 listId = 'suspectedBots'; 1335 break; 1336 case 3: 1337 listTitle = "Known bots"; 1338 listId = 'knownBots'; 1339 break; 1340 default: 1341 console.warn('Unknown list number.'); 1342 } 1343 1344 const details = makeElement('details', { 1345 'data-group': listId, 1346 'data-loaded': false 1347 }); 1348 const title = details.appendChild(makeElement('summary')); 1349 title.appendChild(makeElement('span', {'class':'title'}, listTitle)); 1350 title.appendChild(makeElement('span', {'class':'counter'}, '–')); 1351 details.addEventListener("toggle", this._onDetailsToggle); 1352 1353 parent.appendChild(details); 1354 1355 } 1356 } 1357 }, 1358 1359 _onDetailsToggle: function(e) { 1360 //console.info('BotMon.live.gui.lists._onDetailsToggle()'); 1361 1362 const target = e.target; 1363 1364 if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet 1365 target.setAttribute('data-loaded', 'loading'); 1366 1367 const fillType = target.getAttribute('data-group'); 1368 const fillList = BotMon.live.data.analytics.groups[fillType]; 1369 if (fillList && fillList.length > 0) { 1370 1371 const ul = BotMon.t._makeElement('ul'); 1372 1373 fillList.forEach( (it) => { 1374 ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType)); 1375 }); 1376 1377 target.appendChild(ul); 1378 target.setAttribute('data-loaded', 'true'); 1379 } else { 1380 target.setAttribute('data-loaded', 'false'); 1381 } 1382 1383 } 1384 }, 1385 1386 _makeVisitorItem: function(data, type) { 1387 1388 // shortcut for neater code: 1389 const make = BotMon.t._makeElement; 1390 1391 let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); 1392 const platformName = (data._platform ? data._platform.n : 'Unknown'); 1393 const clientName = (data._client ? data._client.n: 'Unknown'); 1394 1395 const li = make('li'); // root list item 1396 const details = make('details'); 1397 const summary = make('summary'); 1398 details.appendChild(summary); 1399 1400 const span1 = make('span'); /* left-hand group */ 1401 if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */ 1402 1403 const botName = ( data._bot && data._bot.n ? data._bot.n : "Unknown"); 1404 span1.appendChild(make('span', { /* Bot */ 1405 'class': 'bot bot_' + (data._bot ? data._bot.id : 'unknown'), 1406 'title': "Bot: " + botName 1407 }, botName)); 1408 1409 } else if (data._type == BM_USERTYPE.KNOWN_USER) { /* User only */ 1410 1411 span1.appendChild(make('span', { /* User */ 1412 'class': 'user_known', 1413 'title': "User: " + data.usr 1414 }, data.usr)); 1415 1416 } else { /* others */ 1417 1418 if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; 1419 span1.appendChild(make('span', { /* IP-Address */ 1420 'class': 'ipaddr ip' + ipType, 1421 'title': "IP-Address: " + data.ip 1422 }, data.ip)); 1423 1424 } 1425 1426 if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* Not for bots */ 1427 span1.appendChild(make('span', { /* Platform */ 1428 'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'), 1429 'title': "Platform: " + platformName 1430 }, platformName)); 1431 1432 span1.appendChild(make('span', { /* Client */ 1433 'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'), 1434 'title': "Client: " + clientName 1435 }, clientName)); 1436 } 1437 1438 summary.appendChild(span1); 1439 const span2 = make('span'); /* right-hand group */ 1440 1441 span2.appendChild(make('span', { /* page views */ 1442 'class': 'pageviews' 1443 }, data._pageViews.length)); 1444 1445 summary.appendChild(span2); 1446 1447 // add details expandable section: 1448 details.appendChild(BotMon.live.gui.lists._makeVisitorDetails(data, type)); 1449 1450 li.appendChild(details); 1451 return li; 1452 }, 1453 1454 _makeVisitorDetails: function(data, type) { 1455 1456 // shortcut for neater code: 1457 const make = BotMon.t._makeElement; 1458 1459 let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); 1460 if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; 1461 const platformName = (data._platform ? data._platform.n : 'Unknown'); 1462 const clientName = (data._client ? data._client.n: 'Unknown'); 1463 1464 const dl = make('dl', {'class': 'visitor_details'}); 1465 1466 if (data._type == BM_USERTYPE.KNOWN_BOT) { 1467 1468 dl.appendChild(make('dt', {}, "Bot name:")); /* bot info */ 1469 dl.appendChild(make('dd', {'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown')}, 1470 (data._bot ? data._bot.n : 'Unknown'))); 1471 1472 if (data._bot && data._bot.url) { 1473 dl.appendChild(make('dt', {}, "Bot info:")); /* bot info */ 1474 const botInfoDd = dl.appendChild(make('dd')); 1475 botInfoDd.appendChild(make('a', { 1476 'href': data._bot.url, 1477 'target': '_blank' 1478 }, data._bot.url)); /* bot info link*/ 1479 1480 } 1481 1482 } else { /* not for bots */ 1483 1484 dl.appendChild(make('dt', {}, "Client:")); /* client */ 1485 dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')}, 1486 clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) )); 1487 1488 dl.appendChild(make('dt', {}, "Platform:")); /* platform */ 1489 dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')}, 1490 platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) )); 1491 1492 dl.appendChild(make('dt', {}, "IP-Address:")); 1493 dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip)); 1494 1495 /*dl.appendChild(make('dt', {}, "ID:")); 1496 dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id));*/ 1497 } 1498 1499 if (Math.abs(data._lastSeen - data._firstSeen) < 100) { 1500 dl.appendChild(make('dt', {}, "Seen:")); 1501 dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString())); 1502 } else { 1503 dl.appendChild(make('dt', {}, "First seen:")); 1504 dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString())); 1505 dl.appendChild(make('dt', {}, "Last seen:")); 1506 dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString())); 1507 } 1508 1509 dl.appendChild(make('dt', {}, "User-Agent:")); 1510 dl.appendChild(make('dd', {'class': 'agent'}, data.agent)); 1511 1512 dl.appendChild(make('dt', {}, "Languages:")); 1513 dl.appendChild(make('dd', {'class': 'langs'}, "Client accepts: [" + data.accept + "]; Page: [" + data.lang + ']')); 1514 1515 /*dl.appendChild(make('dt', {}, "Visitor Type:")); 1516 dl.appendChild(make('dd', undefined, data._type ));*/ 1517 1518 dl.appendChild(make('dt', {}, "Seen by:")); 1519 dl.appendChild(make('dd', undefined, data._seenBy.join(', ') )); 1520 1521 dl.appendChild(make('dt', {}, "Visited pages:")); 1522 const pagesDd = make('dd', {'class': 'pages'}); 1523 const pageList = make('ul'); 1524 1525 /* list all page views */ 1526 data._pageViews.forEach( (page) => { 1527 const pgLi = make('li'); 1528 1529 let visitTimeStr = "Bounce"; 1530 const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); 1531 if (visitDuration > 0) { 1532 visitTimeStr = Math.floor(visitDuration / 1000) + "s"; 1533 } 1534 1535 pgLi.appendChild(make('span', {}, page.pg)); /* DW Page ID */ 1536 if (page._ref) { 1537 pgLi.appendChild(make('span', { 1538 'data-ref': page._ref.host, 1539 'title': "Referrer: " + page._ref.full 1540 }, page._ref.site)); 1541 } else { 1542 pgLi.appendChild(make('span', { 1543 }, "No referer")); 1544 } 1545 pgLi.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount)); 1546 pgLi.appendChild(make('span', {}, BotMon.t._formatTime(page._firstSeen))); 1547 1548 // get the time difference: 1549 const tDiff = BotMon.t._formatTimeDiff(page._firstSeen, page._lastSeen); 1550 if (tDiff) { 1551 pgLi.appendChild(make('span', {'class': 'visit-length', 'title': 'Last seen: ' + page._lastSeen.toLocaleString()}, tDiff)); 1552 } else { 1553 pgLi.appendChild(make('span', {'class': 'bounce'}, "Bounce")); 1554 } 1555 1556 pageList.appendChild(pgLi); 1557 }); 1558 pagesDd.appendChild(pageList); 1559 dl.appendChild(pagesDd); 1560 1561 /* bot evaluation rating */ 1562 if (data._type !== BM_USERTYPE.KNOWN_BOT && data._type !== BM_USERTYPE.KNOWN_USER) { 1563 dl.appendChild(make('dt', undefined, "Bot rating:")); 1564 dl.appendChild(make('dd', {'class': 'bot-rating'}, ( data._botVal ? data._botVal : '–' ) + '/' + BotMon.live.data.rules._threshold )); 1565 1566 /* add bot evaluation details: */ 1567 if (data._eval) { 1568 dl.appendChild(make('dt', {}, "Bot evaluation details:")); 1569 const evalDd = make('dd'); 1570 const testList = make('ul',{ 1571 'class': 'eval' 1572 }); 1573 data._eval.forEach( test => { 1574 1575 const tObj = BotMon.live.data.rules.getRuleInfo(test); 1576 let tDesc = tObj ? tObj.desc : test; 1577 1578 // special case for Bot IP range test: 1579 if (tObj.func == 'fromKnownBotIP') { 1580 const rangeInfo = BotMon.live.data.rules.getBotIPInfo(data.ip); 1581 if (rangeInfo) { 1582 tDesc += ' (' + (rangeInfo.label ? rangeInfo.label : 'Unknown') + ')'; 1583 } 1584 } 1585 1586 // create the entry field 1587 const tstLi = make('li'); 1588 tstLi.appendChild(make('span', { 1589 'data-testid': test 1590 }, tDesc)); 1591 tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') )); 1592 testList.appendChild(tstLi); 1593 }); 1594 1595 // add total row 1596 const tst2Li = make('li', { 1597 'class': 'total' 1598 }); 1599 /*tst2Li.appendChild(make('span', {}, "Total:")); 1600 tst2Li.appendChild(make('span', {}, data._botVal)); 1601 testList.appendChild(tst2Li);*/ 1602 1603 evalDd.appendChild(testList); 1604 dl.appendChild(evalDd); 1605 } 1606 } 1607 // return the element to add to the UI: 1608 return dl; 1609 } 1610 1611 } 1612 } 1613}; 1614 1615/* launch only if the BotMon admin panel is open: */ 1616if (document.getElementById('botmon__admin')) { 1617 BotMon.init(); 1618}