This commit is contained in:
yaur 2015-01-28 15:50:27 -05:00
commit 4c08672dab
4 changed files with 440 additions and 109 deletions

View file

@ -11,3 +11,31 @@
.button.close
font-size: 63px
.line-graph-label
font-size: 10pt
font-weight: normal
.line-graph-container
height: 500px
width: 100%
position: relative
.x.axis
font-size: 9pt
path
display: none
.y.axis
font-size: 9pt
path
display: none
.key-line
font-size: 9pt
.key-text
font-size: 9pt
.graph-point-info-container
display: none
position: absolute
padding: 10px
border: 1px solid black
z-index: 3
background-color: blanchedalmond
font-size: 10pt

View file

@ -5,7 +5,7 @@ block content
h1(data-i18n="admin.growth_title") Growth
if me.isAdmin()
if crunchingData
h4 Cruncing Data..
h4 Crunching Data..
else
h2 Registered Users
h3 Per-Day

View file

@ -10,49 +10,20 @@
input.form-control#input-startday(type='text', style='width:100px;', value=analytics.startDay)
input.form-control#input-endday(type='text', style='width:100px;', value=analytics.endDay)
button.btn.btn-default.btn-sm#reload-button(style='margin-left:10px;') Reload
h4 Completion Rates
if analytics.levelCompletions.loading
div Loading...
else
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Date
td Started
td Finished
td Completion %
if analytics.levelHelps.levels.length === analytics.levelCompletions.levels.length
td Helps Clicked
td Helps / Started
td Help Videos
td Videos / Started
tbody
- for (var i = 0; i < analytics.levelCompletions.levels.length; i++)
tr
td= analytics.levelCompletions.levels[i].created
td= analytics.levelCompletions.levels[i].started
td= analytics.levelCompletions.levels[i].finished
td= analytics.levelCompletions.levels[i].rate
if analytics.levelHelps.levels.length === analytics.levelCompletions.levels.length && analytics.levelCompletions.levels[i].created == analytics.levelHelps.levels[i].day
td= analytics.levelHelps.levels[i].alertHelps + analytics.levelHelps.levels[i].paletteHelps
td= ((analytics.levelHelps.levels[i].alertHelps + analytics.levelHelps.levels[i].paletteHelps) / analytics.levelCompletions.levels[i].started).toFixed(2)
td= analytics.levelHelps.levels[i].videoStarts
td= (analytics.levelHelps.levels[i].videoStarts / analytics.levelCompletions.levels[i].started).toFixed(2)
h4 Average Playtimes
if analytics.levelPlaytimes.loading
div Loading...
else
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Date
td Average (s)
tbody
- for (var i = 0; i < analytics.levelPlaytimes.levels.length; i++)
tr
td= analytics.levelPlaytimes.levels[i].created
td= analytics.levelPlaytimes.levels[i].average.toFixed(2)
each graph in analytics.graphs
each line in graph.lines
label.line-graph-label
input.line-graph-checkbox(data-lineid="#{line.lineID}", type='checkbox', checked=line.enabled)
span #{line.description}
span &nbsp;&nbsp;
.line-graph-container
each line in graph.lines
each point in line.points
.graph-point-info-container(data-pointid="#{point.pointID}")
div(style='font-weight:bold;') #{point.day}
each value in point.values
div #{value}
h4 Common Problems
if analytics.commonProblems.loading
@ -66,18 +37,18 @@
td Error Hint
td Count
tbody
- for (var i = 0; i < analytics.commonProblems.levels.length && i < 20; i++)
- for (var i = 0; i < analytics.commonProblems.data.length && i < 20; i++)
tr
td= analytics.commonProblems.levels[i].language
td= analytics.commonProblems.levels[i].message
td= analytics.commonProblems.levels[i].hint
td= analytics.commonProblems.levels[i].count
td= analytics.commonProblems.data[i].language
td= analytics.commonProblems.data[i].message
td= analytics.commonProblems.data[i].hint
td= analytics.commonProblems.data[i].count
h4 Recent Sessions
if analytics.recentSessions.loading
div Loading...
else
div(style='font-size:10pt') Latest #{analytics.recentSessions.levels.length} sessions for this level
div(style='font-size:10pt') Latest #{analytics.recentSessions.data.length} sessions for this level
div(style='font-size:10pt') Double-click row to open player and session
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
@ -89,17 +60,61 @@
td Complete
td Changed
tbody
- for (var i = 0; i < analytics.recentSessions.levels.length; i++)
tr.recent-session(data-player-id=analytics.recentSessions.levels[i].creator, data-session-id=analytics.recentSessions.levels[i]._id)
td= analytics.recentSessions.levels[i]._id
td= analytics.recentSessions.levels[i].creatorName || analytics.recentSessions.levels[i].creator
td= analytics.recentSessions.levels[i].codeLanguage
td= analytics.recentSessions.levels[i].playtime
if analytics.recentSessions.levels[i].state && analytics.recentSessions.levels[i].state.complete
td= analytics.recentSessions.levels[i].state.complete
- for (var i = 0; i < analytics.recentSessions.data.length; i++)
tr.recent-session(data-player-id=analytics.recentSessions.data[i].creator, data-session-id=analytics.recentSessions.data[i]._id)
td= analytics.recentSessions.data[i]._id
td= analytics.recentSessions.data[i].creatorName || analytics.recentSessions.data[i].creator
td= analytics.recentSessions.data[i].codeLanguage
td= analytics.recentSessions.data[i].playtime
if analytics.recentSessions.data[i].state && analytics.recentSessions.data[i].state.complete
td= analytics.recentSessions.data[i].state.complete
else
td false
td= analytics.recentSessions.levels[i].changed
td= analytics.recentSessions.data[i].changed
h4 Completion Rates
if analytics.levelCompletions.loading
div Loading...
else
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Date
td Started
td Finished
td Completion %
if analytics.levelHelps.data.length === analytics.levelCompletions.data.length
td Helps Clicked
td Helps / Started
td Help Videos
td Videos / Started
tbody
- for (var i = 0; i < analytics.levelCompletions.data.length; i++)
tr
td= analytics.levelCompletions.data[i].created
td= analytics.levelCompletions.data[i].started
td= analytics.levelCompletions.data[i].finished
td= analytics.levelCompletions.data[i].rate
if analytics.levelHelps.data.length === analytics.levelCompletions.data.length && analytics.levelCompletions.data[i].created == analytics.levelHelps.data[i].day
td= analytics.levelHelps.data[i].alertHelps + analytics.levelHelps.data[i].paletteHelps
td= ((analytics.levelHelps.data[i].alertHelps + analytics.levelHelps.data[i].paletteHelps) / analytics.levelCompletions.data[i].started).toFixed(2)
td= analytics.levelHelps.data[i].videoStarts
td= (analytics.levelHelps.data[i].videoStarts / analytics.levelCompletions.data[i].started).toFixed(2)
h4 Average Playtimes
if analytics.levelPlaytimes.loading
div Loading...
else
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
thead
tr
td Date
td Average (s)
tbody
- for (var i = 0; i < analytics.levelPlaytimes.data.length; i++)
tr
td= analytics.levelPlaytimes.data[i].created
td= analytics.levelPlaytimes.data[i].average.toFixed(2)
if level.get('tasks')
.tasks

