Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-01-28 17:59:29 -08:00
commit 5ad048cc2d
17 changed files with 608 additions and 296 deletions

View file

@ -17,7 +17,7 @@ module.exports = class Tracker
return unless me and @isProduction and analytics? and not me.isAdmin()
# https://segment.io/docs/methods/identify
traits ?= {}
for userTrait in ['email', 'anonymous', 'dateCreated', 'name', 'wizardColor1', 'testGroupNumber', 'gender']
for userTrait in ['email', 'anonymous', 'dateCreated', 'name', 'wizardColor1', 'testGroupNumber', 'gender', 'lastLevel']
traits[userTrait] ?= me.get(userTrait)
analytics.identify me.id, traits

View file

@ -144,7 +144,7 @@ module.exports = class SegmentedSprite extends createjs.SpriteContainer
anim.initialize(mode ? createjs.MovieClip.INDEPENDENT, startPosition ? 0, loops ? true)
anim.specialGoToAndStop = specialGoToAndStop
for tweenData in animData.tweens
for tweenData, i in animData.tweens
stopped = false
tween = createjs.Tween
for func in tweenData

View file

@ -667,3 +667,18 @@ module.exports.thangNames = thangNames =
'Ulna'
'Yorick'
]
'Ogre Headhunter': [
'Bob'
'Deadtooth'
'Ez the Cruel'
'Grroq'
'Mog'
'Mogvar'
'Ral\'thuk'
'Soth'
'Ulxx'
'Ur'
'Veznyr'
'Warlegs'
'Xul Gor'
]

View file

