Skip to Content

Chapitre 7.4 — Tracer hors du LMS

⏱️ TL;DR — C’est le super-pouvoir de xAPI, ce que SCORM ne saura jamais faire : tracer l’apprentissage partout — appli mobile native, atelier présentiel avec la tablette du formateur, simulateur métier, geste sur le terrain — et même hors-ligne. Deux problèmes à régler proprement. Un : l’identité de l’apprenant, puisqu’il n’y a pas de lancement LMS qui te la fournit — tu dois l’établir toi-même (compte applicatif, SSO). Deux : la connexion intermittente — tu captures dans une file d’attente locale, avec l’id et le timestamp figés à l’événement, et tu synchronises en PUT idempotent dès que le réseau revient. Ce chapitre montre le connecteur hors-ligne et les patrons d’identité.

🎯 Objectifs

  • Identifier les sources hors LMS où xAPI brille et SCORM échoue.
  • Identifier l’acteur sans lancement LMS (compte applicatif, SSO, account vs mbox).
  • Concevoir une file d’attente hors-ligne avec synchro différée idempotente.
  • Préserver le timestamp réel de l’événement malgré une synchro tardive.
  • Câbler plusieurs sources sur un seul LRS cohérent.

Là où SCORM s’arrête

Rappel de la limite structurelle de SCORM (Partie 2) : il ne tracke que dans un navigateur, lancé depuis un LMS. Sitôt qu’on sort de ce cadre, il n’a rien à proposer. Or l’apprentissage réel déborde largement l’écran du LMS :

SourceExemple concretPourquoi SCORM ne peut pas
Appli mobile nativeMicro-learning dans les transportsPas de navigateur LMS, souvent hors-ligne
PrésentielLe formateur valide un geste sur sa tabletteAucun contenu web lancé par un LMS
SimulateurUn simulateur de conduite ou de soinsApplication métier autonome
TerrainUn apprenti valide une opération en atelierPas d’écran LMS du tout
Objet connecté / jeuUn serious game, un capteurHors du modèle SCORM

xAPI traite tout ça pareil : chaque expérience devient un statement acteurverbeobjet envoyé au LRS. Le LRS ne sait pas (et se moque de savoir) si le statement vient d’un module web, d’une appli iOS ou d’une tablette de formateur. C’est ce découplage qui débloque le hors-LMS.

Problème n°1 : qui est l’apprenant ?

Dans un module SCORM ou cmi5, le LMS te donne l’apprenant (au lancement, via cmi.core.student_id en SCORM, le paramètre actor en cmi5). Hors LMS, personne ne te le donne : c’est à toi d’identifier l’acteur de façon stable et cohérente.

Rappel (Partie 6) : un acteur xAPI s’identifie par mbox, mbox_sha1sum, account (homePage + name) ou openid. Le choix n’est pas anodin.

IdentifiantFormeQuand l’utiliser
mboxmailto:awa.diop@ecole.frSi tu as un e-mail fiable et stable
mbox_sha1sumempreinte SHA1 du mailto:Même chose, mais e-mail non exposé en clair
accounthomePage + name (id interne)Le plus robuste hors LMS : ton id utilisateur applicatif
openidune URL OpenIDSi tu fédères l’identité via OpenID

Pour une appli mobile FormaCampus, le bon choix est presque toujours account : l’apprenant se connecte à l’appli (compte applicatif, ou SSO de l’établissement), et tu l’identifies par un id interne stable.

{ "objectType": "Agent", "name": "Awa Diop", "account": { "homePage": "https://formacampus.fr", "name": "user-48213" } }

⚠️ Piège — Identifier le même apprenant différemment selon la source. S’il est mbox dans le module web du LMS et account dans l’appli mobile, le LRS voit deux acteurs distincts : ses statistiques sont coupées en deux, la reprise cross-device ne marche pas, le tableau de bord ment. Décide d’une stratégie d’identité et applique-la partout — ou tiens une table de correspondance côté back. C’est la décision d’architecture xAPI à ne pas rater.

