mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
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:
parent
95e627f346
commit
e8c22679d9
3 changed files with 108 additions and 257 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) =>
|
||||
|
|
Loading…
Reference in a new issue