Overview
The Caliper client provides access to the Caliper Analytics API, enabling:- Activity Events: Track learning activity completions with metrics
- Time Spent Events: Record time spent on activities
- Generic Events: Send any IMS Caliper v1.2 event
- Batching: Combine multiple events in a single API call
- Job Monitoring: Track asynchronous event processing
Installation
npm install @timeback/caliper
pnpm add @timeback/caliper
yarn add @timeback/caliper
bun add @timeback/caliper
pip install timeback-caliper
uv add timeback-caliper
Quick Start
import { CaliperClient } from '@timeback/caliper'
const client = new CaliperClient({
env: 'staging',
auth: {
clientId: process.env.CALIPER_CLIENT_ID!,
clientSecret: process.env.CALIPER_CLIENT_SECRET!,
},
})
const result = await client.events.sendActivity('https://myapp.example.com/sensors/main', {
actor: {
id: 'https://api.example.com/users/123',
type: 'TimebackUser',
email: 'student@example.edu',
},
object: {
id: 'https://myapp.example.com/activities/456',
type: 'TimebackActivityContext',
subject: 'Math',
app: { name: 'My Learning App' },
},
metrics: [
{ type: 'totalQuestions', value: 10 },
{ type: 'correctQuestions', value: 8 },
{ type: 'xpEarned', value: 150 },
],
})
const status = await client.jobs.waitForCompletion(result.jobId)
from timeback_caliper import CaliperClient, ActivityCompletedInput
client = CaliperClient(
env="staging",
client_id=os.environ["CALIPER_CLIENT_ID"],
client_secret=os.environ["CALIPER_CLIENT_SECRET"],
)
result = await client.events.send_activity("https://myapp.example.com/sensors/main",
ActivityCompletedInput(
actor={"id": "https://api.example.com/users/123", "type": "TimebackUser", "email": "student@example.edu"},
object={"id": "https://myapp.example.com/activities/456", "type": "TimebackActivityContext", "subject": "Math", "app": {"name": "My Learning App"}},
metrics=[
{"type": "totalQuestions", "value": 10},
{"type": "correctQuestions", "value": 8},
{"type": "xpEarned", "value": 150},
],
),
)
status = await client.jobs.wait_for_completion(result.job_id)
Client Structure
const client = new CaliperClient(options)
// Events
client.events.send(sensor, events) // Send events
client.events.sendEnvelope(envelope) // Send raw envelope
client.events.validate(envelope) // Validate without storing
client.events.sendActivity(sensor, input) // Timeback Profile
client.events.sendTimeSpent(sensor, input) // Timeback Profile
client.events.list(params?) // List events
client.events.get(externalId) // Get event by ID
client.events.stream(params?) // Stream events with pagination
// Jobs
client.jobs.getStatus(jobId) // Check job status
client.jobs.waitForCompletion(jobId, options?) // Poll until complete
Activity Completed Events
Track when users complete learning activities:await client.events.sendActivity(sensorUrl, {
actor: {
id: 'https://api.example.com/users/123',
type: 'TimebackUser',
email: 'student@example.edu',
role: 'student',
},
object: {
id: 'https://myapp.example.com/activities/lesson-1',
type: 'TimebackActivityContext',
subject: 'Math',
app: { name: 'My Learning App' },
course: { name: 'Algebra 101' },
activity: { name: 'Chapter 1 Quiz' },
},
attempt: 1,
generatedExtensions: { pctCompleteApp: 67 },
metrics: [
{ type: 'totalQuestions', value: 10 },
{ type: 'correctQuestions', value: 8 },
{ type: 'xpEarned', value: 150 },
{ type: 'masteredUnits', value: 2 },
],
})
from timeback_caliper import ActivityCompletedInput
await client.events.send_activity(sensor_url,
ActivityCompletedInput(
actor={"id": "https://api.example.com/users/123", "type": "TimebackUser", "email": "student@example.edu"},
object={"id": "https://myapp.example.com/activities/lesson-1", "type": "TimebackActivityContext", "subject": "Math", "app": {"name": "My Learning App"}},
metrics=[
{"type": "totalQuestions", "value": 10},
{"type": "correctQuestions", "value": 8},
{"type": "xpEarned", "value": 150},
{"type": "masteredUnits", "value": 2},
],
),
)
Metric Types
| Type | Description |
|---|---|
totalQuestions | Total number of questions attempted |
correctQuestions | Number of correct answers |
xpEarned | Experience points earned |
masteredUnits | Number of units mastered |
Time Spent Events
Track time spent on activities:await client.events.sendTimeSpent(sensorUrl, {
actor: {
id: 'https://api.example.com/users/123',
type: 'TimebackUser',
email: 'student@example.edu',
},
object: {
id: 'https://myapp.example.com/activities/lesson-1',
type: 'TimebackActivityContext',
subject: 'Reading',
app: { name: 'My App' },
},
metrics: [
{ type: 'active', value: 1800 }, // 30 minutes in seconds
{ type: 'inactive', value: 300 }, // 5 minutes paused
],
})
from timeback_caliper import TimeSpentInput
await client.events.send_time_spent(sensor_url,
TimeSpentInput(
actor={"id": "https://api.example.com/users/123", "type": "TimebackUser", "email": "student@example.edu"},
object={"id": "https://myapp.example.com/activities/lesson-1", "type": "TimebackActivityContext", "subject": "Reading", "app": {"name": "My App"}},
metrics=[
{"type": "active", "value": 1800}, # 30 minutes in seconds
{"type": "inactive", "value": 300}, # 5 minutes paused
],
),
)
Job Monitoring
Event processing is asynchronous. Monitor job status:client.events.sendActivity(sensorUrl, activityData) // Returns { jobId }
client.jobs.getStatus(jobId)
client.jobs.waitForCompletion(jobId)
await client.events.send_activity(sensor_url, activity_data) # Returns { job_id }
await client.jobs.get_status(job_id)
await client.jobs.wait_for_completion(job_id)
| Method | Returns | Description |
|---|---|---|
sendActivity() | { jobId } | Send events, get job ID |
getStatus() | JobStatus | Check status: pending/processing/completed/failed |
waitForCompletion() | JobStatus | Poll until job completes |
Query Events
Retrieve stored events:client.events.list({ limit?, offset?, sensor?, actorId?, actorEmail?, startDate?, endDate? })
client.events.get(externalId)
client.events.stream({ startDate?, max? })
client.events.stream({ startDate?, max? }).toArray()
await client.events.list(limit=..., offset=..., sensor=..., actor_id=..., start_date=..., end_date=...)
await client.events.get(external_id)
async for event in client.events.stream(start_date=..., max_items=...):
...
Batching Events
Use factory functions to combine multiple events in a single API call:import { CaliperClient, createActivityEvent, createTimeSpentEvent } from '@timeback/caliper'
const activityEvent = createActivityEvent({
actor: { ... },
object: { ... },
metrics: [{ type: 'correctQuestions', value: 8 }],
attempt: 2,
generatedExtensions: { pctCompleteApp: 67 },
})
const timeSpentEvent = createTimeSpentEvent({
actor: { ... },
object: { ... },
metrics: [{ type: 'active', value: 1800 }],
})
// Send both in one API call
const result = await client.events.send(sensor, [activityEvent, timeSpentEvent])
Generic Events
For non-Timeback events, use the generic API:const event = {
id: 'urn:uuid:...',
type: 'NavigationEvent',
actor: 'https://example.edu/users/123',
action: 'NavigatedTo',
object: 'https://example.edu/courses/456',
eventTime: new Date().toISOString(),
profile: 'GeneralProfile',
}
await client.events.send(sensor, [event])
Error Handling
import { ApiError } from '@timeback/caliper'
try {
await client.events.get('invalid-id')
} catch (error) {
if (error instanceof ApiError) {
console.log(error.statusCode)
console.log(error.message)
}
}
SDK Integration
When using the full SDK, activity tracking automatically emits Caliper events:import { createTimeback } from '@timeback/sdk'
const timeback = await createTimeback({
/* ... */
})
const activity = timeback.activity.start({
id: 'lesson-1',
name: 'Introduction',
course: { subject: 'Math', grade: 3 },
})
await activity.end({
totalQuestions: 10,
correctQuestions: 8,
xpEarned: 80,
}) // Emits ActivityCompleted + TimeSpent events
Next Steps
Custom Activities
SDK-based activity tracking
EduBridge
Pre-aggregated analytics
Types
Caliper type definitions