Posted by Brian Miller
Posted 22 August 2013
In the other posts in this series I’ve covered some big topics with a post each, but not all properties of Tin Can statements need quite so much attention. The Actor-Verb-Object pattern commonly pointed out as the basis of Tin Can statements is tremendous for deriving meaning from an experience, but there are several top-level properties that are specifically geared towards the mechanics needed to make the Tin Can API function well. Though they usually get less attention, particularly from non-developers, it is the combination of all of the properties that makes the specification such an achievement.
This list of non-elegant, but absolutely critical properties includes ‘id’, ‘timestamp’, ‘stored’, ‘authority’, and ‘version’. All but one have a “simple” value (meaning non-object), and enable specific usages, but also come with their own unique quirks.
The ‘id’ property stores a simple string value, and that string must be a UUID (aka GUID). The short version is that a UUID is a Universally Unique Identifier, and it is the very strict meaning of “universally” that matters in the Tin Can API specification. For Tin Can API statements to provide interoperability they first have to be transferrable from one LRS to another, to make that possible the identifier of a statement can’t be unique to only one system; therefore it must have a universally unique identifier, or an identifier that will never collide with one generated by some other system, whether that system is an LRS or an Activity Provider. How to generate UUIDs is beyond the scope of this post, but most programming languages have commonly available libraries, built in types, or at least sample algorithms for generating them efficiently. An example UUID looks like:
All statements stored in an LRS must have an ‘id.’ The specification purposely leaves it up to the Activity Provider to decide if they want to generate statements with pre-set IDs, though indicates it is a best practice to do so. However, if the Activity Provider does not send an ‘id’ property as part of a statement one will be created and assigned by the LRS. While the above looks like just a series of dash delimited numbers and letters generating a UUID must follow a particular algorithm, it’s critical to generate proper UUIDs to avoid collisions. While creating your own is recommended, it’s much better to let the LRS assign a good UUID than to generate them improperly (they should always be computer generated, not manually created).
Statement IDs are useful for retrieving specific statements, for instance by systems that implement favoriting or perhaps badges, via the LRS Statement API which takes the ‘id’ as a query parameter. Additionally, as mentioned in “Deep Dive: object” the ‘id’ property is necessary for leveraging Statement References, such as when voiding a statement.
The ‘timestamp’ property’s value is an ISO8601 date+time value in a string format that indicates when the statement was created and whose intention is to capture when the experience occurred. Like with the ‘id’, the ‘timestamp’ value will be set by the LRS if not set in the statement by the Activity Provider. Here is an example timestamp value:
Note that ‘timestamp’ values have sub-second precision and must contain a timezone, so implementing systems should be prepared to deal with these (the ‘Z’ above represents UTC, time zones are well beyond the scope of this post). It is intended that ‘timestamp’ values represent either a past time or the current (soon to be past) time. The specification does specifically call out the case of future timestamp values as being useful in SubStatements where it is expected that a related statement will be sent at or near that future time. Naturally when that occurs, both will then be in the past. The notion of timestamps in the past matches up well with the expectation that verbs use past tense and that statements can only capture what has occurred.
The ‘timestamp’ property is one of the mechanical items that allows Tin Can to function in an offline mode. When offline, statements can be created with an accurate ‘timestamp’ value even though they will not reach the LRS immediately. Reporting tools can then leverage the ‘timestamp’ to properly order what occurred. This also helps facilitate queuing mechanisms that may be online, but want to batch report statements for performance reasons. In a similar sense, it is also possible to capture historical events that happened well before the specification existed, for instance I could capture a set of learning records for my school years based on offline records that I’ve kept. The lack of accuracy of the timestamps should be considered, but I could certainly capture experiences close to when they occurred. For instance based on various records I could create a statement for my college graduation at “1999-06-12T14:00:00-04”, the date is accurate, the time zone is accurate, the time itself is a little rough but may be meaningless in a timeline spanning decades.
Timestamps can also help provide meaning in statements. In some cases the ‘timestamp’ value could be sufficient to determine unique context for a given experience. For instance a timestamp in a statement about a conference may imply conference attendance for the given date. When used in conjunction with the ‘duration’ property of the Result object (a future blog post topic) a reporting system can determine overlapping experiences and have another dimension for comparison.
Along with ‘timestamp’, the ‘stored’ property’s value is an ISO8601 date+time value, but has a very different meaning. The ‘stored’ property’s value is purely about the mechanics of the API and as such is set by the LRS when that LRS receives the statement. The ‘stored’ value is then leveraged via the Statement API’s query resource for providing the statement stream in one specific order, and optionally including only a range of statements. For instance a system may periodically poll an LRS for only new statements since a specific point in time using the ‘since’ query parameter. Alternatively, the ‘until’ query parameter allows for requesting statements stored before a point in time. It is important to understand that the ‘stored’ value for a given statement retrieved from two different learning record stores may be different.
The ‘authority’ property is another that will most often be set by the LRS, but has an object value. Specifically, the ‘authority’ will contain an Agent or Group object which I covered in the “Deep Dive: Agent/actor” post. When the statement is stored using 3-legged OAuth, the ‘authority’ will contain a non-identified Group with two members, one for the user and one for the application, otherwise it will hold an Agent representing the user connecting to the LRS.
The authority represents how that statement ended up in the LRS and correspondingly suggests the level of trust of that statement. The level of trust of a statement is directly related to the level of trust of the authority, and the level of trust of the authority is relative; it is the level of trust you have in that authority, and therefore the level of trust you have in a statement. A statement where the ‘actor’ and ‘authority’ match, for instance, has the lowest level of trust as it was self generated. A statement where the ‘authority’ is a single Agent but different from the ‘actor’ will often have a higher level of trust, assuming that the authorizing Agent is trusted. The level of trust of a particular Agent can vary based on how hard it is to assume control of that Agent. For instance, an automated system with significant security controls will likely have a higher degree of trust than a normal user accessing an LRS through a public web interface. The 3-legged OAuth method should provide the most trust because it is based on two parties both agreeing that a statement should be generated, in this case the user has agreed to let the application speak for them and that the application indicates that the experience has occurred.
While the handling of the ‘authority’ property is fairly mechanical in nature, it is the notion of trust that lends to taking meaning from a statement, and as extension how someone will act on the information conveyed by that statement. For example, a pile of statements created by an accredited university may carry more weight in a job interview than those generated by a bookmarklet, or statements generated by a heart monitor machine in a hospital room will be trusted over those generated by manually entering pulse information in a phone app when diagnosing heart conditions.
The ‘version’ property is one of the newest additions to the Tin Can Statement specification. It indicates what version of the API was in use when the statement was recorded, and can be leveraged by systems consuming the statement stream to properly parse and otherwise handle the statement structures without having to make assumptions about the statement version from its structure. In general this property will be set by the LRS, reserving pre-setting of its value for LRS to LRS transfers. Because this property didn’t exist until the 1.0.0 specification, statements retrieved from an LRS using the prior draft specifications will not include this property.
While talking in generalities about the Tin Can API specification, and specifically the Statement structure, it is easy to lose sight of the fact that the meaning we so desperately want to capture has to be codified in actual data elements. But the data elements most often conveyed via “I did X” aren’t sufficient to developers building real systems, particularly interoperable ones. The essential components for meaning combined with the more mechanical ones outlined here make a well rounded specification that can work for both implementers and users (Activity Providers) of learning record stores and the associated web service resources.
Go now, make statements!