2014-12-12 16:15:13 -05:00
'use strict' ;
2019-09-23 00:04:57 -04:00
const cronJob = require ( 'cron' ) . CronJob ;
const winston = require ( 'winston' ) ;
const nconf = require ( 'nconf' ) ;
const crypto = require ( 'crypto' ) ;
const LRU = require ( 'lru-cache' ) ;
2016-02-25 16:12:50 -05:00
2014-12-12 16:15:13 -05:00
2019-09-23 00:04:57 -04:00
const db = require ( './database' ) ;
const utils = require ( './utils' ) ;
const plugins = require ( './plugins' ) ;
2014-12-12 16:15:13 -05:00
2019-09-23 00:04:57 -04:00
const Analytics = module . exports ;
2014-12-12 16:15:13 -05:00
2019-09-23 00:04:57 -04:00
const counters = { } ;
let pageViews = 0 ;
let pageViewsRegistered = 0 ;
let pageViewsGuest = 0 ;
let pageViewsBot = 0 ;
let uniqueIPCount = 0 ;
let 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
}
} ) ;
}
} ;
2019-09-23 00:04:57 -04:00
Analytics . writeData = async function ( ) {
const today = new Date ( ) ;
const month = new Date ( ) ;
const dbQueue = [ ] ;
2016-11-18 15:57:53 +03:00
today . setHours ( today . getHours ( ) , 0 , 0 , 0 ) ;
month . setMonth ( month . getMonth ( ) , 1 ) ;
month . setHours ( 0 , 0 , 0 , 0 ) ;
if ( pageViews > 0 ) {
2019-09-23 00:04:57 -04:00
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:pageviews' , pageViews , today . getTime ( ) ) ) ;
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:pageviews:month' , pageViews , month . getTime ( ) ) ) ;
2016-11-18 15:57:53 +03:00
pageViews = 0 ;
}
2018-10-24 11:24:37 -04:00
if ( pageViewsRegistered > 0 ) {
2019-09-23 00:04:57 -04:00
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:pageviews:registered' , pageViewsRegistered , today . getTime ( ) ) ) ;
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:pageviews:month:registered' , pageViewsRegistered , month . getTime ( ) ) ) ;
2018-10-24 11:24:37 -04:00
pageViewsRegistered = 0 ;
}
if ( pageViewsGuest > 0 ) {
2019-09-23 00:04:57 -04:00
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:pageviews:guest' , pageViewsGuest , today . getTime ( ) ) ) ;
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:pageviews:month:guest' , pageViewsGuest , month . getTime ( ) ) ) ;
2018-10-24 11:24:37 -04:00
pageViewsGuest = 0 ;
}
if ( pageViewsBot > 0 ) {
2019-09-23 00:04:57 -04:00
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:pageviews:bot' , pageViewsBot , today . getTime ( ) ) ) ;
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:pageviews:month:bot' , pageViewsBot , month . getTime ( ) ) ) ;
2018-10-24 11:24:37 -04:00
pageViewsBot = 0 ;
}
2016-11-18 15:57:53 +03:00
if ( uniquevisitors > 0 ) {
2019-09-23 00:04:57 -04:00
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:uniquevisitors' , uniquevisitors , today . getTime ( ) ) ) ;
2016-11-18 15:57:53 +03:00
uniquevisitors = 0 ;
}
if ( uniqueIPCount > 0 ) {
2019-09-23 00:04:57 -04:00
dbQueue . push ( db . incrObjectFieldBy ( 'global' , 'uniqueIPCount' , uniqueIPCount ) ) ;
2016-11-18 15:57:53 +03:00
uniqueIPCount = 0 ;
}
if ( Object . keys ( counters ) . length > 0 ) {
2019-09-23 00:04:57 -04:00
for ( const key in counters ) {
2016-11-18 15:57:53 +03:00
if ( counters . hasOwnProperty ( key ) ) {
2019-09-23 00:04:57 -04:00
dbQueue . push ( db . sortedSetIncrBy ( 'analytics:' + key , counters [ key ] , today . getTime ( ) ) ) ;
2016-11-18 15:57:53 +03:00
delete counters [ key ] ;
}
2014-12-12 16:15:13 -05:00
}
2016-11-18 15:57:53 +03:00
}
2019-09-23 00:04:57 -04:00
try {
await Promise . all ( dbQueue ) ;
} catch ( err ) {
winston . error ( '[analytics] Encountered error while writing analytics to data store' , err ) ;
throw err ;
}
2016-11-18 15:57:53 +03:00
} ;
2015-11-04 17:43:43 -05:00
2019-09-23 00:04:57 -04:00
Analytics . getHourlyStatsForSet = async function ( set , hour , numHours ) {
2020-02-07 11:20:59 -05:00
// Guard against accidental ommission of `analytics:` prefix
if ( ! set . startsWith ( 'analytics:' ) ) {
set = 'analytics:' + set ;
}
2019-09-23 00:04:57 -04:00
const terms = { } ;
const 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
2019-09-23 00:04:57 -04:00
for ( let 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
2019-09-23 00:04:57 -04:00
const counts = await db . sortedSetScores ( set , hoursArr ) ;
2016-02-25 16:12:50 -05:00
2019-09-23 00:04:57 -04:00
hoursArr . forEach ( function ( term , index ) {
terms [ term ] = parseInt ( counts [ index ] , 10 ) || 0 ;
} ) ;
2016-02-25 16:12:50 -05:00
2019-09-23 00:04:57 -04:00
const termsArr = [ ] ;
2014-12-12 16:15:13 -05:00
2019-09-23 00:04:57 -04:00
hoursArr . reverse ( ) ;
hoursArr . forEach ( function ( hour ) {
termsArr . push ( terms [ hour ] ) ;
2016-11-18 15:57:53 +03:00
} ) ;
2015-08-13 12:20:53 -04:00
2019-09-23 00:04:57 -04:00
return termsArr ;
} ;
2015-08-13 12:20:53 -04:00
2019-09-23 00:04:57 -04:00
Analytics . getDailyStatsForSet = async function ( set , day , numDays ) {
2020-02-07 11:20:59 -05:00
// Guard against accidental ommission of `analytics:` prefix
2020-02-07 10:28:32 -05:00
if ( ! set . startsWith ( 'analytics:' ) ) {
set = 'analytics:' + set ;
}
2019-09-23 00:04:57 -04:00
const daysArr = [ ] ;
2016-11-18 15:57:53 +03:00
day = new Date ( day ) ;
2020-02-07 11:20:59 -05:00
day . setDate ( day . getDate ( ) + 2 ) ; // set the date to the day after tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values, and we also want today's values
2016-11-18 15:57:53 +03:00
day . setHours ( 0 , 0 , 0 , 0 ) ;
2015-08-13 12:20:53 -04:00
2019-09-23 00:04:57 -04:00
while ( numDays > 0 ) {
/* eslint-disable no-await-in-loop */
const dayData = await Analytics . getHourlyStatsForSet ( set , day . getTime ( ) - ( 1000 * 60 * 60 * 24 * numDays ) , 24 ) ;
daysArr . push ( dayData . reduce ( ( cur , next ) => cur + next ) ) ;
2017-02-18 01:12:18 -07:00
numDays -= 1 ;
2019-09-23 00:04:57 -04:00
}
return daysArr ;
2016-11-18 15:57:53 +03:00
} ;
Analytics . getUnwrittenPageviews = function ( ) {
return pageViews ;
} ;
2019-09-23 00:04:57 -04:00
Analytics . getSummary = async function ( ) {
const today = new Date ( ) ;
2017-05-11 16:53:30 -04:00
today . setHours ( 0 , 0 , 0 , 0 ) ;
2016-11-18 15:57:53 +03:00
2019-09-23 00:04:57 -04:00
const [ seven , thirty ] = await Promise . all ( [
Analytics . getDailyStatsForSet ( 'analytics:pageviews' , today , 7 ) ,
Analytics . getDailyStatsForSet ( 'analytics:pageviews' , today , 30 ) ,
] ) ;
return {
seven : seven . reduce ( ( sum , cur ) => sum + cur , 0 ) ,
thirty : thirty . reduce ( ( sum , cur ) => sum + cur , 0 ) ,
} ;
2016-11-18 15:57:53 +03:00
} ;
2019-09-23 00:04:57 -04:00
Analytics . getCategoryAnalytics = async function ( cid ) {
return await utils . promiseParallel ( {
'pageviews:hourly' : Analytics . getHourlyStatsForSet ( 'analytics:pageviews:byCid:' + cid , Date . now ( ) , 24 ) ,
'pageviews:daily' : Analytics . getDailyStatsForSet ( 'analytics:pageviews:byCid:' + cid , Date . now ( ) , 30 ) ,
'topics:daily' : Analytics . getDailyStatsForSet ( 'analytics:topics:byCid:' + cid , Date . now ( ) , 7 ) ,
'posts:daily' : Analytics . getDailyStatsForSet ( 'analytics:posts:byCid:' + cid , Date . now ( ) , 7 ) ,
} ) ;
2016-11-18 15:57:53 +03:00
} ;
2019-09-23 00:04:57 -04:00
Analytics . getErrorAnalytics = async function ( ) {
return await utils . promiseParallel ( {
'not-found' : Analytics . getDailyStatsForSet ( 'analytics:errors:404' , Date . now ( ) , 7 ) ,
toobusy : Analytics . getDailyStatsForSet ( 'analytics:errors:503' , Date . now ( ) , 7 ) ,
} ) ;
2016-11-18 15:57:53 +03:00
} ;
2019-09-23 00:04:57 -04:00
Analytics . getBlacklistAnalytics = async function ( ) {
return await utils . promiseParallel ( {
daily : Analytics . getDailyStatsForSet ( 'analytics:blacklist' , Date . now ( ) , 7 ) ,
hourly : Analytics . getHourlyStatsForSet ( 'analytics:blacklist' , Date . now ( ) , 24 ) ,
} ) ;
2017-08-24 12:37:22 -04:00
} ;
2019-06-28 14:59:55 -04:00
Analytics . async = require ( './promisify' ) ( Analytics ) ;