Skip to main content

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' 
    })
  }
}
1

Fetch Records

Retrieve all time tracking records for the user, sorted by most recent.
2

Filter Active Pages

Exclude navigation and metadata pages to focus on learning activity.
3

Per-Page Aggregation

Group records by page and calculate total duration and visit count.
4

Daily Breakdown

Generate last 7 days of activity for time-series visualization.
5

Course Analysis

Extract course-specific time tracking from URL patterns.

Course Breakdown Analysis

Extract Learning Time by Course

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
  })

Enrich with Course Metadata

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

Response Format

{
  "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

Quiz Performance Analytics

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' 
    })
  }
}
1

Fetch Courses

Get all courses created by the educator.
2

Aggregate Results

Group quiz results by course and chapter combination.
3

Calculate Statistics

Compute average scores and performance group distributions.
4

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.
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