Update campaign analytics level ordering
Reading campaign levels from database. Assumes database order is roughly progression order.
This commit is contained in:
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
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Reference in a new issue