2014-12-12 16:15:13 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
2015-11-04 17:43:43 -05:00
|
|
|
var cronJob = require('cron').CronJob;
|
2016-02-25 16:12:50 -05:00
|
|
|
var async = require('async');
|
2016-03-05 19:12:41 -05:00
|
|
|
var winston = require('winston');
|
2018-06-01 15:14:25 -04:00
|
|
|
var nconf = require('nconf');
|
|
|
|
|
var crypto = require('crypto');
|
2018-11-07 15:53:14 -05:00
|
|
|
var LRU = require('lru-cache');
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2015-11-04 17:43:43 -05:00
|
|
|
var db = require('./database');
|
2018-06-14 17:22:04 +02:00
|
|
|
var plugins = require('./plugins');
|
2014-12-12 16:15:13 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
var Analytics = module.exports;
|
2014-12-12 16:15:13 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
var counters = {};
|
2014-12-12 16:15:13 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
var pageViews = 0;
|
2018-10-24 11:24:37 -04:00
|
|
|
var pageViewsRegistered = 0;
|
|
|
|
|
var pageViewsGuest = 0;
|
|
|
|
|
var pageViewsBot = 0;
|
2016-11-18 15:57:53 +03:00
|
|
|
var uniqueIPCount = 0;
|
|
|
|
|
var uniquevisitors = 0;
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2018-06-01 15:14:25 -04:00
|
|
|
/**
|
|
|
|
|
* TODO: allow the cache's max value to be configurable. On high-traffic installs,
|
|
|
|
|
* the cache could be exhausted continuously if there are more than 500 concurrently
|
|
|
|
|
* active users
|
|
|
|
|
*/
|
2018-12-14 17:31:06 -05:00
|
|
|
var ipCache = new LRU({
|
2018-06-01 15:14:25 -04:00
|
|
|
max: 500,
|
|
|
|
|
length: function () { return 1; },
|
|
|
|
|
maxAge: 0,
|
|
|
|
|
});
|
|
|
|
|
|
2016-12-09 14:53:49 -05:00
|
|
|
new cronJob('*/10 * * * * *', function () {
|
2016-11-18 15:57:53 +03:00
|
|
|
Analytics.writeData();
|
|
|
|
|
}, null, true);
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2016-12-09 14:39:31 -05:00
|
|
|
Analytics.increment = function (keys, callback) {
|
2016-11-18 15:57:53 +03:00
|
|
|
keys = Array.isArray(keys) ? keys : [keys];
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2018-06-14 17:22:04 +02:00
|
|
|
plugins.fireHook('action:analytics.increment', { keys: keys });
|
|
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
keys.forEach(function (key) {
|
|
|
|
|
counters[key] = counters[key] || 0;
|
2017-02-18 01:12:18 -07:00
|
|
|
counters[key] += 1;
|
2016-11-18 15:57:53 +03:00
|
|
|
});
|
2016-12-09 14:39:31 -05:00
|
|
|
|
|
|
|
|
if (typeof callback === 'function') {
|
|
|
|
|
callback();
|
|
|
|
|
}
|
2016-11-18 15:57:53 +03:00
|
|
|
};
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
Analytics.pageView = function (payload) {
|
2017-02-18 01:12:18 -07:00
|
|
|
pageViews += 1;
|
2014-12-12 16:15:13 -05:00
|
|
|
|
2018-10-24 11:24:37 -04:00
|
|
|
if (payload.uid > 0) {
|
|
|
|
|
pageViewsRegistered += 1;
|
|
|
|
|
} else if (payload.uid < 0) {
|
|
|
|
|
pageViewsBot += 1;
|
|
|
|
|
} else {
|
|
|
|
|
pageViewsGuest += 1;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
if (payload.ip) {
|
2018-06-01 15:14:25 -04:00
|
|
|
// Retrieve hash or calculate if not present
|
|
|
|
|
let hash = ipCache.get(payload.ip + nconf.get('secret'));
|
|
|
|
|
if (!hash) {
|
|
|
|
|
hash = crypto.createHash('sha1').update(payload.ip + nconf.get('secret')).digest('hex');
|
|
|
|
|
ipCache.set(payload.ip + nconf.get('secret'), hash);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db.sortedSetScore('ip:recent', hash, function (err, score) {
|
2016-11-18 15:57:53 +03:00
|
|
|
if (err) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!score) {
|
2017-02-18 01:12:18 -07:00
|
|
|
uniqueIPCount += 1;
|
2016-11-18 15:57:53 +03:00
|
|
|
}
|
|
|
|
|
var today = new Date();
|
|
|
|
|
today.setHours(today.getHours(), 0, 0, 0);
|
|
|
|
|
if (!score || score < today.getTime()) {
|
2017-02-18 01:12:18 -07:00
|
|
|
uniquevisitors += 1;
|
2018-06-01 15:14:25 -04:00
|
|
|
db.sortedSetAdd('ip:recent', Date.now(), hash);
|
2016-11-18 15:57:53 +03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Analytics.writeData = function (callback) {
|
|
|
|
|
callback = callback || function () {};
|
|
|
|
|
var today = new Date();
|
|
|
|
|
var month = new Date();
|
|
|
|
|
var dbQueue = [];
|
|
|
|
|
|
|
|
|
|
today.setHours(today.getHours(), 0, 0, 0);
|
|
|
|
|
month.setMonth(month.getMonth(), 1);
|
|
|
|
|
month.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
if (pageViews > 0) {
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews', pageViews, today.getTime()));
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month', pageViews, month.getTime()));
|
|
|
|
|
pageViews = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-24 11:24:37 -04:00
|
|
|
if (pageViewsRegistered > 0) {
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:registered', pageViewsRegistered, today.getTime()));
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month:registered', pageViewsRegistered, month.getTime()));
|
|
|
|
|
pageViewsRegistered = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pageViewsGuest > 0) {
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:guest', pageViewsGuest, today.getTime()));
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month:guest', pageViewsGuest, month.getTime()));
|
|
|
|
|
pageViewsGuest = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pageViewsBot > 0) {
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:bot', pageViewsBot, today.getTime()));
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month:bot', pageViewsBot, month.getTime()));
|
|
|
|
|
pageViewsBot = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
if (uniquevisitors > 0) {
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:uniquevisitors', uniquevisitors, today.getTime()));
|
|
|
|
|
uniquevisitors = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (uniqueIPCount > 0) {
|
|
|
|
|
dbQueue.push(async.apply(db.incrObjectFieldBy, 'global', 'uniqueIPCount', uniqueIPCount));
|
|
|
|
|
uniqueIPCount = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Object.keys(counters).length > 0) {
|
2017-02-18 01:52:56 -07:00
|
|
|
for (var key in counters) {
|
2016-11-18 15:57:53 +03:00
|
|
|
if (counters.hasOwnProperty(key)) {
|
|
|
|
|
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:' + key, counters[key], today.getTime()));
|
|
|
|
|
delete counters[key];
|
|
|
|
|
}
|
2014-12-12 16:15:13 -05:00
|
|
|
}
|
2016-11-18 15:57:53 +03:00
|
|
|
}
|
2015-11-04 17:43:43 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
async.parallel(dbQueue, function (err) {
|
|
|
|
|
if (err) {
|
2017-11-01 18:58:44 -06:00
|
|
|
winston.error('[analytics] Encountered error while writing analytics to data store', err);
|
2016-02-25 16:12:50 -05:00
|
|
|
}
|
2016-11-18 15:57:53 +03:00
|
|
|
callback(err);
|
|
|
|
|
});
|
|
|
|
|
};
|
2015-11-04 17:43:43 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
Analytics.getHourlyStatsForSet = function (set, hour, numHours, callback) {
|
2017-02-17 20:20:42 -07:00
|
|
|
var terms = {};
|
|
|
|
|
var hoursArr = [];
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
hour = new Date(hour);
|
|
|
|
|
hour.setHours(hour.getHours(), 0, 0, 0);
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2017-02-18 01:12:18 -07:00
|
|
|
for (var i = 0, ii = numHours; i < ii; i += 1) {
|
2016-11-18 15:57:53 +03:00
|
|
|
hoursArr.push(hour.getTime());
|
|
|
|
|
hour.setHours(hour.getHours() - 1, 0, 0, 0);
|
|
|
|
|
}
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
db.sortedSetScores(set, hoursArr, function (err, counts) {
|
|
|
|
|
if (err) {
|
|
|
|
|
return callback(err);
|
2016-02-25 16:12:50 -05:00
|
|
|
}
|
|
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
hoursArr.forEach(function (term, index) {
|
|
|
|
|
terms[term] = parseInt(counts[index], 10) || 0;
|
|
|
|
|
});
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
var termsArr = [];
|
2016-02-25 16:12:50 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
hoursArr.reverse();
|
|
|
|
|
hoursArr.forEach(function (hour) {
|
|
|
|
|
termsArr.push(terms[hour]);
|
2016-02-25 16:12:50 -05:00
|
|
|
});
|
2014-12-12 16:15:13 -05:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
callback(null, termsArr);
|
|
|
|
|
});
|
|
|
|
|
};
|
2015-08-13 12:20:53 -04:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
Analytics.getDailyStatsForSet = function (set, day, numDays, callback) {
|
|
|
|
|
var daysArr = [];
|
2015-08-13 12:20:53 -04:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
day = new Date(day);
|
|
|
|
|
day.setDate(day.getDate() + 1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values
|
|
|
|
|
day.setHours(0, 0, 0, 0);
|
2015-08-13 12:20:53 -04:00
|
|
|
|
2016-11-18 15:57:53 +03:00
|
|
|
async.whilst(function () {
|
2017-02-18 01:12:18 -07:00
|
|
|
numDays -= 1;
|
|
|
|
|
return numDays + 1;
|
2016-11-18 15:57:53 +03:00
|
|
|
}, function (next) {
|
|
|
|
|
Analytics.getHourlyStatsForSet(set, day.getTime() - (1000 * 60 * 60 * 24 * numDays), 24, function (err, day) {
|
2015-08-13 12:20:53 -04:00
|
|
|
if (err) {
|
2016-11-18 15:57:53 +03:00
|
|
|
return next(err);
|
2015-08-13 12:20:53 -04:00
|
|
|
}
|
2016-11-18 15:57:53 +03:00
|
|
|
|
|
|
|
|
daysArr.push(day.reduce(function (cur, next) {
|
|
|
|
|
return cur + next;
|
|
|
|
|
}));
|
|
|
|
|
next();
|
2015-08-13 12:20:53 -04:00
|
|
|
});
|
2016-11-18 15:57:53 +03:00
|
|
|
}, function (err) {
|
|
|
|
|
callback(err, daysArr);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Analytics.getUnwrittenPageviews = function () {
|
|
|
|
|
return pageViews;
|
|
|
|
|
};
|
|
|
|
|
|
2017-05-11 16:53:30 -04:00
|
|
|
Analytics.getSummary = function (callback) {
|
|
|
|
|
var today = new Date();
|
|
|
|
|
today.setHours(0, 0, 0, 0);
|
2016-11-18 15:57:53 +03:00
|
|
|
|
2017-05-11 16:53:30 -04:00
|
|
|
async.parallel({
|
|
|
|
|
seven: async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews', today, 7),
|
|
|
|
|
thirty: async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews', today, 30),
|
|
|
|
|
}, function (err, scores) {
|
2017-05-11 17:22:40 -04:00
|
|
|
if (err) {
|
|
|
|
|
return callback(null, {
|
|
|
|
|
seven: 0,
|
|
|
|
|
thirty: 0,
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-05-11 16:53:30 -04:00
|
|
|
callback(null, {
|
2017-05-11 17:22:40 -04:00
|
|
|
seven: scores.seven.reduce(function (sum, cur) {
|
|
|
|
|
sum += cur;
|
|
|
|
|
return sum;
|
|
|
|
|
}, 0),
|
|
|
|
|
thirty: scores.thirty.reduce(function (sum, cur) {
|
|
|
|
|
sum += cur;
|
|
|
|
|
return sum;
|
|
|
|
|
}, 0),
|
2017-05-11 16:53:30 -04:00
|
|
|
});
|
2016-11-18 15:57:53 +03:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Analytics.getCategoryAnalytics = function (cid, callback) {
|
|
|
|
|
async.parallel({
|
|
|
|
|
'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24),
|
|
|
|
|
'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30),
|
|
|
|
|
'topics:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:topics:byCid:' + cid, Date.now(), 7),
|
|
|
|
|
'posts:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:posts:byCid:' + cid, Date.now(), 7),
|
|
|
|
|
}, callback);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Analytics.getErrorAnalytics = function (callback) {
|
|
|
|
|
async.parallel({
|
|
|
|
|
'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7),
|
2017-02-18 01:19:20 -07:00
|
|
|
toobusy: async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7),
|
2016-11-18 15:57:53 +03:00
|
|
|
}, callback);
|
|
|
|
|
};
|
|
|
|
|
|
2017-08-24 12:37:22 -04:00
|
|
|
Analytics.getBlacklistAnalytics = function (callback) {
|
|
|
|
|
async.parallel({
|
|
|
|
|
daily: async.apply(Analytics.getDailyStatsForSet, 'analytics:blacklist', Date.now(), 7),
|
|
|
|
|
hourly: async.apply(Analytics.getHourlyStatsForSet, 'analytics:blacklist', Date.now(), 24),
|
|
|
|
|
}, callback);
|
|
|
|
|
};
|