@lessonkit/xapi 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,8 +18,8 @@ npm install @lessonkit/xapi
18
18
  import { createXAPIClient } from "@lessonkit/xapi";
19
19
 
20
20
  const xapi = createXAPIClient({
21
+ courseId: "cyber-basics",
21
22
  transport: (statement) => {
22
- // Send to your LRS (or queue offline).
23
23
  console.log(statement);
24
24
  },
25
25
  });
@@ -27,8 +27,13 @@ const xapi = createXAPIClient({
27
27
  xapi.completeLesson({ lessonId: "phishing-101", durationMs: 1500, success: true, score: 7, maxScore: 10 });
28
28
  ```
29
29
 
30
- ## Notes (0.4.0)
30
+ Prefer mapping from telemetry: `telemetryEventToXAPIStatement(event)` (canonical object URNs).
31
31
 
32
+ ## Notes (0.5.0)
33
+
34
+ - `createXAPIClient` requires `courseId` for lifecycle helpers; React uses the mapper after each `track()`.
32
35
  - If the transport throws/rejects, statements are queued in-memory.
33
- - You can call `await xapi.flush()` to retry queued statements.
36
+ - Call `await xapi.flush()` to retry queued statements.
37
+
38
+ See [`docs/TELEMETRY.md`](../../docs/TELEMETRY.md).
34
39
 
package/dist/index.cjs CHANGED
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  createInMemoryXAPIQueue: () => createInMemoryXAPIQueue,
24
- createXAPIClient: () => createXAPIClient
24
+ createXAPIClient: () => createXAPIClient,
25
+ telemetryEventToXAPIStatement: () => telemetryEventToXAPIStatement
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
27
28
 
@@ -48,6 +49,9 @@ function createInMemoryXAPIQueue() {
48
49
  }
49
50
 
50
51
  // src/client.ts
52
+ var import_core2 = require("@lessonkit/core");
53
+
54
+ // src/telemetryMap.ts
51
55
  var import_core = require("@lessonkit/core");
52
56
 
53
57
  // src/id.ts
@@ -65,14 +69,112 @@ function formatDurationMs(ms) {
65
69
  return `PT${fixed}S`;
66
70
  }
67
71
 
68
- // src/client.ts
72
+ // src/telemetryMap.ts
69
73
  var XAPIVerbs = {
70
- started: "http://adlnet.gov/expapi/verbs/initialized",
71
- completed: "http://adlnet.gov/expapi/verbs/completed"
74
+ initialized: "http://adlnet.gov/expapi/verbs/initialized",
75
+ completed: "http://adlnet.gov/expapi/verbs/completed",
76
+ answered: "http://adlnet.gov/expapi/verbs/answered",
77
+ experienced: "http://adlnet.gov/expapi/verbs/experienced"
72
78
  };
79
+ function telemetryEventToXAPIStatement(event) {
80
+ const { courseId } = event;
81
+ switch (event.name) {
82
+ case "course_started":
83
+ return statementFor((0, import_core.buildLessonkitUrn)({ courseId }), XAPIVerbs.initialized, event.timestamp);
84
+ case "course_completed":
85
+ return statementFor((0, import_core.buildLessonkitUrn)({ courseId }), XAPIVerbs.completed, event.timestamp);
86
+ case "lesson_started": {
87
+ const lessonId = event.lessonId;
88
+ return statementFor(
89
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId }),
90
+ XAPIVerbs.initialized,
91
+ event.timestamp
92
+ );
93
+ }
94
+ case "lesson_completed": {
95
+ const lessonId = event.lessonId;
96
+ const data = event.data;
97
+ const result = {};
98
+ if (typeof data?.durationMs === "number") {
99
+ result.duration = formatDurationMs(data.durationMs);
100
+ }
101
+ if (typeof data?.success === "boolean") result.success = data.success;
102
+ if (typeof data?.score === "number" || typeof data?.maxScore === "number") {
103
+ const max = typeof data.maxScore === "number" ? data.maxScore : void 0;
104
+ const raw = typeof data.score === "number" ? data.score : void 0;
105
+ result.score = {
106
+ raw,
107
+ max,
108
+ min: 0,
109
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
110
+ };
111
+ }
112
+ return statementFor((0, import_core.buildLessonkitUrn)({ courseId, lessonId }), XAPIVerbs.completed, event.timestamp, {
113
+ result: Object.keys(result).length ? result : void 0
114
+ });
115
+ }
116
+ case "lesson_time_on_task":
117
+ return null;
118
+ case "quiz_answered": {
119
+ const lessonId = event.lessonId;
120
+ const checkId = event.data.checkId;
121
+ return statementFor(
122
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId, checkId }),
123
+ XAPIVerbs.answered,
124
+ event.timestamp
125
+ );
126
+ }
127
+ case "quiz_completed": {
128
+ const lessonId = event.lessonId;
129
+ const checkId = event.data.checkId;
130
+ const { score, maxScore } = event.data;
131
+ const result = {};
132
+ if (typeof score === "number" || typeof maxScore === "number") {
133
+ const max = typeof maxScore === "number" ? maxScore : void 0;
134
+ const raw = typeof score === "number" ? score : void 0;
135
+ result.score = {
136
+ raw,
137
+ max,
138
+ min: 0,
139
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
140
+ };
141
+ }
142
+ return statementFor(
143
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId, checkId }),
144
+ XAPIVerbs.completed,
145
+ event.timestamp,
146
+ { result: Object.keys(result).length ? result : void 0 }
147
+ );
148
+ }
149
+ case "interaction": {
150
+ const lessonId = event.lessonId;
151
+ const blockId = event.data?.blockId;
152
+ if (!lessonId || !blockId) return null;
153
+ return statementFor(
154
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId, blockId }),
155
+ XAPIVerbs.experienced,
156
+ event.timestamp
157
+ );
158
+ }
159
+ default:
160
+ return null;
161
+ }
162
+ }
163
+ function statementFor(objectId, verb, timestamp, extra) {
164
+ return {
165
+ id: cryptoRandomId(),
166
+ timestamp,
167
+ verb,
168
+ object: { id: objectId },
169
+ result: extra?.result,
170
+ context: extra?.context
171
+ };
172
+ }
173
+
174
+ // src/client.ts
73
175
  function createXAPIClient(opts) {
74
176
  const transport = opts?.transport;
75
- const baseId = opts?.baseId ?? "urn:lessonkit";
177
+ const courseId = opts?.courseId;
76
178
  const queue = opts?.queue ?? createInMemoryXAPIQueue();
77
179
  const sendOrQueue = (statement) => {
78
180
  if (!transport) {
@@ -83,6 +185,10 @@ function createXAPIClient(opts) {
83
185
  queue.enqueue(statement);
84
186
  });
85
187
  };
188
+ const emit = (event) => {
189
+ const statement = telemetryEventToXAPIStatement(event);
190
+ if (statement) sendOrQueue(statement);
191
+ };
86
192
  return {
87
193
  send: (statement) => {
88
194
  sendOrQueue(statement);
@@ -93,46 +199,44 @@ function createXAPIClient(opts) {
93
199
  await queue.flush(transport);
94
200
  },
95
201
  startedLesson: ({ lessonId }) => {
96
- const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.started);
97
- sendOrQueue(statement);
202
+ if (!courseId) return;
203
+ emit({
204
+ name: "lesson_started",
205
+ timestamp: (0, import_core2.nowIso)(),
206
+ courseId,
207
+ lessonId,
208
+ data: { lessonId }
209
+ });
98
210
  },
99
- completeLesson: ({ lessonId, durationMs, score, maxScore, success }) => {
100
- const result = {};
101
- if (typeof durationMs === "number") result.duration = formatDurationMs(durationMs);
102
- if (typeof success === "boolean") result.success = success;
103
- if (typeof score === "number" || typeof maxScore === "number") {
104
- const max = typeof maxScore === "number" ? maxScore : void 0;
105
- const raw = typeof score === "number" ? score : void 0;
106
- result.score = {
107
- raw,
108
- max,
109
- min: 0,
110
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
111
- };
112
- }
113
- const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.completed, {
114
- result: Object.keys(result).length ? result : void 0
211
+ completeLesson: ({
212
+ lessonId,
213
+ durationMs,
214
+ score,
215
+ maxScore,
216
+ success
217
+ }) => {
218
+ if (!courseId) return;
219
+ emit({
220
+ name: "lesson_completed",
221
+ timestamp: (0, import_core2.nowIso)(),
222
+ courseId,
223
+ lessonId,
224
+ data: { lessonId, durationMs, score, maxScore, success }
115
225
  });
116
- sendOrQueue(statement);
117
226
  },
118
227
  completeCourse: () => {
119
- const statement = statementFor(`${baseId}:course`, XAPIVerbs.completed);
120
- sendOrQueue(statement);
228
+ if (!courseId) return;
229
+ emit({
230
+ name: "course_completed",
231
+ timestamp: (0, import_core2.nowIso)(),
232
+ courseId
233
+ });
121
234
  }
122
235
  };
123
236
  }
124
- function statementFor(objectId, verb, extra) {
125
- return {
126
- id: cryptoRandomId(),
127
- timestamp: (0, import_core.nowIso)(),
128
- verb,
129
- object: { id: objectId },
130
- result: extra?.result,
131
- context: extra?.context
132
- };
133
- }
134
237
  // Annotate the CommonJS export names for ESM import in node:
135
238
  0 && (module.exports = {
136
239
  createInMemoryXAPIQueue,
137
- createXAPIClient
240
+ createXAPIClient,
241
+ telemetryEventToXAPIStatement
138
242
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { LessonId } from '@lessonkit/core';
1
+ import { LessonId, CourseId, TelemetryEvent } from '@lessonkit/core';
2
2
 
3
3
  type XAPIStatement = {
4
4
  id: string;
@@ -38,8 +38,14 @@ declare function createInMemoryXAPIQueue(): XAPIQueue;
38
38
 
39
39
  declare function createXAPIClient(opts?: {
40
40
  transport?: XAPITransport;
41
- baseId?: string;
41
+ courseId?: CourseId;
42
42
  queue?: XAPIQueue;
43
43
  }): XAPIClient;
44
44
 
45
- export { type XAPIClient, type XAPIQueue, type XAPIStatement, type XAPITransport, createInMemoryXAPIQueue, createXAPIClient };
45
+ /**
46
+ * Map a LessonKit telemetry event to an xAPI statement, or null if the event should not emit xAPI.
47
+ * `lesson_time_on_task` returns null (companion metric; lesson_completed carries duration).
48
+ */
49
+ declare function telemetryEventToXAPIStatement(event: TelemetryEvent): XAPIStatement | null;
50
+
51
+ export { type XAPIClient, type XAPIQueue, type XAPIStatement, type XAPITransport, createInMemoryXAPIQueue, createXAPIClient, telemetryEventToXAPIStatement };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LessonId } from '@lessonkit/core';
1
+ import { LessonId, CourseId, TelemetryEvent } from '@lessonkit/core';
2
2
 
3
3
  type XAPIStatement = {
4
4
  id: string;
@@ -38,8 +38,14 @@ declare function createInMemoryXAPIQueue(): XAPIQueue;
38
38
 
39
39
  declare function createXAPIClient(opts?: {
40
40
  transport?: XAPITransport;
41
- baseId?: string;
41
+ courseId?: CourseId;
42
42
  queue?: XAPIQueue;
43
43
  }): XAPIClient;
44
44
 
45
- export { type XAPIClient, type XAPIQueue, type XAPIStatement, type XAPITransport, createInMemoryXAPIQueue, createXAPIClient };
45
+ /**
46
+ * Map a LessonKit telemetry event to an xAPI statement, or null if the event should not emit xAPI.
47
+ * `lesson_time_on_task` returns null (companion metric; lesson_completed carries duration).
48
+ */
49
+ declare function telemetryEventToXAPIStatement(event: TelemetryEvent): XAPIStatement | null;
50
+
51
+ export { type XAPIClient, type XAPIQueue, type XAPIStatement, type XAPITransport, createInMemoryXAPIQueue, createXAPIClient, telemetryEventToXAPIStatement };
package/dist/index.js CHANGED
@@ -23,6 +23,9 @@ function createInMemoryXAPIQueue() {
23
23
  // src/client.ts
24
24
  import { nowIso } from "@lessonkit/core";
25
25
 
26
+ // src/telemetryMap.ts
27
+ import { buildLessonkitUrn } from "@lessonkit/core";
28
+
26
29
  // src/id.ts
27
30
  function cryptoRandomId() {
28
31
  const g = globalThis;
@@ -38,14 +41,112 @@ function formatDurationMs(ms) {
38
41
  return `PT${fixed}S`;
39
42
  }
40
43
 
41
- // src/client.ts
44
+ // src/telemetryMap.ts
42
45
  var XAPIVerbs = {
43
- started: "http://adlnet.gov/expapi/verbs/initialized",
44
- completed: "http://adlnet.gov/expapi/verbs/completed"
46
+ initialized: "http://adlnet.gov/expapi/verbs/initialized",
47
+ completed: "http://adlnet.gov/expapi/verbs/completed",
48
+ answered: "http://adlnet.gov/expapi/verbs/answered",
49
+ experienced: "http://adlnet.gov/expapi/verbs/experienced"
45
50
  };
51
+ function telemetryEventToXAPIStatement(event) {
52
+ const { courseId } = event;
53
+ switch (event.name) {
54
+ case "course_started":
55
+ return statementFor(buildLessonkitUrn({ courseId }), XAPIVerbs.initialized, event.timestamp);
56
+ case "course_completed":
57
+ return statementFor(buildLessonkitUrn({ courseId }), XAPIVerbs.completed, event.timestamp);
58
+ case "lesson_started": {
59
+ const lessonId = event.lessonId;
60
+ return statementFor(
61
+ buildLessonkitUrn({ courseId, lessonId }),
62
+ XAPIVerbs.initialized,
63
+ event.timestamp
64
+ );
65
+ }
66
+ case "lesson_completed": {
67
+ const lessonId = event.lessonId;
68
+ const data = event.data;
69
+ const result = {};
70
+ if (typeof data?.durationMs === "number") {
71
+ result.duration = formatDurationMs(data.durationMs);
72
+ }
73
+ if (typeof data?.success === "boolean") result.success = data.success;
74
+ if (typeof data?.score === "number" || typeof data?.maxScore === "number") {
75
+ const max = typeof data.maxScore === "number" ? data.maxScore : void 0;
76
+ const raw = typeof data.score === "number" ? data.score : void 0;
77
+ result.score = {
78
+ raw,
79
+ max,
80
+ min: 0,
81
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
82
+ };
83
+ }
84
+ return statementFor(buildLessonkitUrn({ courseId, lessonId }), XAPIVerbs.completed, event.timestamp, {
85
+ result: Object.keys(result).length ? result : void 0
86
+ });
87
+ }
88
+ case "lesson_time_on_task":
89
+ return null;
90
+ case "quiz_answered": {
91
+ const lessonId = event.lessonId;
92
+ const checkId = event.data.checkId;
93
+ return statementFor(
94
+ buildLessonkitUrn({ courseId, lessonId, checkId }),
95
+ XAPIVerbs.answered,
96
+ event.timestamp
97
+ );
98
+ }
99
+ case "quiz_completed": {
100
+ const lessonId = event.lessonId;
101
+ const checkId = event.data.checkId;
102
+ const { score, maxScore } = event.data;
103
+ const result = {};
104
+ if (typeof score === "number" || typeof maxScore === "number") {
105
+ const max = typeof maxScore === "number" ? maxScore : void 0;
106
+ const raw = typeof score === "number" ? score : void 0;
107
+ result.score = {
108
+ raw,
109
+ max,
110
+ min: 0,
111
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
112
+ };
113
+ }
114
+ return statementFor(
115
+ buildLessonkitUrn({ courseId, lessonId, checkId }),
116
+ XAPIVerbs.completed,
117
+ event.timestamp,
118
+ { result: Object.keys(result).length ? result : void 0 }
119
+ );
120
+ }
121
+ case "interaction": {
122
+ const lessonId = event.lessonId;
123
+ const blockId = event.data?.blockId;
124
+ if (!lessonId || !blockId) return null;
125
+ return statementFor(
126
+ buildLessonkitUrn({ courseId, lessonId, blockId }),
127
+ XAPIVerbs.experienced,
128
+ event.timestamp
129
+ );
130
+ }
131
+ default:
132
+ return null;
133
+ }
134
+ }
135
+ function statementFor(objectId, verb, timestamp, extra) {
136
+ return {
137
+ id: cryptoRandomId(),
138
+ timestamp,
139
+ verb,
140
+ object: { id: objectId },
141
+ result: extra?.result,
142
+ context: extra?.context
143
+ };
144
+ }
145
+
146
+ // src/client.ts
46
147
  function createXAPIClient(opts) {
47
148
  const transport = opts?.transport;
48
- const baseId = opts?.baseId ?? "urn:lessonkit";
149
+ const courseId = opts?.courseId;
49
150
  const queue = opts?.queue ?? createInMemoryXAPIQueue();
50
151
  const sendOrQueue = (statement) => {
51
152
  if (!transport) {
@@ -56,6 +157,10 @@ function createXAPIClient(opts) {
56
157
  queue.enqueue(statement);
57
158
  });
58
159
  };
160
+ const emit = (event) => {
161
+ const statement = telemetryEventToXAPIStatement(event);
162
+ if (statement) sendOrQueue(statement);
163
+ };
59
164
  return {
60
165
  send: (statement) => {
61
166
  sendOrQueue(statement);
@@ -66,45 +171,43 @@ function createXAPIClient(opts) {
66
171
  await queue.flush(transport);
67
172
  },
68
173
  startedLesson: ({ lessonId }) => {
69
- const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.started);
70
- sendOrQueue(statement);
174
+ if (!courseId) return;
175
+ emit({
176
+ name: "lesson_started",
177
+ timestamp: nowIso(),
178
+ courseId,
179
+ lessonId,
180
+ data: { lessonId }
181
+ });
71
182
  },
72
- completeLesson: ({ lessonId, durationMs, score, maxScore, success }) => {
73
- const result = {};
74
- if (typeof durationMs === "number") result.duration = formatDurationMs(durationMs);
75
- if (typeof success === "boolean") result.success = success;
76
- if (typeof score === "number" || typeof maxScore === "number") {
77
- const max = typeof maxScore === "number" ? maxScore : void 0;
78
- const raw = typeof score === "number" ? score : void 0;
79
- result.score = {
80
- raw,
81
- max,
82
- min: 0,
83
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
84
- };
85
- }
86
- const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.completed, {
87
- result: Object.keys(result).length ? result : void 0
183
+ completeLesson: ({
184
+ lessonId,
185
+ durationMs,
186
+ score,
187
+ maxScore,
188
+ success
189
+ }) => {
190
+ if (!courseId) return;
191
+ emit({
192
+ name: "lesson_completed",
193
+ timestamp: nowIso(),
194
+ courseId,
195
+ lessonId,
196
+ data: { lessonId, durationMs, score, maxScore, success }
88
197
  });
89
- sendOrQueue(statement);
90
198
  },
91
199
  completeCourse: () => {
92
- const statement = statementFor(`${baseId}:course`, XAPIVerbs.completed);
93
- sendOrQueue(statement);
200
+ if (!courseId) return;
201
+ emit({
202
+ name: "course_completed",
203
+ timestamp: nowIso(),
204
+ courseId
205
+ });
94
206
  }
95
207
  };
96
208
  }
97
- function statementFor(objectId, verb, extra) {
98
- return {
99
- id: cryptoRandomId(),
100
- timestamp: nowIso(),
101
- verb,
102
- object: { id: objectId },
103
- result: extra?.result,
104
- context: extra?.context
105
- };
106
- }
107
209
  export {
108
210
  createInMemoryXAPIQueue,
109
- createXAPIClient
211
+ createXAPIClient,
212
+ telemetryEventToXAPIStatement
110
213
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/xapi",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "xAPI statement generation primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -45,7 +45,7 @@
45
45
  "lint": "echo \"(no lint configured yet)\""
46
46
  },
47
47
  "dependencies": {
48
- "@lessonkit/core": "0.4.0"
48
+ "@lessonkit/core": "0.5.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "tsup": "^8.5.0",