@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.js
CHANGED
|
@@ -102,6 +102,122 @@ function buildLessonkitUrn(parts) {
|
|
|
102
102
|
return urn;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// src/compound.ts
|
|
106
|
+
var COMPOUND_RESUME_SCHEMA_VERSION = 1;
|
|
107
|
+
function createCompoundResumeState(input = {}) {
|
|
108
|
+
const childStates = {};
|
|
109
|
+
if (input.childStates) {
|
|
110
|
+
for (const [key, value] of Object.entries(input.childStates)) {
|
|
111
|
+
childStates[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
|
|
116
|
+
activePageIndex: input.activePageIndex ?? 0,
|
|
117
|
+
...input.activeChapterIndex !== void 0 ? { activeChapterIndex: input.activeChapterIndex } : {},
|
|
118
|
+
childStates
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function clampCompoundPageIndex(index, pageCount) {
|
|
122
|
+
if (pageCount < 1) return 0;
|
|
123
|
+
return Math.min(Math.max(0, Math.floor(index)), pageCount - 1);
|
|
124
|
+
}
|
|
125
|
+
function parseCompoundResumeState(raw) {
|
|
126
|
+
if (!raw || typeof raw !== "object") return null;
|
|
127
|
+
const obj = raw;
|
|
128
|
+
if (obj.schemaVersion !== COMPOUND_RESUME_SCHEMA_VERSION) return null;
|
|
129
|
+
if (typeof obj.activePageIndex !== "number" || !Number.isFinite(obj.activePageIndex)) return null;
|
|
130
|
+
const childStates = {};
|
|
131
|
+
if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
|
|
132
|
+
for (const [key, value] of Object.entries(obj.childStates)) {
|
|
133
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
134
|
+
childStates[key] = value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
|
|
139
|
+
return {
|
|
140
|
+
schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
|
|
141
|
+
activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
|
|
142
|
+
...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
|
|
143
|
+
childStates
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/compoundState.ts
|
|
148
|
+
var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
|
|
149
|
+
function compoundStateStorageKey(courseId, compoundId) {
|
|
150
|
+
return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
|
|
151
|
+
}
|
|
152
|
+
function loadCompoundState(storage, courseId, compoundId) {
|
|
153
|
+
const raw = storage.getItem(compoundStateStorageKey(courseId, compoundId));
|
|
154
|
+
if (!raw) return null;
|
|
155
|
+
try {
|
|
156
|
+
return parseCompoundResumeState(JSON.parse(raw));
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function saveCompoundState(storage, courseId, compoundId, state) {
|
|
162
|
+
storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
|
|
163
|
+
}
|
|
164
|
+
function clearCompoundState(storage, courseId, compoundId) {
|
|
165
|
+
storage.removeItem?.(compoundStateStorageKey(courseId, compoundId));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/compoundAllowlists.ts
|
|
169
|
+
var PAGE_ALLOWED_CHILD_TYPES = [
|
|
170
|
+
"Text",
|
|
171
|
+
"Heading",
|
|
172
|
+
"Image",
|
|
173
|
+
"Scenario",
|
|
174
|
+
"Reflection",
|
|
175
|
+
"Quiz",
|
|
176
|
+
"KnowledgeCheck",
|
|
177
|
+
"TrueFalse",
|
|
178
|
+
"FillInTheBlanks",
|
|
179
|
+
"DragAndDrop",
|
|
180
|
+
"DragTheWords",
|
|
181
|
+
"MarkTheWords",
|
|
182
|
+
"Accordion",
|
|
183
|
+
"DialogCards",
|
|
184
|
+
"Flashcards",
|
|
185
|
+
"ImageHotspots",
|
|
186
|
+
"FindHotspot",
|
|
187
|
+
"FindMultipleHotspots",
|
|
188
|
+
"ImageSlider",
|
|
189
|
+
"ProgressTracker"
|
|
190
|
+
];
|
|
191
|
+
var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
|
|
192
|
+
var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
193
|
+
"TrueFalse",
|
|
194
|
+
"FillInTheBlanks",
|
|
195
|
+
"DragAndDrop",
|
|
196
|
+
"DragTheWords",
|
|
197
|
+
"MarkTheWords",
|
|
198
|
+
"Quiz",
|
|
199
|
+
"KnowledgeCheck",
|
|
200
|
+
"FindHotspot",
|
|
201
|
+
"FindMultipleHotspots"
|
|
202
|
+
];
|
|
203
|
+
var ALLOWLISTS = {
|
|
204
|
+
Page: PAGE_ALLOWED_CHILD_TYPES,
|
|
205
|
+
InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
206
|
+
AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
|
|
207
|
+
};
|
|
208
|
+
var COMPOUND_MAX_NESTING_DEPTH = {
|
|
209
|
+
Page: 1,
|
|
210
|
+
InteractiveBook: 2,
|
|
211
|
+
AssessmentSequence: 1
|
|
212
|
+
};
|
|
213
|
+
function getAllowedChildTypes(parent) {
|
|
214
|
+
return ALLOWLISTS[parent];
|
|
215
|
+
}
|
|
216
|
+
function isChildTypeAllowed(parent, childType) {
|
|
217
|
+
return ALLOWLISTS[parent].includes(childType);
|
|
218
|
+
}
|
|
219
|
+
var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
|
|
220
|
+
|
|
105
221
|
// src/telemetryCatalog.ts
|
|
106
222
|
var telemetryCatalogVersion = 1;
|
|
107
223
|
var TELEMETRY_EVENT_CATALOG = [
|
|
@@ -198,35 +314,101 @@ function buildTelemetryCatalogV2() {
|
|
|
198
314
|
return TELEMETRY_EVENT_CATALOG_V2.map((entry) => ({ ...entry }));
|
|
199
315
|
}
|
|
200
316
|
|
|
201
|
-
// src/
|
|
317
|
+
// src/telemetryCatalogV3.ts
|
|
318
|
+
var telemetryCatalogV3Version = 3;
|
|
319
|
+
var TELEMETRY_EVENT_CATALOG_V3 = [
|
|
320
|
+
{
|
|
321
|
+
name: "book_page_viewed",
|
|
322
|
+
description: "Learner viewed a page/chapter in an Interactive Book",
|
|
323
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
324
|
+
dataFields: ["blockId", "pageIndex", "pageTitle"],
|
|
325
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
326
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "compound_page_viewed",
|
|
330
|
+
description: "Learner activated a page inside a compound container",
|
|
331
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
332
|
+
dataFields: ["blockId", "pageIndex", "parentType"],
|
|
333
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
334
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "hotspot_opened",
|
|
338
|
+
description: "Learner opened an image hotspot popover",
|
|
339
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
340
|
+
dataFields: ["blockId", "hotspotId"],
|
|
341
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
342
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "accordion_section_toggled",
|
|
346
|
+
description: "Learner expanded or collapsed an accordion section",
|
|
347
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
348
|
+
dataFields: ["blockId", "sectionId", "expanded"],
|
|
349
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
350
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "flashcard_flipped",
|
|
354
|
+
description: "Learner flipped a flashcard",
|
|
355
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
356
|
+
dataFields: ["blockId", "cardIndex", "face"],
|
|
357
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
358
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "image_slider_changed",
|
|
362
|
+
description: "Learner changed the active slide in an image slider",
|
|
363
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
364
|
+
dataFields: ["blockId", "slideIndex"],
|
|
365
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
366
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
367
|
+
}
|
|
368
|
+
];
|
|
369
|
+
function buildTelemetryCatalogV3() {
|
|
370
|
+
return TELEMETRY_EVENT_CATALOG_V3.map((entry) => ({ ...entry }));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/internal/env.ts
|
|
202
374
|
function isDevEnvironment() {
|
|
203
375
|
const g = globalThis;
|
|
204
376
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
205
377
|
}
|
|
378
|
+
function warnDev(message, err) {
|
|
379
|
+
if (!isDevEnvironment()) return;
|
|
380
|
+
console.warn(message, err instanceof Error ? err.message : err);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/internal/sinkInvoke.ts
|
|
206
384
|
function invokeTrackingSink(sink, event) {
|
|
207
385
|
let result;
|
|
208
386
|
try {
|
|
209
387
|
result = sink(event);
|
|
210
388
|
} catch (err) {
|
|
211
|
-
|
|
212
|
-
console.warn(
|
|
213
|
-
"[lessonkit] tracking sink failed:",
|
|
214
|
-
err instanceof Error ? err.message : err
|
|
215
|
-
);
|
|
216
|
-
}
|
|
389
|
+
warnDev("[lessonkit] tracking sink failed:", err);
|
|
217
390
|
throw err;
|
|
218
391
|
}
|
|
219
392
|
if (result != null && typeof result.catch === "function") {
|
|
220
|
-
void result.catch((err) =>
|
|
221
|
-
if (isDevEnvironment()) {
|
|
222
|
-
console.warn(
|
|
223
|
-
"[lessonkit] tracking sink failed:",
|
|
224
|
-
err instanceof Error ? err.message : err
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
393
|
+
void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
|
|
228
394
|
}
|
|
229
395
|
}
|
|
396
|
+
function invokePipelineSink(sinkId, emit) {
|
|
397
|
+
let result;
|
|
398
|
+
try {
|
|
399
|
+
result = emit();
|
|
400
|
+
} catch (err) {
|
|
401
|
+
warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (result != null && typeof result.catch === "function") {
|
|
405
|
+
void result.catch(
|
|
406
|
+
(err) => warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err)
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/trackingClient.ts
|
|
230
412
|
function createTrackingClient(opts) {
|
|
231
413
|
const sink = opts?.sink;
|
|
232
414
|
const batchSink = opts?.batchSink;
|
|
@@ -344,96 +526,229 @@ function nowIso() {
|
|
|
344
526
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
345
527
|
}
|
|
346
528
|
|
|
347
|
-
// src/
|
|
348
|
-
var warnedMissingQuizLesson = false;
|
|
349
|
-
var warnedMissingAssessmentLesson = false;
|
|
350
|
-
function isDevEnvironment2() {
|
|
351
|
-
const g = globalThis;
|
|
352
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
353
|
-
}
|
|
354
|
-
function resetTelemetryBuilderWarningsForTests() {
|
|
355
|
-
warnedMissingQuizLesson = false;
|
|
356
|
-
warnedMissingAssessmentLesson = false;
|
|
357
|
-
}
|
|
529
|
+
// src/telemetry/eventRegistry.ts
|
|
358
530
|
function resolveLessonId(opts, eventName) {
|
|
359
531
|
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
360
532
|
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
361
533
|
return lessonId;
|
|
362
534
|
}
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
535
|
+
function withLessonScopedData(name, base, lessonId, data) {
|
|
536
|
+
return { name, ...base, lessonId, data: { ...data, lessonId } };
|
|
537
|
+
}
|
|
538
|
+
var TELEMETRY_EVENT_REGISTRY = {
|
|
539
|
+
course_started: {
|
|
540
|
+
build: (_opts, base) => ({ name: "course_started", ...base })
|
|
541
|
+
},
|
|
542
|
+
course_completed: {
|
|
543
|
+
build: (_opts, base) => ({ name: "course_completed", ...base })
|
|
544
|
+
},
|
|
545
|
+
lesson_started: {
|
|
546
|
+
requiresLessonId: true,
|
|
547
|
+
build: (opts, base) => {
|
|
548
|
+
if (opts.name !== "lesson_started") throw new Error("unexpected event");
|
|
377
549
|
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
550
|
+
return withLessonScopedData("lesson_started", base, lessonId, opts.data);
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
lesson_completed: {
|
|
554
|
+
requiresLessonId: true,
|
|
555
|
+
build: (opts, base) => {
|
|
556
|
+
if (opts.name !== "lesson_completed") throw new Error("unexpected event");
|
|
557
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
558
|
+
return withLessonScopedData(opts.name, base, lessonId, opts.data);
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
lesson_time_on_task: {
|
|
562
|
+
requiresLessonId: true,
|
|
563
|
+
build: (opts, base) => {
|
|
564
|
+
if (opts.name !== "lesson_time_on_task") throw new Error("unexpected event");
|
|
565
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
566
|
+
return withLessonScopedData(opts.name, base, lessonId, opts.data);
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
quiz_answered: {
|
|
570
|
+
requiresLessonId: true,
|
|
571
|
+
tryBuildMissingLessonWarning: "quiz",
|
|
572
|
+
build: (opts, base) => {
|
|
573
|
+
if (opts.name !== "quiz_answered") throw new Error("unexpected event");
|
|
574
|
+
const lessonId = opts.lessonId;
|
|
575
|
+
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
378
576
|
return {
|
|
379
|
-
name: "
|
|
577
|
+
name: "quiz_answered",
|
|
380
578
|
...base,
|
|
381
579
|
lessonId,
|
|
382
|
-
data:
|
|
580
|
+
data: opts.data
|
|
383
581
|
};
|
|
384
582
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
583
|
+
},
|
|
584
|
+
quiz_completed: {
|
|
585
|
+
requiresLessonId: true,
|
|
586
|
+
tryBuildMissingLessonWarning: "quiz",
|
|
587
|
+
build: (opts, base) => {
|
|
588
|
+
if (opts.name !== "quiz_completed") throw new Error("unexpected event");
|
|
589
|
+
const lessonId = opts.lessonId;
|
|
590
|
+
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
388
591
|
return {
|
|
389
|
-
name:
|
|
592
|
+
name: "quiz_completed",
|
|
390
593
|
...base,
|
|
391
594
|
lessonId,
|
|
392
|
-
data:
|
|
595
|
+
data: opts.data
|
|
393
596
|
};
|
|
394
597
|
}
|
|
395
|
-
|
|
598
|
+
},
|
|
599
|
+
assessment_answered: {
|
|
600
|
+
requiresLessonId: true,
|
|
601
|
+
tryBuildMissingLessonWarning: "assessment",
|
|
602
|
+
build: (opts, base) => {
|
|
603
|
+
if (opts.name !== "assessment_answered") throw new Error("unexpected event");
|
|
396
604
|
const lessonId = opts.lessonId;
|
|
397
|
-
if (!lessonId) throw new Error("
|
|
398
|
-
return {
|
|
605
|
+
if (!lessonId) throw new Error("assessment_answered requires active lessonId");
|
|
606
|
+
return {
|
|
607
|
+
name: "assessment_answered",
|
|
608
|
+
...base,
|
|
609
|
+
lessonId,
|
|
610
|
+
data: opts.data
|
|
611
|
+
};
|
|
399
612
|
}
|
|
400
|
-
|
|
613
|
+
},
|
|
614
|
+
assessment_completed: {
|
|
615
|
+
requiresLessonId: true,
|
|
616
|
+
tryBuildMissingLessonWarning: "assessment",
|
|
617
|
+
build: (opts, base) => {
|
|
618
|
+
if (opts.name !== "assessment_completed") throw new Error("unexpected event");
|
|
401
619
|
const lessonId = opts.lessonId;
|
|
402
|
-
if (!lessonId) throw new Error("
|
|
403
|
-
return {
|
|
620
|
+
if (!lessonId) throw new Error("assessment_completed requires active lessonId");
|
|
621
|
+
return {
|
|
622
|
+
name: "assessment_completed",
|
|
623
|
+
...base,
|
|
624
|
+
lessonId,
|
|
625
|
+
data: opts.data
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
interaction: {
|
|
630
|
+
build: (opts, base) => {
|
|
631
|
+
if (opts.name !== "interaction") throw new Error("unexpected event");
|
|
632
|
+
return {
|
|
633
|
+
name: "interaction",
|
|
634
|
+
...base,
|
|
635
|
+
lessonId: opts.lessonId,
|
|
636
|
+
data: opts.data
|
|
637
|
+
};
|
|
404
638
|
}
|
|
405
|
-
|
|
639
|
+
},
|
|
640
|
+
book_page_viewed: {
|
|
641
|
+
requiresLessonId: true,
|
|
642
|
+
build: (opts, base) => {
|
|
643
|
+
if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
|
|
406
644
|
const lessonId = opts.lessonId;
|
|
407
|
-
if (!lessonId) throw new Error("
|
|
408
|
-
return {
|
|
645
|
+
if (!lessonId) throw new Error("book_page_viewed requires active lessonId");
|
|
646
|
+
return {
|
|
647
|
+
name: "book_page_viewed",
|
|
648
|
+
...base,
|
|
649
|
+
lessonId,
|
|
650
|
+
data: opts.data
|
|
651
|
+
};
|
|
409
652
|
}
|
|
410
|
-
|
|
653
|
+
},
|
|
654
|
+
compound_page_viewed: {
|
|
655
|
+
requiresLessonId: true,
|
|
656
|
+
build: (opts, base) => {
|
|
657
|
+
if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
|
|
411
658
|
const lessonId = opts.lessonId;
|
|
412
|
-
if (!lessonId) throw new Error("
|
|
413
|
-
return {
|
|
659
|
+
if (!lessonId) throw new Error("compound_page_viewed requires active lessonId");
|
|
660
|
+
return {
|
|
661
|
+
name: "compound_page_viewed",
|
|
662
|
+
...base,
|
|
663
|
+
lessonId,
|
|
664
|
+
data: opts.data
|
|
665
|
+
};
|
|
414
666
|
}
|
|
415
|
-
|
|
667
|
+
},
|
|
668
|
+
hotspot_opened: {
|
|
669
|
+
build: (opts, base) => {
|
|
670
|
+
if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
|
|
416
671
|
return {
|
|
417
|
-
name: "
|
|
672
|
+
name: "hotspot_opened",
|
|
418
673
|
...base,
|
|
419
674
|
lessonId: opts.lessonId,
|
|
420
675
|
data: opts.data
|
|
421
676
|
};
|
|
422
|
-
|
|
423
|
-
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
accordion_section_toggled: {
|
|
680
|
+
build: (opts, base) => {
|
|
681
|
+
if (opts.name !== "accordion_section_toggled") throw new Error("unexpected event");
|
|
682
|
+
return {
|
|
683
|
+
name: "accordion_section_toggled",
|
|
684
|
+
...base,
|
|
685
|
+
lessonId: opts.lessonId,
|
|
686
|
+
data: opts.data
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
flashcard_flipped: {
|
|
691
|
+
build: (opts, base) => {
|
|
692
|
+
if (opts.name !== "flashcard_flipped") throw new Error("unexpected event");
|
|
693
|
+
return {
|
|
694
|
+
name: "flashcard_flipped",
|
|
695
|
+
...base,
|
|
696
|
+
lessonId: opts.lessonId,
|
|
697
|
+
data: opts.data
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
image_slider_changed: {
|
|
702
|
+
build: (opts, base) => {
|
|
703
|
+
if (opts.name !== "image_slider_changed") throw new Error("unexpected event");
|
|
704
|
+
return {
|
|
705
|
+
name: "image_slider_changed",
|
|
706
|
+
...base,
|
|
707
|
+
lessonId: opts.lessonId,
|
|
708
|
+
data: opts.data
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
function buildTelemetryEventFromRegistry(opts) {
|
|
714
|
+
const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
|
|
715
|
+
if (!entry) {
|
|
716
|
+
throw new Error("Unexpected value");
|
|
424
717
|
}
|
|
718
|
+
const base = {
|
|
719
|
+
timestamp: opts.timestamp ?? nowIso(),
|
|
720
|
+
courseId: opts.courseId,
|
|
721
|
+
sessionId: opts.sessionId,
|
|
722
|
+
attemptId: opts.attemptId,
|
|
723
|
+
user: opts.user
|
|
724
|
+
};
|
|
725
|
+
return entry.build(opts, base);
|
|
726
|
+
}
|
|
727
|
+
function getTelemetryEventRegistryEntry(name) {
|
|
728
|
+
return TELEMETRY_EVENT_REGISTRY[name];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/telemetryBuilder.ts
|
|
732
|
+
var warnedMissingQuizLesson = false;
|
|
733
|
+
var warnedMissingAssessmentLesson = false;
|
|
734
|
+
function resetTelemetryBuilderWarningsForTests() {
|
|
735
|
+
warnedMissingQuizLesson = false;
|
|
736
|
+
warnedMissingAssessmentLesson = false;
|
|
737
|
+
}
|
|
738
|
+
function buildTelemetryEvent(opts) {
|
|
739
|
+
return buildTelemetryEventFromRegistry(opts);
|
|
425
740
|
}
|
|
426
741
|
function tryBuildTelemetryEvent(opts) {
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
if (
|
|
430
|
-
if (
|
|
742
|
+
const entry = getTelemetryEventRegistryEntry(opts.name);
|
|
743
|
+
if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
|
|
744
|
+
if (isDevEnvironment()) {
|
|
745
|
+
if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
|
|
431
746
|
warnedMissingQuizLesson = true;
|
|
432
747
|
console.warn(
|
|
433
748
|
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
434
749
|
);
|
|
435
750
|
}
|
|
436
|
-
if (
|
|
751
|
+
if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
|
|
437
752
|
warnedMissingAssessmentLesson = true;
|
|
438
753
|
console.warn(
|
|
439
754
|
`[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
|
|
@@ -446,29 +761,8 @@ function tryBuildTelemetryEvent(opts) {
|
|
|
446
761
|
}
|
|
447
762
|
|
|
448
763
|
// src/telemetryPipeline.ts
|
|
449
|
-
function isDevEnvironment3() {
|
|
450
|
-
const g = globalThis;
|
|
451
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
452
|
-
}
|
|
453
|
-
function warnSinkFailure(sinkId, err) {
|
|
454
|
-
if (isDevEnvironment3()) {
|
|
455
|
-
console.warn(
|
|
456
|
-
`[lessonkit] telemetry sink "${sinkId}" failed:`,
|
|
457
|
-
err instanceof Error ? err.message : err
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
764
|
function invokeSink(sink, event, emitCtx) {
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
result = sink.emit(event, emitCtx);
|
|
465
|
-
} catch (err) {
|
|
466
|
-
warnSinkFailure(sink.id, err);
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
if (result != null && typeof result.catch === "function") {
|
|
470
|
-
void result.catch((err) => warnSinkFailure(sink.id, err));
|
|
471
|
-
}
|
|
765
|
+
invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
|
|
472
766
|
}
|
|
473
767
|
function createTelemetryPipeline(sinks) {
|
|
474
768
|
const list = [...sinks];
|
|
@@ -744,101 +1038,13 @@ function completeCourseWithTelemetry(opts) {
|
|
|
744
1038
|
return true;
|
|
745
1039
|
}
|
|
746
1040
|
|
|
747
|
-
// src/
|
|
748
|
-
function
|
|
749
|
-
const storage = ports.storage ?? createSessionStoragePort();
|
|
750
|
-
const clock = ports.clock ?? createDefaultClock();
|
|
751
|
-
const configSnapshot = { ...config };
|
|
752
|
-
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
753
|
-
let attemptId = configSnapshot.session?.attemptId;
|
|
754
|
-
let user = configSnapshot.session?.user;
|
|
755
|
-
let courseId = configSnapshot.courseId;
|
|
756
|
-
let progress = createProgressController();
|
|
757
|
-
const getSession = () => ({ sessionId, attemptId, user });
|
|
758
|
-
const syncSessionFromConfig = (next) => {
|
|
759
|
-
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
760
|
-
attemptId = next.session?.attemptId;
|
|
761
|
-
user = next.session?.user;
|
|
762
|
-
courseId = next.courseId;
|
|
763
|
-
};
|
|
764
|
-
syncSessionFromConfig(configSnapshot);
|
|
765
|
-
const track = (name, data, emit, lessonId) => {
|
|
766
|
-
const event = tryBuildTelemetryEvent({
|
|
767
|
-
name,
|
|
768
|
-
courseId,
|
|
769
|
-
lessonId: lessonId ?? progress.getState().activeLessonId,
|
|
770
|
-
sessionId,
|
|
771
|
-
attemptId,
|
|
772
|
-
user,
|
|
773
|
-
data
|
|
774
|
-
});
|
|
775
|
-
if (!event) return;
|
|
776
|
-
emit(event);
|
|
777
|
-
};
|
|
778
|
-
const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
|
|
779
|
-
emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
780
|
-
if (durationMs !== void 0) {
|
|
781
|
-
emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
782
|
-
}
|
|
783
|
-
};
|
|
1041
|
+
// src/plugins/context.ts
|
|
1042
|
+
function buildPluginContext(opts) {
|
|
784
1043
|
return {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
return progress;
|
|
790
|
-
},
|
|
791
|
-
getProgressState: () => progress.getState(),
|
|
792
|
-
getSession,
|
|
793
|
-
updateConfig(next) {
|
|
794
|
-
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
795
|
-
if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
796
|
-
if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
|
|
797
|
-
if (next.session !== void 0) {
|
|
798
|
-
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
799
|
-
}
|
|
800
|
-
syncSessionFromConfig(configSnapshot);
|
|
801
|
-
},
|
|
802
|
-
setActiveLesson(lessonId, emitFn) {
|
|
803
|
-
const current = progress.getState();
|
|
804
|
-
if (current.activeLessonId === lessonId) return;
|
|
805
|
-
if (current.completedLessonIds.has(lessonId)) {
|
|
806
|
-
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
const previous = current.activeLessonId;
|
|
810
|
-
if (previous && previous !== lessonId) {
|
|
811
|
-
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
812
|
-
if (completed.didComplete) {
|
|
813
|
-
emitLessonCompleted(previous, completed.durationMs, emitFn);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
817
|
-
emitFn("lesson_started", { lessonId }, lessonId);
|
|
818
|
-
},
|
|
819
|
-
completeLesson(lessonId, emitFn) {
|
|
820
|
-
const result = progress.completeLesson(lessonId, clock.nowMs());
|
|
821
|
-
if (!result.didComplete) return;
|
|
822
|
-
emitLessonCompleted(lessonId, result.durationMs, emitFn);
|
|
823
|
-
},
|
|
824
|
-
completeCourse(emitFn) {
|
|
825
|
-
const current = progress.getState();
|
|
826
|
-
if (current.activeLessonId) {
|
|
827
|
-
const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
|
|
828
|
-
if (lessonResult.didComplete) {
|
|
829
|
-
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
const result = progress.completeCourse();
|
|
833
|
-
if (!result.didComplete) return;
|
|
834
|
-
emitFn("course_completed");
|
|
835
|
-
},
|
|
836
|
-
track,
|
|
837
|
-
resetForCourseChange(nextCourseId) {
|
|
838
|
-
configSnapshot.courseId = nextCourseId;
|
|
839
|
-
courseId = nextCourseId;
|
|
840
|
-
progress = createProgressController();
|
|
841
|
-
}
|
|
1044
|
+
courseId: opts.courseId,
|
|
1045
|
+
sessionId: opts.sessionId,
|
|
1046
|
+
attemptId: opts.attemptId,
|
|
1047
|
+
user: opts.user
|
|
842
1048
|
};
|
|
843
1049
|
}
|
|
844
1050
|
|
|
@@ -929,6 +1135,161 @@ function createPluginRegistry(plugins = []) {
|
|
|
929
1135
|
};
|
|
930
1136
|
}
|
|
931
1137
|
|
|
1138
|
+
// src/runtime/createLessonkitRuntime.ts
|
|
1139
|
+
function resolvePluginHost(plugins) {
|
|
1140
|
+
if (!plugins) return null;
|
|
1141
|
+
if (typeof plugins === "object" && "runTelemetry" in plugins) return plugins;
|
|
1142
|
+
if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
function warnRuntimeV1Deprecated() {
|
|
1146
|
+
const g = globalThis;
|
|
1147
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
1148
|
+
console.warn(
|
|
1149
|
+
'[lessonkit] runtimeVersion "v1" is deprecated; use "v2" (default). v1 will be removed in LessonKit 2.0.'
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
function createLessonkitRuntime(config, ports = {}) {
|
|
1153
|
+
if (config.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
1154
|
+
const storage = ports.storage ?? createSessionStoragePort();
|
|
1155
|
+
const clock = ports.clock ?? createDefaultClock();
|
|
1156
|
+
const configSnapshot = { ...config };
|
|
1157
|
+
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
1158
|
+
let attemptId = configSnapshot.session?.attemptId;
|
|
1159
|
+
let user = configSnapshot.session?.user;
|
|
1160
|
+
let courseId = configSnapshot.courseId;
|
|
1161
|
+
let progress = createProgressController();
|
|
1162
|
+
let pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1163
|
+
const getPluginCtx = () => buildPluginContext({
|
|
1164
|
+
courseId,
|
|
1165
|
+
sessionId,
|
|
1166
|
+
attemptId,
|
|
1167
|
+
user
|
|
1168
|
+
});
|
|
1169
|
+
const getSession = () => ({ sessionId, attemptId, user });
|
|
1170
|
+
const syncSessionFromConfig = (next) => {
|
|
1171
|
+
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
1172
|
+
attemptId = next.session?.attemptId;
|
|
1173
|
+
user = next.session?.user;
|
|
1174
|
+
courseId = next.courseId;
|
|
1175
|
+
};
|
|
1176
|
+
const applyPluginsToEvent = (event) => {
|
|
1177
|
+
if (!pluginHost) return event;
|
|
1178
|
+
return pluginHost.runTelemetry(event, getPluginCtx());
|
|
1179
|
+
};
|
|
1180
|
+
const buildAndApply = (name, data, lessonId) => {
|
|
1181
|
+
const event = tryBuildTelemetryEvent({
|
|
1182
|
+
name,
|
|
1183
|
+
courseId,
|
|
1184
|
+
lessonId: lessonId ?? progress.getState().activeLessonId,
|
|
1185
|
+
sessionId,
|
|
1186
|
+
attemptId,
|
|
1187
|
+
user,
|
|
1188
|
+
data
|
|
1189
|
+
});
|
|
1190
|
+
if (!event) return null;
|
|
1191
|
+
return applyPluginsToEvent(event);
|
|
1192
|
+
};
|
|
1193
|
+
const wrapEmitFn = (emitFn) => {
|
|
1194
|
+
return (name, data, lessonId) => {
|
|
1195
|
+
const event = buildAndApply(name, data, lessonId);
|
|
1196
|
+
if (event === null) return;
|
|
1197
|
+
const eventLessonId = "lessonId" in event ? event.lessonId : lessonId;
|
|
1198
|
+
const eventData = "data" in event ? event.data : data;
|
|
1199
|
+
emitFn(event.name, eventData, eventLessonId);
|
|
1200
|
+
};
|
|
1201
|
+
};
|
|
1202
|
+
syncSessionFromConfig(configSnapshot);
|
|
1203
|
+
const track = (name, data, emit, lessonId) => {
|
|
1204
|
+
const event = buildAndApply(name, data, lessonId);
|
|
1205
|
+
if (!event) return;
|
|
1206
|
+
emit(event);
|
|
1207
|
+
};
|
|
1208
|
+
const emitLessonCompletedEvents = (lessonId, durationMs, emitFn) => {
|
|
1209
|
+
const wrapped = wrapEmitFn(emitFn);
|
|
1210
|
+
wrapped("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
1211
|
+
if (durationMs !== void 0) {
|
|
1212
|
+
wrapped("lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
return {
|
|
1216
|
+
get config() {
|
|
1217
|
+
return configSnapshot;
|
|
1218
|
+
},
|
|
1219
|
+
get progress() {
|
|
1220
|
+
return progress;
|
|
1221
|
+
},
|
|
1222
|
+
get pluginHost() {
|
|
1223
|
+
return pluginHost;
|
|
1224
|
+
},
|
|
1225
|
+
getProgressState: () => progress.getState(),
|
|
1226
|
+
getSession,
|
|
1227
|
+
updateConfig(next) {
|
|
1228
|
+
if (next.plugins !== void 0 && next.plugins !== pluginHost) {
|
|
1229
|
+
pluginHost?.disposeAll();
|
|
1230
|
+
configSnapshot.plugins = next.plugins;
|
|
1231
|
+
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1232
|
+
}
|
|
1233
|
+
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
1234
|
+
if (next.runtimeVersion !== void 0) {
|
|
1235
|
+
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
1236
|
+
configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
1237
|
+
}
|
|
1238
|
+
if (next.session !== void 0) {
|
|
1239
|
+
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
1240
|
+
}
|
|
1241
|
+
syncSessionFromConfig(configSnapshot);
|
|
1242
|
+
},
|
|
1243
|
+
setActiveLesson(lessonId, emitFn) {
|
|
1244
|
+
const wrapped = wrapEmitFn(emitFn);
|
|
1245
|
+
const current = progress.getState();
|
|
1246
|
+
if (current.activeLessonId === lessonId) return;
|
|
1247
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
1248
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const previous = current.activeLessonId;
|
|
1252
|
+
if (previous && previous !== lessonId) {
|
|
1253
|
+
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
1254
|
+
if (completed.didComplete) {
|
|
1255
|
+
emitLessonCompletedEvents(previous, completed.durationMs, wrapped);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1259
|
+
wrapped("lesson_started", { lessonId }, lessonId);
|
|
1260
|
+
},
|
|
1261
|
+
completeLesson(lessonId, emitFn) {
|
|
1262
|
+
completeLessonWithTelemetry({
|
|
1263
|
+
progress,
|
|
1264
|
+
lessonId,
|
|
1265
|
+
nowMs: clock.nowMs(),
|
|
1266
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn))
|
|
1267
|
+
});
|
|
1268
|
+
},
|
|
1269
|
+
completeCourse(emitFn) {
|
|
1270
|
+
completeCourseWithTelemetry({
|
|
1271
|
+
progress,
|
|
1272
|
+
nowMs: clock.nowMs(),
|
|
1273
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn)),
|
|
1274
|
+
emitCourseCompleted: () => wrapEmitFn(emitFn)("course_completed")
|
|
1275
|
+
});
|
|
1276
|
+
},
|
|
1277
|
+
track,
|
|
1278
|
+
scoreAssessment(input, _lessonId) {
|
|
1279
|
+
if (!pluginHost) return null;
|
|
1280
|
+
return pluginHost.scoreAssessment(input, getPluginCtx());
|
|
1281
|
+
},
|
|
1282
|
+
resetForCourseChange(nextCourseId) {
|
|
1283
|
+
configSnapshot.courseId = nextCourseId;
|
|
1284
|
+
courseId = nextCourseId;
|
|
1285
|
+
progress = createProgressController();
|
|
1286
|
+
},
|
|
1287
|
+
dispose() {
|
|
1288
|
+
pluginHost?.disposeAll();
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
932
1293
|
// src/plugins/define.ts
|
|
933
1294
|
function defineTelemetryPlugin(plugin) {
|
|
934
1295
|
return plugin;
|
|
@@ -940,20 +1301,33 @@ function defineLifecyclePlugin(plugin) {
|
|
|
940
1301
|
return plugin;
|
|
941
1302
|
}
|
|
942
1303
|
export {
|
|
1304
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
1305
|
+
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
1306
|
+
COMPOUND_MAX_NESTING_DEPTH,
|
|
1307
|
+
COMPOUND_RESUME_SCHEMA_VERSION,
|
|
943
1308
|
ID_MAX_LENGTH,
|
|
944
1309
|
ID_PATTERN,
|
|
1310
|
+
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
1311
|
+
PAGE_ALLOWED_CHILD_TYPES,
|
|
945
1312
|
SESSION_STORAGE_KEY,
|
|
946
1313
|
TELEMETRY_EVENT_CATALOG,
|
|
947
1314
|
TELEMETRY_EVENT_CATALOG_V2,
|
|
1315
|
+
TELEMETRY_EVENT_CATALOG_V3,
|
|
948
1316
|
assertNever,
|
|
949
1317
|
assertValidId,
|
|
950
1318
|
buildCourseStartedTelemetryEvent,
|
|
951
1319
|
buildLessonkitUrn,
|
|
1320
|
+
buildPluginContext,
|
|
952
1321
|
buildTelemetryCatalog,
|
|
953
1322
|
buildTelemetryCatalogV2,
|
|
1323
|
+
buildTelemetryCatalogV3,
|
|
954
1324
|
buildTelemetryEvent,
|
|
1325
|
+
clampCompoundPageIndex,
|
|
1326
|
+
clearCompoundState,
|
|
955
1327
|
completeCourseWithTelemetry,
|
|
956
1328
|
completeLessonWithTelemetry,
|
|
1329
|
+
compoundStateStorageKey,
|
|
1330
|
+
createCompoundResumeState,
|
|
957
1331
|
createDefaultClock,
|
|
958
1332
|
createGlobalTimer,
|
|
959
1333
|
createLessonkitRuntime,
|
|
@@ -969,10 +1343,13 @@ export {
|
|
|
969
1343
|
defineLifecyclePlugin,
|
|
970
1344
|
defineTelemetryPlugin,
|
|
971
1345
|
deriveId,
|
|
1346
|
+
getAllowedChildTypes,
|
|
972
1347
|
getTabSessionId,
|
|
973
1348
|
hasCourseStarted,
|
|
974
1349
|
hasCourseStartedEmittedToTracking,
|
|
975
1350
|
hasCourseStartedPipelineDelivered,
|
|
1351
|
+
isChildTypeAllowed,
|
|
1352
|
+
loadCompoundState,
|
|
976
1353
|
markCourseStarted,
|
|
977
1354
|
markCourseStartedEmittedToTracking,
|
|
978
1355
|
markCourseStartedPipelineDelivered,
|
|
@@ -980,13 +1357,16 @@ export {
|
|
|
980
1357
|
nowIso,
|
|
981
1358
|
parseBlockId,
|
|
982
1359
|
parseCheckId,
|
|
1360
|
+
parseCompoundResumeState,
|
|
983
1361
|
parseCourseId,
|
|
984
1362
|
parseLessonId,
|
|
985
1363
|
resetStoragePortForTests,
|
|
986
1364
|
resetTelemetryBuilderWarningsForTests,
|
|
987
1365
|
resolveSessionId,
|
|
1366
|
+
saveCompoundState,
|
|
988
1367
|
slugifyId,
|
|
989
1368
|
telemetryCatalogV2Version,
|
|
1369
|
+
telemetryCatalogV3Version,
|
|
990
1370
|
telemetryCatalogVersion,
|
|
991
1371
|
tryBuildTelemetryEvent,
|
|
992
1372
|
tryEmitCourseStarted,
|