codecombat/server/handlers/analytics_perday_handler.coffee
2016-04-07 09:40:53 -07:00

494 lines
20 KiB
CoffeeScript

AnalyticsPerDay = require './../models/AnalyticsPerDay'
AnalyticsString = require './../models/AnalyticsString'
Campaign = require '../models/Campaign'
Level = require '../models/Level'
Handler = require '../commons/Handler'
log = require 'winston'
class AnalyticsPerDayHandler extends Handler
modelClass: AnalyticsPerDay
jsonSchema: require '../../app/schemas/models/analytics_perday'
hasAccess: (req) ->
req.user?.isAdmin() or false
getByRelationship: (req, res, args...) ->
return @sendForbiddenError res unless @hasAccess req
return @getActiveClasses(req, res) if args[1] is 'active_classes'
return @getActiveUsers(req, res) if args[1] is 'active_users'
return @getCampaignCompletionsBySlug(req, res) if args[1] is 'campaign_completions'
return @getLevelCompletionsBySlug(req, res) if args[1] is 'level_completions'
return @getLevelDropsBySlugs(req, res) if args[1] is 'level_drops'
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...)
getActiveClasses: (req, res) ->
events = [
'Active classes paid',
'Active classes trial',
'Active classes free'
]
AnalyticsString.find({v: {$in: events}}).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
eventIDs = []
eventStringMap = {}
for doc in documents
eventStringMap[doc._id.valueOf()] = doc.v
eventIDs.push doc._id
return @sendSuccess res, [] unless eventIDs.length is events.length
AnalyticsPerDay.find({e: {$in: eventIDs}}).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
dayCountsMap = {}
for doc in documents
dayCountsMap[doc.d] ?= {}
dayCountsMap[doc.d][eventStringMap[doc.e.valueOf()]] = doc.c
activeClasses = []
for key, val of dayCountsMap
activeClasses.push day: key, classes: dayCountsMap[key]
@sendSuccess(res, activeClasses)
getActiveUsers: (req, res) ->
events = ['DAU classroom paid', 'DAU classroom trial', 'DAU classroom free', 'DAU campaign paid', 'DAU campaign free',
'MAU classroom paid', 'MAU classroom trial', 'MAU classroom free', 'MAU campaign paid', 'MAU campaign free']
AnalyticsString.find({v: {$in: events}}).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
eventIDs = []
eventStringMap = {}
for doc in documents
eventIDs.push(doc._id)
eventStringMap[doc._id] = doc.v
AnalyticsPerDay.find({e: {$in: eventIDs}}).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
dayCountsMap = {}
for doc in documents
dayCountsMap[doc.d] ?= {}
dayCountsMap[doc.d][eventStringMap[doc.e]] = doc.c
activeUsers = ({day: day, events: eventCountMap} for day, eventCountMap of dayCountsMap)
@sendSuccess(res, activeUsers)
getCampaignCompletionsBySlug: (req, res) ->
# Send back an ordered array of level per-day starts and finishes
# Parameters:
# slug - campaign slug
# startDay - Inclusive, optional, YYYYMMDD e.g. '20141214'
# endDay - Exclusive, optional, YYYYMMDD e.g. '20141216'
campaignSlug = req.query.slug or req.body.slug
startDay = req.query.startDay or req.body.startDay
endDay = req.query.endDay or req.body.endDay
# log.warn "campaign_completions campaignSlug='#{campaignSlug}' startDay=#{startDay} endDay=#{endDay}"
return @sendSuccess res, [] unless campaignSlug?
# Cache results in app server memory for 1 day
@campaignCompletionsCache ?= {}
@campaignCompletionsCachedSince ?= new Date()
if (new Date()) - @campaignCompletionsCachedSince > 86400 * 1000
@campaignCompletionsCache = {}
@campaignCompletionsCachedSince = new Date()
cacheKey = campaignSlug
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
return @sendSuccess res, completions if completions = @campaignCompletionsCache[cacheKey]
getCompletions = (orderedLevelSlugs, levelStringIDSlugMap) =>
# 3. Send back an array of level starts and finishes
# Input:
# orderedLevelSlugs - Ordered list of level slugs, used for sorting results
# levelStringIDSlugMap - Maps level string IDs to level slugs
campaignLevelIDs = Object.keys(levelStringIDSlugMap)
AnalyticsString.find({v: {$in: ['Started Level', 'Saw Victory', 'all']}}).exec (err, documents) =>
if err? then return @sendDatabaseError res, err
for doc in documents
startEventID = doc._id if doc.v is 'Started Level'
finishEventID = doc._id if doc.v is 'Saw Victory'
filterEventID = doc._id if doc.v is 'all'
return @sendSuccess res, [] unless startEventID? and finishEventID? and filterEventID?
queryParams = {$and: [
{$or: [{e: startEventID}, {e: finishEventID}]},
{f: filterEventID},
{l: {$in: campaignLevelIDs}}
]}
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
completions = []
for levelID of levelEventCounts
days = {}
for day of levelEventCounts[levelID]
days[day] =
started: levelEventCounts[levelID][day][startEventID] ? 0
finished: levelEventCounts[levelID][day][finishEventID] ? 0
completions.push
level: levelStringIDSlugMap[levelID]
days: days
completions.sort (a, b) -> orderedLevelSlugs.indexOf(a.level) - orderedLevelSlugs.indexOf(b.level)
@campaignCompletionsCache[cacheKey] = completions
@sendSuccess res, completions
getLevelData = (campaignLevels) =>
# 2. Get ordered level slugs and string ID to level slug mapping
# Input:
# campaignLevels - array of Level IDs
queryParams = {original: {$in: campaignLevels}, "version.isLatestMajor": true, "version.isLatestMinor": true}
Level.find(queryParams).exec (err, documents) =>
if err? then return @sendDatabaseError res, err
# Save original level ID and slug in array for sorting
campaignOriginalSlugs = []
for doc in documents
campaignOriginalSlugs.push
slug: _.str.slugify(doc.get('name'))
original: doc.get('original').toString()
# Sort slugs against original levels from campaign
campaignOriginalSlugs.sort (a, b) ->
if campaignLevels.indexOf(a.original) < campaignLevels.indexOf(b.original) then -1 else 1
# Lookup analytics string IDs for level slugs
orderedLevelSlugs = []
orderedLevelSlugs.push item.slug for item in campaignOriginalSlugs
AnalyticsString.find({v: {$in: orderedLevelSlugs}}).exec (err, documents) =>
if err? then return @sendDatabaseError res, err
levelStringIDSlugMap = {}
levelStringIDSlugMap[doc._id] = doc.v for doc in documents
getCompletions orderedLevelSlugs, levelStringIDSlugMap
# 1. Get campaign levels
Campaign.find({slug: campaignSlug}).exec (err, documents) =>
if err? then return @sendDatabaseError res, err
campaignLevels = []
campaignLevels.push level for level of doc.get('levels') for doc in documents
getLevelData campaignLevels
getLevelDropsBySlugs: (req, res) ->
# Send back an array of level/drops
# Drops - Number of unique users for which this was the last level they played
# 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_drops levelSlugs='#{levelSlugs}' startDay=#{startDay} endDay=#{endDay}"
return @sendSuccess res, [] unless levelSlugs?
# Cache results in app server memory for 1 day
@levelDropsCache ?= {}
@levelDropsCachedSince ?= new Date()
if (new Date()) - @levelDropsCachedSince > 86400 * 1000
@levelDropsCache = {}
@levelDropsCachedSince = new Date()
cacheKey = levelSlugs.join ''
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
return @sendSuccess res, drops if drops = @levelDropsCache[cacheKey]
AnalyticsString.find({v: {$in: ['User Dropped', 'all'].concat(levelSlugs)}}).exec (err, documents) =>
if err? then return @sendDatabaseError res, err
levelStringIDSlugMap = {}
for doc in documents
droppedEventID = doc._id if doc.v is 'User Dropped'
filterEventID = doc._id if doc.v is 'all'
levelStringIDSlugMap[doc._id] = doc.v if doc.v in levelSlugs
return @sendSuccess res, [] unless droppedEventID? and filterEventID?
queryParams = {$and: [
{e: droppedEventID},
{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.e] ?= 0
levelEventCounts[doc.l][doc.e] += doc.c
drops = []
for levelID of levelEventCounts
drops.push
level: levelStringIDSlugMap[levelID]
dropped: levelEventCounts[levelID][droppedEventID] ? 0
@levelDropsCache[cacheKey] = drops
@sendSuccess res, drops
getLevelCompletionsBySlug: (req, res) ->
# Returns an array of per-day starts and finishes for given level
# Parameters:
# slug - level slug
# startDay - Inclusive, optional, YYYYMMDD e.g. '20141214'
# endDay - Exclusive, optional, YYYYMMDD e.g. '20141216'
# TODO: Code is similar to getCampaignCompletionsBySlug
levelSlug = req.query.slug or req.body.slug
startDay = req.query.startDay or req.body.startDay
endDay = req.query.endDay or req.body.endDay
return @sendSuccess res, [] unless levelSlug?
# log.warn "level_completions levelSlug='#{levelSlug}' startDay=#{startDay} endDay=#{endDay}"
# Cache results in app server memory for 1 day
@levelCompletionsCache ?= {}
@levelCompletionsCachedSince ?= new Date()
if (new Date()) - @levelCompletionsCachedSince > 86400 * 1000
@levelCompletionsCache = {}
@levelCompletionsCachedSince = new Date()
cacheKey = levelSlug
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
return @sendSuccess res, levelCompletions if levelCompletions = @levelCompletionsCache[cacheKey]
AnalyticsString.find({v: {$in: ['Started Level', 'Saw Victory', 'all', levelSlug]}}).exec (err, documents) =>
if err? then return @sendDatabaseError res, err
for doc in documents
startEventID = doc._id if doc.v is 'Started Level'
finishEventID = doc._id if doc.v is 'Saw Victory'
filterEventID = doc._id if doc.v is 'all'
levelID = doc._id if doc.v is levelSlug
return @sendSuccess res, [] unless startEventID? and finishEventID? and filterEventID? and levelID?
queryParams = {$and: [{$or: [{e: startEventID}, {e: finishEventID}]},{f: filterEventID},{l: levelID}]}
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
dayEventCounts = {}
for doc in documents
day = doc.get('d')
eventID = doc.get('e')
count = doc.get('c')
dayEventCounts[day] ?= {}
dayEventCounts[day][eventID] = count
completions = []
for day of dayEventCounts
started = 0
finished = 0
for eventID of dayEventCounts[day]
eventID = parseInt eventID
started = dayEventCounts[day][eventID] if eventID is startEventID
finished = dayEventCounts[day][eventID] if eventID is finishEventID
completions.push
created: day
started: started
finished: finished
@levelCompletionsCache[cacheKey] = completions
@sendSuccess res, completions
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:
# 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_subscriptions levelSlugs='#{levelSlugs}' startDay=#{startDay} endDay=#{endDay}"
return @sendSuccess res, [] unless levelSlugs?
# Cache results in app server memory for 1 day
@levelSubscriptionsCache ?= {}
@levelSubscriptionsCachedSince ?= new Date()
if (new Date()) - @levelSubscriptionsCachedSince > 86400 * 1000
@levelSubscriptionsCache = {}
@levelSubscriptionsCachedSince = new Date()
cacheKey = levelSlugs.join ''
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
return @sendSuccess res, subscriptions if subscriptions = @levelSubscriptionsCache[cacheKey]
findQueryParams = {v: {$in: ['Show subscription modal', 'Finished subscription purchase', 'all'].concat(levelSlugs)}}
AnalyticsString.find(findQueryParams).exec (err, documents) =>
if err? then return @sendDatabaseError res, err
levelStringIDSlugMap = {}
for doc in documents
showSubEventID = doc._id if doc.v is 'Show subscription modal'
finishSubEventID = doc._id if doc.v is 'Finished subscription purchase'
filterEventID = doc._id if doc.v is 'all'
levelStringIDSlugMap[doc._id] = doc.v if doc.v in levelSlugs
return @sendSuccess res, [] unless showSubEventID? and finishSubEventID? and filterEventID?
queryParams = {$and: [
{e: {$in: [showSubEventID, finishSubEventID]}},
{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.e] ?= 0
levelEventCounts[doc.l][doc.e] += doc.c
subscriptions = []
for levelID of levelEventCounts
subsShown = 0
subsPurchased = 0
for eventID of levelEventCounts[levelID]
subsShown = levelEventCounts[levelID][eventID] if parseInt(eventID) is showSubEventID
subsPurchased = levelEventCounts[levelID][eventID] if parseInt(eventID) is finishSubEventID
subscriptions.push
level: levelStringIDSlugMap[levelID]
shown: subsShown
purchased: subsPurchased
@levelSubscriptionsCache[cacheKey] = subscriptions
@sendSuccess res, subscriptions
getRecurringRevenue: (req, res) ->
events = [
'DRR gems',
'DRR school sales',
'DRR yearly subs',
'DRR monthly subs',
'DRR paypal']
AnalyticsString.find({v: {$in: events}}).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
eventIDs = []
eventStringMap = {}
for doc in documents
eventStringMap[doc._id.valueOf()] = doc.v
eventIDs.push doc._id
return @sendSuccess res, [] unless eventIDs.length is events.length
AnalyticsPerDay.find({e: {$in: eventIDs}}).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
dayCountsMap = {}
for doc in documents
dayCountsMap[doc.d] ?= {}
dayCountsMap[doc.d][eventStringMap[doc.e.valueOf()]] = doc.c
recurringRevenue = []
for key, val of dayCountsMap
recurringRevenue.push day: key, groups: dayCountsMap[key] ? {}
@sendSuccess(res, recurringRevenue)
module.exports = new AnalyticsPerDayHandler()