codecombat/server/analytics/analytics_perday_handler.coffee
Matt Lott 05f028d944 Update campaign editor analytics
Adding ‘left game’ counts to overall campaign view.  This is the number
of players that left the game after playing the given level.
2015-01-18 16:29:47 -08:00

257 lines
10 KiB
CoffeeScript

AnalyticsPerDay = require './AnalyticsPerDay'
AnalyticsString = require './AnalyticsString'
Campaign = require '../campaigns/Campaign'
Level = require '../levels/Level'
Handler = require '../commons/Handler'
log = require 'winston'
class AnalyticsPerDayHandler extends Handler
modelClass: AnalyticsPerDay
jsonSchema: require '../../app/schemas/models/analytics_perday'
hasAccess: (req) ->
req.method in ['GET'] or req.user?.isAdmin()
getByRelationship: (req, res, args...) ->
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'
super(arguments...)
getCampaignCompletionsBySlug: (req, res) ->
# Send back an ordered array of level 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.e] ?= 0
levelEventCounts[doc.l][doc.e] += doc.c
completions = []
for levelID of levelEventCounts
completions.push
level: levelStringIDSlugMap[levelID]
started: levelEventCounts[levelID][startEventID] ? 0
finished: levelEventCounts[levelID][finishEventID] ? 0
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 mappping
# 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: doc.get('name').toLowerCase().replace new RegExp(' ', 'g'), '-'
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
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
module.exports = new AnalyticsPerDayHandler()