Add campaign drop-offs analytics to editor

This commit is contained in:
Matt Lott 2015-01-01 12:01:43 -08:00
parent 0e9f1e0370
commit e65887ec79
5 changed files with 281 additions and 5 deletions
app
styles/editor/campaign
templates/editor/campaign
views/editor/campaign
server

View file

@ -21,3 +21,12 @@
bottom: 0
right: 0
width: 75%
#analytics-button
position: absolute
right: 1%
top: 1%
padding: 3px 8px
#analytics-modal .modal-content
background-color: white

View file

@ -40,5 +40,40 @@ block outer_content
#right-column
#campaign-view
#campaign-level-view.hidden
if campaignDropOffs
button.btn.btn-default#analytics-button(title="Analytics", data-toggle="modal" data-target="#analytics-modal") Analytics
.modal.fade#analytics-modal(tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true")
.modal-dialog
.modal-content
.modal-header
button.close(type="button", data-dismiss="modal", aria-label="Close")
span(aria-hidden="true") ×
h4.modal-title Analytics
.modal-body
if campaignDropOffs.startDay
if campaignDropOffs.endDay
div #{campaignDropOffs.startDay} to #{campaignDropOffs.endDay}
else
div #{campaignDropOffs.startDay} to today
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Level
td Started
td Dropped
td Drop %
td Finished
td Dropped
td Drop %
tbody
- for (var i = 0; i < campaignDropOffs.levels.length; i++)
tr
td= campaignDropOffs.levels[i].level
td= campaignDropOffs.levels[i].started
td= campaignDropOffs.levels[i].startDropped
td= campaignDropOffs.levels[i].startDropRate
td= campaignDropOffs.levels[i].finished
td= campaignDropOffs.levels[i].finishDropped
td= campaignDropOffs.levels[i].finishDropRate
block footer

View file

@ -47,6 +47,8 @@ module.exports = class CampaignEditorView extends RootView
@listenToOnce @levels, 'sync', @onFundamentalLoaded
@listenToOnce @achievements, 'sync', @onFundamentalLoaded
_.delay @getCampaignDropOffs, 1000
loadThangTypeNames: ->
# Load the names of the ThangTypes that this level's Treema nodes might want to display.
originals = []
@ -130,6 +132,7 @@ module.exports = class CampaignEditorView extends RootView
getRenderData: ->
c = super()
c.campaign = @campaign
c.campaignDropOffs = @campaignDropOffs
c
onClickSaveButton: ->
@ -236,6 +239,35 @@ module.exports = class CampaignEditorView extends RootView
achievement.set 'rewards', newRewards
if achievement.hasLocalChanges()
@toSave.add achievement
getCampaignDropOffs: =>
# 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()
success = (data) =>
return if @destroyed
# API returns all the campaign data currently
@campaignDropOffs = data[@campaignHandle]
mapFn = (item) ->
item.startDropRate = (item.startDropped / item.started * 100).toFixed(2)
item.finishDropRate = (item.finishDropped / item.finished * 100).toFixed(2)
item
@campaignDropOffs.levels = _.map @campaignDropOffs.levels, mapFn, @
@campaignDropOffs.startDay = startDay
@render()
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'campaign_drop_offs', {
url: '/db/analytics_log_event/-/campaign_drop_offs'
data: {startDay: startDay, slugs: [@campaignHandle]}
method: 'POST'
success: success
}, 0
request.load()
class LevelsNode extends TreemaObjectNode
valueClass: 'treema-levels'

View file

