@lessonkit/core 1.4.0 → 1.6.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/README.md +28 -2
- package/dist/{chunk-PEWFPVQ6.js → chunk-NGCHHJSM.js} +357 -50
- package/dist/index.cjs +822 -131
- package/dist/index.d.cts +142 -15
- package/dist/index.d.ts +142 -15
- package/dist/index.js +534 -138
- package/dist/{testing-BhVGckZ5.d.cts → testing-CzgxF1Ru.d.cts} +193 -8
- package/dist/{testing-BhVGckZ5.d.ts → testing-CzgxF1Ru.d.ts} +193 -8
- package/dist/testing.cjs +1 -3
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +3 -3
- package/telemetry-catalog.v3.json +177 -0
|
@@ -21,7 +21,7 @@ declare const ID_PATTERN: RegExp;
|
|
|
21
21
|
declare const ID_MAX_LENGTH = 64;
|
|
22
22
|
|
|
23
23
|
/** H5P-aligned interaction kinds for assessment telemetry and xAPI. */
|
|
24
|
-
type AssessmentInteractionType = "mcq" | "trueFalse" | "fillInBlanks" | "markTheWords" | "dragTheWords" | "dragAndDrop" | "assessmentSequence" | "findHotspot" | "findMultipleHotspots" | "summary" | "imagePairing" | "imageSequencing" | "essay" | "arithmeticQuiz" | "memoryGame";
|
|
24
|
+
type AssessmentInteractionType = "mcq" | "trueFalse" | "fillInBlanks" | "markTheWords" | "dragTheWords" | "dragAndDrop" | "assessmentSequence" | "findHotspot" | "findMultipleHotspots" | "summary" | "imagePairing" | "imageSequencing" | "essay" | "arithmeticQuiz" | "memoryGame" | "combinationLock" | "crossword" | "wordSearch";
|
|
25
25
|
/** Serializable resume blob for a single assessment block. */
|
|
26
26
|
type AssessmentResumeState = Record<string, unknown>;
|
|
27
27
|
/** Behaviour flags aligned with H5P question types. */
|
|
@@ -65,7 +65,7 @@ type McqAssessmentProps = AssessmentBaseProps & {
|
|
|
65
65
|
answer: string;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed" | "video_cue_reached" | "video_segment_completed" | "memory_card_flipped" | "information_wall_search" | "parallax_slide_viewed" | "questionnaire_submitted";
|
|
68
|
+
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed" | "video_cue_reached" | "video_segment_completed" | "memory_card_flipped" | "information_wall_search" | "parallax_slide_viewed" | "questionnaire_submitted" | "branch_node_viewed" | "branch_selected" | "image_juxtaposition_changed" | "timeline_event_viewed" | "image_sequence_changed" | "audio_recording_started" | "audio_recording_completed" | "qr_content_revealed" | "advent_door_opened" | "map_stage_viewed" | "map_exit_selected";
|
|
69
69
|
type TelemetryUser = {
|
|
70
70
|
id?: string;
|
|
71
71
|
email?: string;
|
|
@@ -75,6 +75,8 @@ type TelemetryUser = {
|
|
|
75
75
|
type TelemetryEventBase = {
|
|
76
76
|
timestamp: string;
|
|
77
77
|
courseId: CourseId;
|
|
78
|
+
/** Optional stable id for sink deduplication (e.g. deliver retry). */
|
|
79
|
+
id?: string;
|
|
78
80
|
sessionId?: string;
|
|
79
81
|
attemptId?: string;
|
|
80
82
|
user?: TelemetryUser;
|
|
@@ -181,6 +183,55 @@ type QuestionnaireSubmittedData = {
|
|
|
181
183
|
blockId: BlockId;
|
|
182
184
|
fieldCount: number;
|
|
183
185
|
};
|
|
186
|
+
type BranchNodeViewedData = {
|
|
187
|
+
blockId: BlockId;
|
|
188
|
+
nodeId: string;
|
|
189
|
+
nodeIndex: number;
|
|
190
|
+
nodeTitle?: string;
|
|
191
|
+
};
|
|
192
|
+
type BranchSelectedData = {
|
|
193
|
+
blockId: BlockId;
|
|
194
|
+
fromNodeId: string;
|
|
195
|
+
toNodeId: string;
|
|
196
|
+
label: string;
|
|
197
|
+
scoreWeight?: number;
|
|
198
|
+
};
|
|
199
|
+
type ImageJuxtapositionChangedData = {
|
|
200
|
+
blockId: BlockId;
|
|
201
|
+
position: number;
|
|
202
|
+
};
|
|
203
|
+
type TimelineEventViewedData = {
|
|
204
|
+
blockId: BlockId;
|
|
205
|
+
eventId: string;
|
|
206
|
+
};
|
|
207
|
+
type ImageSequenceChangedData = {
|
|
208
|
+
blockId: BlockId;
|
|
209
|
+
frameIndex: number;
|
|
210
|
+
};
|
|
211
|
+
type AudioRecordingData = {
|
|
212
|
+
blockId: BlockId;
|
|
213
|
+
};
|
|
214
|
+
type QrContentRevealedData = {
|
|
215
|
+
blockId: BlockId;
|
|
216
|
+
};
|
|
217
|
+
type AdventDoorOpenedData = {
|
|
218
|
+
blockId: BlockId;
|
|
219
|
+
doorId: string;
|
|
220
|
+
day: number;
|
|
221
|
+
};
|
|
222
|
+
type MapStageViewedData = {
|
|
223
|
+
blockId: BlockId;
|
|
224
|
+
stageId: string;
|
|
225
|
+
stageIndex: number;
|
|
226
|
+
stageLabel?: string;
|
|
227
|
+
};
|
|
228
|
+
type MapExitSelectedData = {
|
|
229
|
+
blockId: BlockId;
|
|
230
|
+
fromStageId: string;
|
|
231
|
+
toStageId: string;
|
|
232
|
+
label: string;
|
|
233
|
+
scoreWeight?: number;
|
|
234
|
+
};
|
|
184
235
|
type TelemetryEvent = (TelemetryEventBase & {
|
|
185
236
|
name: "course_started";
|
|
186
237
|
lessonId?: LessonId;
|
|
@@ -273,6 +324,50 @@ type TelemetryEvent = (TelemetryEventBase & {
|
|
|
273
324
|
name: "questionnaire_submitted";
|
|
274
325
|
lessonId: LessonId;
|
|
275
326
|
data: QuestionnaireSubmittedData;
|
|
327
|
+
}) | (TelemetryEventBase & {
|
|
328
|
+
name: "branch_node_viewed";
|
|
329
|
+
lessonId: LessonId;
|
|
330
|
+
data: BranchNodeViewedData;
|
|
331
|
+
}) | (TelemetryEventBase & {
|
|
332
|
+
name: "branch_selected";
|
|
333
|
+
lessonId: LessonId;
|
|
334
|
+
data: BranchSelectedData;
|
|
335
|
+
}) | (TelemetryEventBase & {
|
|
336
|
+
name: "image_juxtaposition_changed";
|
|
337
|
+
lessonId?: LessonId;
|
|
338
|
+
data: ImageJuxtapositionChangedData;
|
|
339
|
+
}) | (TelemetryEventBase & {
|
|
340
|
+
name: "timeline_event_viewed";
|
|
341
|
+
lessonId?: LessonId;
|
|
342
|
+
data: TimelineEventViewedData;
|
|
343
|
+
}) | (TelemetryEventBase & {
|
|
344
|
+
name: "image_sequence_changed";
|
|
345
|
+
lessonId?: LessonId;
|
|
346
|
+
data: ImageSequenceChangedData;
|
|
347
|
+
}) | (TelemetryEventBase & {
|
|
348
|
+
name: "audio_recording_started";
|
|
349
|
+
lessonId?: LessonId;
|
|
350
|
+
data: AudioRecordingData;
|
|
351
|
+
}) | (TelemetryEventBase & {
|
|
352
|
+
name: "audio_recording_completed";
|
|
353
|
+
lessonId?: LessonId;
|
|
354
|
+
data: AudioRecordingData;
|
|
355
|
+
}) | (TelemetryEventBase & {
|
|
356
|
+
name: "qr_content_revealed";
|
|
357
|
+
lessonId?: LessonId;
|
|
358
|
+
data: QrContentRevealedData;
|
|
359
|
+
}) | (TelemetryEventBase & {
|
|
360
|
+
name: "advent_door_opened";
|
|
361
|
+
lessonId?: LessonId;
|
|
362
|
+
data: AdventDoorOpenedData;
|
|
363
|
+
}) | (TelemetryEventBase & {
|
|
364
|
+
name: "map_stage_viewed";
|
|
365
|
+
lessonId: LessonId;
|
|
366
|
+
data: MapStageViewedData;
|
|
367
|
+
}) | (TelemetryEventBase & {
|
|
368
|
+
name: "map_exit_selected";
|
|
369
|
+
lessonId: LessonId;
|
|
370
|
+
data: MapExitSelectedData;
|
|
276
371
|
});
|
|
277
372
|
/** Payload shape for a telemetry event name. */
|
|
278
373
|
type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
|
|
@@ -283,7 +378,8 @@ type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
|
|
|
283
378
|
type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
|
|
284
379
|
type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
|
|
285
380
|
type TrackingClient = {
|
|
286
|
-
|
|
381
|
+
/** Returns false when the event was dropped (e.g. buffer cap or after dispose). */
|
|
382
|
+
track: (event: TelemetryEvent) => boolean;
|
|
287
383
|
/** Delivers one event and resolves to true only when the sink accepted it (batch: includes flush). */
|
|
288
384
|
deliver?: (event: TelemetryEvent) => Promise<boolean>;
|
|
289
385
|
/** Resolves to true when all buffered events were delivered; false when a sink failure re-queued events. */
|
|
@@ -414,6 +510,50 @@ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
|
|
|
414
510
|
name: "questionnaire_submitted";
|
|
415
511
|
lessonId?: LessonId;
|
|
416
512
|
data: QuestionnaireSubmittedData;
|
|
513
|
+
}) | (BuildTelemetryEventContext & {
|
|
514
|
+
name: "branch_node_viewed";
|
|
515
|
+
lessonId?: LessonId;
|
|
516
|
+
data: BranchNodeViewedData;
|
|
517
|
+
}) | (BuildTelemetryEventContext & {
|
|
518
|
+
name: "branch_selected";
|
|
519
|
+
lessonId?: LessonId;
|
|
520
|
+
data: BranchSelectedData;
|
|
521
|
+
}) | (BuildTelemetryEventContext & {
|
|
522
|
+
name: "image_juxtaposition_changed";
|
|
523
|
+
lessonId?: LessonId;
|
|
524
|
+
data: ImageJuxtapositionChangedData;
|
|
525
|
+
}) | (BuildTelemetryEventContext & {
|
|
526
|
+
name: "timeline_event_viewed";
|
|
527
|
+
lessonId?: LessonId;
|
|
528
|
+
data: TimelineEventViewedData;
|
|
529
|
+
}) | (BuildTelemetryEventContext & {
|
|
530
|
+
name: "image_sequence_changed";
|
|
531
|
+
lessonId?: LessonId;
|
|
532
|
+
data: ImageSequenceChangedData;
|
|
533
|
+
}) | (BuildTelemetryEventContext & {
|
|
534
|
+
name: "audio_recording_started";
|
|
535
|
+
lessonId?: LessonId;
|
|
536
|
+
data: AudioRecordingData;
|
|
537
|
+
}) | (BuildTelemetryEventContext & {
|
|
538
|
+
name: "audio_recording_completed";
|
|
539
|
+
lessonId?: LessonId;
|
|
540
|
+
data: AudioRecordingData;
|
|
541
|
+
}) | (BuildTelemetryEventContext & {
|
|
542
|
+
name: "qr_content_revealed";
|
|
543
|
+
lessonId?: LessonId;
|
|
544
|
+
data: QrContentRevealedData;
|
|
545
|
+
}) | (BuildTelemetryEventContext & {
|
|
546
|
+
name: "advent_door_opened";
|
|
547
|
+
lessonId?: LessonId;
|
|
548
|
+
data: AdventDoorOpenedData;
|
|
549
|
+
}) | (BuildTelemetryEventContext & {
|
|
550
|
+
name: "map_stage_viewed";
|
|
551
|
+
lessonId?: LessonId;
|
|
552
|
+
data: MapStageViewedData;
|
|
553
|
+
}) | (BuildTelemetryEventContext & {
|
|
554
|
+
name: "map_exit_selected";
|
|
555
|
+
lessonId?: LessonId;
|
|
556
|
+
data: MapExitSelectedData;
|
|
417
557
|
});
|
|
418
558
|
|
|
419
559
|
/** Reset dev-warning state (tests only). */
|
|
@@ -421,10 +561,23 @@ declare function resetTelemetryBuilderWarningsForTests(): void;
|
|
|
421
561
|
/**
|
|
422
562
|
* Build a typed telemetry event from a catalog event name and context.
|
|
423
563
|
* Validates lesson-scoped events require `lessonId`.
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```ts
|
|
567
|
+
* import { buildTelemetryEvent } from "@lessonkit/core";
|
|
568
|
+
*
|
|
569
|
+
* const event = buildTelemetryEvent({
|
|
570
|
+
* name: "lesson_completed",
|
|
571
|
+
* courseId: "sec-101",
|
|
572
|
+
* lessonId: "phishing-101",
|
|
573
|
+
* sessionId: "tab-abc",
|
|
574
|
+
* });
|
|
575
|
+
* ```
|
|
424
576
|
*/
|
|
425
577
|
declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent;
|
|
426
578
|
/**
|
|
427
|
-
* Like `buildTelemetryEvent`, but returns null
|
|
579
|
+
* Like `buildTelemetryEvent`, but returns null when lesson-scoped events lack `lessonId`
|
|
580
|
+
* (with dev warnings for quiz/assessment events).
|
|
428
581
|
*/
|
|
429
582
|
declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
|
|
430
583
|
|
|
@@ -457,7 +610,9 @@ declare function hasCourseStartedEmittedToTracking(storage: StoragePort, session
|
|
|
457
610
|
declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
458
611
|
declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
459
612
|
declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
460
|
-
|
|
613
|
+
declare function hasCourseStartedXapiSent(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
614
|
+
declare function markCourseStartedXapiSent(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
615
|
+
/** @internal Reset volatile session ids between tests. */
|
|
461
616
|
declare function resetSharedVolatileSessionIdForTests(): void;
|
|
462
617
|
declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
|
|
463
618
|
|
|
@@ -547,18 +702,48 @@ type CourseLifecycleContext = {
|
|
|
547
702
|
type CourseLifecycleDeps = {
|
|
548
703
|
emitCourseStartedEvent: (ctx: CourseLifecycleContext) => boolean;
|
|
549
704
|
};
|
|
550
|
-
|
|
705
|
+
/**
|
|
706
|
+
* Emit `course_started` once per tab session when tracking/xAPI are active.
|
|
707
|
+
* Coalesces concurrent calls via an in-flight guard.
|
|
708
|
+
*/
|
|
709
|
+
declare function tryEmitCourseStarted(ctx: CourseLifecycleContext, deps: CourseLifecycleDeps, alreadyEmittedToSink: boolean): Promise<{
|
|
551
710
|
emitted: boolean;
|
|
552
711
|
marked: boolean;
|
|
553
|
-
}
|
|
712
|
+
}>;
|
|
554
713
|
declare function buildCourseStartedTelemetryEvent(ctx: CourseLifecycleContext): TelemetryEvent;
|
|
555
714
|
type LessonCompletionEmitter = (lessonId: LessonId, durationMs?: number) => void;
|
|
715
|
+
/**
|
|
716
|
+
* Mark a lesson complete in progress state and emit `lesson_completed` when newly completed.
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* ```ts
|
|
720
|
+
* completeLessonWithTelemetry({
|
|
721
|
+
* progress,
|
|
722
|
+
* lessonId: "lesson-1",
|
|
723
|
+
* nowMs: Date.now(),
|
|
724
|
+
* emitLessonCompleted: (id, durationMs) => track("lesson_completed", { lessonId: id, durationMs }),
|
|
725
|
+
* });
|
|
726
|
+
* ```
|
|
727
|
+
*/
|
|
556
728
|
declare function completeLessonWithTelemetry(opts: {
|
|
557
729
|
progress: ProgressController;
|
|
558
730
|
lessonId: LessonId;
|
|
559
731
|
nowMs: number;
|
|
560
732
|
emitLessonCompleted: LessonCompletionEmitter;
|
|
561
733
|
}): boolean;
|
|
734
|
+
/**
|
|
735
|
+
* Complete the active lesson (if any), then mark the course complete and emit `course_completed`.
|
|
736
|
+
*
|
|
737
|
+
* @example
|
|
738
|
+
* ```ts
|
|
739
|
+
* completeCourseWithTelemetry({
|
|
740
|
+
* progress,
|
|
741
|
+
* nowMs: Date.now(),
|
|
742
|
+
* emitLessonCompleted: (id) => track("lesson_completed", { lessonId: id }),
|
|
743
|
+
* emitCourseCompleted: () => track("course_completed", {}),
|
|
744
|
+
* });
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
562
747
|
declare function completeCourseWithTelemetry(opts: {
|
|
563
748
|
progress: ProgressController;
|
|
564
749
|
nowMs: number;
|
|
@@ -566,4 +751,4 @@ declare function completeCourseWithTelemetry(opts: {
|
|
|
566
751
|
emitCourseCompleted: () => void;
|
|
567
752
|
}): boolean;
|
|
568
753
|
|
|
569
|
-
export { type
|
|
754
|
+
export { type LessonCompletionEmitter as $, type AssessmentResumeState as A, type BlockId as B, type CourseId as C, type AssessmentInteractionType as D, type AssessmentXAPIData as E, type BookPageViewedData as F, type BranchNodeViewedData as G, type BranchSelectedData as H, type IdentityIdPath as I, type BuildTelemetryEventInput as J, type CompoundPageViewedData as K, type LessonId as L, type CourseLifecycleContext as M, type CourseLifecycleDeps as N, type FlashcardFlippedData as O, type PluginRegistry as P, type HotspotOpenedData as Q, ID_MAX_LENGTH as R, type StoragePort as S, type TelemetryEventName as T, ID_PATTERN as U, type IdentityValidationIssue as V, type ImageSliderChangedData as W, type InformationWallSearchData as X, type InteractionBlockRegistration as Y, type InteractionData as Z, type InteractionPlugin as _, type CheckId as a, type LessonLifecycleData as a0, type LessonkitPluginKind as a1, type McqAssessmentProps as a2, type MemoryCardFlippedData as a3, type ParallaxSlideViewedData as a4, type PluginIdentity as a5, type QuestionnaireSubmittedData as a6, type QuizAnsweredData as a7, type QuizCompletedData as a8, SESSION_STORAGE_KEY as a9, resetTelemetryBuilderWarningsForTests as aA, resolveSessionId as aB, tryBuildTelemetryEvent as aC, tryEmitCourseStarted as aD, resetCourseStartedEmitFlightForTests as aE, type SlideViewedData as aa, type TelemetryEventBase as ab, type TimerPort as ac, type VideoCueReachedData as ad, type VideoSegmentCompletedData as ae, buildCourseStartedTelemetryEvent as af, buildTelemetryEvent as ag, completeCourseWithTelemetry as ah, completeLessonWithTelemetry as ai, createDefaultClock as aj, createGlobalTimer as ak, createNoopStorage as al, createProgressController as am, createSessionStoragePort as an, getTabSessionId as ao, hasCourseStarted as ap, hasCourseStartedEmittedToTracking as aq, hasCourseStartedPipelineDelivered as ar, hasCourseStartedXapiSent as as, markCourseStarted as at, markCourseStartedEmittedToTracking as au, markCourseStartedPipelineDelivered as av, markCourseStartedXapiSent as aw, migrateCourseStartedMark as ax, resetSharedVolatileSessionIdForTests as ay, resetStoragePortForTests as az, type IdentityValidationResult as b, type LessonkitUrn as c, type TelemetrySink as d, type TelemetryBatchSink as e, type TrackingClient as f, type TelemetryEvent as g, type TelemetryUser as h, type LessonkitPlugin as i, type ProgressController as j, type PluginHost as k, type ProgressState as l, type TelemetryDataFor as m, type AssessmentScoreInput as n, type AssessmentScoreResult as o, type ClockPort as p, type AssessmentPlugin as q, type LifecyclePlugin as r, type TelemetryPlugin as s, type LessonkitPluginContext as t, type AccordionSectionToggledData as u, type AssessmentAnsweredData as v, type AssessmentBaseProps as w, type AssessmentBehaviour as x, type AssessmentCompletedData as y, type AssessmentHandle as z };
|
|
@@ -21,7 +21,7 @@ declare const ID_PATTERN: RegExp;
|
|
|
21
21
|
declare const ID_MAX_LENGTH = 64;
|
|
22
22
|
|
|
23
23
|
/** H5P-aligned interaction kinds for assessment telemetry and xAPI. */
|
|
24
|
-
type AssessmentInteractionType = "mcq" | "trueFalse" | "fillInBlanks" | "markTheWords" | "dragTheWords" | "dragAndDrop" | "assessmentSequence" | "findHotspot" | "findMultipleHotspots" | "summary" | "imagePairing" | "imageSequencing" | "essay" | "arithmeticQuiz" | "memoryGame";
|
|
24
|
+
type AssessmentInteractionType = "mcq" | "trueFalse" | "fillInBlanks" | "markTheWords" | "dragTheWords" | "dragAndDrop" | "assessmentSequence" | "findHotspot" | "findMultipleHotspots" | "summary" | "imagePairing" | "imageSequencing" | "essay" | "arithmeticQuiz" | "memoryGame" | "combinationLock" | "crossword" | "wordSearch";
|
|
25
25
|
/** Serializable resume blob for a single assessment block. */
|
|
26
26
|
type AssessmentResumeState = Record<string, unknown>;
|
|
27
27
|
/** Behaviour flags aligned with H5P question types. */
|
|
@@ -65,7 +65,7 @@ type McqAssessmentProps = AssessmentBaseProps & {
|
|
|
65
65
|
answer: string;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed" | "video_cue_reached" | "video_segment_completed" | "memory_card_flipped" | "information_wall_search" | "parallax_slide_viewed" | "questionnaire_submitted";
|
|
68
|
+
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed" | "video_cue_reached" | "video_segment_completed" | "memory_card_flipped" | "information_wall_search" | "parallax_slide_viewed" | "questionnaire_submitted" | "branch_node_viewed" | "branch_selected" | "image_juxtaposition_changed" | "timeline_event_viewed" | "image_sequence_changed" | "audio_recording_started" | "audio_recording_completed" | "qr_content_revealed" | "advent_door_opened" | "map_stage_viewed" | "map_exit_selected";
|
|
69
69
|
type TelemetryUser = {
|
|
70
70
|
id?: string;
|
|
71
71
|
email?: string;
|
|
@@ -75,6 +75,8 @@ type TelemetryUser = {
|
|
|
75
75
|
type TelemetryEventBase = {
|
|
76
76
|
timestamp: string;
|
|
77
77
|
courseId: CourseId;
|
|
78
|
+
/** Optional stable id for sink deduplication (e.g. deliver retry). */
|
|
79
|
+
id?: string;
|
|
78
80
|
sessionId?: string;
|
|
79
81
|
attemptId?: string;
|
|
80
82
|
user?: TelemetryUser;
|
|
@@ -181,6 +183,55 @@ type QuestionnaireSubmittedData = {
|
|
|
181
183
|
blockId: BlockId;
|
|
182
184
|
fieldCount: number;
|
|
183
185
|
};
|
|
186
|
+
type BranchNodeViewedData = {
|
|
187
|
+
blockId: BlockId;
|
|
188
|
+
nodeId: string;
|
|
189
|
+
nodeIndex: number;
|
|
190
|
+
nodeTitle?: string;
|
|
191
|
+
};
|
|
192
|
+
type BranchSelectedData = {
|
|
193
|
+
blockId: BlockId;
|
|
194
|
+
fromNodeId: string;
|
|
195
|
+
toNodeId: string;
|
|
196
|
+
label: string;
|
|
197
|
+
scoreWeight?: number;
|
|
198
|
+
};
|
|
199
|
+
type ImageJuxtapositionChangedData = {
|
|
200
|
+
blockId: BlockId;
|
|
201
|
+
position: number;
|
|
202
|
+
};
|
|
203
|
+
type TimelineEventViewedData = {
|
|
204
|
+
blockId: BlockId;
|
|
205
|
+
eventId: string;
|
|
206
|
+
};
|
|
207
|
+
type ImageSequenceChangedData = {
|
|
208
|
+
blockId: BlockId;
|
|
209
|
+
frameIndex: number;
|
|
210
|
+
};
|
|
211
|
+
type AudioRecordingData = {
|
|
212
|
+
blockId: BlockId;
|
|
213
|
+
};
|
|
214
|
+
type QrContentRevealedData = {
|
|
215
|
+
blockId: BlockId;
|
|
216
|
+
};
|
|
217
|
+
type AdventDoorOpenedData = {
|
|
218
|
+
blockId: BlockId;
|
|
219
|
+
doorId: string;
|
|
220
|
+
day: number;
|
|
221
|
+
};
|
|
222
|
+
type MapStageViewedData = {
|
|
223
|
+
blockId: BlockId;
|
|
224
|
+
stageId: string;
|
|
225
|
+
stageIndex: number;
|
|
226
|
+
stageLabel?: string;
|
|
227
|
+
};
|
|
228
|
+
type MapExitSelectedData = {
|
|
229
|
+
blockId: BlockId;
|
|
230
|
+
fromStageId: string;
|
|
231
|
+
toStageId: string;
|
|
232
|
+
label: string;
|
|
233
|
+
scoreWeight?: number;
|
|
234
|
+
};
|
|
184
235
|
type TelemetryEvent = (TelemetryEventBase & {
|
|
185
236
|
name: "course_started";
|
|
186
237
|
lessonId?: LessonId;
|
|
@@ -273,6 +324,50 @@ type TelemetryEvent = (TelemetryEventBase & {
|
|
|
273
324
|
name: "questionnaire_submitted";
|
|
274
325
|
lessonId: LessonId;
|
|
275
326
|
data: QuestionnaireSubmittedData;
|
|
327
|
+
}) | (TelemetryEventBase & {
|
|
328
|
+
name: "branch_node_viewed";
|
|
329
|
+
lessonId: LessonId;
|
|
330
|
+
data: BranchNodeViewedData;
|
|
331
|
+
}) | (TelemetryEventBase & {
|
|
332
|
+
name: "branch_selected";
|
|
333
|
+
lessonId: LessonId;
|
|
334
|
+
data: BranchSelectedData;
|
|
335
|
+
}) | (TelemetryEventBase & {
|
|
336
|
+
name: "image_juxtaposition_changed";
|
|
337
|
+
lessonId?: LessonId;
|
|
338
|
+
data: ImageJuxtapositionChangedData;
|
|
339
|
+
}) | (TelemetryEventBase & {
|
|
340
|
+
name: "timeline_event_viewed";
|
|
341
|
+
lessonId?: LessonId;
|
|
342
|
+
data: TimelineEventViewedData;
|
|
343
|
+
}) | (TelemetryEventBase & {
|
|
344
|
+
name: "image_sequence_changed";
|
|
345
|
+
lessonId?: LessonId;
|
|
346
|
+
data: ImageSequenceChangedData;
|
|
347
|
+
}) | (TelemetryEventBase & {
|
|
348
|
+
name: "audio_recording_started";
|
|
349
|
+
lessonId?: LessonId;
|
|
350
|
+
data: AudioRecordingData;
|
|
351
|
+
}) | (TelemetryEventBase & {
|
|
352
|
+
name: "audio_recording_completed";
|
|
353
|
+
lessonId?: LessonId;
|
|
354
|
+
data: AudioRecordingData;
|
|
355
|
+
}) | (TelemetryEventBase & {
|
|
356
|
+
name: "qr_content_revealed";
|
|
357
|
+
lessonId?: LessonId;
|
|
358
|
+
data: QrContentRevealedData;
|
|
359
|
+
}) | (TelemetryEventBase & {
|
|
360
|
+
name: "advent_door_opened";
|
|
361
|
+
lessonId?: LessonId;
|
|
362
|
+
data: AdventDoorOpenedData;
|
|
363
|
+
}) | (TelemetryEventBase & {
|
|
364
|
+
name: "map_stage_viewed";
|
|
365
|
+
lessonId: LessonId;
|
|
366
|
+
data: MapStageViewedData;
|
|
367
|
+
}) | (TelemetryEventBase & {
|
|
368
|
+
name: "map_exit_selected";
|
|
369
|
+
lessonId: LessonId;
|
|
370
|
+
data: MapExitSelectedData;
|
|
276
371
|
});
|
|
277
372
|
/** Payload shape for a telemetry event name. */
|
|
278
373
|
type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
|
|
@@ -283,7 +378,8 @@ type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
|
|
|
283
378
|
type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
|
|
284
379
|
type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
|
|
285
380
|
type TrackingClient = {
|
|
286
|
-
|
|
381
|
+
/** Returns false when the event was dropped (e.g. buffer cap or after dispose). */
|
|
382
|
+
track: (event: TelemetryEvent) => boolean;
|
|
287
383
|
/** Delivers one event and resolves to true only when the sink accepted it (batch: includes flush). */
|
|
288
384
|
deliver?: (event: TelemetryEvent) => Promise<boolean>;
|
|
289
385
|
/** Resolves to true when all buffered events were delivered; false when a sink failure re-queued events. */
|
|
@@ -414,6 +510,50 @@ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
|
|
|
414
510
|
name: "questionnaire_submitted";
|
|
415
511
|
lessonId?: LessonId;
|
|
416
512
|
data: QuestionnaireSubmittedData;
|
|
513
|
+
}) | (BuildTelemetryEventContext & {
|
|
514
|
+
name: "branch_node_viewed";
|
|
515
|
+
lessonId?: LessonId;
|
|
516
|
+
data: BranchNodeViewedData;
|
|
517
|
+
}) | (BuildTelemetryEventContext & {
|
|
518
|
+
name: "branch_selected";
|
|
519
|
+
lessonId?: LessonId;
|
|
520
|
+
data: BranchSelectedData;
|
|
521
|
+
}) | (BuildTelemetryEventContext & {
|
|
522
|
+
name: "image_juxtaposition_changed";
|
|
523
|
+
lessonId?: LessonId;
|
|
524
|
+
data: ImageJuxtapositionChangedData;
|
|
525
|
+
}) | (BuildTelemetryEventContext & {
|
|
526
|
+
name: "timeline_event_viewed";
|
|
527
|
+
lessonId?: LessonId;
|
|
528
|
+
data: TimelineEventViewedData;
|
|
529
|
+
}) | (BuildTelemetryEventContext & {
|
|
530
|
+
name: "image_sequence_changed";
|
|
531
|
+
lessonId?: LessonId;
|
|
532
|
+
data: ImageSequenceChangedData;
|
|
533
|
+
}) | (BuildTelemetryEventContext & {
|
|
534
|
+
name: "audio_recording_started";
|
|
535
|
+
lessonId?: LessonId;
|
|
536
|
+
data: AudioRecordingData;
|
|
537
|
+
}) | (BuildTelemetryEventContext & {
|
|
538
|
+
name: "audio_recording_completed";
|
|
539
|
+
lessonId?: LessonId;
|
|
540
|
+
data: AudioRecordingData;
|
|
541
|
+
}) | (BuildTelemetryEventContext & {
|
|
542
|
+
name: "qr_content_revealed";
|
|
543
|
+
lessonId?: LessonId;
|
|
544
|
+
data: QrContentRevealedData;
|
|
545
|
+
}) | (BuildTelemetryEventContext & {
|
|
546
|
+
name: "advent_door_opened";
|
|
547
|
+
lessonId?: LessonId;
|
|
548
|
+
data: AdventDoorOpenedData;
|
|
549
|
+
}) | (BuildTelemetryEventContext & {
|
|
550
|
+
name: "map_stage_viewed";
|
|
551
|
+
lessonId?: LessonId;
|
|
552
|
+
data: MapStageViewedData;
|
|
553
|
+
}) | (BuildTelemetryEventContext & {
|
|
554
|
+
name: "map_exit_selected";
|
|
555
|
+
lessonId?: LessonId;
|
|
556
|
+
data: MapExitSelectedData;
|
|
417
557
|
});
|
|
418
558
|
|
|
419
559
|
/** Reset dev-warning state (tests only). */
|
|
@@ -421,10 +561,23 @@ declare function resetTelemetryBuilderWarningsForTests(): void;
|
|
|
421
561
|
/**
|
|
422
562
|
* Build a typed telemetry event from a catalog event name and context.
|
|
423
563
|
* Validates lesson-scoped events require `lessonId`.
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```ts
|
|
567
|
+
* import { buildTelemetryEvent } from "@lessonkit/core";
|
|
568
|
+
*
|
|
569
|
+
* const event = buildTelemetryEvent({
|
|
570
|
+
* name: "lesson_completed",
|
|
571
|
+
* courseId: "sec-101",
|
|
572
|
+
* lessonId: "phishing-101",
|
|
573
|
+
* sessionId: "tab-abc",
|
|
574
|
+
* });
|
|
575
|
+
* ```
|
|
424
576
|
*/
|
|
425
577
|
declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent;
|
|
426
578
|
/**
|
|
427
|
-
* Like `buildTelemetryEvent`, but returns null
|
|
579
|
+
* Like `buildTelemetryEvent`, but returns null when lesson-scoped events lack `lessonId`
|
|
580
|
+
* (with dev warnings for quiz/assessment events).
|
|
428
581
|
*/
|
|
429
582
|
declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
|
|
430
583
|
|
|
@@ -457,7 +610,9 @@ declare function hasCourseStartedEmittedToTracking(storage: StoragePort, session
|
|
|
457
610
|
declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
458
611
|
declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
459
612
|
declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
460
|
-
|
|
613
|
+
declare function hasCourseStartedXapiSent(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
614
|
+
declare function markCourseStartedXapiSent(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
615
|
+
/** @internal Reset volatile session ids between tests. */
|
|
461
616
|
declare function resetSharedVolatileSessionIdForTests(): void;
|
|
462
617
|
declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
|
|
463
618
|
|
|
@@ -547,18 +702,48 @@ type CourseLifecycleContext = {
|
|
|
547
702
|
type CourseLifecycleDeps = {
|
|
548
703
|
emitCourseStartedEvent: (ctx: CourseLifecycleContext) => boolean;
|
|
549
704
|
};
|
|
550
|
-
|
|
705
|
+
/**
|
|
706
|
+
* Emit `course_started` once per tab session when tracking/xAPI are active.
|
|
707
|
+
* Coalesces concurrent calls via an in-flight guard.
|
|
708
|
+
*/
|
|
709
|
+
declare function tryEmitCourseStarted(ctx: CourseLifecycleContext, deps: CourseLifecycleDeps, alreadyEmittedToSink: boolean): Promise<{
|
|
551
710
|
emitted: boolean;
|
|
552
711
|
marked: boolean;
|
|
553
|
-
}
|
|
712
|
+
}>;
|
|
554
713
|
declare function buildCourseStartedTelemetryEvent(ctx: CourseLifecycleContext): TelemetryEvent;
|
|
555
714
|
type LessonCompletionEmitter = (lessonId: LessonId, durationMs?: number) => void;
|
|
715
|
+
/**
|
|
716
|
+
* Mark a lesson complete in progress state and emit `lesson_completed` when newly completed.
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* ```ts
|
|
720
|
+
* completeLessonWithTelemetry({
|
|
721
|
+
* progress,
|
|
722
|
+
* lessonId: "lesson-1",
|
|
723
|
+
* nowMs: Date.now(),
|
|
724
|
+
* emitLessonCompleted: (id, durationMs) => track("lesson_completed", { lessonId: id, durationMs }),
|
|
725
|
+
* });
|
|
726
|
+
* ```
|
|
727
|
+
*/
|
|
556
728
|
declare function completeLessonWithTelemetry(opts: {
|
|
557
729
|
progress: ProgressController;
|
|
558
730
|
lessonId: LessonId;
|
|
559
731
|
nowMs: number;
|
|
560
732
|
emitLessonCompleted: LessonCompletionEmitter;
|
|
561
733
|
}): boolean;
|
|
734
|
+
/**
|
|
735
|
+
* Complete the active lesson (if any), then mark the course complete and emit `course_completed`.
|
|
736
|
+
*
|
|
737
|
+
* @example
|
|
738
|
+
* ```ts
|
|
739
|
+
* completeCourseWithTelemetry({
|
|
740
|
+
* progress,
|
|
741
|
+
* nowMs: Date.now(),
|
|
742
|
+
* emitLessonCompleted: (id) => track("lesson_completed", { lessonId: id }),
|
|
743
|
+
* emitCourseCompleted: () => track("course_completed", {}),
|
|
744
|
+
* });
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
562
747
|
declare function completeCourseWithTelemetry(opts: {
|
|
563
748
|
progress: ProgressController;
|
|
564
749
|
nowMs: number;
|
|
@@ -566,4 +751,4 @@ declare function completeCourseWithTelemetry(opts: {
|
|
|
566
751
|
emitCourseCompleted: () => void;
|
|
567
752
|
}): boolean;
|
|
568
753
|
|
|
569
|
-
export { type
|
|
754
|
+
export { type LessonCompletionEmitter as $, type AssessmentResumeState as A, type BlockId as B, type CourseId as C, type AssessmentInteractionType as D, type AssessmentXAPIData as E, type BookPageViewedData as F, type BranchNodeViewedData as G, type BranchSelectedData as H, type IdentityIdPath as I, type BuildTelemetryEventInput as J, type CompoundPageViewedData as K, type LessonId as L, type CourseLifecycleContext as M, type CourseLifecycleDeps as N, type FlashcardFlippedData as O, type PluginRegistry as P, type HotspotOpenedData as Q, ID_MAX_LENGTH as R, type StoragePort as S, type TelemetryEventName as T, ID_PATTERN as U, type IdentityValidationIssue as V, type ImageSliderChangedData as W, type InformationWallSearchData as X, type InteractionBlockRegistration as Y, type InteractionData as Z, type InteractionPlugin as _, type CheckId as a, type LessonLifecycleData as a0, type LessonkitPluginKind as a1, type McqAssessmentProps as a2, type MemoryCardFlippedData as a3, type ParallaxSlideViewedData as a4, type PluginIdentity as a5, type QuestionnaireSubmittedData as a6, type QuizAnsweredData as a7, type QuizCompletedData as a8, SESSION_STORAGE_KEY as a9, resetTelemetryBuilderWarningsForTests as aA, resolveSessionId as aB, tryBuildTelemetryEvent as aC, tryEmitCourseStarted as aD, resetCourseStartedEmitFlightForTests as aE, type SlideViewedData as aa, type TelemetryEventBase as ab, type TimerPort as ac, type VideoCueReachedData as ad, type VideoSegmentCompletedData as ae, buildCourseStartedTelemetryEvent as af, buildTelemetryEvent as ag, completeCourseWithTelemetry as ah, completeLessonWithTelemetry as ai, createDefaultClock as aj, createGlobalTimer as ak, createNoopStorage as al, createProgressController as am, createSessionStoragePort as an, getTabSessionId as ao, hasCourseStarted as ap, hasCourseStartedEmittedToTracking as aq, hasCourseStartedPipelineDelivered as ar, hasCourseStartedXapiSent as as, markCourseStarted as at, markCourseStartedEmittedToTracking as au, markCourseStartedPipelineDelivered as av, markCourseStartedXapiSent as aw, migrateCourseStartedMark as ax, resetSharedVolatileSessionIdForTests as ay, resetStoragePortForTests as az, type IdentityValidationResult as b, type LessonkitUrn as c, type TelemetrySink as d, type TelemetryBatchSink as e, type TrackingClient as f, type TelemetryEvent as g, type TelemetryUser as h, type LessonkitPlugin as i, type ProgressController as j, type PluginHost as k, type ProgressState as l, type TelemetryDataFor as m, type AssessmentScoreInput as n, type AssessmentScoreResult as o, type ClockPort as p, type AssessmentPlugin as q, type LifecyclePlugin as r, type TelemetryPlugin as s, type LessonkitPluginContext as t, type AccordionSectionToggledData as u, type AssessmentAnsweredData as v, type AssessmentBaseProps as w, type AssessmentBehaviour as x, type AssessmentCompletedData as y, type AssessmentHandle as z };
|
package/dist/testing.cjs
CHANGED
|
@@ -41,13 +41,11 @@ function resetStoragePortForTests(storage) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// src/session.ts
|
|
44
|
-
var sharedVolatileSessionId = null;
|
|
45
44
|
function resetSharedVolatileSessionIdForTests() {
|
|
46
|
-
sharedVolatileSessionId = null;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
// src/runtime/courseLifecycle.ts
|
|
50
|
-
var courseStartedEmitFlights = /* @__PURE__ */ new
|
|
48
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
51
49
|
function resetCourseStartedEmitFlightForTests() {
|
|
52
50
|
courseStartedEmitFlights.clear();
|
|
53
51
|
}
|
package/dist/testing.d.cts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { aE as resetCourseStartedEmitFlightForTests, ay as resetSharedVolatileSessionIdForTests, az as resetStoragePortForTests, aA as resetTelemetryBuilderWarningsForTests } from './testing-CzgxF1Ru.cjs';
|
package/dist/testing.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { aE as resetCourseStartedEmitFlightForTests, ay as resetSharedVolatileSessionIdForTests, az as resetStoragePortForTests, aA as resetTelemetryBuilderWarningsForTests } from './testing-CzgxF1Ru.js';
|
package/dist/testing.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
resetSharedVolatileSessionIdForTests,
|
|
4
4
|
resetStoragePortForTests,
|
|
5
5
|
resetTelemetryBuilderWarningsForTests
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-NGCHHJSM.js";
|
|
7
7
|
export {
|
|
8
8
|
resetCourseStartedEmitFlightForTests,
|
|
9
9
|
resetSharedVolatileSessionIdForTests,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared types and telemetry primitives for LessonKit.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"typecheck": "tsc -p tsconfig.json",
|
|
55
55
|
"test": "vitest run --passWithNoTests",
|
|
56
56
|
"test:coverage": "vitest run --coverage --passWithNoTests=false",
|
|
57
|
-
"lint": "
|
|
57
|
+
"lint": "eslint --max-warnings 0 \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\""
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^25.9.2",
|
|
61
61
|
"tsup": "^8.5.0",
|
|
62
|
-
"typescript": "^
|
|
62
|
+
"typescript": "^6.0.3",
|
|
63
63
|
"vitest": "^4.1.8"
|
|
64
64
|
}
|
|
65
65
|
}
|