mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
Capture active user metrics
Watching for these events: Level completed User registered Playtime of 30s in a level Purchase Payment Subscribe Earned an achievement
This commit is contained in:
parent
7025f0a488
commit
e3abb9ceb3
14 changed files with 184 additions and 16 deletions
16
app/schemas/models/analytics_users_active.coffee
Normal file
16
app/schemas/models/analytics_users_active.coffee
Normal file
|
@ -0,0 +1,16 @@
|
|||
c = require './../schemas'
|
||||
|
||||
AnalyticsUsersActiveSchema = c.object {
|
||||
title: 'Analytics Users Active'
|
||||
description: 'Active users data.'
|
||||
}
|
||||
|
||||
_.extend AnalyticsUsersActiveSchema.properties,
|
||||
creator: c.objectId(links: [{rel: 'extra', href: '/db/user/{($)}'}])
|
||||
created: c.date({title: 'Created', readOnly: true})
|
||||
|
||||
event: {type: 'string'}
|
||||
|
||||
c.extendBasicProperties AnalyticsUsersActiveSchema, 'analytics.users.active'
|
||||
|
||||
module.exports = AnalyticsUsersActiveSchema
|
|
@ -71,4 +71,6 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
|
|||
earnedPoints = worth
|
||||
wrapUp(doc)
|
||||
|
||||
User.saveActiveUser userID, "achievement"
|
||||
|
||||
module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
|
||||
|
|
10
server/analytics/AnalyticsUsersActive.coffee
Normal file
10
server/analytics/AnalyticsUsersActive.coffee
Normal file
|
@ -0,0 +1,10 @@
|
|||
mongoose = require 'mongoose'
|
||||
plugins = require '../plugins/plugins'
|
||||
|
||||
AnalyticsUsersActiveSchema = new mongoose.Schema({
|
||||
created:
|
||||
type: Date
|
||||
'default': Date.now
|
||||
}, {strict: false})
|
||||
|
||||
module.exports = AnalyticsUsersActive = mongoose.model('analytics.users.active', AnalyticsUsersActiveSchema)
|
16
server/analytics/analytics_users_active_handler.coffee
Normal file
16
server/analytics/analytics_users_active_handler.coffee
Normal file
|
@ -0,0 +1,16 @@
|
|||
AnalyticsUsersActive = require './AnalyticsUsersActive'
|
||||
Handler = require '../commons/Handler'
|
||||
|
||||
class AnalyticsUsersActiveHandler extends Handler
|
||||
modelClass: AnalyticsUsersActive
|
||||
jsonSchema: require '../../app/schemas/models/analytics_users_active'
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method in ['GET'] or req.user?.isAdmin()
|
||||
|
||||
makeNewInstance: (req) ->
|
||||
instance = super(req)
|
||||
instance.set('creator', req.user._id)
|
||||
instance
|
||||
|
||||
module.exports = new AnalyticsUsersActiveHandler()
|
|
@ -1,4 +1,5 @@
|
|||
module.exports.handlers =
|
||||
'analytics_users_active': 'analytics/analytics_users_active_handler'
|
||||
'article': 'articles/article_handler'
|
||||
'level': 'levels/level_handler'
|
||||
'level_component': 'levels/components/level_component_handler'
|
||||
|
|
|
@ -18,22 +18,34 @@ previous = {}
|
|||
|
||||
LevelSessionSchema.post 'init', (doc) ->
|
||||
previous[doc.get 'id'] =
|
||||
'state.completed': doc.get 'state.completed'
|
||||
'state.complete': doc.get 'state.complete'
|
||||
'playtime': doc.get 'playtime'
|
||||
|
||||
LevelSessionSchema.pre 'save', (next) ->
|
||||
User = require '../../users/User' # Avoid mutual inclusion cycles
|
||||
@set('changed', new Date())
|
||||
|
||||
id = @get('id')
|
||||
initd = id of previous
|
||||
levelID = @get('levelID')
|
||||
userID = @get('creator')
|
||||
activeUserEvent = null
|
||||
|
||||
# newly completed level
|
||||
if not (initd and previous[id]['state.completed']) and @get('state.completed')
|
||||
User = require '../../users/User' # Avoid mutual inclusion cycles
|
||||
User.update {_id: @get 'creator'}, {$inc: 'stats.gamesCompleted': 1}, {}, (err, count) ->
|
||||
# Newly completed level
|
||||
if not (initd and previous[id]['state']?['complete']) and @get('state.complete')
|
||||
User.update {_id: userID}, {$inc: 'stats.gamesCompleted': 1}, {}, (err, count) ->
|
||||
log.error err if err?
|
||||
activeUserEvent = "level-completed/#{levelID}"
|
||||
|
||||
# Spent at least 30s playing this level
|
||||
if not initd and @get('playtime') >= 30 or initd and (@get('playtime') - previous[id]['playtime'] >= 30)
|
||||
activeUserEvent = "level-playtime/#{levelID}"
|
||||
|
||||
delete previous[id] if initd
|
||||
next()
|
||||
if activeUserEvent?
|
||||
User.saveActiveUser userID, activeUserEvent, next
|
||||
else
|
||||
next()
|
||||
|
||||
LevelSessionSchema.statics.privateProperties = ['code', 'submittedCode', 'unsubscribed']
|
||||
LevelSessionSchema.statics.editableProperties = ['multiplayer', 'players', 'code', 'codeLanguage', 'completed', 'state',
|
||||
|
|
|
@ -80,10 +80,13 @@ PaymentHandler = class PaymentHandler extends Handler
|
|||
@logPaymentError(req, 'Missing apple transaction id')
|
||||
return @sendBadInputError(res, 'Apple purchase? Need to specify which transaction.')
|
||||
@handleApplePaymentPost(req, res, appleReceipt, appleTransactionID, appleLocalPrice)
|
||||
|
||||
@onPostSuccess req
|
||||
else
|
||||
@handleStripePaymentPost(req, res, stripeTimestamp, productID, stripeToken)
|
||||
@onPostSuccess req
|
||||
|
||||
onPostSuccess: (req) ->
|
||||
req.user?.saveActiveUser 'payment'
|
||||
|
||||
#- Apple payments
|
||||
|
||||
|
|
|
@ -103,7 +103,6 @@ class SubscriptionHandler extends Handler
|
|||
|
||||
@updateUser(req, user, customer.subscriptions.data[0], true, done)
|
||||
|
||||
|
||||
updateUser: (req, user, subscription, increment, done) ->
|
||||
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||
stripeInfo.planID = 'basic'
|
||||
|
@ -123,9 +122,9 @@ class SubscriptionHandler extends Handler
|
|||
if err
|
||||
@logSubscriptionError(req, 'Stripe user plan saving error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
req.user?.saveActiveUser 'subscribe'
|
||||
return done()
|
||||
|
||||
|
||||
unsubscribeUser: (req, user, done) ->
|
||||
stripeInfo = _.cloneDeep(user.get('stripe'))
|
||||
stripe.customers.cancelSubscription stripeInfo.customerID, stripeInfo.subscriptionID, { at_period_end: true }, (err) =>
|
||||
|
@ -139,7 +138,7 @@ class SubscriptionHandler extends Handler
|
|||
if err
|
||||
@logSubscriptionError(req, 'User save unsubscribe error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
req.user?.saveActiveUser 'unsubscribe'
|
||||
return done()
|
||||
|
||||
|
||||
module.exports = new SubscriptionHandler()
|
||||
|
|
|
@ -54,9 +54,10 @@ PurchaseHandler = class PurchaseHandler extends Handler
|
|||
|
||||
else
|
||||
super(req, res)
|
||||
|
||||
|
||||
onPostSuccess: (req) ->
|
||||
@addPurchaseToUser(req)
|
||||
req.user?.saveActiveUser 'purchase'
|
||||
|
||||
addPurchaseToUser: (req) ->
|
||||
user = req.user
|
||||
|
@ -80,7 +81,7 @@ PurchaseHandler = class PurchaseHandler extends Handler
|
|||
spent = hadSpent = user.get('spent') ? 0
|
||||
spent += item.get('gems')
|
||||
user.set('spent', spent)
|
||||
|
||||
|
||||
user.save()
|
||||
|
||||
|
||||
module.exports = new PurchaseHandler()
|
||||
|
|
|
@ -5,6 +5,7 @@ crypto = require 'crypto'
|
|||
mail = require '../commons/mail'
|
||||
log = require 'winston'
|
||||
plugins = require '../plugins/plugins'
|
||||
AnalyticsUsersActive = require '../analytics/AnalyticsUsersActive'
|
||||
|
||||
config = require '../../server_config'
|
||||
stripe = require('stripe')(config.stripe.secretKey)
|
||||
|
@ -162,7 +163,7 @@ UserSchema.statics.unconflictName = unconflictName = (name, done) ->
|
|||
|
||||
UserSchema.methods.register = (done) ->
|
||||
@set('anonymous', false)
|
||||
@set('permissions', ['admin']) if not isProduction
|
||||
@set('permissions', ['admin']) if not isProduction and not GLOBAL.testing
|
||||
if (name = @get 'name')? and name isnt ''
|
||||
unconflictName name, (err, uniqueName) =>
|
||||
return done err if err
|
||||
|
@ -176,6 +177,38 @@ UserSchema.methods.register = (done) ->
|
|||
sendwithus.api.send data, (err, result) ->
|
||||
log.error "sendwithus post-save error: #{err}, result: #{result}" if err
|
||||
delighted.addDelightedUser @
|
||||
@saveActiveUser 'register'
|
||||
|
||||
UserSchema.statics.saveActiveUser = (id, event, done=null) ->
|
||||
id = mongoose.Types.ObjectId id if _.isString id
|
||||
@findById id, (err, user) ->
|
||||
if err?
|
||||
log.error err
|
||||
else
|
||||
user?.saveActiveUser event
|
||||
done?()
|
||||
|
||||
UserSchema.methods.saveActiveUser = (event, done=null) ->
|
||||
try
|
||||
return done?() if @isAdmin()
|
||||
userID = @get('_id')
|
||||
|
||||
# Create if no active user entry for today
|
||||
today = new Date()
|
||||
minDate = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()))
|
||||
AnalyticsUsersActive.findOne({created: {$gte: minDate}, creator: mongoose.Types.ObjectId(userID)}).exec (err, activeUser) ->
|
||||
if err?
|
||||
log.error "saveActiveUser error retrieving active users: #{err}"
|
||||
else if not activeUser
|
||||
newActiveUser = new AnalyticsUsersActive()
|
||||
newActiveUser.set 'creator', userID
|
||||
newActiveUser.set 'event', event
|
||||
newActiveUser.save (err) ->
|
||||
log.error "Level session saveActiveUser error saving active user: #{err}" if err?
|
||||
done?()
|
||||
catch err
|
||||
log.error err
|
||||
done?()
|
||||
|
||||
UserSchema.pre('save', (next) ->
|
||||
@set('emailLower', @get('email')?.toLowerCase())
|
||||
|
|
|
@ -552,7 +552,7 @@ UserHandler = class UserHandler extends Handler
|
|||
usersTotal += 1
|
||||
userID = user.get('_id').toHexString()
|
||||
|
||||
LevelSession.count {creator: userID, 'state.completed': true}, (err, count) ->
|
||||
LevelSession.count {creator: userID, 'state.complete': true}, (err, count) ->
|
||||
update = if count then {$set: 'stats.gamesCompleted': count} else {$unset: 'stats.gamesCompleted': ''}
|
||||
User.findByIdAndUpdate user.get('_id'), update, doneWithUser
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ GLOBAL.tv4 = require 'tv4' # required for TreemaUtils to work
|
|||
# _.str = require 'underscore.string'
|
||||
|
||||
models_path = [
|
||||
'../../server/analytics/AnalyticsUsersActive'
|
||||
'../../server/articles/Article'
|
||||
'../../server/levels/Level'
|
||||
'../../server/levels/components/LevelComponent'
|
||||
|
|
|
@ -342,7 +342,7 @@ describe 'Statistics', ->
|
|||
session = new LevelSession
|
||||
name: 'Beat Gandalf'
|
||||
permissions: simplePermissions
|
||||
state: completed: true
|
||||
state: complete: true
|
||||
|
||||
unittest.getNormalJoe (joe) ->
|
||||
expect(joe.get 'stats.gamesCompleted').toBeUndefined()
|
||||
|
|
74
test/server/unit/analytics.spec.coffee
Normal file
74
test/server/unit/analytics.spec.coffee
Normal file
|
@ -0,0 +1,74 @@
|
|||
GLOBAL._ = require 'lodash'
|
||||
|
||||
require '../common'
|
||||
request = require 'request'
|
||||
AnalyticsUsersActive = require '../../../server/analytics/AnalyticsUsersActive'
|
||||
LevelSession = require '../../../server/levels/sessions/LevelSession'
|
||||
User = require '../../../server/users/User'
|
||||
|
||||
# TODO: these tests have some rerun/cleanup issues
|
||||
# TODO: add tests for purchase, payment, subscribe, unsubscribe, and earned achievements
|
||||
|
||||
describe 'Analytics', ->
|
||||
|
||||
it 'registered user', (done) ->
|
||||
clearModels [AnalyticsUsersActive], (err) ->
|
||||
expect(err).toBeNull()
|
||||
user = new User
|
||||
permissions: []
|
||||
name: "Fred" + Math.floor(Math.random() * 10000)
|
||||
user.save (err) ->
|
||||
expect(err).toBeNull()
|
||||
userID = mongoose.Types.ObjectId(user.get('_id'))
|
||||
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
|
||||
expect(activeUsers.length).toEqual(0)
|
||||
user.register ->
|
||||
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
|
||||
expect(err).toBeNull()
|
||||
expect(activeUsers.length).toEqual(1)
|
||||
expect(activeUsers[0]?.get('event')).toEqual('register')
|
||||
done()
|
||||
|
||||
it 'level completed', (done) ->
|
||||
clearModels [AnalyticsUsersActive], (err) ->
|
||||
expect(err).toBeNull()
|
||||
unittest.getNormalJoe (joe) ->
|
||||
userID = mongoose.Types.ObjectId(joe.get('_id'))
|
||||
session = new LevelSession
|
||||
name: 'Beat Gandalf'
|
||||
levelID: 'lotr'
|
||||
permissions: simplePermissions
|
||||
state: complete: false
|
||||
creator: userID
|
||||
session.save (err) ->
|
||||
expect(err).toBeNull()
|
||||
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
|
||||
expect(activeUsers.length).toEqual(0)
|
||||
session.set 'state', complete: true
|
||||
session.save (err) ->
|
||||
expect(err).toBeNull()
|
||||
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
|
||||
expect(err).toBeNull()
|
||||
expect(activeUsers.length).toEqual(1)
|
||||
expect(activeUsers[0]?.get('event')).toEqual('level-completed/lotr')
|
||||
done()
|
||||
|
||||
it 'level playtime', (done) ->
|
||||
clearModels [AnalyticsUsersActive], (err) ->
|
||||
expect(err).toBeNull()
|
||||
unittest.getNormalJoe (joe) ->
|
||||
userID = mongoose.Types.ObjectId(joe.get('_id'))
|
||||
session = new LevelSession
|
||||
name: 'Beat Gandalf'
|
||||
levelID: 'lotr'
|
||||
permissions: simplePermissions
|
||||
playtime: 60
|
||||
creator: userID
|
||||
session.save (err) ->
|
||||
expect(err).toBeNull()
|
||||
AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) ->
|
||||
expect(err).toBeNull()
|
||||
expect(activeUsers.length).toEqual(1)
|
||||
expect(activeUsers[0]?.get('event')).toEqual('level-playtime/lotr')
|
||||
done()
|
||||
|
Loading…
Reference in a new issue