Add admin analytics page with MAUs

Includes updating analytics insert script used to inject aggregated
data into production database.
This commit is contained in:
Matt Lott 2015-11-04 10:54:40 -08:00
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

View file

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

View 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

View 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

View 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

View file

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

View file

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