Update analytics dashboard

Splitting out classroom and campaign active users
Adding more detailed active users graphs
Patching up missing analytics log events for 2/2/16-2/9/16
This commit is contained in:
Matt Lott 2016-02-16 09:23:38 -08:00
parent 1f52329c7a
commit 73657d5428
6 changed files with 410 additions and 139 deletions

View file

@ -66,11 +66,12 @@ module.exports.createLineChart = (containerSelector, chartLines) ->
.call(xAxis)
.selectAll("text")
.attr("dy", ".35em")
.attr("transform", "translate(" + (margin + yAxisWidth) + "," + (height + margin) + ")")
.attr("transform", "translate(" + (margin + yAxisWidth * yScaleCount) + "," + (height + margin) + ")")
.style("text-anchor", "start")
if line.showYScale
# y-Axis
lineColor = if yScaleCount > 1 then line.lineColor else 'black'
yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
yAxis = d3.svg.axis()
.scale(yRange)
@ -78,12 +79,12 @@ module.exports.createLineChart = (containerSelector, chartLines) ->
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (margin + yAxisWidth * currentYScale) + "," + margin + ")")
.style("color", line.lineColor)
.style("color", lineColor)
.call(yAxis)
.selectAll("text")
.attr("y", 0)
.attr("x", 0)
.attr("fill", line.lineColor)
.attr("fill", lineColor)
.style("text-anchor", "start")
currentYScale++

View file

@ -8,7 +8,9 @@
color: blue
.recurring-revenue
color: green
.active-users
.campaign-active-users
color: purple
.classroom-active-users
color: red
.count
font-size: 70pt

View file

