Update campaign analytics level ordering

Reading campaign levels from database.  Assumes database order is
roughly progression order.
This commit is contained in:
Matt Lott 2015-01-02 13:31:43 -08:00
parent 3457828a6b
commit b5969e0abc
4 changed files with 150 additions and 172 deletions
app
styles/editor/campaign
templates/editor/campaign
views/editor/campaign
server/analytics

View file

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

View file

@ -75,5 +75,7 @@ block outer_content
td= campaignDropOffs.levels[i].finished td= campaignDropOffs.levels[i].finished
td= campaignDropOffs.levels[i].finishDropped td= campaignDropOffs.levels[i].finishDropped
td= campaignDropOffs.levels[i].finishDropRate td= campaignDropOffs.levels[i].finishDropRate
else
button.btn.btn-default.disabled#analytics-button Analytics Loading...
block footer block footer

View file

@ -262,7 +262,7 @@ module.exports = class CampaignEditorView extends RootView
# TODO: Why do we need this url dash? # TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'campaign_drop_offs', { request = @supermodel.addRequestResource 'campaign_drop_offs', {
url: '/db/analytics_log_event/-/campaign_drop_offs' url: '/db/analytics_log_event/-/campaign_drop_offs'
data: {startDay: startDay, slugs: [@campaignHandle]} data: {startDay: startDay, slug: @campaignHandle}
method: 'POST' method: 'POST'
success: success success: success
}, 0 }, 0

View file

@ -1,4 +1,6 @@
AnalyticsLogEvent = require './AnalyticsLogEvent' AnalyticsLogEvent = require './AnalyticsLogEvent'
Campaign = require '../campaigns/Campaign'
Level = require '../levels/Level'
Handler = require '../commons/Handler' Handler = require '../commons/Handler'
log = require 'winston' log = require 'winston'
@ -103,16 +105,17 @@ class AnalyticsLogEventHandler extends Handler
# startDay - Inclusive, optional, e.g. '2014-12-14' # startDay - Inclusive, optional, e.g. '2014-12-14'
# endDay - Exclusive, optional, e.g. '2014-12-16' # endDay - Exclusive, optional, e.g. '2014-12-16'
# TODO: Read per-campaign level progression data from a legit source # TODO: Must be a better way to organize this series of 3 database calls (campaigns, levels, analytics)
# TODO: An uncached call can take over 30s locally # TODO: An uncached call can take over 30s locally
# TODO: Returns all the campaigns # TODO: Returns all the campaigns
# TODO: Calculate overall campaign stats # TODO: Calculate overall campaign stats
# TODO: Assumes db campaign levels are in progression order. Should build this based on actual progression.
campaignSlugs = req.query.slugs or req.body.slugs campaignSlug = req.query.slug or req.body.slug
startDay = req.query.startDay or req.body.startDay startDay = req.query.startDay or req.body.startDay
endDay = req.query.endDay or req.body.endDay endDay = req.query.endDay or req.body.endDay
return @sendSuccess res, [] unless campaignSlugs? return @sendSuccess res, [] unless campaignSlug?
# Cache results for 1 day # Cache results for 1 day
@campaignDropOffsCache ?= {} @campaignDropOffsCache ?= {}
@ -120,178 +123,151 @@ class AnalyticsLogEventHandler extends Handler
if (new Date()) - @campaignDropOffsCachedSince > 86400 * 1000 # Dumb cache expiration if (new Date()) - @campaignDropOffsCachedSince > 86400 * 1000 # Dumb cache expiration
@campaignDropOffsCache = {} @campaignDropOffsCache = {}
@campaignDropOffsCachedSince = new Date() @campaignDropOffsCachedSince = new Date()
cacheKey = campaignSlugs.join(',') cacheKey = campaignSlug
cacheKey += 's' + startDay if startDay? cacheKey += 's' + startDay if startDay?
cacheKey += 'e' + endDay if endDay? cacheKey += 'e' + endDay if endDay?
return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey] return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey]
queryParams = {$and: [{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}]} calculateDropOffs = (campaigns) =>
queryParams["$and"].push created: {$gte: new Date(startDay + "T00:00:00.000Z")} if startDay? # Calculate campaign drop off rates
queryParams["$and"].push created: {$lt: new Date(endDay + "T00:00:00.000Z")} if endDay? # Input:
# campaigns - per-campaign dictionary of ordered level slugs
AnalyticsLogEvent.find(queryParams).select('created event properties user').exec (err, data) => queryParams = {$and: [{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}]}
if err? then return @sendDatabaseError res, err 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?
# Bucketize events by user AnalyticsLogEvent.find(queryParams).select('created event properties user').exec (err, data) =>
userProgression = {} if err? then return @sendDatabaseError res, err
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 # Bucketize events by user
# Cache other individual campaigns too, since we have them userProgression = {}
@campaignDropOffsCache[cacheKey] = campaignRates for item in data
for campaign of campaignRates created = item.get('created')
cacheKey = campaign event = item.get('event')
cacheKey += 's' + startDay if startDay? if event is 'Saw Victory'
cacheKey += 'e' + endDay if endDay? 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 @campaignDropOffsCache[cacheKey] = campaignRates
@sendSuccess res, 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 getLevelData = (campaigns, campaignLevelIDs) =>
dungeonLevels = [ # Get level data and replace levelIDs with level slugs in campaigns
'dungeons-of-kithgard', # Input:
'gems-in-the-deep', # campaigns - per-campaign dictionary of ordered levelIDs
'shadow-guard', # campaignLevelIDs - dictionary of all campaign levelIDs
'kounter-kithwise', # Output:
'crawlways-of-kithgard', # campaigns - per-campaign dictionary of ordered level slugs
'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 = [ Level.find({original: {$in: campaignLevelIDs}, "version.isLatestMajor": true, "version.isLatestMinor": true}).exec (err, documents) =>
'defense-of-plainswood', if err? then return @sendDatabaseError res, err
'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 = [ levelSlugMap = {}
'the-dunes', for doc in documents
'the-mighty-sand-yak', levelID = doc.get('original')
'oasis', levelSlug = doc.get('name').toLowerCase().replace new RegExp(' ', 'g'), '-'
'sarven-road', levelSlugMap[levelID] = levelSlug
'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 = { # Replace levelIDs with level slugs
'dungeon': dungeonLevels, for campaign of campaigns
'forest': forestLevels, mapFn = (item) -> levelSlugMap[item]
'desert': desertLevels campaigns[campaign] = _.map campaigns[campaign], mapFn, @
} # Forest campaign levels are reversed for some reason
campaigns[campaign].reverse() if campaign is 'forest'
calculateDropOffs campaigns
getCampaignData = () =>
# Get campaign data
# Output:
# campaigns - per-campaign dictionary of ordered levelIDs
# campaignLevelIDs - dictionary of all campaign levelIDs
Campaign.find().exec (err, documents) =>
if err? then return @sendDatabaseError res, err
campaigns = {}
levelCampaignMap = {}
campaignLevelIDs = []
for doc in documents
campaignSlug = doc.get('slug')
levels = doc.get('levels')
campaigns[campaignSlug] = []
levelCampaignMap[campaignSlug] = {}
for levelID of levels
campaigns[campaignSlug].push levelID
campaignLevelIDs.push levelID
levelCampaignMap[levelID] = campaignSlug
getLevelData campaigns, campaignLevelIDs
getCampaignData()
module.exports = new AnalyticsLogEventHandler() module.exports = new AnalyticsLogEventHandler()