mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
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:
parent
1f52329c7a
commit
73657d5428
6 changed files with 410 additions and 139 deletions
|
@ -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++
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) ->
|
||||
|
|
Loading…
Reference in a new issue