@ -81,7 +81,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
awaiting_levels_adventurer_prefix: "Nosotros creamos 5 nuevos niveles cada semana"
awaiting_levels_adventurer: "Registrate como un aventurero"
awaiting_levels_adventurer_suffix: "para ser el primero en jugar nuevos niveles."
# adjust_volume: "Adjust volume"
adjust_volume: "Ajustar el volumen"
choose_your_level: "Elige tu nivel" # The rest of this section is the old play view at /play-old and isn't very important.
adventurer_prefix: "Puedes saltar a cualquier nivel de abajo, o discutir los niveles en "
adventurer_forum: "el foro del aventurero"
@ -160,10 +160,10 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
date: "Fecha"
body: "Cuerpo"
version: "Versión"
# pending: "Pending"
# accepted: "Accepted"
# rejected: "Rejected"
# withdrawn: "Withdrawn"
pending: "Pendiente"
accepted: "Aceptado"
rejected: "Rechazado"
withdrawn: "Retirado"
submitter: "Emisor"
submitted: "Enviado"
commit_msg: "Enviar mensaje"
@ -171,10 +171,10 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
version_history: "Historial de Versiones"
version_history_for: "Historial de Versiones para: "
select_changes: "Selcciona dos cambios abajo para ver la diferencia"
# undo_prefix: "Undo"
# undo_shortcut: "(Ctrl+Z)"
# redo_prefix: "Redo"
# redo_shortcut: "(Ctrl+Shift+Z)"
undo_prefix: "Deshacer"
undo_shortcut: "(Ctrl+Z)"
redo_prefix: "Rehacer"
redo_shortcut: "(Ctrl+Shift+Z)"
play_preview: "Mira el avance del nivel"
result: "Resultado"
results: "Resultados"
@ -198,9 +198,9 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
hard: "Difícil"
player: "Jugador"
player_level: "Nivel" # Like player level 5, not like level: Dungeons of Kithgard
# warrior: "Warrior"
# ranger: "Ranger"
# wizard: "Wizard"
warrior: "Guerrero"
ranger: "Guardabosques"
wizard: "Mago"
units:
second: "segundo"
@ -371,13 +371,13 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
subscribe:
subscribe_title: "Suscribirse"
unsubscribe: "Des-suscribirse"
# confirm_unsubscribe: "Confirm Unsubscribe"
# never_mind: "Never Mind, I Still Love You"
# thank_you_months_prefix: "Thank you for supporting us these last"
# thank_you_months_suffix: "months."
# thank_you: "Thank you for supporting CodeCombat."
# sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
# unsubscribe_feedback_placeholder: "O, what have we done?"
confirm_unsubscribe: "Confirmar cancelacion de suscripción"
never_mind: "Olvidalo, Te sigo queriendo"
thank_you_months_prefix: "Gracias por tu apoyo en estos ultimos"
thank_you_months_suffix: "meses."
thank_you: "Gracias por apoyar CodeCombat."
sorry_to_see_you_go: "¡Sentimos que te vayas! Por favor, haznos saber lo que podríamos haber hecho mejor."
unsubscribe_feedback_placeholder: "¿Pero qué hemos hecho?"
levels: "Adquirí más practica con un nivel bonus!"
heroes: "Héroes más poderosos!"
gems: "Bonus de 3500 todos los meses!"
@ -401,7 +401,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
javascript_blurb: "El lenguaje de la web (no es Java)."
coffeescript_blurb: "JavaScript pero más bonito."
clojure_blurb: "Un Lisp moderno."
lua_blurb: "Lenguaje ara Juegos."
lua_blurb: "Lenguaje para Juegos."
io_blurb: "Simple pero oscuro."
status: "Estado"
weapons: "Armas"
@ -625,12 +625,12 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
revert: "Revertir"
revert_models: "Revertir Modelos"
pick_a_terrain: "Elije un Terreno"
# dungeon: "Dungeon"
# indoor: "Indoor"
# desert: "Desert"
dungeon: "Calabozo"
indoor: "Interior"
desert: "Desierto"
grassy: "Herboso"
small: "Pequeño"
# large: "Large"
large: "Grande"
fork_title: "Fork de Nueva Versión"
fork_creating: "Creando Fork..."
generate_terrain: "Generar terreno"
@ -651,9 +651,9 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
level_tab_thangs_all: "Todo"
level_tab_thangs_conditions: "Condiciones Iniciales"
level_tab_thangs_add: "Agregar Thangs"
# add_components: "Add Components"
# component_configs: "Component Configurations"
# config_thang: "Double click to configure a thang"
add_components: "Agregar Componentes"
component_configs: "Configuraciones del Componente"
config_thang: "Doble clic para configurar un thang"
delete: "Borrar"
duplicate: "Duplicar"
# stop_duplicate: "Stop Duplicate"
@ -691,7 +691,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
# achievement_query_goals: "Key achievement off of level goals"
# level_completion: "Level Completion"
# pop_i18n: "Populate I18N"
# tasks: "Tasks"
tasks: "Tareas"
article:
edit_btn_preview: "Vista previa"
@ -900,7 +900,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
leaderboard: "Clasificación"
user_schema: "Esquema de Usuario"
user_profile: "Perfil de Usuario"
# patch: "Patch"
patch: "Parche"
patches: "Parches"
patched_model: "Documento fuente"
model: "Modelo"
@ -923,13 +923,13 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
employers: "Empleadores"
candidates: "Candidatos"
candidate_sessions: "Sesión de candidato"
# user_remark: "User Remark"
# user_remarks: "User Remarks"
user_remark: "Observación del usuario"
user_remarks: "Observaciones del usuario"
versions: "Versiones"
items: "Items"
heroes: "Héroes"
achievement: "Logros"
# clas: "CLAs"
clas: "CLAs"
play_counts: "Conteo de juegos"
feedback: "Feedback"
payment_info: "Información de pago"
@ -965,7 +965,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
archmage_wiki_url: "nuestra wiki de Archimago"
opensource_description_suffix: "Para la lista de softwares que hacen al juego posible."
practices_title: "Mejores prácticas respetuosas"
# practices_description: "These are our promises to you, the player, in slightly less legalese."
practices_description: "Estas son nuestras promesas hacia ti, el jugador, en términos menos legales."
privacy_title: "Privacidad"
privacy_description: "No vederemos nada sobre tu información personalWe will not sell any of your personal information."
security_title: "Seguridad"

View file

