Merge branch 'master' into production

This commit is contained in:
Nick Winter 2016-07-11 10:44:13 -07:00
commit dbc99ba52b
8 changed files with 275 additions and 251 deletions

View file

@ -267,7 +267,7 @@ LevelSchema = c.object {
victory: {} victory: {}
type: 'hero' type: 'hero'
goals: [ goals: [
{id: 'ogres-die', name: 'Ogres must die.', killThangs: ['ogres'], worldEndsAfter: 3} {id: 'ogres-die', name: 'Defeat the ogres.', killThangs: ['ogres'], worldEndsAfter: 3}
{id: 'humans-survive', name: 'Your hero must survive.', saveThangs: ['Hero Placeholder'], howMany: 1, worldEndsAfter: 3, hiddenGoal: true} {id: 'humans-survive', name: 'Your hero must survive.', saveThangs: ['Hero Placeholder'], howMany: 1, worldEndsAfter: 3, hiddenGoal: true}
] ]
concepts: ['basic_syntax'] concepts: ['basic_syntax']

View file

@ -267,9 +267,9 @@ block content
h1 Active Users h1 Active Users
if view.activeUsers.length > 0 if view.activeUsers.length > 0
- var eventNames = []; - var eventNames = [];
each count, event in view.activeUsers[0].events each event in view.activeUserEventNames
if event.indexOf('classroom') >= 0 if event.indexOf('classroom') >= 0
- eventNames.push(event) - eventNames.push(event);
- eventNames.sort(function (a, b) {return a.localeCompare(b);}); - eventNames.sort(function (a, b) {return a.localeCompare(b);});
table.table.table-striped.table-condensed table.table.table-striped.table-condensed
tr tr
@ -322,9 +322,9 @@ block content
h1 Active Users h1 Active Users
if view.activeUsers.length > 0 if view.activeUsers.length > 0
- var eventNames = []; - var eventNames = [];
each count, event in view.activeUsers[0].events each event in view.activeUserEventNames
if event.indexOf('campaign') >= 0 if event.indexOf('campaign') >= 0
- eventNames.push(event) - eventNames.push(event);
- eventNames.sort(function (a, b) {return a.localeCompare(b);}); - eventNames.sort(function (a, b) {return a.localeCompare(b);});
table.table.table-striped.table-condensed table.table.table-striped.table-condensed
tr tr
@ -346,27 +346,13 @@ block content
h1 Active Users h1 Active Users
if view.activeUsers.length > 0 if view.activeUsers.length > 0
- var eventNames = [];
each count, event in view.activeUsers[0].events
- eventNames.push(event)
- eventNames.sort(function (a, b) {
- if (a.indexOf('campaign') == b.indexOf('campaign') || a.indexOf('classroom') == b.indexOf('classroom')) {
- return a.localeCompare(b);
- }
- else if (a.indexOf('campaign') > b.indexOf('campaign')) {
- return 1;
- }
- else {
- return -1;
- }
- });
table.table.table-striped.table-condensed table.table.table-striped.table-condensed
tr tr
th(style='min-width:85px;') Day th(style='min-width:85px;') Day
each eventName in eventNames each eventName in view.activeUserEventNames
th= eventName th= eventName
each activeUser in view.activeUsers each activeUser in view.activeUsers
tr tr
td= activeUser.day td= activeUser.day
each eventName in eventNames each eventName in view.activeUserEventNames
td= activeUser.events[eventName] || 0 td= activeUser.events[eventName] || 0

View file

@ -73,6 +73,7 @@ module.exports = class AnalyticsView extends RootView
# Add campaign/classroom DAU 30-day averages and daily totals # Add campaign/classroom DAU 30-day averages and daily totals
campaignDauTotals = [] campaignDauTotals = []
classroomDauTotals = [] classroomDauTotals = []
eventMap = {}
for entry in @activeUsers for entry in @activeUsers
day = entry.day day = entry.day
campaignDauTotal = 0 campaignDauTotal = 0
@ -82,18 +83,31 @@ module.exports = class AnalyticsView extends RootView
campaignDauTotal += count campaignDauTotal += count
else if event.indexOf('DAU classroom') >= 0 else if event.indexOf('DAU classroom') >= 0
classroomDauTotal += count classroomDauTotal += count
eventMap[event] = true;
entry.events['DAU campaign total'] = campaignDauTotal entry.events['DAU campaign total'] = campaignDauTotal
eventMap['DAU campaign total'] = true;
campaignDauTotals.unshift(campaignDauTotal) campaignDauTotals.unshift(campaignDauTotal)
campaignDauTotals.pop() while campaignDauTotals.length > 30 campaignDauTotals.pop() while campaignDauTotals.length > 30
if campaignDauTotals.length is 30 if campaignDauTotals.length is 30
entry.events['DAU campaign 30-day average'] = Math.round(_.reduce(campaignDauTotals, (a, b) -> a + b) / 30) entry.events['DAU campaign 30-day average'] = Math.round(_.reduce(campaignDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU campaign 30-day average'] = true;
entry.events['DAU classroom total'] = classroomDauTotal entry.events['DAU classroom total'] = classroomDauTotal
eventMap['DAU classroom total'] = true;
classroomDauTotals.unshift(classroomDauTotal) classroomDauTotals.unshift(classroomDauTotal)
classroomDauTotals.pop() while classroomDauTotals.length > 30 classroomDauTotals.pop() while classroomDauTotals.length > 30
if classroomDauTotals.length is 30 if classroomDauTotals.length is 30
entry.events['DAU classroom 30-day average'] = Math.round(_.reduce(classroomDauTotals, (a, b) -> a + b) / 30) entry.events['DAU classroom 30-day average'] = Math.round(_.reduce(classroomDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU classroom 30-day average'] = true;
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day) @activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
@activeUserEventNames = Object.keys(eventMap)
@activeUserEventNames.sort (a, b) ->
if a.indexOf('campaign') is b.indexOf('campaign') or a.indexOf('classroom') is b.indexOf('classroom')
a.localeCompare(b)
else if a.indexOf('campaign') > b.indexOf('campaign')
1
else
-1
@updateAllKPIChartData() @updateAllKPIChartData()
@updateActiveUsersChartData() @updateActiveUsersChartData()
@ -183,7 +197,7 @@ module.exports = class AnalyticsView extends RootView
@supermodel.addRequestResource({ @supermodel.addRequestResource({
url: '/db/prepaid/-/courses' url: '/db/prepaid/-/courses'
method: 'POST' method: 'POST'
data: {project: {maxRedeemers: 1, properties: 1, redeemers: 1}} data: {project: {endDate: 1, maxRedeemers: 1, properties: 1, redeemers: 1}}
success: (prepaids) => success: (prepaids) =>
paidDayMaxMap = {} paidDayMaxMap = {}
paidDayRedeemedMap = {} paidDayRedeemedMap = {}
@ -201,14 +215,13 @@ module.exports = class AnalyticsView extends RootView
redeemDay = redeemer.date.substring(0, 10) redeemDay = redeemer.date.substring(0, 10)
trialDayRedeemedMap[redeemDay] ?= 0 trialDayRedeemedMap[redeemDay] ?= 0
trialDayRedeemedMap[redeemDay]++ trialDayRedeemedMap[redeemDay]++
else else if not prepaid.endDate? or new Date(prepaid.endDate) > new Date()
paidDayMaxMap[day] ?= 0 paidDayMaxMap[day] ?= 0
paidDayMaxMap[day] += prepaid.maxRedeemers paidDayMaxMap[day] += prepaid.maxRedeemers
for redeemer in prepaid.redeemers for redeemer in prepaid.redeemers
redeemDay = redeemer.date.substring(0, 10) redeemDay = redeemer.date.substring(0, 10)
paidDayRedeemedMap[redeemDay] ?= 0 paidDayRedeemedMap[redeemDay] ?= 0
paidDayRedeemedMap[redeemDay]++ paidDayRedeemedMap[redeemDay]++
@dayEnrollmentsMap = {} @dayEnrollmentsMap = {}
@paidCourseTotalEnrollments = [] @paidCourseTotalEnrollments = []
for day, count of paidDayMaxMap for day, count of paidDayMaxMap
@ -368,6 +381,9 @@ module.exports = class AnalyticsView extends RootView
# Trim points preceding days # Trim points preceding days
if points.length and days.length and points[0].day.localeCompare(days[0]) < 0 if points.length and days.length and points[0].day.localeCompare(days[0]) < 0
if points[points.length - 1].day.localeCompare(days[0]) < 0
points = []
else
for point, i in points for point, i in points
if point.day.localeCompare(days[0]) >= 0 if point.day.localeCompare(days[0]) >= 0
points.splice(0, i) points.splice(0, i)
@ -375,7 +391,7 @@ module.exports = class AnalyticsView extends RootView
# Ensure points for each day # Ensure points for each day
for day, i in days for day, i in days
if points.length <= i or points[i].day isnt day if points.length <= i or points[i]?.day isnt day
prevY = if i > 0 then points[i - 1].y else 0.0 prevY = if i > 0 then points[i - 1].y else 0.0
points.splice i, 0, points.splice i, 0,
day: day day: day
@ -648,16 +664,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = 0 dailyMax = 0
data = [] data = []
total = 0
for entry in @paidCourseTotalEnrollments for entry in @paidCourseTotalEnrollments
total += entry.count
data.push data.push
day: entry.day day: entry.day
value: total value: entry.count
points = @createLineChartPoints(days, data) points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push @enrollmentsChartLines.push
points: points points: points
description: 'Total paid enrollments issued' description: 'Paid enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length] lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1 strokeWidth: 1
min: 0 min: 0
@ -666,16 +680,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = _.max([dailyMax, _.max(points, 'y').y]) dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = [] data = []
total = 0
for entry in @paidCourseRedeemedEnrollments for entry in @paidCourseRedeemedEnrollments
total += entry.count
data.push data.push
day: entry.day day: entry.day
value: total value: entry.count
points = @createLineChartPoints(days, data) points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push @enrollmentsChartLines.push
points: points points: points
description: 'Total paid enrollments redeemed' description: 'Paid enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length] lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1 strokeWidth: 1
min: 0 min: 0
@ -684,16 +696,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = _.max([dailyMax, _.max(points, 'y').y]) dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = [] data = []
total = 0
for entry in @trialCourseTotalEnrollments for entry in @trialCourseTotalEnrollments
total += entry.count
data.push data.push
day: entry.day day: entry.day
value: total value: entry.count
points = @createLineChartPoints(days, data) points = @createLineChartPoints(days, data, true)
@enrollmentsChartLines.push @enrollmentsChartLines.push
points: points points: points
description: 'Total trial enrollments issued' description: 'Trial enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length] lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1 strokeWidth: 1
min: 0 min: 0
@ -702,16 +712,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = _.max([dailyMax, _.max(points, 'y').y]) dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = [] data = []
total = 0
for entry in @trialCourseRedeemedEnrollments for entry in @trialCourseRedeemedEnrollments
total += entry.count
data.push data.push
day: entry.day day: entry.day
value: total value: entry.count
points = @createLineChartPoints(days, data) points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push @enrollmentsChartLines.push
points: points points: points
description: 'Total trial enrollments redeemed' description: 'Trial enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length] lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1 strokeWidth: 1
min: 0 min: 0

View file

@ -31,31 +31,10 @@ module.exports = class MainAdminView extends RootView
getTitle: -> return $.i18n.t('account_settings.admin') getTitle: -> return $.i18n.t('account_settings.admin')
initialize: -> initialize: ->
@campaigns = new Campaigns()
@courses = new CocoCollection([], { url: "/db/course", model: Course})
if window.amActually if window.amActually
@amActually = new User({_id: window.amActually}) @amActually = new User({_id: window.amActually})
@amActually.fetch() @amActually.fetch()
@supermodel.trackModel(@amActually) @supermodel.trackModel(@amActually)
if me.isAdmin()
@supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels' } })
@supermodel.loadCollection(@courses, 'courses')
super()
onLoaded: ->
campaignCourseIndexMap = {}
for course, index in @courses.models
campaignCourseIndexMap[course.get('campaignID')] = index + 1
@courseLevels = []
for campaign in @campaigns.models
continue unless campaignCourseIndexMap[campaign.id]
for levelID, level of campaign.get('levels')
@courseLevels.push({
levelID
slug: level.slug
courseIndex: campaignCourseIndexMap[campaign.id]
})
super() super()
onClickStopSpyingButton: -> onClickStopSpyingButton: ->
@ -157,42 +136,31 @@ module.exports = class MainAdminView extends RootView
@supermodel.addRequestResource('create_prepaid', options, 0).load() @supermodel.addRequestResource('create_prepaid', options, 0).load()
onClickExportProgress: -> onClickExportProgress: ->
return unless @courseLevels?.length > 0
$('.classroom-progress-csv').prop('disabled', true) $('.classroom-progress-csv').prop('disabled', true)
classCode = $('.classroom-progress-class-code').val() classCode = $('.classroom-progress-class-code').val()
classroom = null
courseLevels = []
sessions = null
users = null
userMap = {} userMap = {}
new Promise((resolve, reject) => Promise.resolve(new Classroom().fetchByCode(classCode))
new Classroom().fetchByCode(classCode, { .then (model) =>
success: resolve classroom = new Classroom({ _id: model.data._id })
error: (model, response, options) => reject(response) Promise.resolve(classroom.fetch())
}) .then (model) =>
) for course, index in classroom.get('courses')
.then (classroom) => for level in course.levels
new Promise((resolve, reject) => courseLevels.push
new Classroom({ _id: classroom.id }).fetch({ courseIndex: index + 1
success: resolve levelID: level.original
error: (model, response, options) => reject(response) slug: level.slug
}) users = new Users()
) Promise.resolve($.when(users.fetchForClassroom(classroom)...))
.then (classroom) => .then (models) =>
new Promise((resolve, reject) =>
new Users().fetchForClassroom(classroom, {
success: (models, response, options) =>
resolve([classroom, models]) if models?.loaded
error: (models, response, options) => reject(response)
})
)
.then ([classroom, users]) =>
userMap[user.id] = user for user in users.models userMap[user.id] = user for user in users.models
new Promise((resolve, reject) => sessions = new LevelSessions()
new LevelSessions().fetchForAllClassroomMembers(classroom, { Promise.resolve($.when(sessions.fetchForAllClassroomMembers(classroom)...))
success: (models, response, options) => .then (models) =>
resolve(models) if models?.loaded
error: (models, response, options) => reject(response)
})
)
.then (sessions) =>
userLevelPlaytimeMap = {} userLevelPlaytimeMap = {}
for session in sessions.models for session in sessions.models
continue unless session.get('state')?.complete continue unless session.get('state')?.complete
@ -205,7 +173,7 @@ module.exports = class MainAdminView extends RootView
userPlaytimes = [] userPlaytimes = []
for userID, user of userMap for userID, user of userMap
playtimes = [user.get('name') ? 'Anonymous'] playtimes = [user.get('name') ? 'Anonymous']
for level in @courseLevels for level in courseLevels
if userLevelPlaytimeMap[userID]?[level.levelID]? if userLevelPlaytimeMap[userID]?[level.levelID]?
rawSeconds = parseInt(userLevelPlaytimeMap[userID][level.levelID]) rawSeconds = parseInt(userLevelPlaytimeMap[userID][level.levelID])
hours = Math.floor(rawSeconds / 60 / 60) hours = Math.floor(rawSeconds / 60 / 60)
@ -222,7 +190,7 @@ module.exports = class MainAdminView extends RootView
columnLabels = "Username" columnLabels = "Username"
currentLevel = 1 currentLevel = 1
lastCourseIndex = 1 lastCourseIndex = 1
for level in @courseLevels for level in courseLevels
unless level.courseIndex is lastCourseIndex unless level.courseIndex is lastCourseIndex
currentLevel = 1 currentLevel = 1
lastCourseIndex = level.courseIndex lastCourseIndex = level.courseIndex
@ -238,3 +206,4 @@ module.exports = class MainAdminView extends RootView
.catch (error) -> .catch (error) ->
$('.classroom-progress-csv').prop('disabled', false) $('.classroom-progress-csv').prop('disabled', false)
console.error error console.error error
throw error

View file

@ -11,7 +11,7 @@ module.exports = class SegmentCheckView extends CocoView
events: events:
'click .back-to-account-type': -> @trigger 'nav-back' 'click .back-to-account-type': -> @trigger 'nav-back'
'input .class-code-input': 'onInputClassCode' 'input .class-code-input': 'onInputClassCode'
'input .birthday-form-group': 'onInputBirthday' 'change .birthday-form-group': 'onInputBirthday'
'submit form.segment-check': 'onSubmitSegmentCheck' 'submit form.segment-check': 'onSubmitSegmentCheck'
'click .individual-path-button': -> @trigger 'choose-path', 'individual' 'click .individual-path-button': -> @trigger 'choose-path', 'individual'

View file

@ -275,8 +275,7 @@ module.exports = class SpellView extends CocoView
e.editor.execCommand 'gotolineend' e.editor.execCommand 'gotolineend'
return true return true
if me.level() < 20 or aceConfig.indentGuides # Add visual indent guides
# Add visual ident guides
language = @spell.language language = @spell.language
ensureLineStartsBlock = (line) -> ensureLineStartsBlock = (line) ->
return false unless language is "python" return false unless language is "python"

View file

@ -6,51 +6,49 @@
// Usage: // Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password> // mongo <address>:<port>/<database> <script file> -u <username> -p <password>
try { // TODO: Does not handle course prepaid updates on a user
var logDB = new Mongo("localhost").getDB("analytics") // TODO: Does not handle class membership changes
var scriptStartTime = new Date();
var analyticsStringCache = {};
var minClassSize = 12; // TODO: Investigate abrupt trial drop off at 4/1/16. Showed up when fixing coursePrepaid.
var minActiveCount = 6;
var eventNamePaid = 'Active classes paid'; var analyticsDB = new Mongo("localhost").getDB("analytics")
var eventNameTrial = 'Active classes trial'; var scriptStartTime = new Date();
var eventNameFree = 'Active classes free'; var analyticsStringCache = {};
var numDays = 40; var minClassSize = 12;
var daysInMonth = 30; var minActiveCount = 6;
var startDay = new Date(); var eventNamePaid = 'Active classes paid';
var today = startDay.toISOString().substr(0, 10); var eventNameTrial = 'Active classes trial';
startDay.setUTCDate(startDay.getUTCDate() - numDays); var eventNameFree = 'Active classes free';
startDay = startDay.toISOString().substr(0, 10);
log("Today is " + today); var numDays = 40;
log("Start day is " + startDay); var daysInMonth = 30;
log("Getting active class counts.."); var startDay = new Date();
var activeClassCounts = getActiveClassCounts(startDay); var today = startDay.toISOString().substr(0, 10);
// printjson(activeClassCounts); startDay.setUTCDate(startDay.getUTCDate() - numDays);
log("Inserting active class counts.."); startDay = startDay.toISOString().substr(0, 10);
for (var event in activeClassCounts) {
log("Today is " + today);
log("Start day is " + startDay);
log("Getting active class counts..");
var activeClassCounts = getActiveClassCounts(startDay);
// printjson(activeClassCounts);
// log("Inserting active class counts..");
for (var event in activeClassCounts) {
for (var day in activeClassCounts[event]) { for (var day in activeClassCounts[event]) {
if (today === day) continue; // Never save data for today because it's incomplete if (today === day) continue; // Never save data for today because it's incomplete
// print(event, day, activeClassCounts[event][day]); // print(event, day, activeClassCounts[event][day]);
insertEventCount(event, day, activeClassCounts[event][day]); insertEventCount(event, day, activeClassCounts[event][day]);
} }
} }
log("Script runtime: " + (new Date() - scriptStartTime)); log("Script runtime: " + (new Date() - scriptStartTime));
}
catch(err) {
log("ERROR: " + err);
printjson(err);
}
function getActiveClassCounts(startDay) { function getActiveClassCounts(startDay) {
// Tally active classes per day, for paid, trial, and free // Tally active classes per day, for paid, trial, and free
// TODO: does not handle class membership changes
if (!startDay) return {}; if (!startDay) return {};
@ -60,7 +58,7 @@ function getActiveClassCounts(startDay) {
// paid: at least one paid member // paid: at least one paid member
// trial: not paid, at least one trial member // trial: not paid, at least one trial member
// free: not paid, not free trial // free: not paid, not free trial
// user.coursePrepaidID set means access to paid courses // user.coursePrepaidID or user.coursePrepaid set means access to paid courses
// prepaid.properties.trialRequestID means access was via trial // prepaid.properties.trialRequestID means access was via trial
// Find classroom users // Find classroom users
@ -86,30 +84,46 @@ function getActiveClassCounts(startDay) {
} }
} }
} }
// log("DEBUG: Classroom users: " + classroomUserIDs.length);
log("Find user types.."); log("Find user types..");
var userEventMap = {}; var userEventEndDateMap = {};
var prepaidUsersMap = {}; var prepaidUsersMap = {};
var prepaidIDs = []; var prepaidIDs = [];
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaidID: 1}); cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaid: 1, coursePrepaidID: 1});
while (cursor.hasNext()) { while (cursor.hasNext()) {
doc = cursor.next(); doc = cursor.next();
userEventEndDateMap[doc._id.valueOf()] = {};
userEventEndDateMap[doc._id.valueOf()][eventNameFree] = new Date();
if (doc.coursePrepaid) {
if (!doc.coursePrepaid.endDate) throw new Error("No endDate for new prepaid " + doc._id.valuOf());
userEventEndDateMap[doc._id.valueOf()][eventNamePaid] = new Date(doc.coursePrepaid.endDate);
if (!prepaidUsersMap[doc.coursePrepaid._id.valueOf()]) prepaidUsersMap[doc.coursePrepaid._id.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaid._id.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaid._id);
}
if (doc.coursePrepaidID) { if (doc.coursePrepaidID) {
userEventMap[doc._id.valueOf()] = eventNamePaid; if (!userEventEndDateMap[doc._id.valueOf()][eventNamePaid]) {
userEventEndDateMap[doc._id.valueOf()][eventNamePaid] = new Date();
}
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = []; if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf()); prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaidID); prepaidIDs.push(doc.coursePrepaidID);
} }
else {
userEventMap[doc._id.valueOf()] = eventNameFree;
} }
} cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {endDate: 1, properties: 1});
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
while (cursor.hasNext()) { while (cursor.hasNext()) {
doc = cursor.next(); doc = cursor.next();
if (doc.properties && doc.properties.trialRequestID) { if (doc.properties && doc.properties.trialRequestID) {
var endDate = new Date();
if (doc.endDate) {
endDate = new Date(doc.endDate);
}
else if (doc.properties.endDate) {
endDate = new Date(doc.properties.endDate);
}
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) { for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
userEventMap[prepaidUsersMap[doc._id.valueOf()][i]] = eventNameTrial; userEventEndDateMap[prepaidUsersMap[doc._id.valueOf()][i]][eventNameTrial] = endDate;
} }
} }
} }
@ -121,18 +135,22 @@ function getActiveClassCounts(startDay) {
var endDate = ISODate(startDay + "T00:00:00.000Z"); var endDate = ISODate(startDay + "T00:00:00.000Z");
var todayDate = new Date(new Date().toISOString().substring(0, 10)); var todayDate = new Date(new Date().toISOString().substring(0, 10));
var startObj = objectIdWithTimestamp(startDate); var startObj = objectIdWithTimestamp(startDate);
// Batch size test times: 10k 427005, 5k 361361, 1k 799068, 2k 791521
var batchSize = 5000;
for (var j = 0; j < classroomUserIDs.length / batchSize + 1; j++) {
// log("DEBUG: Fetching classroom events batch " + (j * batchSize) + " " + (j * batchSize + batchSize));
var queryParams = {$and: [ var queryParams = {$and: [
{_id: {$gte: startObj}}, {_id: {$gte: startObj}},
{user: {$in: classroomUserIDs}}, {user: {$in: classroomUserIDs.slice(j * batchSize, j * batchSize + batchSize)}},
{event: 'Started Level'} {event: 'Started Level'}
]}; ]};
cursor = logDB['log'].find(queryParams, {user: 1}); cursor = analyticsDB['log'].find(queryParams, {user: 1});
while (cursor.hasNext()) { while (cursor.hasNext()) {
doc = cursor.next(); doc = cursor.next();
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = []; if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
userPlayedMap[doc.user].push(doc._id.getTimestamp()); userPlayedMap[doc.user].push(doc._id.getTimestamp());
} }
// printjson(userPlayedMap); }
log("Calculate number of active members per classroom per day per event type.."); log("Calculate number of active members per classroom per day per event type..");
var classDayTypeMap = {}; var classDayTypeMap = {};
@ -159,7 +177,19 @@ function getActiveClassCounts(startDay) {
if (userPlayedMap[member]) { if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) { for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) { if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
classDayTypeMap[classroom][endDay][userEventMap[member]]++; if (userEventEndDateMap[member][eventNameTrial] > endDate) {
classDayTypeMap[classroom][endDay][eventNameTrial]++;
}
else if (userEventEndDateMap[member][eventNamePaid] > endDate) {
classDayTypeMap[classroom][endDay][eventNamePaid]++;
}
else if (userEventEndDateMap[member][eventNameFree] > endDate) {
classDayTypeMap[classroom][endDay][eventNameFree]++;
}
else {
print("ERROR: no event for " + member);
printjson(userEventEndDateMap[member]);
}
break; break;
} }
} }

