Telemetry & xAPI (1.7.x)

LessonKit emits versioned telemetry events from @lessonkit/react and maps them to xAPI via @lessonkit/xapi.

Event catalog

  • Version: telemetryCatalogV3Version = 3 (exported from @lessonkit/core; v1 catalog uses telemetryCatalogVersion = 1)

  • JSON: @lessonkit/core/telemetry-catalog.v3.json (must match buildTelemetryCatalogV3() in tests)

  • Types: discriminated TelemetryEvent with required courseId on every event

Event

When

Key data fields

course_started

First provider mount per tab session + course

course_completed

completeCourse()

lesson_started

Lesson becomes active

lessonId

lesson_completed

Lesson completed

lessonId, durationMs?

lesson_time_on_task

With lesson_completed when duration known

lessonId, durationMs

quiz_answered

Quiz choice selected

checkId, question, choice, correct

quiz_completed

First correct answer (built-in Quiz) or useQuizState().complete()

checkId, score?, maxScore?

assessment_answered / assessment_completed

P0 assessment blocks

checkId, scores, interaction metadata

interaction

Custom UI / branching via track()

kind, optional blockId, free-form fields

book_page_viewed / compound_page_viewed

Compound navigation

blockId, page index, parent type

slide_viewed

SlideDeck navigation (1.3+)

blockId, slideIndex, slideTitle

video_cue_reached

InteractiveVideo cue activation (1.4+)

blockId, cueIndex, atSeconds, cueLabel?

video_segment_completed

InteractiveVideo cue dismissed/completed (1.4+)

blockId, segmentIndex, atSeconds, segmentLabel?

memory_card_flipped

MemoryGame card flip (1.4+)

blockId, cardIndex, face

information_wall_search

InformationWall search (1.4+)

blockId, query

parallax_slide_viewed

ParallaxSlideshow slide view (1.4+)

blockId, slideIndex

questionnaire_submitted

Questionnaire submit (1.4+)

blockId, fieldCount

branch_node_viewed

BranchingScenario node activation (1.5+)

blockId, nodeId, nodeIndex, nodeTitle?

branch_selected

BranchChoice selection (1.5+)

blockId, fromNodeId, toNodeId, label, scoreWeight?

hotspot_opened

ImageHotspots panel open (1.2+)

blockId, hotspotId

accordion_section_toggled

Accordion expand/collapse (1.2+)

blockId, sectionId, expanded

flashcard_flipped

Flashcards / DialogCards flip (1.2+)

blockId, cardIndex

image_slider_changed

ImageSlider position change (1.2+)

blockId, position

compound_page_viewed

Compound navigation (1.2+)

blockId, page index, parent type

image_juxtaposition_changed

ImageJuxtaposition divider (1.6+)

blockId, position

timeline_event_viewed

Timeline event focus (1.6+)

blockId, eventId

image_sequence_changed

ImageSequence frame change (1.6+)

blockId, frameIndex

audio_recording_started / audio_recording_completed

AudioRecorder (1.6+)

blockId

qr_content_revealed

QrContent reveal (1.6+)

blockId

advent_door_opened

AdventCalendar door (1.6+)

blockId, doorId, day

map_stage_viewed / map_exit_selected

GameMap navigation (1.6+)

blockId, stageId / exitId

Session fields on all events: sessionId, optional attemptId, optional user.

xAPI mapping

Canonical mapper: telemetryEventToXAPIStatement(event) in @lessonkit/xapi.

Telemetry

xAPI verb

Object URN

course_started

initialized

…:course:{courseId}

course_completed

completed

…:course:{courseId}

lesson_started

initialized

…:lesson:{lessonId}

lesson_completed

completed

…:lesson:{lessonId} (+ result.duration when durationMs set)

lesson_time_on_task

(none)

Returns null — use lesson_completed for xAPI duration

quiz_answered

answered

…:check:{checkId} (+ result.success when correct set)

quiz_completed

completed

…:check:{checkId} (+ score when provided)

assessment_answered

answered

…:check:{checkId} (+ result.success when correct set)

assessment_completed

completed

…:check:{checkId} (+ score when provided)

interaction

experienced

…:block:{blockId} when lessonId and data.blockId are set

book_page_viewed

experienced

…:block:{blockId}

slide_viewed

experienced

…:block:{blockId}

compound_page_viewed

experienced

…:block:{blockId}

hotspot_opened

experienced

…:block:{blockId}

accordion_section_toggled

experienced

…:block:{blockId}

flashcard_flipped

experienced

…:block:{blockId}

image_slider_changed

experienced

…:block:{blockId}

video_cue_reached

experienced

…:block:{blockId}

