diff --git a/server/handlers/analytics_perday_handler.coffee b/server/handlers/analytics_perday_handler.coffee index d2a9d13fad4..5c78f2ee154 100644 --- a/server/handlers/analytics_perday_handler.coffee +++ b/server/handlers/analytics_perday_handler.coffee @@ -14,90 +14,11 @@ class AnalyticsPerDayHandler extends Handler getByRelationship: (req, res, args...) -> return @sendForbiddenError res unless @hasAccess req - return @getLevelHelpsBySlugs(req, res) if args[1] is 'level_helps' return @getLevelSubscriptionsBySlugs(req, res) if args[1] is 'level_subscriptions' return @getRecurringRevenue(req, res) if args[1] is 'recurring_revenue' super(arguments...) - getLevelHelpsBySlugs: (req, res) -> - # Send back an array of per-day level help buttons clicked and videos started - # Parameters: - # slugs - level slugs - # startDay - Inclusive, optional, YYYYMMDD e.g. '20141214' - # endDay - Exclusive, optional, YYYYMMDD e.g. '20141216' - - levelSlugs = req.query.slugs or req.body.slugs - startDay = req.query.startDay or req.body.startDay - endDay = req.query.endDay or req.body.endDay - - # log.warn "level_helps levelSlugs='#{levelSlugs}' startDay=#{startDay} endDay=#{endDay}" - - return @sendSuccess res, [] unless levelSlugs? - - # Cache results in app server memory for 1 day - @levelHelpsCache ?= {} - @levelHelpsCachedSince ?= new Date() - if (new Date()) - @levelHelpsCachedSince > 86400 * 1000 - @levelHelpsCache = {} - @levelHelpsCachedSince = new Date() - cacheKey = levelSlugs.join '' - cacheKey += 's' + startDay if startDay? - cacheKey += 'e' + endDay if endDay? - return @sendSuccess res, helps if helps = @levelHelpsCache[cacheKey] - - findQueryParams = {v: {$in: ['Problem alert help clicked', 'Spell palette help clicked', 'Start help video', 'all'].concat(levelSlugs)}} - AnalyticsString.find(findQueryParams).exec (err, documents) => - if err? then return @sendDatabaseError res, err - - levelStringIDSlugMap = {} - for doc in documents - alertHelpEventID = doc._id if doc.v is 'Problem alert help clicked' - palettteHelpEventID = doc._id if doc.v is 'Spell palette help clicked' - videoHelpEventID = doc._id if doc.v is 'Start help video' - filterEventID = doc._id if doc.v is 'all' - levelStringIDSlugMap[doc._id] = doc.v if doc.v in levelSlugs - - return @sendSuccess res, [] unless alertHelpEventID? and palettteHelpEventID? and videoHelpEventID? and filterEventID? - - queryParams = {$and: [ - {e: {$in: [alertHelpEventID, palettteHelpEventID, videoHelpEventID]}}, - {f: filterEventID}, - {l: {$in: Object.keys(levelStringIDSlugMap)}} - ]} - queryParams["$and"].push {d: {$gte: startDay}} if startDay? - queryParams["$and"].push {d: {$lt: endDay}} if endDay? - AnalyticsPerDay.find(queryParams).exec (err, documents) => - if err? then return @sendDatabaseError res, err - - levelEventCounts = {} - for doc in documents - levelEventCounts[doc.l] ?= {} - levelEventCounts[doc.l][doc.d] ?= {} - levelEventCounts[doc.l][doc.d][doc.e] ?= 0 - levelEventCounts[doc.l][doc.d][doc.e] += doc.c - - helps = [] - for levelID of levelEventCounts - for day of levelEventCounts[levelID] - alertHelps = 0 - paletteHelps = 0 - videoStarts = 0 - for eventID of levelEventCounts[levelID][day] - alertHelps = levelEventCounts[levelID][day][eventID] if parseInt(eventID) is alertHelpEventID - paletteHelps = levelEventCounts[levelID][day][eventID] if parseInt(eventID) is palettteHelpEventID - videoStarts = levelEventCounts[levelID][day][eventID] if parseInt(eventID) is videoHelpEventID - helps.push - level: levelStringIDSlugMap[levelID] - day: day - alertHelps: alertHelps - paletteHelps: paletteHelps - videoStarts: videoStarts - - @levelHelpsCache[cacheKey] = helps - @sendSuccess res, helps - - getLevelSubscriptionsBySlugs: (req, res) -> # Send back an array of level subscriptions shown and purchased counts # Parameters: diff --git a/server/middleware/analytics-perday.coffee b/server/middleware/analytics-perday.coffee index d9fe5b8648e..060358c8c59 100644 --- a/server/middleware/analytics-perday.coffee +++ b/server/middleware/analytics-perday.coffee @@ -283,10 +283,87 @@ getLevelDropsBySlugs = wrap (req, res) -> res.send(drops) +getLevelHelpsBySlugs = wrap (req, res) -> + # Send back an array of per-day level help buttons clicked and videos started + # Parameters: + # slugs - level slugs + # startDay - Inclusive, optional, YYYYMMDD e.g. '20141214' + # endDay - Exclusive, optional, YYYYMMDD e.g. '20141216' + + levelSlugs = req.body.slugs + startDay = req.body.startDay + endDay = req.body.endDay + + # log.warn "level_helps levelSlugs='#{levelSlugs}' startDay=#{startDay} endDay=#{endDay}" + + return res.send([]) unless levelSlugs? + + # Cache results in app server memory for 1 day + module.exports.levelHelpsCache ?= {} + module.exports.levelHelpsCachedSince ?= new Date() + if (new Date()) - module.exports.levelHelpsCachedSince > 86400 * 1000 + module.exports.levelHelpsCache = {} + module.exports.levelHelpsCachedSince = new Date() + cacheKey = levelSlugs.join '' + cacheKey += 's' + startDay if startDay? + cacheKey += 'e' + endDay if endDay? + return res.send(helps) if helps = module.exports.levelHelpsCache[cacheKey] + + findQueryParams = {v: {$in: ['Problem alert help clicked', 'Spell palette help clicked', 'Start help video', 'all'].concat(levelSlugs)}} + documents = yield AnalyticsString.find(findQueryParams) + + levelStringIDSlugMap = {} + for doc in documents + alertHelpEventID = doc._id if doc.v is 'Problem alert help clicked' + palettteHelpEventID = doc._id if doc.v is 'Spell palette help clicked' + videoHelpEventID = doc._id if doc.v is 'Start help video' + filterEventID = doc._id if doc.v is 'all' + levelStringIDSlugMap[doc._id] = doc.v if doc.v in levelSlugs + + return res.send([]) unless alertHelpEventID? and palettteHelpEventID? and videoHelpEventID? and filterEventID? + + queryParams = {$and: [ + {e: {$in: [alertHelpEventID, palettteHelpEventID, videoHelpEventID]}}, + {f: filterEventID}, + {l: {$in: Object.keys(levelStringIDSlugMap)}} + ]} + queryParams["$and"].push {d: {$gte: startDay}} if startDay? + queryParams["$and"].push {d: {$lt: endDay}} if endDay? + documents = yield AnalyticsPerDay.find(queryParams) + + levelEventCounts = {} + for doc in documents + levelEventCounts[doc.l] ?= {} + levelEventCounts[doc.l][doc.d] ?= {} + levelEventCounts[doc.l][doc.d][doc.e] ?= 0 + levelEventCounts[doc.l][doc.d][doc.e] += doc.c + + helps = [] + for levelID of levelEventCounts + for day of levelEventCounts[levelID] + alertHelps = 0 + paletteHelps = 0 + videoStarts = 0 + for eventID of levelEventCounts[levelID][day] + alertHelps = levelEventCounts[levelID][day][eventID] if parseInt(eventID) is alertHelpEventID + paletteHelps = levelEventCounts[levelID][day][eventID] if parseInt(eventID) is palettteHelpEventID + videoStarts = levelEventCounts[levelID][day][eventID] if parseInt(eventID) is videoHelpEventID + helps.push + level: levelStringIDSlugMap[levelID] + day: day + alertHelps: alertHelps + paletteHelps: paletteHelps + videoStarts: videoStarts + + module.exports.levelHelpsCache[cacheKey] = helps + res.send(helps) + + module.exports = { getActiveClasses, getActiveUsers, getCampaignCompletionsBySlug, getLevelCompletionsBySlug, - getLevelDropsBySlugs + getLevelDropsBySlugs, + getLevelHelpsBySlugs } diff --git a/server/routes/index.coffee b/server/routes/index.coffee index 9057979ff8a..20a5f153640 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -69,7 +69,8 @@ module.exports.setup = (app) -> app.post('/db/analytics_perday/-/campaign_completions', mw.auth.checkHasPermission(['admin']), mw.analyticsPerDay.getCampaignCompletionsBySlug) app.post('/db/analytics_perday/-/level_completions', mw.auth.checkHasPermission(['admin']), mw.analyticsPerDay.getLevelCompletionsBySlug) app.post('/db/analytics_perday/-/level_drops', mw.auth.checkHasPermission(['admin']), mw.analyticsPerDay.getLevelDropsBySlugs) - + app.post('/db/analytics_perday/-/level_helps', mw.auth.checkHasPermission(['admin']), mw.analyticsPerDay.getLevelHelpsBySlugs) + Article = require '../models/Article' app.get('/db/article', mw.rest.get(Article)) app.post('/db/article', mw.auth.checkLoggedIn(), mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Article)) diff --git a/spec/server/functional/analytics-perday.spec.coffee b/spec/server/functional/analytics-perday.spec.coffee index d8dfae40ddb..f596395fd8a 100644 --- a/spec/server/functional/analytics-perday.spec.coffee +++ b/spec/server/functional/analytics-perday.spec.coffee @@ -233,6 +233,68 @@ describe 'POST /db/analytics_perday/-/level_drops', -> expect(res.statusCode).toBe(200) expect(AnalyticsPerDay.find.calls.count()).toBe(1) + it 'accepts start and date inputs', utils.wrap -> + json = { + slugs: [@level.get('slug')] + startDay: '20140101' + endDay: '20160101' + } + [res] = yield request.postAsync({@url, json}) + expect(res.body.length).toBe(1) + + json = { + slugs: [@level.get('slug')] + startDay: '20160101' + endDay: '20180101' + } + [res] = yield request.postAsync({@url, json}) + expect(res.body.length).toBe(0) + + +describe 'POST /db/analytics_perday/-/level_helps', -> + beforeEach utils.wrap -> + admin = yield utils.initAdmin() + yield utils.loginUser(admin) + + @level = yield utils.makeLevel() + levelString = yield utils.makeAnalyticsString({v:@level.get('slug')}) + alertString = yield utils.makeAnalyticsString({v: 'Problem alert help clicked'}) + paletteString = yield utils.makeAnalyticsString({v: 'Spell palette help clicked'}) + videoString = yield utils.makeAnalyticsString({v: 'Start help video'}) + + allString = yield utils.makeAnalyticsString({v:'all'}) + i = 100 + yield utils.makeAnalyticsPerDay({d: '20150101', c: i++}, {e: alertString, f: allString, l:levelString}) + yield utils.makeAnalyticsPerDay({d: '20150101', c: i++}, {e: paletteString, f: allString, l:levelString}) + yield utils.makeAnalyticsPerDay({d: '20150101', c: i++}, {e: videoString, f: allString, l:levelString}) + + @url = utils.getUrl('/db/analytics_perday/-/level_helps') + @json = { slugs: [@level.get('slug')] } + + + it 'returns 403 unless you are an admin', utils.wrap -> + user = yield utils.initUser() + yield utils.loginUser(user) + [res] = yield request.postAsync({@url, @json}) + expect(res.statusCode).toBe(403) + + it 'returns start and finish data for levels in a given level, and saves a cache', utils.wrap -> + spyOn(AnalyticsPerDay, 'find').and.callThrough() + expect(middleware.analyticsPerDay.levelHelpsCache).toBeUndefined() + [res] = yield request.postAsync({@url, @json}) + expect(res.body).toEqual([ { + level: @level.get('slug') + day: '20150101', + alertHelps: 100, + paletteHelps: 101, + videoStarts: 102 + } ]) + expect(middleware.analyticsPerDay.levelHelpsCache).toBeDefined() + expect(AnalyticsPerDay.find.calls.count()).toBe(1) + [res] = yield request.postAsync({@url, @json}) + expect(res.statusCode).toBe(200) + expect(AnalyticsPerDay.find.calls.count()).toBe(1) + it 'accepts start and date inputs', utils.wrap -> json = { slugs: [@level.get('slug')]