Skip to Content

Chapitre 7.2 — Requêter le LRS

⏱️ TL;DR — Lire dans un LRS, c’est un GET statements avec des filtres en paramètres d’URL : agent, verb, activity, registration, since, until, limit, ascending. La réponse est un objet avec un tableau statements et, s’il reste des résultats, une propriété more : une URL relative à suivre pour la page suivante — c’est la pagination. Deux subtilités qui piègent : l’agent se passe en JSON encodé dans l’URL (pas juste un e-mail), et les statements annulés (voided) n’apparaissent plus dans les requêtes normales — on les annule en émettant un statement avec le verbe voided. Ce chapitre montre comment récupérer, par exemple, « toutes les complétions d’un apprenant » proprement.

🎯 Objectifs

  • Construire un GET statements filtré et lire l’objet de résultat.
  • Encoder correctement un agent et une IRI de verb / activity dans l’URL.
  • Paginer en suivant more jusqu’à épuisement.
  • Comprendre le mécanisme d’annulation (voiding) et son effet sur les requêtes.
  • Écrire la requête « toutes les complétions de tel apprenant » de bout en bout.

Un GET filtré

Sans filtre, GET statements renvoie tout (paginé) — rarement ce qu’on veut. On affine avec des paramètres de requête. Les principaux :

FiltreTypeSélectionne…
agentAgent en JSON, URL-encodéles statements où cet agent est l’acteur (ou l’objet)
verbIRIles statements portant ce verbe
activityIRIles statements dont l’objet est cette activité
registrationUUIDles statements de cette inscription (une tentative de cours)
sincehorodatage ISO 8601les statements stockés après cette date (exclus)
untilhorodatage ISO 8601les statements stockés jusqu’à cette date (inclus)
limitentiernombre max de statements par page (0 = défaut du LRS)
ascendingbooléenordre chronologique croissant si true (défaut : décroissant)

Exemple : les statements de complétion d’une apprenante, du plus ancien au plus récent.

GET https://lrs.formacampus.fr/xapi/statements?verb=http%3A%2F%2Fadlnet.gov%2Fexpapi%2Fverbs%2Fcompleted&agent=%7B%22mbox%22%3A%22mailto%3Aawa.diop%40ecole.fr%22%7D&ascending=true&limit=50 HTTP/1.1 Host: lrs.formacampus.fr Authorization: Basic Zm9ybWFjYW1wdXM6czNjcjN0 X-Experience-API-Version: 1.0.3

Deux points d’attention dans cette URL :

  • Le verb est une IRI complète, URL-encodée : http://adlnet.gov/expapi/verbs/completed devient http%3A%2F%2F....
  • L’agent n’est pas un simple e-mail : c’est un objet Agent en JSON, lui aussi URL-encodé. Décodé, le paramètre ci-dessus vaut :
{ "mbox": "mailto:awa.diop@ecole.fr" }

⚠️ Piège — Passer agent=awa.diop@ecole.fr. Le filtre agent attend un objet Agent JSON ({"mbox":"mailto:..."} ou {"account":{...}}), pas une chaîne. Et l’identifiant doit correspondre exactement à celui utilisé à l’écriture : si tu as tracé avec un account (homePage + name), filtrer par mbox ne renverra rien. Trace et interroge avec le même type d’identifiant.

En JavaScript

URLSearchParams gère l’encodage pour toi — y compris le JSON de l’agent, qu’il faut JSON.stringify d’abord.

