Skip to content

TypeScript implementation of the IMSGlobal/caliper-js library

License

Notifications You must be signed in to change notification settings

CarnegieLearningWeb/caliper-ts

Repository files navigation

caliper-ts

The Caliper Analytics® Specification provides a structured approach to describing, collecting and exchanging learning activity data at scale. Caliper also defines an application programming interface (the Sensor API™) for marshalling and transmitting event data from instrumented applications to target endpoints for storage, analysis and use.

caliper-ts is a reference implementation of the Sensor API™ written in TypeScript, based on the caliper-js library.

NOTE: See this page for the different RAD Tailpipe service receiver endpoints https://weldnorthed.atlassian.net/wiki/spaces/TECH/pages/1242202248/RAD+Tailpipe+Endpoints

NOTE: See this page for the different Deadletter service receiver endpoints https://weldnorthed.atlassian.net/wiki/spaces/TECH/pages/131270774660/RAD+Pipeline+Service+Endpoints

NOTE: See this page for the official Caliper Specification from IMS Global https://www.imsglobal.org/spec/caliper/v1p2

Installation

The caliper-ts package is available on GitHub Package Registry. To install it, you will need to configure your project by adding a .npmrc file to the project root with the following content:

@imaginelearning:registry=https://npm.pkg.github.com

You can then install it using npm or yarn.

npm install @imaginelearning/caliper-ts

Or

yarn add @imaginelearning/caliper-ts

Caliper vocabulary

The Caliper Analytics® Specification defines a set of concepts, relationships and rules for describing learning activities. Each activity domain modeled is described in a profile. Each profile is composed of one or more Event types (e.g., AssessmentEvent, NavigationEvent). Each Event type is associated with a set of actions undertaken by learners, instructors, and others. Various Entity types representing people, groups, and resources are provided in order to better describe both the relationships established between participating entities and the contextual elements relevant to the interaction (e.g., Assessment, Attempt, CourseSection, Person).

Usage

caliper-ts provides a number of classes and factory functions to facilitate working with the Sensor API in a consistent way. Below is a basic example of configuring a sensor and sending an event, as well as more in-depth documentation of the various classes, factories, and utility functions.

Basic example

// Set application URI if using DLQ
Caliper.settings.applicationUri = 'https://example.org';

// Initialize Caliper sensor
const sensor = new Sensor('http://example.org/sensors/1');

// Initialize and register client
const client = httpClient(
	'http://example.org/sensors/1/clients/2',
	'https://example.edu/caliper/target/endpoint',
	'40dI6P62Q_qrWxpTk95z8w',
	'https://dlq.rad.dev.edgenuityapp.com/api/DeadletterMessage'
);
sensor.registerClient(client);

// Set Event property values
// Note: only actor and object property assignments shown
const actor = createPerson({ id: 'https://example.edu/users/554433' });
const object = createAssessment({
	id: 'https://example.edu/terms/201801/courses/7/sections/1/assess/1',
	dateToStartOn: getFormattedDateTime('2018-08-16T05:00:00.000Z'),
	dateToSubmit: getFormattedDateTime('2018-09-28T11:59:59.000Z'),
	maxAttempts: 1,
	maxScore: 25.0,
	// ... add additional optional property assignments
});

// ... Use the entity factories to mint additional entity values.
const membership = createMembership({
	// ...
});
const session = createSession({
	// ...
});

// Create Event
const event = sensor.createEvent(createAssessmentEvent, {
	actor,
	action: Action.Started,
	object,
	membership,
	session,
});

// ... Create additional events and/or entity describes.

// Create envelope with data payload
const envelope = sensor.createEnvelope({
	data: [
		event,
		// ... add additional events and/or entity describes
	],
});

// Delegate transmission responsibilities to client
sensor.sendToClient(client, envelope);

Sensor class

The Sensor class manages clients for interacting with a Sensor API, as well as providing a helper function for creating properly formatted Envelope objects for transmitting Caliper events.

Constructor: new Sensor(id: string, config?: SensorConfig)

Creates a new instance of a Sensor with the specified ID. Optionally takes a SensorConfig object which can provide the SoftwareApplication to include in events, a flag to enable/disable event validation, and a Record of objects that implement the Client interface, as an alternative to using the Sensor.registerClient function.

