mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-02 16:21:01 -04:00
Add admin analytics page with MAUs
Includes updating analytics insert script used to inject aggregated data into production database.
This commit is contained in:
parent
d987b644a9
commit
d445024cb6
6 changed files with 192 additions and 7 deletions
app
core
styles/admin
templates/admin
views/admin
scripts/analytics/mongodb/queries
server/analytics
|
@ -33,6 +33,7 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'admin/clas': go('admin/CLAsView')
|
||||
'admin/employers': go('admin/EmployersListView')
|
||||
'admin/files': go('admin/FilesView')
|
||||
'admin/analytics': go('admin/AnalyticsView')
|
||||
'admin/analytics/users': go('admin/AnalyticsUsersView')
|
||||
'admin/analytics/subscriptions': go('admin/AnalyticsSubscriptionsView')
|
||||
'admin/level-sessions': go('admin/LevelSessionsView')
|
||||
|
|
12
app/styles/admin/analytics.sass
Normal file
12
app/styles/admin/analytics.sass
Normal file
|
@ -0,0 +1,12 @@
|
|||
#admin-analytics-view
|
||||
|
||||
#site-content-area
|
||||
width: 100%
|
||||
.big-stat
|
||||
width: auto
|
||||
.active-users
|
||||
color: green
|
||||
.count
|
||||
font-size: 50pt
|
||||
.description
|
||||
font-size: 8pt
|
29
app/templates/admin/analytics.jade
Normal file
29
app/templates/admin/analytics.jade
Normal file
|
@ -0,0 +1,29 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
|
||||
if me.isAdmin()
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-5.big-stat.active-users
|
||||
if activeUsers.length > 0
|
||||
div.description 30-day Active Users
|
||||
div.count= activeUsers[0].monthlyCount
|
||||
|
||||
h1 Active Users
|
||||
table.table.table-striped.table-condensed
|
||||
tr
|
||||
th Day
|
||||
th Daily Actives
|
||||
th Monthly Actives
|
||||
th DAUs / MAUs
|
||||
each activeUser in activeUsers
|
||||
tr
|
||||
td= activeUser.day
|
||||
td= activeUser.dailyCount
|
||||
if activeUser.monthlyCount
|
||||
td= activeUser.monthlyCount
|
||||
td #{(activeUser.dailyCount / activeUser.monthlyCount * 100).toFixed(2)}%
|
||||
else
|
||||
td
|
||||
td
|
27
app/views/admin/AnalyticsView.coffee
Normal file
27
app/views/admin/AnalyticsView.coffee
Normal file
|
@ -0,0 +1,27 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/admin/analytics'
|
||||
utils = require 'core/utils'
|
||||
|
||||
module.exports = class AnalyticsView extends RootView
|
||||
id: 'admin-analytics-view'
|
||||
template: template
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
startDay = utils.getUTCDay(-30).replace(/-/g, '')
|
||||
endDay = utils.getUTCDay(-30).replace(/-/g, '')
|
||||
request = @supermodel.addRequestResource 'active_users', {
|
||||
url: '/db/analytics_perday/-/active_users'
|
||||
data: {startDay: startDay, endDay: endDay}
|
||||
method: 'POST'
|
||||
success: (data) =>
|
||||
@activeUsers = data
|
||||
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
|
||||
@render?()
|
||||
}, 0
|
||||
request.load()
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
context.activeUsers = @activeUsers ? []
|
||||
context
|
|
@ -17,8 +17,8 @@ try {
|
|||
var scriptStartTime = new Date();
|
||||
var analyticsStringCache = {};
|
||||
|
||||
// Look at last 30 days, same as Mixpanel
|
||||
var numDays = 30;
|
||||
var numDays = 32;
|
||||
var daysInMonth = 30;
|
||||
|
||||
var startDay = new Date();
|
||||
today = startDay.toISOString().substr(0, 10);
|
||||
|
@ -27,6 +27,7 @@ try {
|
|||
|
||||
var levelCompletionFunnel = ['Started Level', 'Saw Victory'];
|
||||
var levelHelpEvents = ['Problem alert help clicked', 'Spell palette help clicked', 'Start help video'];
|
||||
var activeUserEvents = ['Finished Signup', 'Started Level'];
|
||||
|
||||
log("Today is " + today);
|
||||
log("Start day is " + startDay);
|
||||
|
@ -39,7 +40,7 @@ try {
|
|||
for (day in levelCompletionData[level]) {
|
||||
if (today === day) continue; // Never save data for today because it's incomplete
|
||||
for (event in levelCompletionData[level][day]) {
|
||||
insertEventCount(event, level, day, levelCompletionData[level][day][event]);
|
||||
insertLevelEventCount(event, level, day, levelCompletionData[level][day][event]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +51,7 @@ try {
|
|||
for (level in levelDropCounts) {
|
||||
for (day in levelDropCounts[level]) {
|
||||
if (today === day) continue; // Never save data for today because it's incomplete
|
||||
insertEventCount('User Dropped', level, day, levelDropCounts[level][day]);
|
||||
insertLevelEventCount('User Dropped', level, day, levelDropCounts[level][day]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,7 +62,7 @@ try {
|
|||
for (day in levelHelpCounts[level]) {
|
||||
if (today === day) continue; // Never save data for today because it's incomplete
|
||||
for (event in levelHelpCounts[level][day]) {
|
||||
insertEventCount(event, level, day, levelHelpCounts[level][day][event]);
|
||||
insertLevelEventCount(event, level, day, levelHelpCounts[level][day][event]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,11 +74,22 @@ try {
|
|||
for (day in levelSubscriptionCounts[level]) {
|
||||
if (today === day) continue; // Never save data for today because it's incomplete
|
||||
for (event in levelSubscriptionCounts[level][day]) {
|
||||
insertEventCount(event, level, day, levelSubscriptionCounts[level][day][event]);
|
||||
insertLevelEventCount(event, level, day, levelSubscriptionCounts[level][day][event]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log("Getting active user counts...");
|
||||
var activeUserCounts = getActiveUserCounts(startDay, activeUserEvents);
|
||||
// printjson(activeUserCounts);
|
||||
log("Inserting active user counts...");
|
||||
for (day in activeUserCounts) {
|
||||
if (today === day) continue; // Never save data for today because it's incomplete
|
||||
for (event in activeUserCounts[day]) {
|
||||
insertEventCount(event, day, activeUserCounts[day][event]);
|
||||
}
|
||||
}
|
||||
|
||||
log("Script runtime: " + (new Date() - scriptStartTime));
|
||||
}
|
||||
catch(err) {
|
||||
|
@ -383,7 +395,87 @@ function getLevelSubscriptionCounts(startDay) {
|
|||
return levelFunnelData;
|
||||
}
|
||||
|
||||
function insertEventCount(event, level, day, count) {
|
||||
function getActiveUserCounts(startDay, activeUserEvents) {
|
||||
// Counts active users per day
|
||||
if (!startDay) return {};
|
||||
|
||||
var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
|
||||
var queryParams = {$and: [
|
||||
{_id: {$gte: startObj}},
|
||||
{'event': {$in: activeUserEvents}}
|
||||
]};
|
||||
var cursor = logDB['log'].find(queryParams);
|
||||
|
||||
var dayUserMap = {};
|
||||
while (cursor.hasNext()) {
|
||||
var doc = cursor.next();
|
||||
var created = doc._id.getTimestamp().toISOString();
|
||||
var day = created.substring(0, 10);
|
||||
var user = doc.user;
|
||||
|
||||
if (!dayUserMap[day]) dayUserMap[day] = {};
|
||||
dayUserMap[day][user] = true;
|
||||
}
|
||||
// printjson(dayUserMap['2015-11-01']);
|
||||
|
||||
var activeUsersCounts = {};
|
||||
var monthlyActives = [];
|
||||
for (day in dayUserMap) {
|
||||
activeUsersCounts[day] = {'Daily Active Users': Object.keys(dayUserMap[day]).length};
|
||||
monthlyActives.push({day: day, users: dayUserMap[day]});
|
||||
}
|
||||
|
||||
monthlyActives.sort(function (a, b) {return a.day.localeCompare(b.day);});
|
||||
|
||||
// Calculate monthly actives for each day, starting when we have enough data
|
||||
for (var i = daysInMonth - 1; i < monthlyActives.length; i++) {
|
||||
var monthUserMap = {};
|
||||
for (var j = i - daysInMonth + 1; j <= i; j++) {
|
||||
for (var user in monthlyActives[j].users) {
|
||||
monthUserMap[user] = true;
|
||||
}
|
||||
}
|
||||
activeUsersCounts[monthlyActives[i].day]['Monthly Active Users'] = Object.keys(monthUserMap).length;
|
||||
}
|
||||
return activeUsersCounts;
|
||||
}
|
||||
|
||||
function insertEventCount(event, day, count) {
|
||||
// analytics.perdays schema in server/analytics/AnalyticsPeryDay.coffee
|
||||
day = day.replace(/-/g, '');
|
||||
|
||||
var eventID = getAnalyticsString(event);
|
||||
var filterID = getAnalyticsString('all');
|
||||
|
||||
var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
|
||||
var queryParams = {$and: [{d: day}, {e: eventID}, {f: filterID}]};
|
||||
var doc = db['analytics.perdays'].findOne(queryParams);
|
||||
if (doc && doc.c === count) return;
|
||||
|
||||
if (doc && doc.c !== count) {
|
||||
// Update existing count, assume new one is more accurate
|
||||
// log("Updating count in db for " + day + " " + event + " " + doc.c + " => " + count);
|
||||
var results = db['analytics.perdays'].update(queryParams, {$set: {c: count}});
|
||||
if (results.nMatched !== 1 && results.nModified !== 1) {
|
||||
log("ERROR: update event count failed");
|
||||
printjson(results);
|
||||
}
|
||||
}
|
||||
else {
|
||||
var insertDoc = {d: day, e: eventID, f: filterID, c: count};
|
||||
var results = db['analytics.perdays'].insert(insertDoc);
|
||||
if (results.nInserted !== 1) {
|
||||
log("ERROR: insert event failed");
|
||||
printjson(results);
|
||||
printjson(insertDoc);
|
||||
}
|
||||
// else {
|
||||
// log("Added " + day + " " + event + " " + count);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
function insertLevelEventCount(event, level, day, count) {
|
||||
// analytics.perdays schema in server/analytics/AnalyticsPeryDay.coffee
|
||||
day = day.replace(/-/g, '');
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ class AnalyticsPerDayHandler extends Handler
|
|||
|
||||
getByRelationship: (req, res, args...) ->
|
||||
return @sendForbiddenError res unless @hasAccess req
|
||||
return @getActiveUsers(req, res) if args[1] is 'active_users'
|
||||
return @getCampaignCompletionsBySlug(req, res) if args[1] is 'campaign_completions'
|
||||
return @getLevelCompletionsBySlug(req, res) if args[1] is 'level_completions'
|
||||
return @getLevelDropsBySlugs(req, res) if args[1] is 'level_drops'
|
||||
|
@ -21,6 +22,29 @@ class AnalyticsPerDayHandler extends Handler
|
|||
return @getLevelSubscriptionsBySlugs(req, res) if args[1] is 'level_subscriptions'
|
||||
super(arguments...)
|
||||
|
||||
getActiveUsers: (req, res) ->
|
||||
AnalyticsString.find({v: {$in: ['Daily Active Users', 'Monthly Active Users']}}).exec (err, documents) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
for doc in documents
|
||||
dailyID = doc._id if doc.v is 'Daily Active Users'
|
||||
monthlyID = doc._id if doc.v is 'Monthly Active Users'
|
||||
return @sendSuccess res, [] unless dailyID? and monthlyID?
|
||||
|
||||
AnalyticsPerDay.find({e: {$in: [dailyID, monthlyID]}}).exec (err, documents) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
dayCountsMap = {}
|
||||
for doc in documents
|
||||
dayCountsMap[doc.d] ?= {}
|
||||
dayCountsMap[doc.d]['dailyCount'] = doc.c if doc.e is dailyID
|
||||
dayCountsMap[doc.d]['monthlyCount'] = doc.c if doc.e is monthlyID
|
||||
activeUsers = []
|
||||
for key, val of dayCountsMap
|
||||
data = day: key
|
||||
data.dailyCount = val.dailyCount if val.dailyCount
|
||||
data.monthlyCount = val.monthlyCount if val.monthlyCount
|
||||
activeUsers.push data
|
||||
@sendSuccess(res, activeUsers)
|
||||
|
||||
getCampaignCompletionsBySlug: (req, res) ->
|
||||
# Send back an ordered array of level per-day starts and finishes
|
||||
# Parameters:
|
||||
|
|
Loading…
Add table
Reference in a new issue