Update campaign editor analytics

Optimize analytics.log.event user event data find() to use stream()
instead of exec(), which is better for large result sets
Fix startDay formatting bug
Per-level recent sessions to 100
This commit is contained in:
Matt Lott 2015-01-07 14:49:21 -08:00
parent 3eb34bab5c
commit c54fea929e
5 changed files with 120 additions and 103 deletions

View file

@ -131,6 +131,18 @@ module.exports.kindaEqual = compare = (l, r) ->
else
return false
# Return UTC string "YYYY-MM-DD" for today + offset
module.exports.getUTCDay = (offset=0) ->
# TODO: Move to utility
day = new Date()
day.setDate(day.getUTCDate() + offset)
partYear = day.getUTCFullYear()
partMonth = (day.getUTCMonth() + 1)
partMonth = "0" + partMonth if partMonth < 10
partDay = day.getUTCDate()
partDay = "0" + partDay if partDay < 10
"#{partYear}-#{partMonth}-#{partDay}"
# Fast, basic way to replace text in an element when you don't need much.
# http://stackoverflow.com/a/4962398/540620
if document?

View file

@ -65,7 +65,7 @@
h4 Recent Sessions
if recentSessions
div(style='font-size:10pt') Latest 10 sessions for this level
div(style='font-size:10pt') Latest #{recentSessions.length} sessions for this level
div(style='font-size:10pt') Double-click row to open player and session
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead

View file

@ -243,9 +243,7 @@ module.exports = class CampaignEditorView extends RootView
getCampaignCompletions: =>
# Fetch last 7 days of campaign drop-off rates
startDay = new Date()
startDay.setDate(startDay.getUTCDate() - 6)
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
startDay = utils.getUTCDay -6
success = (data) =>
return if @destroyed

View file

@ -3,6 +3,7 @@ Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
ModelModal = require 'views/modal/ModelModal'
User = require 'models/User'
utils = require 'core/utils'
module.exports = class CampaignLevelView extends CocoView
id: 'campaign-level-view'
@ -47,9 +48,7 @@ module.exports = class CampaignLevelView extends CocoView
getCommonLevelProblems: ->
# Fetch last 30 days of common level problems
startDay = new Date()
startDay.setDate(startDay.getUTCDate() - 29)
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
startDay = utils.getUTCDay -29
success = (data) =>
return if @destroyed
@ -77,9 +76,7 @@ module.exports = class CampaignLevelView extends CocoView
@levelCompletions = _.map data, mapFn, @
@render()
startDay = new Date()
startDay.setDate(startDay.getUTCDate() - 6)
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
startDay = utils.getUTCDay -6
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'level_completions', {
@ -97,10 +94,8 @@ module.exports = class CampaignLevelView extends CocoView
@levelPlaytimes = data.sort (a, b) -> if a.created < b.created then 1 else -1
@render()
startDay = new Date()
startDay.setDate(startDay.getUTCDate() - 6)
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
startDay = utils.getUTCDay -6
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'playtime_averages', {
url: '/db/level/-/playtime_averages'
@ -111,7 +106,7 @@ module.exports = class CampaignLevelView extends CocoView
request.load()
getRecentSessions: ->
limit = 10
limit = 100
success = (data) =>
return if @destroyed

View file

@ -106,11 +106,12 @@ class AnalyticsLogEventHandler extends Handler
# startDay - Inclusive, optional, e.g. '2014-12-14'
# endDay - Exclusive, optional, e.g. '2014-12-16'
# TODO: Must be a better way to organize this series of 3 database calls (campaigns, levels, analytics)
# TODO: Must be a better way to organize this series of database calls (campaigns, levels, analytics)
# TODO: An uncached call can take over 30s locally
# TODO: Returns all the campaigns
# TODO: Calculate overall campaign stats
# TODO: Assumes db campaign levels are in progression order. Should build this based on actual progression.
# TODO: Remove earliest duplicate event so our dropped counts will be more accurate.
campaignSlug = req.query.slug or req.body.slug
startDay = req.query.startDay or req.body.startDay
@ -129,100 +130,111 @@ class AnalyticsLogEventHandler extends Handler
cacheKey += 'e' + endDay if endDay?
return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey]
getCompletions = (campaigns) =>
getCompletions = (campaigns, userProgression) =>
# Calculate campaign drop off rates
# Input:
# campaigns - per-campaign dictionary of ordered level slugs
# userProgression - per-user event lists
# Remove duplicate user events
for user of userProgression
userProgression[user] = _.uniq userProgression[user], false, (val, index, arr) -> val.event + val.level
# Order user progression by created
for user of userProgression
userProgression[user].sort (a,b) -> if a.created < b.created then return -1 else 1
# Per-level start/drop/finish/drop
levelProgression = {}
for user of userProgression
for i in [0...userProgression[user].length]
event = userProgression[user][i].event
level = userProgression[user][i].level
levelProgression[level] ?=
started: 0
startDropped: 0
finished: 0
finishDropped: 0
if event is 'Started Level'
levelProgression[level].started++
levelProgression[level].startDropped++ if i is userProgression[user].length - 1
else if event is 'Saw Victory'
levelProgression[level].finished++
levelProgression[level].finishDropped++ if i is userProgression[user].length - 1
# Put in campaign order
completions = {}
for level of levelProgression
for campaign of campaigns
if level in campaigns[campaign]
started = levelProgression[level].started
startDropped = levelProgression[level].startDropped
finished = levelProgression[level].finished
finishDropped = levelProgression[level].finishDropped
completions[campaign] ?=
levels: []
# overall:
# started: 0,
# startDropped: 0,
# finished: 0,
# finishDropped: 0
completions[campaign].levels.push
level: level
started: started
startDropped: startDropped
finished: finished
finishDropped: finishDropped
break
# Sort level data by campaign order
for campaign of completions
completions[campaign].levels.sort (a, b) ->
if campaigns[campaign].indexOf(a.level) < campaigns[campaign].indexOf(b.level) then return -1 else 1
# Return all campaign data for simplicity
# Cache other individual campaigns too, since we have them
@campaignDropOffsCache[cacheKey] = completions
for campaign of completions
cacheKey = campaign
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
@campaignDropOffsCache[cacheKey] = completions
@sendSuccess res, completions
getUserEventData = (campaigns) =>
# Gather user start and finish event data
# Input:
# campaigns - per-campaign dictionary of ordered level slugs
# Output:
# userProgression - per-user event lists
userProgression = {}
queryParams = {$and: [{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}]}
queryParams["$and"].push created: {$gte: new Date(startDay + "T00:00:00.000Z")} if startDay?
queryParams["$and"].push created: {$lt: new Date(endDay + "T00:00:00.000Z")} if endDay?
AnalyticsLogEvent.find(queryParams).select('created event properties user').exec (err, data) =>
if err? then return @sendDatabaseError res, err
# Bucketize events by user
userProgression = {}
userLevelEventMap = {} # Only want unique users per-level/event
for item in data
created = item.get('created')
event = item.get('event')
if event is 'Saw Victory'
level = item.get('properties.level').toLowerCase().replace new RegExp(' ', 'g'), '-'
else
level = item.get('properties.levelID')
continue unless level?
user = item.get('user')
userLevelEventMap[user] ?= {}
userLevelEventMap[user][level] ?= {}
unless userLevelEventMap[user][level][event]
userLevelEventMap[user][level][event] = true
userProgression[user] ?= []
userProgression[user].push
created: created
event: event
level: level
# Order user progression by created
for user of userProgression
userProgression[user].sort (a,b) -> if a.created < b.created then return -1 else 1
# Per-level start/drop/finish/drop
levelProgression = {}
for user of userProgression
for i in [0...userProgression[user].length]
event = userProgression[user][i].event
level = userProgression[user][i].level
levelProgression[level] ?=
started: 0
startDropped: 0
finished: 0
finishDropped: 0
if event is 'Started Level'
levelProgression[level].started++
levelProgression[level].startDropped++ if i is userProgression[user].length - 1
else if event is 'Saw Victory'
levelProgression[level].finished++
levelProgression[level].finishDropped++ if i is userProgression[user].length - 1
# Put in campaign order
completions = {}
for level of levelProgression
for campaign of campaigns
if level in campaigns[campaign]
started = levelProgression[level].started
startDropped = levelProgression[level].startDropped
finished = levelProgression[level].finished
finishDropped = levelProgression[level].finishDropped
completions[campaign] ?=
levels: []
# overall:
# started: 0,
# startDropped: 0,
# finished: 0,
# finishDropped: 0
completions[campaign].levels.push
level: level
started: started
startDropped: startDropped
finished: finished
finishDropped: finishDropped
break
# Sort level data by campaign order
for campaign of completions
completions[campaign].levels.sort (a, b) ->
if campaigns[campaign].indexOf(a.level) < campaigns[campaign].indexOf(b.level) then return -1 else 1
# Return all campaign data for simplicity
# Cache other individual campaigns too, since we have them
@campaignDropOffsCache[cacheKey] = completions
for campaign of completions
cacheKey = campaign
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
@campaignDropOffsCache[cacheKey] = completions
@sendSuccess res, completions
# Query stream is better for large results
# http://mongoosejs.com/docs/api.html#query_Query-stream
stream = AnalyticsLogEvent.find(queryParams).select('created event properties user').stream()
stream.on 'data', (item) =>
created = item.get('created')
event = item.get('event')
if event is 'Saw Victory'
level = item.get('properties.level').toLowerCase().replace new RegExp(' ', 'g'), '-'
else
level = item.get('properties.levelID')
return unless level?
user = item.get('user')
userProgression[user] ?= []
userProgression[user].push
created: created
event: event
level: level
.on 'error', (err) =>
return @sendDatabaseError res, err
.on 'close', () =>
getCompletions campaigns, userProgression
getLevelData = (campaigns, campaignLevelIDs) =>
# Get level data and replace levelIDs with level slugs in campaigns
@ -246,7 +258,7 @@ class AnalyticsLogEventHandler extends Handler
mapFn = (item) -> levelSlugMap[item]
campaigns[campaign] = _.map campaigns[campaign], mapFn, @
getCompletions campaigns
getUserEventData campaigns
getCampaignData = () =>
# Get campaign data