@ -9,16 +9,28 @@ block content
.row
.col-md-5.big-stat.active-classes
if activeClasses.length > 0
div.description 30-day Active Classes
div.description Monthly Active Classes
div.count= activeClasses[0].groups[activeClasses[0].groups.length - 1]
.col-md-5.big-stat.recurring-revenue
if revenue.length > 0
div.description 30-day Monthly Recurring Revenue
div.description Monthly Recurring Revenue
div.count $#{Math.round((revenue[0].groups[revenue[0].groups.length - 1]) / 100)}
.col-md-5.big-stat.active-users
.col-md-5.big-stat.classroom-active-users
if activeUsers.length > 0
div.description 30-day Active Users
div.count= activeUsers[0].monthlyCount
- var classroomBigMAU = 0;
each count, event in activeUsers[0].events
if event.indexOf('MAU classroom') >= 0
- classroomBigMAU += count;
div.description Classroom Monthly Active Users
div.count= classroomBigMAU
.col-md-5.big-stat.campaign-active-users
if activeUsers.length > 0
- var campaignBigMAU = 0;
each count, event in activeUsers[0].events
if event.indexOf('MAU campaign') >= 0
- campaignBigMAU += count;
div.description Campaign Monthly Active Users
div.count= campaignBigMAU
h3 KPI 60 days
.kpi-recent-chart.line-chart-container
@ -26,7 +38,39 @@ block content
h3 KPI 365 days
.kpi-chart.line-chart-container
h3 Active Classes 90 days
h1 Table of Contents
b Graphs
div
a(href='#active-classes-graph') Active Classes
div
a(href='#recurring-revenue-graph') Recurring Revenue
div
a(href='#classroom-daus-graph') Classroom Daily Active Users
div
a(href='#classroom-maus-graph') Classroom Monthly Active Users
div
a(href='#campaign-daus-graph') Campaign Daily Active Users
div
a(href='#campaign-maus-graph') Campaign Monthly Active Users
div
a(href='#furthest-courses-table') Campaign vs Classroom Paid Monthly Active Users
div
a(href='#furthest-courses-table') Enrollments Issued and Redeemed
b Tables
div
a(href='#furthest-courses-table') Furthest Course
div
a(href='#school-counts-table') School Counts
div
a(href='#active-classes-table') Active Classes
div
a(href='#recurring-revenue-table') Recurring Revenue
div
a(href='#active-users-table') Active Users
div
a(href='#enrollments-table') Enrollments
h3#active-classes-graph Active Classes 90 days
.small Active class: 12+ students in a classroom, with 6+ who played in last 30 days.
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
@ -35,16 +79,33 @@ block content
.small Free class: not paid, not trial
.active-classes-chart.line-chart-container
h3 Recurring Revenue 90 days
h3#recurring-revenue-graph Recurring Revenue 90 days
.recurring-revenue-chart.line-chart-container
h3 Active Users 90 days
.active-users-chart.line-chart-container
h3#classroom-daus-graph Classroom Daily Active Users 90 days
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
.small Free student: not paid, not trial
.classroom-daily-active-users-chart.line-chart-container
h3#classroom-maus-graph Classroom Monthly Active Users 90 days
.classroom-monthly-active-users-chart.line-chart-container
h3#campaign-daus-graph Campaign Daily Active Users 90 days
.small Paid user: had monthly or yearly sub on given day
.small Free user: not paid
.campaign-daily-active-users-chart.line-chart-container
h3#campaign-maus-graph Campaign Monthly Active Users 90 days
.campaign-monthly-active-users-chart.line-chart-container
h3 Campaign vs Classroom Paid Monthly Active Users 90 days
.campaign-vs-classroom-monthly-active-users-chart.line-chart-container
h3 Enrollments Issued and Redeemed 90 days
.paid-courses-chart.line-chart-container
h3 Furthest Course
h3#furthest-courses-table Furthest Course
.small Teacher: owner of a course instance
.small Student: member of a course instance (assigned to course)
.small For course instances created in last #{view.furthestCourseDayRange} days, not Single Player, hourOfCode != true
@ -65,7 +126,7 @@ block content
else
div Loading ...
h3 School Counts
h3#school-counts-table School Counts
.small Only including schools with #{view.minSchoolCount}+ counts
if view.schoolCounts
table.table.table-striped.table-condensed
@ -81,7 +142,7 @@ block content
else
div Loading ...
h1 Active Classes
h1#active-classes-table Active Classes
table.table.table-striped.table-condensed
tr
th Day
@ -93,7 +154,7 @@ block content
each val in activeClass.groups
td= val
h1 Recurring Revenue
h1#recurring-revenue-table Recurring Revenue
table.table.table-striped.table-condensed
tr
th Day
@ -105,25 +166,33 @@ block content
each val in entry.groups
td $#{(val / 100).toFixed(2)}
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
h1#active-users-table Active Users
if activeUsers.length > 0
- var eventNames = [];
each count, event in activeUsers[0].events
- eventNames.push(event)
table.table.table-striped.table-condensed
tr
td= activeUser.day
td= activeUser.dailyCount
if activeUser.monthlyCount
td= activeUser.monthlyCount
td #{(activeUser.dailyCount / activeUser.monthlyCount * 100).toFixed(2)}%
else
td
td
th(style='min-width:85px;') Day
each eventName in eventNames
th= eventName
th DAU Campaign Total
th DAU Classroom Total
each activeUser in activeUsers
tr
td= activeUser.day
- var dauCampaignTotal = 0
- var dauClassroomTotal = 0
each eventName in eventNames
td= activeUser.events[eventName] || 0
if eventName.indexOf('DAU campaign') >= 0
- dauCampaignTotal += (activeUser.events[eventName] || 0);
else if eventName.indexOf('DAU classroom') >= 0
- dauClassroomTotal += (activeUser.events[eventName] || 0);
td= dauCampaignTotal
td= dauClassroomTotal
h1 Enrollments
h1#enrollments-table Enrollments
table.table.table-striped.table-condensed
tr
th Day

View file