// Set application URI if using DLQ
Caliper.settings.applicationUri = 'https://example.org';

const sensor1 = new Sensor('http://example.org/sensors/1');

// With SensorConfig
const sensor2 = new Sensor('http://example.org/sensors/2', {
	edApp: createSoftwareApplication({ id: 'https://example.org' }),
	validationEnabled: true,
});

// With SensorConfig including HttpClients
const client = httpClient(
	'http://example.org/sensors/1/clients/2',
	'https://example.edu/caliper/target/endpoint',
	'40dI6P62Q_qrWxpTk95z8w',
	'https://dlq.rad.dev.edgenuityapp.com/api/DeadletterMessage'
);
const sensor3 = new Sensor('http://example.org/sensors/3', {
	edApp: createSoftwareApplication({ id: 'https://example.org' }),
	validationEnabled: true,
	clients: {
		[client.getId()]: client,
	},
});

Sensor.createEnvelope<T>(opts: EnvelopeOptions<T>): Envelope<T>

Creates a new Envelope object with the specified options, where the data field is an array of type T.

EnvelopeOptions<T> contains the following properties:

  • sensor: string: ID of the sensor
  • sendTime?: string: ISO 8601 formatted date with time (defaults to current date and time)
  • dataVersion?: string: Version of the Caliper context being used (defaults to http://purl.imsglobal.org/ctx/caliper/v1p1)
  • data?: T | T[]: Object(s) to be transmitted in the envelope, typically an Event, Entity, or combination.
const data = sensor.createEvent(createSessionEvent, {
	// See documentation on creating events
});
const envelope = sensor.createEnvelope<SessionEvent>({ data });
console.log(envelope);
/* => {
  sensor: 'http://example.org/sensors/1',
  sendTime: '2020-09-09T21:47:01.959Z',
  dataVersion: 'http://purl.imsglobal.org/ctx/caliper/v1p1',
  data: [
    {
      "@context": "http://purl.imsglobal.org/ctx/caliper/v1p1",
      "id": "urn:uuid:fcd495d0-3740-4298-9bec-1154571dc211",
      "type": "SessionEvent",
      ...
    }
  ]
}
*/

`Sensor.createEvent<TEvent extends Event, TParams>(eventFactory: (params: TParams, edApp?: SoftwareApplication) => TEvent, params: TParams): TEvent

Creates a new event of type TEvent using the provided factory function and the SoftwareApplication object from the Sensor instance.

const client = httpClient(
	'http://example.org/sensors/1/clients/1',
	'https://example.edu/caliper/target/endpoint',
	'40dI6P62Q_qrWxpTk95z8w',
	'https://dlq.rad.dev.edgenuityapp.com/api/DeadletterMessage'
);
const sensor = new Sensor('http://example.org/sensors/1', {
	edApp: createSoftwareApplication({ id: 'https://example.org' }),
	validationEnabled: true,
	clients: {
		[client.getId()]: client,
	},
});

const event = sensor.createEvent(createAssessmentEvent, {
	// ... data for AssessmentEventParams
});
console.log(event);
/* => {
	type: 'AssessmentEvent',
	'@context': ['http://purl.imsglobal.org/ctx/caliper/v1p2'],
	edApp: {
		id: 'https://example.org,
		type: 'SoftwareApplication'
	}
	...
}
*/

Sensor.getClient(id: string): Client

Returns the Client instance registered under the specified ID.

Sensor.getClients(): Client[]

Returns an array containing all registered Client instances.

Sensor.getId(): string

Returns the ID of the current Sensor instance.

Sensor.registerClient(client: Client): void

Adds the specified Client to the Sensor instance's collection of registered clients.

sensor.registerClient(
	httpClient(
		'http://example.org/sensors/1/clients/2',
		'https://example.edu/caliper/target/endpoint'
	)
);

Sensor.sendToClient<TEnvelope, TResponse>(client: Client | string, envelope: Envelope<T>): Promise<TResponse>

Sends the specified Envelope via the specified Client. Returns Promise<TResponse> that resolves when the HTTP request has completed.

// Register HttpClient with Sensor
const client = httpClient(
	'http://example.org/sensors/1/clients/2',
	'https://example.edu/caliper/target/endpoint'
);
sensor.registerClient(client);

// Create Envelope
const envelope = sensor.createEnvelope<SessionEvent>({ data });

// Send via client by reference
sensor.sendToClient<SessionEvent, { success: boolean }>(client, envelope).then((response) => {
	console.log(response);
	// => { success: true }
});

// Or send via client by ID
sensor
	.sendToClient<SessionEvent, { success: boolean }>(
		'http://example.org/sensors/1/clients/2',
		envelope
	)
	.then((response) => {
		console.log(response);
		// => { success: true }
	});

Sensor.sendToClients<TEnvelope, TResponse>(envelope: Envelope<TEnvelope>): Promise<TResponse[]>

Sends the specified Envelope via all registered HttpClient instances. Returns Promise<TResponse[]> that resolves when all HTTP requests have completed.

// Register clients
const client1 = httpClient(
	'http://example.org/sensors/1/clients/1',
	'https://example.edu/caliper/target/endpoint1'
);
sensor.registerClient(client1);

const client2 = httpClient(
	'http://example.org/sensors/1/clients/2',
	'https://example.edu/caliper/target/endpoint2'
);
sensor.registerClient(client2);

// Create Envelope
const envelope = sensor.createEnvelope<SessionEvent>({ data });

// Sends posts envelope to both endpoints
sensor.sendToClients<SessionEvent, { success: boolean }>(envelope).then((response) => {
	console.log(response);
	// => [{ success: true }, { success: true }]
});

Sensor.unregisterClient(id: string): void

Removes the Client instance with the specified ID from the Sensor instance's collection of registered clients.

Client interface

The Client interface defines the required functionality for posting HTTP requests to a Sensor API. Any object that implements the Client interface can be registered with the Sensor as a client. For convenience, caliper-ts includes an HttpClient class which implements the Client interface using the Fetch API. However, using the Client interface you can implement your own client using your preferred method for making HTTP requests.

The Client interface requires the following functions in the implementing class:

  • getId(): string: Returns the ID of the client.
  • send<TEnvelope, TResponse>(envelope: Envelope<TEnvelope>): Promise<TResponse>: Makes a POST request to a Sensor API endpoint with the specified Envelope as the payload. Returns a promise that resolves with the response from the endpoint. This function should also ensure that the appropriate authorization header is included with the request.

HttpClient class

The HttpClient is a complete implementation of the Client interface using the Fetch API. Depending on what browsers you need to support for your application, you may need to include an appropriate polyfill, such as whatwg-fetch. Each HttpClient is configured for a single endpoint, but multiple clients can be registered with a single sensor.

httpClient(id: string, uri: string, token?: string): HttpClient

This factory function returns a new instance of the HttpClient class, configured with the specified ID, URI, and optional access token.

// Create HttpClient that will post to https://example.edu/caliper/target/endpoint
// and include the header `Authorization: Bearer 40dI6P62Q_qrWxpTk95z8w`
const client = httpClient(
	'http://example.org/sensors/1/clients/2',
	'https://example.edu/caliper/target/endpoint',
	'40dI6P62Q_qrWxpTk95z8w'
);

HttpClient.bearer(token?: string): HttpClient

Returns a new instance of HttpClient configured to include the specified bearer token in the Authorization header for any request sent with the send function.

// Create HttpClient that will post to https://example.edu/caliper/target/endpoint
// and configure to include the header `Authorization: Bearer 40dI6P62Q_qrWxpTk95z8w`
const client = httpClient(
	'http://example.org/sensors/1/clients/2',
	'https://example.edu/caliper/target/endpoint'
).bearer('40dI6P62Q_qrWxpTk95z8w');

HttpClient.getId(): string

Returns the ID of the client.

const client = httpClient(
	'http://example.org/sensors/1/clients/2',
	'https://example.edu/caliper/target/endpoint'
);
const id = client.getId();
console.log(id);
// => "http://example.org/sensors/1/clients/2"

HttpClient.send<TEnvelope, TResponse>(envelope: TEnvelope): Promise<TResponse>

Makes a POST request to the configured Sensor API endpoint with the specified Envelope as the payload. It includes the Authorization header in the request if the client has been configured with a bearer token. Returns a promise that resolves with the parsed JSON response.

const envelope = sensor.createEnvelope<SessionEvent>({ data });
const client = httpClient(
	'http://example.org/sensors/1/clients/2',
	'https://example.edu/caliper/target/endpoint2'
);

client.send<Envelope<SessionEvent>, { success: boolean }>(envelope).then((result) => {
	console.log(result);
	// => { "success": true }
});

Note: The send function is called by the Sensor via the sendToClient and sendToClients functions. You would not invoke the send function directly in a typical application.

Entity factory functions

Caliper entities can be created through factory functions provided by the caliper-ts-models library. Each factory function takes a single parameters: a delegate, which is an object defining values for properties to be set in the entity (see the Entity Subtypes section of the Caliper Spec).

const assessment = createAssessment({
	dateCreated: '2016-08-01T06:00:00.000Z',
	dateModified: '2016-09-02T11:30:00.000Z',
	datePublished: '2016-08-15T09:30:00.000Z',
	dateToActivate: '2016-08-16T05:00:00.000Z',
	dateToShow: '2016-08-16T05:00:00.000Z',
	dateToStartOn: '2016-08-16T05:00:00.000Z',
	dateToSubmit: '2016-09-28T11:59:59.000Z',
	id: 'https://example.edu/terms/201601/courses/7/sections/1/assess/1',
	items: [
		AssessmentItem({
			id: 'https://example.edu/terms/201601/courses/7/sections/1/assess/1/items/1',
		}),
		AssessmentItem({
			id: 'https://example.edu/terms/201601/courses/7/sections/1/assess/1/items/2',
		}),
		AssessmentItem({
			id: 'https://example.edu/terms/201601/courses/7/sections/1/assess/1/items/3',
		}),
	],
	maxAttempts: 2,
	maxScore: 15,
	maxSubmits: 2,
	name: 'Quiz One',
	version: '1.0',
});
console.log(assessment);
/* => {
  "@context": "http://purl.imsglobal.org/ctx/caliper/v1p1",
  "id": "https://example.edu/terms/201601/courses/7/sections/1/assess/1",
  "type": "Assessment",
  "name": "Quiz One",
  "items": [
    {
      "id": "https://example.edu/terms/201601/courses/7/sections/1/assess/1/items/1",
      "type": "AssessmentItem"
    },
    {
      "id": "https://example.edu/terms/201601/courses/7/sections/1/assess/1/items/2",
      "type": "AssessmentItem"
    },
    {
      "id": "https://example.edu/terms/201601/courses/7/sections/1/assess/1/items/3",
      "type": "AssessmentItem"
    }
  ],
  "dateCreated": "2016-08-01T06:00:00.000Z",
  "dateModified": "2016-09-02T11:30:00.000Z",
  "datePublished": "2016-08-15T09:30:00.000Z",
  "dateToActivate": "2016-08-16T05:00:00.000Z",
  "dateToShow": "2016-08-16T05:00:00.000Z",
  "dateToStartOn": "2016-08-16T05:00:00.000Z",
  "dateToSubmit": "2016-09-28T11:59:59.000Z",
  "maxAttempts": 2,
  "maxScore": 15.0,
  "maxSubmits": 2,
  "version": "1.0"
}
*/

Event factory functions

Caliper events can be created through factory functions. Each factory function takes two parameters: 1) a delegate, which is an object defining values for properties to be set in the event (see the Event Subtypes section of the Caliper Spec), and 2) an optional SoftwareApplication object to use for populating the edApp property in the event.

The recommended way to create events is to use the createEvent function on the Sensor object. This function takes the factory function and delegate object as parameters, and automatically passes the SoftwareApplication object from the Sensor instance to the factory function.

const sessionEvent = sensor.createEvent(createSessionEvent, {
	action: Action.LoggedIn,
	actor: createPerson({ id: 'https://example.edu/users/554433' }),
	object: createSoftwareApplication({ id: 'https://example.edu', version: 'v2' }),
	session: createSession({
		dateCreated: '2016-11-15T10:00:00.000Z',
		id: 'https://example.edu/sessions/1f6442a482de72ea6ad134943812bff564a76259',
		startedAtTime: '2016-11-15T10:00:00.000Z',
		user: 'https://example.edu/users/554433',
	}),
});
console.log(sessionEvent);
/* => {
  "@context": "http://purl.imsglobal.org/ctx/caliper/v1p1",
  "id": "urn:uuid:fcd495d0-3740-4298-9bec-1154571dc211",
  "type": "SessionEvent",
  "actor": {
    "id": "https://example.edu/users/554433",
    "type": "Person"
  },
  "action": "LoggedIn",
  "object": {
    "id": "https://example.edu",
    "type": "SoftwareApplication",
    "version": "v2"
  },
  "eventTime": "2016-11-15T10:15:00.000Z",
  "edApp": {
    "id": "https://example.edu",
    "type": "SoftwareApplication"
  },
  "session": {
    "id": "https://example.edu/sessions/1f6442a482de72ea6ad134943812bff564a76259",
    "type": "Session",
    "user": "https://example.edu/users/554433",
    "dateCreated": "2016-11-15T10:00:00.000Z",
    "startedAtTime": "2016-11-15T10:00:00.000Z"
  }
}
*/

Utility functions

There are a handful of utility functions provided for convenience in properly formatting dates and IDs.

getFormattedDateTime(date?: Date | number | string): string

Takes an optional Date object, number (Unix timestamp), or string and returns a properly formatted ISO-8601 date and time string. If no parameter is specified, it uses the current date and time.

const date = getFormattedDateTime('9/2/2020, 6:00:00 AM');
console.log(date);
// => "2020-09-02T12:00:00.000Z"

getFormattedDuration(startedAtTime: Date | string, endedAtTime: Date | string): string

Takes start and end Date objects or strings, calculates the duration between the specified dates, and returns a properly formatted ISO-8601 duration string.

const duration = getFormattedDuration('1969-07-20T02:56:00+0000', '1969-07-21T17:54:00+0000');
console.log(duration);
// => "P0Y0M1DT14H58M0S"

getFormattedUrn(urn: URN): string

Takes a URN object which consists of a namespace ID (nid) and namespace-specific string (nss) and formats it as a URN string.

const urn = getFormattedUrn({ nid: 'WNE', nss: 'GUID_OF_AWESOMENESS' });
console.log(urn);
// => "urn:wne:guid_of_awesomeness"

getFormattedUrnUuid(uuid?: string): string

Takes an optional UUID and formats it as a URN according to RFC-4122. If no UUID is provided, a v4 UUID will be generated with uuid.

const urn = getFormattedUrnUuid('ff9ec22a-fc59-4ae1-ae8d-2c9463ee2f8f');
console.log(urn);
// => "urn:uuid:ff9ec22a-fc59-4ae1-ae8d-2c9463ee2f8f"

Development

Dependencies

Dependencies in this project are managed with Yarn.You can install dependencies by running the following command in the project's root directory:

yarn

Commands

yarn build

Builds the caliper-ts library.

yarn test

Runs Jest with the --watch flag.

yarn test:ci

Runs Jest in CI mode.

yarn lint

Runs ESLint in the project.

Configuration

Code quality is set up for you with eslint using the using the @imaginelearning/eslint-config/base configuration, prettier using the @imaginelearning/prettier-config configuration, husky, and lint-staged.

Rollup

TSDX uses Rollup as a bundler and generates multiple rollup configs for various module formats and build settings. See Optimizations for details.

TypeScript

tsconfig.json is set up to interpret dom and esnext types, as well as react for jsx. Adjust according to your needs.

Optimizations

Please see the main tsdx optimizations docs. In particular, know that you can take advantage of development-only optimizations:

// ./types/index.d.ts
declare var __DEV__: boolean;

// inside your code...
if (__DEV__) {
	console.log('foo');
}

You can also choose to install and use invariant and warning functions.

Module formats

CJS, ESModules, and UMD module formats are supported.

The appropriate paths are configured in package.json and dist/index.js accordingly. Please report if any issues are found.

Named exports

Per Palmer Group guidelines, always use named exports. Code split inside your app instead of your library.

Code generation

This repository contains events and entities that are generated with the caliper-code-generator using caliper-net as the source of truth.

About

TypeScript implementation of the IMSGlobal/caliper-js library

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published