@ -323,8 +323,8 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
tip_reusable_software: "Antes de um software poder ser reutilizável, primeiro tem de ser utilizável."
tip_optimization_operator: "Todas as linguagens têm um operador de otimização. Na maior parte delas esse operador é //."
tip_lines_of_code: "Medir o progresso em programação pelo número de linhas de código é como medir o progresso da construção de um avião pelo peso. — Bill Gates"
# tip_source_code: "I want to change the world but they would not give me the source code."
# tip_javascript_java: "Java is to JavaScript what Car is to Carpet. - Chris Heilmann"
tip_source_code: "Quero mudar o mundo, mas não há maneira de me darem o código-fonte."
tip_javascript_java: "Java é para JavaScript o mesmo que Carro (Car) para Tapete (Carpet). - Chris Heilmann"
game_menu:
inventory_tab: "Inventário"
@ -371,13 +371,13 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
subscribe:
subscribe_title: "Subscrever"
unsubscribe: "Cancelar Subscrição"
# confirm_unsubscribe: "Confirm Unsubscribe"
# never_mind: "Never Mind, I Still Love You"
# thank_you_months_prefix: "Thank you for supporting us these last"
# thank_you_months_suffix: "months."
# thank_you: "Thank you for supporting CodeCombat."
# sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
# unsubscribe_feedback_placeholder: "O, what have we done?"
confirm_unsubscribe: "Confirmar Cancelamento da Subscrição"
never_mind: "Não Importa, Gostamos de Ti à Mesma"
thank_you_months_prefix: "Obrigado por nos teres apoiado neste(s) último(s)"
thank_you_months_suffix: "mês(meses)."
thank_you: "Obrigado por apoiares o CodeCombat."
sorry_to_see_you_go: "Lamentamos ver-te partir! Por favor, diz-nos o que podíamos ter feito melhor."
unsubscribe_feedback_placeholder: "Oh, o que fomos fazer?"
levels: "Pratica mais com níveis bónus!"
heroes: "Heróis mais poderosos!"
gems: "3500 gemas de bónus todos os meses!"
@ -865,8 +865,8 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
price: "Preço"
gems: "Gemas"
active: "Activa"
subscribed: "Subscrito"
unsubscribed: "Não Subscrito"
subscribed: "Subscrito(a)"
unsubscribed: "Não Subscrito(a)"
active_until: "Ativa Até"
cost: "Custo"
next_payment: "Próximo Pagamento"

View file

@ -323,8 +323,8 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
tip_reusable_software: "Прежде, чем программное обеспечение станет повторно используемым, оно должно стать в принципе используемым."
tip_optimization_operator: "В каждом языке есть оператор оптимизации. В большинстве языков это оператор //"
tip_lines_of_code: "Измерение прогресса программирования в строках кода - это как измерять прогресс построения самолета по его весу. — Bill Gates"
# tip_source_code: "I want to change the world but they would not give me the source code."
# tip_javascript_java: "Java is to JavaScript what Car is to Carpet. - Chris Heilmann"
tip_source_code: "Я хочу изменить мир, но они вряд ли дадут мне исходники."
tip_javascript_java: "Java к JavaScript относится так же, как кол относится к колготкам. - Chris Heilmann (перефраз.)"
game_menu:
inventory_tab: "Инвентарь"
@ -371,13 +371,13 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
subscribe:
subscribe_title: "Подпишись"
unsubscribe: "Отписаться"
# confirm_unsubscribe: "Confirm Unsubscribe"
# never_mind: "Never Mind, I Still Love You"
# thank_you_months_prefix: "Thank you for supporting us these last"
# thank_you_months_suffix: "months."
# thank_you: "Thank you for supporting CodeCombat."
# sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
# unsubscribe_feedback_placeholder: "O, what have we done?"
confirm_unsubscribe: "Подтвердить отмену подписки"
never_mind: "Неважно, Я Все Равно Тебя Люблю"
thank_you_months_prefix: "Спасибо Вам за поддерживание нас в течение последних"
thank_you_months_suffix: "месяцев."
thank_you: "Спасибо за поддержку CodeCombat."
sorry_to_see_you_go: "Жаль, что вы уходите! Пожалуйста, расскажите нам, что мы могли бы сделать лучше."
unsubscribe_feedback_placeholder: "О, что мы наделали?"
levels: "Получите больше практики с бонусными уровнями!"
heroes: "Более сильные герои!"
gems: "3500 бонусных самоцветов каждый месяц!"

View file

@ -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)

View file

@ -111,7 +111,7 @@
.user-dropdown-header
background: #E4CF8C
height: 160px
height: auto
padding: 10px
text-align: center
color: black

View file

@ -18,12 +18,6 @@
top: -61px
left: 0px
#subscribe-gems
position: absolute
top: 155px
right: 65px
//- Header
h1
position: absolute
@ -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
@ -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

View file

@ -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")

View file

@ -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: ->

View file

@ -286,6 +286,7 @@ module.exports = class PlayLevelView extends RootView
if not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
me.set('lastLevel', @levelID)
me.save()
application.tracker?.identify()
@saveRecentMatch() if @otherSession
@levelLoader.destroy()
@levelLoader = null

View file

@ -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) ->

View file

@ -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

View 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))

View file

@ -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,6 +22,8 @@ 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(
@ -26,16 +31,25 @@ if __name__ == '__main__':
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))

View file

@ -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));