Students drop courses if they don't see their own progress. A simple counter like "5 of 20 lessons" neither motivates nor informs the instructor about who is falling behind. We develop learning progress systems that collect data at the second level: how much video was watched, which assignments were completed, how often the student logs in. This data forms the basis for analytics and alerts. Over years of work, we have implemented such systems for ten educational platforms—from small schools to corporate universities. Our experience guarantees the system will work under load and not lose a single event. Instructor time savings of up to 30% through automatic notifications.
What Problems Does Progress Tracking Solve
Inactive students. Without automatic notifications, the instructor learns about churn post-factum. Our progress system sends alerts to instructors if a student has not logged in for 7 days with an unfinished course. This allows timely intervention.
Low student engagement. Streak (consecutive days) is a powerful motivator. We calculate it automatically and display it in the personal account. If the streak is broken, the student sees they need to return.
Lack of learning analytics. Activity events show which lessons are toughest and where students rewatch videos. For example, if 70% of students rewind a specific segment, the content needs revision.
Why Is Streak Important for Retention?
Streak motivates 30% more effectively than a simple counter. If a student misses a day, the streak resets to 1 (not 0), psychologically giving a chance to rebuild the habit. We have seen streak implementation increase return rate by 40% in a short period on one platform.
How Do We Track Video Progress?
We use the stack: React on the frontend, Laravel on the backend, PostgreSQL for storage, Redis for activity caching. Example video tracker:
// Frontend: sending video progress
class VideoProgressTracker {
constructor(videoElement, lessonId) {
this.video = videoElement;
this.lessonId = lessonId;
this.maxReached = 0;
this.setupListeners();
}
setupListeners() {
// Send progress on pause, not on every timeupdate
this.video.addEventListener('pause', () => this.reportProgress());
this.video.addEventListener('ended', () => this.markCompleted());
// Track maximum watched point (do not count rewinding)
this.video.addEventListener('timeupdate', () => {
const pct = (this.video.currentTime / this.video.duration) * 100;
if (pct > this.maxReached) this.maxReached = pct;
});
}
async reportProgress() {
await api.post(`/lessons/${this.lessonId}/progress`, {
videoProgress: this.maxReached,
lastPosition: Math.floor(this.video.currentTime),
timeSpentSec: Math.floor(this.video.currentTime),
});
}
async markCompleted() {
if (this.maxReached >= 85) { // Consider lesson watched at 85%
await api.post(`/lessons/${this.lessonId}/complete`);
}
}
}
This code sends progress only on pause—not on every timeupdate—to avoid overloading the server. A lesson is considered complete at 85% watch time.
Technical Implementation
Data Model
-- Lesson progress
CREATE TABLE lesson_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
student_id UUID REFERENCES users(id),
lesson_id UUID REFERENCES lessons(id),
course_id UUID REFERENCES courses(id),
status VARCHAR(30) DEFAULT 'not_started', -- not_started, in_progress, completed
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
time_spent_sec INT DEFAULT 0,
video_progress NUMERIC(5,2), -- % video watched
last_position INT, -- last video second
UNIQUE(student_id, lesson_id)
);
-- Course progress (aggregate)
CREATE TABLE course_progress (
student_id UUID REFERENCES users(id),
course_id UUID REFERENCES courses(id),
lessons_completed INT DEFAULT 0,
lessons_total INT NOT NULL,
percentage NUMERIC(5,2) DEFAULT 0,
last_activity_at TIMESTAMPTZ,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
streak_days INT DEFAULT 0,
PRIMARY KEY(student_id, course_id)
);
-- Detailed activity log for analytics
CREATE TABLE activity_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
student_id UUID REFERENCES users(id),
event_type VARCHAR(100) NOT NULL, -- 'video_played', 'video_paused', 'lesson_completed'
entity_type VARCHAR(50),
entity_id UUID,
metadata JSONB DEFAULT '{}', -- position, duration, device, etc.
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ON activity_events (student_id, created_at DESC);
CREATE INDEX ON activity_events (entity_id, event_type);
How the Streak Is Calculated
Streak is the number of consecutive days a student performed at least one action. Algorithm:
async function updateStreak(studentId, courseId) {
const lastActivity = await db.courseProgress.findOne({ studentId, courseId }, 'last_activity_at');
const today = new Date().toDateString();
const yesterday = new Date(Date.now() - 86400000).toDateString();
const lastDate = new Date(lastActivity.lastActivityAt).toDateString();
let streakDelta = 0;
if (lastDate === today) {
streakDelta = 0; // Already updated today
} else if (lastDate === yesterday) {
streakDelta = 1; // Streak continues
} else {
// Streak broken — start over from 1
await db.courseProgress.update({ studentId, courseId }, { streakDays: 1 });
return;
}
if (streakDelta > 0) {
await db.courseProgress.increment({ studentId, courseId }, 'streak_days', 1);
}
}
Identifying At-Risk Students
Automatic alerts for instructors:
- Student hasn't logged in for 7+ days with an unfinished course.
- Progress < 20% two weeks after enrollment.
- Sharp slowdown: last week 5 lessons, this week 0.
-- Students inactive for 7+ days
SELECT cp.student_id, u.name, u.email,
cp.percentage, cp.last_activity_at
FROM course_progress cp
JOIN users u ON u.id = cp.student_id
WHERE cp.course_id = $1
AND cp.completed_at IS NULL
AND cp.last_activity_at < NOW() - INTERVAL '7 days';
Tracking Levels: Comparison
| Level | What We Track | Technical Complexity | Approx. Implementation Time |
|---|---|---|---|
| Basic | Number of completed lessons | Low | 2–3 days |
| Medium | Video progress, streak, session time | Medium | 4–5 days |
| Advanced | Activity events, cohorts, alerts | High | 7–10 days |
We implement the level that fits your needs. Most often, Medium level is sufficient—it delivers 80% of the value with 50% of the effort.
Typical Mistakes in Tracking Implementation
| Mistake | Consequence | How to Avoid |
|---|---|---|
| Sending progress on every timeupdate | High server load, data loss on pauses | Send only on pause/completion |
| Resetting streak on inactivity | Demotivates students | Start streak at 1 after a skip, not 0 |
| Storing all events without archiving | Table growth to terabytes | Use partitioning and TTL indexes |
We eliminate these mistakes at the design stage. Our engineers with years of LMS experience anticipate bottlenecks in advance.
Implementation Process
- Analytics. We study your LMS, course list, reporting requirements.
- Design. We design the data model, API endpoints, event schema.
- Implementation. We write backend on Laravel, frontend on React, integrate with video player.
- Testing. We verify data collection correctness, perform load testing.
- Deployment. We deploy on your server or cloud. Provide documentation.
What's Included in the Work
- Data model (migrations, indexes)
- API for recording and reading progress
- Frontend components (indicators, progress bar)
- Teacher alerts (email/telegram)
- LMS analytics dashboards (active students, completion rate)
- Operation manual
- 6-month code warranty
Timeline and Cost
Basic tracking (lessons + course) — 3–4 days. Adding video tracking and streak — another 2–3 days. Analytics and alerts — 3–4 days. Final cost is calculated individually after auditing your LMS. Contact us—we'll evaluate the project within one business day. Get a consultation on the optimal tracking level.
Why Choose Us
Years of experience in educational platform development. We have completed 10+ projects, support over 5000 students. We know how to work with PostgreSQL on millions of records without performance degradation. We use proven patterns—Repository, BFF—to keep code maintainable. Our clients reduce training costs by 25% and increase student LTV by 30%. Get in touch for a detailed discussion of your project.







