Overview
SkillRise tracks detailed learning analytics including time spent on pages, daily activity patterns, and per-course breakdowns. The system provides insights for both students and educators.
Time Tracking Model
TimeTracking Schema
server/models/TimeTracking.js
import mongoose from 'mongoose'
const timeTrackingSchema = new mongoose . Schema (
{
userId: { type: String , required: true },
page: { type: String , required: true },
path: { type: String , required: true },
duration: { type: Number , required: true }, // in seconds
date: { type: Date , default: Date . now },
},
{ timestamps: true }
)
const TimeTracking = mongoose . model ( 'TimeTracking' , timeTrackingSchema )
export default TimeTracking
Each tracking record captures a single page session with duration in seconds, allowing granular analysis of learning patterns.
Tracking Implementation
Record Time Spent
Endpoint for frontend to report time spent on pages:
server/controllers/timeTrackingController.js
import TimeTracking from '../models/TimeTracking.js'
import { z } from 'zod'
const TrackTimeBodySchema = z . object ({
page: z . string (). min ( 1 ),
path: z . string (). min ( 1 ),
duration: z . number (). positive (),
})
export const trackTime = async ( req , res ) => {
try {
const userId = req . auth . userId
const bodyResult = TrackTimeBodySchema . safeParse ( req . body )
if ( ! bodyResult . success ) {
return res . status ( 400 ). json ({
success: false ,
message: 'Invalid tracking data'
})
}
const { page , path , duration } = bodyResult . data
await TimeTracking . create ({
userId ,
page ,
path ,
duration: Math . round ( duration )
})
res . json ({ success: true })
} catch ( error ) {
console . error ( error )
res . status ( 500 ). json ({
success: false ,
message: 'An unexpected error occurred'
})
}
}
Duration is rounded to the nearest second to ensure consistent data storage.
Analytics Dashboard
Comprehensive Analytics Endpoint
Generate multi-faceted analytics from tracking data:
server/controllers/timeTrackingController.js
const EXCLUDED_PAGES = new Set ([
'Home' ,
'Browse Courses' ,
'Course Details' ,
'Analytics' ,
'Other'
])
export const getAnalytics = async ( req , res ) => {
try {
const userId = req . auth . userId
const records = await TimeTracking . find ({ userId })
. sort ({ date: - 1 })
const activeRecords = records . filter (
( r ) => ! EXCLUDED_PAGES . has ( r . page )
)
// Per-page aggregation
const pageMap = {}
activeRecords . forEach (( r ) => {
if ( ! pageMap [ r . page ]) {
pageMap [ r . page ] = {
page: r . page ,
path: r . path ,
totalDuration: 0 ,
visits: 0
}
}
pageMap [ r . page ]. totalDuration += r . duration
pageMap [ r . page ]. visits += 1
})
const pageStats = Object . values ( pageMap )
. sort (( a , b ) => b . totalDuration - a . totalDuration )
// Last 7 days daily breakdown
const dailyMap = {}
for ( let i = 6 ; i >= 0 ; i -- ) {
const d = new Date ()
d . setDate ( d . getDate () - i )
dailyMap [ d . toISOString (). split ( 'T' )[ 0 ]] = 0
}
activeRecords . forEach (( r ) => {
const key = new Date ( r . date ). toISOString (). split ( 'T' )[ 0 ]
if ( key in dailyMap ) dailyMap [ key ] += r . duration
})
const dailyStats = Object . entries ( dailyMap )
. map (([ date , duration ]) => ({ date , duration }))
const totalDuration = activeRecords . reduce (
( sum , r ) => sum + r . duration ,
0
)
const totalSessions = activeRecords . length
// Course breakdown logic continues...
const courseBreakdown = buildCourseBreakdown ( records )
res . json ({
success: true ,
analytics: {
totalDuration ,
totalSessions ,
pageStats ,
dailyStats ,
courseBreakdown
},
})
} catch ( error ) {
console . error ( error )
res . status ( 500 ). json ({
success: false ,
message: 'An unexpected error occurred'
})
}
}
Fetch Records
Retrieve all time tracking records for the user, sorted by most recent.
Filter Active Pages
Exclude navigation and metadata pages to focus on learning activity.
Per-Page Aggregation
Group records by page and calculate total duration and visit count.
Daily Breakdown
Generate last 7 days of activity for time-series visualization.
Course Analysis
Extract course-specific time tracking from URL patterns.
Course Breakdown Analysis
server/controllers/timeTrackingController.js
const breakdownMap = {}
// Group learning time by courseId (from /player/:courseId)
records
. filter (( r ) => r . path . startsWith ( '/player/' ))
. forEach (( r ) => {
const courseId = r . path . split ( '/' )[ 2 ]
if ( ! courseId ) return
if ( ! breakdownMap [ courseId ])
breakdownMap [ courseId ] = {
courseId ,
learningDuration: 0 ,
learningSessions: 0 ,
chapters: {},
}
breakdownMap [ courseId ]. learningDuration += r . duration
breakdownMap [ courseId ]. learningSessions += 1
})
// Group quiz time by courseId + chapterId (from /quiz/:courseId/:chapterId)
records
. filter (( r ) => r . path . startsWith ( '/quiz/' ))
. forEach (( r ) => {
const parts = r . path . split ( '/' )
const courseId = parts [ 2 ]
const chapterId = parts [ 3 ]
if ( ! courseId || ! chapterId ) return
if ( ! breakdownMap [ courseId ])
breakdownMap [ courseId ] = {
courseId ,
learningDuration: 0 ,
learningSessions: 0 ,
chapters: {},
}
if ( ! breakdownMap [ courseId ]. chapters [ chapterId ]) {
breakdownMap [ courseId ]. chapters [ chapterId ] = {
chapterId ,
quizDuration: 0 ,
quizSessions: 0 ,
}
}
breakdownMap [ courseId ]. chapters [ chapterId ]. quizDuration += r . duration
breakdownMap [ courseId ]. chapters [ chapterId ]. quizSessions += 1
})
server/controllers/timeTrackingController.js
// Fetch course titles + chapter titles in one query
const allCourseIds = Object . keys ( breakdownMap )
const courses = await Course . find ({ _id: { $in: allCourseIds } })
. select ( '_id courseTitle courseThumbnail courseContent' )
const courseDataMap = {}
courses . forEach (( c ) => {
const chapterMap = {}
c . courseContent . forEach (( ch ) => {
chapterMap [ ch . chapterId ] = ch . chapterTitle
})
courseDataMap [ c . _id . toString ()] = {
title: c . courseTitle ,
thumbnail: c . courseThumbnail ,
chapterMap ,
}
})
const courseBreakdown = Object . values ( breakdownMap )
. map (( entry ) => {
const info = courseDataMap [ entry . courseId ] || {}
const chapters = Object . values ( entry . chapters )
. map (( ch ) => ({
chapterId: ch . chapterId ,
chapterTitle: info . chapterMap ?.[ ch . chapterId ] || 'Unknown Chapter' ,
quizDuration: ch . quizDuration ,
quizSessions: ch . quizSessions ,
}))
. sort (( a , b ) => b . quizDuration - a . quizDuration )
const totalQuizDuration = chapters . reduce (
( s , c ) => s + c . quizDuration ,
0
)
return {
courseId: entry . courseId ,
courseTitle: info . title || 'Unknown Course' ,
courseThumbnail: info . thumbnail || null ,
learningDuration: entry . learningDuration ,
learningSessions: entry . learningSessions ,
totalQuizDuration ,
chapters ,
}
})
. sort (
( a , b ) =>
b . learningDuration + b . totalQuizDuration -
( a . learningDuration + a . totalQuizDuration )
)
Course breakdown separates learning time (video player) from quiz time, providing granular insights into study patterns.
Analytics Data Structure
{
"success" : true ,
"analytics" : {
"totalDuration" : 12650 ,
"totalSessions" : 47 ,
"pageStats" : [
{
"page" : "Course Player" ,
"path" : "/player/course123" ,
"totalDuration" : 8340 ,
"visits" : 28
},
{
"page" : "Quiz" ,
"path" : "/quiz/course123/chapter1" ,
"totalDuration" : 2150 ,
"visits" : 12
}
],
"dailyStats" : [
{ "date" : "2026-02-26" , "duration" : 1820 },
{ "date" : "2026-02-27" , "duration" : 2340 },
{ "date" : "2026-02-28" , "duration" : 1950 },
{ "date" : "2026-03-01" , "duration" : 2680 },
{ "date" : "2026-03-02" , "duration" : 1740 },
{ "date" : "2026-03-03" , "duration" : 1520 },
{ "date" : "2026-03-04" , "duration" : 600 }
],
"courseBreakdown" : [
{
"courseId" : "course123" ,
"courseTitle" : "Advanced JavaScript" ,
"courseThumbnail" : "https://..." ,
"learningDuration" : 8340 ,
"learningSessions" : 28 ,
"totalQuizDuration" : 2150 ,
"chapters" : [
{
"chapterId" : "chapter1" ,
"chapterTitle" : "Closures and Scope" ,
"quizDuration" : 1200 ,
"quizSessions" : 7
},
{
"chapterId" : "chapter2" ,
"chapterTitle" : "Async Patterns" ,
"quizDuration" : 950 ,
"quizSessions" : 5
}
]
}
]
}
}
Educator Quiz Insights
Educators can view aggregated quiz performance for their courses:
server/controllers/quizController.js
export const getEducatorQuizInsights = async ( req , res ) => {
try {
const educatorId = req . auth . userId
const courses = await Course . find ({ educatorId })
const courseIds = courses . map (( c ) => c . _id . toString ())
const allResults = await QuizResult . find ({
courseId: { $in: courseIds }
})
// Group by courseId + chapterId
const statsMap = {}
allResults . forEach (( r ) => {
const key = ` ${ r . courseId } __ ${ r . chapterId } `
if ( ! statsMap [ key ]) {
statsMap [ key ] = {
courseId: r . courseId ,
chapterId: r . chapterId ,
attempts: 0 ,
totalPct: 0 ,
needs_review: 0 ,
on_track: 0 ,
mastered: 0 ,
}
}
statsMap [ key ]. attempts ++
statsMap [ key ]. totalPct += r . percentage
statsMap [ key ][ r . group ] ++
})
// Attach chapter + course titles
const insights = Object . values ( statsMap ). map (( entry ) => {
const course = courses . find (
( c ) => c . _id . toString () === entry . courseId
)
const chapter = course ?. courseContent . find (
( ch ) => ch . chapterId === entry . chapterId
)
return {
... entry ,
courseTitle: course ?. courseTitle || 'Unknown' ,
chapterTitle: chapter ?. chapterTitle || 'Unknown' ,
avgPct: Math . round ( entry . totalPct / entry . attempts ),
}
})
// Sort by most attempts desc
insights . sort (( a , b ) => b . attempts - a . attempts )
res . json ({ success: true , insights })
} catch ( error ) {
console . error ( error )
res . status ( 500 ). json ({
success: false ,
message: 'An unexpected error occurred'
})
}
}
Fetch Courses
Get all courses created by the educator.
Aggregate Results
Group quiz results by course and chapter combination.
Calculate Statistics
Compute average scores and performance group distributions.
Enrich Data
Add course and chapter titles for readable output.
Analytics Visualizations
Time Series Daily activity over the last 7 days shows learning consistency patterns.
Course Distribution Per-course time breakdown reveals which courses students engage with most.
Session Analytics Total sessions and average duration per page provide engagement metrics.
Quiz Performance Performance group distribution helps educators identify challenging chapters.
Key Metrics
Sum of all learning time (excluding navigation pages) measured in seconds.
Count of individual page visits, indicating engagement frequency.
Separate tracking for video player sessions and quiz attempts.
Chapter-Level Granularity
Quiz time is tracked per chapter, enabling fine-grained difficulty analysis.
Quiz results categorized as needs_review, on_track, or mastered.
Frontend Integration
Tracking Hook Example
client/src/hooks/useTimeTracking.js
import { useEffect , useRef } from 'react'
import { useLocation } from 'react-router-dom'
export const useTimeTracking = ( pageName ) => {
const location = useLocation ()
const startTime = useRef ( Date . now ())
useEffect (() => {
startTime . current = Date . now ()
return () => {
const duration = Math . floor (( Date . now () - startTime . current ) / 1000 )
if ( duration > 5 ) { // Only track sessions > 5 seconds
fetch ( '/api/time-tracking/track' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
page: pageName ,
path: location . pathname ,
duration ,
}),
})
}
}
}, [ location . pathname , pageName ])
}
Track time on component unmount to capture the complete session duration accurately.
Best Practices
Filter Noise Exclude navigation and meta pages to focus analytics on learning content.
Minimum Threshold Only track sessions longer than 5 seconds to avoid false data from quick navigations.
URL Pattern Matching Use URL patterns (/player/:id, /quiz/:id/:chapter) to extract contextual metadata.
Aggregate Efficiently Use in-memory aggregation for dashboard queries to minimize database load.
Privacy Considerations
All analytics are user-scoped and require authentication. Students can only view their own analytics, and educators can only view aggregate data for their courses.
Next Steps