diff --git a/public/src/admin/advanced/errors.js b/public/src/admin/advanced/errors.js new file mode 100644 index 0000000000..1713311eb7 --- /dev/null +++ b/public/src/admin/advanced/errors.js @@ -0,0 +1,64 @@ +"use strict"; +/*global config, define, app, socket, ajaxify, bootbox, templates, Chart, utils */ + +define('admin/advanced/errors', ['Chart'], function(Chart) { + var Errors = {}; + + Errors.init = function() { + var notFoundCanvas = document.getElementById('not-found'), + tooBusyCanvas = document.getElementById('toobusy'), + dailyLabels = utils.getDaysArray(); + + dailyLabels.length = 7; + + if (utils.isMobile()) { + Chart.defaults.global.showTooltips = false; + } + + var data = { + 'not-found': { + labels: dailyLabels, + datasets: [ + { + label: "", + fillColor: "rgba(186,139,175,0.2)", + strokeColor: "rgba(186,139,175,1)", + pointColor: "rgba(186,139,175,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(186,139,175,1)", + data: ajaxify.data.analytics['not-found'] + } + ] + }, + 'toobusy': { + labels: dailyLabels, + datasets: [ + { + label: "", + fillColor: "rgba(151,187,205,0.2)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(151,187,205,1)", + data: ajaxify.data.analytics['toobusy'] + } + ] + } + }; + + notFoundCanvas.width = $(notFoundCanvas).parent().width(); + tooBusyCanvas.width = $(tooBusyCanvas).parent().width(); + new Chart(notFoundCanvas.getContext('2d')).Line(data['not-found'], { + responsive: true, + animation: false + }); + new Chart(tooBusyCanvas.getContext('2d')).Line(data['toobusy'], { + responsive: true, + animation: false + }); + }; + + return Errors; +}); \ No newline at end of file diff --git a/public/src/admin/manage/category-analytics.js b/public/src/admin/manage/category-analytics.js index d9f38ed259..cf78cfa5e3 100644 --- a/public/src/admin/manage/category-analytics.js +++ b/public/src/admin/manage/category-analytics.js @@ -1,7 +1,7 @@ "use strict"; -/*global config, define, app, socket, ajaxify, bootbox, templates, Chart, utils */ +/*global config, define, app, socket, ajaxify, bootbox, templates, utils */ -define('admin/manage/category-analytics', [], function() { +define('admin/manage/category-analytics', ['Chart'], function(Chart) { var CategoryAnalytics = {}; CategoryAnalytics.init = function() { diff --git a/src/analytics.js b/src/analytics.js index c1ede42eba..bbb1c06794 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -187,4 +187,11 @@ var db = require('./database'); }, callback); }; + Analytics.getErrorAnalytics = function(callback) { + async.parallel({ + 'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7), + 'toobusy': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7) + }, callback); + }; + }(exports)); \ No newline at end of file diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 2bba60cae6..c3ce96d205 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -14,6 +14,7 @@ var adminController = { }, events: require('./admin/events'), logs: require('./admin/logs'), + errors: require('./admin/errors'), database: require('./admin/database'), postCache: require('./admin/postCache'), plugins: require('./admin/plugins'), diff --git a/src/controllers/admin/errors.js b/src/controllers/admin/errors.js new file mode 100644 index 0000000000..d5ce5a1b76 --- /dev/null +++ b/src/controllers/admin/errors.js @@ -0,0 +1,20 @@ +'use strict'; + +var async = require('async'); + +var meta = require('../../meta'), + analytics = require('../../analytics'); + +var errorsController = {}; + +errorsController.get = function(req, res) { + async.parallel({ + 'not-found': async.apply(meta.errors.get), + analytics: async.apply(analytics.getErrorAnalytics) + }, function(err, data) { + res.render('admin/advanced/errors', data); + }); +}; + + +module.exports = errorsController; \ No newline at end of file diff --git a/src/controllers/index.js b/src/controllers/index.js index 09f156b19d..26b7527ce9 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -373,12 +373,14 @@ Controllers.handle404 = function(req, res) { } else if (isLanguage.test(req.url)) { res.status(200).json({}); } else if (req.path.startsWith(relativePath + '/uploads') || (req.get('accept') && req.get('accept').indexOf('text/html') === -1) || req.path === '/favicon.ico') { + meta.errors.log404(req.path || ''); res.sendStatus(404); } else if (req.accepts('html')) { if (process.env.NODE_ENV === 'development') { winston.warn('Route requested but not found: ' + req.url); } + meta.errors.log404(req.path.replace(/^\/api/, '') || ''); res.status(404); if (res.locals.isAPI) { diff --git a/src/meta.js b/src/meta.js index eb5816fdaf..3806c69556 100644 --- a/src/meta.js +++ b/src/meta.js @@ -24,6 +24,7 @@ var async = require('async'), require('./meta/sounds')(Meta); require('./meta/settings')(Meta); require('./meta/logs')(Meta); + require('./meta/errors')(Meta); require('./meta/tags')(Meta); require('./meta/dependencies')(Meta); Meta.templates = require('./meta/templates'); diff --git a/src/meta/errors.js b/src/meta/errors.js new file mode 100644 index 0000000000..5582d537b8 --- /dev/null +++ b/src/meta/errors.js @@ -0,0 +1,36 @@ +'use strict'; + +var async = require('async'), + winston = require('winston'), + validator = require('validator'); + +var db = require('../database'), + analytics = require('../analytics'); + +module.exports = function(Meta) { + + Meta.errors = {}; + + Meta.errors.log404 = function(route, callback) { + callback = callback || function() {}; + route = route.replace(/\/$/, ''); // remove trailing slashes + analytics.increment('errors:404'); + db.sortedSetIncrBy('errors:404', 1, route, callback); + }; + + Meta.errors.get = function(callback) { + db.getSortedSetRevRangeByScoreWithScores('errors:404', 0, -1, '+inf', '-inf', function(err, data) { + data = data.map(function(nfObject) { + nfObject.value = validator.escape(nfObject.value); + return nfObject; + }); + + callback(null, data); + }); + }; + + Meta.errors.clear = function(callback) { + console.log('clear errors'); + callback(); + }; +}; diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 9267289d3f..7409bd1577 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -302,6 +302,7 @@ middleware.privateUploads = function(req, res, next) { middleware.busyCheck = function(req, res, next) { if (global.env === 'production' && (!meta.config.hasOwnProperty('eventLoopCheckEnabled') || parseInt(meta.config.eventLoopCheckEnabled, 10) === 1) && toobusy()) { + analytics.increment('errors:503'); res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html')); } else { next(); diff --git a/src/routes/admin.js b/src/routes/admin.js index 66391053a7..9767c26e9d 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -82,6 +82,7 @@ function addRoutes(router, middleware, controllers) { router.get('/advanced/database', middlewares, controllers.admin.database.get); router.get('/advanced/events', middlewares, controllers.admin.events.get); router.get('/advanced/logs', middlewares, controllers.admin.logs.get); + router.get('/advanced/errors', middlewares, controllers.admin.errors.get); router.get('/advanced/post-cache', middlewares, controllers.admin.postCache.get); router.get('/development/logger', middlewares, controllers.admin.logger.get); diff --git a/src/views/admin/advanced/errors.tpl b/src/views/admin/advanced/errors.tpl new file mode 100644 index 0000000000..996f71f818 --- /dev/null +++ b/src/views/admin/advanced/errors.tpl @@ -0,0 +1,61 @@ +
Route | +Count | + + + +
---|---|
{../value} | +{../score} | +
+
+ Hooray! There are no routes that were not found.
+
+ |
+