@lessonkit/core 1.0.0 → 1.0.1
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 +63 -13
- package/dist/index.d.cts +73 -15
- package/dist/index.d.ts +73 -15
- package/dist/index.js +56 -13
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
ID_PATTERN: () => ID_PATTERN,
|
|
25
25
|
SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
|
|
26
26
|
TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
|
|
27
|
+
assertNever: () => assertNever,
|
|
27
28
|
assertValidId: () => assertValidId,
|
|
28
29
|
buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
|
|
29
30
|
buildLessonkitUrn: () => buildLessonkitUrn,
|
|
@@ -49,10 +50,16 @@ __export(index_exports, {
|
|
|
49
50
|
getTabSessionId: () => getTabSessionId,
|
|
50
51
|
hasCourseStarted: () => hasCourseStarted,
|
|
51
52
|
hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
|
|
53
|
+
hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
|
|
52
54
|
markCourseStarted: () => markCourseStarted,
|
|
53
55
|
markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
|
|
56
|
+
markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
|
|
54
57
|
migrateCourseStartedMark: () => migrateCourseStartedMark,
|
|
55
58
|
nowIso: () => nowIso,
|
|
59
|
+
parseBlockId: () => parseBlockId,
|
|
60
|
+
parseCheckId: () => parseCheckId,
|
|
61
|
+
parseCourseId: () => parseCourseId,
|
|
62
|
+
parseLessonId: () => parseLessonId,
|
|
56
63
|
resetStoragePortForTests: () => resetStoragePortForTests,
|
|
57
64
|
resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
|
|
58
65
|
resolveSessionId: () => resolveSessionId,
|
|
@@ -68,6 +75,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
68
75
|
var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
69
76
|
var ID_MAX_LENGTH = 64;
|
|
70
77
|
|
|
78
|
+
// src/assertNever.ts
|
|
79
|
+
function assertNever(value, message = "Unexpected value") {
|
|
80
|
+
throw new Error(`${message}: ${String(value)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
// src/validateId.ts
|
|
72
84
|
function validateId(input, path = "id") {
|
|
73
85
|
if (typeof input !== "string") {
|
|
@@ -96,6 +108,22 @@ function validateId(input, path = "id") {
|
|
|
96
108
|
}
|
|
97
109
|
return { ok: true, id };
|
|
98
110
|
}
|
|
111
|
+
function parseCourseId(input) {
|
|
112
|
+
const result = validateId(input, "courseId");
|
|
113
|
+
return result.ok ? result.id : null;
|
|
114
|
+
}
|
|
115
|
+
function parseLessonId(input) {
|
|
116
|
+
const result = validateId(input, "lessonId");
|
|
117
|
+
return result.ok ? result.id : null;
|
|
118
|
+
}
|
|
119
|
+
function parseCheckId(input) {
|
|
120
|
+
const result = validateId(input, "checkId");
|
|
121
|
+
return result.ok ? result.id : null;
|
|
122
|
+
}
|
|
123
|
+
function parseBlockId(input) {
|
|
124
|
+
const result = validateId(input, "blockId");
|
|
125
|
+
return result.ok ? result.id : null;
|
|
126
|
+
}
|
|
99
127
|
function assertValidId(input, path = "id") {
|
|
100
128
|
const result = validateId(input, path);
|
|
101
129
|
if (!result.ok) {
|
|
@@ -369,6 +397,11 @@ function isDevEnvironment2() {
|
|
|
369
397
|
function resetTelemetryBuilderWarningsForTests() {
|
|
370
398
|
warnedMissingQuizLesson = false;
|
|
371
399
|
}
|
|
400
|
+
function resolveLessonId(opts, eventName) {
|
|
401
|
+
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
402
|
+
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
403
|
+
return lessonId;
|
|
404
|
+
}
|
|
372
405
|
function buildTelemetryEvent(opts) {
|
|
373
406
|
const base = {
|
|
374
407
|
timestamp: opts.timestamp ?? nowIso(),
|
|
@@ -383,39 +416,33 @@ function buildTelemetryEvent(opts) {
|
|
|
383
416
|
case "course_completed":
|
|
384
417
|
return { name: "course_completed", ...base };
|
|
385
418
|
case "lesson_started": {
|
|
386
|
-
const
|
|
387
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
388
|
-
if (!lessonId) throw new Error("lesson_started requires lessonId");
|
|
419
|
+
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
389
420
|
return {
|
|
390
421
|
name: "lesson_started",
|
|
391
422
|
...base,
|
|
392
423
|
lessonId,
|
|
393
|
-
data: { ...data, lessonId }
|
|
424
|
+
data: { ...opts.data, lessonId }
|
|
394
425
|
};
|
|
395
426
|
}
|
|
396
427
|
case "lesson_completed":
|
|
397
428
|
case "lesson_time_on_task": {
|
|
398
|
-
const
|
|
399
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
400
|
-
if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
|
|
429
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
401
430
|
return {
|
|
402
431
|
name: opts.name,
|
|
403
432
|
...base,
|
|
404
433
|
lessonId,
|
|
405
|
-
data: { ...data, lessonId }
|
|
434
|
+
data: { ...opts.data, lessonId }
|
|
406
435
|
};
|
|
407
436
|
}
|
|
408
437
|
case "quiz_answered": {
|
|
409
|
-
const data = opts.data;
|
|
410
438
|
const lessonId = opts.lessonId;
|
|
411
439
|
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
412
|
-
return { name: "quiz_answered", ...base, lessonId, data };
|
|
440
|
+
return { name: "quiz_answered", ...base, lessonId, data: opts.data };
|
|
413
441
|
}
|
|
414
442
|
case "quiz_completed": {
|
|
415
|
-
const data = opts.data;
|
|
416
443
|
const lessonId = opts.lessonId;
|
|
417
444
|
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
418
|
-
return { name: "quiz_completed", ...base, lessonId, data };
|
|
445
|
+
return { name: "quiz_completed", ...base, lessonId, data: opts.data };
|
|
419
446
|
}
|
|
420
447
|
case "interaction":
|
|
421
448
|
return {
|
|
@@ -425,7 +452,7 @@ function buildTelemetryEvent(opts) {
|
|
|
425
452
|
data: opts.data
|
|
426
453
|
};
|
|
427
454
|
default:
|
|
428
|
-
return
|
|
455
|
+
return assertNever(opts);
|
|
429
456
|
}
|
|
430
457
|
}
|
|
431
458
|
function tryBuildTelemetryEvent(opts) {
|
|
@@ -622,6 +649,7 @@ function getTabSessionId(storage) {
|
|
|
622
649
|
}
|
|
623
650
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
624
651
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
652
|
+
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
625
653
|
function resolveSessionId(storage, provided) {
|
|
626
654
|
if (provided) return provided;
|
|
627
655
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
@@ -636,6 +664,9 @@ function courseStartedStorageKey(sessionId, courseId) {
|
|
|
636
664
|
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
637
665
|
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
638
666
|
}
|
|
667
|
+
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
668
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
669
|
+
}
|
|
639
670
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
640
671
|
if (!courseId) return false;
|
|
641
672
|
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
@@ -652,6 +683,14 @@ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
|
652
683
|
if (!courseId) return;
|
|
653
684
|
storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
654
685
|
}
|
|
686
|
+
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
687
|
+
if (!courseId) return false;
|
|
688
|
+
return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
|
|
689
|
+
}
|
|
690
|
+
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
691
|
+
if (!courseId) return;
|
|
692
|
+
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
693
|
+
}
|
|
655
694
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
656
695
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
657
696
|
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
@@ -662,6 +701,10 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
662
701
|
markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
|
|
663
702
|
storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
|
|
664
703
|
}
|
|
704
|
+
if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
|
|
705
|
+
markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
|
|
706
|
+
storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
|
|
707
|
+
}
|
|
665
708
|
}
|
|
666
709
|
|
|
667
710
|
// src/runtime/courseLifecycle.ts
|
|
@@ -911,6 +954,7 @@ function defineLifecyclePlugin(plugin) {
|
|
|
911
954
|
ID_PATTERN,
|
|
912
955
|
SESSION_STORAGE_KEY,
|
|
913
956
|
TELEMETRY_EVENT_CATALOG,
|
|
957
|
+
assertNever,
|
|
914
958
|
assertValidId,
|
|
915
959
|
buildCourseStartedTelemetryEvent,
|
|
916
960
|
buildLessonkitUrn,
|
|
@@ -936,10 +980,16 @@ function defineLifecyclePlugin(plugin) {
|
|
|
936
980
|
getTabSessionId,
|
|
937
981
|
hasCourseStarted,
|
|
938
982
|
hasCourseStartedEmittedToTracking,
|
|
983
|
+
hasCourseStartedPipelineDelivered,
|
|
939
984
|
markCourseStarted,
|
|
940
985
|
markCourseStartedEmittedToTracking,
|
|
986
|
+
markCourseStartedPipelineDelivered,
|
|
941
987
|
migrateCourseStartedMark,
|
|
942
988
|
nowIso,
|
|
989
|
+
parseBlockId,
|
|
990
|
+
parseCheckId,
|
|
991
|
+
parseCourseId,
|
|
992
|
+
parseLessonId,
|
|
943
993
|
resetStoragePortForTests,
|
|
944
994
|
resetTelemetryBuilderWarningsForTests,
|
|
945
995
|
resolveSessionId,
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,8 @@ type CourseId = string;
|
|
|
2
2
|
type LessonId = string;
|
|
3
3
|
type CheckId = string;
|
|
4
4
|
type BlockId = string;
|
|
5
|
+
/** Stable URN string returned by {@link buildLessonkitUrn}. */
|
|
6
|
+
type LessonkitUrn = string;
|
|
5
7
|
type IdentityValidationIssue = {
|
|
6
8
|
path: string;
|
|
7
9
|
message: string;
|
|
@@ -13,12 +15,27 @@ type IdentityValidationResult = {
|
|
|
13
15
|
ok: false;
|
|
14
16
|
issues: IdentityValidationIssue[];
|
|
15
17
|
};
|
|
18
|
+
type IdentityIdPath = "courseId" | "lessonId" | "checkId" | "blockId" | "id";
|
|
16
19
|
/** LessonKit id format: letter first, then alphanumeric, `_`, `-`; length 1–64. */
|
|
17
20
|
declare const ID_PATTERN: RegExp;
|
|
18
21
|
declare const ID_MAX_LENGTH = 64;
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Exhaustiveness helper for switch/default branches.
|
|
25
|
+
* @throws when called at runtime with an unexpected value.
|
|
26
|
+
*/
|
|
27
|
+
declare function assertNever(value: never, message?: string): never;
|
|
28
|
+
|
|
29
|
+
declare function validateId(input: unknown, path?: IdentityIdPath | string): IdentityValidationResult;
|
|
30
|
+
declare function parseCourseId(input: unknown): CourseId | null;
|
|
31
|
+
declare function parseLessonId(input: unknown): LessonId | null;
|
|
32
|
+
declare function parseCheckId(input: unknown): CheckId | null;
|
|
33
|
+
declare function parseBlockId(input: unknown): BlockId | null;
|
|
34
|
+
declare function assertValidId(input: unknown, path: "courseId"): CourseId;
|
|
35
|
+
declare function assertValidId(input: unknown, path: "lessonId"): LessonId;
|
|
36
|
+
declare function assertValidId(input: unknown, path: "checkId"): CheckId;
|
|
37
|
+
declare function assertValidId(input: unknown, path: "blockId"): BlockId;
|
|
38
|
+
declare function assertValidId(input: unknown, path?: IdentityIdPath | string): string;
|
|
22
39
|
|
|
23
40
|
/** Convert human-readable text to a candidate LessonKit id (may still need collision handling via deriveId). */
|
|
24
41
|
declare function slugifyId(input: string): string;
|
|
@@ -35,7 +52,7 @@ type LessonkitUrnParts = {
|
|
|
35
52
|
* Build a stable LessonKit URN for courses, lessons, checks, and blocks.
|
|
36
53
|
* Segments are validated and encoded in path order.
|
|
37
54
|
*/
|
|
38
|
-
declare function buildLessonkitUrn(parts: LessonkitUrnParts):
|
|
55
|
+
declare function buildLessonkitUrn(parts: LessonkitUrnParts): LessonkitUrn;
|
|
39
56
|
|
|
40
57
|
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
|
|
41
58
|
type TelemetryUser = {
|
|
@@ -109,6 +126,12 @@ type TelemetryEvent = (TelemetryEventBase & {
|
|
|
109
126
|
lessonId?: LessonId;
|
|
110
127
|
data?: InteractionData;
|
|
111
128
|
});
|
|
129
|
+
/** Payload shape for a telemetry event name. */
|
|
130
|
+
type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
|
|
131
|
+
name: N;
|
|
132
|
+
}> extends {
|
|
133
|
+
data?: infer D;
|
|
134
|
+
} ? D : never;
|
|
112
135
|
type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
|
|
113
136
|
type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
|
|
114
137
|
type TrackingClient = {
|
|
@@ -143,16 +166,46 @@ declare function createSessionId(): string;
|
|
|
143
166
|
|
|
144
167
|
declare function nowIso(): string;
|
|
145
168
|
|
|
146
|
-
type
|
|
147
|
-
name: TelemetryEventName;
|
|
169
|
+
type BuildTelemetryEventContext = {
|
|
148
170
|
courseId: CourseId;
|
|
149
|
-
lessonId?: LessonId;
|
|
150
171
|
sessionId?: string;
|
|
151
172
|
attemptId?: string;
|
|
152
173
|
user?: TelemetryUser;
|
|
153
|
-
data?: unknown;
|
|
154
174
|
timestamp?: string;
|
|
155
175
|
};
|
|
176
|
+
type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
|
|
177
|
+
name: "course_started";
|
|
178
|
+
lessonId?: LessonId;
|
|
179
|
+
data?: undefined;
|
|
180
|
+
}) | (BuildTelemetryEventContext & {
|
|
181
|
+
name: "course_completed";
|
|
182
|
+
lessonId?: LessonId;
|
|
183
|
+
data?: undefined;
|
|
184
|
+
}) | (BuildTelemetryEventContext & {
|
|
185
|
+
name: "lesson_started";
|
|
186
|
+
lessonId?: LessonId;
|
|
187
|
+
data?: LessonLifecycleData;
|
|
188
|
+
}) | (BuildTelemetryEventContext & {
|
|
189
|
+
name: "lesson_completed";
|
|
190
|
+
lessonId?: LessonId;
|
|
191
|
+
data?: LessonLifecycleData;
|
|
192
|
+
}) | (BuildTelemetryEventContext & {
|
|
193
|
+
name: "lesson_time_on_task";
|
|
194
|
+
lessonId?: LessonId;
|
|
195
|
+
data?: LessonLifecycleData;
|
|
196
|
+
}) | (BuildTelemetryEventContext & {
|
|
197
|
+
name: "quiz_answered";
|
|
198
|
+
lessonId?: LessonId;
|
|
199
|
+
data: QuizAnsweredData;
|
|
200
|
+
}) | (BuildTelemetryEventContext & {
|
|
201
|
+
name: "quiz_completed";
|
|
202
|
+
lessonId?: LessonId;
|
|
203
|
+
data: QuizCompletedData;
|
|
204
|
+
}) | (BuildTelemetryEventContext & {
|
|
205
|
+
name: "interaction";
|
|
206
|
+
lessonId?: LessonId;
|
|
207
|
+
data?: InteractionData;
|
|
208
|
+
});
|
|
156
209
|
/** Reset dev-warning state (tests only). */
|
|
157
210
|
declare function resetTelemetryBuilderWarningsForTests(): void;
|
|
158
211
|
/**
|
|
@@ -166,7 +219,7 @@ declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryE
|
|
|
166
219
|
declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
|
|
167
220
|
|
|
168
221
|
type EmitContext = {
|
|
169
|
-
courseId:
|
|
222
|
+
courseId: CourseId;
|
|
170
223
|
sessionId?: string;
|
|
171
224
|
attemptId?: string;
|
|
172
225
|
};
|
|
@@ -230,6 +283,8 @@ declare function hasCourseStarted(storage: StoragePort, sessionId: string, cours
|
|
|
230
283
|
declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
231
284
|
declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
232
285
|
declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
286
|
+
declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
287
|
+
declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
233
288
|
declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
|
|
234
289
|
|
|
235
290
|
/** Plugin category — aligns with roadmap extension areas. */
|
|
@@ -241,8 +296,8 @@ type LessonkitPluginContext = {
|
|
|
241
296
|
user?: TelemetryUser;
|
|
242
297
|
};
|
|
243
298
|
type AssessmentScoreInput = {
|
|
244
|
-
checkId:
|
|
245
|
-
lessonId?:
|
|
299
|
+
checkId: CheckId;
|
|
300
|
+
lessonId?: LessonId;
|
|
246
301
|
response: unknown;
|
|
247
302
|
};
|
|
248
303
|
type AssessmentScoreResult = {
|
|
@@ -347,6 +402,9 @@ type HeadlessRuntimePorts = {
|
|
|
347
402
|
storage?: StoragePort;
|
|
348
403
|
clock?: ClockPort;
|
|
349
404
|
};
|
|
405
|
+
type TelemetryEmitFn = {
|
|
406
|
+
<N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, lessonId?: LessonId): void;
|
|
407
|
+
};
|
|
350
408
|
type HeadlessLessonkitRuntime = {
|
|
351
409
|
readonly config: HeadlessLessonkitConfig;
|
|
352
410
|
readonly progress: ProgressController;
|
|
@@ -357,10 +415,10 @@ type HeadlessLessonkitRuntime = {
|
|
|
357
415
|
user?: TelemetryUser;
|
|
358
416
|
};
|
|
359
417
|
updateConfig: (next: Partial<HeadlessLessonkitConfig>) => void;
|
|
360
|
-
setActiveLesson: (lessonId: LessonId, emit:
|
|
361
|
-
completeLesson: (lessonId: LessonId, emit:
|
|
362
|
-
completeCourse: (emit:
|
|
363
|
-
track: (name:
|
|
418
|
+
setActiveLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
|
|
419
|
+
completeLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
|
|
420
|
+
completeCourse: (emit: TelemetryEmitFn) => void;
|
|
421
|
+
track: <N extends TelemetryEventName>(name: N, data: TelemetryDataFor<N> | undefined, emit: (event: TelemetryEvent) => void, lessonId?: LessonId) => void;
|
|
364
422
|
resetForCourseChange: (courseId: CourseId) => void;
|
|
365
423
|
};
|
|
366
424
|
declare function createLessonkitRuntime(config: HeadlessLessonkitConfig, ports?: HeadlessRuntimePorts): HeadlessLessonkitRuntime;
|
|
@@ -371,4 +429,4 @@ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin
|
|
|
371
429
|
declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
|
|
372
430
|
declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
|
|
373
431
|
|
|
374
|
-
export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, markCourseStarted, markCourseStartedEmittedToTracking, migrateCourseStartedMark, nowIso, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
|
|
432
|
+
export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ type CourseId = string;
|
|
|
2
2
|
type LessonId = string;
|
|
3
3
|
type CheckId = string;
|
|
4
4
|
type BlockId = string;
|
|
5
|
+
/** Stable URN string returned by {@link buildLessonkitUrn}. */
|
|
6
|
+
type LessonkitUrn = string;
|
|
5
7
|
type IdentityValidationIssue = {
|
|
6
8
|
path: string;
|
|
7
9
|
message: string;
|
|
@@ -13,12 +15,27 @@ type IdentityValidationResult = {
|
|
|
13
15
|
ok: false;
|
|
14
16
|
issues: IdentityValidationIssue[];
|
|
15
17
|
};
|
|
18
|
+
type IdentityIdPath = "courseId" | "lessonId" | "checkId" | "blockId" | "id";
|
|
16
19
|
/** LessonKit id format: letter first, then alphanumeric, `_`, `-`; length 1–64. */
|
|
17
20
|
declare const ID_PATTERN: RegExp;
|
|
18
21
|
declare const ID_MAX_LENGTH = 64;
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Exhaustiveness helper for switch/default branches.
|
|
25
|
+
* @throws when called at runtime with an unexpected value.
|
|
26
|
+
*/
|
|
27
|
+
declare function assertNever(value: never, message?: string): never;
|
|
28
|
+
|
|
29
|
+
declare function validateId(input: unknown, path?: IdentityIdPath | string): IdentityValidationResult;
|
|
30
|
+
declare function parseCourseId(input: unknown): CourseId | null;
|
|
31
|
+
declare function parseLessonId(input: unknown): LessonId | null;
|
|
32
|
+
declare function parseCheckId(input: unknown): CheckId | null;
|
|
33
|
+
declare function parseBlockId(input: unknown): BlockId | null;
|
|
34
|
+
declare function assertValidId(input: unknown, path: "courseId"): CourseId;
|
|
35
|
+
declare function assertValidId(input: unknown, path: "lessonId"): LessonId;
|
|
36
|
+
declare function assertValidId(input: unknown, path: "checkId"): CheckId;
|
|
37
|
+
declare function assertValidId(input: unknown, path: "blockId"): BlockId;
|
|
38
|
+
declare function assertValidId(input: unknown, path?: IdentityIdPath | string): string;
|
|
22
39
|
|
|
23
40
|
/** Convert human-readable text to a candidate LessonKit id (may still need collision handling via deriveId). */
|
|
24
41
|
declare function slugifyId(input: string): string;
|
|
@@ -35,7 +52,7 @@ type LessonkitUrnParts = {
|
|
|
35
52
|
* Build a stable LessonKit URN for courses, lessons, checks, and blocks.
|
|
36
53
|
* Segments are validated and encoded in path order.
|
|
37
54
|
*/
|
|
38
|
-
declare function buildLessonkitUrn(parts: LessonkitUrnParts):
|
|
55
|
+
declare function buildLessonkitUrn(parts: LessonkitUrnParts): LessonkitUrn;
|
|
39
56
|
|
|
40
57
|
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
|
|
41
58
|
type TelemetryUser = {
|
|
@@ -109,6 +126,12 @@ type TelemetryEvent = (TelemetryEventBase & {
|
|
|
109
126
|
lessonId?: LessonId;
|
|
110
127
|
data?: InteractionData;
|
|
111
128
|
});
|
|
129
|
+
/** Payload shape for a telemetry event name. */
|
|
130
|
+
type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
|
|
131
|
+
name: N;
|
|
132
|
+
}> extends {
|
|
133
|
+
data?: infer D;
|
|
134
|
+
} ? D : never;
|
|
112
135
|
type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
|
|
113
136
|
type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
|
|
114
137
|
type TrackingClient = {
|
|
@@ -143,16 +166,46 @@ declare function createSessionId(): string;
|
|
|
143
166
|
|
|
144
167
|
declare function nowIso(): string;
|
|
145
168
|
|
|
146
|
-
type
|
|
147
|
-
name: TelemetryEventName;
|
|
169
|
+
type BuildTelemetryEventContext = {
|
|
148
170
|
courseId: CourseId;
|
|
149
|
-
lessonId?: LessonId;
|
|
150
171
|
sessionId?: string;
|
|
151
172
|
attemptId?: string;
|
|
152
173
|
user?: TelemetryUser;
|
|
153
|
-
data?: unknown;
|
|
154
174
|
timestamp?: string;
|
|
155
175
|
};
|
|
176
|
+
type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
|
|
177
|
+
name: "course_started";
|
|
178
|
+
lessonId?: LessonId;
|
|
179
|
+
data?: undefined;
|
|
180
|
+
}) | (BuildTelemetryEventContext & {
|
|
181
|
+
name: "course_completed";
|
|
182
|
+
lessonId?: LessonId;
|
|
183
|
+
data?: undefined;
|
|
184
|
+
}) | (BuildTelemetryEventContext & {
|
|
185
|
+
name: "lesson_started";
|
|
186
|
+
lessonId?: LessonId;
|
|
187
|
+
data?: LessonLifecycleData;
|
|
188
|
+
}) | (BuildTelemetryEventContext & {
|
|
189
|
+
name: "lesson_completed";
|
|
190
|
+
lessonId?: LessonId;
|
|
191
|
+
data?: LessonLifecycleData;
|
|
192
|
+
}) | (BuildTelemetryEventContext & {
|
|
193
|
+
name: "lesson_time_on_task";
|
|
194
|
+
lessonId?: LessonId;
|
|
195
|
+
data?: LessonLifecycleData;
|
|
196
|
+
}) | (BuildTelemetryEventContext & {
|
|
197
|
+
name: "quiz_answered";
|
|
198
|
+
lessonId?: LessonId;
|
|
199
|
+
data: QuizAnsweredData;
|
|
200
|
+
}) | (BuildTelemetryEventContext & {
|
|
201
|
+
name: "quiz_completed";
|
|
202
|
+
lessonId?: LessonId;
|
|
203
|
+
data: QuizCompletedData;
|
|
204
|
+
}) | (BuildTelemetryEventContext & {
|
|
205
|
+
name: "interaction";
|
|
206
|
+
lessonId?: LessonId;
|
|
207
|
+
data?: InteractionData;
|
|
208
|
+
});
|
|
156
209
|
/** Reset dev-warning state (tests only). */
|
|
157
210
|
declare function resetTelemetryBuilderWarningsForTests(): void;
|
|
158
211
|
/**
|
|
@@ -166,7 +219,7 @@ declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryE
|
|
|
166
219
|
declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
|
|
167
220
|
|
|
168
221
|
type EmitContext = {
|
|
169
|
-
courseId:
|
|
222
|
+
courseId: CourseId;
|
|
170
223
|
sessionId?: string;
|
|
171
224
|
attemptId?: string;
|
|
172
225
|
};
|
|
@@ -230,6 +283,8 @@ declare function hasCourseStarted(storage: StoragePort, sessionId: string, cours
|
|
|
230
283
|
declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
231
284
|
declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
232
285
|
declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
286
|
+
declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
287
|
+
declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
233
288
|
declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
|
|
234
289
|
|
|
235
290
|
/** Plugin category — aligns with roadmap extension areas. */
|
|
@@ -241,8 +296,8 @@ type LessonkitPluginContext = {
|
|
|
241
296
|
user?: TelemetryUser;
|
|
242
297
|
};
|
|
243
298
|
type AssessmentScoreInput = {
|
|
244
|
-
checkId:
|
|
245
|
-
lessonId?:
|
|
299
|
+
checkId: CheckId;
|
|
300
|
+
lessonId?: LessonId;
|
|
246
301
|
response: unknown;
|
|
247
302
|
};
|
|
248
303
|
type AssessmentScoreResult = {
|
|
@@ -347,6 +402,9 @@ type HeadlessRuntimePorts = {
|
|
|
347
402
|
storage?: StoragePort;
|
|
348
403
|
clock?: ClockPort;
|
|
349
404
|
};
|
|
405
|
+
type TelemetryEmitFn = {
|
|
406
|
+
<N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, lessonId?: LessonId): void;
|
|
407
|
+
};
|
|
350
408
|
type HeadlessLessonkitRuntime = {
|
|
351
409
|
readonly config: HeadlessLessonkitConfig;
|
|
352
410
|
readonly progress: ProgressController;
|
|
@@ -357,10 +415,10 @@ type HeadlessLessonkitRuntime = {
|
|
|
357
415
|
user?: TelemetryUser;
|
|
358
416
|
};
|
|
359
417
|
updateConfig: (next: Partial<HeadlessLessonkitConfig>) => void;
|
|
360
|
-
setActiveLesson: (lessonId: LessonId, emit:
|
|
361
|
-
completeLesson: (lessonId: LessonId, emit:
|
|
362
|
-
completeCourse: (emit:
|
|
363
|
-
track: (name:
|
|
418
|
+
setActiveLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
|
|
419
|
+
completeLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
|
|
420
|
+
completeCourse: (emit: TelemetryEmitFn) => void;
|
|
421
|
+
track: <N extends TelemetryEventName>(name: N, data: TelemetryDataFor<N> | undefined, emit: (event: TelemetryEvent) => void, lessonId?: LessonId) => void;
|
|
364
422
|
resetForCourseChange: (courseId: CourseId) => void;
|
|
365
423
|
};
|
|
366
424
|
declare function createLessonkitRuntime(config: HeadlessLessonkitConfig, ports?: HeadlessRuntimePorts): HeadlessLessonkitRuntime;
|
|
@@ -371,4 +429,4 @@ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin
|
|
|
371
429
|
declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
|
|
372
430
|
declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
|
|
373
431
|
|
|
374
|
-
export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, markCourseStarted, markCourseStartedEmittedToTracking, migrateCourseStartedMark, nowIso, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
|
|
432
|
+
export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
3
3
|
var ID_MAX_LENGTH = 64;
|
|
4
4
|
|
|
5
|
+
// src/assertNever.ts
|
|
6
|
+
function assertNever(value, message = "Unexpected value") {
|
|
7
|
+
throw new Error(`${message}: ${String(value)}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
// src/validateId.ts
|
|
6
11
|
function validateId(input, path = "id") {
|
|
7
12
|
if (typeof input !== "string") {
|
|
@@ -30,6 +35,22 @@ function validateId(input, path = "id") {
|
|
|
30
35
|
}
|
|
31
36
|
return { ok: true, id };
|
|
32
37
|
}
|
|
38
|
+
function parseCourseId(input) {
|
|
39
|
+
const result = validateId(input, "courseId");
|
|
40
|
+
return result.ok ? result.id : null;
|
|
41
|
+
}
|
|
42
|
+
function parseLessonId(input) {
|
|
43
|
+
const result = validateId(input, "lessonId");
|
|
44
|
+
return result.ok ? result.id : null;
|
|
45
|
+
}
|
|
46
|
+
function parseCheckId(input) {
|
|
47
|
+
const result = validateId(input, "checkId");
|
|
48
|
+
return result.ok ? result.id : null;
|
|
49
|
+
}
|
|
50
|
+
function parseBlockId(input) {
|
|
51
|
+
const result = validateId(input, "blockId");
|
|
52
|
+
return result.ok ? result.id : null;
|
|
53
|
+
}
|
|
33
54
|
function assertValidId(input, path = "id") {
|
|
34
55
|
const result = validateId(input, path);
|
|
35
56
|
if (!result.ok) {
|
|
@@ -303,6 +324,11 @@ function isDevEnvironment2() {
|
|
|
303
324
|
function resetTelemetryBuilderWarningsForTests() {
|
|
304
325
|
warnedMissingQuizLesson = false;
|
|
305
326
|
}
|
|
327
|
+
function resolveLessonId(opts, eventName) {
|
|
328
|
+
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
329
|
+
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
330
|
+
return lessonId;
|
|
331
|
+
}
|
|
306
332
|
function buildTelemetryEvent(opts) {
|
|
307
333
|
const base = {
|
|
308
334
|
timestamp: opts.timestamp ?? nowIso(),
|
|
@@ -317,39 +343,33 @@ function buildTelemetryEvent(opts) {
|
|
|
317
343
|
case "course_completed":
|
|
318
344
|
return { name: "course_completed", ...base };
|
|
319
345
|
case "lesson_started": {
|
|
320
|
-
const
|
|
321
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
322
|
-
if (!lessonId) throw new Error("lesson_started requires lessonId");
|
|
346
|
+
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
323
347
|
return {
|
|
324
348
|
name: "lesson_started",
|
|
325
349
|
...base,
|
|
326
350
|
lessonId,
|
|
327
|
-
data: { ...data, lessonId }
|
|
351
|
+
data: { ...opts.data, lessonId }
|
|
328
352
|
};
|
|
329
353
|
}
|
|
330
354
|
case "lesson_completed":
|
|
331
355
|
case "lesson_time_on_task": {
|
|
332
|
-
const
|
|
333
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
334
|
-
if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
|
|
356
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
335
357
|
return {
|
|
336
358
|
name: opts.name,
|
|
337
359
|
...base,
|
|
338
360
|
lessonId,
|
|
339
|
-
data: { ...data, lessonId }
|
|
361
|
+
data: { ...opts.data, lessonId }
|
|
340
362
|
};
|
|
341
363
|
}
|
|
342
364
|
case "quiz_answered": {
|
|
343
|
-
const data = opts.data;
|
|
344
365
|
const lessonId = opts.lessonId;
|
|
345
366
|
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
346
|
-
return { name: "quiz_answered", ...base, lessonId, data };
|
|
367
|
+
return { name: "quiz_answered", ...base, lessonId, data: opts.data };
|
|
347
368
|
}
|
|
348
369
|
case "quiz_completed": {
|
|
349
|
-
const data = opts.data;
|
|
350
370
|
const lessonId = opts.lessonId;
|
|
351
371
|
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
352
|
-
return { name: "quiz_completed", ...base, lessonId, data };
|
|
372
|
+
return { name: "quiz_completed", ...base, lessonId, data: opts.data };
|
|
353
373
|
}
|
|
354
374
|
case "interaction":
|
|
355
375
|
return {
|
|
@@ -359,7 +379,7 @@ function buildTelemetryEvent(opts) {
|
|
|
359
379
|
data: opts.data
|
|
360
380
|
};
|
|
361
381
|
default:
|
|
362
|
-
return
|
|
382
|
+
return assertNever(opts);
|
|
363
383
|
}
|
|
364
384
|
}
|
|
365
385
|
function tryBuildTelemetryEvent(opts) {
|
|
@@ -556,6 +576,7 @@ function getTabSessionId(storage) {
|
|
|
556
576
|
}
|
|
557
577
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
558
578
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
579
|
+
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
559
580
|
function resolveSessionId(storage, provided) {
|
|
560
581
|
if (provided) return provided;
|
|
561
582
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
@@ -570,6 +591,9 @@ function courseStartedStorageKey(sessionId, courseId) {
|
|
|
570
591
|
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
571
592
|
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
572
593
|
}
|
|
594
|
+
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
595
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
596
|
+
}
|
|
573
597
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
574
598
|
if (!courseId) return false;
|
|
575
599
|
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
@@ -586,6 +610,14 @@ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
|
586
610
|
if (!courseId) return;
|
|
587
611
|
storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
588
612
|
}
|
|
613
|
+
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
614
|
+
if (!courseId) return false;
|
|
615
|
+
return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
|
|
616
|
+
}
|
|
617
|
+
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
618
|
+
if (!courseId) return;
|
|
619
|
+
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
620
|
+
}
|
|
589
621
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
590
622
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
591
623
|
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
@@ -596,6 +628,10 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
596
628
|
markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
|
|
597
629
|
storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
|
|
598
630
|
}
|
|
631
|
+
if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
|
|
632
|
+
markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
|
|
633
|
+
storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
|
|
634
|
+
}
|
|
599
635
|
}
|
|
600
636
|
|
|
601
637
|
// src/runtime/courseLifecycle.ts
|
|
@@ -844,6 +880,7 @@ export {
|
|
|
844
880
|
ID_PATTERN,
|
|
845
881
|
SESSION_STORAGE_KEY,
|
|
846
882
|
TELEMETRY_EVENT_CATALOG,
|
|
883
|
+
assertNever,
|
|
847
884
|
assertValidId,
|
|
848
885
|
buildCourseStartedTelemetryEvent,
|
|
849
886
|
buildLessonkitUrn,
|
|
@@ -869,10 +906,16 @@ export {
|
|
|
869
906
|
getTabSessionId,
|
|
870
907
|
hasCourseStarted,
|
|
871
908
|
hasCourseStartedEmittedToTracking,
|
|
909
|
+
hasCourseStartedPipelineDelivered,
|
|
872
910
|
markCourseStarted,
|
|
873
911
|
markCourseStartedEmittedToTracking,
|
|
912
|
+
markCourseStartedPipelineDelivered,
|
|
874
913
|
migrateCourseStartedMark,
|
|
875
914
|
nowIso,
|
|
915
|
+
parseBlockId,
|
|
916
|
+
parseCheckId,
|
|
917
|
+
parseCourseId,
|
|
918
|
+
parseLessonId,
|
|
876
919
|
resetStoragePortForTests,
|
|
877
920
|
resetTelemetryBuilderWarningsForTests,
|
|
878
921
|
resolveSessionId,
|