💡 Réflexe — Préfère un id interne opaque (account.name = "user-48213") à l’e-mail comme identifiant principal. Un e-mail change (mariage, changement d’établissement) et c’est une donnée personnelle exposée. Un id interne est stable et neutre — tu gardes la correspondance id ↔ personne dans ta base, pas répétée dans chaque statement. On y revient côté vie privée au chapitre 7.5.

Problème n°2 : le réseau qui va et vient

Une appli mobile trace dans le métro, un formateur en atelier sans Wi-Fi fiable, un technicien en zone blanche. Envoyer chaque statement en direct au LRS n’est pas envisageable : il faut capturer maintenant, envoyer plus tard.

Le patron : une file d’attente locale (SQLite, IndexedDB, fichier…) où chaque statement est complet et figé à la capture — surtout son id (UUID) et son timestamp. Un synchroniseur vide la file dès qu’une connexion est là, en PUT idempotent.

Trois exigences non négociables :

  1. timestamp à la capture, pas à l’envoi. Sinon un événement de mardi synchronisé jeudi sera daté de jeudi. On horodate l’événement au moment où il se produit (chapitre 7.1).
  2. id (UUID) généré à la capture. C’est lui qui rend la synchro idempotente : si l’appli n’est pas sûre qu’un envoi a abouti, elle rejoue le même PUT sans risque de doublon.
  3. File persistante. Elle survit à la fermeture de l’appli et à l’extinction du téléphone. Une file en mémoire perd tout au premier crash.

Le connecteur hors-ligne

// File persistante (ici abstraite : queue.add / queue.all / queue.remove). // En pratique : IndexedDB (web), SQLite/AsyncStorage (mobile). function captureStatement(partial) { const statement = { ...partial, id: crypto.randomUUID(), // (2) id fige a la capture timestamp: new Date().toISOString(), // (1) heure reelle de l evenement }; return queue.add(statement); // (3) persiste localement, tout de suite } async function syncQueue() { if (!navigator.onLine) return; // rien a tenter hors ligne for (const stmt of await queue.all()) { try { const res = await fetch(`${LRS}/statements?statementId=${stmt.id}`, { method: "PUT", headers: { "Content-Type": "application/json", "Authorization": AUTH, // via un proxy serveur cote mobile "X-Experience-API-Version": "1.0.3", }, body: JSON.stringify(stmt), }); // 204 = cree ; 409 = deja present : dans les deux cas, c est envoye. if (res.status === 204 || res.status === 409) { await queue.remove(stmt.id); } // 400/401 : erreur permanente -> a logguer, sortir de la file "en erreur" // (ne pas boucler indefiniment sur un statement invalide). } catch (networkError) { break; // reseau retombe : on garde la file, on retentera plus tard } } } // Declencher la synchro au retour du reseau et periodiquement. window.addEventListener("online", syncQueue); setInterval(syncQueue, 30_000);

💡 Réflexe — Pour vider une grosse file, POST un tableau de statements en un appel (chapitre 7.1) : plus efficace que N appels. Chaque statement du tableau garde son id figé dans son corps, donc l’idempotence est préservée. PUT un par un reste plus simple à raisonner pour le retrait de file ; POST par lots pour la performance. Choisis selon le volume.

⚠️ Piège — Boucler indéfiniment sur un statement invalide. Un 400 (statement mal formé) ne se résoudra jamais par un renvoi : si tu le laisses en tête de file, tu bloques toute la synchro derrière lui. Distingue erreur réseau/5xx (on retente) d’erreur client 4xx (on sort le statement de la file de synchro, on le loggue pour investigation). Sans ça, un seul statement pourri gèle toute la remontée.

Le cas de l’atelier présentiel

Cas typique FormaCampus : un formateur, en atelier, valide sur sa tablette que chaque apprenti a réussi un geste technique. Ici, l’acteur du statement n’est pas l’utilisateur de l’appareil (le formateur) mais l’apprenti observé. Le formateur est, lui, l’autorité (qui atteste).