View file

@ -8,6 +8,8 @@
// Usage: // Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password> // mongo <address>:<port>/<database> <script file> -u <username> -p <password>
// TODO: classroom paid active users before 4/13/16 not correct
try { try {
var logDB = new Mongo("localhost").getDB("analytics") var logDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date(); var scriptStartTime = new Date();
@ -129,32 +131,47 @@ function getActiveUserCounts(startDay, endDay, activeUserEvents) {
log("Classroom user count: " + classroomUserObjectIds.length); log("Classroom user count: " + classroomUserObjectIds.length);
// Classrooms free/trial/paid // Classrooms free/trial/paid
// Paid user: user.coursePrepaidID set means access to paid courses // Paid user: user.coursePrepaid or user.coursePrepaidID set means access to paid courses
// Trial user: prepaid.properties.trialRequestID means access was via trial // Trial user: prepaid.properties.trialRequestID means access was via trial
// Free: not paid, not trial // Free: not paid, not trial
log("Finding classroom users free/trial/paid status.."); log("Finding classroom users free/trial/paid status..");
var classroomUserEventMap = {}; var classroomUserEventEndDateMap = {};
var prepaidUsersMap = {}; var prepaidUsersMap = {};
var prepaidIDs = []; var prepaidIDs = [];
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaidID: 1}); cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaid: 1, coursePrepaidID: 1});
while (cursor.hasNext()) { while (cursor.hasNext()) {
doc = cursor.next(); doc = cursor.next();
classroomUserEventEndDateMap[doc._id.valueOf()] = {};
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom free'] = new Date();
if (doc.coursePrepaid) {
if (!doc.coursePrepaid.endDate) throw new Error("No endDate for new prepaid " + doc._id.valuOf());
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid'] = new Date(doc.coursePrepaid.endDate);
if (!prepaidUsersMap[doc.coursePrepaid._id.valueOf()]) prepaidUsersMap[doc.coursePrepaid._id.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaid._id.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaid._id);
}
if (doc.coursePrepaidID) { if (doc.coursePrepaidID) {
classroomUserEventMap[doc._id.valueOf()] = 'DAU classroom paid'; if (!classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid']) {
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid'] = new Date();
}
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = []; if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf()); prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaidID); prepaidIDs.push(doc.coursePrepaidID);
} }
else {
classroomUserEventMap[doc._id.valueOf()] = 'DAU classroom free';
}
} }
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1}); cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
while (cursor.hasNext()) { while (cursor.hasNext()) {
doc = cursor.next(); doc = cursor.next();
if (doc.properties && doc.properties.trialRequestID) { if (doc.properties && doc.properties.trialRequestID) {
var endDate = new Date();
if (doc.endDate) {
endDate = new Date(doc.endDate);
}
else if (doc.properties.endDate) {
endDate = new Date(doc.properties.endDate);
}
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) { for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
classroomUserEventMap[prepaidUsersMap[doc._id.valueOf()][i]] = 'DAU classroom trial'; classroomUserEventEndDateMap[prepaidUsersMap[doc._id.valueOf()][i]]['DAU classroom trial'] = endDate;
} }
} }
} }
@ -199,7 +216,22 @@ function getActiveUserCounts(startDay, endDay, activeUserEvents) {
var userDayEventMap = {} var userDayEventMap = {}
for (day in dayUserActiveMap) { for (day in dayUserActiveMap) {
for (var user in dayUserActiveMap[day]) { for (var user in dayUserActiveMap[day]) {
var event = classroomUserEventMap[user] || (dayCampaignUserPaidMap[day] && dayCampaignUserPaidMap[day][user] ? 'DAU campaign paid' : 'DAU campaign free'); var event = null;
var endDate = new Date(day + "T00:00:00.000Z");
if (classroomUserEventEndDateMap[user]) {
if (classroomUserEventEndDateMap[user]['DAU classroom trial'] > endDate) {
event = 'DAU classroom trial';
}
else if (classroomUserEventEndDateMap[user]['DAU classroom paid'] > endDate) {
event = 'DAU classroom paid';
}
else if (classroomUserEventEndDateMap[user]['DAU classroom free'] > endDate) {
event = 'DAU classroom free';
}
}
if (!event) {
event = dayCampaignUserPaidMap[day] && dayCampaignUserPaidMap[day][user] ? 'DAU campaign paid' : 'DAU campaign free';
}
dailyEventNames[event] = true; dailyEventNames[event] = true;
if (!activeUsersCounts[day]) activeUsersCounts[day] = {}; if (!activeUsersCounts[day]) activeUsersCounts[day] = {};
if (!activeUsersCounts[day][event]) activeUsersCounts[day][event] = 0; if (!activeUsersCounts[day][event]) activeUsersCounts[day][event] = 0;