View file

@ -10,16 +10,18 @@ module.exports = class CampaignLevelView extends CocoView
template: require 'templates/editor/campaign/campaign-level-view'
events:
'change .line-graph-checkbox': 'updateGraphCheckbox'
'click .close': 'onClickClose'
'click #reload-button': 'onClickReloadButton'
'dblclick .recent-session': 'onDblClickRecentSession'
'mouseenter .graph-point': 'onMouseEnterPoint'
'mouseleave .graph-point': 'onMouseLeavePoint'
constructor: (options, @level) ->
super(options)
@fullLevel = new Level _id: @level.id
@fullLevel.fetch()
@listenToOnce @fullLevel, 'sync', => @render?()
@levelSlug = @level.get('slug')
@getAnalytics()
@ -33,6 +35,17 @@ module.exports = class CampaignLevelView extends CocoView
super()
$("#input-startday").datepicker dateFormat: "yy-mm-dd"
$("#input-endday").datepicker dateFormat: "yy-mm-dd"
# TODO: Why does this have to be called from afterRender() instead of getRenderData()?
@updateAnalyticsGraphs()
updateGraphCheckbox: (e) ->
lineID = $(e.target).data('lineid')
checked = $(e.target).prop('checked')
for graph in @analytics.graphs
for line in graph.lines
if line.lineID is lineID
line.enabled = checked
return @render()
onClickClose: ->
@$el.addClass('hidden')
@ -51,7 +64,297 @@ module.exports = class CampaignLevelView extends CocoView
session = new LevelSession _id: row.data 'session-id'
@openModalView new ModelModal models: [session, player]
onMouseEnterPoint: (e) ->
pointID = $(e.target).data('pointid')
container = @$el.find(".graph-point-info-container[data-pointid=#{pointID}]").show()
margin = 20
width = container.outerWidth()
height = container.outerHeight()
container.css('left', e.offsetX - width / 2)
container.css('top', e.offsetY - height - margin)
onMouseLeavePoint: (e) ->
pointID = $(e.target).data('pointid')
@$el.find(".graph-point-info-container[data-pointid=#{pointID}]").hide()
updateAnalyticsGraphData: ->
# console.log 'updateAnalyticsGraphData'
# Build graphs based on available @analytics data
# Currently only one graph
@analytics.graphs = [graphID: 'level-completions', lines: []]
# TODO: Where should this metadata live?
# TODO: lineIDs assumed to be unique across graphs
completionLineID = 'level-completions'
playtimeLineID = 'level-playtime'
helpsLineID = 'helps-clicked'
videosLineID = 'help-videos'
lineMetadata = {}
lineMetadata[completionLineID] =
description: 'Level Completion (%)'
color: 'red'
lineMetadata[playtimeLineID] =
description: 'Average Playtime (s)'
color: 'green'
lineMetadata[helpsLineID] =
description: 'Help click rate (%)'
color: 'blue'
lineMetadata[videosLineID] =
description: 'Help video rate (%)'
color: 'purple'
# Last day may be missing due to caching, will use this days aggregate to clean up individual graph lines
days = {}
days[day.created] = true for day in @analytics.levelCompletions.data if @analytics?.levelCompletions?.data?
days[day.created.replace(/-/g, '')] = true for day in @analytics.levelPlaytimes.data if @analytics?.levelPlaytimes?.data?
days[day.day] = true for day in @analytics.levelHelps.data if @analytics?.levelHelps?.data?
days = Object.keys(days).sort (a, b) -> if a < b then -1 else 1
# Update level completion graph data
dayStartedMap = {}
if @analytics?.levelCompletions?.data?.length > 0
# Build line data
levelPoints = []
for day, i in @analytics.levelCompletions.data
dayStartedMap[day.created] = day.started
rate = parseFloat(day.rate)
levelPoints.push
x: i
y: rate
started: day.started
day: "#{day.created[0..3]}-#{day.created[4..5]}-#{day.created[6..7]}"
pointID: "#{completionLineID}#{i}"
values: ["Started: #{day.started}", "Finished: #{day.finished}", "Completion rate: #{rate.toFixed(2)}%"]
# Ensure points for each day
if levelPoints.length < days.length
for i in [1..days.length - levelPoints.length]
day = days[days.length - i]
x = levelPoints[levelPoints.length - 1].x + 1
levelPoints.push
x: x
y: 0.0
started: 0
day: "#{day[0..3]}-#{day[4..5]}-#{day[6..7]}"
pointID: "#{completionLineID}#{x}"
values: []
@analytics.graphs[0].lines.push
lineID: completionLineID
enabled: true
points: levelPoints
description: lineMetadata[completionLineID].description
lineColor: lineMetadata[completionLineID].color
min: 0
max: 100.0
# Update average playtime graph data
if @analytics?.levelPlaytimes?.data?.length > 0
# Build line data
playtimePoints = []
for day, i in @analytics.levelPlaytimes.data
avg = parseFloat(day.average)
playtimePoints.push
x: i
y: avg
day: day.created
pointID: "#{playtimeLineID}#{i}"
values: ["Average playtime: #{avg.toFixed(2)}s"]
# Ensure points for each day
if playtimePoints.length < days.length
for i in [1..days.length - playtimePoints.length]
day = days[days.length - i]
x = playtimePoints[playtimePoints.length - 1].x + 1
playtimePoints.push
x: x
y: 0.0
started: 0
day: "#{day[0..3]}-#{day[4..5]}-#{day[6..7]}"
pointID: "#{completionLineID}#{x}"
values: []
@analytics.graphs[0].lines.push
lineID: playtimeLineID
enabled: true
points: playtimePoints
description: lineMetadata[playtimeLineID].description
lineColor: lineMetadata[playtimeLineID].color
min: 0
max: d3.max(playtimePoints, (d) -> d.y)
# Update help graph data
if @analytics?.levelHelps?.data?.length > 0
# Build line data
helpPoints = []
videoPoints = []
for day, i in @analytics.levelHelps.data
helpCount = day.alertHelps + day.paletteHelps
started = dayStartedMap[day.day] ? 0
clickRate = if started > 0 then helpCount / started * 100 else -1.0
videoRate = day.videoStarts / helpCount * 100
helpPoints.push
x: i
y: clickRate
day: "#{day.day[0..3]}-#{day.day[4..5]}-#{day.day[6..7]}"
pointID: "#{helpsLineID}#{i}"
values: ["Helps clicked: #{helpCount}", "Helps click clickRate: #{clickRate.toFixed(2)}%"]
videoPoints.push
x: i
y: videoRate
day: "#{day.day[0..3]}-#{day.day[4..5]}-#{day.day[6..7]}"
pointID: "#{videosLineID}#{i}"
values: ["Help videos started: #{day.videoStarts}", "Help videos start rate: #{videoRate.toFixed(2)}%"]
# Ensure points for each day
if helpPoints.length < days.length
for i in [1..days.length - helpPoints.length]
day = days[days.length - i]
x = helpPoints[helpPoints.length - 1].x + 1
helpPoints.push
x: x
y: 0.0
started: 0
day: "#{day[0..3]}-#{day[4..5]}-#{day[6..7]}"
pointID: "#{helpsLineID}#{x}"
values: []
if videoPoints.length < days.length
for i in [1..days.length - videoPoints.length]
day = days[days.length - i]
x = videoPoints[videoPoints.length - 1].x + 1
helpPoints.push
x: x
y: 0.0
started: 0
day: "#{day[0..3]}-#{day[4..5]}-#{day[6..7]}"
pointID: "#{videosLineID}#{x}"
values: []
@analytics.graphs[0].lines.push
lineID: helpsLineID
enabled: true
points: helpPoints
description: lineMetadata[helpsLineID].description
lineColor: lineMetadata[helpsLineID].color
min: 0
max: 100.0
if d3.max(videoPoints, (d) -> d.y) > 0
@analytics.graphs[0].lines.push
lineID: videosLineID
enabled: true
points: videoPoints
description: lineMetadata[videosLineID].description
lineColor: lineMetadata[videosLineID].color
min: 0
max: 100.0
updateAnalyticsGraphs: ->
# Build d3 graphs
return unless @analytics?.graphs?.length > 0
containerSelector = '.line-graph-container'
# console.log 'updateAnalyticsGraphs', containerSelector, @analytics.graphs
margin = 20
keyHeight = 20
xAxisHeight = 20
yAxisWidth = 40
containerWidth = $(containerSelector).width()
containerHeight = $(containerSelector).height()
for graph in @analytics.graphs
graphLineCount = _.reduce graph.lines, ((sum, item) -> if item.enabled then sum + 1 else sum), 0
svg = d3.select(containerSelector).append("svg")
.attr("width", containerWidth)
.attr("height", containerHeight)
width = containerWidth - margin * 2 - yAxisWidth * graphLineCount
height = containerHeight - margin * 2 - xAxisHeight - keyHeight * graphLineCount
currentLine = 0
for line in graph.lines
continue unless line.enabled
xRange = d3.scale.linear().range([0, width]).domain([d3.min(line.points, (d) -> d.x), d3.max(line.points, (d) -> d.x)])
yRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
# x-Axis and guideline once
if currentLine is 0
startDay = new Date(line.points[0].day)
endDay = new Date(line.points[line.points.length - 1].day)
xAxisRange = d3.time.scale()
.domain([startDay, endDay])
.range([0, width])
xAxis = d3.svg.axis()
.scale(xAxisRange)
svg.append("g")
.attr("class", "x axis")
.call(xAxis)
.selectAll("text")
.attr("dy", ".35em")
.attr("transform", "translate(" + (margin + yAxisWidth * (graphLineCount - 1)) + "," + (height + margin) + ")")
.style("text-anchor", "start")
# Horizontal guidelines
svg.selectAll(".line")
.data([10, 30, 50, 70, 90])
.enter()
.append("line")
.attr("x1", margin + yAxisWidth * graphLineCount)
.attr("y1", (d) -> margin + yRange(d))
.attr("x2", margin + yAxisWidth * graphLineCount + width)
.attr("y2", (d) -> margin + yRange(d))
.attr("stroke", line.lineColor)
.style("opacity", "0.5")
# y-Axis
yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
yAxis = d3.svg.axis()
.scale(yRange)
.orient("left")
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (margin + yAxisWidth * currentLine) + "," + margin + ")")
.style("color", line.lineColor)
.call(yAxis)
.selectAll("text")
.attr("y", 0)
.attr("x", 0)
.attr("fill", line.lineColor)
.style("text-anchor", "start")
# Key
svg.append("line")
.attr("x1", margin)
.attr("y1", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
.attr("x2", margin + 40)
.attr("y2", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
.attr("stroke", line.lineColor)
.attr("class", "key-line")
svg.append("text")
.attr("x", margin + 40 + 10)
.attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2)
.attr("fill", line.lineColor)
.attr("class", "key-text")
.text(line.description)
# Path and points
svg.selectAll(".circle")
.data(line.points)
.enter()
.append("circle")
.attr("transform", "translate(" + (margin + yAxisWidth * graphLineCount) + "," + margin + ")")
.attr("cx", (d) -> xRange(d.x))
.attr("cy", (d) -> yRange(d.y))
.attr("r", (d) -> if d.started then Math.max(3, Math.min(10, Math.log(parseInt(d.started)))) else 4)
.attr("fill", line.lineColor)
.attr("stroke-width", 1)
.attr("class", "graph-point")
.attr("data-pointid", (d) -> "#{line.lineID}#{d.x}")
d3line = d3.svg.line()
.x((d) -> xRange(d.x))
.y((d) -> yRange(d.y))
.interpolate("linear")
svg.append("path")
.attr("d", d3line(line.points))
.attr("transform", "translate(" + (margin + yAxisWidth * graphLineCount) + "," + margin + ")")
.style("stroke-width", 1)
.style("stroke", line.lineColor)
.style("fill", "none")
currentLine++
getAnalytics: (startDay, endDay) =>
# Analytics APIs use 2 different day formats
if startDay?
startDayDashed = startDay
startDay = startDay.replace(/-/g, '')
@ -61,53 +364,40 @@ module.exports = class CampaignLevelView extends CocoView
if endDay?
endDayDashed = endDay
endDay = endDay.replace(/-/g, '')
else
else
endDay = utils.getUTCDay -1
endDayDashed = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}"
@analytics =
# Initialize
@analytics =
startDay: startDayDashed
endDay: endDayDashed
commonProblems:
levels: []
loading: true
levelCompletions:
levels: []
loading: true
levelHelps:
levels: []
loading: true
levelPlaytimes:
levels: []
loading: true
recentSessions:
levels: []
loading: true
@render()
commonProblems: {data: [], loading: true}
levelCompletions: {data: [], loading: true}
levelHelps: {data: [], loading: true}
levelPlaytimes: {data: [], loading: true}
recentSessions: {data: [], loading: true}
graphs: []
@render() # Hide old analytics data while we fetch new data
@getCommonLevelProblems startDayDashed, endDayDashed, () =>
@analytics.commonProblems.loading = false
@render()
@getLevelCompletions startDay, endDay, () =>
@analytics.levelCompletions.loading = false
@render()
@getLevelHelps startDay, endDay, () =>
@analytics.levelHelps.loading = false
@render()
@getLevelPlaytimes startDayDashed, endDayDashed, () =>
@analytics.levelPlaytimes.loading = false
@render()
@getRecentSessions () =>
@analytics.recentSessions.loading = false
@render()
makeFinishDataFetch = (data) =>
return =>
return if @destroyed
@updateAnalyticsGraphData()
data.loading = false
@render()
@getCommonLevelProblems startDayDashed, endDayDashed, makeFinishDataFetch(@analytics.commonProblems)
@getLevelCompletions startDay, endDay, makeFinishDataFetch(@analytics.levelCompletions)
@getLevelHelps startDay, endDay, makeFinishDataFetch(@analytics.levelHelps)
@getLevelPlaytimes startDayDashed, endDayDashed, makeFinishDataFetch(@analytics.levelPlaytimes)
@getRecentSessions makeFinishDataFetch(@analytics.recentSessions)
getCommonLevelProblems: (startDay, endDay, doneCallback) ->
success = (data) =>
return doneCallback() if @destroyed
@analytics.commonProblems.levels = data
# console.log 'getCommonLevelProblems', data
@analytics.commonProblems.data = data
doneCallback()
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'common_problems', {
url: '/db/user_code_problem/-/common_problems'
data: {startDay: startDay, endDay: endDay, slug: @levelSlug}
@ -119,13 +409,13 @@ module.exports = class CampaignLevelView extends CocoView
getLevelCompletions: (startDay, endDay, doneCallback) ->
success = (data) =>
return doneCallback() if @destroyed
data.sort (a, b) -> if a.created < b.created then 1 else -1
mapFn = (item) ->
item.rate = (item.finished / item.started * 100).toFixed(2)
# console.log 'getLevelCompletions', data
data.sort (a, b) -> if a.created < b.created then -1 else 1
mapFn = (item) ->
item.rate = item.finished / item.started * 100
item
@analytics.levelCompletions.levels = _.map data, mapFn, @
@analytics.levelCompletions.data = _.map data, mapFn, @
doneCallback()
request = @supermodel.addRequestResource 'level_completions', {
url: '/db/analytics_perday/-/level_completions'
data: {startDay: startDay, endDay: endDay, slug: @levelSlug}
@ -137,9 +427,9 @@ module.exports = class CampaignLevelView extends CocoView
getLevelHelps: (startDay, endDay, doneCallback) ->
success = (data) =>
return doneCallback() if @destroyed
@analytics.levelHelps.levels = data.sort (a, b) -> if a.day < b.day then 1 else -1
# console.log 'getLevelHelps', data
@analytics.levelHelps.data = data.sort (a, b) -> if a.day < b.day then -1 else 1
doneCallback()
request = @supermodel.addRequestResource 'level_helps', {
url: '/db/analytics_perday/-/level_helps'
data: {startDay: startDay, endDay: endDay, slugs: [@levelSlug]}
@ -151,9 +441,9 @@ module.exports = class CampaignLevelView extends CocoView
getLevelPlaytimes: (startDay, endDay, doneCallback) ->
success = (data) =>
return doneCallback() if @destroyed
@analytics.levelPlaytimes.levels = data.sort (a, b) -> if a.created < b.created then 1 else -1
# console.log 'getLevelPlaytimes', data
@analytics.levelPlaytimes.data = data.sort (a, b) -> if a.created < b.created then -1 else 1
doneCallback()
request = @supermodel.addRequestResource 'playtime_averages', {
url: '/db/level/-/playtime_averages'
data: {startDay: startDay, endDay: endDay, slugs: [@levelSlug]}
@ -164,17 +454,15 @@ module.exports = class CampaignLevelView extends CocoView
getRecentSessions: (doneCallback) ->
limit = 100
success = (data) =>
return doneCallback() if @destroyed
@analytics.recentSessions.levels = data
# console.log 'getRecentSessions', data
@analytics.recentSessions.data = data
doneCallback()
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'level_sessions_recent', {
url: "/db/level_session/-/recent"
data: {slug: @levelSlug, limit: limit}
method: 'POST'
success: success
}, 0
request.load()
request.load()