mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
Merge branch 'master' of https://github.com/codecombat/codecombat
This commit is contained in:
commit
a6043ca1be
9 changed files with 517 additions and 221 deletions
|
@ -123,19 +123,6 @@ module.exports = class User extends CocoModel
|
|||
application.tracker.identify gemPromptGroup: @gemPromptGroup unless me.isAdmin()
|
||||
@gemPromptGroup
|
||||
|
||||
getSubscribeCopyGroup: ->
|
||||
# A/B Testing alternate subscribe modal copy
|
||||
return @subscribeCopyGroup if @subscribeCopyGroup
|
||||
group = me.get('testGroupNumber') % 6
|
||||
@subscribeCopyGroup = switch group
|
||||
when 0, 1, 2 then 'original'
|
||||
when 3, 4, 5 then 'new'
|
||||
if (not @get('preferredLanguage') or /^en/.test(@get('preferredLanguage'))) and not me.isAdmin()
|
||||
application.tracker.identify subscribeCopyGroup: @subscribeCopyGroup
|
||||
else
|
||||
@subscribeCopyGroup = 'original'
|
||||
@subscribeCopyGroup
|
||||
|
||||
getVideoTutorialStylesIndex: (numVideos=0)->
|
||||
# A/B Testing video tutorial styles
|
||||
# Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently)
|
||||
|
|
|
@ -18,12 +18,6 @@
|
|||
top: -61px
|
||||
left: 0px
|
||||
|
||||
#subscribe-gems
|
||||
position: absolute
|
||||
top: 155px
|
||||
right: 65px
|
||||
|
||||
|
||||
//- Header
|
||||
h1
|
||||
position: absolute
|
||||
|
@ -37,7 +31,7 @@
|
|||
text-shadow: black 4px 4px 0, black -4px -4px 0, black 4px -4px 0, black -4px 4px 0, black 4px 0px 0, black 0px -4px 0, black -4px 0px 0, black 0px 4px 0, black 6px 6px 6px
|
||||
font-variant: normal
|
||||
text-transform: uppercase
|
||||
|
||||
|
||||
|
||||
//- Close modal button
|
||||
|
||||
|
@ -57,7 +51,7 @@
|
|||
&:hover
|
||||
color: yellow
|
||||
|
||||
|
||||
|
||||
//- Selling points
|
||||
|
||||
#selling-points
|
||||
|
@ -70,7 +64,7 @@
|
|||
color: black
|
||||
font-family: $headings-font-family
|
||||
font-size: 18px
|
||||
|
||||
|
||||
.point
|
||||
width: 150px
|
||||
overflow: none
|
||||
|
@ -85,22 +79,6 @@
|
|||
text-decoration: underline
|
||||
cursor: pointer
|
||||
|
||||
#selling-points-BTest
|
||||
position: absolute
|
||||
left: 65px
|
||||
top: 150px
|
||||
width: 500px
|
||||
font-weight: normal
|
||||
line-height: 18px
|
||||
color: black
|
||||
font-family: $headings-font-family
|
||||
font-size: 18px
|
||||
|
||||
.point
|
||||
overflow: none
|
||||
text-align: left
|
||||
margin: 20px
|
||||
|
||||
.popover
|
||||
z-index: 1050
|
||||
|
||||
|
@ -138,7 +116,7 @@
|
|||
padding: 2px 0 0 2px
|
||||
color: white
|
||||
|
||||
|
||||
|
||||
//- Errors
|
||||
|
||||
.alert
|
||||
|
@ -155,7 +133,7 @@ html.no-borderimage #subscribe-modal
|
|||
background-image: url(/images/level/code_toolbar_submit_button_active.png)
|
||||
background-size: 100% 100%
|
||||
padding: 7px 10px 10px 10px
|
||||
|
||||
|
||||
&:hover
|
||||
background-image: url(/images/level/code_toolbar_submit_button_zazz.png)
|
||||
border: 0
|
||||
|
@ -164,4 +142,3 @@ html.no-borderimage #subscribe-modal
|
|||
background-image: url(/images/level/code_toolbar_submit_button_zazz_pressed.png)
|
||||
padding: 9px 8px 8px 12px
|
||||
border: 0
|
||||
|
||||
|
|
|
@ -7,39 +7,24 @@
|
|||
#retrying-alert.alert.alert-danger(data-i18n="buy_gems.retrying")
|
||||
|
||||
else
|
||||
if BTest
|
||||
img(src="/images/pages/play/modal/subscribe-background-blank.png")#subscribe-background
|
||||
img(src="/images/pages/play/modal/subscribe-gems.png")#subscribe-gems
|
||||
else
|
||||
img(src="/images/pages/play/modal/subscribe-background.png")#subscribe-background
|
||||
|
||||
img(src="/images/pages/play/modal/subscribe-background.png")#subscribe-background
|
||||
|
||||
h1(data-i18n="subscribe.subscribe_title") Subscribe
|
||||
|
||||
div#close-modal
|
||||
span.glyphicon.glyphicon-remove
|
||||
|
||||
if BTest
|
||||
#selling-points-BTest
|
||||
#point-levels.point
|
||||
.blurb(style="font-style:italic") "Great product ... I have been looking for a good tool to teach my kids programming."
|
||||
#point-heroes.point
|
||||
.blurb Join the CodeCombat subscription and get even more learn-to-code goodness!
|
||||
#point-gems.point
|
||||
.blurb For $#{price}/mo, you'll get access to bonus levels and 3500 extra gems per month! Players who complete bonus levels learn more programming and advance further in the game.
|
||||
#point-items.point
|
||||
.blurb There's no risk: 100% money back guarantee.
|
||||
else
|
||||
#selling-points
|
||||
#point-levels.point
|
||||
.blurb(data-i18n="subscribe.levels")
|
||||
#point-heroes.point
|
||||
.blurb(data-i18n="subscribe.heroes")
|
||||
#point-gems.point
|
||||
.blurb(data-i18n="subscribe.gems")
|
||||
#point-items.point
|
||||
.blurb(data-i18n="subscribe.items")
|
||||
#selling-points
|
||||
#point-levels.point
|
||||
.blurb(data-i18n="subscribe.levels")
|
||||
#point-heroes.point
|
||||
.blurb(data-i18n="subscribe.heroes")
|
||||
#point-gems.point
|
||||
.blurb(data-i18n="subscribe.gems")
|
||||
#point-items.point
|
||||
.blurb(data-i18n="subscribe.items")
|
||||
|
||||
#parents-info(data-i18n="subscribe.parents")
|
||||
#parents-info(data-i18n="subscribe.parents")
|
||||
|
||||
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_button")
|
||||
|
||||
|
|
|
@ -30,10 +30,6 @@ module.exports = class SubscribeModal extends ModalView
|
|||
c.stateMessage = @stateMessage
|
||||
c.price = @product.amount / 100
|
||||
#c.price = 3.99 # Sale
|
||||
|
||||
# A/B Testing alternate subscription copy
|
||||
c.BTest = me.getSubscribeCopyGroup() is 'new'
|
||||
|
||||
return c
|
||||
|
||||
afterRender: ->
|
||||
|
|
|
@ -94,9 +94,17 @@ module.exports = class LevelGuideView extends CocoView
|
|||
window.tracker?.trackEvent 'Finish help video', level: @levelID, ls: @sessionID, style: @helpVideos[@helpVideosIndex].style
|
||||
@trackedHelpVideoFinish = true
|
||||
|
||||
# we wan't to always use the same scheme (HTTP/HTTPS) as the page was loaded with, but don't want to require Artisans to have to remember
|
||||
# not to include a scheme in help video url
|
||||
fixupUri = (uri) ->
|
||||
n = uri.indexOf('/')
|
||||
if n < 1
|
||||
return uri
|
||||
return uri.slice(n)
|
||||
|
||||
setupVideoPlayer: () ->
|
||||
return unless @helpVideos.length > 0
|
||||
helpVideoURL = @helpVideos[@helpVideosIndex].url
|
||||
helpVideoURL = fixupUri(@helpVideos[@helpVideosIndex].url)
|
||||
@setupVimeoVideoPlayer helpVideoURL
|
||||
|
||||
setupVimeoVideoPlayer: (helpVideoURL) ->
|
||||
|
|
|
@ -8,7 +8,7 @@ TRAVIS = process.env.COCO_TRAVIS_TEST
|
|||
|
||||
|
||||
#- regJoin replace a single '/' with '[\/\\]' so it can handle either forward or backslash
|
||||
regJoin = (s) -> new RegExp(s.replace(/\//, '[\\\/\\\\]'))
|
||||
regJoin = (s) -> new RegExp(s.replace(/\//g, '[\\\/\\\\]'))
|
||||
|
||||
|
||||
#- Build the config
|
||||
|
@ -197,12 +197,8 @@ exports.config =
|
|||
|
||||
modules:
|
||||
definition: (path, data) ->
|
||||
needHeaders = [
|
||||
'public/javascripts/app.js'
|
||||
'public/javascripts/world.js'
|
||||
'public/javascripts/whole-app.js'
|
||||
]
|
||||
defn = if path in needHeaders then commonjsHeader else ''
|
||||
needHeaderExpr = regJoin('^public/javascripts/?(app.js|world.js|whole-app.js)')
|
||||
defn = if path.match(needHeaderExpr) then commonjsHeader else ''
|
||||
return defn
|
||||
|
||||
#- Find all .coffee and .jade files in /app
|
||||
|
|
184
scripts/analytics/mixpanelGetEvent.py
Normal file
184
scripts/analytics/mixpanelGetEvent.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
# Get mixpanel event data via export API
|
||||
# Useful for debugging Mixpanel data weirdness
|
||||
|
||||
targetLevels = ['dungeons-of-kithgard', 'the-raised-sword', 'endangered-burl']
|
||||
targetLevels = ['dungeons-of-kithgard']
|
||||
eventFunnel = ['Started Level', 'Saw Victory']
|
||||
# eventFunnel = ['Saw Victory']
|
||||
# eventFunnel = ['Started Level']
|
||||
|
||||
import sys
|
||||
from pprint import pprint
|
||||
from datetime import datetime, timedelta
|
||||
from mixpanel import Mixpanel
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
# NOTE: mixpanel dates are by day and inclusive
|
||||
# E.g. '2014-12-08' is any date that day, up to 2014-12-09 12am
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not len(sys.argv) is 3:
|
||||
print "Script format: <script> <api_key> <api_secret>"
|
||||
else:
|
||||
scriptStart = datetime.now()
|
||||
|
||||
api_key = sys.argv[1]
|
||||
api_secret = sys.argv[2]
|
||||
api = Mixpanel(
|
||||
api_key = api_key,
|
||||
api_secret = api_secret
|
||||
)
|
||||
|
||||
startDate = '2015-01-01'
|
||||
endDate = '2015-01-26'
|
||||
|
||||
startEvent = eventFunnel[0]
|
||||
endEvent = eventFunnel[-1]
|
||||
|
||||
print("Requesting data for {0} to {1}".format(startDate, endDate))
|
||||
data = api.request(['export'], {
|
||||
# 'where': '"539c630f30a67c3b05d98d95" == properties["id"]',
|
||||
# 'where': "('539c630f30a67c3b05d98d95' == properties['id'] or '539c630f30a67c3b05d98d95' == properties['distinct_id'])",
|
||||
'event': eventFunnel,
|
||||
'from_date': startDate,
|
||||
'to_date': endDate
|
||||
})
|
||||
|
||||
|
||||
weirdUserIDs = []
|
||||
eventUsers = {}
|
||||
levelEventUserDayMap = {}
|
||||
levelUserEventDayMap = {}
|
||||
lines = data.split('\n')
|
||||
print "Received %d entries" % len(lines)
|
||||
for line in lines:
|
||||
try:
|
||||
if len(line) is 0: continue
|
||||
eventData = json.loads(line)
|
||||
# pprint(eventData)
|
||||
# break
|
||||
eventName = eventData['event']
|
||||
if not eventName in eventFunnel:
|
||||
print 'Unexpected event ' + eventName
|
||||
break
|
||||
if not 'properties' in eventData:
|
||||
print('no properties, skpping')
|
||||
continue
|
||||
properties = eventData['properties']
|
||||
if not 'distinct_id' in properties:
|
||||
print('no distinct_id, skpping')
|
||||
continue
|
||||
user = properties['distinct_id']
|
||||
if not 'time' in properties:
|
||||
print('no time, skpping')
|
||||
continue
|
||||
time = properties['time']
|
||||
pst = datetime.fromtimestamp(int(properties['time']))
|
||||
utc = pst + timedelta(0, 8 * 60 * 60)
|
||||
dateCreated = utc.isoformat()
|
||||
day = dateCreated[0:10]
|
||||
if day < startDate or day > endDate:
|
||||
print "Skipping {0}".format(day)
|
||||
continue
|
||||
|
||||
if 'levelID' in properties:
|
||||
level = properties['levelID']
|
||||
elif 'level' in properties:
|
||||
level = properties['level'].lower().replace(' ', '-')
|
||||
else:
|
||||
print("Unkonwn level for", eventName)
|
||||
print(properties)
|
||||
break
|
||||
|
||||
if not level in targetLevels: continue
|
||||
|
||||
# if user != "539c630f30a67c3b05d98d95": continue
|
||||
pprint(eventData)
|
||||
|
||||
# if user == "54c1fc3a08652d5305442c6b":
|
||||
# pprint(eventData)
|
||||
# break
|
||||
# if '-' in user:
|
||||
# weirdUserIDs.append(user)
|
||||
# # pprint(eventData)
|
||||
# # break
|
||||
# continue
|
||||
|
||||
# print level
|
||||
|
||||
if not level in levelEventUserDayMap: levelEventUserDayMap[level] = {}
|
||||
if not eventName in levelEventUserDayMap[level]: levelEventUserDayMap[level][eventName] = {}
|
||||
if not user in levelEventUserDayMap[level][eventName] or levelEventUserDayMap[level][eventName][user] > day:
|
||||
levelEventUserDayMap[level][eventName][user] = day
|
||||
|
||||
if not user in eventUsers: eventUsers[user] = True
|
||||
|
||||
if not level in levelUserEventDayMap: levelUserEventDayMap[level] = {}
|
||||
if not user in levelUserEventDayMap[level]: levelUserEventDayMap[level][user] = {}
|
||||
if not eventName in levelUserEventDayMap[level][user] or levelUserEventDayMap[level][user][eventName] > day:
|
||||
levelUserEventDayMap[level][user][eventName] = day
|
||||
|
||||
except:
|
||||
print "Unexpected error:", sys.exc_info()[0]
|
||||
print line
|
||||
break
|
||||
|
||||
# pprint(levelEventUserDayMap)
|
||||
|
||||
print("Weird user IDs: {0}".format(len(weirdUserIDs)))
|
||||
|
||||
for level in levelEventUserDayMap:
|
||||
for event in levelEventUserDayMap[level]:
|
||||
print("{0} {1} {2}".format(level, event, len(levelEventUserDayMap[level][event])))
|
||||
print("Users: {0}".format(len(eventUsers)))
|
||||
|
||||
noStartDayUsers = []
|
||||
levelFunnelData = {}
|
||||
for level in levelUserEventDayMap:
|
||||
for user in levelUserEventDayMap[level]:
|
||||
# 6455
|
||||
# for event in levelUserEventDayMap[level][user]:
|
||||
# day = levelUserEventDayMap[level][user][event]
|
||||
# if not level in levelFunnelData: levelFunnelData[level] = {}
|
||||
# if not day in levelFunnelData[level]: levelFunnelData[level][day] = {}
|
||||
# if not event in levelFunnelData[level][day]: levelFunnelData[level][day][event] = 0
|
||||
# levelFunnelData[level][day][event] += 1
|
||||
|
||||
# 5382
|
||||
funnelStartDay = None
|
||||
for event in levelUserEventDayMap[level][user]:
|
||||
day = levelUserEventDayMap[level][user][event]
|
||||
if not level in levelFunnelData: levelFunnelData[level] = {}
|
||||
if not day in levelFunnelData[level]: levelFunnelData[level][day] = {}
|
||||
if not event in levelFunnelData[level][day]: levelFunnelData[level][day][event] = 0
|
||||
if eventFunnel[0] == event:
|
||||
levelFunnelData[level][day][event] += 1
|
||||
funnelStartDay = day
|
||||
break
|
||||
|
||||
if funnelStartDay:
|
||||
for event in levelUserEventDayMap[level][user]:
|
||||
if not event in levelFunnelData[level][funnelStartDay]:
|
||||
levelFunnelData[level][funnelStartDay][event] = 0
|
||||
if eventFunnel[0] != event:
|
||||
levelFunnelData[level][funnelStartDay][event] += 1
|
||||
for i in range(1, len(eventFunnel)):
|
||||
event = eventFunnel[i]
|
||||
if not event in levelFunnelData[level][funnelStartDay]:
|
||||
levelFunnelData[level][funnelStartDay][event] = 0
|
||||
else:
|
||||
noStartDayUsers.append(user)
|
||||
|
||||
pprint(levelFunnelData)
|
||||
print("No start day count: {0}".format(len(noStartDayUsers)))
|
||||
noStartDayUsers.sort()
|
||||
for i in range(len(noStartDayUsers)):
|
||||
if i > 50: break
|
||||
print(noStartDayUsers[i])
|
||||
|
||||
|
||||
print("Script runtime: {0}".format(datetime.now() - scriptStart))
|
|
@ -1,10 +1,13 @@
|
|||
# Calculate level completion rates via mixpanel export API
|
||||
|
||||
# TODO: unique users
|
||||
# TODO: align output
|
||||
# TODO: order output
|
||||
# TODO: why are our 'time' fields in PST time?
|
||||
|
||||
targetLevels = ['dungeons-of-kithgard', 'the-raised-sword', 'endangered-burl']
|
||||
eventFunnel = ['Started Level', 'Saw Victory']
|
||||
|
||||
import sys
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from mixpanel import Mixpanel
|
||||
|
||||
try:
|
||||
|
@ -19,23 +22,34 @@ if __name__ == '__main__':
|
|||
if not len(sys.argv) is 3:
|
||||
print "Script format: <script> <api_key> <api_secret>"
|
||||
else:
|
||||
scriptStart = datetime.now()
|
||||
|
||||
api_key = sys.argv[1]
|
||||
api_secret = sys.argv[2]
|
||||
api = Mixpanel(
|
||||
api_key = api_key,
|
||||
api_secret = api_secret
|
||||
)
|
||||
|
||||
startDate = '2014-12-31'
|
||||
endDate = '2015-01-05'
|
||||
|
||||
# startDate = '2015-01-11'
|
||||
# endDate = '2015-01-17'
|
||||
startDate = '2015-01-23'
|
||||
endDate = '2015-01-23'
|
||||
# endDate = '2015-01-28'
|
||||
|
||||
startEvent = eventFunnel[0]
|
||||
endEvent = eventFunnel[-1]
|
||||
|
||||
print("Requesting data for {0} to {1}".format(startDate, endDate))
|
||||
data = api.request(['export'], {
|
||||
'event' : ['Started Level', 'Saw Victory'],
|
||||
'event' : eventFunnel,
|
||||
'from_date' : startDate,
|
||||
'to_date' : endDate
|
||||
})
|
||||
|
||||
levelRates = {}
|
||||
|
||||
|
||||
# Map ordering: level, user, event, day
|
||||
userDataMap = {}
|
||||
lines = data.split('\n')
|
||||
print "Received %d entries" % len(lines)
|
||||
for line in lines:
|
||||
|
@ -43,40 +57,102 @@ if __name__ == '__main__':
|
|||
if len(line) is 0: continue
|
||||
eventData = json.loads(line)
|
||||
eventName = eventData['event']
|
||||
if not eventName in ['Started Level', 'Saw Victory']:
|
||||
if not eventName in eventFunnel:
|
||||
print 'Unexpected event ' + eventName
|
||||
break
|
||||
if not 'properties' in eventData: continue
|
||||
properties = eventData['properties']
|
||||
if not 'distinct_id' in properties: continue
|
||||
user = properties['distinct_id']
|
||||
if not 'time' in properties: continue
|
||||
time = properties['time']
|
||||
pst = datetime.fromtimestamp(int(properties['time']))
|
||||
utc = pst + timedelta(0, 8 * 60 * 60)
|
||||
dateCreated = utc.isoformat()
|
||||
day = dateCreated[0:10]
|
||||
if day < startDate or day > endDate:
|
||||
print "Skipping {0}".format(day)
|
||||
continue
|
||||
|
||||
if 'levelID' in properties:
|
||||
levelID = properties['levelID']
|
||||
level = properties['levelID']
|
||||
elif 'level' in properties:
|
||||
levelID = properties['level'].lower().replace(' ', '-')
|
||||
level = properties['level'].lower().replace(' ', '-')
|
||||
else:
|
||||
print("Unkonwn levelID for", eventName)
|
||||
print("Unkonwn level for", eventName)
|
||||
print(properties)
|
||||
break
|
||||
if not levelID in levelRates:
|
||||
levelRates[levelID] = {'started': 0, 'finished': 0}
|
||||
if eventName == 'Started Level':
|
||||
levelRates[levelID]['started'] += 1
|
||||
elif eventName == 'Saw Victory':
|
||||
levelRates[levelID]['finished'] += 1
|
||||
else:
|
||||
print("Unknown event name", eventName)
|
||||
print(eventData)
|
||||
break
|
||||
|
||||
if not level in targetLevels:
|
||||
continue
|
||||
|
||||
# print level
|
||||
|
||||
if not level in userDataMap: userDataMap[level] = {}
|
||||
if not user in userDataMap[level]: userDataMap[level][user] = {}
|
||||
if not eventName in userDataMap[level][user] or userDataMap[level][user][eventName] > day:
|
||||
userDataMap[level][user][eventName] = day
|
||||
except:
|
||||
print "Unexpected error:", sys.exc_info()[0]
|
||||
print line
|
||||
break
|
||||
|
||||
# print(levelRates)
|
||||
for levelID in levelRates:
|
||||
started = levelRates[levelID]['started']
|
||||
finished = levelRates[levelID]['finished']
|
||||
# if not levelID == 'endangered-burl':
|
||||
# continue
|
||||
# print(userDataMap)
|
||||
|
||||
levelFunnelData = {}
|
||||
for level in userDataMap:
|
||||
for user in userDataMap[level]:
|
||||
funnelStartDay = None
|
||||
for event in userDataMap[level][user]:
|
||||
day = userDataMap[level][user][event]
|
||||
if not level in levelFunnelData: levelFunnelData[level] = {}
|
||||
if not day in levelFunnelData[level]: levelFunnelData[level][day] = {}
|
||||
if not event in levelFunnelData[level][day]: levelFunnelData[level][day][event] = 0
|
||||
if eventFunnel[0] == event:
|
||||
levelFunnelData[level][day][event] += 1
|
||||
funnelStartDay = day
|
||||
break
|
||||
|
||||
if funnelStartDay:
|
||||
for event in userDataMap[level][user]:
|
||||
if not event in levelFunnelData[level][funnelStartDay]:
|
||||
levelFunnelData[level][funnelStartDay][event] = 0
|
||||
if not eventFunnel[0] == event:
|
||||
levelFunnelData[level][funnelStartDay][event] += 1
|
||||
for i in range(1, len(eventFunnel)):
|
||||
event = eventFunnel[i]
|
||||
if not event in levelFunnelData[level][funnelStartDay]:
|
||||
levelFunnelData[level][funnelStartDay][event] = 0
|
||||
|
||||
# print(levelFunnelData)
|
||||
|
||||
totals = {}
|
||||
for level in levelFunnelData:
|
||||
for day in levelFunnelData[level]:
|
||||
if startEvent in levelFunnelData[level][day]:
|
||||
started = levelFunnelData[level][day][startEvent]
|
||||
else:
|
||||
started = 0
|
||||
if endEvent in levelFunnelData[level][day]:
|
||||
finished = levelFunnelData[level][day][endEvent]
|
||||
else:
|
||||
finished = 0
|
||||
if not level in totals: totals[level] = {}
|
||||
if not startEvent in totals[level]: totals[level][startEvent] = 0
|
||||
if not endEvent in totals[level]: totals[level][endEvent] = 0
|
||||
totals[level][startEvent] += started
|
||||
totals[level][endEvent] += finished
|
||||
if started > 0:
|
||||
print("{0}\t{1}\t{2}\t{3}\t{4}%".format(level, day, started, finished, float(finished) / started * 100))
|
||||
else:
|
||||
print("{0}\t{1}\t{2}\t{3}\t".format(level, day, started, finished))
|
||||
|
||||
for level in totals:
|
||||
started = totals[level][startEvent]
|
||||
finished = totals[level][endEvent]
|
||||
if started > 0:
|
||||
print("{0}\t{1}\t{2}\t{3}%".format(levelID, started, finished, float(finished) / started * 100))
|
||||
print("{0}\t{1}\t{2}\t{3}%".format(level, started, finished, float(finished) / started * 100))
|
||||
else:
|
||||
print("{0}\t{1}\t{2}".format(levelID, started, finished))
|
||||
print("{0}\t{1}\t{2}\t".format(level, started, finished))
|
||||
|
||||
print("Script runtime: {0}".format(datetime.now() - scriptStart))
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
|
||||
// TODO: Why do a small number of 'Started level' not have properties.levelID set?
|
||||
|
||||
// TODO: spot check the data: NaN, only some 0.0 dates, etc.
|
||||
// TODO: exclude levels with no interesting data?
|
||||
// TODO: Fix addPlaytimeAverages() and addUserCodeProblemCounts()
|
||||
// TODO: getLevelFunnelData() outputs different data structure now.
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
|
@ -20,12 +20,22 @@ var today = new Date();
|
|||
today = today.toISOString().substr(0, 10);
|
||||
print("Today is " + today);
|
||||
|
||||
var todayMinus6 = new Date();
|
||||
todayMinus6.setUTCDate(todayMinus6.getUTCDate() - 6);
|
||||
var startDate = todayMinus6.toISOString().substr(0, 10) + "T00:00:00.000Z";
|
||||
print("Start date is " + startDate)
|
||||
// startDate = "2015-01-02T00:00:00.000Z";
|
||||
// var endDate = "2015-01-09T00:00:00.000Z";
|
||||
// var todayMinus6 = new Date();
|
||||
// todayMinus6.setUTCDate(todayMinus6.getUTCDate() - 6);
|
||||
// var startDate = todayMinus6.toISOString().substr(0, 10) + "T00:00:00.000Z";
|
||||
// startDate = "2015-01-23T00:00:00.000Z";
|
||||
// print("Start date is " + startDate)
|
||||
// var endDate = "2015-01-24T00:00:00.000Z";
|
||||
// print("End date is " + endDate)
|
||||
|
||||
var levelCompletionFunnel = ['Started Level', 'Saw Victory'];
|
||||
var dataStartDay = "2015-01-15";
|
||||
var startDay = "2015-01-23";
|
||||
var endDay = "2015-01-24";
|
||||
print(startDay + " to " + endDay);
|
||||
print("Data start day " + dataStartDay);
|
||||
|
||||
var targetLevels = ['dungeons-of-kithgard'];
|
||||
|
||||
function objectIdWithTimestamp(timestamp)
|
||||
{
|
||||
|
@ -38,72 +48,139 @@ function objectIdWithTimestamp(timestamp)
|
|||
return constructedObjectId
|
||||
}
|
||||
|
||||
function getCompletionRates() {
|
||||
print("Getting completion rates...");
|
||||
var queryParams = {
|
||||
$and: [
|
||||
{_id: {$gte: objectIdWithTimestamp(ISODate(startDate))}},
|
||||
{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}
|
||||
]
|
||||
};
|
||||
function getLevelFunnelData(startDay, endDay, eventFunnel) {
|
||||
// Copied from insertPerDayAnalytics.js
|
||||
if (!startDay || !eventFunnel || eventFunnel.length === 0) return {};
|
||||
|
||||
// var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
|
||||
var startObj = objectIdWithTimestamp(ISODate(dataStartDay + "T00:00:00.000Z"));
|
||||
var endObj = objectIdWithTimestamp(ISODate(endDay + "T00:00:00.000Z"));
|
||||
var queryParams = {$and: [{_id: {$gte: startObj}},{_id: {$lt: endObj}},{"event": {$in: eventFunnel}}]};
|
||||
// var queryParams = {$and: [{user: ObjectId("539c630f30a67c3b05d98d95")},{_id: {$gte: startObj}},{_id: {$lt: endObj}},{"event": {$in: eventFunnel}}]};
|
||||
var cursor = db['analytics.log.events'].find(queryParams);
|
||||
|
||||
// <level><date><data>
|
||||
var levelData = {};
|
||||
// Map ordering: level, user, event, day
|
||||
var recordCount = 0;
|
||||
var duplicates = {};
|
||||
var levelEventUserDayMap = {};
|
||||
var levelUserEventDayMap = {};
|
||||
while (cursor.hasNext()) {
|
||||
recordCount++;
|
||||
var doc = cursor.next();
|
||||
var created = doc.created.toISOString().substring(0, 10);
|
||||
var created = doc._id.getTimestamp().toISOString();
|
||||
var day = created.substring(0, 10);
|
||||
var event = doc.event;
|
||||
var properties = doc.properties;
|
||||
var user = doc.user;
|
||||
var level;
|
||||
|
||||
// TODO: Switch to properties.levelID for 'Saw Victory'
|
||||
if (event === 'Saw Victory' && properties.level) level = properties.level.toLowerCase().replace(/ /g, '-');
|
||||
else if (properties.levelID) level = properties.levelID
|
||||
else continue
|
||||
var user = doc.user;
|
||||
|
||||
// if (targetLevels.indexOf(level) < 0) continue;
|
||||
|
||||
// print(day + " " + created);
|
||||
// print(JSON.stringify(doc, null, 2));
|
||||
|
||||
if (level.length > longestLevelName) longestLevelName = level.length;
|
||||
|
||||
if (!levelData[level]) levelData[level] = {};
|
||||
if (!levelData[level][created]) levelData[level][created] = {};
|
||||
if (!levelData[level][created]['started']) levelData[level][created]['started'] = {};
|
||||
if (!levelData[level][created]['finished']) levelData[level][created]['finished'] = {}
|
||||
if (event === 'Started Level') levelData[level][created]['started'][user] = true;
|
||||
else levelData[level][created]['finished'][user] = true;
|
||||
if (!levelUserEventDayMap[level]) levelUserEventDayMap[level] = {};
|
||||
if (!levelUserEventDayMap[level][user]) levelUserEventDayMap[level][user] = {};
|
||||
if (levelUserEventDayMap[level][user][event]) {
|
||||
if (!duplicates[event]) duplicates[event] = 0;
|
||||
duplicates[event]++;
|
||||
}
|
||||
if (!levelUserEventDayMap[level][user][event] || levelUserEventDayMap[level][user][event].localeCompare(day) > 0) {
|
||||
// if (!levelUserEventDayMap[level][user][event] || day.localeCompare(levelUserEventDayMap[level][user][event]) > 0) {
|
||||
// day is earlier than levelUserEventDayMap[level][user][event]
|
||||
levelUserEventDayMap[level][user][event] = day;
|
||||
}
|
||||
|
||||
if (!levelEventUserDayMap[level]) levelEventUserDayMap[level] = {};
|
||||
if (!levelEventUserDayMap[level][event]) levelEventUserDayMap[level][event] = {};
|
||||
if (!levelEventUserDayMap[level][event][user] || levelEventUserDayMap[level][event][user].localeCompare(day) > 0) {
|
||||
levelEventUserDayMap[level][event][user] = day;
|
||||
}
|
||||
}
|
||||
|
||||
// print("Records: " + recordCount);
|
||||
// print("Duplicates");
|
||||
// print(JSON.stringify(duplicates, null, 2));
|
||||
longestLevelName += 2;
|
||||
|
||||
var levelRates = [];
|
||||
for (level in levelData) {
|
||||
var dateData = [];
|
||||
var dateIndex = 0;
|
||||
for (created in levelData[level]) {
|
||||
var started =
|
||||
dateData.push({
|
||||
level: level,
|
||||
created: created,
|
||||
started: Object.keys(levelData[level][created]['started']).length,
|
||||
finished: Object.keys(levelData[level][created]['finished']).length
|
||||
});
|
||||
if (dates.length === dateIndex) dates.push(created.substring(5));
|
||||
dateIndex++;
|
||||
// Data: level, day, event
|
||||
var noStartDayUsers = [];
|
||||
var levelFunnelData = {};
|
||||
for (level in levelUserEventDayMap) {
|
||||
for (user in levelUserEventDayMap[level]) {
|
||||
|
||||
// Find first event date
|
||||
var funnelStartDay = null;
|
||||
for (event in levelUserEventDayMap[level][user]) {
|
||||
var day = levelUserEventDayMap[level][user][event];
|
||||
if (day.localeCompare(startDay) < 0) {
|
||||
// day earlier than startDay
|
||||
continue;
|
||||
}
|
||||
if (!levelFunnelData[level]) levelFunnelData[level] = {};
|
||||
if (!levelFunnelData[level][day]) levelFunnelData[level][day] = {};
|
||||
if (!levelFunnelData[level][day][event]) levelFunnelData[level][day][event] = 0;
|
||||
if (eventFunnel[0] === event) {
|
||||
// First event gets attributed to current date
|
||||
levelFunnelData[level][day][event]++;
|
||||
funnelStartDay = day;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (funnelStartDay) {
|
||||
// Add remaining funnel steps/events to first step's date
|
||||
for (event in levelUserEventDayMap[level][user]) {
|
||||
if (!levelFunnelData[level][funnelStartDay][event]) levelFunnelData[level][funnelStartDay][event] = 0;
|
||||
if (eventFunnel[0] != event) levelFunnelData[level][funnelStartDay][event]++;
|
||||
}
|
||||
// Zero remaining funnel events
|
||||
for (var i = 1; i < eventFunnel.length; i++) {
|
||||
var event = eventFunnel[i];
|
||||
if (!levelFunnelData[level][funnelStartDay][event]) levelFunnelData[level][funnelStartDay][event] = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TODO: calc no start days
|
||||
for (event in levelUserEventDayMap[level][user]) {
|
||||
var day = levelUserEventDayMap[level][user][event];
|
||||
if (day.localeCompare(startDay) < 0) {
|
||||
// day earlier than startDay
|
||||
continue;
|
||||
}
|
||||
if (eventFunnel[0] != event) {
|
||||
noStartDayUsers.push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
levelRates.push(dateData);
|
||||
}
|
||||
// printjson(levelRates);
|
||||
|
||||
levelRates.sort(function(a,b) {return a[0].level < b[0].level ? -1 : 1});
|
||||
for (levelKey in levelRates) levelRates[levelKey].sort(function(a,b) {return a.created < b.created ? 1 : -1});
|
||||
// print("No start day count: " + noStartDayUsers.length);
|
||||
// for (var i = 0; i < noStartDayUsers.length && i < 50; i++) {
|
||||
// print(noStartDayUsers[i]);
|
||||
// }
|
||||
|
||||
return levelRates;
|
||||
return levelFunnelData;
|
||||
}
|
||||
|
||||
function addPlaytimeAverages(levelRates) {
|
||||
function addPlaytimeAverages(startDay, endDay, levelRates) {
|
||||
print("Getting playtimes...");
|
||||
// printjson(levelRates);
|
||||
var startObj = objectIdWithTimestamp(ISODate(dataStartDay + "T00:00:00.000Z"));
|
||||
var endObj = objectIdWithTimestamp(ISODate(endDay + "T00:00:00.000Z"));
|
||||
// var match = {"$match" : {$and: [{_id: { $gte: startObj}}, {_id: { $lt: endObj}}]}};
|
||||
var match = {
|
||||
"$match" : {
|
||||
$and: [
|
||||
{"created": { $gte: ISODate(startDate)}},
|
||||
{_id: { $gte: startObj}},
|
||||
{_id: { $lt: endObj}},
|
||||
{"state.complete": true},
|
||||
{"playtime": {$gt: 0}}
|
||||
]
|
||||
|
@ -113,12 +190,12 @@ function addPlaytimeAverages(levelRates) {
|
|||
"_id" : 0,
|
||||
"levelID" : 1,
|
||||
"playtime": 1,
|
||||
"created": {"$substr" : ["$created", 0, 10]}
|
||||
"day": {"$substr" : ["$created", 0, 10]}
|
||||
}};
|
||||
|
||||
var group = {"$group" : {
|
||||
"_id" : {
|
||||
"created" : "$created",
|
||||
"day" : "$day",
|
||||
"level": "$levelID"
|
||||
},
|
||||
"average" : {
|
||||
|
@ -131,36 +208,38 @@ function addPlaytimeAverages(levelRates) {
|
|||
var levelPlaytimeData = {};
|
||||
while (cursor.hasNext()) {
|
||||
var doc = cursor.next();
|
||||
var created = doc._id.created;
|
||||
var day = doc._id.day;
|
||||
var level = doc._id.level;
|
||||
if (!levelPlaytimeData[level]) levelPlaytimeData[level] = {};
|
||||
levelPlaytimeData[level][created] = doc.average;
|
||||
levelPlaytimeData[level][day] = doc.average;
|
||||
}
|
||||
|
||||
for (levelIndex in levelRates) {
|
||||
for (dateIndex in levelRates[levelIndex]) {
|
||||
var level = levelRates[levelIndex][dateIndex].level;
|
||||
var created = levelRates[levelIndex][dateIndex].created;
|
||||
if (levelPlaytimeData[level] && levelPlaytimeData[level][created]) {
|
||||
levelRates[levelIndex][dateIndex].averagePlaytime = levelPlaytimeData[level][created];
|
||||
var day = levelRates[levelIndex][dateIndex].day;
|
||||
if (levelPlaytimeData[level] && levelPlaytimeData[level][day]) {
|
||||
levelRates[levelIndex][dateIndex].averagePlaytime = levelPlaytimeData[level][day];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addUserCodeProblemCounts(levelRates) {
|
||||
function addUserCodeProblemCounts(startDay, endDay, levelRates) {
|
||||
print("Getting user code problem counts...");
|
||||
var match = {"$match" : {"created": { $gte: ISODate(startDate)}}};
|
||||
var startObj = objectIdWithTimestamp(ISODate(dataStartDay + "T00:00:00.000Z"));
|
||||
var endObj = objectIdWithTimestamp(ISODate(endDay + "T00:00:00.000Z"));
|
||||
var match = {"$match" : {$and: [{_id: { $gte: startObj}}, {_id: { $lt: endObj}}]}};
|
||||
|
||||
var proj0 = {"$project": {
|
||||
"_id" : 0,
|
||||
"levelID" : 1,
|
||||
"created": {"$substr" : ["$created", 0, 10]}
|
||||
"day": {"$substr" : ["$created", 0, 10]}
|
||||
}};
|
||||
|
||||
var group = {"$group" : {
|
||||
"_id" : {
|
||||
"created" : "$created",
|
||||
"day" : "$day",
|
||||
"level": "$levelID"
|
||||
},
|
||||
"count" : {
|
||||
|
@ -173,18 +252,18 @@ function addUserCodeProblemCounts(levelRates) {
|
|||
var levelPlaytimeData = {};
|
||||
while (cursor.hasNext()) {
|
||||
var doc = cursor.next();
|
||||
var created = doc._id.created;
|
||||
var day = doc._id.day;
|
||||
var level = doc._id.level;
|
||||
if (!levelPlaytimeData[level]) levelPlaytimeData[level] = {};
|
||||
levelPlaytimeData[level][created] = doc.count;
|
||||
levelPlaytimeData[level][day] = doc.count;
|
||||
}
|
||||
|
||||
for (levelIndex in levelRates) {
|
||||
for (dateIndex in levelRates[levelIndex]) {
|
||||
var level = levelRates[levelIndex][dateIndex].level;
|
||||
var created = levelRates[levelIndex][dateIndex].created;
|
||||
if (levelPlaytimeData[level] && levelPlaytimeData[level][created]) {
|
||||
levelRates[levelIndex][dateIndex].codeProblems = levelPlaytimeData[level][created];
|
||||
var day = levelRates[levelIndex][dateIndex].day;
|
||||
if (levelPlaytimeData[level] && levelPlaytimeData[level][day]) {
|
||||
levelRates[levelIndex][dateIndex].codeProblems = levelPlaytimeData[level][day];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,60 +272,68 @@ function addUserCodeProblemCounts(levelRates) {
|
|||
var longestLevelName = -1;
|
||||
var dates = [];
|
||||
|
||||
var levelRates = getCompletionRates();
|
||||
// addPlaytimeAverages(levelRates);
|
||||
// addUserCodeProblemCounts(levelRates);
|
||||
var levelRates = getLevelFunnelData(startDay, endDay, levelCompletionFunnel);
|
||||
// addPlaytimeAverages(startDay, endDay, levelRates);
|
||||
// addUserCodeProblemCounts(startDay, endDay, levelRates);
|
||||
|
||||
// print(JSON.stringify(levelRates, null, 2))
|
||||
|
||||
// Print out all data
|
||||
print("Columns: level, day, started, finished, completion rate, average finish playtime, average code problem count");
|
||||
// print("Columns: level, day, started, finished, completion rate, average finish playtime, average code problem count");
|
||||
print("Columns: level, day, started, finished, completion rate");
|
||||
for (levelKey in levelRates) {
|
||||
for (dateKey in levelRates[levelKey]) {
|
||||
var created = levelRates[levelKey][dateKey].created;
|
||||
var level = levelRates[levelKey][dateKey].level;
|
||||
var started = levelRates[levelKey][dateKey].started;
|
||||
var finished = levelRates[levelKey][dateKey].finished;
|
||||
// var day = levelRates[levelKey][dateKey].day;
|
||||
// var level = levelRates[levelKey][dateKey].level;
|
||||
// var started = levelRates[levelKey][dateKey].started;
|
||||
// var finished = levelRates[levelKey][dateKey].finished;
|
||||
var started = levelRates[levelKey][dateKey][levelCompletionFunnel[0]] || 0;
|
||||
var finished = levelRates[levelKey][dateKey][levelCompletionFunnel[levelCompletionFunnel.length - 1]] || 0;
|
||||
var completionRate = started > 0 ? finished / started : 0;
|
||||
var averagePlaytime = levelRates[levelKey][dateKey].averagePlaytime;
|
||||
averagePlaytime = averagePlaytime ? Math.round(averagePlaytime) : 0;
|
||||
var averageCodeProblems = levelRates[levelKey][dateKey].codeProblems;
|
||||
averageCodeProblems = averageCodeProblems ? (averageCodeProblems / started).toFixed(2) : 0.0;
|
||||
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
||||
print(level + levelSpacer + created + "\t" + started + "\t" + finished + "\t" + (completionRate * 100).toFixed(2) + "% " + averagePlaytime + "s " + averageCodeProblems);
|
||||
// var averagePlaytime = levelRates[levelKey][dateKey].averagePlaytime;
|
||||
// averagePlaytime = averagePlaytime ? Math.round(averagePlaytime) : 0;
|
||||
// var averageCodeProblems = levelRates[levelKey][dateKey].codeProblems;
|
||||
// averageCodeProblems = averageCodeProblems ? (averageCodeProblems / started).toFixed(2) : 0.0;
|
||||
if ((longestLevelName - levelKey.length) < 0)
|
||||
throw new Error(longestLevelName + " " + levelKey.length);
|
||||
var levelSpacer = new Array(longestLevelName - levelKey.length).join(' ');
|
||||
// print(levelKey + levelSpacer + dateKey + "\t" + started + "\t" + finished + "\t" + (completionRate * 100).toFixed(2) + "% " + averagePlaytime + "s " + averageCodeProblems);
|
||||
print(levelKey + levelSpacer + dateKey + "\t" + started + "\t" + finished + "\t" + (completionRate * 100).toFixed(2) + "%");
|
||||
}
|
||||
}
|
||||
|
||||
// Print out a nice grid of levels with 7 days of data
|
||||
print("Columns: level, completion rate/average playtime/average code problems, completion rate/average playtime/average code problems ...");
|
||||
print(new Array(longestLevelName).join(' ') + dates.join('\t\t'));
|
||||
for (levelKey in levelRates) {
|
||||
var hasStarted = false;
|
||||
for (dateKey in levelRates[levelKey]) {
|
||||
if (levelRates[levelKey][dateKey].started > 0) {
|
||||
hasStarted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasStarted) continue;
|
||||
|
||||
if (levelRates[levelKey].length < 6) continue;
|
||||
|
||||
var level = levelRates[levelKey][0].level;
|
||||
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
||||
var msg = level + levelSpacer;
|
||||
|
||||
for (dateKey in levelRates[levelKey]) {
|
||||
var created = levelRates[levelKey][dateKey].created;
|
||||
var started = levelRates[levelKey][dateKey].started;
|
||||
var finished = levelRates[levelKey][dateKey].finished;
|
||||
var averagePlaytime = levelRates[levelKey][dateKey].averagePlaytime;
|
||||
averagePlaytime = averagePlaytime ? Math.round(averagePlaytime) : 0;
|
||||
var averageCodeProblems = levelRates[levelKey][dateKey].codeProblems;
|
||||
averageCodeProblems = averageCodeProblems ? averageCodeProblems / started : 0.0;
|
||||
var completionRate = started > 0 ? finished / started : 0;
|
||||
msg += (completionRate * 100).toFixed(2) + "/" + averagePlaytime + "/" + averageCodeProblems.toFixed(2) + "\t";
|
||||
}
|
||||
print(msg);
|
||||
}
|
||||
// print("Columns: level, completion rate/average playtime/average code problems, completion rate/average playtime/average code problems ...");
|
||||
// print(new Array(longestLevelName).join(' ') + dates.join('\t\t'));
|
||||
// for (levelKey in levelRates) {
|
||||
// var hasStarted = false;
|
||||
// for (dateKey in levelRates[levelKey]) {
|
||||
// if (levelRates[levelKey][dateKey].started > 0) {
|
||||
// hasStarted = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// if (!hasStarted) continue;
|
||||
//
|
||||
// if (levelRates[levelKey].length < 6) continue;
|
||||
//
|
||||
// var level = levelRates[levelKey][0].level;
|
||||
// var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
||||
// var msg = level + levelSpacer;
|
||||
//
|
||||
// for (dateKey in levelRates[levelKey]) {
|
||||
// var day = levelRates[levelKey][dateKey].day;
|
||||
// var started = levelRates[levelKey][dateKey].started;
|
||||
// var finished = levelRates[levelKey][dateKey].finished;
|
||||
// var averagePlaytime = levelRates[levelKey][dateKey].averagePlaytime;
|
||||
// averagePlaytime = averagePlaytime ? Math.round(averagePlaytime) : 0;
|
||||
// var averageCodeProblems = levelRates[levelKey][dateKey].codeProblems;
|
||||
// averageCodeProblems = averageCodeProblems ? averageCodeProblems / started : 0.0;
|
||||
// var completionRate = started > 0 ? finished / started : 0;
|
||||
// msg += (completionRate * 100).toFixed(2) + "/" + averagePlaytime + "/" + averageCodeProblems.toFixed(2) + "\t";
|
||||
// }
|
||||
// print(msg);
|
||||
// }
|
||||
|
||||
var endTime = new Date();
|
||||
print("Runtime: " + (endTime - startTime));
|
||||
print("Runtime: " + (endTime - startTime));
|
||||
|
|
Loading…
Reference in a new issue