@ -250,18 +250,19 @@ module.exports = class AnalyticsView extends RootView
y: entry.value
# Trim points preceding days
for point, i in points
if point.day is days[0]
points.splice(0, i)
break
if points.length and days.length and points[0].day.localeCompare(days[0]) < 0
for point, i in points
if point.day.localeCompare(days[0]) >= 0
points.splice(0, i)
break
# Ensure points for each day
for day, i in days
if points.length <= i or points[i].day isnt day
prevY = if i > 0 then points[i - 1].y else 0.0
points.splice i, 0,
y: prevY
day: day
y: prevY
points[i].x = i
points.splice(0, points.length - days.length) if points.length > days.length
@ -271,7 +272,11 @@ module.exports = class AnalyticsView extends RootView
d3Utils.createLineChart('.kpi-recent-chart', @kpiRecentChartLines)
d3Utils.createLineChart('.kpi-chart', @kpiChartLines)
d3Utils.createLineChart('.active-classes-chart', @activeClassesChartLines)
d3Utils.createLineChart('.active-users-chart', @activeUsersChartLines)
d3Utils.createLineChart('.classroom-daily-active-users-chart', @classroomDailyActiveUsersChartLines)
d3Utils.createLineChart('.classroom-monthly-active-users-chart', @classroomMonthlyActiveUsersChartLines)
d3Utils.createLineChart('.campaign-daily-active-users-chart', @campaignDailyActiveUsersChartLines)
d3Utils.createLineChart('.campaign-monthly-active-users-chart', @campaignMonthlyActiveUsersChartLines)
d3Utils.createLineChart('.campaign-vs-classroom-monthly-active-users-chart.line-chart-container', @campaignVsClassroomMonthlyActiveUsersChartLines)
d3Utils.createLineChart('.paid-courses-chart', @enrollmentsChartLines)
d3Utils.createLineChart('.recurring-revenue-chart', @revenueChartLines)
@ -284,6 +289,7 @@ module.exports = class AnalyticsView extends RootView
updateKPIChartData: (timeframeDays, chartLines) ->
days = d3Utils.createContiguousDays(timeframeDays)
# Build active classes KPI line
if @activeClasses?.length > 0
data = []
for entry in @activeClasses
@ -294,13 +300,14 @@ module.exports = class AnalyticsView extends RootView
points = @createLineChartPoints(days, data)
chartLines.push
points: points
description: '30-day Active Classes'
description: 'Monthly Active Classes'
lineColor: 'blue'
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
# Build recurring revenue KPI line
if @revenue?.length > 0
data = []
for entry in @revenue
@ -311,31 +318,60 @@ module.exports = class AnalyticsView extends RootView
points = @createLineChartPoints(days, data)
chartLines.push
points: points
description: '30-day Recurring Revenue (in thousands)'
description: 'Monthly Recurring Revenue (in thousands)'
lineColor: 'green'
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
# Build campaign and classroom MAU KPI lines
if @activeUsers?.length > 0
data = []
eventDayDataMap = {}
for entry in @activeUsers
break unless entry.monthlyCount
data.push
day: entry.day
value: entry.monthlyCount / 1000
data.reverse()
points = @createLineChartPoints(days, data)
day = entry.day
for event, count of entry.events
if event.indexOf('MAU campaign') >= 0
eventDayDataMap['MAU campaign'] ?= {}
eventDayDataMap['MAU campaign'][day] ?= 0
eventDayDataMap['MAU campaign'][day] += count
else if event.indexOf('MAU classroom') >= 0
eventDayDataMap['MAU classroom'] ?= {}
eventDayDataMap['MAU classroom'][day] ?= 0
eventDayDataMap['MAU classroom'][day] += count
campaignData = []
classroomData = []
for event, entry of eventDayDataMap
if event is 'MAU campaign'
for day, count of entry
campaignData.push day: day, value: count / 1000
else
for day, count of entry
classroomData.push day: day, value: count / 1000
campaignData.reverse()
classroomData.reverse()
points = @createLineChartPoints(days, classroomData)
chartLines.push
points: points
description: '30-day Active Users (in thousands)'
description: 'Classroom Monthly Active Users (in thousands)'
lineColor: 'red'
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
points = @createLineChartPoints(days, campaignData)
chartLines.push
points: points
description: 'Campaign Monthly Active Users (in thousands)'
lineColor: 'purple'
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
updateActiveClassesChartData: ->
@activeClassesChartLines = []
return unless @activeClasses?.length
@ -370,55 +406,95 @@ module.exports = class AnalyticsView extends RootView
line.max = totalMax for line in @activeClassesChartLines
updateActiveUsersChartData: ->
@activeUsersChartLines = []
# Create chart lines for the active user events returned by active_users in analytics_perday_handler
@campaignDailyActiveUsersChartLines = []
@campaignMonthlyActiveUsersChartLines = []
@classroomDailyActiveUsersChartLines = []
@classroomMonthlyActiveUsersChartLines = []
@campaignVsClassroomMonthlyActiveUsersChartLines = []
return unless @activeUsers?.length
days = d3Utils.createContiguousDays(90)
dailyData = []
monthlyData = []
dausmausData = []
colorIndex = 0
# Separate day/value arrays by event
eventDataMap = {}
for entry in @activeUsers
dailyData.push
day: entry.day
value: entry.dailyCount / 1000
if entry.monthlyCount
monthlyData.push
day = entry.day
for event, count of entry.events
eventDataMap[event] ?= []
eventDataMap[event].push
day: entry.day
value: entry.monthlyCount / 1000
dausmausData.push
day: entry.day
value: Math.round(entry.dailyCount / entry.monthlyCount * 100)
dailyData.reverse()
monthlyData.reverse()
dausmausData.reverse()
dailyPoints = @createLineChartPoints(days, dailyData)
monthlyPoints = @createLineChartPoints(days, monthlyData)
dausmausPoints = @createLineChartPoints(days, dausmausData)
@activeUsersChartLines.push
points: dailyPoints
description: 'Daily active users (in thousands)'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(dailyPoints, 'y').y
showYScale: true
@activeUsersChartLines.push
points: monthlyPoints
description: 'Monthly active users (in thousands)'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(monthlyPoints, 'y').y
showYScale: true
@activeUsersChartLines.push
points: dausmausPoints
description: 'DAUs/MAUs %'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(dausmausPoints, 'y').y
showYScale: true
value: count
# Build chart lines for each event
eventLineMap =
'DAU campaign': {max: 0, colorIndex: 0}
'MAU campaign': {max: 0, colorIndex: 0}
'DAU classroom': {max: 0, colorIndex: 0}
'MAU classroom': {max: 0, colorIndex: 0}
for event, data of eventDataMap
data.reverse()
points = @createLineChartPoints(days, data)
max = _.max(points, 'y').y
if event.indexOf('DAU campaign') >= 0
chartLines = @campaignDailyActiveUsersChartLines
eventLineMap['DAU campaign'].max = Math.max(eventLineMap['DAU campaign'].max, max)
lineColor = @lineColors[eventLineMap['DAU campaign'].colorIndex++ % @lineColors.length]
else if event.indexOf('MAU campaign') >= 0
chartLines = @campaignMonthlyActiveUsersChartLines
eventLineMap['MAU campaign'].max = Math.max(eventLineMap['MAU campaign'].max, max)
lineColor = @lineColors[eventLineMap['MAU campaign'].colorIndex++ % @lineColors.length]
else if event.indexOf('DAU classroom') >= 0
chartLines = @classroomDailyActiveUsersChartLines
eventLineMap['DAU classroom'].max = Math.max(eventLineMap['DAU classroom'].max, max)
lineColor = @lineColors[eventLineMap['DAU classroom'].colorIndex++ % @lineColors.length]
else if event.indexOf('MAU classroom') >= 0
chartLines = @classroomMonthlyActiveUsersChartLines
eventLineMap['MAU classroom'].max = Math.max(eventLineMap['MAU classroom'].max, max)
lineColor = @lineColors[eventLineMap['MAU classroom'].colorIndex++ % @lineColors.length]
chartLines.push
points: points
description: event
lineColor: lineColor
strokeWidth: 1
min: 0
showYScale: false
# Update line Y scales and maxes
showYScaleSet = false
for line in @campaignDailyActiveUsersChartLines
line.max = eventLineMap['DAU campaign'].max
unless showYScaleSet
line.showYScale = true
showYScaleSet = true
showYScaleSet = false
for line in @campaignMonthlyActiveUsersChartLines
line.max = eventLineMap['MAU campaign'].max
unless showYScaleSet
line.showYScale = true
showYScaleSet = true
if line.description is 'MAU campaign paid'
@campaignVsClassroomMonthlyActiveUsersChartLines.push(_.cloneDeep(line))
showYScaleSet = false
for line in @classroomDailyActiveUsersChartLines
line.max = eventLineMap['DAU classroom'].max
unless showYScaleSet
line.showYScale = true
showYScaleSet = true
showYScaleSet = false
for line in @classroomMonthlyActiveUsersChartLines
line.max = eventLineMap['MAU classroom'].max
unless showYScaleSet
line.showYScale = true
showYScaleSet = true
if line.description is 'MAU classroom paid'
@campaignVsClassroomMonthlyActiveUsersChartLines.push(_.cloneDeep(line))
max = 0
for line in @campaignVsClassroomMonthlyActiveUsersChartLines
max = Math.max(_.max(line.points, 'y').y, max)
for line in @campaignVsClassroomMonthlyActiveUsersChartLines
line.max = max
line.showYScale = true if line.description is 'MAU campaign paid'
updateEnrollmentsChartData: ->
@enrollmentsChartLines = []