video_segment_completed

completed

…:block:{blockId}

memory_card_flipped

experienced

…:block:{blockId}

information_wall_search

experienced

…:block:{blockId}

parallax_slide_viewed

experienced

…:block:{blockId}

questionnaire_submitted

completed

…:block:{blockId}

branch_node_viewed

experienced

…:block:{blockId}:node:{nodeId}

branch_selected

experienced

…:block:{blockId}:node:{toNodeId}

React runtime: after each track(), the provider calls the mapper and xapi.send(statement) when a statement is returned (single path; no duplicate lifecycle helpers).

Prefer createFetchTransport from @lessonkit/xapi for production LRS delivery (timeout, retry backoff, keepalive exitTransport for pagehide). See Telemetry & xAPI guide.

Custom interactions and blocks

For block-level xAPI on interaction events:

  1. Set blockId on Scenario / Reflection (or pass blockId in interaction payload).

  2. Ensure an active lessonId (normal when inside Lesson).

  3. Call track("interaction", { kind: "…", blockId: "my-block", }).

Without blockId, interaction events are tracked but do not emit xAPI.

Batching and buffer limits

When config.tracking.batch.enabled is true (or batchSink is set), events are queued in an in-memory buffer before delivery.

Setting

Default

Behavior

batch.flushIntervalMs

5000

Periodic flush while the sink is slow or unavailable

batch.maxBatchSize

25

Flush when the buffer reaches this size

Internal telemetry buffer cap

1000 events

When full, new events are dropped until the buffer drains

If the sink throws or rejects, failed events are re-queued (entire batch for batchSink; undelivered tail for per-event sinks). Under prolonged outage the telemetry buffer can hit the cap; production is silent unless you wire config.observability.onTelemetryBufferDrop. Monitor sink failures via onTelemetrySinkError (covers both sink and batchSink). See production checklist.

The xAPI in-memory queue (default 1000 statements) drops the oldest statement when full — wire onXapiQueueCap and onXapiQueueDepth.

Non-batched mode (batch.enabled: false) invokes sink synchronously per event with no buffer cap.

Tab exit and pagehide

LessonkitProvider calls flushOnExit (keepalive batch / xAPI delivery when configured) then async flush on visibilitychange (hidden) and pagehide. Wire exitTransport from createFetchTransport and exitBatchSink from createFetchBatchSink for best-effort delivery when the tab closes.

Identity

All events require courseId. Lesson-scoped events require lessonId. Component ids are trimmed at the React provider boundary (assertValidId) so telemetry payloads and xAPI URNs stay aligned. See Identity reference.

course_started dedupe

The runtime uses separate session-storage marks:

Key pattern

Purpose

lessonkit:course_started:{sessionId}:{courseId}

xAPI / session bootstrap (may fire before tracking sink is ready)

lessonkit:course_started_tracking:{sessionId}:{courseId}

Tracking sink delivery — set only after a successful tracking flush

lessonkit:course_started_pipeline_delivered:{sessionId}:{courseId}

Non-tracking pipeline (xAPI mapper, bridge, extra sinks)

When config.session.sessionId changes, migrateCourseStartedMark moves dedupe state to the new session id so learners do not receive duplicate course_started events after LMS handoff.

Quiz and assessment telemetry

Built-in Quiz / KnowledgeCheck must be wrapped in <Lesson>. Events without an enclosing lessonId are dropped by tryBuildTelemetryEvent.

P0 assessment blocks (TrueFalse, FillInTheBlanks, etc.) emit assessment_answered / assessment_completed. Legacy Quiz emits quiz_answered / quiz_completed. Both namespaces map to xAPI; LMS bridge forwarding uses bridge.track for answered events and submitAssessment for completion (see Packaging reference).

LMS bridge (LXPack)

When config.lxpack.bridge is "auto", completion events (course_completed, lesson_completed, assessment_completed, quiz_completed) call the parent lxpackBridge.v1 API. Answered events (assessment_answered, quiz_answered) forward via bridge.track. Wire observability.onLxpackBridgeMiss and onLxpackBridgeError in production.

Production observability

Required hooks depend on what you enable — see the production checklist for the full matrix. When both tracking and xAPI delivery are configured, wire all six hooks including onXapiTransportError (required for xAPI, not optional):

Config

Required hooks

Tracking or xAPI enabled

onLxpackBridgeMiss

Tracking delivery (sink or batchSink)

+ onTelemetrySinkError, onTelemetryBufferDrop

xAPI delivery (transport or client)

+ onXapiQueueDepth, onXapiQueueCap, onXapiTransportError

Without these hooks, buffer/queue drops and sink failures are silent in production builds.