@lessonkit/core 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-PEWFPVQ6.js +628 -0
- package/dist/index.cjs +244 -25
- package/dist/index.d.cts +14 -483
- package/dist/index.d.ts +14 -483
- package/dist/index.js +196 -545
- package/dist/testing-BhVGckZ5.d.cts +569 -0
- package/dist/testing-BhVGckZ5.d.ts +569 -0
- package/dist/testing.cjs +60 -0
- package/dist/testing.d.cts +1 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +12 -0
- package/package.json +9 -4
- package/telemetry-catalog.v3.json +170 -14
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// src/ids.ts
|
|
2
|
+
function createSessionId() {
|
|
3
|
+
const g = globalThis;
|
|
4
|
+
if (g.crypto?.randomUUID) return g.crypto.randomUUID();
|
|
5
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/time.ts
|
|
9
|
+
function nowIso() {
|
|
10
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/internal/env.ts
|
|
14
|
+
function isDevEnvironment() {
|
|
15
|
+
const g = globalThis;
|
|
16
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
17
|
+
}
|
|
18
|
+
function warnDev(message, err) {
|
|
19
|
+
if (!isDevEnvironment()) return;
|
|
20
|
+
console.warn(message, err instanceof Error ? err.message : err);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/telemetry/eventRegistry.ts
|
|
24
|
+
function resolveLessonId(opts, eventName) {
|
|
25
|
+
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
26
|
+
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
27
|
+
return lessonId;
|
|
28
|
+
}
|
|
29
|
+
function withLessonScopedData(name, base, lessonId, data) {
|
|
30
|
+
return { name, ...base, lessonId, data: { ...data, lessonId } };
|
|
31
|
+
}
|
|
32
|
+
var TELEMETRY_EVENT_REGISTRY = {
|
|
33
|
+
course_started: {
|
|
34
|
+
build: (_opts, base) => ({ name: "course_started", ...base })
|
|
35
|
+
},
|
|
36
|
+
course_completed: {
|
|
37
|
+
build: (_opts, base) => ({ name: "course_completed", ...base })
|
|
38
|
+
},
|
|
39
|
+
lesson_started: {
|
|
40
|
+
requiresLessonId: true,
|
|
41
|
+
build: (opts, base) => {
|
|
42
|
+
if (opts.name !== "lesson_started") throw new Error("unexpected event");
|
|
43
|
+
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
44
|
+
return withLessonScopedData("lesson_started", base, lessonId, opts.data);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
lesson_completed: {
|
|
48
|
+
requiresLessonId: true,
|
|
49
|
+
build: (opts, base) => {
|
|
50
|
+
if (opts.name !== "lesson_completed") throw new Error("unexpected event");
|
|
51
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
52
|
+
return withLessonScopedData(opts.name, base, lessonId, opts.data);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
lesson_time_on_task: {
|
|
56
|
+
requiresLessonId: true,
|
|
57
|
+
build: (opts, base) => {
|
|
58
|
+
if (opts.name !== "lesson_time_on_task") throw new Error("unexpected event");
|
|
59
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
60
|
+
return withLessonScopedData(opts.name, base, lessonId, opts.data);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
quiz_answered: {
|
|
64
|
+
requiresLessonId: true,
|
|
65
|
+
tryBuildMissingLessonWarning: "quiz",
|
|
66
|
+
build: (opts, base) => {
|
|
67
|
+
if (opts.name !== "quiz_answered") throw new Error("unexpected event");
|
|
68
|
+
const lessonId = opts.lessonId;
|
|
69
|
+
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
70
|
+
return {
|
|
71
|
+
name: "quiz_answered",
|
|
72
|
+
...base,
|
|
73
|
+
lessonId,
|
|
74
|
+
data: opts.data
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
quiz_completed: {
|
|
79
|
+
requiresLessonId: true,
|
|
80
|
+
tryBuildMissingLessonWarning: "quiz",
|
|
81
|
+
build: (opts, base) => {
|
|
82
|
+
if (opts.name !== "quiz_completed") throw new Error("unexpected event");
|
|
83
|
+
const lessonId = opts.lessonId;
|
|
84
|
+
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
85
|
+
return {
|
|
86
|
+
name: "quiz_completed",
|
|
87
|
+
...base,
|
|
88
|
+
lessonId,
|
|
89
|
+
data: opts.data
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
assessment_answered: {
|
|
94
|
+
requiresLessonId: true,
|
|
95
|
+
tryBuildMissingLessonWarning: "assessment",
|
|
96
|
+
build: (opts, base) => {
|
|
97
|
+
if (opts.name !== "assessment_answered") throw new Error("unexpected event");
|
|
98
|
+
const lessonId = opts.lessonId;
|
|
99
|
+
if (!lessonId) throw new Error("assessment_answered requires active lessonId");
|
|
100
|
+
return {
|
|
101
|
+
name: "assessment_answered",
|
|
102
|
+
...base,
|
|
103
|
+
lessonId,
|
|
104
|
+
data: opts.data
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
assessment_completed: {
|
|
109
|
+
requiresLessonId: true,
|
|
110
|
+
tryBuildMissingLessonWarning: "assessment",
|
|
111
|
+
build: (opts, base) => {
|
|
112
|
+
if (opts.name !== "assessment_completed") throw new Error("unexpected event");
|
|
113
|
+
const lessonId = opts.lessonId;
|
|
114
|
+
if (!lessonId) throw new Error("assessment_completed requires active lessonId");
|
|
115
|
+
return {
|
|
116
|
+
name: "assessment_completed",
|
|
117
|
+
...base,
|
|
118
|
+
lessonId,
|
|
119
|
+
data: opts.data
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
interaction: {
|
|
124
|
+
build: (opts, base) => {
|
|
125
|
+
if (opts.name !== "interaction") throw new Error("unexpected event");
|
|
126
|
+
return {
|
|
127
|
+
name: "interaction",
|
|
128
|
+
...base,
|
|
129
|
+
lessonId: opts.lessonId,
|
|
130
|
+
data: opts.data
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
book_page_viewed: {
|
|
135
|
+
requiresLessonId: true,
|
|
136
|
+
build: (opts, base) => {
|
|
137
|
+
if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
|
|
138
|
+
const lessonId = opts.lessonId;
|
|
139
|
+
if (!lessonId) throw new Error("book_page_viewed requires active lessonId");
|
|
140
|
+
return {
|
|
141
|
+
name: "book_page_viewed",
|
|
142
|
+
...base,
|
|
143
|
+
lessonId,
|
|
144
|
+
data: opts.data
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
slide_viewed: {
|
|
149
|
+
requiresLessonId: true,
|
|
150
|
+
build: (opts, base) => {
|
|
151
|
+
if (opts.name !== "slide_viewed") throw new Error("unexpected event");
|
|
152
|
+
const lessonId = opts.lessonId;
|
|
153
|
+
if (!lessonId) throw new Error("slide_viewed requires active lessonId");
|
|
154
|
+
return {
|
|
155
|
+
name: "slide_viewed",
|
|
156
|
+
...base,
|
|
157
|
+
lessonId,
|
|
158
|
+
data: opts.data
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
compound_page_viewed: {
|
|
163
|
+
requiresLessonId: true,
|
|
164
|
+
build: (opts, base) => {
|
|
165
|
+
if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
|
|
166
|
+
const lessonId = opts.lessonId;
|
|
167
|
+
if (!lessonId) throw new Error("compound_page_viewed requires active lessonId");
|
|
168
|
+
return {
|
|
169
|
+
name: "compound_page_viewed",
|
|
170
|
+
...base,
|
|
171
|
+
lessonId,
|
|
172
|
+
data: opts.data
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
hotspot_opened: {
|
|
177
|
+
build: (opts, base) => {
|
|
178
|
+
if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
|
|
179
|
+
return {
|
|
180
|
+
name: "hotspot_opened",
|
|
181
|
+
...base,
|
|
182
|
+
lessonId: opts.lessonId,
|
|
183
|
+
data: opts.data
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
accordion_section_toggled: {
|
|
188
|
+
build: (opts, base) => {
|
|
189
|
+
if (opts.name !== "accordion_section_toggled") throw new Error("unexpected event");
|
|
190
|
+
return {
|
|
191
|
+
name: "accordion_section_toggled",
|
|
192
|
+
...base,
|
|
193
|
+
lessonId: opts.lessonId,
|
|
194
|
+
data: opts.data
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
flashcard_flipped: {
|
|
199
|
+
build: (opts, base) => {
|
|
200
|
+
if (opts.name !== "flashcard_flipped") throw new Error("unexpected event");
|
|
201
|
+
return {
|
|
202
|
+
name: "flashcard_flipped",
|
|
203
|
+
...base,
|
|
204
|
+
lessonId: opts.lessonId,
|
|
205
|
+
data: opts.data
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
image_slider_changed: {
|
|
210
|
+
build: (opts, base) => {
|
|
211
|
+
if (opts.name !== "image_slider_changed") throw new Error("unexpected event");
|
|
212
|
+
return {
|
|
213
|
+
name: "image_slider_changed",
|
|
214
|
+
...base,
|
|
215
|
+
lessonId: opts.lessonId,
|
|
216
|
+
data: opts.data
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
video_cue_reached: {
|
|
221
|
+
requiresLessonId: true,
|
|
222
|
+
build: (opts, base) => {
|
|
223
|
+
if (opts.name !== "video_cue_reached") throw new Error("unexpected event");
|
|
224
|
+
const lessonId = opts.lessonId;
|
|
225
|
+
if (!lessonId) throw new Error("video_cue_reached requires active lessonId");
|
|
226
|
+
return {
|
|
227
|
+
name: "video_cue_reached",
|
|
228
|
+
...base,
|
|
229
|
+
lessonId,
|
|
230
|
+
data: opts.data
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
video_segment_completed: {
|
|
235
|
+
requiresLessonId: true,
|
|
236
|
+
build: (opts, base) => {
|
|
237
|
+
if (opts.name !== "video_segment_completed") throw new Error("unexpected event");
|
|
238
|
+
const lessonId = opts.lessonId;
|
|
239
|
+
if (!lessonId) throw new Error("video_segment_completed requires active lessonId");
|
|
240
|
+
return {
|
|
241
|
+
name: "video_segment_completed",
|
|
242
|
+
...base,
|
|
243
|
+
lessonId,
|
|
244
|
+
data: opts.data
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
memory_card_flipped: {
|
|
249
|
+
build: (opts, base) => {
|
|
250
|
+
if (opts.name !== "memory_card_flipped") throw new Error("unexpected event");
|
|
251
|
+
return {
|
|
252
|
+
name: "memory_card_flipped",
|
|
253
|
+
...base,
|
|
254
|
+
lessonId: opts.lessonId,
|
|
255
|
+
data: opts.data
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
information_wall_search: {
|
|
260
|
+
build: (opts, base) => {
|
|
261
|
+
if (opts.name !== "information_wall_search") throw new Error("unexpected event");
|
|
262
|
+
return {
|
|
263
|
+
name: "information_wall_search",
|
|
264
|
+
...base,
|
|
265
|
+
lessonId: opts.lessonId,
|
|
266
|
+
data: opts.data
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
parallax_slide_viewed: {
|
|
271
|
+
build: (opts, base) => {
|
|
272
|
+
if (opts.name !== "parallax_slide_viewed") throw new Error("unexpected event");
|
|
273
|
+
return {
|
|
274
|
+
name: "parallax_slide_viewed",
|
|
275
|
+
...base,
|
|
276
|
+
lessonId: opts.lessonId,
|
|
277
|
+
data: opts.data
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
questionnaire_submitted: {
|
|
282
|
+
requiresLessonId: true,
|
|
283
|
+
build: (opts, base) => {
|
|
284
|
+
if (opts.name !== "questionnaire_submitted") throw new Error("unexpected event");
|
|
285
|
+
const lessonId = opts.lessonId;
|
|
286
|
+
if (!lessonId) throw new Error("questionnaire_submitted requires active lessonId");
|
|
287
|
+
return {
|
|
288
|
+
name: "questionnaire_submitted",
|
|
289
|
+
...base,
|
|
290
|
+
lessonId,
|
|
291
|
+
data: opts.data
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
function buildTelemetryEventFromRegistry(opts) {
|
|
297
|
+
const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
|
|
298
|
+
if (!entry) {
|
|
299
|
+
throw new Error("Unexpected value");
|
|
300
|
+
}
|
|
301
|
+
const base = {
|
|
302
|
+
timestamp: opts.timestamp ?? nowIso(),
|
|
303
|
+
courseId: opts.courseId,
|
|
304
|
+
sessionId: opts.sessionId,
|
|
305
|
+
attemptId: opts.attemptId,
|
|
306
|
+
user: opts.user
|
|
307
|
+
};
|
|
308
|
+
return entry.build(opts, base);
|
|
309
|
+
}
|
|
310
|
+
function getTelemetryEventRegistryEntry(name) {
|
|
311
|
+
return TELEMETRY_EVENT_REGISTRY[name];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/telemetryBuilder.ts
|
|
315
|
+
var warnedMissingQuizLesson = false;
|
|
316
|
+
var warnedMissingAssessmentLesson = false;
|
|
317
|
+
function resetTelemetryBuilderWarningsForTests() {
|
|
318
|
+
warnedMissingQuizLesson = false;
|
|
319
|
+
warnedMissingAssessmentLesson = false;
|
|
320
|
+
}
|
|
321
|
+
function buildTelemetryEvent(opts) {
|
|
322
|
+
return buildTelemetryEventFromRegistry(opts);
|
|
323
|
+
}
|
|
324
|
+
function tryBuildTelemetryEvent(opts) {
|
|
325
|
+
const entry = getTelemetryEventRegistryEntry(opts.name);
|
|
326
|
+
if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
|
|
327
|
+
if (isDevEnvironment()) {
|
|
328
|
+
if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
|
|
329
|
+
warnedMissingQuizLesson = true;
|
|
330
|
+
console.warn(
|
|
331
|
+
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
|
|
335
|
+
warnedMissingAssessmentLesson = true;
|
|
336
|
+
console.warn(
|
|
337
|
+
`[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return buildTelemetryEvent(opts);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/ports.ts
|
|
347
|
+
function createDefaultClock() {
|
|
348
|
+
return {
|
|
349
|
+
nowMs: () => Date.now(),
|
|
350
|
+
nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function createNoopStorage() {
|
|
354
|
+
return {
|
|
355
|
+
getItem: () => null,
|
|
356
|
+
setItem: () => true
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function createMemoryBackedSessionStorage(session) {
|
|
360
|
+
const memory = /* @__PURE__ */ new Map();
|
|
361
|
+
let warnedPersistFailure = false;
|
|
362
|
+
const warnPersistFailure = () => {
|
|
363
|
+
if (warnedPersistFailure) return;
|
|
364
|
+
warnedPersistFailure = true;
|
|
365
|
+
const g = globalThis;
|
|
366
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
|
|
367
|
+
console.warn(
|
|
368
|
+
"[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
getItem: (key) => {
|
|
374
|
+
if (memory.has(key)) return memory.get(key);
|
|
375
|
+
try {
|
|
376
|
+
const value = session.getItem(key);
|
|
377
|
+
if (value !== null) memory.set(key, value);
|
|
378
|
+
return value;
|
|
379
|
+
} catch {
|
|
380
|
+
return memory.get(key) ?? null;
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
setItem: (key, value) => {
|
|
384
|
+
memory.set(key, value);
|
|
385
|
+
try {
|
|
386
|
+
session.setItem(key, value);
|
|
387
|
+
return true;
|
|
388
|
+
} catch {
|
|
389
|
+
warnPersistFailure();
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
removeItem: (key) => {
|
|
394
|
+
memory.delete(key);
|
|
395
|
+
try {
|
|
396
|
+
session.removeItem(key);
|
|
397
|
+
} catch {
|
|
398
|
+
warnPersistFailure();
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
resetForTests: () => {
|
|
402
|
+
memory.clear();
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function resetStoragePortForTests(storage) {
|
|
407
|
+
storage.resetForTests?.();
|
|
408
|
+
}
|
|
409
|
+
function createInMemorySessionStoragePort() {
|
|
410
|
+
const memory = /* @__PURE__ */ new Map();
|
|
411
|
+
return {
|
|
412
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
413
|
+
setItem: (key, value) => {
|
|
414
|
+
memory.set(key, value);
|
|
415
|
+
return true;
|
|
416
|
+
},
|
|
417
|
+
removeItem: (key) => {
|
|
418
|
+
memory.delete(key);
|
|
419
|
+
},
|
|
420
|
+
resetForTests: () => {
|
|
421
|
+
memory.clear();
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function resolveBrowserSessionStorage() {
|
|
426
|
+
try {
|
|
427
|
+
if (typeof sessionStorage === "undefined" || sessionStorage == null) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
return sessionStorage;
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function createSessionStoragePort() {
|
|
436
|
+
const session = resolveBrowserSessionStorage();
|
|
437
|
+
if (!session) {
|
|
438
|
+
return createInMemorySessionStoragePort();
|
|
439
|
+
}
|
|
440
|
+
return createMemoryBackedSessionStorage(session);
|
|
441
|
+
}
|
|
442
|
+
function createGlobalTimer() {
|
|
443
|
+
return {
|
|
444
|
+
setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
|
|
445
|
+
clearInterval: (id) => globalThis.clearInterval(id)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/session.ts
|
|
450
|
+
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
451
|
+
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
452
|
+
var sharedVolatileSessionId = null;
|
|
453
|
+
function isDevEnvironment2() {
|
|
454
|
+
const g = globalThis;
|
|
455
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
456
|
+
}
|
|
457
|
+
function getTabSessionId(storage) {
|
|
458
|
+
return storage.getItem(SESSION_STORAGE_KEY);
|
|
459
|
+
}
|
|
460
|
+
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
461
|
+
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
462
|
+
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
463
|
+
function resolveSessionId(storage, provided) {
|
|
464
|
+
if (provided !== void 0) {
|
|
465
|
+
const trimmed = provided.trim();
|
|
466
|
+
if (trimmed.length > 0) return trimmed;
|
|
467
|
+
}
|
|
468
|
+
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
469
|
+
if (existing) return existing;
|
|
470
|
+
const volatile = volatileSessionIds.get(storage);
|
|
471
|
+
if (volatile) return volatile;
|
|
472
|
+
const id = createSessionId();
|
|
473
|
+
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
474
|
+
if (!persisted) {
|
|
475
|
+
if (!sharedVolatileSessionId) {
|
|
476
|
+
sharedVolatileSessionId = id;
|
|
477
|
+
}
|
|
478
|
+
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
479
|
+
if (isDevEnvironment2()) {
|
|
480
|
+
console.warn(
|
|
481
|
+
"[lessonkit] session id could not be persisted; reusing in-memory id for this tab."
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return sharedVolatileSessionId;
|
|
485
|
+
}
|
|
486
|
+
return id;
|
|
487
|
+
}
|
|
488
|
+
function courseStartedStorageKey(sessionId, courseId) {
|
|
489
|
+
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
490
|
+
}
|
|
491
|
+
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
492
|
+
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
493
|
+
}
|
|
494
|
+
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
495
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
496
|
+
}
|
|
497
|
+
function hasCourseStarted(storage, sessionId, courseId) {
|
|
498
|
+
if (!courseId) return false;
|
|
499
|
+
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
500
|
+
}
|
|
501
|
+
function markCourseStarted(storage, sessionId, courseId) {
|
|
502
|
+
if (!courseId) return false;
|
|
503
|
+
return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
504
|
+
}
|
|
505
|
+
function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
506
|
+
if (!courseId) return false;
|
|
507
|
+
return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
|
|
508
|
+
}
|
|
509
|
+
function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
510
|
+
if (!courseId) return false;
|
|
511
|
+
return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
512
|
+
}
|
|
513
|
+
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
514
|
+
if (!courseId) return false;
|
|
515
|
+
return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
|
|
516
|
+
}
|
|
517
|
+
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
518
|
+
if (!courseId) return false;
|
|
519
|
+
return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
520
|
+
}
|
|
521
|
+
function resetSharedVolatileSessionIdForTests() {
|
|
522
|
+
sharedVolatileSessionId = null;
|
|
523
|
+
}
|
|
524
|
+
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
525
|
+
if (!courseId || fromSessionId === toSessionId) return;
|
|
526
|
+
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
527
|
+
markCourseStarted(storage, toSessionId, courseId);
|
|
528
|
+
storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
|
|
529
|
+
}
|
|
530
|
+
if (hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)) {
|
|
531
|
+
markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
|
|
532
|
+
storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
|
|
533
|
+
}
|
|
534
|
+
if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
|
|
535
|
+
markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
|
|
536
|
+
storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/runtime/courseLifecycle.ts
|
|
541
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Set();
|
|
542
|
+
function resetCourseStartedEmitFlightForTests() {
|
|
543
|
+
courseStartedEmitFlights.clear();
|
|
544
|
+
}
|
|
545
|
+
function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
546
|
+
const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
|
|
547
|
+
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
548
|
+
if (alreadyEmittedToSink) {
|
|
549
|
+
return { emitted: true, marked };
|
|
550
|
+
}
|
|
551
|
+
if (courseStartedEmitFlights.has(flightKey)) {
|
|
552
|
+
return { emitted: false, marked };
|
|
553
|
+
}
|
|
554
|
+
courseStartedEmitFlights.add(flightKey);
|
|
555
|
+
try {
|
|
556
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
557
|
+
if (emitted && !marked) {
|
|
558
|
+
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
emitted,
|
|
562
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
563
|
+
};
|
|
564
|
+
} finally {
|
|
565
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function buildCourseStartedTelemetryEvent(ctx) {
|
|
569
|
+
return buildTelemetryEvent({
|
|
570
|
+
name: "course_started",
|
|
571
|
+
courseId: ctx.courseId,
|
|
572
|
+
sessionId: ctx.sessionId,
|
|
573
|
+
attemptId: ctx.attemptId,
|
|
574
|
+
user: ctx.user
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function completeLessonWithTelemetry(opts) {
|
|
578
|
+
const result = opts.progress.completeLesson(opts.lessonId, opts.nowMs);
|
|
579
|
+
if (!result.didComplete) return false;
|
|
580
|
+
opts.emitLessonCompleted(opts.lessonId, result.durationMs);
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
function completeCourseWithTelemetry(opts) {
|
|
584
|
+
const current = opts.progress.getState();
|
|
585
|
+
if (current.activeLessonId) {
|
|
586
|
+
completeLessonWithTelemetry({
|
|
587
|
+
progress: opts.progress,
|
|
588
|
+
lessonId: current.activeLessonId,
|
|
589
|
+
nowMs: opts.nowMs,
|
|
590
|
+
emitLessonCompleted: opts.emitLessonCompleted
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const result = opts.progress.completeCourse();
|
|
594
|
+
if (!result.didComplete) return false;
|
|
595
|
+
opts.emitCourseCompleted();
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export {
|
|
600
|
+
isDevEnvironment,
|
|
601
|
+
warnDev,
|
|
602
|
+
createSessionId,
|
|
603
|
+
nowIso,
|
|
604
|
+
resetTelemetryBuilderWarningsForTests,
|
|
605
|
+
buildTelemetryEvent,
|
|
606
|
+
tryBuildTelemetryEvent,
|
|
607
|
+
createDefaultClock,
|
|
608
|
+
createNoopStorage,
|
|
609
|
+
resetStoragePortForTests,
|
|
610
|
+
createSessionStoragePort,
|
|
611
|
+
createGlobalTimer,
|
|
612
|
+
SESSION_STORAGE_KEY,
|
|
613
|
+
getTabSessionId,
|
|
614
|
+
resolveSessionId,
|
|
615
|
+
hasCourseStarted,
|
|
616
|
+
markCourseStarted,
|
|
617
|
+
hasCourseStartedEmittedToTracking,
|
|
618
|
+
markCourseStartedEmittedToTracking,
|
|
619
|
+
hasCourseStartedPipelineDelivered,
|
|
620
|
+
markCourseStartedPipelineDelivered,
|
|
621
|
+
resetSharedVolatileSessionIdForTests,
|
|
622
|
+
migrateCourseStartedMark,
|
|
623
|
+
resetCourseStartedEmitFlightForTests,
|
|
624
|
+
tryEmitCourseStarted,
|
|
625
|
+
buildCourseStartedTelemetryEvent,
|
|
626
|
+
completeLessonWithTelemetry,
|
|
627
|
+
completeCourseWithTelemetry
|
|
628
|
+
};
|