Tracking Emission: SCORM and xAPI¶
Reference document for how a published eXeLearning package reports learner scores to a host (LMS / LRS). Two channels coexist:
- SCORM 1.2/2004 — emitted only by SCORM exports, via the bundled SCORM API wrapper. Pre-existing behaviour.
- xAPI (Experience API) — emitted by every export format, always on, via
libs/xapi/exe_xapi.js. Added so any published package is xAPI-compatible out of the box, with no export-time option.
Both channels are fed from the same single score source in
public/app/common/common.js (the gamification namespace), so the score math
is never duplicated.
Related: export-pipeline.md · libraries.md · ids.md
Scope¶
- This feature adds xAPI statement emission to published packages.
- It does not replace SCORM — SCORM output is unchanged and both channels can run side by side.
- It does not implement cmi5. cmi5 requires additional launch (fetch
token), session
LaunchData, AU metadata,moveOnrules, cmi5-defined context categories and packaging semantics that are intentionally out of scope here. Theinitialized/terminatedstatements below are generic xAPI lifecycle statements, not cmi5 ones. - The package only emits statements. The host LMS/LRS is responsible for authentication, learner identity, validation, storage and gradebook mapping. The emitter never invents learner PII.
1. How SCORM is emitted¶
SCORM emission lives in public/app/common/common.js under
$exeDevices.iDevice.gamification.scorm, with the ADL wrapper files added to
SCORM exports only.
| Piece | Where |
|---|---|
| Score logic | gamification.scorm in public/app/common/common.js |
| SCORM API wrapper (pipwerks) | public/app/common/scorm/SCORM_API_wrapper.js |
SCO lifecycle (loadPage/unloadPage) |
public/app/common/scorm/SCOFunctions.js |
| Injected for | SCORM exports only — SCORM_LIBRARIES + getScormHeadScripts() |
| Runtime gate | $("body").hasClass("exe-scorm") |
Flow:
- The exporter sets
body class="exe-scorm exe-scorm12"and injects the wrapper scripts in<head>(Scorm12Exporter.getScormHeadScripts()). - On load,
SCOFunctions.loadPage()callsscorm.init()(pipwerks). - Each gradable iDevice calls
gamification.scorm.registerActivity(game)and, on submit,gamification.scorm.sendScoreNew(auto, game). - Per-iDevice scores are serialised into
cmi.suspend_data(<n>. "<title>"; Score: <s>%; Weight: <w>%). - The weighted package total (
getFinalScore) is written tocmi.core.score.rawand the status tocmi.core.lesson_status(passedwhen total ≥ 50, elsefailed).
Every gamification.scorm.* method early-returns when pipwerks is undefined,
so SCORM does nothing outside SCORM exports.
2. How xAPI is emitted¶
xAPI emission lives in public/app/common/xapi/exe_xapi.js
($exeDevices.iDevice.xapi), included in every export via BASE_LIBRARIES.
It does not depend on SCORM/pipwerks.
2.1 Wiring at export time¶
| Piece | Where |
|---|---|
| Emitter library | public/app/common/xapi/exe_xapi.js |
| Always-on inclusion | BASE_LIBRARIES (src/shared/export/constants.ts) + FileSystemResourceProvider.fetchBaseLibraries() |
Identity config in <head> |
window.exeXapi injected by PageRenderer (both multi-page and single-page heads) |
| Config source | Html5Exporter / PageExporter pass { odeId, packageTitle, language } from meta |
The exporter injects, before the emitter script:
<script>window.exeXapi={"odeId":"202604272111114JQLDV","packageTitle":"…","language":"en"}</script>
<script src="libs/xapi/exe_xapi.js"> </script>
The serialized config is HTML-safe: PageRenderer.serializeForScript() escapes
< (→ <) plus U+2028/U+2029 before embedding, so a package title containing
</script> cannot break out of the inline <script> (no XSS).
The config shape is a single source of truth: the TypeScript type XapiConfig
(src/shared/export/interfaces.ts) declares exactly the keys the emitter reads
in exe_xapi.js#_resolveConfig. It has two groups of keys:
- Identity keys —
odeId,baseIri,activityId,packageTitle,language. Populated byHtml5Exporter/PageExporterfrommetaon every export. - Delivery keys —
parentOrigin,actor,registration. These are opt-in and NOT populated by the default export pipeline. Origin-restricted postMessage delivery (parentOrigin), a pre-resolved learneractor, and an attemptregistrationrequire runtime context the static exporter does not have (the embedding origin / LMS-provided learner). They are supplied at runtime by the embedding bridge (or by xAPI launch URL params; see §2.4) rather than baked into the export. When absent, the emitter broadcasts to'*'with an anonymous actor.
2.2 Identifiers (IRIs)¶
Stable, derived from the package odeId and per-iDevice odeIdeviceId
(see ids.md):
- Package activity:
https://exelearning.net/xapi/{odeId} - Per iDevice:
https://exelearning.net/xapi/{odeId}/idevice/{odeIdeviceId}
When no odeId is available the emitter falls back to the document URL, so
statements stay structurally valid.
2.3 Statements¶
Fed from gamification.track('answered', game) (called by sendScoreNew, before
the SCORM gate) and the running aggregate (reusing the pure getFinalScore):
- Per iDevice — verb
answered, object = per-iDevice IRI,result.score = { scaled, raw, min: 0, max: 10 }. - Package — verb
completedpluspassed/failedat the same ≥ 50 threshold,result.score = { scaled, raw, min: 0, max: 100 }. - Lifecycle (generic xAPI, not cmi5) —
initializedemitted once on load andterminatedonce onpagehide/unload, both against the package Activity and only when a transport is available. They carry noresult.
Each statement also includes richer metadata:
object.definitionwith a stabletypeIRI (…/activities/assessmentfor the package,…/activities/cmi.interactionfor an iDevice) and a localizednamelanguage map using the packagelanguage.context.registrationwhen a registration value is available, from the launch URL (registration=) or the injected config (window.exeXapi.registration).context.contextActivities.parentlinking each iDevice statement to the package Activity.context.extensionswith eXeLearning metadata, each key present only when its value is available (no invented data), under stable IRIs:https://exelearning.net/xapi/extensions/package-idhttps://exelearning.net/xapi/extensions/idevice-idhttps://exelearning.net/xapi/extensions/idevice-typehttps://exelearning.net/xapi/extensions/page-id(when supplied by the event)https://exelearning.net/xapi/extensions/page-title(when supplied by the event)
Statement shape follows the xAPI Data spec: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md.
2.4 Transport (silent fall-through)¶
window.postMessageto the parent (default; for packages embedded in a host activity module). Message contract:
window.parent.postMessage({ type: 'exe-xapi-statement', statement }, targetOrigin);
targetOrigin is the configured parentOrigin so the statement is delivered
only to the intended host. It falls back to '*' only when no
parentOrigin is configured (best-effort delivery); this is safe because the
emitter never puts learner PII in a statement (anonymous account agent when
none is supplied).
Embedding platforms such as Moodle can consume these statements by adding a listener in the host activity module. In Moodle, a proper integration should validate the authenticated user/session, verify that the statement belongs to the current activity instance, and map accepted statements to Moodle events and/or gradebook updates — for example through a plugin xAPI handler. This host-side consumption is not implemented in this repository.
- LRS POST when xAPI launch parameters are present in the URL
(
endpoint,auth, optionalactor,registration):
POST {endpoint}statements
Authorization: {auth}
X-Experience-API-Version: 1.0.3
See https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md.
- No-op when neither a parent window nor launch parameters exist (plain web
/ offline EPUB). All transport code is wrapped in
try/catchso tracking can never break page rendering.
2.5 Notes¶
- The emitter debounces duplicate statements (same iDevice + same score) and
assigns each statement a UUID
idfor LRS idempotency. - The package-level statement reuses
gamification.scorm.getFinalScore()(a pure function) for the weighted total — single source of truth with SCORM. - The
initialized/terminatedlifecycle statements are emitted at most once and are plain xAPI lifecycle statements. They are not cmi5: this emitter does not implement cmi5 launch, fetch token,LaunchData,moveOnrules, AU metadata, cmi5 context categories or cmi5 packaging. - xAPI primer: https://xapi.com/statements-101/. Moodle xAPI subsystem reference: https://moodledev.io/docs/apis/subsystems/xapi.