Розробка LMS-платформи з підтримкою xAPI (Experience API)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка LMS-платформи з підтримкою xAPI (Experience API)
Складна
від 2 тижнів до 3 місяців
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Підтримка xAPI (Experience API / Tin Can) в LMS

xAPI — сучасна заміна SCORM. На відміну від SCORM, xAPI не потребує iframe та працює через REST API до Learning Record Store (LRS). Записує "statements" — твердження типу "Іван завершив урок Python", "Марія набрала 85 балів на тесті".

Концепція xAPI Statement

{
  "actor": {
    "objectType": "Agent",
    "name": "Ivan Ivanov",
    "mbox": "mailto:[email protected]"
  },
  "verb": {
    "id": "http://adlnet.gov/expapi/verbs/completed",
    "display": { "en-US": "completed", "uk-UA": "завершив" }
  },
  "object": {
    "objectType": "Activity",
    "id": "https://lms.example.com/courses/python-basics/lessons/variables",
    "definition": {
      "name": { "uk-UA": "Змінні в Python" },
      "type": "http://adlnet.gov/expapi/activities/lesson"
    }
  },
  "result": {
    "score": { "scaled": 0.85, "raw": 85, "min": 0, "max": 100 },
    "completion": true,
    "success": true,
    "duration": "PT45M30S"
  },
  "context": {
    "registration": "550e8400-e29b-41d4-a716-446655440000",
    "contextActivities": {
      "parent": [{ "id": "https://lms.example.com/courses/python-basics" }]
    }
  },
  "timestamp": "2026-03-28T10:30:00Z"
}

Користувацький LRS (Learning Record Store)

LRS — це REST-сервіс, який приймає та зберігає xAPI statements. Можна використовувати готові рішення (SCORM Cloud, Learning Locker, ADL LRS) або побудувати власне:

import { Router } from 'express';
const xapi = Router();

// PUT/POST /xapi/statements — прийняти statement(и)
xapi.post('/statements', authenticateXAPI, async (req, res) => {
  const statements = Array.isArray(req.body) ? req.body : [req.body];

  const ids = await Promise.all(
    statements.map(async (stmt) => {
      // Перевірити обов'язкові поля
      if (!stmt.actor || !stmt.verb || !stmt.object) {
        throw new Error('Invalid xAPI statement: missing required fields');
      }

      // Додати ID якщо немає
      if (!stmt.id) stmt.id = crypto.randomUUID();

      // Зберегти
      await db.xapiStatements.create({
        id: stmt.id,
        actor: stmt.actor,
        verb: stmt.verb,
        object: stmt.object,
        result: stmt.result ?? null,
        context: stmt.context ?? null,
        timestamp: stmt.timestamp ? new Date(stmt.timestamp) : new Date(),
        storedAt: new Date(),
      });

      // Оновити прогрес учня
      await updateLearnerProgress(stmt);

      return stmt.id;
    })
  );

  res.status(200).json(ids);
});

// GET /xapi/statements — запросити statements
xapi.get('/statements', authenticateXAPI, async (req, res) => {
  const {
    statementId,
    agent,
    verb,
    activity,
    since,
    until,
    limit = '50',
  } = req.query;

  const statements = await db.xapiStatements.query({
    statementId: statementId as string,
    actor: agent ? JSON.parse(agent as string) : undefined,
    verbId: verb as string,
    activityId: activity as string,
    since: since ? new Date(since as string) : undefined,
    until: until ? new Date(until as string) : undefined,
    limit: Math.min(Number(limit), 500),
  });

  // xAPI потребує заголовок X-Experience-API-Version
  res.setHeader('X-Experience-API-Version', '1.0.3');
  res.json({
    statements,
    more: '',  // URL для пагінації якщо більше результатів
  });
});

Оновлення прогресу зі Statements

