@lessonkit/core 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,11 +24,13 @@ __export(index_exports, {
24
24
  ID_PATTERN: () => ID_PATTERN,
25
25
  SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
26
26
  TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
27
+ TELEMETRY_EVENT_CATALOG_V2: () => TELEMETRY_EVENT_CATALOG_V2,
27
28
  assertNever: () => assertNever,
28
29
  assertValidId: () => assertValidId,
29
30
  buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
30
31
  buildLessonkitUrn: () => buildLessonkitUrn,
31
32
  buildTelemetryCatalog: () => buildTelemetryCatalog,
33
+ buildTelemetryCatalogV2: () => buildTelemetryCatalogV2,
32
34
  buildTelemetryEvent: () => buildTelemetryEvent,
33
35
  completeCourseWithTelemetry: () => completeCourseWithTelemetry,
34
36
  completeLessonWithTelemetry: () => completeLessonWithTelemetry,
@@ -64,6 +66,7 @@ __export(index_exports, {
64
66
  resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
65
67
  resolveSessionId: () => resolveSessionId,
66
68
  slugifyId: () => slugifyId,
69
+ telemetryCatalogV2Version: () => telemetryCatalogV2Version,
67
70
  telemetryCatalogVersion: () => telemetryCatalogVersion,
68
71
  tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
69
72
  tryEmitCourseStarted: () => tryEmitCourseStarted,
@@ -247,6 +250,30 @@ function buildTelemetryCatalog() {
247
250
  return TELEMETRY_EVENT_CATALOG.map((entry) => ({ ...entry }));
248
251
  }
249
252
 
253
+ // src/telemetryCatalogV2.ts
254
+ var telemetryCatalogV2Version = 2;
255
+ var TELEMETRY_EVENT_CATALOG_V2 = [
256
+ {
257
+ name: "assessment_answered",
258
+ description: "Learner submitted an assessment interaction answer",
259
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
260
+ dataFields: ["checkId", "interactionType", "question", "response", "correct"],
261
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
262
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
263
+ },
264
+ {
265
+ name: "assessment_completed",
266
+ description: "Assessment interaction completed (passing criteria met)",
267
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
268
+ dataFields: ["checkId", "interactionType", "score", "maxScore", "passingScore"],
269
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
270
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
271
+ }
272
+ ];
273
+ function buildTelemetryCatalogV2() {
274
+ return TELEMETRY_EVENT_CATALOG_V2.map((entry) => ({ ...entry }));
275
+ }
276
+
250
277
  // src/trackingClient.ts
251
278
  function isDevEnvironment() {
252
279
  const g = globalThis;
@@ -279,6 +306,11 @@ function invokeTrackingSink(sink, event) {
279
306
  function createTrackingClient(opts) {
280
307
  const sink = opts?.sink;
281
308
  const batchSink = opts?.batchSink;
309
+ if (batchSink != null && opts?.batch?.enabled === false) {
310
+ throw new Error(
311
+ "[lessonkit] tracking.batchSink cannot be used with batch.enabled: false; omit batch.enabled or set it to true"
312
+ );
313
+ }
282
314
  const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
283
315
  const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
284
316
  const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
@@ -350,7 +382,7 @@ function createTrackingClient(opts) {
350
382
  if (disposed || disposing) return;
351
383
  if (buffer.length >= maxBufferSize) {
352
384
  buffer.shift();
353
- if (!warnedBufferCap && typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
385
+ if (!warnedBufferCap && isDevEnvironment()) {
354
386
  warnedBufferCap = true;
355
387
  console.warn(
356
388
  `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
@@ -390,12 +422,14 @@ function nowIso() {
390
422
 
391
423
  // src/telemetryBuilder.ts
392
424
  var warnedMissingQuizLesson = false;
425
+ var warnedMissingAssessmentLesson = false;
393
426
  function isDevEnvironment2() {
394
427
  const g = globalThis;
395
428
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
396
429
  }
397
430
  function resetTelemetryBuilderWarningsForTests() {
398
431
  warnedMissingQuizLesson = false;
432
+ warnedMissingAssessmentLesson = false;
399
433
  }
400
434
  function resolveLessonId(opts, eventName) {
401
435
  const lessonId = opts.lessonId ?? opts.data?.lessonId;
@@ -444,6 +478,16 @@ function buildTelemetryEvent(opts) {
444
478
  if (!lessonId) throw new Error("quiz_completed requires active lessonId");
445
479
  return { name: "quiz_completed", ...base, lessonId, data: opts.data };
446
480
  }
481
+ case "assessment_answered": {
482
+ const lessonId = opts.lessonId;
483
+ if (!lessonId) throw new Error("assessment_answered requires active lessonId");
484
+ return { name: "assessment_answered", ...base, lessonId, data: opts.data };
485
+ }
486
+ case "assessment_completed": {
487
+ const lessonId = opts.lessonId;
488
+ if (!lessonId) throw new Error("assessment_completed requires active lessonId");
489
+ return { name: "assessment_completed", ...base, lessonId, data: opts.data };
490
+ }
447
491
  case "interaction":
448
492
  return {
449
493
  name: "interaction",
@@ -456,13 +500,21 @@ function buildTelemetryEvent(opts) {
456
500
  }
457
501
  }
458
502
  function tryBuildTelemetryEvent(opts) {
459
- const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
460
- if (isQuiz && !opts.lessonId) {
461
- if (isDevEnvironment2() && !warnedMissingQuizLesson) {
462
- warnedMissingQuizLesson = true;
463
- console.warn(
464
- `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
465
- );
503
+ const needsLesson = opts.name === "quiz_answered" || opts.name === "quiz_completed" || opts.name === "assessment_answered" || opts.name === "assessment_completed";
504
+ if (needsLesson && !opts.lessonId) {
505
+ if (isDevEnvironment2()) {
506
+ if (opts.name.startsWith("quiz_") && !warnedMissingQuizLesson) {
507
+ warnedMissingQuizLesson = true;
508
+ console.warn(
509
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
510
+ );
511
+ }
512
+ if (opts.name.startsWith("assessment_") && !warnedMissingAssessmentLesson) {
513
+ warnedMissingAssessmentLesson = true;
514
+ console.warn(
515
+ `[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
516
+ );
517
+ }
466
518
  }
467
519
  return null;
468
520
  }
@@ -539,7 +591,8 @@ function createMemoryBackedSessionStorage(session) {
539
591
  const warnPersistFailure = () => {
540
592
  if (warnedPersistFailure) return;
541
593
  warnedPersistFailure = true;
542
- if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
594
+ const g = globalThis;
595
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
543
596
  console.warn(
544
597
  "[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
545
598
  );
@@ -580,23 +633,37 @@ function createMemoryBackedSessionStorage(session) {
580
633
  function resetStoragePortForTests(storage) {
581
634
  storage.resetForTests?.();
582
635
  }
636
+ function createInMemorySessionStoragePort() {
637
+ const memory = /* @__PURE__ */ new Map();
638
+ return {
639
+ getItem: (key) => memory.get(key) ?? null,
640
+ setItem: (key, value) => {
641
+ memory.set(key, value);
642
+ },
643
+ removeItem: (key) => {
644
+ memory.delete(key);
645
+ },
646
+ resetForTests: () => {
647
+ memory.clear();
648
+ }
649
+ };
650
+ }
651
+ function resolveBrowserSessionStorage() {
652
+ try {
653
+ if (typeof sessionStorage === "undefined" || sessionStorage == null) {
654
+ return null;
655
+ }
656
+ return sessionStorage;
657
+ } catch {
658
+ return null;
659
+ }
660
+ }
583
661
  function createSessionStoragePort() {
584
- if (typeof sessionStorage === "undefined") {
585
- const memory = /* @__PURE__ */ new Map();
586
- return {
587
- getItem: (key) => memory.get(key) ?? null,
588
- setItem: (key, value) => {
589
- memory.set(key, value);
590
- },
591
- removeItem: (key) => {
592
- memory.delete(key);
593
- },
594
- resetForTests: () => {
595
- memory.clear();
596
- }
597
- };
662
+ const session = resolveBrowserSessionStorage();
663
+ if (!session) {
664
+ return createInMemorySessionStoragePort();
598
665
  }
599
- return createMemoryBackedSessionStorage(sessionStorage);
666
+ return createMemoryBackedSessionStorage(session);
600
667
  }
601
668
  function createGlobalTimer() {
602
669
  return {
@@ -954,11 +1021,13 @@ function defineLifecyclePlugin(plugin) {
954
1021
  ID_PATTERN,
955
1022
  SESSION_STORAGE_KEY,
956
1023
  TELEMETRY_EVENT_CATALOG,
1024
+ TELEMETRY_EVENT_CATALOG_V2,
957
1025
  assertNever,
958
1026
  assertValidId,
959
1027
  buildCourseStartedTelemetryEvent,
960
1028
  buildLessonkitUrn,
961
1029
  buildTelemetryCatalog,
1030
+ buildTelemetryCatalogV2,
962
1031
  buildTelemetryEvent,
963
1032
  completeCourseWithTelemetry,
964
1033
  completeLessonWithTelemetry,
@@ -994,6 +1063,7 @@ function defineLifecyclePlugin(plugin) {
994
1063
  resetTelemetryBuilderWarningsForTests,
995
1064
  resolveSessionId,
996
1065
  slugifyId,
1066
+ telemetryCatalogV2Version,
997
1067
  telemetryCatalogVersion,
998
1068
  tryBuildTelemetryEvent,
999
1069
  tryEmitCourseStarted,
package/dist/index.d.cts CHANGED
@@ -54,7 +54,41 @@ type LessonkitUrnParts = {
54
54
  */
55
55
  declare function buildLessonkitUrn(parts: LessonkitUrnParts): LessonkitUrn;
56
56
 
57
- type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
57
+ /** H5P-aligned interaction kinds for assessment telemetry and xAPI. */
58
+ type AssessmentInteractionType = "mcq" | "trueFalse" | "fillInBlanks" | "markTheWords" | "dragTheWords" | "dragAndDrop" | "assessmentSequence";
59
+ /** Behaviour flags aligned with H5P question types. */
60
+ type AssessmentBehaviour = {
61
+ enableRetry?: boolean;
62
+ enableSolutionsButton?: boolean;
63
+ autoCheck?: boolean;
64
+ };
65
+ /** Payload for xAPI mapping from assessment components. */
66
+ type AssessmentXAPIData = {
67
+ checkId: CheckId;
68
+ interactionType: AssessmentInteractionType;
69
+ response?: string | string[] | boolean | Record<string, unknown>;
70
+ correct?: boolean;
71
+ score?: number;
72
+ maxScore?: number;
73
+ };
74
+ /**
75
+ * Imperative handle for scored blocks (H5P question-type contract analogue).
76
+ * Parent containers (`AssessmentSequence`, future compounds) may call these methods.
77
+ */
78
+ type AssessmentHandle = {
79
+ getScore: () => number;
80
+ getMaxScore: () => number;
81
+ getAnswerGiven: () => boolean;
82
+ resetTask: () => void;
83
+ showSolutions: () => void;
84
+ getXAPIData: () => AssessmentXAPIData;
85
+ };
86
+ type AssessmentBaseProps = AssessmentBehaviour & {
87
+ checkId: CheckId;
88
+ passingScore?: number;
89
+ };
90
+
91
+ type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction";
58
92
  type TelemetryUser = {
59
93
  id?: string;
60
94
  email?: string;
@@ -87,6 +121,20 @@ type QuizCompletedData = {
87
121
  maxScore?: number;
88
122
  passingScore?: number;
89
123
  };
124
+ type AssessmentAnsweredData = {
125
+ checkId: CheckId;
126
+ interactionType: AssessmentInteractionType;
127
+ question?: string;
128
+ response?: string | string[] | boolean | Record<string, unknown>;
129
+ correct?: boolean;
130
+ };
131
+ type AssessmentCompletedData = {
132
+ checkId: CheckId;
133
+ interactionType: AssessmentInteractionType;
134
+ score?: number;
135
+ maxScore?: number;
136
+ passingScore?: number;
137
+ };
90
138
  type InteractionData = {
91
139
  kind?: string;
92
140
  blockId?: BlockId;
@@ -121,6 +169,14 @@ type TelemetryEvent = (TelemetryEventBase & {
121
169
  name: "quiz_completed";
122
170
  lessonId: LessonId;
123
171
  data: QuizCompletedData;
172
+ }) | (TelemetryEventBase & {
173
+ name: "assessment_answered";
174
+ lessonId: LessonId;
175
+ data: AssessmentAnsweredData;
176
+ }) | (TelemetryEventBase & {
177
+ name: "assessment_completed";
178
+ lessonId: LessonId;
179
+ data: AssessmentCompletedData;
124
180
  }) | (TelemetryEventBase & {
125
181
  name: "interaction";
126
182
  lessonId?: LessonId;
@@ -152,6 +208,18 @@ type TelemetryCatalogEntry = {
152
208
  declare const TELEMETRY_EVENT_CATALOG: TelemetryCatalogEntry[];
153
209
  declare function buildTelemetryCatalog(): TelemetryCatalogEntry[];
154
210
 
211
+ declare const telemetryCatalogV2Version: 2;
212
+ type TelemetryCatalogV2Entry = {
213
+ name: Extract<TelemetryEventName, "assessment_answered" | "assessment_completed">;
214
+ description: string;
215
+ requiredFields: string[];
216
+ dataFields: string[];
217
+ xapiVerb: string;
218
+ urnPattern: string;
219
+ };
220
+ declare const TELEMETRY_EVENT_CATALOG_V2: TelemetryCatalogV2Entry[];
221
+ declare function buildTelemetryCatalogV2(): TelemetryCatalogV2Entry[];
222
+
155
223
  declare function createTrackingClient(opts?: {
156
224
  sink?: TelemetrySink;
157
225
  batch?: {
@@ -201,6 +269,14 @@ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
201
269
  name: "quiz_completed";
202
270
  lessonId?: LessonId;
203
271
  data: QuizCompletedData;
272
+ }) | (BuildTelemetryEventContext & {
273
+ name: "assessment_answered";
274
+ lessonId?: LessonId;
275
+ data: AssessmentAnsweredData;
276
+ }) | (BuildTelemetryEventContext & {
277
+ name: "assessment_completed";
278
+ lessonId?: LessonId;
279
+ data: AssessmentCompletedData;
204
280
  }) | (BuildTelemetryEventContext & {
205
281
  name: "interaction";
206
282
  lessonId?: LessonId;
@@ -429,4 +505,4 @@ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin
429
505
  declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
430
506
  declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
431
507
 
432
- export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
508
+ export { type AssessmentAnsweredData, type AssessmentBaseProps, type AssessmentBehaviour, type AssessmentCompletedData, type AssessmentHandle, type AssessmentInteractionType, type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type AssessmentXAPIData, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, TELEMETRY_EVENT_CATALOG_V2, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryCatalogV2Entry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryCatalogV2, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogV2Version, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
package/dist/index.d.ts CHANGED
@@ -54,7 +54,41 @@ type LessonkitUrnParts = {
54
54
  */
55
55
  declare function buildLessonkitUrn(parts: LessonkitUrnParts): LessonkitUrn;
56
56
 
57
- type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
57
+ /** H5P-aligned interaction kinds for assessment telemetry and xAPI. */
58
+ type AssessmentInteractionType = "mcq" | "trueFalse" | "fillInBlanks" | "markTheWords" | "dragTheWords" | "dragAndDrop" | "assessmentSequence";
59
+ /** Behaviour flags aligned with H5P question types. */
60
+ type AssessmentBehaviour = {
61
+ enableRetry?: boolean;
62
+ enableSolutionsButton?: boolean;
63
+ autoCheck?: boolean;
64
+ };
65
+ /** Payload for xAPI mapping from assessment components. */
66
+ type AssessmentXAPIData = {
67
+ checkId: CheckId;
68
+ interactionType: AssessmentInteractionType;
69
+ response?: string | string[] | boolean | Record<string, unknown>;
70
+ correct?: boolean;
71
+ score?: number;
72
+ maxScore?: number;
73
+ };
74
+ /**
75
+ * Imperative handle for scored blocks (H5P question-type contract analogue).
76
+ * Parent containers (`AssessmentSequence`, future compounds) may call these methods.
77
+ */
78
+ type AssessmentHandle = {
79
+ getScore: () => number;
80
+ getMaxScore: () => number;
81
+ getAnswerGiven: () => boolean;
82
+ resetTask: () => void;
83
+ showSolutions: () => void;
84
+ getXAPIData: () => AssessmentXAPIData;
85
+ };
86
+ type AssessmentBaseProps = AssessmentBehaviour & {
87
+ checkId: CheckId;
88
+ passingScore?: number;
89
+ };
90
+
91
+ type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction";
58
92
  type TelemetryUser = {
59
93
  id?: string;
60
94
  email?: string;
@@ -87,6 +121,20 @@ type QuizCompletedData = {
87
121
  maxScore?: number;
88
122
  passingScore?: number;
89
123
  };
124
+ type AssessmentAnsweredData = {
125
+ checkId: CheckId;
126
+ interactionType: AssessmentInteractionType;
127
+ question?: string;
128
+ response?: string | string[] | boolean | Record<string, unknown>;
129
+ correct?: boolean;
130
+ };
131
+ type AssessmentCompletedData = {
132
+ checkId: CheckId;
133
+ interactionType: AssessmentInteractionType;
134
+ score?: number;
135
+ maxScore?: number;
136
+ passingScore?: number;
137
+ };
90
138
  type InteractionData = {
91
139
  kind?: string;
92
140
  blockId?: BlockId;
@@ -121,6 +169,14 @@ type TelemetryEvent = (TelemetryEventBase & {
121
169
  name: "quiz_completed";
122
170
  lessonId: LessonId;
123
171
  data: QuizCompletedData;
172
+ }) | (TelemetryEventBase & {
173
+ name: "assessment_answered";
174
+ lessonId: LessonId;
175
+ data: AssessmentAnsweredData;
176
+ }) | (TelemetryEventBase & {
177
+ name: "assessment_completed";
178
+ lessonId: LessonId;
179
+ data: AssessmentCompletedData;
124
180
  }) | (TelemetryEventBase & {
125
181
  name: "interaction";
126
182
  lessonId?: LessonId;
@@ -152,6 +208,18 @@ type TelemetryCatalogEntry = {
152
208
  declare const TELEMETRY_EVENT_CATALOG: TelemetryCatalogEntry[];
153
209
  declare function buildTelemetryCatalog(): TelemetryCatalogEntry[];
154
210
 
211
+ declare const telemetryCatalogV2Version: 2;
212
+ type TelemetryCatalogV2Entry = {
213
+ name: Extract<TelemetryEventName, "assessment_answered" | "assessment_completed">;
214
+ description: string;
215
+ requiredFields: string[];
216
+ dataFields: string[];
217
+ xapiVerb: string;
218
+ urnPattern: string;
219
+ };
220
+ declare const TELEMETRY_EVENT_CATALOG_V2: TelemetryCatalogV2Entry[];
221
+ declare function buildTelemetryCatalogV2(): TelemetryCatalogV2Entry[];
222
+
155
223
  declare function createTrackingClient(opts?: {
156
224
  sink?: TelemetrySink;
157
225
  batch?: {
@@ -201,6 +269,14 @@ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
201
269
  name: "quiz_completed";
202
270
  lessonId?: LessonId;
203
271
  data: QuizCompletedData;
272
+ }) | (BuildTelemetryEventContext & {
273
+ name: "assessment_answered";
274
+ lessonId?: LessonId;
275
+ data: AssessmentAnsweredData;
276
+ }) | (BuildTelemetryEventContext & {
277
+ name: "assessment_completed";
278
+ lessonId?: LessonId;
279
+ data: AssessmentCompletedData;
204
280
  }) | (BuildTelemetryEventContext & {
205
281
  name: "interaction";
206
282
  lessonId?: LessonId;
@@ -429,4 +505,4 @@ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin
429
505
  declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
430
506
  declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
431
507
 
432
- export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
508
+ export { type AssessmentAnsweredData, type AssessmentBaseProps, type AssessmentBehaviour, type AssessmentCompletedData, type AssessmentHandle, type AssessmentInteractionType, type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type AssessmentXAPIData, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, TELEMETRY_EVENT_CATALOG_V2, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryCatalogV2Entry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryCatalogV2, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogV2Version, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
package/dist/index.js CHANGED
@@ -174,6 +174,30 @@ function buildTelemetryCatalog() {
174
174
  return TELEMETRY_EVENT_CATALOG.map((entry) => ({ ...entry }));
175
175
  }
176
176
 
177
+ // src/telemetryCatalogV2.ts
178
+ var telemetryCatalogV2Version = 2;
179
+ var TELEMETRY_EVENT_CATALOG_V2 = [
180
+ {
181
+ name: "assessment_answered",
182
+ description: "Learner submitted an assessment interaction answer",
183
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
184
+ dataFields: ["checkId", "interactionType", "question", "response", "correct"],
185
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
186
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
187
+ },
188
+ {
189
+ name: "assessment_completed",
190
+ description: "Assessment interaction completed (passing criteria met)",
191
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
192
+ dataFields: ["checkId", "interactionType", "score", "maxScore", "passingScore"],
193
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
194
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
195
+ }
196
+ ];
197
+ function buildTelemetryCatalogV2() {
198
+ return TELEMETRY_EVENT_CATALOG_V2.map((entry) => ({ ...entry }));
199
+ }
200
+
177
201
  // src/trackingClient.ts
178
202
  function isDevEnvironment() {
179
203
  const g = globalThis;
@@ -206,6 +230,11 @@ function invokeTrackingSink(sink, event) {
206
230
  function createTrackingClient(opts) {
207
231
  const sink = opts?.sink;
208
232
  const batchSink = opts?.batchSink;
233
+ if (batchSink != null && opts?.batch?.enabled === false) {
234
+ throw new Error(
235
+ "[lessonkit] tracking.batchSink cannot be used with batch.enabled: false; omit batch.enabled or set it to true"
236
+ );
237
+ }
209
238
  const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
210
239
  const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
211
240
  const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
@@ -277,7 +306,7 @@ function createTrackingClient(opts) {
277
306
  if (disposed || disposing) return;
278
307
  if (buffer.length >= maxBufferSize) {
279
308
  buffer.shift();
280
- if (!warnedBufferCap && typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
309
+ if (!warnedBufferCap && isDevEnvironment()) {
281
310
  warnedBufferCap = true;
282
311
  console.warn(
283
312
  `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
@@ -317,12 +346,14 @@ function nowIso() {
317
346
 
318
347
  // src/telemetryBuilder.ts
319
348
  var warnedMissingQuizLesson = false;
349
+ var warnedMissingAssessmentLesson = false;
320
350
  function isDevEnvironment2() {
321
351
  const g = globalThis;
322
352
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
323
353
  }
324
354
  function resetTelemetryBuilderWarningsForTests() {
325
355
  warnedMissingQuizLesson = false;
356
+ warnedMissingAssessmentLesson = false;
326
357
  }
327
358
  function resolveLessonId(opts, eventName) {
328
359
  const lessonId = opts.lessonId ?? opts.data?.lessonId;
@@ -371,6 +402,16 @@ function buildTelemetryEvent(opts) {
371
402
  if (!lessonId) throw new Error("quiz_completed requires active lessonId");
372
403
  return { name: "quiz_completed", ...base, lessonId, data: opts.data };
373
404
  }
405
+ case "assessment_answered": {
406
+ const lessonId = opts.lessonId;
407
+ if (!lessonId) throw new Error("assessment_answered requires active lessonId");
408
+ return { name: "assessment_answered", ...base, lessonId, data: opts.data };
409
+ }
410
+ case "assessment_completed": {
411
+ const lessonId = opts.lessonId;
412
+ if (!lessonId) throw new Error("assessment_completed requires active lessonId");
413
+ return { name: "assessment_completed", ...base, lessonId, data: opts.data };
414
+ }
374
415
  case "interaction":
375
416
  return {
376
417
  name: "interaction",
@@ -383,13 +424,21 @@ function buildTelemetryEvent(opts) {
383
424
  }
384
425
  }
385
426
  function tryBuildTelemetryEvent(opts) {
386
- const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
387
- if (isQuiz && !opts.lessonId) {
388
- if (isDevEnvironment2() && !warnedMissingQuizLesson) {
389
- warnedMissingQuizLesson = true;
390
- console.warn(
391
- `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
392
- );
427
+ const needsLesson = opts.name === "quiz_answered" || opts.name === "quiz_completed" || opts.name === "assessment_answered" || opts.name === "assessment_completed";
428
+ if (needsLesson && !opts.lessonId) {
429
+ if (isDevEnvironment2()) {
430
+ if (opts.name.startsWith("quiz_") && !warnedMissingQuizLesson) {
431
+ warnedMissingQuizLesson = true;
432
+ console.warn(
433
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
434
+ );
435
+ }
436
+ if (opts.name.startsWith("assessment_") && !warnedMissingAssessmentLesson) {
437
+ warnedMissingAssessmentLesson = true;
438
+ console.warn(
439
+ `[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
440
+ );
441
+ }
393
442
  }
394
443
  return null;
395
444
  }
@@ -466,7 +515,8 @@ function createMemoryBackedSessionStorage(session) {
466
515
  const warnPersistFailure = () => {
467
516
  if (warnedPersistFailure) return;
468
517
  warnedPersistFailure = true;
469
- if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
518
+ const g = globalThis;
519
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
470
520
  console.warn(
471
521
  "[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
472
522
  );
@@ -507,23 +557,37 @@ function createMemoryBackedSessionStorage(session) {
507
557
  function resetStoragePortForTests(storage) {
508
558
  storage.resetForTests?.();
509
559
  }
560
+ function createInMemorySessionStoragePort() {
561
+ const memory = /* @__PURE__ */ new Map();
562
+ return {
563
+ getItem: (key) => memory.get(key) ?? null,
564
+ setItem: (key, value) => {
565
+ memory.set(key, value);
566
+ },
567
+ removeItem: (key) => {
568
+ memory.delete(key);
569
+ },
570
+ resetForTests: () => {
571
+ memory.clear();
572
+ }
573
+ };
574
+ }
575
+ function resolveBrowserSessionStorage() {
576
+ try {
577
+ if (typeof sessionStorage === "undefined" || sessionStorage == null) {
578
+ return null;
579
+ }
580
+ return sessionStorage;
581
+ } catch {
582
+ return null;
583
+ }
584
+ }
510
585
  function createSessionStoragePort() {
511
- if (typeof sessionStorage === "undefined") {
512
- const memory = /* @__PURE__ */ new Map();
513
- return {
514
- getItem: (key) => memory.get(key) ?? null,
515
- setItem: (key, value) => {
516
- memory.set(key, value);
517
- },
518
- removeItem: (key) => {
519
- memory.delete(key);
520
- },
521
- resetForTests: () => {
522
- memory.clear();
523
- }
524
- };
586
+ const session = resolveBrowserSessionStorage();
587
+ if (!session) {
588
+ return createInMemorySessionStoragePort();
525
589
  }
526
- return createMemoryBackedSessionStorage(sessionStorage);
590
+ return createMemoryBackedSessionStorage(session);
527
591
  }
528
592
  function createGlobalTimer() {
529
593
  return {
@@ -880,11 +944,13 @@ export {
880
944
  ID_PATTERN,
881
945
  SESSION_STORAGE_KEY,
882
946
  TELEMETRY_EVENT_CATALOG,
947
+ TELEMETRY_EVENT_CATALOG_V2,
883
948
  assertNever,
884
949
  assertValidId,
885
950
  buildCourseStartedTelemetryEvent,
886
951
  buildLessonkitUrn,
887
952
  buildTelemetryCatalog,
953
+ buildTelemetryCatalogV2,
888
954
  buildTelemetryEvent,
889
955
  completeCourseWithTelemetry,
890
956
  completeLessonWithTelemetry,
@@ -920,6 +986,7 @@ export {
920
986
  resetTelemetryBuilderWarningsForTests,
921
987
  resolveSessionId,
922
988
  slugifyId,
989
+ telemetryCatalogV2Version,
923
990
  telemetryCatalogVersion,
924
991
  tryBuildTelemetryEvent,
925
992
  tryEmitCourseStarted,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/core",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Shared types and telemetry primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -48,8 +48,9 @@
48
48
  "lint": "echo \"(no lint configured yet)\""
49
49
  },
50
50
  "devDependencies": {
51
+ "@types/node": "^22.13.10",
51
52
  "tsup": "^8.5.0",
52
53
  "typescript": "^5.8.3",
53
- "vitest": "^3.2.4"
54
+ "vitest": "^4.1.8"
54
55
  }
55
56
  }