{ "actor": { "objectType": "Agent", "name": "Karim Benali", "account": { "homePage": "https://formacampus.fr", "name": "app-10744" } }, "verb": { "id": "https://formacampus.fr/xapi/verbs/a-realise-le-geste", "display": { "fr-FR": "a réalisé le geste" } }, "object": { "objectType": "Activity", "id": "https://formacampus.fr/xapi/activities/soudure-tig-passe-fond", "definition": { "name": { "fr-FR": "Soudure TIG : passe de fond" }, "type": "http://adlnet.gov/expapi/activities/performance" } }, "result": { "success": true }, "context": { "instructor": { "objectType": "Agent", "name": "M. Rossi", "account": { "homePage": "https://formacampus.fr", "name": "formateur-22" } }, "platform": "Tablette formateur", "registration": "b7d3c1a0-5e42-4f8a-9c11-3a2b6e0f7d99" }, "timestamp": "2026-05-06T14:07:00+02:00" }

Deux points d’expert ici : l’acteur est l’apprenti (pas la tablette), et le formateur figure dans context.instructor — le LRS sait qui a fait et qui a attesté. Le context.platform documente la provenance. C’est un niveau de finesse impossible en SCORM.

💡 Réflexe — Un seul appareil, plusieurs acteurs : sur la tablette du formateur, on change d’acteur à chaque apprenti observé. Ne code pas l’acteur en dur au niveau de l’appareil ; fais-en un paramètre de la capture. La tablette est un outil de saisie multi-apprenants, pas le compte d’une personne.

Un seul LRS, plusieurs sources

Tout converge vers un LRS. C’est là que se paie (ou se rentabilise) la cohérence : si module web, appli mobile et tablette utilisent la même identité d’acteur, les mêmes IRI d’activités et un profil de verbes partagé, alors le tableau de bord agrège une vue de l’apprenant. Sinon, tu obtiens un LRS rempli de données… incomparables entre elles. D’où le chapitre suivant.

🧭 Sur FormaCampus — FormaCampus branche trois sources sur son LRS : le module web (dans le LMS des écoles), l’appli mobile (micro-learning hors-ligne, file d’attente + synchro PUT idempotente), et la tablette formateur en atelier (validation de gestes, acteur = l’apprenti, formateur en context.instructor). Clé de voûte : une seule identité d’acteur (un account avec l’id interne FormaCampus) partagée par les trois. Résultat : le tableau de bord montre, pour chaque apprenant, et ses modules web et son activité mobile et ses validations d’atelier, sur une même timeline. C’est ce que SCORM seul ne permettra jamais.

🔌 Côté intégration — Côté mobile, l’auth Basic vers le LRS ne doit pas vivre dans le binaire de l’appli (décompilable). On passe par un proxy côté serveur FormaCampus : l’appli s’authentifie auprès de son back (session utilisateur), et c’est le back qui détient le secret LRS et relaie les statements. Bonus : le proxy peut valider, enrichir (ajouter authority, context) et filtrer avant d’écrire dans le LRS. Ne jamais exposer un credential LRS dans un client distribué.

📚 La spec — Les modes d’identification de l’acteur (mbox, mbox_sha1sum, account, openid), le champ context (dont instructor, platform, registration) et l’usage de authority sont définis dans la spec xAPI d’ADL (adlnet.gov). xAPI ne prescrit pas de mécanisme de file d’attente : c’est un patron d’implémentation, mais l’idempotence du PUT (via statementId) et l’immuabilité des statements sont, elles, garanties par la spec — ce sont elles qui le rendent sûr.

✏️ Exercices

Exercice 1 — L’apprenant coupé en deux. Le tableau de bord montre, pour Awa, deux profils séparés : l’un avec son activité web, l’autre avec son activité mobile, jamais réunis. Diagnostic et correctif ?

✅ Solution