View file

@ -1,18 +1,21 @@
/* global ObjectId */
/* global db */
/* global printjson */
// Insert per-day active user counts into analytics.perdays collection
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
try {
logDB = new Mongo("localhost").getDB("analytics")
var logDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date();
var analyticsStringCache = {};
var numDays = 40;
var numDays = 30;
var daysInMonth = 30;
var startDay = new Date();
today = startDay.toISOString().substr(0, 10);
var today = startDay.toISOString().substr(0, 10);
startDay.setUTCDate(startDay.getUTCDate() - numDays);
startDay = startDay.toISOString().substr(0, 10);
@ -21,82 +24,206 @@ try {
log("Today is " + today);
log("Start day is " + startDay);
log("Getting active user counts...");
log("Getting active user counts..");
var activeUserCounts = getActiveUserCounts(startDay, activeUserEvents);
// printjson(activeUserCounts);
log("Inserting active user counts...");
for (day in activeUserCounts) {
log("Inserting active user counts..");
for (var day in activeUserCounts) {
if (today === day) continue; // Never save data for today because it's incomplete
for (event in activeUserCounts[day]) {
for (var event in activeUserCounts[day]) {
// print(day, '\t', event, '\t', activeUserCounts[day][event]);
insertEventCount(event, day, activeUserCounts[day][event]);
}
}
log("Script runtime: " + (new Date() - scriptStartTime));
}
catch(err) {
log("ERROR: " + err);
log("ERROR!");
printjson(err);
}
finally {
log("Script runtime: " + (new Date() - scriptStartTime));
}
function getActiveUserCounts(startDay, activeUserEvents) {
// Counts active users per day
if (!startDay) return {};
var cursor, doc;
log("Finding active user log events..");
var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
var queryParams = {$and: [
{_id: {$gte: startObj}},
{'event': {$in: activeUserEvents}}
]};
var cursor = logDB['log'].find(queryParams);
cursor = logDB['log'].find(queryParams);
var campaignUserMap = {};
var dayUserMap = {};
var userIDs = [];
while (cursor.hasNext()) {
var doc = cursor.next();
doc = cursor.next();
var created = doc._id.getTimestamp().toISOString();
var day = created.substring(0, 10);
var user = doc.user;
var user = doc.user.valueOf();
campaignUserMap[user] = true;
if (!dayUserMap[day]) dayUserMap[day] = {};
dayUserMap[day][user] = true;
userIDs.push(ObjectId(user));
}
print('User count: ', userIDs.length);
log("Finding classroom members..");
var classroomUserObjectIds = [];
var batchSize = 100000;
for (var j = 0; j < userIDs.length / batchSize + 1; j++) {
cursor = db.classrooms.find({members: {$in: userIDs.slice(j * batchSize, j * batchSize + batchSize)}}, {members: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (doc.members) {
for (var i = 0; i < doc.members.length; i++) {
var userID = doc.members[i].valueOf();
campaignUserMap[userID] = false;
classroomUserObjectIds.push(doc.members[i]);
}
}
}
}
log("Classroom user count: " + classroomUserObjectIds.length);
// Classrooms free/trial/paid
// Paid user: user.coursePrepaidID set means access to paid courses
// Trial user: prepaid.properties.trialRequestID means access was via trial
// Free: not paid, not trial
log("Finding classroom users free/trial/paid status..");
var userEventMap = {};
var prepaidUsersMap = {};
var prepaidIDs = [];
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaidID: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (doc.coursePrepaidID) {
userEventMap[doc._id.valueOf()] = 'DAU classroom paid';
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaidID);
}
else {
userEventMap[doc._id.valueOf()] = 'DAU classroom free';
}
}
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (doc.properties && doc.properties.trialRequestID) {
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
userEventMap[prepaidUsersMap[doc._id.valueOf()][i]] = 'DAU classroom trial';
}
}
}
// Campaign free/paid
// Monthly sub: recipient for payment.stripe.subscriptionID == 'basic'
// Yearly sub: recipient for paymen.stripe.gems == 42000
// TODO: missing a number of corner cases here (e.g. cancelled sub, purchased via admin)
var campaignUserIDs = [];
for (var userID in campaignUserMap) {
if (campaignUserMap[userID]) campaignUserIDs.push(ObjectId(userID));
}
log("Finding campaign paid users..");
var dayUserPaidMap = {};
batchSize = 100000;
for (var j = 0; j < campaignUserIDs.length / batchSize + 1; j++) {
cursor = db.payments.find({$and: [
{recipient: {$in: campaignUserIDs.slice(j * batchSize, j * batchSize + batchSize)}},
{$or: [
{'stripe.subscriptionID': 'basic'},
{gems: 42000}
]}
]}, {created: 1, gems: 1, recipient: 1});
while (cursor.hasNext()) {
doc = cursor.next();
var currentDate = new Date(doc.created || doc._id.getTimestamp());
userID = doc.recipient.valueOf();
var numDays = doc.gems === 42000 ? 365 : 30;
for (var i = 0; i < numDays; i++) {
day = currentDate.toISOString().substring(0, 10);
if (!dayUserPaidMap[day]) dayUserPaidMap[day] = {};
dayUserPaidMap[day][userID] = true;
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
}
}
}
var updateActiveUserCounts = function (activeUsersCounts, day, user, isMAU) {
var event = userEventMap[user];
if (!event) {
if (dayUserPaidMap[day] && dayUserPaidMap[day][user]) {
event = 'DAU campaign paid';
}
else {
event = 'DAU campaign free';
}
}
if (isMAU) event = event.replace('DAU', 'MAU');
if (!activeUsersCounts[day]) activeUsersCounts[day] = {};
if (!activeUsersCounts[day][event]) activeUsersCounts[day][event] = 0;
activeUsersCounts[day][event]++;
};
log("Calculating DAUs..");
var activeUsersCounts = {};
var monthlyActives = [];
for (day in dayUserMap) {
activeUsersCounts[day] = {'Daily Active Users': Object.keys(dayUserMap[day]).length};
for (var user in dayUserMap[day]) {
updateActiveUserCounts(activeUsersCounts, day, user, false);
}
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
log("Calculating MAUs..");
monthlyActives.sort(function (a, b) {return a.day.localeCompare(b.day);});
for (var i = daysInMonth - 1; i < monthlyActives.length; i++) {
var monthUserMap = {};
for (var j = i - daysInMonth + 1; j <= i; j++) {
day = monthlyActives[i].day;
for (var user in monthlyActives[j].users) {
monthUserMap[user] = true;
updateActiveUserCounts(activeUsersCounts, day, user, true);
}
}
activeUsersCounts[monthlyActives[i].day]['Monthly Active Users'] = Object.keys(monthUserMap).length;
}
// NOTE: analytics logging failure resulted in lost data for 2/2/16 through 2/9/16. Approximating those missing days here.
// Correction for a given event: previous week's value + previous week's diff from start to end if > 0
var missingDataDays = ['2016-02-02', '2016-02-03', '2016-02-04', '2016-02-05', '2016-02-06', '2016-02-07', '2016-02-08', '2016-02-09'];
for (var day in activeUsersCounts) {
if (missingDataDays.indexOf(day) >= 0) {
var prevDate = new Date(day + "T00:00:00.000Z");
prevDate.setUTCDate(prevDate.getUTCDate() - 7);
var prevStartDate = new Date(prevDate);
prevStartDate.setUTCDate(prevStartDate.getUTCDate() - 7);
var prevStartDay = prevStartDate.toISOString().substring(0, 10);
if (activeUsersCounts[prevStartDay]) {
var prevDay = prevDate.toISOString().substring(0, 10);
for (var event in activeUsersCounts[day]) {
var prevValue = activeUsersCounts[prevDay][event];
var prevStartValue = activeUsersCounts[prevStartDay][event];
var prevWeekDiff = Math.max(prevValue - prevStartValue, 0);
var betterValue = prevValue + prevWeekDiff;
activeUsersCounts[day][event] = betterValue;
// var currentValue = activeUsersCounts[day][event];
// print(prevStartDay, '\t', prevDay, '\t', prevValue, '-', prevStartValue, '\t', prevWeekDiff, '\t', day, '\t', event, '\t', prevValue, '\t', currentValue, '\t', betterValue);
}
}
}
}
return activeUsersCounts;
}
// *** Helper functions ***
function slugify(text)
// https://gist.github.com/mathewbyrne/1280286
{
return text.toString().toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, ''); // Trim - from end of text
}
function log(str) {
print(new Date().toISOString() + " " + str);
}
@ -152,7 +279,6 @@ function insertEventCount(event, day, count) {
var eventID = getAnalyticsString(event);
var filterID = getAnalyticsString('all');
var startObj = objectIdWithTimestamp(new Date(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;

View file

@ -52,26 +52,23 @@ class AnalyticsPerDayHandler extends Handler
@sendSuccess(res, activeClasses)
getActiveUsers: (req, res) ->
AnalyticsString.find({v: {$in: ['Daily Active Users', 'Monthly Active Users']}}).exec (err, documents) =>
events = ['DAU classroom paid', 'DAU classroom trial', 'DAU classroom free', 'DAU campaign paid', 'DAU campaign free',
'MAU classroom paid', 'MAU classroom trial', 'MAU classroom free', 'MAU campaign paid', 'MAU campaign free']
AnalyticsString.find({v: {$in: events}}).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
eventIDs = []
eventStringMap = {}
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?
eventIDs.push(doc._id)
eventStringMap[doc._id] = doc.v
AnalyticsPerDay.find({e: {$in: [dailyID, monthlyID]}}).exec (err, documents) =>
AnalyticsPerDay.find({e: {$in: eventIDs}}).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
dayCountsMap[doc.d][eventStringMap[doc.e]] = doc.c
activeUsers = ({day: day, events: eventCountMap} for day, eventCountMap of dayCountsMap)
@sendSuccess(res, activeUsers)
getCampaignCompletionsBySlug: (req, res) ->