NPM / ANALYTICS

Analytics with NPM

Step-by-step NPM integration: Player result generation, learner identification, hand-in, static indexes and Analytics reporting, without a database.

1. Install Player and Analytics from NPM

Player opens courses; Analytics reads their result files. Both packages use the same .p2lrn-result contract, so the host application does not need to invent its own data converter.

Terminal
npm install @p2lrn/player @p2lrn/analytics

Confirm that the installed Player and Analytics versions are compatible. The @p2lrn/analytics/results subpath exposes helper functions for loading result sources.

Availability: the new file-based src, result recorder and multi-import APIs exist in the redesign branch. They are not yet confirmed in a published NPM/CDN release. Installing the currently published packages does not guarantee these features; deploy the matching, tested builds only after their publication.

2. Understand the end-to-end workflow

  1. The teacher prepares a .p2lrn course and decides between platform authentication or individual learner codes.
  2. The learner opens the course in Player. Result capture must be configured before the activity begins.
  3. At the end, Player generates a .p2lrn-result file, which is explicitly saved or downloaded.
  4. The result reaches the teacher through a manual hand-in (email, USB, authorized folder) or a file-storage flow implemented by the platform.
  5. Analytics receives a local import or authorized result/index URL via src, checks SHA-256 and computes metrics.
  6. As more results arrive, they are imported or added to the index before refreshing Analytics.

The original course does not subsequently contain every learner's answers, and Player's result recorder does not send files to a remote service by itself. No database is required for this file-based flow.

3. Read a .p2lrn-result file directly via src

Add an Analytics element to the page:

HTML
<p2lrn-analytics id="panel.analytics"></p2lrn-analytics>

Then use it from your application:

JavaScript / TypeScript
import '@p2lrn/analytics';
// Select by HTML id; the dot is part of the id, NOT a CSS class.
const panel = document.getElementById('panel.analytics');
panel.src = '/results/result-001.p2lrn-result';

src addresses the result over HTTP(S), not the .p2lrn course. id="panel.analytics" identifies the HTML element, not a course or learner. Use getElementById('panel.analytics'), since the dot requires escaping in CSS selectors.

The browser must have access to the result URL. For a private file stored on the teacher's computer, use local import rather than inventing an HTTP route.

4. Generate learner results without a platform: teacher code

The teacher assigns a unique, stable code to each learner. The learner enters it before starting; Player includes it in the result's declared identity. This example includes the controls and Player configuration:

HTML
<label for="learner-code">Individual learner code</label>
<input id="learner-code" required autocomplete="off">
<button id="start-course" type="button">Start course</button>
<div id="course-player"></div>
<button id="finish-attempt" type="button" hidden>Download result</button>
JavaScript / TypeScript
import {mountP2LRN} from '@p2lrn/player';

let player;
document.getElementById('start-course').addEventListener('click', async () => {
  const input = document.getElementById('learner-code');
  const code = input.value.trim();
  if (!code) { input.reportValidity(); return; }
  player = await mountP2LRN('#course-player', {
    src: '/courses/course.p2lrn',
    resultIdentity: {mode:'code', issuer:'my-school', code}
  });
  document.getElementById('finish-attempt').hidden = false;
});
document.getElementById('finish-attempt').addEventListener('click', () => {
  if (player?.resultRecorder)
    player.resultRecorder.download('result.p2lrn-result');
});

The button downloads the result into the learner's browser. The result is not automatically sent to the teacher. New attempts require a new result with a distinct attempt ID while retaining the same learner code.

Codes must differ between learners from the same issuer. The teacher must keep their own student-to-code list, because the code alone does not verify real-world identity.

5. Generate results for an authenticated platform user

If the learner is already signed in to an LMS or website, use the identity from that app's authenticated session. issuer names the platform and subjectId is the stable user identifier. The session function below belongs to your application, not P2LRN.

JavaScript / TypeScript
import {mountP2LRN} from '@p2lrn/player';

// Authenticated session data from YOUR host app (not a P2LRN API).
const session = await myPlatform.getAuthenticatedSession();
const player = await mountP2LRN('#course-player', {
  src: '/courses/course.p2lrn',
  resultIdentity: {
    mode: 'platform',
    issuer: session.issuer,
    subjectId: session.userId,
    label: session.displayName
  }
});
// After the learner finishes (e.g. from a button):
const result = await player.resultRecorder.finish();
// Your host may save this unchanged in protected file storage.
// Do not treat the identity claim as proof without host checks.

The host must check permissions and associate the result file with the authenticated user before saving it. A mode:'platform' field in JSON does not by itself provide a signature or proof of identity. Official grades require validation by the authorized host.

6. Collect multiple local results without a server

If learners return files by USB, email or an authorized shared folder, the teacher can import them in one operation. Analytics also includes Open results and Open folder controls (depending on browser support). Programmatically:

HTML
<p2lrn-analytics id="panel.analytics"></p2lrn-analytics>
<input id="ficheros" type="file" accept=".p2lrn-result" multiple>
<button id="importar" type="button">Importar archivos</button>
JavaScript / TypeScript
import '@p2lrn/analytics';
document.getElementById('importar').addEventListener('click', async () => {
  const files = [...document.getElementById('ficheros').files];
  if (!files.length) return;
  const panel = document.getElementById('panel.analytics');
  await panel.importOfflineResults(files); // Verifies SHA-256 and merges attempts.
});

importOfflineResults(files) checks the files, deduplicates identical results and combines attempts. If any selected file is invalid, the batch is rejected. Results remain in the panel's memory until re-imported: Analytics does not permanently store them automatically.