async function findStatements(filters) { const qs = new URLSearchParams(); if (filters.verb) qs.set("verb", filters.verb); if (filters.activity) qs.set("activity", filters.activity); if (filters.registration) qs.set("registration", filters.registration); if (filters.since) qs.set("since", filters.since); if (filters.until) qs.set("until", filters.until); if (filters.limit) qs.set("limit", String(filters.limit)); if (filters.ascending) qs.set("ascending", "true"); if (filters.agent) qs.set("agent", JSON.stringify(filters.agent)); // objet -> JSON const res = await fetch(`${LRS}/statements?${qs.toString()}`, { headers: { "Authorization": AUTH, "X-Experience-API-Version": "1.0.3", }, }); if (!res.ok) throw new Error(`LRS ${res.status}`); return res.json(); // { statements: [...], more: "..." } }

La réponse : statements + more

Un GET statements filtré ne renvoie pas un tableau brut, mais un objet StatementResult :

{ "statements": [ { "id": "fd41c918-b88b-4b20-a0a5-a4c32391aaa0", "actor": { "objectType": "Agent", "mbox": "mailto:awa.diop@ecole.fr" }, "verb": { "id": "http://adlnet.gov/expapi/verbs/completed" }, "object": { "id": "https://formacampus.fr/xapi/activities/module-securite-numerique" }, "result": { "completion": true, "score": { "scaled": 0.85 } }, "timestamp": "2026-03-14T09:32:11Z", "stored": "2026-03-14T09:32:12Z" } ], "more": "/xapi/statements/more/5f2a9c8e10bd" }
  • statements : la page de résultats (au plus limit éléments).
  • more : présent s’il reste des pages. C’est une URL relative (à préfixer par l’hôte du LRS) qui, appelée telle quelle, renvoie la page suivante. Quand more est absent ou vide (""), tu es au bout.

💡 Réflexe — Ne fabrique jamais l’URL de page suivante toi-même (avec un offset, par exemple). xAPI ne marche pas comme ça : tu suis l’URL opaque de more, point. Le LRS y encode ce dont il a besoin (curseur, tri, filtres figés). Reconstruire à la main, c’est se tromper de résultats.

Paginer jusqu’au bout

async function findAllStatements(filters) { const first = await findStatements(filters); let all = first.statements; let more = first.more; while (more) { const res = await fetch(`https://lrs.formacampus.fr${more}`, { headers: { "Authorization": AUTH, "X-Experience-API-Version": "1.0.3", }, }); const page = await res.json(); all = all.concat(page.statements); more = page.more; // vide ou absent => on s'arrête } return all; }

⚠️ Piège — Croire que la première réponse contient tout. Si tu oublies de suivre more, tu ne vois que la première page (souvent quelques dizaines de statements) et tu conclus à tort « il n’y a que ça ». Sur un LRS de production, la pagination est la norme, pas l’exception.

Récupérer un statement précis

Pour un seul statement dont tu connais l’id, pas besoin de filtres : statementId renvoie le statement lui-même (pas un StatementResult).

GET https://lrs.formacampus.fr/xapi/statements?statementId=fd41c918-b88b-4b20-a0a5-a4c32391aaa0 HTTP/1.1 Host: lrs.formacampus.fr Authorization: Basic Zm9ybWFjYW1wdXM6czNjcjN0 X-Experience-API-Version: 1.0.3

Annuler un statement : le voiding

On l’a vu au chapitre 7.1 : un statement est immuable. On ne le supprime pas, on ne le modifie pas. Mais on peut le déclarer erroné : c’est l’annulation (voiding).

Le mécanisme : émettre un nouveau statement dont

  • le verbe est http://adlnet.gov/expapi/verbs/voided (le verbe réservé d’ADL pour ça),
  • l’objet est une référence au statement à annuler, de type StatementRef.
{ "actor": { "objectType": "Agent", "name": "Service pédagogique", "mbox": "mailto:admin@formacampus.fr" }, "verb": { "id": "http://adlnet.gov/expapi/verbs/voided", "display": { "en-US": "voided", "fr-FR": "a annulé" } }, "object": { "objectType": "StatementRef", "id": "fd41c918-b88b-4b20-a0a5-a4c32391aaa0" } }

Effet côté requêtes :

  • Le statement annulé disparaît des résultats d’un GET statements normal. Il n’est pas effacé (l’historique est préservé) mais il est masqué des lectures courantes.
  • Le statement d’annulation, lui, reste dans le magasin (c’est un statement comme un autre).
  • Pour relire un statement annulé, on utilise le paramètre dédié voidedStatementId (au lieu de statementId).

⚠️ Piège — Vouloir « supprimer » un statement faux via une requête DELETE. La ressource statements n’a pas de DELETE : on n’efface pas un fait d’apprentissage. On l’annule (voided + StatementRef), puis on émet le bon. C’est la traçabilité par conception : on voit qu’il y a eu correction, pas juste le résultat final.

💡 Réflexe — Le voiding annule le statement, pas ses effets métier que tu aurais calculés ailleurs (une note recopiée dans un carnet, un badge attribué). Si tu dérives des décisions des statements, prévois de recalculer après une annulation. Le LRS gère l’immuabilité ; la cohérence en aval, c’est toi.

Le cas type : « toutes les complétions d’un apprenant »

Mets bout à bout : on veut, pour Awa, tous ses statements de complétion, du plus ancien au plus récent, en gérant la pagination.

const completions = await findAllStatements({ agent: { mbox: "mailto:awa.diop@ecole.fr" }, verb: "http://adlnet.gov/expapi/verbs/completed", ascending: true, limit: 100, }); // completions = tableau de statements "completed" d'Awa, toutes pages fusionnées. const modulesTermines = completions.map((s) => s.object.id);

Trois décisions y sont visibles : filtrer par agent + verbe (pas juste l’un), demander l’ordre croissant pour une timeline lisible, et suivre more pour ne rien rater. C’est le patron d’une bonne partie des tableaux de bord.

🧭 Sur FormaCampus — Le tableau de bord formateur de FormaCampus liste, par apprenant, les modules terminés et le score. Sous le capot : un GET statements filtré par agent (l’apprenant) et verb=completed, paginé via more, côté serveur (le secret LRS ne quitte pas le back). Pour la vue « une session de formation précise », on ajoute le filtre registration — l’UUID de la tentative — pour ne récupérer que les statements de cette session, sans mélanger les tentatives passées.

🔌 Côté intégration — Quand un LRS tiers alimente ton tableau de bord, deux questions décident de tout : quel type d’identifiant d’acteur est utilisé (tu dois filtrer avec le même : mbox, account…) et quels verbes/activityId sont réellement émis. Un LRS où chaque source invente ses verbes est presque ininterrogeable — d’où l’importance d’un profil partagé (chapitre 7.5).

📚 La spec — Les filtres de GET statements, le format StatementResult, la pagination par more (URL opaque à suivre), le mécanisme de voiding (verbe http://adlnet.gov/expapi/verbs/voided + objet StatementRef) et le paramètre voidedStatementId sont définis dans la spec xAPI d’ADL (adlnet.gov).

✏️ Exercices

Exercice 1 — Débuguer un filtre agent. Un dev cherche les statements d’un apprenant et n’obtient jamais rien, alors qu’il sait que l’apprenant a de l’activité. Sa requête (décodée) :

GET statements?agent="mailto:leo@ecole.fr"&verb=http://adlnet.gov/expapi/verbs/completed

Qu’est-ce qui cloche (deux problèmes possibles) ?

✅ Solution

Problème 1 — le format de l’agent. agent attend un objet Agent JSON, pas une chaîne. Il faut passer {"mbox":"mailto:leo@ecole.fr"} (URL-encodé), pas "mailto:leo@ecole.fr".

Problème 2 — l’identifiant doit correspondre. Si l’activité de Léo a été tracée avec un account ({"account":{"homePage":"https://formacampus.fr","name":"leo-123"}}) et non un mbox, alors filtrer par mbox renvoie zéro résultat même bien formaté. On interroge avec le même type d’identifiant que celui utilisé à l’écriture. (Sans oublier d’encoder les IRI et le JSON dans l’URL.)

Exercice 2 — Pagination. Cette fonction ne renvoie que les 50 premiers statements alors qu’il y en a des centaines. Corrige-la.

async function getAll(filters) { const res = await fetch(`${LRS}/statements?limit=50`, { headers: H }); const data = await res.json(); return data.statements; }

✅ Solution

Elle ignore more. Il faut boucler tant que la réponse contient un more non vide, en appelant l’URL de more (relative, à préfixer par l’hôte du LRS) et en concaténant les pages :

async function getAll(filters) { let url = `${LRS}/statements?limit=50`; let out = []; while (url) { const data = await (await fetch(url, { headers: H })).json(); out = out.concat(data.statements); url = data.more ? `https://lrs.formacampus.fr${data.more}` : null; } return out; }

On suit l’URL de more sans jamais la reconstruire à la main.

Exercice 3 — Corriger sans effacer. Un statement « a réussi » a été émis pour le mauvais apprenant. On veut « le supprimer ». Décris la marche à suivre conforme.

✅ Solution

On n’efface pas (pas de DELETE sur statements). On annule : émettre un statement avec le verbe http://adlnet.gov/expapi/verbs/voided et un objet StatementRef pointant l’id du statement fautif. Il disparaît alors des requêtes normales (relisible via voidedStatementId). Puis, si nécessaire, on émet un nouveau statement correct (bon apprenant, nouvel id). L’historique montre la correction plutôt que de la cacher.

🧠 Quiz de révision

1. Que contient la réponse d’un GET statements filtré ?

Un objet StatementResult : un tableau statements (au plus limit éléments) et, s’il reste des résultats, une propriété more — une URL relative à suivre pour la page suivante. Pas un tableau brut.

2. Comment passe-t-on le filtre agent dans l’URL ?

Comme un objet Agent JSON ({"mbox":"mailto:..."} ou {"account":{...}}), URL-encodé — pas comme une simple chaîne e-mail. Et l’identifiant doit être du même type que celui utilisé à l’écriture, sinon zéro résultat.

3. Comment récupère-t-on la page suivante de résultats ?

En suivant l’URL fournie dans more (relative, à préfixer par l’hôte du LRS), telle quelle, jusqu’à ce que more soit absent ou vide. On ne reconstruit jamais cette URL soi-même (pas d’offset maison).

4. Comment « corrige-t-on » un statement erroné ?

On l’annule (voiding) : un nouveau statement avec le verbe http://adlnet.gov/expapi/verbs/voided et un objet StatementRef visant l’id fautif. Il est alors masqué des requêtes normales (relisible via voidedStatementId), puis on émet le bon statement. Aucune suppression : l’historique est préservé.

5. À quoi sert le filtre registration ?

À ne récupérer que les statements d’une tentative de cours précise (identifiée par un UUID). Indispensable pour isoler une session d’apprentissage d’un apprenant sans mélanger ses tentatives antérieures — par exemple pour un tableau de bord « cette formation-ci ».


Chapitre suivant : 7.3 — State & profils — au-delà du journal d’événements, comment le LRS mémorise l’état courant d’un apprenant pour lui permettre de reprendre.

Last updated on