@lessonkit/xapi 0.4.0 → 0.6.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 +9 -4
- package/dist/index.cjs +159 -37
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +157 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@lessonkit/xapi`
|
|
2
2
|
|
|
3
|
-
[](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@lessonkit/xapi)
|
|
5
5
|
[](../../LICENSE)
|
|
6
6
|
|
|
@@ -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
|
-
|
|
30
|
+
Prefer mapping from telemetry: `telemetryEventToXAPIStatement(event)` (canonical object URNs).
|
|
31
31
|
|
|
32
|
+
## Notes (0.6.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
|
-
-
|
|
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,24 +69,144 @@ function formatDurationMs(ms) {
|
|
|
65
69
|
return `PT${fixed}S`;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
// src/
|
|
72
|
+
// src/telemetryMap.ts
|
|
69
73
|
var XAPIVerbs = {
|
|
70
|
-
|
|
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
|
|
175
|
+
function isDevEnvironment() {
|
|
176
|
+
const g = globalThis;
|
|
177
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
178
|
+
}
|
|
73
179
|
function createXAPIClient(opts) {
|
|
74
180
|
const transport = opts?.transport;
|
|
75
|
-
const
|
|
181
|
+
const courseId = opts?.courseId;
|
|
76
182
|
const queue = opts?.queue ?? createInMemoryXAPIQueue();
|
|
183
|
+
let warnedNoTransport = false;
|
|
184
|
+
let warnedTransportFailure = false;
|
|
77
185
|
const sendOrQueue = (statement) => {
|
|
78
186
|
if (!transport) {
|
|
79
187
|
queue.enqueue(statement);
|
|
188
|
+
if (isDevEnvironment() && !warnedNoTransport) {
|
|
189
|
+
warnedNoTransport = true;
|
|
190
|
+
console.warn(
|
|
191
|
+
"[lessonkit] xAPI statements are queued but no transport is configured; pass config.xapi.transport or config.xapi.client"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
80
194
|
return;
|
|
81
195
|
}
|
|
82
196
|
void Promise.resolve().then(() => transport(statement)).catch(() => {
|
|
83
197
|
queue.enqueue(statement);
|
|
198
|
+
if (isDevEnvironment() && !warnedTransportFailure) {
|
|
199
|
+
warnedTransportFailure = true;
|
|
200
|
+
console.warn(
|
|
201
|
+
"[lessonkit] xAPI transport failed; statement re-queued. Check your LRS endpoint or transport implementation."
|
|
202
|
+
);
|
|
203
|
+
}
|
|
84
204
|
});
|
|
85
205
|
};
|
|
206
|
+
const emit = (event) => {
|
|
207
|
+
const statement = telemetryEventToXAPIStatement(event);
|
|
208
|
+
if (statement) sendOrQueue(statement);
|
|
209
|
+
};
|
|
86
210
|
return {
|
|
87
211
|
send: (statement) => {
|
|
88
212
|
sendOrQueue(statement);
|
|
@@ -93,46 +217,44 @@ function createXAPIClient(opts) {
|
|
|
93
217
|
await queue.flush(transport);
|
|
94
218
|
},
|
|
95
219
|
startedLesson: ({ lessonId }) => {
|
|
96
|
-
|
|
97
|
-
|
|
220
|
+
if (!courseId) return;
|
|
221
|
+
emit({
|
|
222
|
+
name: "lesson_started",
|
|
223
|
+
timestamp: (0, import_core2.nowIso)(),
|
|
224
|
+
courseId,
|
|
225
|
+
lessonId,
|
|
226
|
+
data: { lessonId }
|
|
227
|
+
});
|
|
98
228
|
},
|
|
99
|
-
completeLesson: ({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.completed, {
|
|
114
|
-
result: Object.keys(result).length ? result : void 0
|
|
229
|
+
completeLesson: ({
|
|
230
|
+
lessonId,
|
|
231
|
+
durationMs,
|
|
232
|
+
score,
|
|
233
|
+
maxScore,
|
|
234
|
+
success
|
|
235
|
+
}) => {
|
|
236
|
+
if (!courseId) return;
|
|
237
|
+
emit({
|
|
238
|
+
name: "lesson_completed",
|
|
239
|
+
timestamp: (0, import_core2.nowIso)(),
|
|
240
|
+
courseId,
|
|
241
|
+
lessonId,
|
|
242
|
+
data: { lessonId, durationMs, score, maxScore, success }
|
|
115
243
|
});
|
|
116
|
-
sendOrQueue(statement);
|
|
117
244
|
},
|
|
118
245
|
completeCourse: () => {
|
|
119
|
-
|
|
120
|
-
|
|
246
|
+
if (!courseId) return;
|
|
247
|
+
emit({
|
|
248
|
+
name: "course_completed",
|
|
249
|
+
timestamp: (0, import_core2.nowIso)(),
|
|
250
|
+
courseId
|
|
251
|
+
});
|
|
121
252
|
}
|
|
122
253
|
};
|
|
123
254
|
}
|
|
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
255
|
// Annotate the CommonJS export names for ESM import in node:
|
|
135
256
|
0 && (module.exports = {
|
|
136
257
|
createInMemoryXAPIQueue,
|
|
137
|
-
createXAPIClient
|
|
258
|
+
createXAPIClient,
|
|
259
|
+
telemetryEventToXAPIStatement
|
|
138
260
|
});
|
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
|
-
|
|
41
|
+
courseId?: CourseId;
|
|
42
42
|
queue?: XAPIQueue;
|
|
43
43
|
}): XAPIClient;
|
|
44
44
|
|
|
45
|
-
|
|
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
|
-
|
|
41
|
+
courseId?: CourseId;
|
|
42
42
|
queue?: XAPIQueue;
|
|
43
43
|
}): XAPIClient;
|
|
44
44
|
|
|
45
|
-
|
|
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,24 +41,144 @@ function formatDurationMs(ms) {
|
|
|
38
41
|
return `PT${fixed}S`;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
// src/
|
|
44
|
+
// src/telemetryMap.ts
|
|
42
45
|
var XAPIVerbs = {
|
|
43
|
-
|
|
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
|
|
147
|
+
function isDevEnvironment() {
|
|
148
|
+
const g = globalThis;
|
|
149
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
150
|
+
}
|
|
46
151
|
function createXAPIClient(opts) {
|
|
47
152
|
const transport = opts?.transport;
|
|
48
|
-
const
|
|
153
|
+
const courseId = opts?.courseId;
|
|
49
154
|
const queue = opts?.queue ?? createInMemoryXAPIQueue();
|
|
155
|
+
let warnedNoTransport = false;
|
|
156
|
+
let warnedTransportFailure = false;
|
|
50
157
|
const sendOrQueue = (statement) => {
|
|
51
158
|
if (!transport) {
|
|
52
159
|
queue.enqueue(statement);
|
|
160
|
+
if (isDevEnvironment() && !warnedNoTransport) {
|
|
161
|
+
warnedNoTransport = true;
|
|
162
|
+
console.warn(
|
|
163
|
+
"[lessonkit] xAPI statements are queued but no transport is configured; pass config.xapi.transport or config.xapi.client"
|
|
164
|
+
);
|
|
165
|
+
}
|
|
53
166
|
return;
|
|
54
167
|
}
|
|
55
168
|
void Promise.resolve().then(() => transport(statement)).catch(() => {
|
|
56
169
|
queue.enqueue(statement);
|
|
170
|
+
if (isDevEnvironment() && !warnedTransportFailure) {
|
|
171
|
+
warnedTransportFailure = true;
|
|
172
|
+
console.warn(
|
|
173
|
+
"[lessonkit] xAPI transport failed; statement re-queued. Check your LRS endpoint or transport implementation."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
57
176
|
});
|
|
58
177
|
};
|
|
178
|
+
const emit = (event) => {
|
|
179
|
+
const statement = telemetryEventToXAPIStatement(event);
|
|
180
|
+
if (statement) sendOrQueue(statement);
|
|
181
|
+
};
|
|
59
182
|
return {
|
|
60
183
|
send: (statement) => {
|
|
61
184
|
sendOrQueue(statement);
|
|
@@ -66,45 +189,43 @@ function createXAPIClient(opts) {
|
|
|
66
189
|
await queue.flush(transport);
|
|
67
190
|
},
|
|
68
191
|
startedLesson: ({ lessonId }) => {
|
|
69
|
-
|
|
70
|
-
|
|
192
|
+
if (!courseId) return;
|
|
193
|
+
emit({
|
|
194
|
+
name: "lesson_started",
|
|
195
|
+
timestamp: nowIso(),
|
|
196
|
+
courseId,
|
|
197
|
+
lessonId,
|
|
198
|
+
data: { lessonId }
|
|
199
|
+
});
|
|
71
200
|
},
|
|
72
|
-
completeLesson: ({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const statement = statementFor(`${baseId}:lesson:${lessonId}`, XAPIVerbs.completed, {
|
|
87
|
-
result: Object.keys(result).length ? result : void 0
|
|
201
|
+
completeLesson: ({
|
|
202
|
+
lessonId,
|
|
203
|
+
durationMs,
|
|
204
|
+
score,
|
|
205
|
+
maxScore,
|
|
206
|
+
success
|
|
207
|
+
}) => {
|
|
208
|
+
if (!courseId) return;
|
|
209
|
+
emit({
|
|
210
|
+
name: "lesson_completed",
|
|
211
|
+
timestamp: nowIso(),
|
|
212
|
+
courseId,
|
|
213
|
+
lessonId,
|
|
214
|
+
data: { lessonId, durationMs, score, maxScore, success }
|
|
88
215
|
});
|
|
89
|
-
sendOrQueue(statement);
|
|
90
216
|
},
|
|
91
217
|
completeCourse: () => {
|
|
92
|
-
|
|
93
|
-
|
|
218
|
+
if (!courseId) return;
|
|
219
|
+
emit({
|
|
220
|
+
name: "course_completed",
|
|
221
|
+
timestamp: nowIso(),
|
|
222
|
+
courseId
|
|
223
|
+
});
|
|
94
224
|
}
|
|
95
225
|
};
|
|
96
226
|
}
|
|
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
227
|
export {
|
|
108
228
|
createInMemoryXAPIQueue,
|
|
109
|
-
createXAPIClient
|
|
229
|
+
createXAPIClient,
|
|
230
|
+
telemetryEventToXAPIStatement
|
|
110
231
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/xapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.
|
|
48
|
+
"@lessonkit/core": "0.6.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"tsup": "^8.5.0",
|