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:
Matt Lott 2014-12-04 12:57:57 -08:00
parent 7025f0a488
commit e3abb9ceb3
14 changed files with 184 additions and 16 deletions

View 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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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