Add trials to active classes analytics dashboard

Only looking at active classrooms, split into paid, trial, and free.
Active class: 12+ students total, 6+ active in last 30 days.
This commit is contained in:
Matt Lott 2016-02-04 16:31:47 -08:00
parent 95e627f346
commit e8c22679d9
3 changed files with 108 additions and 257 deletions

View file

@ -27,6 +27,7 @@ block content
.kpi-chart.line-chart-container
h3 Active Classes 90 days
.small Active class: 12+ students in a classroom, with 6+ who played in last 30 days.
.active-classes-chart.line-chart-container
h3 Recurring Revenue 90 days

View file

@ -1,31 +1,42 @@
/* global db */
/* global Mongo */
/* global ISODate */
// Insert per-day active class 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 minClassSize = 12;
var minActiveCount = 6;
var eventNamePaid = 'Active classes paid';
var eventNameTrial = 'Active classes trial';
var eventNameFree = 'Active classes free';
var numDays = 40;
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);
log("Today is " + today);
log("Start day is " + startDay);
log("Getting active class counts...");
log("Getting active class counts..");
var activeClassCounts = getActiveClassCounts(startDay);
// printjson(activeClassCounts);
log("Inserting active class counts...");
log("Inserting active class counts..");
for (var event in activeClassCounts) {
for (var day in activeClassCounts[event]) {
if (today === day) continue; // Never save data for today because it's incomplete
// print(event, day, activeClassCounts[event][day]);
insertEventCount(event, day, activeClassCounts[event][day]);
}
}
@ -38,130 +49,73 @@ catch(err) {
}
function getActiveClassCounts(startDay) {
// Tally active classes per day
// Tally active classes per day, for paid, trial, and free
// TODO: does not handle class membership changes
if (!startDay) return {};
var minGroupSize = 12;
var classes = {
'Active classes private clan': [],
'Active classes managed subscription': [],
'Active classes bulk subscription': [],
'Active classes prepaid': [],
'Active classes course free': [],
'Active classes course paid': []
};
var userPlayedMap = {};
// Private clans
// TODO: does not handle clan membership changes over time
var cursor = db.clans.find({$and: [{type: 'private'}, {$where: 'this.members.length >= ' + minGroupSize}]});
while (cursor.hasNext()) {
var doc = cursor.next();
var members = doc.members.map(function(a) {
userPlayedMap[a.valueOf()] = [];
return a.valueOf();
});
classes['Active classes private clan'].push({
owner: doc.ownerID.valueOf(),
members: members,
activeDayMap: {}
});
}
// Managed subscriptions
// TODO: does not handle former recipients playing after sponsorship ends
var bulkSubGroups = {};
cursor = db.payments.find({$and: [{service: 'stripe'}, {$where: '!this.purchaser.equals(this.recipient)'}]});
while (cursor.hasNext()) {
var doc = cursor.next();
var purchaser = doc.purchaser.valueOf();
if (!bulkSubGroups[purchaser]) bulkSubGroups[purchaser] = {};
bulkSubGroups[purchaser][doc.recipient.valueOf()] = true;
}
for (var purchaser in bulkSubGroups) {
if (Object.keys(bulkSubGroups[purchaser]).length >= minGroupSize) {
for (var member in bulkSubGroups[purchaser]) {
userPlayedMap[member] = [];
}
classes['Active classes managed subscription'].push({
owner: purchaser,
members: Object.keys(bulkSubGroups[purchaser]),
activeDayMap: {}
});
}
}
// Bulk subscriptions
bulkSubGroups = {};
cursor = db.payments.find({$and: [{service: 'external'}, {$where: '!this.purchaser.equals(this.recipient)'}]});
while (cursor.hasNext()) {
var doc = cursor.next();
var purchaser = doc.purchaser.valueOf();
if (!bulkSubGroups[purchaser]) bulkSubGroups[purchaser] = {};
bulkSubGroups[purchaser][doc.recipient.valueOf()] = true;
}
for (var purchaser in bulkSubGroups) {
if (Object.keys(bulkSubGroups[purchaser]).length >= minGroupSize) {
for (var member in bulkSubGroups[purchaser]) {
userPlayedMap[member] = [];
}
classes['Active classes bulk subscription'].push({
owner: purchaser,
members: Object.keys(bulkSubGroups[purchaser]),
activeDayMap: {}
});
}
}
// Prepaids terminal_subscription
bulkSubGroups = {};
cursor = db.prepaids.find(
{$and: [{type: 'terminal_subscription'}, {$where: 'this.redeemers && this.redeemers.length >= ' + minGroupSize}]},
{creator: 1, type: 1, redeemers: 1}
);
while (cursor.hasNext()) {
var doc = cursor.next();
var owner = doc.creator.valueOf();
var members = [];
for (var i = 0 ; i < doc.redeemers.length; i++) {
userPlayedMap[doc.redeemers[i].userID.valueOf()] = [];
members.push(doc.redeemers[i].userID.valueOf());
}
classes['Active classes prepaid'].push({
owner: owner,
members: members,
activeDayMap: {}
});
}
var cursor, doc;
// Classrooms
var classroomCourseInstancesMap = {};
cursor = db.course.instances.find(
{$where: 'this.members && this.members.length >= ' + minGroupSize},
{classroomID: 1, courseID: 1, members: 1, ownerID: 1}
);
// paid: at least one paid member
// free trial: not paid, at least one trial member
// free: not paid, not free trial
// user.coursePrepaidID set means access to paid courses
// prepaid.properties.trialRequestID means access was via trial
// Find classroom users
log("Finding classrooms..");
var userClassroomsMap = {};
var classroomUsersMap = {};
var classroomUserIDs = [];
var classroomUserObjectIds = [];
cursor = db.classrooms.find({}, {members: 1});
while (cursor.hasNext()) {
var doc = cursor.next();
var owner = doc.ownerID.valueOf();
var classroom = doc.classroomID ? doc.classroomID.valueOf() : doc._id.valueOf();
var members = [];
doc = cursor.next();
if (doc.members) {
var classroomID = doc._id.valueOf();
for (var i = 0; i < doc.members.length; i++) {
userPlayedMap[doc.members[i].valueOf()] = [];
members.push(doc.members[i].valueOf());
if (doc.members.length < minClassSize) continue;
var userID = doc.members[i].valueOf();
if (!userClassroomsMap[userID]) userClassroomsMap[userID] = [];
userClassroomsMap[userID].push(classroomID);
if (!classroomUsersMap[classroomID]) classroomUsersMap[classroomID] = [];
classroomUsersMap[classroomID].push(userID)
classroomUserIDs.push(doc.members[i].valueOf());
classroomUserObjectIds.push(doc.members[i]);
}
}
if (!classroomCourseInstancesMap[classroom]) classroomCourseInstancesMap[classroom] = [];
classroomCourseInstancesMap[classroom].push({
course: doc.courseID.valueOf(),
owner: owner,
members: members,
});
}
// printjson(classroomCourseInstancesMap);
log("Find user types..");
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()] = eventNamePaid;
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()] = eventNameFree;
}
}
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]] = eventNameTrial;
}
}
}
// Find all the started level events for our class members, for startDay - daysInMonth
log("Find Started Level log events for all classroom members for last " + (numDays + daysInMonth) + " days..");
var userPlayedMap = {};
var startDate = ISODate(startDay + "T00:00:00.000Z");
startDate.setUTCDate(startDate.getUTCDate() - daysInMonth);
var endDate = ISODate(startDay + "T00:00:00.000Z");
@ -169,162 +123,72 @@ function getActiveClassCounts(startDay) {
var startObj = objectIdWithTimestamp(startDate);
var queryParams = {$and: [
{_id: {$gte: startObj}},
{user: {$in: Object.keys(userPlayedMap)}},
{user: {$in: classroomUserIDs}},
{event: 'Started Level'}
]};
cursor = logDB['log'].find(queryParams, {user: 1});
while (cursor.hasNext()) {
var doc = cursor.next();
doc = cursor.next();
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
userPlayedMap[doc.user].push(doc._id.getTimestamp());
}
// printjson(userPlayedMap);
// print(startDate, endDate, todayDate);
// Now we have a set of classes, and when users played
// For a given day, walk classes and find out how many members were active during the previous daysInMonth
while (endDate < todayDate) {
var endDay = endDate.toISOString().substring(0, 10);
// For each class
for (var event in classes) {
for (var i = 0; i < classes[event].length; i++) {
// For each member of current class
var activeMemberCount = 0;
for (var j = 0; j < classes[event][i].members.length; j++) {
var member = classes[event][i].members[j];
// Was member active during current timeframe?
if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
activeMemberCount++;
break;
}
}
}
}
// Classes active for a given day if has minGroupSize members, and at least 1/2 played in last daysInMonth days
if (activeMemberCount >= Math.round(classes[event][i].members.length / 2)) {
classes[event][i].activeDayMap[endDay] = true;
}
}
}
startDate.setUTCDate(startDate.getUTCDate() + 1);
endDate.setUTCDate(endDate.getUTCDate() + 1);
}
// Classrooms are processed differently because they could be free or paid active classes
var courseNameMap = {};
cursor = db.courses.find({}, {name: 1});
while (cursor.hasNext()) {
var doc = cursor.next();
courseNameMap[doc._id.valueOf()] = doc.name;
}
// For each classroom, check free and paid members separately
for (var classroom in classroomCourseInstancesMap) {
var freeMembers = {};
var paidMembers = {};
var owner = null;
for (var i = 0; i < classroomCourseInstancesMap[classroom].length; i++) {
var courseInstance = classroomCourseInstancesMap[classroom][i];
if (!owner) owner = courseInstance.owner;
for (var j = 0; j < courseInstance.members.length; j++) {
if (courseNameMap[courseInstance.course] === 'Introduction to Computer Science') {
freeMembers[courseInstance.members[j]] = true;
}
else {
paidMembers[courseInstance.members[j]] = true;
}
}
}
var freeClass = {
owner: owner,
members: Object.keys(freeMembers),
activeDayMap: {}
};
var paidClass = {
owner: owner,
members: Object.keys(paidMembers),
activeDayMap: {}
};
// print('Processing classroom', classroom, freeClass.members.length, paidClass.members.length);
log("Calculate number of active members per classroom per day per event type..");
var classDayTypeMap = {};
for (var classroom in classroomUsersMap) {
if (classroomUsersMap[classroom].length < minClassSize) continue;
// For each each day in our target date range
classDayTypeMap[classroom] = {};
startDate = ISODate(startDay + "T00:00:00.000Z");
startDate.setUTCDate(startDate.getUTCDate() - daysInMonth);
endDate = ISODate(startDay + "T00:00:00.000Z");
while (endDate < todayDate) {
var endDay = endDate.toISOString().substring(0, 10);
classDayTypeMap[classroom][endDay] = {};
classDayTypeMap[classroom][endDay][eventNamePaid] = 0;
classDayTypeMap[classroom][endDay][eventNameTrial] = 0;
classDayTypeMap[classroom][endDay][eventNameFree] = 0;
// For each paid member of current class
var paidActiveMemberCount = 0;
for (var j = 0; j < paidClass.members.length; j++) {
var member = paidClass.members[j];
// Count active users of each type for current day
for (var j = 0; j < classroomUsersMap[classroom].length; j++) {
var member = classroomUsersMap[classroom][j];
// Was member active during current timeframe?
if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
paidActiveMemberCount++;
classDayTypeMap[classroom][endDay][userEventMap[member]]++;
break;
}
}
}
}
// Classes active for a given day if has minGroupSize members, and at least 1/2 played in last daysInMonth days
if (paidClass.members.length > minGroupSize && paidActiveMemberCount >= Math.round(paidClass.members.length / 2)) {
// print('paid classroom', classroom, endDay);
paidClass.activeDayMap[endDay] = true;
}
else {
// For each free member of current class
var freeActiveMemberCount = 0;
for (var j = 0; j < freeClass.members.length; j++) {
var member = freeClass.members[j];
// Was member active during current timeframe?
if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
freeActiveMemberCount++;
break;
}
}
}
}
if (freeClass.members.length > minGroupSize && freeActiveMemberCount >= Math.round(freeClass.members.length / 2)) {
// print('free classroom', classroom, endDay);
freeClass.activeDayMap[endDay] = true;
}
}
startDate.setUTCDate(startDate.getUTCDate() + 1);
endDate.setUTCDate(endDate.getUTCDate() + 1);
}
// printjson(freeClass);
// printjson(paidClass);
classes['Active classes course free'].push(freeClass);
classes['Active classes course paid'].push(paidClass);
}
// printjson(classes['Active classes course paid']);
log("Aggregate class counts by day and type..");
var activeClassCounts = {};
for (var event in classes) {
if (!activeClassCounts[event]) activeClassCounts[event] = {};
for (var i = 0; i < classes[event].length; i++) {
for (var endDay in classes[event][i].activeDayMap) {
if (!activeClassCounts[event][endDay]) activeClassCounts[event][endDay] = 0;
activeClassCounts[event][endDay]++;
for (var classroom in classDayTypeMap) {
for (var endDay in classDayTypeMap[classroom]) {
var activeStudents = 0;
var classEvent = eventNameFree;
for (var event in classDayTypeMap[classroom][endDay]) {
if (classDayTypeMap[classroom][endDay][event] > 1) {
activeStudents += classDayTypeMap[classroom][endDay][event];
if (event === eventNamePaid) classEvent = event;
if (classEvent !== eventNamePaid && event === eventNameTrial) classEvent = event;
}
}
if (activeStudents >= minActiveCount) {
if (!activeClassCounts[classEvent]) activeClassCounts[classEvent] = {};
if (!activeClassCounts[classEvent][endDay]) activeClassCounts[classEvent][endDay] = 0;
activeClassCounts[classEvent][endDay]++;
}
}
}
@ -334,17 +198,6 @@ function getActiveClassCounts(startDay) {
// *** 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);
}

View file

@ -26,12 +26,9 @@ class AnalyticsPerDayHandler extends Handler
getActiveClasses: (req, res) ->
events = [
'Active classes private clan',
'Active classes managed subscription',
'Active classes bulk subscription',
'Active classes prepaid',
'Active classes course free',
'Active classes course paid'
'Active classes paid',
'Active classes trial',
'Active classes free'
]
AnalyticsString.find({v: {$in: events}}).exec (err, documents) =>