mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-28 10:06:08 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
5ad048cc2d
17 changed files with 608 additions and 296 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 бонусных самоцветов каждый месяц!"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
|
||||
.user-dropdown-header
|
||||
background: #E4CF8C
|
||||
height: 160px
|
||||
height: auto
|
||||
padding: 10px
|
||||
text-align: center
|
||||
color: black
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,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))
|
||||
|
|
|
@ -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));
|
Loading…
Reference in a new issue