@ -20,6 +20,7 @@ class AnalyticsLogEventHandler extends Handler
getByRelationship: (req, res, args...) ->
return @getLevelCompletionsBySlugs(req, res) if args[1] is 'level_completions'
return @getCampaignDropOffs(req, res) if args[1] is 'campaign_drop_offs'
super(arguments...)
getLevelCompletionsBySlugs: (req, res) ->
@ -29,7 +30,7 @@ class AnalyticsLogEventHandler extends Handler
# startDay - Inclusive, optional, e.g. '2014-12-14'
# endDay - Exclusive, optional, e.g. '2014-12-16'
# TODO: An uncached call takes about 15s
# TODO: An uncached call takes about 15s locally
levelSlugs = req.query.slugs or req.body.slugs
startDay = req.query.startDay or req.body.startDay
@ -42,7 +43,7 @@ class AnalyticsLogEventHandler extends Handler
@levelCompletionsCachedSince ?= new Date()
if (new Date()) - @levelCompletionsCachedSince > 86400 * 1000 # Dumb cache expiration
@levelCompletionsCache = {}
@levelCompletionsCacheSince = new Date()
@levelCompletionsCachedSince = new Date()
cacheKey = levelSlugs.join(',')
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
@ -90,4 +91,203 @@ class AnalyticsLogEventHandler extends Handler
@levelCompletionsCache[cacheKey] = completions
@sendSuccess res, completions
getCampaignDropOffs: (req, res) ->
# Returns a dictionary of per-campaign level start and finish drop-offs
# Drop-off: last started or finished level event
# Parameters:
# slugs - array of campaign slugs
# startDay - Inclusive, optional, e.g. '2014-12-14'
# endDay - Exclusive, optional, e.g. '2014-12-16'
# TODO: Read per-campaign level progression data from a legit source
# TODO: An uncached call can take over 30s locally
# TODO: Returns all the campaigns
# TODO: Calculate overall campaign stats
campaignSlugs = req.query.slugs or req.body.slugs
startDay = req.query.startDay or req.body.startDay
endDay = req.query.endDay or req.body.endDay
return @sendSuccess res, [] unless campaignSlugs?
# Cache results for 1 day
@campaignDropOffsCache ?= {}
@campaignDropOffsCachedSince ?= new Date()
if (new Date()) - @campaignDropOffsCachedSince > 86400 * 1000 # Dumb cache expiration
@campaignDropOffsCache = {}
@campaignDropOffsCachedSince = new Date()
cacheKey = campaignSlugs.join(',')
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey]
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 = {}
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')
userProgression[user] ?= []
userProgression[user].push
created: created
event: event
level: level
# Order user progression by created
for user in 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
campaignRates = {}
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
campaignRates[campaign] ?=
levels: []
# overall:
# started: 0,
# startDropped: 0,
# finished: 0,
# finishDropped: 0
campaignRates[campaign].levels.push
level: level
started: started
startDropped: startDropped
finished: finished
finishDropped: finishDropped
break
# Sort level data by campaign order
for campaign of campaignRates
campaignRates[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] = campaignRates
for campaign of campaignRates
cacheKey = campaign
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?
@campaignDropOffsCache[cacheKey] = campaignRates
@sendSuccess res, campaignRates
# Copied from WorldMapView
dungeonLevels = [
'dungeons-of-kithgard',
'gems-in-the-deep',
'shadow-guard',
'kounter-kithwise',
'crawlways-of-kithgard',
'forgetful-gemsmith',
'true-names',
'favorable-odds',
'the-raised-sword',
'haunted-kithmaze',
'riddling-kithmaze',
'descending-further',
'the-second-kithmaze',
'dread-door',
'known-enemy',
'master-of-names',
'lowly-kithmen',
'closing-the-distance',
'tactical-strike',
'the-final-kithmaze',
'the-gauntlet',
'kithgard-gates',
'cavern-survival'
];
forestLevels = [
'defense-of-plainswood',
'winding-trail',
'patrol-buster',
'endangered-burl',
'village-guard',
'thornbush-farm',
'back-to-back',
'ogre-encampment',
'woodland-cleaver',
'shield-rush',
'peasant-protection',
'munchkin-swarm',
'munchkin-harvest',
'swift-dagger',
'shrapnel',
'arcane-ally',
'touch-of-death',
'bonemender',
'coinucopia',
'copper-meadows',
'drop-the-flag',
'deadly-pursuit',
'rich-forager',
'siege-of-stonehold',
'multiplayer-treasure-grove',
'dueling-grounds'
];
desertLevels = [
'the-dunes',
'the-mighty-sand-yak',
'oasis',
'sarven-road',
'sarven-gaps',
'thunderhooves',
'medical-attention',
'minesweeper',
'sarven-sentry',
'keeping-time',
'hoarding-gold',
'decoy-drill',
'yakstraction',
'sarven-brawl',
'desert-combat',
'dust',
'mirage-maker',
'sarven-savior',
'odd-sandstorm'
];
campaigns = {
'dungeon': dungeonLevels,
'forest': forestLevels,
'desert': desertLevels
}
module.exports = new AnalyticsLogEventHandler()

View file

@ -322,7 +322,7 @@ LevelHandler = class LevelHandler extends Handler
@playCountCachedSince ?= new Date()
if (new Date()) - @playCountCachedSince > 86400 * 1000 # Dumb cache expiration
@playCountCache = {}
@playCountCacheSince = new Date()
@playCountCachedSince = new Date()
cacheKey = levelIDs.join ','
if playCounts = @playCountCache[cacheKey]
return @sendSuccess res, playCounts
@ -349,7 +349,7 @@ LevelHandler = class LevelHandler extends Handler
# startDay - Inclusive, optional, e.g. '2014-12-14'
# endDay - Exclusive, optional, e.g. '2014-12-16'
# TODO: An uncached call takes about 20s for dungeons-of-kithgard locally
# TODO: An uncached call takes about 5s for dungeons-of-kithgard locally
# TODO: This is very similar to getLevelCompletionsBySlugs(), time to generalize analytics APIs?
levelSlugs = req.query.slugs or req.body.slugs
@ -363,7 +363,7 @@ LevelHandler = class LevelHandler extends Handler
@levelPlaytimesCachedSince ?= new Date()
if (new Date()) - @levelPlaytimesCachedSince > 86400 * 1000 # Dumb cache expiration
@levelPlaytimesCache = {}
@levelPlaytimesCacheSince = new Date()
@levelPlaytimesCachedSince = new Date()
cacheKey = levelSlugs.join(',')
cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay?