Plugin cookbook — console analytics¶
This walkthrough adds a minimal analytics plugin to a LessonKit React course. It mirrors examples/_shared/plugins/consoleAnalyticsPlugin.ts.
1. Define the plugin¶
import { defineTelemetryPlugin } from "@lessonkit/react";
export const consoleAnalyticsPlugin = defineTelemetryPlugin({
id: "example.console-analytics",
version: "1.0.0",
kind: "analytics",
name: "Console analytics",
setup(ctx) {
console.info("[plugin] course", ctx.courseId);
},
onTelemetry(event) {
if (event.name === "quiz_completed") {
console.info("[plugin] quiz_completed", event.data);
}
return event;
},
});
2. Register on the course¶
import { Course, Lesson, Quiz } from "@lessonkit/react";
import { consoleAnalyticsPlugin } from "./plugins/consoleAnalyticsPlugin";
export default function App() {
return (
<Course
title="Demo"
courseId="plugin-demo"
config={{
plugins: [consoleAnalyticsPlugin],
tracking: { sink: (e) => console.log("[sink]", e.name) },
}}
>
<Lesson title="Lesson" lessonId="lesson-1">
<Quiz
checkId="check-1"
question="Pick one"
choices={["A", "B"]}
answer="B"
/>
</Lesson>
</Course>
);
}
3. Wrap the tracking sink (optional)¶
Analytics plugins can decorate the configured sink:
wrapTrackingSink(sink, ctx) {
return async (event) => {
console.info("[wrapped]", ctx.courseId, event.name);
await sink(event);
};
},
Registration order matters: the first plugin in the array is the innermost wrapper.
4. Filter telemetry¶
Return null from onTelemetry to drop an event before it reaches tracking/xAPI:
onTelemetry(event) {
if (event.name === "interaction") return null;
return event;
},
5. Custom scoring with scoreAssessment¶
Use kind: "assessment" and implement scoreAssessment for open-response or rubric scoring. The host uses the first non-null result from assessment plugins.
Define the plugin¶
import { defineAssessmentPlugin } from "@lessonkit/react";
export const rubricPlugin = defineAssessmentPlugin({
id: "example.rubric",
version: "1.0.0",
kind: "assessment",
name: "Simple rubric",
scoreAssessment(ctx) {
if (ctx.interactionType !== "essay") return null;
const text = String(ctx.response ?? "");
const passed = text.trim().length >= 20;
return {
score: passed ? 1 : 0,
maxScore: 1,
passed,
};
},
});
Register and verify telemetry¶
<Course
courseId="plugin-demo"
title="Essay demo"
config={{
plugins: [rubricPlugin],
tracking: { sink: (e) => console.log(e.name, e) },
}}
>
<Lesson title="Lesson" lessonId="lesson-1">
<Essay checkId="essay-1" prompt="Describe your incident response plan." />
</Lesson>
</Course>
Complete the essay and confirm assessment_completed in the sink with your custom score / maxScore.
Test the plugin¶
import { describe, expect, it } from "vitest";
import { rubricPlugin } from "./rubricPlugin";
describe("rubricPlugin", () => {
it("passes long responses", () => {
const result = rubricPlugin.scoreAssessment?.({
interactionType: "essay",
response: "a".repeat(25),
checkId: "essay-1",
lessonId: "lesson-1",
courseId: "plugin-demo",
} as never);
expect(result?.passed).toBe(true);
});
});