Instrument the content rather than the LMS. Record the last content position before session end, using the Page Visibility API and a sendBeacon call on pagehide. Aggregate by position across 30–50 sessions per module and the drop-off points are usually obvious without any statistical work.
Why the LMS can't tell you
Your LMS knows three things: whether the module was completed, roughly how long it was open, and what the assessment score was.
All three are module-level facts. Drop-off is a position-level question. The gap between those is why most L&D teams can describe their problem ("completion is low") but not locate it ("the escalation section loses people").
Some platforms expose partial progress — "learner reached 60%" — which is closer but still not enough. 60% of what? A percentage of elapsed time is not a content position, and it is not something you can hand to an instructional designer.
What you need is: for each session that ended before completion, which content position was active when it ended.
What to instrument
Three things, in order of importance:
1. Content position. Every section, slide or segment needs a stable identifier that survives content updates. This is the part people skip and regret — if your identifiers are positional indices, inserting a slide invalidates all your historical data.
2. Session end. Both the deliberate kind (closing the tab) and the ambiguous kind (walking away). Handle these differently.
3. Focus state. So you can distinguish "left at position 14" from "had position 14 open in a background tab for 40 minutes then closed it." Very different diagnoses.
The minimum implementation
This is genuinely small. The core is a position tracker plus a beacon on exit.
let currentPosition = null;
let positionEnteredAt = Date.now();
let focusedMs = 0;
let lastFocusAt = document.hasFocus() ? Date.now() : null;
// Track which section is on screen
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
currentPosition = entry.target.dataset.sectionId;
positionEnteredAt = Date.now();
}
});
}, { threshold: [0.5] });
document.querySelectorAll('[data-section-id]').forEach((el) => observer.observe(el));
// Accumulate genuinely focused time, not just elapsed time
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (lastFocusAt) focusedMs += Date.now() - lastFocusAt;
lastFocusAt = null;
} else {
lastFocusAt = Date.now();
}
});
// Report on exit — sendBeacon survives page teardown, fetch usually does not
addEventListener('pagehide', () => {
if (lastFocusAt) focusedMs += Date.now() - lastFocusAt;
navigator.sendBeacon('/collect', JSON.stringify({
moduleId: MODULE_ID,
lastPosition: currentPosition,
elapsedMs: Date.now() - SESSION_START,
focusedMs,
completed: COURSE_COMPLETE,
}));
});
Two details worth not getting wrong:
Use pagehide, not beforeunload. beforeunload is unreliable on mobile and is ignored in some back/forward-cache scenarios. pagehide fires in cases beforeunload does not.
Use sendBeacon, not fetch. The browser is tearing the page down. sendBeacon is designed to survive that; a normal request will frequently be cancelled and you will silently lose exactly the sessions you most wanted to measure.
How much data you need
Less than people expect, because you are looking for a spike rather than a subtle effect.
| Sessions per module | What you can see |
|---|---|
| 10–20 | Pronounced drop-off points, if any exist |
| 30–50 | Reliable content-level patterns |
| 100+ | Cohort comparison, and A/B testing content variants |
| 500+ | Segment-level differences by role or region |
If 60% of your leavers go at the same position, that is visible at 20 sessions. You do not need statistical sophistication to find a cliff.
Reading the result
Aggregate by position and plot the count of sessions ending at each. Then interpret the shape:
A cliff — a single position taking a large share of all exits. The clearest and best result. Rewrite that section.
A staircase — several distinct drops. Usually means multiple issues, or one issue repeating (each new concept overloading).
A slope — steady attrition with no features. This is fatigue or monotony rather than any single failure, and the answer is usually structural: shorter modules, more format variation.
A cliff in the first two minutes — the opening failed. Extremely common, and usually front-loaded abstraction.
A spike right before the assessment — people reached the test, realised they were not prepared, and left rather than fail. The problem is upstream of where the exit shows.
Drop-off tells you where people left. Replay tells you where they struggled. A position with high replay and high drop-off is cognitive overload — they tried twice and gave up. High replay with low drop-off is a clarity problem worth fixing but not urgent. Those two need completely different interventions, and drop-off data alone cannot distinguish them.
Common mistakes
Conflating pauses with drop-offs. A session ending at position 14 that resumes at position 14 tomorrow is a pause. Counting it as a drop-off inflates the problem and points you at an innocent section. Track resumption separately.
Using elapsed time instead of focused time. A module open in a background tab for 40 minutes reads as deep engagement. Focus-adjust or the data actively misleads.
Positional identifiers that break on edit. Use stable IDs, not slide numbers. Otherwise every content update destroys your history.
Instrumenting everything at once. Start with one important module. You will learn more from one well-instrumented course than from partial data across twenty.
Reporting at individual level. Aggregating to content level answers the question you actually have — what should we fix — and avoids the DPIA complexity, works council conversation and cultural resistance that individual-level tracking brings. It is both the easier and the more useful choice.
The whole method reduces to one sentence: record which section was active when each session ended, aggregate by section, and look for the spike. Everything above is detail on doing that without lying to yourself.
Frequently asked questions
Can't my LMS already show me this?
How many sessions before the data is meaningful?
Does this need a webcam?
What about learners who leave and come back?
Or skip the build
Signals collects drop-off, dwell and tab-switching from one script tag, mapped to your content structure.