7. Read multiple learners from a URL: static index

To read result files without selecting them every time, place them in an access-controlled web directory and create a small index:

Folder layout
results/
  result-001.p2lrn-result
  result-002.p2lrn-result
  index.p2lrn-results.json
index.p2lrn-results.json
{
  "schema": "https://p2lrn.org/schema/analytics-results-index/1",
  "results": [
    "result-001.p2lrn-result",
    "result-002.p2lrn-result"
  ]
}

Entries in results are paths relative to the index. The result files must share the index's web origin, with at most 500 results per load.

JavaScript / TypeScript
// Same component, multiple result files through a static index:
panel.src = '/results/index.p2lrn-results.json';

The helper API can also load the converted dataset:

JavaScript / TypeScript
import {loadAnalyticsResultSource} from '@p2lrn/analytics/results';
const dataset = await loadAnalyticsResultSource(
  '/results/index.p2lrn-results.json'
);
document.getElementById('panel.analytics').data = dataset;

The index does not store scores or answers. It only lists result files. An unlisted new result will not be discovered by the index URL.

8. Create and refresh the index: manual or Node

Manual update

  1. Save each returned file in the directory without overwriting earlier attempts; use distinct filenames.
  2. Create index.p2lrn-results.json using the JSON above, or open the existing file.
  3. Append the exact filenames of new results to results while keeping previous entries.
  4. Save valid JSON, update the files on the authorized hosting location and click Refresh in Analytics. Check caching if the old index still appears.

Automatic update with Node

Download the standalone index generator and save it next to your results directory. Run it after receiving new files:

Terminal
node p2lrn-results-index.mjs ./results

The script needs no NPM dependencies or database: it checks each direct .p2lrn-result file's schema and SHA-256, sorts filenames and replaces the complete index. If a result is invalid, the index is left unchanged. Nothing is uploaded to the internet. Delivering the files or publishing them with restricted access remains the responsibility of their administrator.

9. Where files are kept and how they reach the teacher

Fully local, no platform

Learners download their result files and return them. The teacher keeps them on their computer and imports them. No index, server or database is necessary.

Static website with access controls

Files and the index are copied to a protected web directory. Analytics reads that URL. The website does not automatically receive or discover files.

Authenticated platform

The application may accept each file, verify the user and keep the unchanged result in private file storage. It must implement its own authorized submission, index-update and teacher-access workflow. This does not require converting data or installing a database by default.

A public static host or JavaScript CDN does not protect learner privacy. Never place identifiable results in an unrestricted public directory. For fully offline import, distribute or host the Analytics assets locally because the CDN requires internet access.

10. Integrity, learner grouping and troubleshooting

Analytics associates attempts using result identity, assignment.packageId and attempt.id. Platform users are grouped by issuer and user ID; account-free learners by issuer and code. Two learners sharing one code can be grouped incorrectly. Older files without explicit identity may be ambiguous.

SHA-256 shows whether file contents match their digest, but it is not a digital signature and does not prove who answered. A user with file access may recompute a digest. Browser-generated scores remain informative until official validation.

New results are missing

Confirm the file was received, added to the index, loaded after refresh and not held in HTTP cache.

404, CORS or denied access

Check the URL, common index/result origin, authenticated session and storage permissions.

Integrity error

Request the original result; do not hand-edit the JSON.

No internet connection

Import local files and serve Analytics assets locally for fully offline use.

11. Optional advanced integration

If your platform already exposes an authorized metrics JSON resource, you can still use src, panel.data or panel.dataSource. An endpoint is not required for working with .p2lrn-result files.

JavaScript / TypeScript
// Advanced integration only, if an authorized Host already provides metrics:
panel.src = '/api/p2lrn/analytics/snapshot';
// Or: panel.data = dataset;
// Or: panel.dataSource = source;

See the complete CDN workflow, the analytics reference and offline delivery for more detail.

12. Player and Analytics on your own platform

Authenticated learners do not download or manually return results. Player automatically creates the same .p2lrn-result format with in_progress and later submitted status. The Host stores it privately, separate from course files, and Analytics reads a teacher-authorized result index. No additional database is required.

JavaScript / NPM
import {createPlayerAnalyticsSession,createHttpResultHost} from '@p2lrn/player';
const host=createHttpResultHost({baseUrl:'/api/p2lrn/results',csrfToken:()=>platform.csrfToken});
const player=document.createElement('p2lrn-player');
const session=await createPlayerAnalyticsSession(player,{identity:{mode:'platform',issuer:user.issuer,subjectId:user.id},courseId:'REAL_COURSE_ID',host});
// Attach the Host before Player mounts so saved activity states are restored.
player.setAttribute('src','/courses/course.p2lrn');
document.querySelector('#player').replaceChildren(player);
// Call session.finish() when the learner completes the course.
Analytics · authorized index
const panel=document.getElementById('panel.analytics');
panel.src=host.indexUrl('REAL_COURSE_ID');
await panel.refresh();

The reference backend is exposed as @p2lrn/player/server/results. Your own platform must provide authenticate, authorize and checkCsrf and a private persistent writable directory. Course storage remains read-only. If saving fails, Player receives an error rather than false confirmation. The Host must verify authenticated learner identities and course permissions and must never expose student result files publicly.

See spec/RESULTS-ANALYTICS.md in the repository. These APIs are present in the integration source; check that the published npm/CDN version includes the contract before relying on it.