MONITORING / METRICS
Metrics, progress and analytics
P2LRN separates portable content from tracking data. The Player displays status, scores, events and progression; The integrating platform decides what data to persist and how to relate it to users, intents, reports and analytics systems.
1. Tracking data sources
During playback, P2LRN exposes four types of information for different purposes: resume status, scores, events, and progression.
loadState and saveState They serve to resume an activity exactly where it was. It is a replaceable snapshot, not a history.
reportScore provides a normalized score when the activity has a usable grade.
emitEvent receives the chronological flow p2lrn:*. It is the right source for analytics, auditing and time series.
The Player maintains a separate state with viewed, completed, passed and scorePercent to apply course rules.
Host Adapter and events p2lrn:* are the interfaces intended for tracking and persistence. The Player's visible HTML and the internal content of SCORM/H5P iframes are part of the presentation, not the data contract.
2. Integration architecture
The Player remains independent of the host database. The integrating application provides the identity of the user and the attempt; Host Adapter matches that private data with the portable identifiers exposed by P2LRN.
Identity, registration, group, call and attempt belong to the integrating platform. They are not stored within the .p2lrn and P2LRN does not impose its data model.
3. Context and identification of activities
He context The Player uses package and graph identifiers. Combining them allows each tracking data to be associated with the exact version of the content without introducing private LMS identifiers into the package.
packageIdLogical identity of the package.
contentRootContent integrity footprint. It changes when content changes and avoids accidentally restoring state over another revision.
entityIdActivity, unit or root to which the data belongs.
resourceIdResource associated with the activity when it exists.
representationIdConcrete representation that the Player is executing.
rendererIdPresentation/runtime chosen for that representation.
profile / runtimeSome system states, such as progression, identify their surface using these fields.
A robust persistence key typically combines learnerId and attemptId of the platform with packageId, contentRoot, entityId and the surface (rendererId, profile or runtime).
4. Integration via Host Adapter
The following example separates the private platform data from the P2LRN portable contract. currentUser, currentAttempt and learningApi belong to the integrating application; The Player only invokes the functions defined by the Host Adapter.
const session = Object.freeze({
learnerId: currentUser.id,
attemptId: currentAttempt.id,
});
function metricKey(context) {
return {
learnerId: session.learnerId,
attemptId: session.attemptId,
packageId: context.packageId,
contentRoot: context.contentRoot,
entityId: context.entityId ?? null,
surface:
context.profile ??
context.rendererId ??
context.runtime ??
'package',
};
}
const adapter = P2LRN.createHostAdapter({
async loadState(context) {
return learningApi.loadState(metricKey(context));
},
async saveState(state, context) {
await learningApi.saveState(metricKey(context), state);
},
async reportScore(score, context) {
await learningApi.saveScore(metricKey(context), score);
},
async emitEvent(event, context) {
await learningApi.appendEvent({
...metricKey(context),
event,
});
},
});
document.querySelector('p2lrn-player').adapter = adapter;Tokens, private endpoints, and user identity should remain outside the package. The platform API must authenticate and authorize each persistence operation.
5. Callback contract
loadState(context)Called when opening an activity or progression. Returns the last supported snapshot or null. It is not an analytical event.
saveState(state, context)Receives the complete status that allows you to resume. You must replace the previous snapshot of that same logical key.
reportScore(score, context)Receive a structured score. It can be invoked more than once in contents that make successive commits; the “last”, “best” or “per attempt” policy belongs to the integrating platform.
emitEvent(event, context)Receive the canonical envelope of the event. The calls are serialized in order by Adapter; It is the recommended way for an analytical log.
6. Canonical structure of events
All persistable events share the same envelope. He payload changes depending on the type of event and context allows it to be attributed to the corresponding activity.
{
"schema": "https://p2lrn.org/schema/runtime-event/1",
"version": "1",
"type": "p2lrn:score",
"timestamp": "2026-10-16T08:30:00.000Z",
"context": {
"packageId": "urn:uuid:...",
"contentRoot": "sha256:...",
"entityId": "urn:uuid:...",
"resourceId": "urn:uuid:...",
"representationId": "urn:uuid:...",
"rendererId": "https://p2lrn.org/renderer/questionnaire/standard"
},
"payload": {
"graded": true,
"raw": 8,
"max": 10,
"normalized": 0.8
}
}In DOM listeners, the payload fields also appear directly in event.detail and the full wrap is available in event.detail.event. In HostAdapter.emitEvent() the canonical event is received directly.
7. Data available in quizzes and games
Quizzes and quiz-based games share the same session. The contract allows you to obtain answers, progress, time, attempts, final score and detail per question without depending on the visible interface.
p2lrn:answerIncludes questionId and the normalized answer to that question.
p2lrn:progressIncludes value from 0 to 1, answered and total. It is progress within the quiz, not a percentage of the completed course.
p2lrn:statechangeSnapshot with answers, current question, number of attempts completed, saved score, game status and timing.
p2lrn:timeupdateDisplays remaining time for the attempt and question when time limits exist.
p2lrn:timeoutIndicates whether the entire attempt or a specific question timed out.
p2lrn:scoreProvide total grade and breakdown by question.
p2lrn:completeProvide score, final answers and reason for ending.
Native punctuation has this format. normalized goes from 0 to 1 when the correction is automatic; if the correction mode belongs to the host, graded es false and normalized es null.
{
"graded": true,
"raw": 8,
"max": 10,
"normalized": 0.8,
"questions": [
{
"questionId": "q1",
"graded": true,
"correct": true,
"hits": 1,
"errors": 0,
"pointsAwarded": 2,
"pointsPossible": 2
}
]
}The details of successes, errors, points obtained and unanswered questions are obtained through score.questions and the status of responses. The visible interface should not be used as a source to reconstruct this data.
8. Monitoring data in SCORM 1.2 and SCORM 2004
SCORM maintains its CMI model. P2LRN preserves that state and delivers it via saveState and p2lrn:statechange. The object contains state.values; the exact keys depend on the version and what the SCO actually writes.
Note: cmi.core.score.raw, max, min. Status: cmi.core.lesson_status. Position: cmi.core.lesson_location. Tiempo: cmi.core.session_time and cmi.core.total_time.
Note: cmi.score.raw, max, min, scaled. Status: cmi.completion_status and cmi.success_status. Progreso: cmi.progress_measure. Position: cmi.location. Tiempo: cmi.session_time and cmi.total_time.
cmi.interactions.* may contain id, type, learner response, result, latency, correct answers and objectives when published by the SCO.
cmi.objectives.* preserves objectives and their notes/states when the package uses them.
P2LRN deriva reportScore when doing Commit or Terminate if there is a usable note and it has changed. For SCORM 2004 use first cmi.score.scaled; if it does not exist, you can normalize raw / max. The field passed comes out of cmi.success_status in 2004 or cmi.core.lesson_status in 1.2.
Supporting a CMI key in runtime does not imply that all packages write it. The state should be kept as received and each field should be treated as optional.
9. Tracking data in H5P and xAPI
H5P communicates status for resume and can also issue xAPI. P2LRN forwards each statement as p2lrn:xapi and, when it contains a result, it derives a common score so that the integrating platform can obtain the basic score without interpreting each type of H5P content.
p2lrn:statechangeH5P status obtained from runtime to resume activity.
p2lrn:xapiIncludes the original statement in payload.statement, in addition to runtime:"h5p", source:"content-runtime" and authoritative:false.
p2lrn:scoreDerives raw, max, normalized, passed and completed from statement.result when they are available.
reportScoreCalled for xAPI terminal with verbs completed, passed or failed. The score is still marked as non-authoritative.
p2lrn:completeIt is emitted when receiving one of those terminal verbs and includes the statement and the derived punctuation.
Before sending them to an LRS, the platform server must rebuild actor, authority, registration/attempt and any other data controlled by the LMS.
10. Course Progression Status
The progression uses a state separate from the internal states of each activity. The Player recalculates it from rendered, completion and scoring, saves it using the Host Adapter and outputs p2lrn:progressionchange when it changes.
{
"version": "p2lrn-progression-state/1",
"entities": {
"urn:uuid:activity-1": {
"viewed": true,
"completed": true,
"passed": true,
"scorePercent": 82.5,
"updatedAt": "2026-10-16T08:30:00.000Z"
}
}
}p2lrn:rendered you can mark an activity as viewed; p2lrn:complete the completed mark; the scores provide passed and scorePercent. Then they apply required, gateFollowing, completionRequirement, minScore and unlockWhen.
p2lrn:progress represents internal progress of an activity.The overall progress of the course is obtained from the progression status and the rules declared by its entities. For example, a questionnaire can use p2lrn:progress to indicate how many questions have been answered.
11. Persistence model
Persistence typically separates the resume snapshot from the analytic history. The physical scheme belongs to the integrating platform; Separation by responsibility avoids mixing data with different life cycles.
One record per learner/attempt/activity/surface. It is replaced with each saveState.
Each report can be kept per attempt or subject to an explicit last or best grade policy. P2LRN does not define that policy.
Optional log append-only with the canonical envelope, plus a receivedAt of the server.
Entity of the integrating platform with start, closure, registration, user and business rules. It is not part of the package.
To safely resume, the key must include contentRoot. If the content changes, the platform can migrate the state or initiate a compatible attempt instead of silently applying a snapshot of another version.
12. High frequency event management
High frequency events should not automatically become a persistent write for each change. p2lrn:timeupdate, responses and successive commits can be grouped according to analytics requirements.
saveStatepreserves the last resume snapshot and does not replace an event history.- Temporal analytics can be persisted asynchronously or in batches from
emitEvent. - The event includes
timestamp; For auditing, it is also advisable to add areceivedAtgenerated by the server. - The business session time should also be measured from the opening and closing of the attempt on the server, without depending solely on the browser's clock.
- CMI times published by SCO can be maintained in SCORM; in timed quizzes, the timing status; in H5P, xAPI durations should be treated as content-generated data.
13. Retries, duplicates, and idempotency
emitEvent It is delivered in order within a single Adapter, but the P2LRN event does not define a durable database identifier. When the transport can retry requests, the server must use its own ingestion key.
A common strategy is to generate an identifier upon receiving the event and, when deterministic deduplication is required, combine intent + type + timestamp + context + payload hash. The exact policy depends on whether analytics should preserve legitimate replays.
14. Common integration cases
reportScore provides raw, max, normalized and, when they exist, passed/completed.
loadState + saveState They allow you to keep the snapshot by attempt and activity.
The questionnaires expose p2lrn:answer, score.questions and complete.answers.
The keys cmi.interactions.* The saved state contains any interactions that the package has published.
p2lrn:xapi preserves the statement as content input; identity and authority They must be rebuilt on the server before being sent to the LRS.
p2lrn-progression-state/1 preserves the state upon which the Player applies progression rules.
15. Privacy and trust limits
The P2LRN package is portable and should not contain the learner's personal history. Host Adapter belongs to the trusted context of the host. Content events can describe the activity performed, but identity, registration, intent, permissions and final authority belong to the integrating platform.
The reference of Events documents the contract for each event; Progress describes the termination rules and Integration explains the connection with the platform services.
Rich pedagogical analytics
In addition to status, grade and completion, the native runtime allows you to observe the pedagogical journey with semantic events that do not depend on the visible HTML.
p2lrn:attempt differentiates a new attempt, a resumption and a final delivery. This avoids inferring sessions from browser times or reloads.
p2lrn:interaction with action:"answer-change" allows you to measure reconsideration and navigation by question without duplicating the raw response in the analytical event.
Voting, note posting/deletion, contribution, and peer review generate semantic interactions within the same event contract.
Relevant opens and interactions can be incorporated into the history with their activity/resource context, keeping the presentation separate from the analytics.
The platform can combine these events with contentRoot, entityId, intent, and host user to build engagement, resumption, collaboration, and content usage metrics without introducing those private identifiers into the .p2lrn.
Learning events and active time API
The Web SDK alpha.11 can project runtime events to an ordered learning flow without changing the envelope p2lrn:*.
createLearningEvent()Normalizes a runtime event with schema, version, id, sequence, category and context.
createLearningEventStream()Preserves sequence and, optionally, sessionId/attemptId.
observeLearningEvents()Looks at the known families in an EventTarget and outputs the normalized model.
createActivityTracker()Measure active time considering visibility, focus and interaction. By default, the interaction is limited to the Player so as not to count third-party UI clicks.
Active time is opt-in telemetry; It does not by itself substitute a pedagogical rule, an official note or an external temporal authority.
P2LRN Analytics
When you need to query these metrics for many learners and courses, P2LRN Analytics provides an embeddable panel via CDN or NPM. Keeps observed activity and Host authoritative notes separate.