@lessonkit/core 0.9.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -154,18 +154,48 @@ function buildTelemetryCatalog() {
154
154
  }
155
155
 
156
156
  // src/trackingClient.ts
157
+ function isDevEnvironment() {
158
+ const g = globalThis;
159
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
160
+ }
161
+ function invokeTrackingSink(sink, event) {
162
+ let result;
163
+ try {
164
+ result = sink(event);
165
+ } catch (err) {
166
+ if (isDevEnvironment()) {
167
+ console.warn(
168
+ "[lessonkit] tracking sink failed:",
169
+ err instanceof Error ? err.message : err
170
+ );
171
+ }
172
+ throw err;
173
+ }
174
+ if (result != null && typeof result.catch === "function") {
175
+ void result.catch((err) => {
176
+ if (isDevEnvironment()) {
177
+ console.warn(
178
+ "[lessonkit] tracking sink failed:",
179
+ err instanceof Error ? err.message : err
180
+ );
181
+ }
182
+ });
183
+ }
184
+ }
157
185
  function createTrackingClient(opts) {
158
186
  const sink = opts?.sink;
159
187
  const batchSink = opts?.batchSink;
160
188
  const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
161
189
  const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
162
190
  const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
191
+ const maxBufferSize = 1e3;
192
+ let warnedBufferCap = false;
163
193
  if (!batchEnabled) {
164
194
  let disposed2 = false;
165
195
  return {
166
196
  track: (event) => {
167
197
  if (disposed2) return;
168
- void sink?.(event);
198
+ if (sink) invokeTrackingSink(sink, event);
169
199
  },
170
200
  dispose: () => {
171
201
  disposed2 = true;
@@ -224,6 +254,15 @@ function createTrackingClient(opts) {
224
254
  return {
225
255
  track: (event) => {
226
256
  if (disposed || disposing) return;
257
+ if (buffer.length >= maxBufferSize) {
258
+ buffer.shift();
259
+ if (!warnedBufferCap && typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
260
+ warnedBufferCap = true;
261
+ console.warn(
262
+ `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
263
+ );
264
+ }
265
+ }
227
266
  buffer.push(event);
228
267
  if (buffer.length >= maxBatchSize) void flush();
229
268
  },
@@ -255,16 +294,461 @@ function nowIso() {
255
294
  return (/* @__PURE__ */ new Date()).toISOString();
256
295
  }
257
296
 
258
- // src/plugins.ts
259
- function defineLessonkitPlugin(plugin) {
260
- return plugin;
297
+ // src/telemetryBuilder.ts
298
+ var warnedMissingQuizLesson = false;
299
+ function isDevEnvironment2() {
300
+ const g = globalThis;
301
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
302
+ }
303
+ function resetTelemetryBuilderWarningsForTests() {
304
+ warnedMissingQuizLesson = false;
305
+ }
306
+ function buildTelemetryEvent(opts) {
307
+ const base = {
308
+ timestamp: opts.timestamp ?? nowIso(),
309
+ courseId: opts.courseId,
310
+ sessionId: opts.sessionId,
311
+ attemptId: opts.attemptId,
312
+ user: opts.user
313
+ };
314
+ switch (opts.name) {
315
+ case "course_started":
316
+ return { name: "course_started", ...base };
317
+ case "course_completed":
318
+ return { name: "course_completed", ...base };
319
+ case "lesson_started": {
320
+ const data = opts.data;
321
+ const lessonId = opts.lessonId ?? data?.lessonId;
322
+ if (!lessonId) throw new Error("lesson_started requires lessonId");
323
+ return {
324
+ name: "lesson_started",
325
+ ...base,
326
+ lessonId,
327
+ data: { ...data, lessonId }
328
+ };
329
+ }
330
+ case "lesson_completed":
331
+ case "lesson_time_on_task": {
332
+ const data = opts.data;
333
+ const lessonId = opts.lessonId ?? data?.lessonId;
334
+ if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
335
+ return {
336
+ name: opts.name,
337
+ ...base,
338
+ lessonId,
339
+ data: { ...data, lessonId }
340
+ };
341
+ }
342
+ case "quiz_answered": {
343
+ const data = opts.data;
344
+ const lessonId = opts.lessonId;
345
+ if (!lessonId) throw new Error("quiz_answered requires active lessonId");
346
+ return { name: "quiz_answered", ...base, lessonId, data };
347
+ }
348
+ case "quiz_completed": {
349
+ const data = opts.data;
350
+ const lessonId = opts.lessonId;
351
+ if (!lessonId) throw new Error("quiz_completed requires active lessonId");
352
+ return { name: "quiz_completed", ...base, lessonId, data };
353
+ }
354
+ case "interaction":
355
+ return {
356
+ name: "interaction",
357
+ ...base,
358
+ lessonId: opts.lessonId,
359
+ data: opts.data
360
+ };
361
+ default:
362
+ return { name: opts.name, ...base };
363
+ }
364
+ }
365
+ function tryBuildTelemetryEvent(opts) {
366
+ const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
367
+ if (isQuiz && !opts.lessonId) {
368
+ if (isDevEnvironment2() && !warnedMissingQuizLesson) {
369
+ warnedMissingQuizLesson = true;
370
+ console.warn(
371
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
372
+ );
373
+ }
374
+ return null;
375
+ }
376
+ return buildTelemetryEvent(opts);
377
+ }
378
+
379
+ // src/telemetryPipeline.ts
380
+ function isDevEnvironment3() {
381
+ const g = globalThis;
382
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
383
+ }
384
+ function warnSinkFailure(sinkId, err) {
385
+ if (isDevEnvironment3()) {
386
+ console.warn(
387
+ `[lessonkit] telemetry sink "${sinkId}" failed:`,
388
+ err instanceof Error ? err.message : err
389
+ );
390
+ }
391
+ }
392
+ function invokeSink(sink, event, emitCtx) {
393
+ let result;
394
+ try {
395
+ result = sink.emit(event, emitCtx);
396
+ } catch (err) {
397
+ warnSinkFailure(sink.id, err);
398
+ return;
399
+ }
400
+ if (result != null && typeof result.catch === "function") {
401
+ void result.catch((err) => warnSinkFailure(sink.id, err));
402
+ }
403
+ }
404
+ function createTelemetryPipeline(sinks) {
405
+ const list = [...sinks];
406
+ return {
407
+ sinks: list,
408
+ emit(event, ctx) {
409
+ const emitCtx = ctx ?? {
410
+ courseId: event.courseId,
411
+ sessionId: event.sessionId,
412
+ attemptId: event.attemptId
413
+ };
414
+ for (const sink of list) {
415
+ invokeSink(sink, event, emitCtx);
416
+ }
417
+ }
418
+ };
419
+ }
420
+ function createTrackingPipelineSink(id, track) {
421
+ return {
422
+ id,
423
+ emit(event) {
424
+ track(event);
425
+ }
426
+ };
427
+ }
428
+
429
+ // src/ports.ts
430
+ function createDefaultClock() {
431
+ return {
432
+ nowMs: () => Date.now(),
433
+ nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
434
+ };
435
+ }
436
+ function createNoopStorage() {
437
+ return {
438
+ getItem: () => null,
439
+ setItem: () => {
440
+ }
441
+ };
442
+ }
443
+ function createMemoryBackedSessionStorage(session) {
444
+ const memory = /* @__PURE__ */ new Map();
445
+ let warnedPersistFailure = false;
446
+ const warnPersistFailure = () => {
447
+ if (warnedPersistFailure) return;
448
+ warnedPersistFailure = true;
449
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
450
+ console.warn(
451
+ "[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
452
+ );
453
+ }
454
+ };
455
+ return {
456
+ getItem: (key) => {
457
+ if (memory.has(key)) return memory.get(key);
458
+ try {
459
+ const value = session.getItem(key);
460
+ if (value !== null) memory.set(key, value);
461
+ return value;
462
+ } catch {
463
+ return memory.get(key) ?? null;
464
+ }
465
+ },
466
+ setItem: (key, value) => {
467
+ memory.set(key, value);
468
+ try {
469
+ session.setItem(key, value);
470
+ } catch {
471
+ warnPersistFailure();
472
+ }
473
+ },
474
+ removeItem: (key) => {
475
+ memory.delete(key);
476
+ try {
477
+ session.removeItem(key);
478
+ } catch {
479
+ warnPersistFailure();
480
+ }
481
+ },
482
+ resetForTests: () => {
483
+ memory.clear();
484
+ }
485
+ };
486
+ }
487
+ function resetStoragePortForTests(storage) {
488
+ storage.resetForTests?.();
489
+ }
490
+ function createSessionStoragePort() {
491
+ if (typeof sessionStorage === "undefined") {
492
+ const memory = /* @__PURE__ */ new Map();
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
+ };
505
+ }
506
+ return createMemoryBackedSessionStorage(sessionStorage);
507
+ }
508
+ function createGlobalTimer() {
509
+ return {
510
+ setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
511
+ clearInterval: (id) => globalThis.clearInterval(id)
512
+ };
513
+ }
514
+
515
+ // src/progress.ts
516
+ function createProgressController() {
517
+ let activeLessonId;
518
+ let completedLessonIds = /* @__PURE__ */ new Set();
519
+ let courseCompleted = false;
520
+ const lessonStartTimes = /* @__PURE__ */ new Map();
521
+ return {
522
+ getState: () => ({
523
+ activeLessonId,
524
+ completedLessonIds: new Set(completedLessonIds),
525
+ courseCompleted
526
+ }),
527
+ setActiveLesson: (lessonId, startedAtMs) => {
528
+ const previousLessonId = activeLessonId;
529
+ activeLessonId = lessonId;
530
+ lessonStartTimes.set(lessonId, startedAtMs);
531
+ return { previousLessonId };
532
+ },
533
+ completeLesson: (lessonId, completedAtMs) => {
534
+ if (completedLessonIds.has(lessonId)) return { didComplete: false };
535
+ completedLessonIds = new Set(completedLessonIds).add(lessonId);
536
+ if (activeLessonId === lessonId) {
537
+ activeLessonId = void 0;
538
+ }
539
+ const startedAt = lessonStartTimes.get(lessonId);
540
+ lessonStartTimes.delete(lessonId);
541
+ const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
542
+ return { durationMs, didComplete: true };
543
+ },
544
+ completeCourse: () => {
545
+ if (courseCompleted) return { didComplete: false };
546
+ courseCompleted = true;
547
+ return { didComplete: true };
548
+ }
549
+ };
550
+ }
551
+
552
+ // src/session.ts
553
+ var SESSION_STORAGE_KEY = "lessonkit:sessionId";
554
+ function getTabSessionId(storage) {
555
+ return storage.getItem(SESSION_STORAGE_KEY);
556
+ }
557
+ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
558
+ var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
559
+ function resolveSessionId(storage, provided) {
560
+ if (provided) return provided;
561
+ const existing = storage.getItem(SESSION_STORAGE_KEY);
562
+ if (existing) return existing;
563
+ const id = createSessionId();
564
+ storage.setItem(SESSION_STORAGE_KEY, id);
565
+ return id;
566
+ }
567
+ function courseStartedStorageKey(sessionId, courseId) {
568
+ return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
261
569
  }
570
+ function courseStartedTrackingStorageKey(sessionId, courseId) {
571
+ return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
572
+ }
573
+ function hasCourseStarted(storage, sessionId, courseId) {
574
+ if (!courseId) return false;
575
+ return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
576
+ }
577
+ function markCourseStarted(storage, sessionId, courseId) {
578
+ if (!courseId) return;
579
+ storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
580
+ }
581
+ function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
582
+ if (!courseId) return false;
583
+ return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
584
+ }
585
+ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
586
+ if (!courseId) return;
587
+ storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
588
+ }
589
+ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
590
+ if (!courseId || fromSessionId === toSessionId) return;
591
+ if (hasCourseStarted(storage, fromSessionId, courseId)) {
592
+ markCourseStarted(storage, toSessionId, courseId);
593
+ storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
594
+ }
595
+ if (hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)) {
596
+ markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
597
+ storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
598
+ }
599
+ }
600
+
601
+ // src/runtime/courseLifecycle.ts
602
+ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
603
+ const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
604
+ if (alreadyEmittedToSink) {
605
+ return { emitted: true, marked };
606
+ }
607
+ if (marked) {
608
+ return { emitted: false, marked: true };
609
+ }
610
+ const emitted = deps.emitCourseStartedEvent(ctx);
611
+ if (emitted) {
612
+ markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
613
+ }
614
+ return { emitted, marked: emitted };
615
+ }
616
+ function buildCourseStartedTelemetryEvent(ctx) {
617
+ return buildTelemetryEvent({
618
+ name: "course_started",
619
+ courseId: ctx.courseId,
620
+ sessionId: ctx.sessionId,
621
+ attemptId: ctx.attemptId,
622
+ user: ctx.user
623
+ });
624
+ }
625
+ function completeLessonWithTelemetry(opts) {
626
+ const result = opts.progress.completeLesson(opts.lessonId, opts.nowMs);
627
+ if (!result.didComplete) return false;
628
+ opts.emitLessonCompleted(opts.lessonId, result.durationMs);
629
+ return true;
630
+ }
631
+ function completeCourseWithTelemetry(opts) {
632
+ const current = opts.progress.getState();
633
+ if (current.activeLessonId) {
634
+ completeLessonWithTelemetry({
635
+ progress: opts.progress,
636
+ lessonId: current.activeLessonId,
637
+ nowMs: opts.nowMs,
638
+ emitLessonCompleted: opts.emitLessonCompleted
639
+ });
640
+ }
641
+ const result = opts.progress.completeCourse();
642
+ if (!result.didComplete) return false;
643
+ opts.emitCourseCompleted();
644
+ return true;
645
+ }
646
+
647
+ // src/runtime/createLessonkitRuntime.ts
648
+ function createLessonkitRuntime(config, ports = {}) {
649
+ const storage = ports.storage ?? createSessionStoragePort();
650
+ const clock = ports.clock ?? createDefaultClock();
651
+ const configSnapshot = { ...config };
652
+ let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
653
+ let attemptId = configSnapshot.session?.attemptId;
654
+ let user = configSnapshot.session?.user;
655
+ let courseId = configSnapshot.courseId;
656
+ let progress = createProgressController();
657
+ const getSession = () => ({ sessionId, attemptId, user });
658
+ const syncSessionFromConfig = (next) => {
659
+ sessionId = resolveSessionId(storage, next.session?.sessionId);
660
+ attemptId = next.session?.attemptId;
661
+ user = next.session?.user;
662
+ courseId = next.courseId;
663
+ };
664
+ syncSessionFromConfig(configSnapshot);
665
+ const track = (name, data, emit, lessonId) => {
666
+ const event = tryBuildTelemetryEvent({
667
+ name,
668
+ courseId,
669
+ lessonId: lessonId ?? progress.getState().activeLessonId,
670
+ sessionId,
671
+ attemptId,
672
+ user,
673
+ data
674
+ });
675
+ if (!event) return;
676
+ emit(event);
677
+ };
678
+ const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
679
+ emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
680
+ if (durationMs !== void 0) {
681
+ emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
682
+ }
683
+ };
684
+ return {
685
+ get config() {
686
+ return configSnapshot;
687
+ },
688
+ get progress() {
689
+ return progress;
690
+ },
691
+ getProgressState: () => progress.getState(),
692
+ getSession,
693
+ updateConfig(next) {
694
+ if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
695
+ if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
696
+ if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
697
+ if (next.session !== void 0) {
698
+ configSnapshot.session = { ...configSnapshot.session, ...next.session };
699
+ }
700
+ syncSessionFromConfig(configSnapshot);
701
+ },
702
+ setActiveLesson(lessonId, emitFn) {
703
+ const current = progress.getState();
704
+ if (current.activeLessonId === lessonId) return;
705
+ if (current.completedLessonIds.has(lessonId)) {
706
+ progress.setActiveLesson(lessonId, clock.nowMs());
707
+ return;
708
+ }
709
+ const previous = current.activeLessonId;
710
+ if (previous && previous !== lessonId) {
711
+ const completed = progress.completeLesson(previous, clock.nowMs());
712
+ if (completed.didComplete) {
713
+ emitLessonCompleted(previous, completed.durationMs, emitFn);
714
+ }
715
+ }
716
+ progress.setActiveLesson(lessonId, clock.nowMs());
717
+ emitFn("lesson_started", { lessonId }, lessonId);
718
+ },
719
+ completeLesson(lessonId, emitFn) {
720
+ const result = progress.completeLesson(lessonId, clock.nowMs());
721
+ if (!result.didComplete) return;
722
+ emitLessonCompleted(lessonId, result.durationMs, emitFn);
723
+ },
724
+ completeCourse(emitFn) {
725
+ const current = progress.getState();
726
+ if (current.activeLessonId) {
727
+ const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
728
+ if (lessonResult.didComplete) {
729
+ emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
730
+ }
731
+ }
732
+ const result = progress.completeCourse();
733
+ if (!result.didComplete) return;
734
+ emitFn("course_completed");
735
+ },
736
+ track,
737
+ resetForCourseChange(nextCourseId) {
738
+ configSnapshot.courseId = nextCourseId;
739
+ courseId = nextCourseId;
740
+ progress = createProgressController();
741
+ }
742
+ };
743
+ }
744
+
745
+ // src/plugins/registry.ts
262
746
  function warnDuplicatePlugin(id) {
263
747
  const g = globalThis;
264
748
  if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
265
749
  console.warn(`[lessonkit] plugin id "${id}" was registered more than once; using the latest definition`);
266
750
  }
267
- function createPluginHost(plugins = []) {
751
+ function createPluginRegistry(plugins = []) {
268
752
  const registry = /* @__PURE__ */ new Map();
269
753
  for (const plugin of plugins) {
270
754
  if (registry.has(plugin.id)) warnDuplicatePlugin(plugin.id);
@@ -302,11 +786,26 @@ function createPluginHost(plugins = []) {
302
786
  }
303
787
  return events;
304
788
  };
305
- const composeTrackingSink = (sink, ctx) => {
789
+ const composeTrackingSink = (sink, ctxSource) => {
790
+ if (!sink) return void 0;
791
+ const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
792
+ const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}`;
793
+ const layers = [];
306
794
  let composed = sink;
307
795
  for (const plugin of list) {
308
- if (!plugin.wrapTrackingSink || !composed) continue;
309
- composed = plugin.wrapTrackingSink(composed, ctx);
796
+ if (!plugin.wrapTrackingSink) continue;
797
+ const inner = composed;
798
+ const layer = { plugin, inner, wrapped: null, lastCtxKey: "" };
799
+ layers.push(layer);
800
+ composed = (event) => {
801
+ const ctx = resolveCtx();
802
+ const key = ctxKey(ctx);
803
+ if (!layer.wrapped || layer.lastCtxKey !== key) {
804
+ layer.wrapped = layer.plugin.wrapTrackingSink(layer.inner, ctx) ?? layer.inner;
805
+ layer.lastCtxKey = key;
806
+ }
807
+ return layer.wrapped(event);
808
+ };
310
809
  }
311
810
  return composed;
312
811
  };
@@ -329,20 +828,57 @@ function createPluginHost(plugins = []) {
329
828
  scoreAssessment
330
829
  };
331
830
  }
831
+
832
+ // src/plugins/define.ts
833
+ function defineTelemetryPlugin(plugin) {
834
+ return plugin;
835
+ }
836
+ function defineAssessmentPlugin(plugin) {
837
+ return plugin;
838
+ }
839
+ function defineLifecyclePlugin(plugin) {
840
+ return plugin;
841
+ }
332
842
  export {
333
843
  ID_MAX_LENGTH,
334
844
  ID_PATTERN,
845
+ SESSION_STORAGE_KEY,
335
846
  TELEMETRY_EVENT_CATALOG,
336
847
  assertValidId,
848
+ buildCourseStartedTelemetryEvent,
337
849
  buildLessonkitUrn,
338
850
  buildTelemetryCatalog,
339
- createPluginHost,
851
+ buildTelemetryEvent,
852
+ completeCourseWithTelemetry,
853
+ completeLessonWithTelemetry,
854
+ createDefaultClock,
855
+ createGlobalTimer,
856
+ createLessonkitRuntime,
857
+ createNoopStorage,
858
+ createPluginRegistry,
859
+ createProgressController,
340
860
  createSessionId,
861
+ createSessionStoragePort,
862
+ createTelemetryPipeline,
341
863
  createTrackingClient,
342
- defineLessonkitPlugin,
864
+ createTrackingPipelineSink,
865
+ defineAssessmentPlugin,
866
+ defineLifecyclePlugin,
867
+ defineTelemetryPlugin,
343
868
  deriveId,
869
+ getTabSessionId,
870
+ hasCourseStarted,
871
+ hasCourseStartedEmittedToTracking,
872
+ markCourseStarted,
873
+ markCourseStartedEmittedToTracking,
874
+ migrateCourseStartedMark,
344
875
  nowIso,
876
+ resetStoragePortForTests,
877
+ resetTelemetryBuilderWarningsForTests,
878
+ resolveSessionId,
345
879
  slugifyId,
346
880
  telemetryCatalogVersion,
881
+ tryBuildTelemetryEvent,
882
+ tryEmitCourseStarted,
347
883
  validateId
348
884
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/core",
3
- "version": "0.9.2",
3
+ "version": "1.0.0",
4
4
  "private": false,
5
5
  "description": "Shared types and telemetry primitives for LessonKit.",
6
6
  "license": "Apache-2.0",