async function updateLearnerProgress(stmt: XAPIStatement) {
  // Витягти id учня
  const email = stmt.actor.mbox?.replace('mailto:', '') ??
    stmt.actor.account?.name;
  if (!email) return;

  const user = await db.users.findByEmail(email);
  if (!user) return;

  // Визначити тип події за глаголом
  const verbId = stmt.verb.id;
  const activityId = stmt.object.id;

  const VERB_COMPLETED = 'http://adlnet.gov/expapi/verbs/completed';
  const VERB_PASSED = 'http://adlnet.gov/expapi/verbs/passed';
  const VERB_FAILED = 'http://adlnet.gov/expapi/verbs/failed';
  const VERB_ANSWERED = 'http://adlnet.gov/expapi/verbs/answered';
  const VERB_PROGRESSED = 'http://adlnet.gov/expapi/verbs/progressed';

  switch (verbId) {
    case VERB_COMPLETED:
    case VERB_PASSED:
      await db.lessonProgress.markCompleted(user.id, activityId, {
        score: stmt.result?.score?.scaled,
        duration: parseDuration(stmt.result?.duration),
        completedAt: new Date(stmt.timestamp ?? new Date()),
      });
      await checkCourseCompletion(user.id, activityId);
      break;

    case VERB_FAILED:
      await db.lessonProgress.markFailed(user.id, activityId, {
        score: stmt.result?.score?.scaled,
      });
      break;

    case VERB_ANSWERED:
      await db.quizAnswers.create({
        userId: user.id,
        questionId: activityId,
        score: stmt.result?.score?.raw,
        success: stmt.result?.success,
      });
      break;

    case VERB_PROGRESSED:
      const progress = stmt.result?.extensions?.[
        'https://w3id.org/xapi/video/extensions/progress'
      ];
      if (progress) {
        await db.lessonProgress.updateProgress(user.id, activityId, Number(progress));
      }
      break;
  }
}

Аутентифікація LRS

xAPI використовує Basic Auth або OAuth 2.0 для авторизації запитів від контенту:

function authenticateXAPI(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Basic ')) {
    res.setHeader('WWW-Authenticate', 'Basic realm="xAPI LRS"');
    return res.status(401).end();
  }

  const [key, secret] = Buffer.from(authHeader.slice(6), 'base64')
    .toString()
    .split(':');

  // Перевірити ключ/секрет додатка
  const app = lrsClients.find(c => c.key === key && c.secret === secret);
  if (!app) return res.status(401).end();

  req.lrsClient = app;
  next();
}

xAPI Launch — запуск контенту без iframe

На відміну від SCORM, xAPI-контент запускається прямо та сам відправляє statements через fetch:

// Генерація Launch URL з параметрами
function generateXAPILaunchUrl(
  courseUrl: string,
  userId: string,
  userEmail: string
): string {
  const params = new URLSearchParams({
    endpoint: `${process.env.APP_URL}/xapi/`,
    auth: `Basic ${Buffer.from(`${lrsKey}:${lrsSecret}`).toString('base64')}`,
    actor: JSON.stringify({
      objectType: 'Agent',
      name: userId,
      mbox: `mailto:${userEmail}`,
    }),
    registration: crypto.randomUUID(),
  });

  return `${courseUrl}?${params.toString()}`;
}

Аналітика через xAPI

LRS накопичує rich-дані про поведінку учнів — дозволяє будувати детальну аналітику:

-- Середній бал по урокам
SELECT
  s.object->>'id' AS activity_id,
  s.object->'definition'->'name'->>'uk-UA' AS lesson_name,
  AVG((s.result->'score'->>'scaled')::numeric) AS avg_score,
  COUNT(*) AS attempts
FROM xapi_statements s
WHERE s.verb->>'id' = 'http://adlnet.gov/expapi/verbs/completed'
  AND s.result->'score' IS NOT NULL
GROUP BY 1, 2
ORDER BY avg_score;

Строки виконання

Базовий LRS з прийманням statements та оновленням прогресу — 1 тиждень. З OAuth2, аналітикою та підтримкою xAPI Launch — ще 3–5 днів.