File:LearningAnalytics.js

/**
 * @module Container
 * @namespace springroll.pbskids
 */
(function()
{
	// Import classes
	var UUID;
	var BufferedQueue;
	var Platform;
	var Identity;

	/**
	 * A module that consumers can use to expose Super Vision kid labels to this plugin. Basically, the 
	 * "PBS.KIDS.CurrentKidLabel" (if it exists), should be an object with a get and set method to allow "setters" to
	 * update the value on the fly, so that this plugin can always get the latest value when needed
	 *
	 * @var {Object} CurrentKidLabel
	 */
	var CurrentKidLabel;

	/**
	 * A module that consumers can use to expose the Super Vision channel id to this plugin. Basically, the
	 * "PBS.KIDS.CurrentChannelId" (if it exists), should be an object witha get and set method to allow "setters" to
	 * update the value on the fly, so that this plugin can always get the latest value when needed
	 *
	 * @var {Object} CurrentChannelId
	 */
	var CurrentChannelId;

	/**
	 * A module that consumers can use to expose the a content origin to this plugin. Basically, the
	 * "PBS.KIDS.ContentOrigin" (if it exists), should be an object with a get and set method to allow "setters" to
	 * update the value on the fly, so that this plugin can always get the latest value when needed. For instance, a value
	 * might look like "org.pbskids.measureup", or "org.pbskids.gamesapp" to distinguish *where* the event came from. As
	 * more Springroll games get embedded in multiple places, this will be valuable when wanting to filter those events
	 * based on *where* the game was played (Measure Up vs. pbskids.org vs. Games vs. whatever)
	 *
	 * @var {Object} ContentOrigin
	 */
	var ContentOrigin;

	/**
	 * Handle the Learning Analytics events
	 * @class LearningAnalytics
	 * @constructor
	 * @param {string} [domain='http://progresstracker.pbskids.org'] The domain for the end-point
	 *        this should only be set for mobile devices or testing otherwise
	 *        it uses a root-relative end-point and will use the progresstracker
	 *        on the current domain
	 * @param {string} [resource='game'] The type of resource
	 */
	var LearningAnalytics = function(domain, resource)
	{
		if (UUID === undefined)
		{
			UUID = include('springroll.pbskids.UUID');
			BufferedQueue = include('springroll.pbskids.BufferedQueue');
			Platform = include('springroll.pbskids.Platform');
			Identity = include('PBS.KIDS.identity', false);
			CurrentKidLabel = include('PBS.KIDS.CurrentKidLabel', false);
			CurrentChannelId = include('PBS.KIDS.CurrentChannelId', false);
			ContentOrigin = include('PBS.KIDS.ContentOrigin', false);
		}

		// Param defaults
		resource = resource || 'game';
		domain = domain || 'http://progresstracker.pbskids.org:8000';

		// Determine the service path to use
		var servicePath = SERVICE_PATHS.v1;

		/**
		 * The valid event keys for the resource type
		 * @property {array} eventKeys
		 * @private
		 */
		this.eventKeys = EVENT_KEYS.v1;

		// Upgrade to version 2 of the API
		if (resource && !!SERVICE_PATHS.v2[resource])
		{
			servicePath = SERVICE_PATHS.v2[resource];
			this.eventKeys = EVENT_KEYS.v2[resource];
		}

		/**
		 * Queuing object for the event buffer
		 * @property {pbskids.BufferedQueue} queue
		 */
		this.queue = new BufferedQueue(
			BATCH_SIZE,
			KEY_PREFIX,
			domain + servicePath
		);

		/**
		 * For getting platform details
		 * @property {pbskids.Platform} platform
		 */
		this.platform = new Platform();

		/**
		 * This variable contains the Session Id for current play session.
		 * @property {string} sessionId
		 */
		this.sessionId = null;

		/**
		 * The setInterval ping to check online status
		 * @property {int} _timer
		 * @private
		 */
		this._timer = null;

		/**
		 * Internal enabled boolean
		 * @property {Boolean} _enabled
		 * @private
		 * @default true
		 */
		this._enabled = false;

		// Create a new session id
		this.resetSessionId();

		// Enable the tracker by default
		this.enabled = false;
	};

	/**
	 *  The prefix for the event data
	 *  @property {string} KEY_PREFIX
	 *  @private
	 *  @default 'PBS_event_'
	 *  @static
	 *  @readOnly
	 */
	var KEY_PREFIX = 'PBS_event_';

	/**
	 *  The root-relative service URL pathing
	 *  @property {object} SERVICE_PATHS
	 *  @private
	 *  @static
	 *  @readOnly
	 */
	var SERVICE_PATHS = {
		v2:
		{
			video: '/progresstracker/api/v2/videos/events.json',
			game: '/progresstracker/api/v2/games/events.json'
		},
		v1: '/progresstracker/api/v1/rawevents.json'
	};

	/**
	 *  The batch size of the queue
	 *  @property {int} BATCH_SIZE
	 *  @private
	 *  @default 5
	 *  @static
	 *  @readOnly
	 */
	var BATCH_SIZE = 5;

	/**
	 *  JSON object keys for validation
	 *  @property {object} EVENT_KEYS
	 *  @private
	 *  @static
	 *  @readOnly
	 */
	var EVENT_KEYS = {
		v2:
		{
			game: [
				'timestamp',
				'user_ids',
				'game_id',
				'device_id',
				'platform_id',
				'event_id',
				'event_data',
				'game_session'
			],
			video: [
				'timestamp',
				'user_ids',
				'video_id',
				'device_id',
				'platform_id',
				'event_id',
				'event_data',
				'video_session'
			]
		},
		v1: [
			'timestamp',
			'user_ids',
			'game_id',
			'device_id',
			'platform_id',
			'event_id',
			'event_data',
			'game_session'
		]
	};

	// Reference to the prototype
	var p = LearningAnalytics.prototype;

	/**
	 * Reset the current session id
	 * @method resetSessionId
	 */
	p.resetSessionId = function()
	{
		this.sessionId = UUID.genV4().hexString + UUID.genV4().hexString;
	};

	/**
	 * End the current queue
	 * @method end
	 */
	p.end = function()
	{
		this.queue.end();
	};

	/**
	 * If the queueing should autoflush
	 * @property {boolean} autoFlush
	 */
	p.setAutoFlush = function(autoFlush)
	{
		this.queue.setAutoFlush(autoFlush);
	};

	/**
	 * Check the online status internally on a timer
	 * Auto calling function to keep updating online staus
	 * @method checkStatus
	 * @private
	 */
	p.checkStatus = function()
	{
		this.queue.setOnline(navigator.onLine);
	};

	/**
	 * If the tracking is enabled
	 * @property {boolean} enabled
	 * @default true
	 */
	Object.defineProperty(p, 'enabled',
	{
		get: function()
		{
			return this._enabled;
		},
		set: function(enabled)
		{
			this._enabled = enabled;

			if (this._timer)
			{
				clearInterval(this._timer);
				this._timer = null;
			}

			if (enabled)
			{
				this._timer = setInterval(this.checkStatus.bind(this), 10000);
				this.queue.enable();
			}
			else
			{
				this.queue.disable();
			}
		}
	});

	/**
	 * Flush the queue
	 * @method flushAll
	 */
	p.flushAll = function()
	{
		this.queue.flushAll();
	};

	/**
	 * Insert Game SessionId in JSON Data
	 * @method appendSessionId
	 * @private
	 * @param {object} eventData The event object
	 * @return {LearningAnalytics} Instance for chaining
	 */
	p.appendSessionId = function(eventData)
	{
		eventData.game_session = this.sessionId;
		return this;
	};

	/**
	 * Insert platform in JSON Data
	 * @method appendPlatformDetails
	 * @private
	 * @param {object} eventData The event object data
	 * @return {LearningAnalytics} Instance for chaining
	 */
	p.appendPlatformDetails = function(eventData)
	{
		eventData.platform_id = this.platform.browser;
		eventData.device_id = this.platform.OS;
		return this;
	};

	/**
	 * Insert user information in JSON Data
	 * @method appendUserData
	 * @private
	 * @param {object} eventData The event object data
	 * @return {LearningAnalytics} Instance for chaining
	 */
	p.appendUserData = function(eventData)
	{
		if (Identity)
		{
			eventData.user_ids = [];
			var users = Identity.getCurrentUsers();
			for (var i = 0; i < users.length; i++)
			{
				eventData.user_ids.push(users[i].userid);
				eventData.is_logged_in = users[i].isloggedin;
			}
		}

		// If clients have defined a "PBS.KIDS.CurrentKidLabel" module, we'll also include the kid label there as well
		if (CurrentKidLabel instanceof Object)
		{
			eventData.kid_label_guid = CurrentKidLabel.get();
		}

		return this;
	};

	/**
	 * Adds the current super vision channel id to the event JSON data
	 * @method appendChannelId
	 * @private
	 * @param {object} eventData The event object data
	 * @return {LearningAnalytics} Instance for chaining
	 */
	p.appendChannelId = function(eventData)
	{
		// If clients have defined a "PBS.KIDS.CurrentChannelId" module, we'll also include the channel id as well
		if (CurrentChannelId instanceof Object)
		{
			eventData.channel_id = CurrentChannelId.get();
		}

		return this;
	};

	/**
	 * Adds the current Super Vision content origin to the event JSON data
	 * @method appendContentOrigin
	 * @private
	 * @param {Object} eventData The event object data
	 * @return {LearningAnalytics} Instance for chaining
	 */
	p.appendContentOrigin = function(eventData)
	{
		// If clients have defined a content origin, we'll attach it
		if (ContentOrigin instanceof Object)
		{
			eventData.content_origin = ContentOrigin.get();
		}

		return this;
	};

	/**
	 * Add the timestamp to the event JSON data
	 * @method appendTimeStamp
	 * @private
	 * @param {object} eventData The event object data
	 * @return {LearningAnalytics} Instance for chaining
	 */
	p.appendTimeStamp = function(eventData)
	{
		var da = new Date();
		eventData.timestamp = da.getTime();
		return this;
	};

	/**
	 * Push a learning event
	 * @method pushEvent
	 * @param {object} eventData The event object data
	 * @param {string} eventData.game_id The GUID for the game
	 * @param {string} eventData.event_id The GUID for the event
	 * @param {object} eventData.event_data The data for event
	 * @return {object} The event data with any appended data
	 */
	p.pushEvent = function(eventData)
	{
		// Ignore if we aren't enabled
		if (!this._enabled)
		{
			return this;
		}

		if (eventData.user_ids === undefined)
		{
			eventData.user_ids = [];
		}

		eventData.is_logged_in = false;

		this.appendTimeStamp(eventData)
			.appendSessionId(eventData)
			.appendPlatformDetails(eventData)
			.appendUserData(eventData)
			.appendChannelId(eventData)
			.appendContentOrigin(eventData);

		if (this.validateEvent(eventData))
		{
			this.queue.pushEvent(eventData);
		}
		return eventData;
	};

	/**
	 * Validate Event JSON data
	 * @method validateEvent
	 * @param {object} eventData
	 * @private
	 * @return {Boolean} if the event is valid
	 */
	p.validateEvent = function(eventData)
	{
		for (var i = 0; i < this.eventKeys.length; i++)
		{
			if (!validateKey(eventData[this.eventKeys[i]]))
			{
				return false;
			}
		}
		return true;
	};

	/**
	 * Validate event data keys from EVENT_KEYS
	 * @method validateKaye
	 * @private
	 * @param {object} key Validate
	 * @return {boolean} If the key is valid
	 */
	var validateKey = function(key)
	{
		return !(key === null || key == 'undefined' || key === '');
	};

	/**
	 * Cleanup and don't use after this
	 * @method destroy
	 */
	p.destroy = function()
	{
		this.enabled = false;
		this.queue.destroy();
		this.queue = null;

		this.platform = null;
	};

	// Assign to namespace
	namespace('springroll.pbskids').LearningAnalytics = LearningAnalytics;

}());