@lessonkit/core 1.0.0 → 1.0.2
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 +100 -30
- package/dist/index.d.cts +73 -15
- package/dist/index.d.ts +73 -15
- package/dist/index.js +93 -30
- package/package.json +3 -2
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) {
|
|
@@ -251,6 +279,11 @@ function invokeTrackingSink(sink, event) {
|
|
|
251
279
|
function createTrackingClient(opts) {
|
|
252
280
|
const sink = opts?.sink;
|
|
253
281
|
const batchSink = opts?.batchSink;
|
|
282
|
+
if (batchSink != null && opts?.batch?.enabled === false) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
"[lessonkit] tracking.batchSink cannot be used with batch.enabled: false; omit batch.enabled or set it to true"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
254
287
|
const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
|
|
255
288
|
const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
|
|
256
289
|
const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
|
|
@@ -322,7 +355,7 @@ function createTrackingClient(opts) {
|
|
|
322
355
|
if (disposed || disposing) return;
|
|
323
356
|
if (buffer.length >= maxBufferSize) {
|
|
324
357
|
buffer.shift();
|
|
325
|
-
if (!warnedBufferCap &&
|
|
358
|
+
if (!warnedBufferCap && isDevEnvironment()) {
|
|
326
359
|
warnedBufferCap = true;
|
|
327
360
|
console.warn(
|
|
328
361
|
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
|
|
@@ -369,6 +402,11 @@ function isDevEnvironment2() {
|
|
|
369
402
|
function resetTelemetryBuilderWarningsForTests() {
|
|
370
403
|
warnedMissingQuizLesson = false;
|
|
371
404
|
}
|
|
405
|
+
function resolveLessonId(opts, eventName) {
|
|
406
|
+
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
407
|
+
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
408
|
+
return lessonId;
|
|
409
|
+
}
|
|
372
410
|
function buildTelemetryEvent(opts) {
|
|
373
411
|
const base = {
|
|
374
412
|
timestamp: opts.timestamp ?? nowIso(),
|
|
@@ -383,39 +421,33 @@ function buildTelemetryEvent(opts) {
|
|
|
383
421
|
case "course_completed":
|
|
384
422
|
return { name: "course_completed", ...base };
|
|
385
423
|
case "lesson_started": {
|
|
386
|
-
const
|
|
387
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
388
|
-
if (!lessonId) throw new Error("lesson_started requires lessonId");
|
|
424
|
+
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
389
425
|
return {
|
|
390
426
|
name: "lesson_started",
|
|
391
427
|
...base,
|
|
392
428
|
lessonId,
|
|
393
|
-
data: { ...data, lessonId }
|
|
429
|
+
data: { ...opts.data, lessonId }
|
|
394
430
|
};
|
|
395
431
|
}
|
|
396
432
|
case "lesson_completed":
|
|
397
433
|
case "lesson_time_on_task": {
|
|
398
|
-
const
|
|
399
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
400
|
-
if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
|
|
434
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
401
435
|
return {
|
|
402
436
|
name: opts.name,
|
|
403
437
|
...base,
|
|
404
438
|
lessonId,
|
|
405
|
-
data: { ...data, lessonId }
|
|
439
|
+
data: { ...opts.data, lessonId }
|
|
406
440
|
};
|
|
407
441
|
}
|
|
408
442
|
case "quiz_answered": {
|
|
409
|
-
const data = opts.data;
|
|
410
443
|
const lessonId = opts.lessonId;
|
|
411
444
|
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
412
|
-
return { name: "quiz_answered", ...base, lessonId, data };
|
|
445
|
+
return { name: "quiz_answered", ...base, lessonId, data: opts.data };
|
|
413
446
|
}
|
|
414
447
|
case "quiz_completed": {
|
|
415
|
-
const data = opts.data;
|
|
416
448
|
const lessonId = opts.lessonId;
|
|
417
449
|
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
418
|
-
return { name: "quiz_completed", ...base, lessonId, data };
|
|
450
|
+
return { name: "quiz_completed", ...base, lessonId, data: opts.data };
|
|
419
451
|
}
|
|
420
452
|
case "interaction":
|
|
421
453
|
return {
|
|
@@ -425,7 +457,7 @@ function buildTelemetryEvent(opts) {
|
|
|
425
457
|
data: opts.data
|
|
426
458
|
};
|
|
427
459
|
default:
|
|
428
|
-
return
|
|
460
|
+
return assertNever(opts);
|
|
429
461
|
}
|
|
430
462
|
}
|
|
431
463
|
function tryBuildTelemetryEvent(opts) {
|
|
@@ -512,7 +544,8 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
512
544
|
const warnPersistFailure = () => {
|
|
513
545
|
if (warnedPersistFailure) return;
|
|
514
546
|
warnedPersistFailure = true;
|
|
515
|
-
|
|
547
|
+
const g = globalThis;
|
|
548
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
|
|
516
549
|
console.warn(
|
|
517
550
|
"[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
|
|
518
551
|
);
|
|
@@ -553,23 +586,37 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
553
586
|
function resetStoragePortForTests(storage) {
|
|
554
587
|
storage.resetForTests?.();
|
|
555
588
|
}
|
|
589
|
+
function createInMemorySessionStoragePort() {
|
|
590
|
+
const memory = /* @__PURE__ */ new Map();
|
|
591
|
+
return {
|
|
592
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
593
|
+
setItem: (key, value) => {
|
|
594
|
+
memory.set(key, value);
|
|
595
|
+
},
|
|
596
|
+
removeItem: (key) => {
|
|
597
|
+
memory.delete(key);
|
|
598
|
+
},
|
|
599
|
+
resetForTests: () => {
|
|
600
|
+
memory.clear();
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function resolveBrowserSessionStorage() {
|
|
605
|
+
try {
|
|
606
|
+
if (typeof sessionStorage === "undefined" || sessionStorage == null) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
return sessionStorage;
|
|
610
|
+
} catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
556
614
|
function createSessionStoragePort() {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return
|
|
560
|
-
getItem: (key) => memory.get(key) ?? null,
|
|
561
|
-
setItem: (key, value) => {
|
|
562
|
-
memory.set(key, value);
|
|
563
|
-
},
|
|
564
|
-
removeItem: (key) => {
|
|
565
|
-
memory.delete(key);
|
|
566
|
-
},
|
|
567
|
-
resetForTests: () => {
|
|
568
|
-
memory.clear();
|
|
569
|
-
}
|
|
570
|
-
};
|
|
615
|
+
const session = resolveBrowserSessionStorage();
|
|
616
|
+
if (!session) {
|
|
617
|
+
return createInMemorySessionStoragePort();
|
|
571
618
|
}
|
|
572
|
-
return createMemoryBackedSessionStorage(
|
|
619
|
+
return createMemoryBackedSessionStorage(session);
|
|
573
620
|
}
|
|
574
621
|
function createGlobalTimer() {
|
|
575
622
|
return {
|
|
@@ -622,6 +669,7 @@ function getTabSessionId(storage) {
|
|
|
622
669
|
}
|
|
623
670
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
624
671
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
672
|
+
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
625
673
|
function resolveSessionId(storage, provided) {
|
|
626
674
|
if (provided) return provided;
|
|
627
675
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
@@ -636,6 +684,9 @@ function courseStartedStorageKey(sessionId, courseId) {
|
|
|
636
684
|
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
637
685
|
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
638
686
|
}
|
|
687
|
+
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
688
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
689
|
+
}
|
|
639
690
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
640
691
|
if (!courseId) return false;
|
|
641
692
|
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
@@ -652,6 +703,14 @@ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
|
652
703
|
if (!courseId) return;
|
|
653
704
|
storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
654
705
|
}
|
|
706
|
+
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
707
|
+
if (!courseId) return false;
|
|
708
|
+
return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
|
|
709
|
+
}
|
|
710
|
+
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
711
|
+
if (!courseId) return;
|
|
712
|
+
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
713
|
+
}
|
|
655
714
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
656
715
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
657
716
|
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
@@ -662,6 +721,10 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
662
721
|
markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
|
|
663
722
|
storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
|
|
664
723
|
}
|
|
724
|
+
if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
|
|
725
|
+
markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
|
|
726
|
+
storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
|
|
727
|
+
}
|
|
665
728
|
}
|
|
666
729
|
|
|
667
730
|
// src/runtime/courseLifecycle.ts
|
|
@@ -911,6 +974,7 @@ function defineLifecyclePlugin(plugin) {
|
|
|
911
974
|
ID_PATTERN,
|
|
912
975
|
SESSION_STORAGE_KEY,
|
|
913
976
|
TELEMETRY_EVENT_CATALOG,
|
|
977
|
+
assertNever,
|
|
914
978
|
assertValidId,
|
|
915
979
|
buildCourseStartedTelemetryEvent,
|
|
916
980
|
buildLessonkitUrn,
|
|
@@ -936,10 +1000,16 @@ function defineLifecyclePlugin(plugin) {
|
|
|
936
1000
|
getTabSessionId,
|
|
937
1001
|
hasCourseStarted,
|
|
938
1002
|
hasCourseStartedEmittedToTracking,
|
|
1003
|
+
hasCourseStartedPipelineDelivered,
|
|
939
1004
|
markCourseStarted,
|
|
940
1005
|
markCourseStartedEmittedToTracking,
|
|
1006
|
+
markCourseStartedPipelineDelivered,
|
|
941
1007
|
migrateCourseStartedMark,
|
|
942
1008
|
nowIso,
|
|
1009
|
+
parseBlockId,
|
|
1010
|
+
parseCheckId,
|
|
1011
|
+
parseCourseId,
|
|
1012
|
+
parseLessonId,
|
|
943
1013
|
resetStoragePortForTests,
|
|
944
1014
|
resetTelemetryBuilderWarningsForTests,
|
|
945
1015
|
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) {
|
|
@@ -185,6 +206,11 @@ function invokeTrackingSink(sink, event) {
|
|
|
185
206
|
function createTrackingClient(opts) {
|
|
186
207
|
const sink = opts?.sink;
|
|
187
208
|
const batchSink = opts?.batchSink;
|
|
209
|
+
if (batchSink != null && opts?.batch?.enabled === false) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"[lessonkit] tracking.batchSink cannot be used with batch.enabled: false; omit batch.enabled or set it to true"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
188
214
|
const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
|
|
189
215
|
const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
|
|
190
216
|
const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
|
|
@@ -256,7 +282,7 @@ function createTrackingClient(opts) {
|
|
|
256
282
|
if (disposed || disposing) return;
|
|
257
283
|
if (buffer.length >= maxBufferSize) {
|
|
258
284
|
buffer.shift();
|
|
259
|
-
if (!warnedBufferCap &&
|
|
285
|
+
if (!warnedBufferCap && isDevEnvironment()) {
|
|
260
286
|
warnedBufferCap = true;
|
|
261
287
|
console.warn(
|
|
262
288
|
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
|
|
@@ -303,6 +329,11 @@ function isDevEnvironment2() {
|
|
|
303
329
|
function resetTelemetryBuilderWarningsForTests() {
|
|
304
330
|
warnedMissingQuizLesson = false;
|
|
305
331
|
}
|
|
332
|
+
function resolveLessonId(opts, eventName) {
|
|
333
|
+
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
334
|
+
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
335
|
+
return lessonId;
|
|
336
|
+
}
|
|
306
337
|
function buildTelemetryEvent(opts) {
|
|
307
338
|
const base = {
|
|
308
339
|
timestamp: opts.timestamp ?? nowIso(),
|
|
@@ -317,39 +348,33 @@ function buildTelemetryEvent(opts) {
|
|
|
317
348
|
case "course_completed":
|
|
318
349
|
return { name: "course_completed", ...base };
|
|
319
350
|
case "lesson_started": {
|
|
320
|
-
const
|
|
321
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
322
|
-
if (!lessonId) throw new Error("lesson_started requires lessonId");
|
|
351
|
+
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
323
352
|
return {
|
|
324
353
|
name: "lesson_started",
|
|
325
354
|
...base,
|
|
326
355
|
lessonId,
|
|
327
|
-
data: { ...data, lessonId }
|
|
356
|
+
data: { ...opts.data, lessonId }
|
|
328
357
|
};
|
|
329
358
|
}
|
|
330
359
|
case "lesson_completed":
|
|
331
360
|
case "lesson_time_on_task": {
|
|
332
|
-
const
|
|
333
|
-
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
334
|
-
if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
|
|
361
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
335
362
|
return {
|
|
336
363
|
name: opts.name,
|
|
337
364
|
...base,
|
|
338
365
|
lessonId,
|
|
339
|
-
data: { ...data, lessonId }
|
|
366
|
+
data: { ...opts.data, lessonId }
|
|
340
367
|
};
|
|
341
368
|
}
|
|
342
369
|
case "quiz_answered": {
|
|
343
|
-
const data = opts.data;
|
|
344
370
|
const lessonId = opts.lessonId;
|
|
345
371
|
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
346
|
-
return { name: "quiz_answered", ...base, lessonId, data };
|
|
372
|
+
return { name: "quiz_answered", ...base, lessonId, data: opts.data };
|
|
347
373
|
}
|
|
348
374
|
case "quiz_completed": {
|
|
349
|
-
const data = opts.data;
|
|
350
375
|
const lessonId = opts.lessonId;
|
|
351
376
|
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
352
|
-
return { name: "quiz_completed", ...base, lessonId, data };
|
|
377
|
+
return { name: "quiz_completed", ...base, lessonId, data: opts.data };
|
|
353
378
|
}
|
|
354
379
|
case "interaction":
|
|
355
380
|
return {
|
|
@@ -359,7 +384,7 @@ function buildTelemetryEvent(opts) {
|
|
|
359
384
|
data: opts.data
|
|
360
385
|
};
|
|
361
386
|
default:
|
|
362
|
-
return
|
|
387
|
+
return assertNever(opts);
|
|
363
388
|
}
|
|
364
389
|
}
|
|
365
390
|
function tryBuildTelemetryEvent(opts) {
|
|
@@ -446,7 +471,8 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
446
471
|
const warnPersistFailure = () => {
|
|
447
472
|
if (warnedPersistFailure) return;
|
|
448
473
|
warnedPersistFailure = true;
|
|
449
|
-
|
|
474
|
+
const g = globalThis;
|
|
475
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
|
|
450
476
|
console.warn(
|
|
451
477
|
"[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
|
|
452
478
|
);
|
|
@@ -487,23 +513,37 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
487
513
|
function resetStoragePortForTests(storage) {
|
|
488
514
|
storage.resetForTests?.();
|
|
489
515
|
}
|
|
516
|
+
function createInMemorySessionStoragePort() {
|
|
517
|
+
const memory = /* @__PURE__ */ new Map();
|
|
518
|
+
return {
|
|
519
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
520
|
+
setItem: (key, value) => {
|
|
521
|
+
memory.set(key, value);
|
|
522
|
+
},
|
|
523
|
+
removeItem: (key) => {
|
|
524
|
+
memory.delete(key);
|
|
525
|
+
},
|
|
526
|
+
resetForTests: () => {
|
|
527
|
+
memory.clear();
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function resolveBrowserSessionStorage() {
|
|
532
|
+
try {
|
|
533
|
+
if (typeof sessionStorage === "undefined" || sessionStorage == null) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
return sessionStorage;
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
490
541
|
function createSessionStoragePort() {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return
|
|
494
|
-
getItem: (key) => memory.get(key) ?? null,
|
|
495
|
-
setItem: (key, value) => {
|
|
496
|
-
memory.set(key, value);
|
|
497
|
-
},
|
|
498
|
-
removeItem: (key) => {
|
|
499
|
-
memory.delete(key);
|
|
500
|
-
},
|
|
501
|
-
resetForTests: () => {
|
|
502
|
-
memory.clear();
|
|
503
|
-
}
|
|
504
|
-
};
|
|
542
|
+
const session = resolveBrowserSessionStorage();
|
|
543
|
+
if (!session) {
|
|
544
|
+
return createInMemorySessionStoragePort();
|
|
505
545
|
}
|
|
506
|
-
return createMemoryBackedSessionStorage(
|
|
546
|
+
return createMemoryBackedSessionStorage(session);
|
|
507
547
|
}
|
|
508
548
|
function createGlobalTimer() {
|
|
509
549
|
return {
|
|
@@ -556,6 +596,7 @@ function getTabSessionId(storage) {
|
|
|
556
596
|
}
|
|
557
597
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
558
598
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
599
|
+
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
559
600
|
function resolveSessionId(storage, provided) {
|
|
560
601
|
if (provided) return provided;
|
|
561
602
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
@@ -570,6 +611,9 @@ function courseStartedStorageKey(sessionId, courseId) {
|
|
|
570
611
|
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
571
612
|
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
572
613
|
}
|
|
614
|
+
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
615
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
616
|
+
}
|
|
573
617
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
574
618
|
if (!courseId) return false;
|
|
575
619
|
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
@@ -586,6 +630,14 @@ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
|
586
630
|
if (!courseId) return;
|
|
587
631
|
storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
588
632
|
}
|
|
633
|
+
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
634
|
+
if (!courseId) return false;
|
|
635
|
+
return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
|
|
636
|
+
}
|
|
637
|
+
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
638
|
+
if (!courseId) return;
|
|
639
|
+
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
640
|
+
}
|
|
589
641
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
590
642
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
591
643
|
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
@@ -596,6 +648,10 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
596
648
|
markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
|
|
597
649
|
storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
|
|
598
650
|
}
|
|
651
|
+
if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
|
|
652
|
+
markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
|
|
653
|
+
storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
|
|
654
|
+
}
|
|
599
655
|
}
|
|
600
656
|
|
|
601
657
|
// src/runtime/courseLifecycle.ts
|
|
@@ -844,6 +900,7 @@ export {
|
|
|
844
900
|
ID_PATTERN,
|
|
845
901
|
SESSION_STORAGE_KEY,
|
|
846
902
|
TELEMETRY_EVENT_CATALOG,
|
|
903
|
+
assertNever,
|
|
847
904
|
assertValidId,
|
|
848
905
|
buildCourseStartedTelemetryEvent,
|
|
849
906
|
buildLessonkitUrn,
|
|
@@ -869,10 +926,16 @@ export {
|
|
|
869
926
|
getTabSessionId,
|
|
870
927
|
hasCourseStarted,
|
|
871
928
|
hasCourseStartedEmittedToTracking,
|
|
929
|
+
hasCourseStartedPipelineDelivered,
|
|
872
930
|
markCourseStarted,
|
|
873
931
|
markCourseStartedEmittedToTracking,
|
|
932
|
+
markCourseStartedPipelineDelivered,
|
|
874
933
|
migrateCourseStartedMark,
|
|
875
934
|
nowIso,
|
|
935
|
+
parseBlockId,
|
|
936
|
+
parseCheckId,
|
|
937
|
+
parseCourseId,
|
|
938
|
+
parseLessonId,
|
|
876
939
|
resetStoragePortForTests,
|
|
877
940
|
resetTelemetryBuilderWarningsForTests,
|
|
878
941
|
resolveSessionId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared types and telemetry primitives for LessonKit.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,8 +48,9 @@
|
|
|
48
48
|
"lint": "echo \"(no lint configured yet)\""
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.13.10",
|
|
51
52
|
"tsup": "^8.5.0",
|
|
52
53
|
"typescript": "^5.8.3",
|
|
53
|
-
"vitest": "^
|
|
54
|
+
"vitest": "^4.1.8"
|
|
54
55
|
}
|
|
55
56
|
}
|