Awa est identifiée différemment selon la source : par ex. mbox (mailto:awa.diop@ecole.fr) dans le module web, et account ({homePage, name:"user-48213"}) dans l’appli mobile. Le LRS les traite comme deux acteurs distincts → deux profils. Correctif : adopter une seule stratégie d’identité appliquée à toutes les sources (idéalement un account avec l’id interne stable), ou, si l’historique est déjà pollué, tenir une table de correspondance côté back et réconcilier. C’est la première décision d’architecture d’un déploiement xAPI multi-sources.

Exercice 2 — La date qui saute. Une appli mobile trace hors-ligne le lundi et synchronise le mercredi. Sur le tableau de bord, toutes les activités du lundi apparaissent mercredi. Où est le bug ?

✅ Solution

Le timestamp du statement est posé au moment de l’envoi (mercredi) au lieu du moment de l’événement (lundi) — ou bien il est laissé vide, et le LRS met alors son heure de réception (stored, mercredi). Correctif : figer timestamp: new Date().toISOString() à la capture, dans la file locale, et l’envoyer tel quel à la synchro. Le stored du LRS restera mercredi (normal), mais le timestamp — celui qu’on affiche — dira bien lundi.

Exercice 3 — Le statement qui bloque la file. La synchro d’une appli s’est arrêtée : plus rien ne remonte depuis deux jours, alors que le réseau est bon. En tête de file, un statement renvoie 400 à chaque tentative. Que se passe-t-il et comment coder pour l’éviter ?

✅ Solution

Le statement en tête de file est invalide (400 : mal formé, IRI manquante…). Comme le code ne retire de la file que sur succès et retente en boucle, ce statement pourri bloque tous les suivants — la file ne se vide jamais. Correctif : traiter 4xx comme une erreur permanente (retirer le statement de la file de synchro, le logguer/mettre en quarantaine pour investigation) et ne retenter que sur erreur réseau ou 5xx. Un envoi ne doit jamais pouvoir geler toute la remontée.

🧠 Quiz de révision

1. Pourquoi xAPI peut-il tracer hors du LMS là où SCORM ne peut pas ?

Parce que SCORM ne tracke que dans un navigateur lancé depuis un LMS, via son API JS locale. xAPI, lui, envoie des statements à un LRS par une API REST : n’importe quelle source (appli mobile, tablette, simulateur, terrain) peut émettre, en ligne ou différé. Le LRS ne se soucie pas de la provenance.

2. Comment identifie-t-on l’apprenant sans lancement LMS, et quel est le piège ?

On l’établit soi-même (compte applicatif, SSO), le plus souvent via un account (homePage + id interne stable). Le piège : identifier le même apprenant différemment selon la source (mbox ici, account là) — le LRS voit alors deux acteurs distincts et coupe ses données en deux. Une seule stratégie d’identité, partout.

3. Pourquoi figer l’id et le timestamp à la capture, pas à l’envoi ?

Le timestamp à la capture préserve l’heure réelle de l’événement malgré une synchro tardive (sinon tout est daté du jour de synchro). L’id (UUID) figé rend la synchro idempotente : rejouer le même PUT après une incertitude réseau ne crée pas de doublon (le LRS reconnaît l’id).

4. Comment éviter qu’un statement invalide bloque toute la file de synchro ?

En distinguant les erreurs : réseau / 5xx → on retente (statement gardé en file) ; 4xx (400/401) → erreur permanente, on sort le statement de la file de synchro et on le loggue. Un 400 ne se résout jamais par un renvoi ; le laisser en tête de file gèle tout ce qui est derrière.

5. Sur la tablette d’un formateur qui valide des gestes, qui est l’acteur du statement ?

L’apprenti observé, pas le formateur ni la tablette. Le formateur, qui atteste, va dans context.instructor ; la provenance dans context.platform. On change d’acteur à chaque apprenti : l’appareil est un outil de saisie multi-apprenants, pas le compte d’une personne.


Chapitre suivant : 7.5 — Profils xAPI & bonnes pratiques — maintenant que tout converge vers un LRS, comment garder ces données exploitables : verbes stables, gouvernance, vie privée.

Last updated on