@lessonkit/core 1.1.0 → 1.2.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 +583 -184
- package/dist/index.d.cts +189 -26
- package/dist/index.d.ts +189 -26
- package/dist/index.js +564 -184
- package/package.json +6 -2
- package/telemetry-catalog.v2.json +21 -0
- package/telemetry-catalog.v3.json +53 -0
package/dist/index.cjs
CHANGED
|
@@ -20,20 +20,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES: () => ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
24
|
+
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES: () => ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
25
|
+
COMPOUND_MAX_NESTING_DEPTH: () => COMPOUND_MAX_NESTING_DEPTH,
|
|
26
|
+
COMPOUND_RESUME_SCHEMA_VERSION: () => COMPOUND_RESUME_SCHEMA_VERSION,
|
|
23
27
|
ID_MAX_LENGTH: () => ID_MAX_LENGTH,
|
|
24
28
|
ID_PATTERN: () => ID_PATTERN,
|
|
29
|
+
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: () => INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
30
|
+
PAGE_ALLOWED_CHILD_TYPES: () => PAGE_ALLOWED_CHILD_TYPES,
|
|
25
31
|
SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
|
|
26
32
|
TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
|
|
27
33
|
TELEMETRY_EVENT_CATALOG_V2: () => TELEMETRY_EVENT_CATALOG_V2,
|
|
34
|
+
TELEMETRY_EVENT_CATALOG_V3: () => TELEMETRY_EVENT_CATALOG_V3,
|
|
28
35
|
assertNever: () => assertNever,
|
|
29
36
|
assertValidId: () => assertValidId,
|
|
30
37
|
buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
|
|
31
38
|
buildLessonkitUrn: () => buildLessonkitUrn,
|
|
39
|
+
buildPluginContext: () => buildPluginContext,
|
|
32
40
|
buildTelemetryCatalog: () => buildTelemetryCatalog,
|
|
33
41
|
buildTelemetryCatalogV2: () => buildTelemetryCatalogV2,
|
|
42
|
+
buildTelemetryCatalogV3: () => buildTelemetryCatalogV3,
|
|
34
43
|
buildTelemetryEvent: () => buildTelemetryEvent,
|
|
44
|
+
clampCompoundPageIndex: () => clampCompoundPageIndex,
|
|
45
|
+
clearCompoundState: () => clearCompoundState,
|
|
35
46
|
completeCourseWithTelemetry: () => completeCourseWithTelemetry,
|
|
36
47
|
completeLessonWithTelemetry: () => completeLessonWithTelemetry,
|
|
48
|
+
compoundStateStorageKey: () => compoundStateStorageKey,
|
|
49
|
+
createCompoundResumeState: () => createCompoundResumeState,
|
|
37
50
|
createDefaultClock: () => createDefaultClock,
|
|
38
51
|
createGlobalTimer: () => createGlobalTimer,
|
|
39
52
|
createLessonkitRuntime: () => createLessonkitRuntime,
|
|
@@ -49,10 +62,13 @@ __export(index_exports, {
|
|
|
49
62
|
defineLifecyclePlugin: () => defineLifecyclePlugin,
|
|
50
63
|
defineTelemetryPlugin: () => defineTelemetryPlugin,
|
|
51
64
|
deriveId: () => deriveId,
|
|
65
|
+
getAllowedChildTypes: () => getAllowedChildTypes,
|
|
52
66
|
getTabSessionId: () => getTabSessionId,
|
|
53
67
|
hasCourseStarted: () => hasCourseStarted,
|
|
54
68
|
hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
|
|
55
69
|
hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
|
|
70
|
+
isChildTypeAllowed: () => isChildTypeAllowed,
|
|
71
|
+
loadCompoundState: () => loadCompoundState,
|
|
56
72
|
markCourseStarted: () => markCourseStarted,
|
|
57
73
|
markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
|
|
58
74
|
markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
|
|
@@ -60,13 +76,16 @@ __export(index_exports, {
|
|
|
60
76
|
nowIso: () => nowIso,
|
|
61
77
|
parseBlockId: () => parseBlockId,
|
|
62
78
|
parseCheckId: () => parseCheckId,
|
|
79
|
+
parseCompoundResumeState: () => parseCompoundResumeState,
|
|
63
80
|
parseCourseId: () => parseCourseId,
|
|
64
81
|
parseLessonId: () => parseLessonId,
|
|
65
82
|
resetStoragePortForTests: () => resetStoragePortForTests,
|
|
66
83
|
resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
|
|
67
84
|
resolveSessionId: () => resolveSessionId,
|
|
85
|
+
saveCompoundState: () => saveCompoundState,
|
|
68
86
|
slugifyId: () => slugifyId,
|
|
69
87
|
telemetryCatalogV2Version: () => telemetryCatalogV2Version,
|
|
88
|
+
telemetryCatalogV3Version: () => telemetryCatalogV3Version,
|
|
70
89
|
telemetryCatalogVersion: () => telemetryCatalogVersion,
|
|
71
90
|
tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
|
|
72
91
|
tryEmitCourseStarted: () => tryEmitCourseStarted,
|
|
@@ -178,6 +197,122 @@ function buildLessonkitUrn(parts) {
|
|
|
178
197
|
return urn;
|
|
179
198
|
}
|
|
180
199
|
|
|
200
|
+
// src/compound.ts
|
|
201
|
+
var COMPOUND_RESUME_SCHEMA_VERSION = 1;
|
|
202
|
+
function createCompoundResumeState(input = {}) {
|
|
203
|
+
const childStates = {};
|
|
204
|
+
if (input.childStates) {
|
|
205
|
+
for (const [key, value] of Object.entries(input.childStates)) {
|
|
206
|
+
childStates[key] = value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
|
|
211
|
+
activePageIndex: input.activePageIndex ?? 0,
|
|
212
|
+
...input.activeChapterIndex !== void 0 ? { activeChapterIndex: input.activeChapterIndex } : {},
|
|
213
|
+
childStates
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function clampCompoundPageIndex(index, pageCount) {
|
|
217
|
+
if (pageCount < 1) return 0;
|
|
218
|
+
return Math.min(Math.max(0, Math.floor(index)), pageCount - 1);
|
|
219
|
+
}
|
|
220
|
+
function parseCompoundResumeState(raw) {
|
|
221
|
+
if (!raw || typeof raw !== "object") return null;
|
|
222
|
+
const obj = raw;
|
|
223
|
+
if (obj.schemaVersion !== COMPOUND_RESUME_SCHEMA_VERSION) return null;
|
|
224
|
+
if (typeof obj.activePageIndex !== "number" || !Number.isFinite(obj.activePageIndex)) return null;
|
|
225
|
+
const childStates = {};
|
|
226
|
+
if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
|
|
227
|
+
for (const [key, value] of Object.entries(obj.childStates)) {
|
|
228
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
229
|
+
childStates[key] = value;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
|
|
234
|
+
return {
|
|
235
|
+
schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
|
|
236
|
+
activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
|
|
237
|
+
...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
|
|
238
|
+
childStates
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/compoundState.ts
|
|
243
|
+
var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
|
|
244
|
+
function compoundStateStorageKey(courseId, compoundId) {
|
|
245
|
+
return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
|
|
246
|
+
}
|
|
247
|
+
function loadCompoundState(storage, courseId, compoundId) {
|
|
248
|
+
const raw = storage.getItem(compoundStateStorageKey(courseId, compoundId));
|
|
249
|
+
if (!raw) return null;
|
|
250
|
+
try {
|
|
251
|
+
return parseCompoundResumeState(JSON.parse(raw));
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function saveCompoundState(storage, courseId, compoundId, state) {
|
|
257
|
+
storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
|
|
258
|
+
}
|
|
259
|
+
function clearCompoundState(storage, courseId, compoundId) {
|
|
260
|
+
storage.removeItem?.(compoundStateStorageKey(courseId, compoundId));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/compoundAllowlists.ts
|
|
264
|
+
var PAGE_ALLOWED_CHILD_TYPES = [
|
|
265
|
+
"Text",
|
|
266
|
+
"Heading",
|
|
267
|
+
"Image",
|
|
268
|
+
"Scenario",
|
|
269
|
+
"Reflection",
|
|
270
|
+
"Quiz",
|
|
271
|
+
"KnowledgeCheck",
|
|
272
|
+
"TrueFalse",
|
|
273
|
+
"FillInTheBlanks",
|
|
274
|
+
"DragAndDrop",
|
|
275
|
+
"DragTheWords",
|
|
276
|
+
"MarkTheWords",
|
|
277
|
+
"Accordion",
|
|
278
|
+
"DialogCards",
|
|
279
|
+
"Flashcards",
|
|
280
|
+
"ImageHotspots",
|
|
281
|
+
"FindHotspot",
|
|
282
|
+
"FindMultipleHotspots",
|
|
283
|
+
"ImageSlider",
|
|
284
|
+
"ProgressTracker"
|
|
285
|
+
];
|
|
286
|
+
var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
|
|
287
|
+
var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
288
|
+
"TrueFalse",
|
|
289
|
+
"FillInTheBlanks",
|
|
290
|
+
"DragAndDrop",
|
|
291
|
+
"DragTheWords",
|
|
292
|
+
"MarkTheWords",
|
|
293
|
+
"Quiz",
|
|
294
|
+
"KnowledgeCheck",
|
|
295
|
+
"FindHotspot",
|
|
296
|
+
"FindMultipleHotspots"
|
|
297
|
+
];
|
|
298
|
+
var ALLOWLISTS = {
|
|
299
|
+
Page: PAGE_ALLOWED_CHILD_TYPES,
|
|
300
|
+
InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
301
|
+
AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
|
|
302
|
+
};
|
|
303
|
+
var COMPOUND_MAX_NESTING_DEPTH = {
|
|
304
|
+
Page: 1,
|
|
305
|
+
InteractiveBook: 2,
|
|
306
|
+
AssessmentSequence: 1
|
|
307
|
+
};
|
|
308
|
+
function getAllowedChildTypes(parent) {
|
|
309
|
+
return ALLOWLISTS[parent];
|
|
310
|
+
}
|
|
311
|
+
function isChildTypeAllowed(parent, childType) {
|
|
312
|
+
return ALLOWLISTS[parent].includes(childType);
|
|
313
|
+
}
|
|
314
|
+
var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
|
|
315
|
+
|
|
181
316
|
// src/telemetryCatalog.ts
|
|
182
317
|
var telemetryCatalogVersion = 1;
|
|
183
318
|
var TELEMETRY_EVENT_CATALOG = [
|
|
@@ -274,35 +409,101 @@ function buildTelemetryCatalogV2() {
|
|
|
274
409
|
return TELEMETRY_EVENT_CATALOG_V2.map((entry) => ({ ...entry }));
|
|
275
410
|
}
|
|
276
411
|
|
|
277
|
-
// src/
|
|
412
|
+
// src/telemetryCatalogV3.ts
|
|
413
|
+
var telemetryCatalogV3Version = 3;
|
|
414
|
+
var TELEMETRY_EVENT_CATALOG_V3 = [
|
|
415
|
+
{
|
|
416
|
+
name: "book_page_viewed",
|
|
417
|
+
description: "Learner viewed a page/chapter in an Interactive Book",
|
|
418
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
419
|
+
dataFields: ["blockId", "pageIndex", "pageTitle"],
|
|
420
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
421
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "compound_page_viewed",
|
|
425
|
+
description: "Learner activated a page inside a compound container",
|
|
426
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
427
|
+
dataFields: ["blockId", "pageIndex", "parentType"],
|
|
428
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
429
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "hotspot_opened",
|
|
433
|
+
description: "Learner opened an image hotspot popover",
|
|
434
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
435
|
+
dataFields: ["blockId", "hotspotId"],
|
|
436
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
437
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: "accordion_section_toggled",
|
|
441
|
+
description: "Learner expanded or collapsed an accordion section",
|
|
442
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
443
|
+
dataFields: ["blockId", "sectionId", "expanded"],
|
|
444
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
445
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "flashcard_flipped",
|
|
449
|
+
description: "Learner flipped a flashcard",
|
|
450
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
451
|
+
dataFields: ["blockId", "cardIndex", "face"],
|
|
452
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
453
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: "image_slider_changed",
|
|
457
|
+
description: "Learner changed the active slide in an image slider",
|
|
458
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
459
|
+
dataFields: ["blockId", "slideIndex"],
|
|
460
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
461
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
462
|
+
}
|
|
463
|
+
];
|
|
464
|
+
function buildTelemetryCatalogV3() {
|
|
465
|
+
return TELEMETRY_EVENT_CATALOG_V3.map((entry) => ({ ...entry }));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/internal/env.ts
|
|
278
469
|
function isDevEnvironment() {
|
|
279
470
|
const g = globalThis;
|
|
280
471
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
281
472
|
}
|
|
473
|
+
function warnDev(message, err) {
|
|
474
|
+
if (!isDevEnvironment()) return;
|
|
475
|
+
console.warn(message, err instanceof Error ? err.message : err);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/internal/sinkInvoke.ts
|
|
282
479
|
function invokeTrackingSink(sink, event) {
|
|
283
480
|
let result;
|
|
284
481
|
try {
|
|
285
482
|
result = sink(event);
|
|
286
483
|
} catch (err) {
|
|
287
|
-
|
|
288
|
-
console.warn(
|
|
289
|
-
"[lessonkit] tracking sink failed:",
|
|
290
|
-
err instanceof Error ? err.message : err
|
|
291
|
-
);
|
|
292
|
-
}
|
|
484
|
+
warnDev("[lessonkit] tracking sink failed:", err);
|
|
293
485
|
throw err;
|
|
294
486
|
}
|
|
295
487
|
if (result != null && typeof result.catch === "function") {
|
|
296
|
-
void result.catch((err) =>
|
|
297
|
-
if (isDevEnvironment()) {
|
|
298
|
-
console.warn(
|
|
299
|
-
"[lessonkit] tracking sink failed:",
|
|
300
|
-
err instanceof Error ? err.message : err
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
});
|
|
488
|
+
void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
|
|
304
489
|
}
|
|
305
490
|
}
|
|
491
|
+
function invokePipelineSink(sinkId, emit) {
|
|
492
|
+
let result;
|
|
493
|
+
try {
|
|
494
|
+
result = emit();
|
|
495
|
+
} catch (err) {
|
|
496
|
+
warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (result != null && typeof result.catch === "function") {
|
|
500
|
+
void result.catch(
|
|
501
|
+
(err) => warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/trackingClient.ts
|
|
306
507
|
function createTrackingClient(opts) {
|
|
307
508
|
const sink = opts?.sink;
|
|
308
509
|
const batchSink = opts?.batchSink;
|
|
@@ -420,96 +621,229 @@ function nowIso() {
|
|
|
420
621
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
421
622
|
}
|
|
422
623
|
|
|
423
|
-
// src/
|
|
424
|
-
var warnedMissingQuizLesson = false;
|
|
425
|
-
var warnedMissingAssessmentLesson = false;
|
|
426
|
-
function isDevEnvironment2() {
|
|
427
|
-
const g = globalThis;
|
|
428
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
429
|
-
}
|
|
430
|
-
function resetTelemetryBuilderWarningsForTests() {
|
|
431
|
-
warnedMissingQuizLesson = false;
|
|
432
|
-
warnedMissingAssessmentLesson = false;
|
|
433
|
-
}
|
|
624
|
+
// src/telemetry/eventRegistry.ts
|
|
434
625
|
function resolveLessonId(opts, eventName) {
|
|
435
626
|
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
436
627
|
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
437
628
|
return lessonId;
|
|
438
629
|
}
|
|
439
|
-
function
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
630
|
+
function withLessonScopedData(name, base, lessonId, data) {
|
|
631
|
+
return { name, ...base, lessonId, data: { ...data, lessonId } };
|
|
632
|
+
}
|
|
633
|
+
var TELEMETRY_EVENT_REGISTRY = {
|
|
634
|
+
course_started: {
|
|
635
|
+
build: (_opts, base) => ({ name: "course_started", ...base })
|
|
636
|
+
},
|
|
637
|
+
course_completed: {
|
|
638
|
+
build: (_opts, base) => ({ name: "course_completed", ...base })
|
|
639
|
+
},
|
|
640
|
+
lesson_started: {
|
|
641
|
+
requiresLessonId: true,
|
|
642
|
+
build: (opts, base) => {
|
|
643
|
+
if (opts.name !== "lesson_started") throw new Error("unexpected event");
|
|
453
644
|
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
645
|
+
return withLessonScopedData("lesson_started", base, lessonId, opts.data);
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
lesson_completed: {
|
|
649
|
+
requiresLessonId: true,
|
|
650
|
+
build: (opts, base) => {
|
|
651
|
+
if (opts.name !== "lesson_completed") throw new Error("unexpected event");
|
|
652
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
653
|
+
return withLessonScopedData(opts.name, base, lessonId, opts.data);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
lesson_time_on_task: {
|
|
657
|
+
requiresLessonId: true,
|
|
658
|
+
build: (opts, base) => {
|
|
659
|
+
if (opts.name !== "lesson_time_on_task") throw new Error("unexpected event");
|
|
660
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
661
|
+
return withLessonScopedData(opts.name, base, lessonId, opts.data);
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
quiz_answered: {
|
|
665
|
+
requiresLessonId: true,
|
|
666
|
+
tryBuildMissingLessonWarning: "quiz",
|
|
667
|
+
build: (opts, base) => {
|
|
668
|
+
if (opts.name !== "quiz_answered") throw new Error("unexpected event");
|
|
669
|
+
const lessonId = opts.lessonId;
|
|
670
|
+
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
454
671
|
return {
|
|
455
|
-
name: "
|
|
672
|
+
name: "quiz_answered",
|
|
456
673
|
...base,
|
|
457
674
|
lessonId,
|
|
458
|
-
data:
|
|
675
|
+
data: opts.data
|
|
459
676
|
};
|
|
460
677
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
678
|
+
},
|
|
679
|
+
quiz_completed: {
|
|
680
|
+
requiresLessonId: true,
|
|
681
|
+
tryBuildMissingLessonWarning: "quiz",
|
|
682
|
+
build: (opts, base) => {
|
|
683
|
+
if (opts.name !== "quiz_completed") throw new Error("unexpected event");
|
|
684
|
+
const lessonId = opts.lessonId;
|
|
685
|
+
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
464
686
|
return {
|
|
465
|
-
name:
|
|
687
|
+
name: "quiz_completed",
|
|
466
688
|
...base,
|
|
467
689
|
lessonId,
|
|
468
|
-
data:
|
|
690
|
+
data: opts.data
|
|
469
691
|
};
|
|
470
692
|
}
|
|
471
|
-
|
|
693
|
+
},
|
|
694
|
+
assessment_answered: {
|
|
695
|
+
requiresLessonId: true,
|
|
696
|
+
tryBuildMissingLessonWarning: "assessment",
|
|
697
|
+
build: (opts, base) => {
|
|
698
|
+
if (opts.name !== "assessment_answered") throw new Error("unexpected event");
|
|
472
699
|
const lessonId = opts.lessonId;
|
|
473
|
-
if (!lessonId) throw new Error("
|
|
474
|
-
return {
|
|
700
|
+
if (!lessonId) throw new Error("assessment_answered requires active lessonId");
|
|
701
|
+
return {
|
|
702
|
+
name: "assessment_answered",
|
|
703
|
+
...base,
|
|
704
|
+
lessonId,
|
|
705
|
+
data: opts.data
|
|
706
|
+
};
|
|
475
707
|
}
|
|
476
|
-
|
|
708
|
+
},
|
|
709
|
+
assessment_completed: {
|
|
710
|
+
requiresLessonId: true,
|
|
711
|
+
tryBuildMissingLessonWarning: "assessment",
|
|
712
|
+
build: (opts, base) => {
|
|
713
|
+
if (opts.name !== "assessment_completed") throw new Error("unexpected event");
|
|
477
714
|
const lessonId = opts.lessonId;
|
|
478
|
-
if (!lessonId) throw new Error("
|
|
479
|
-
return {
|
|
715
|
+
if (!lessonId) throw new Error("assessment_completed requires active lessonId");
|
|
716
|
+
return {
|
|
717
|
+
name: "assessment_completed",
|
|
718
|
+
...base,
|
|
719
|
+
lessonId,
|
|
720
|
+
data: opts.data
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
interaction: {
|
|
725
|
+
build: (opts, base) => {
|
|
726
|
+
if (opts.name !== "interaction") throw new Error("unexpected event");
|
|
727
|
+
return {
|
|
728
|
+
name: "interaction",
|
|
729
|
+
...base,
|
|
730
|
+
lessonId: opts.lessonId,
|
|
731
|
+
data: opts.data
|
|
732
|
+
};
|
|
480
733
|
}
|
|
481
|
-
|
|
734
|
+
},
|
|
735
|
+
book_page_viewed: {
|
|
736
|
+
requiresLessonId: true,
|
|
737
|
+
build: (opts, base) => {
|
|
738
|
+
if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
|
|
482
739
|
const lessonId = opts.lessonId;
|
|
483
|
-
if (!lessonId) throw new Error("
|
|
484
|
-
return {
|
|
740
|
+
if (!lessonId) throw new Error("book_page_viewed requires active lessonId");
|
|
741
|
+
return {
|
|
742
|
+
name: "book_page_viewed",
|
|
743
|
+
...base,
|
|
744
|
+
lessonId,
|
|
745
|
+
data: opts.data
|
|
746
|
+
};
|
|
485
747
|
}
|
|
486
|
-
|
|
748
|
+
},
|
|
749
|
+
compound_page_viewed: {
|
|
750
|
+
requiresLessonId: true,
|
|
751
|
+
build: (opts, base) => {
|
|
752
|
+
if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
|
|
487
753
|
const lessonId = opts.lessonId;
|
|
488
|
-
if (!lessonId) throw new Error("
|
|
489
|
-
return {
|
|
754
|
+
if (!lessonId) throw new Error("compound_page_viewed requires active lessonId");
|
|
755
|
+
return {
|
|
756
|
+
name: "compound_page_viewed",
|
|
757
|
+
...base,
|
|
758
|
+
lessonId,
|
|
759
|
+
data: opts.data
|
|
760
|
+
};
|
|
490
761
|
}
|
|
491
|
-
|
|
762
|
+
},
|
|
763
|
+
hotspot_opened: {
|
|
764
|
+
build: (opts, base) => {
|
|
765
|
+
if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
|
|
492
766
|
return {
|
|
493
|
-
name: "
|
|
767
|
+
name: "hotspot_opened",
|
|
494
768
|
...base,
|
|
495
769
|
lessonId: opts.lessonId,
|
|
496
770
|
data: opts.data
|
|
497
771
|
};
|
|
498
|
-
|
|
499
|
-
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
accordion_section_toggled: {
|
|
775
|
+
build: (opts, base) => {
|
|
776
|
+
if (opts.name !== "accordion_section_toggled") throw new Error("unexpected event");
|
|
777
|
+
return {
|
|
778
|
+
name: "accordion_section_toggled",
|
|
779
|
+
...base,
|
|
780
|
+
lessonId: opts.lessonId,
|
|
781
|
+
data: opts.data
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
flashcard_flipped: {
|
|
786
|
+
build: (opts, base) => {
|
|
787
|
+
if (opts.name !== "flashcard_flipped") throw new Error("unexpected event");
|
|
788
|
+
return {
|
|
789
|
+
name: "flashcard_flipped",
|
|
790
|
+
...base,
|
|
791
|
+
lessonId: opts.lessonId,
|
|
792
|
+
data: opts.data
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
image_slider_changed: {
|
|
797
|
+
build: (opts, base) => {
|
|
798
|
+
if (opts.name !== "image_slider_changed") throw new Error("unexpected event");
|
|
799
|
+
return {
|
|
800
|
+
name: "image_slider_changed",
|
|
801
|
+
...base,
|
|
802
|
+
lessonId: opts.lessonId,
|
|
803
|
+
data: opts.data
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
function buildTelemetryEventFromRegistry(opts) {
|
|
809
|
+
const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
|
|
810
|
+
if (!entry) {
|
|
811
|
+
throw new Error("Unexpected value");
|
|
500
812
|
}
|
|
813
|
+
const base = {
|
|
814
|
+
timestamp: opts.timestamp ?? nowIso(),
|
|
815
|
+
courseId: opts.courseId,
|
|
816
|
+
sessionId: opts.sessionId,
|
|
817
|
+
attemptId: opts.attemptId,
|
|
818
|
+
user: opts.user
|
|
819
|
+
};
|
|
820
|
+
return entry.build(opts, base);
|
|
821
|
+
}
|
|
822
|
+
function getTelemetryEventRegistryEntry(name) {
|
|
823
|
+
return TELEMETRY_EVENT_REGISTRY[name];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/telemetryBuilder.ts
|
|
827
|
+
var warnedMissingQuizLesson = false;
|
|
828
|
+
var warnedMissingAssessmentLesson = false;
|
|
829
|
+
function resetTelemetryBuilderWarningsForTests() {
|
|
830
|
+
warnedMissingQuizLesson = false;
|
|
831
|
+
warnedMissingAssessmentLesson = false;
|
|
832
|
+
}
|
|
833
|
+
function buildTelemetryEvent(opts) {
|
|
834
|
+
return buildTelemetryEventFromRegistry(opts);
|
|
501
835
|
}
|
|
502
836
|
function tryBuildTelemetryEvent(opts) {
|
|
503
|
-
const
|
|
504
|
-
if (
|
|
505
|
-
if (
|
|
506
|
-
if (
|
|
837
|
+
const entry = getTelemetryEventRegistryEntry(opts.name);
|
|
838
|
+
if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
|
|
839
|
+
if (isDevEnvironment()) {
|
|
840
|
+
if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
|
|
507
841
|
warnedMissingQuizLesson = true;
|
|
508
842
|
console.warn(
|
|
509
843
|
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
510
844
|
);
|
|
511
845
|
}
|
|
512
|
-
if (
|
|
846
|
+
if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
|
|
513
847
|
warnedMissingAssessmentLesson = true;
|
|
514
848
|
console.warn(
|
|
515
849
|
`[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
|
|
@@ -522,29 +856,8 @@ function tryBuildTelemetryEvent(opts) {
|
|
|
522
856
|
}
|
|
523
857
|
|
|
524
858
|
// src/telemetryPipeline.ts
|
|
525
|
-
function isDevEnvironment3() {
|
|
526
|
-
const g = globalThis;
|
|
527
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
528
|
-
}
|
|
529
|
-
function warnSinkFailure(sinkId, err) {
|
|
530
|
-
if (isDevEnvironment3()) {
|
|
531
|
-
console.warn(
|
|
532
|
-
`[lessonkit] telemetry sink "${sinkId}" failed:`,
|
|
533
|
-
err instanceof Error ? err.message : err
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
859
|
function invokeSink(sink, event, emitCtx) {
|
|
538
|
-
|
|
539
|
-
try {
|
|
540
|
-
result = sink.emit(event, emitCtx);
|
|
541
|
-
} catch (err) {
|
|
542
|
-
warnSinkFailure(sink.id, err);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
if (result != null && typeof result.catch === "function") {
|
|
546
|
-
void result.catch((err) => warnSinkFailure(sink.id, err));
|
|
547
|
-
}
|
|
860
|
+
invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
|
|
548
861
|
}
|
|
549
862
|
function createTelemetryPipeline(sinks) {
|
|
550
863
|
const list = [...sinks];
|
|
@@ -820,101 +1133,13 @@ function completeCourseWithTelemetry(opts) {
|
|
|
820
1133
|
return true;
|
|
821
1134
|
}
|
|
822
1135
|
|
|
823
|
-
// src/
|
|
824
|
-
function
|
|
825
|
-
const storage = ports.storage ?? createSessionStoragePort();
|
|
826
|
-
const clock = ports.clock ?? createDefaultClock();
|
|
827
|
-
const configSnapshot = { ...config };
|
|
828
|
-
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
829
|
-
let attemptId = configSnapshot.session?.attemptId;
|
|
830
|
-
let user = configSnapshot.session?.user;
|
|
831
|
-
let courseId = configSnapshot.courseId;
|
|
832
|
-
let progress = createProgressController();
|
|
833
|
-
const getSession = () => ({ sessionId, attemptId, user });
|
|
834
|
-
const syncSessionFromConfig = (next) => {
|
|
835
|
-
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
836
|
-
attemptId = next.session?.attemptId;
|
|
837
|
-
user = next.session?.user;
|
|
838
|
-
courseId = next.courseId;
|
|
839
|
-
};
|
|
840
|
-
syncSessionFromConfig(configSnapshot);
|
|
841
|
-
const track = (name, data, emit, lessonId) => {
|
|
842
|
-
const event = tryBuildTelemetryEvent({
|
|
843
|
-
name,
|
|
844
|
-
courseId,
|
|
845
|
-
lessonId: lessonId ?? progress.getState().activeLessonId,
|
|
846
|
-
sessionId,
|
|
847
|
-
attemptId,
|
|
848
|
-
user,
|
|
849
|
-
data
|
|
850
|
-
});
|
|
851
|
-
if (!event) return;
|
|
852
|
-
emit(event);
|
|
853
|
-
};
|
|
854
|
-
const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
|
|
855
|
-
emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
856
|
-
if (durationMs !== void 0) {
|
|
857
|
-
emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
858
|
-
}
|
|
859
|
-
};
|
|
1136
|
+
// src/plugins/context.ts
|
|
1137
|
+
function buildPluginContext(opts) {
|
|
860
1138
|
return {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
return progress;
|
|
866
|
-
},
|
|
867
|
-
getProgressState: () => progress.getState(),
|
|
868
|
-
getSession,
|
|
869
|
-
updateConfig(next) {
|
|
870
|
-
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
871
|
-
if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
872
|
-
if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
|
|
873
|
-
if (next.session !== void 0) {
|
|
874
|
-
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
875
|
-
}
|
|
876
|
-
syncSessionFromConfig(configSnapshot);
|
|
877
|
-
},
|
|
878
|
-
setActiveLesson(lessonId, emitFn) {
|
|
879
|
-
const current = progress.getState();
|
|
880
|
-
if (current.activeLessonId === lessonId) return;
|
|
881
|
-
if (current.completedLessonIds.has(lessonId)) {
|
|
882
|
-
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
const previous = current.activeLessonId;
|
|
886
|
-
if (previous && previous !== lessonId) {
|
|
887
|
-
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
888
|
-
if (completed.didComplete) {
|
|
889
|
-
emitLessonCompleted(previous, completed.durationMs, emitFn);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
893
|
-
emitFn("lesson_started", { lessonId }, lessonId);
|
|
894
|
-
},
|
|
895
|
-
completeLesson(lessonId, emitFn) {
|
|
896
|
-
const result = progress.completeLesson(lessonId, clock.nowMs());
|
|
897
|
-
if (!result.didComplete) return;
|
|
898
|
-
emitLessonCompleted(lessonId, result.durationMs, emitFn);
|
|
899
|
-
},
|
|
900
|
-
completeCourse(emitFn) {
|
|
901
|
-
const current = progress.getState();
|
|
902
|
-
if (current.activeLessonId) {
|
|
903
|
-
const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
|
|
904
|
-
if (lessonResult.didComplete) {
|
|
905
|
-
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
const result = progress.completeCourse();
|
|
909
|
-
if (!result.didComplete) return;
|
|
910
|
-
emitFn("course_completed");
|
|
911
|
-
},
|
|
912
|
-
track,
|
|
913
|
-
resetForCourseChange(nextCourseId) {
|
|
914
|
-
configSnapshot.courseId = nextCourseId;
|
|
915
|
-
courseId = nextCourseId;
|
|
916
|
-
progress = createProgressController();
|
|
917
|
-
}
|
|
1139
|
+
courseId: opts.courseId,
|
|
1140
|
+
sessionId: opts.sessionId,
|
|
1141
|
+
attemptId: opts.attemptId,
|
|
1142
|
+
user: opts.user
|
|
918
1143
|
};
|
|
919
1144
|
}
|
|
920
1145
|
|
|
@@ -1005,6 +1230,161 @@ function createPluginRegistry(plugins = []) {
|
|
|
1005
1230
|
};
|
|
1006
1231
|
}
|
|
1007
1232
|
|
|
1233
|
+
// src/runtime/createLessonkitRuntime.ts
|
|
1234
|
+
function resolvePluginHost(plugins) {
|
|
1235
|
+
if (!plugins) return null;
|
|
1236
|
+
if (typeof plugins === "object" && "runTelemetry" in plugins) return plugins;
|
|
1237
|
+
if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
function warnRuntimeV1Deprecated() {
|
|
1241
|
+
const g = globalThis;
|
|
1242
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
1243
|
+
console.warn(
|
|
1244
|
+
'[lessonkit] runtimeVersion "v1" is deprecated; use "v2" (default). v1 will be removed in LessonKit 2.0.'
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
function createLessonkitRuntime(config, ports = {}) {
|
|
1248
|
+
if (config.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
1249
|
+
const storage = ports.storage ?? createSessionStoragePort();
|
|
1250
|
+
const clock = ports.clock ?? createDefaultClock();
|
|
1251
|
+
const configSnapshot = { ...config };
|
|
1252
|
+
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
1253
|
+
let attemptId = configSnapshot.session?.attemptId;
|
|
1254
|
+
let user = configSnapshot.session?.user;
|
|
1255
|
+
let courseId = configSnapshot.courseId;
|
|
1256
|
+
let progress = createProgressController();
|
|
1257
|
+
let pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1258
|
+
const getPluginCtx = () => buildPluginContext({
|
|
1259
|
+
courseId,
|
|
1260
|
+
sessionId,
|
|
1261
|
+
attemptId,
|
|
1262
|
+
user
|
|
1263
|
+
});
|
|
1264
|
+
const getSession = () => ({ sessionId, attemptId, user });
|
|
1265
|
+
const syncSessionFromConfig = (next) => {
|
|
1266
|
+
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
1267
|
+
attemptId = next.session?.attemptId;
|
|
1268
|
+
user = next.session?.user;
|
|
1269
|
+
courseId = next.courseId;
|
|
1270
|
+
};
|
|
1271
|
+
const applyPluginsToEvent = (event) => {
|
|
1272
|
+
if (!pluginHost) return event;
|
|
1273
|
+
return pluginHost.runTelemetry(event, getPluginCtx());
|
|
1274
|
+
};
|
|
1275
|
+
const buildAndApply = (name, data, lessonId) => {
|
|
1276
|
+
const event = tryBuildTelemetryEvent({
|
|
1277
|
+
name,
|
|
1278
|
+
courseId,
|
|
1279
|
+
lessonId: lessonId ?? progress.getState().activeLessonId,
|
|
1280
|
+
sessionId,
|
|
1281
|
+
attemptId,
|
|
1282
|
+
user,
|
|
1283
|
+
data
|
|
1284
|
+
});
|
|
1285
|
+
if (!event) return null;
|
|
1286
|
+
return applyPluginsToEvent(event);
|
|
1287
|
+
};
|
|
1288
|
+
const wrapEmitFn = (emitFn) => {
|
|
1289
|
+
return (name, data, lessonId) => {
|
|
1290
|
+
const event = buildAndApply(name, data, lessonId);
|
|
1291
|
+
if (event === null) return;
|
|
1292
|
+
const eventLessonId = "lessonId" in event ? event.lessonId : lessonId;
|
|
1293
|
+
const eventData = "data" in event ? event.data : data;
|
|
1294
|
+
emitFn(event.name, eventData, eventLessonId);
|
|
1295
|
+
};
|
|
1296
|
+
};
|
|
1297
|
+
syncSessionFromConfig(configSnapshot);
|
|
1298
|
+
const track = (name, data, emit, lessonId) => {
|
|
1299
|
+
const event = buildAndApply(name, data, lessonId);
|
|
1300
|
+
if (!event) return;
|
|
1301
|
+
emit(event);
|
|
1302
|
+
};
|
|
1303
|
+
const emitLessonCompletedEvents = (lessonId, durationMs, emitFn) => {
|
|
1304
|
+
const wrapped = wrapEmitFn(emitFn);
|
|
1305
|
+
wrapped("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
1306
|
+
if (durationMs !== void 0) {
|
|
1307
|
+
wrapped("lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
return {
|
|
1311
|
+
get config() {
|
|
1312
|
+
return configSnapshot;
|
|
1313
|
+
},
|
|
1314
|
+
get progress() {
|
|
1315
|
+
return progress;
|
|
1316
|
+
},
|
|
1317
|
+
get pluginHost() {
|
|
1318
|
+
return pluginHost;
|
|
1319
|
+
},
|
|
1320
|
+
getProgressState: () => progress.getState(),
|
|
1321
|
+
getSession,
|
|
1322
|
+
updateConfig(next) {
|
|
1323
|
+
if (next.plugins !== void 0 && next.plugins !== pluginHost) {
|
|
1324
|
+
pluginHost?.disposeAll();
|
|
1325
|
+
configSnapshot.plugins = next.plugins;
|
|
1326
|
+
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1327
|
+
}
|
|
1328
|
+
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
1329
|
+
if (next.runtimeVersion !== void 0) {
|
|
1330
|
+
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
1331
|
+
configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
1332
|
+
}
|
|
1333
|
+
if (next.session !== void 0) {
|
|
1334
|
+
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
1335
|
+
}
|
|
1336
|
+
syncSessionFromConfig(configSnapshot);
|
|
1337
|
+
},
|
|
1338
|
+
setActiveLesson(lessonId, emitFn) {
|
|
1339
|
+
const wrapped = wrapEmitFn(emitFn);
|
|
1340
|
+
const current = progress.getState();
|
|
1341
|
+
if (current.activeLessonId === lessonId) return;
|
|
1342
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
1343
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const previous = current.activeLessonId;
|
|
1347
|
+
if (previous && previous !== lessonId) {
|
|
1348
|
+
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
1349
|
+
if (completed.didComplete) {
|
|
1350
|
+
emitLessonCompletedEvents(previous, completed.durationMs, wrapped);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1354
|
+
wrapped("lesson_started", { lessonId }, lessonId);
|
|
1355
|
+
},
|
|
1356
|
+
completeLesson(lessonId, emitFn) {
|
|
1357
|
+
completeLessonWithTelemetry({
|
|
1358
|
+
progress,
|
|
1359
|
+
lessonId,
|
|
1360
|
+
nowMs: clock.nowMs(),
|
|
1361
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn))
|
|
1362
|
+
});
|
|
1363
|
+
},
|
|
1364
|
+
completeCourse(emitFn) {
|
|
1365
|
+
completeCourseWithTelemetry({
|
|
1366
|
+
progress,
|
|
1367
|
+
nowMs: clock.nowMs(),
|
|
1368
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn)),
|
|
1369
|
+
emitCourseCompleted: () => wrapEmitFn(emitFn)("course_completed")
|
|
1370
|
+
});
|
|
1371
|
+
},
|
|
1372
|
+
track,
|
|
1373
|
+
scoreAssessment(input, _lessonId) {
|
|
1374
|
+
if (!pluginHost) return null;
|
|
1375
|
+
return pluginHost.scoreAssessment(input, getPluginCtx());
|
|
1376
|
+
},
|
|
1377
|
+
resetForCourseChange(nextCourseId) {
|
|
1378
|
+
configSnapshot.courseId = nextCourseId;
|
|
1379
|
+
courseId = nextCourseId;
|
|
1380
|
+
progress = createProgressController();
|
|
1381
|
+
},
|
|
1382
|
+
dispose() {
|
|
1383
|
+
pluginHost?.disposeAll();
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1008
1388
|
// src/plugins/define.ts
|
|
1009
1389
|
function defineTelemetryPlugin(plugin) {
|
|
1010
1390
|
return plugin;
|
|
@@ -1017,20 +1397,33 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1017
1397
|
}
|
|
1018
1398
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1019
1399
|
0 && (module.exports = {
|
|
1400
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
1401
|
+
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
1402
|
+
COMPOUND_MAX_NESTING_DEPTH,
|
|
1403
|
+
COMPOUND_RESUME_SCHEMA_VERSION,
|
|
1020
1404
|
ID_MAX_LENGTH,
|
|
1021
1405
|
ID_PATTERN,
|
|
1406
|
+
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
1407
|
+
PAGE_ALLOWED_CHILD_TYPES,
|
|
1022
1408
|
SESSION_STORAGE_KEY,
|
|
1023
1409
|
TELEMETRY_EVENT_CATALOG,
|
|
1024
1410
|
TELEMETRY_EVENT_CATALOG_V2,
|
|
1411
|
+
TELEMETRY_EVENT_CATALOG_V3,
|
|
1025
1412
|
assertNever,
|
|
1026
1413
|
assertValidId,
|
|
1027
1414
|
buildCourseStartedTelemetryEvent,
|
|
1028
1415
|
buildLessonkitUrn,
|
|
1416
|
+
buildPluginContext,
|
|
1029
1417
|
buildTelemetryCatalog,
|
|
1030
1418
|
buildTelemetryCatalogV2,
|
|
1419
|
+
buildTelemetryCatalogV3,
|
|
1031
1420
|
buildTelemetryEvent,
|
|
1421
|
+
clampCompoundPageIndex,
|
|
1422
|
+
clearCompoundState,
|
|
1032
1423
|
completeCourseWithTelemetry,
|
|
1033
1424
|
completeLessonWithTelemetry,
|
|
1425
|
+
compoundStateStorageKey,
|
|
1426
|
+
createCompoundResumeState,
|
|
1034
1427
|
createDefaultClock,
|
|
1035
1428
|
createGlobalTimer,
|
|
1036
1429
|
createLessonkitRuntime,
|
|
@@ -1046,10 +1439,13 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1046
1439
|
defineLifecyclePlugin,
|
|
1047
1440
|
defineTelemetryPlugin,
|
|
1048
1441
|
deriveId,
|
|
1442
|
+
getAllowedChildTypes,
|
|
1049
1443
|
getTabSessionId,
|
|
1050
1444
|
hasCourseStarted,
|
|
1051
1445
|
hasCourseStartedEmittedToTracking,
|
|
1052
1446
|
hasCourseStartedPipelineDelivered,
|
|
1447
|
+
isChildTypeAllowed,
|
|
1448
|
+
loadCompoundState,
|
|
1053
1449
|
markCourseStarted,
|
|
1054
1450
|
markCourseStartedEmittedToTracking,
|
|
1055
1451
|
markCourseStartedPipelineDelivered,
|
|
@@ -1057,13 +1453,16 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1057
1453
|
nowIso,
|
|
1058
1454
|
parseBlockId,
|
|
1059
1455
|
parseCheckId,
|
|
1456
|
+
parseCompoundResumeState,
|
|
1060
1457
|
parseCourseId,
|
|
1061
1458
|
parseLessonId,
|
|
1062
1459
|
resetStoragePortForTests,
|
|
1063
1460
|
resetTelemetryBuilderWarningsForTests,
|
|
1064
1461
|
resolveSessionId,
|
|
1462
|
+
saveCompoundState,
|
|
1065
1463
|
slugifyId,
|
|
1066
1464
|
telemetryCatalogV2Version,
|
|
1465
|
+
telemetryCatalogV3Version,
|
|
1067
1466
|
telemetryCatalogVersion,
|
|
1068
1467
|
tryBuildTelemetryEvent,
|
|
1069
1468
|
tryEmitCourseStarted,
|