blob: b90ceb16dee0e234002eab7dbe49b999cfa4c1fc [file] [log] [blame]
/*======================================================================
*======================JAVASCRIPT ACTIVITY LOGGER=======================
*=====================Draper Laboratory, June 2013======================
*
* This library is intended for use for javascrip software component and
* webapp developers implemnting the XDATA Activity Logging API. To send
* activity log messages using this libary, components must:
* 1) Instantiate an ActivityLogger object
* 2) Call registerActivityLogger(...) to pass in required networking
* and version information.
* 3) Call one of the logging functions:
* logSystemActivity(...)
* logUser(...)
* logUILayout(...)
*
* An example use of this library is included below:
<script src="ActivityLogger.js"></script>
<script>
//Instantiate the Activity Logger
var ac = new activityLogger();
//Register the logger.
//In this case, we register the logger to look for the logging server
//on port 1337 of the machine "localhost". The component name is
//left blank, so it will default to the address of this web app.
//The software component version is 3.04 and the session ID is
//"AC34523452345"
ac.registerActivityLogger("http://localhost:1337", "", "3.04",
"AC34523452345");
//Re-register the logger.
//In this case, we register the logger to look for the logger on
//port 1337 of the machine "localhost", telling it that this
//software component is version 3.04 of the software named "Draper
//Test Component" and that the session ID is "AC34523452345"
ac.registerActivityLogger("http://localhost:1337",
"Draper Test Component", "3.04", "AC34523452345");
//Send a System Activity Message with optional metadata included.
//In this case, we send a System Activity message with the
//action description 'Testing System Activity Message' and optional
//metadata with two key-value pairs of:
// 'Test Window Val'='Main
// 'Data Source'='healthcare'
ac.logSystemActivity('Testing System Activity Message',
{'Test Window Val':'Main', 'Data Source':'healthCare'});
//Send a System Activity Message
//In this case, we send a System Activity message with the
//action description 'Testing System Activity Message'
ac.logSystemActivity('Testing System Activity Message');
//Send a User Activity Message
//In this case, we send a User Activity message with the
//action description 'Testing User Activity Message', a
//developer-defined user action "watch", and the
//workflow constant WF_EXAMINE, defined in the Activity
//Log API.
ac.logUserActivity('Testing User Activity Message', 'Watch', ac.WF_EXAMINE );
//Send a User Activity Message with optional metadata included
//In this case, we send a User Activity message with the
//action description 'Testing User Activity Message', a
//developer-defined user action "watch", and a
//the workflow constant WF_EXAMINE, defined in the Activity
//Log API. This message also contains optional
//metadata with two key-value pairs of:
// 'Test Window Val'='Main
// 'Data Source'='healthcare'
ac.logUserActivity('Testing User Activity Message', 'watch', ac.WF_EXAMINE,
{'Test Window Val':'Main', 'Data Source':'healthCare'});
//Send a UI Layout Message
//In this case, we send a UI Layout message with action description of
//'Testing User Activity Message'. The name of the UI element is
//'SearchWindow A', visibility=true, meaning SearchWindow A is currently
//visible. The left, right, top and bottom bounds of the UI element are
// 234px, 256px, 33px, and 500px from the top right of the screen.
ac.logUILayout('Testing User Activity Message', 'SearchWindow A', true,
234, 256, 33, 500);
//Send a UI Layout Message with optional metadata included
//In this case, we send a UI Layout message with action description of
//'Testing User Activity Message'. The name of the UI element is
//'SearchWindow A', visibility=true, meaning SearchWindow A is currently
//visible. The left, right, top and bottom bounds of the UI element are
// 234px, 256px, 33px, and 500px from the top right of the screen. This
//message also contains optional metadata with two key-value pairs of:
// 'Test Window Val'='Main
// 'Data Source'='healthcare'
ac.logUILayout('Testing User Activity Message', 'SearchWindow A', true,
234, 256, 33, 500, {'Test Window Val':'Main', 'Data Source':'healthCare'});
*
*/
function activityLogger()
{
/*========================INTERNAL CONSTANTS========================
*
* These constant define values associated with this specific version
* of this library, and should not be changed by the implementor.
*/
// The version number of the Draper Activity Logging API implemented
// by this library.
var apiVersion = 2;
//The workflow coding version used by this Activity Logging API.
var workflowCodingVersion = 1;
/* WORKFLOW CODES
* These constants specify the workflow codes defined in the Draper
* Activity Logging API version <apiVersion>. One of these
* constants must be passed in the parameter <userWorkflowState>
* in the function <logUserActivity> below.
*/
this.WF_OTHER = 0;
this.WF_PLAN = 1;
this.WF_SEARCH = 2;
this.WF_EXAMINE = 3;
this.WF_MARSHAL = 4;
this.WF_REASON = 5;
this.WF_COLLABORATE = 6;
this.WF_REPORT = 7;
// The domain for all structured data elements necessary to send
// IETF RCF 5424 compliant Syslog messages. 15038 is Draper Lab's
// IANA Private Enterprise Number.
var structuredDataDomain = 15038;
// The language in which this helper library is implemented
var implementationLanguage = "JavaScript";
// If true, this library has updated the IP address of the
// Computer on which it is running with information from
// the Activity Logging Server, or <registerActivityLogger>.
var clientsHostnameUpdated = false;
//====================END INTERNAL CONSTANTS========================
/*======================== REGISTRATION ============================
* These variables are assigned by calling the
* <registerActivityLogger> function below. They are persistent until
* a new ActivityLogger object is instantiated, or until modification
* by the <registerActivityLogger> function.
*/
/* Register this event logger. <registerActivityLogger> MUST be
* called before log messages can be sent with this library.
*
* PARAMETERS:
* @activityLogServerIN: String. The address of the logging server.
* See documentation for <activityLogServerURL> below.
* @componentNameIN: String. The name of the app or component using
* this library. See documentation for <componentName> below. If
* the empty string, defaults to the hostname of the web app that
* loaded this library
* @componentVersionIN String. The version of this app or
* component. See documentation for <componentVersion> below.
* If the empty string, defaults to 'unknown'.
* @sessionIdIN String. A unique ID for the current user session.
* See documentation for <sessionID> below. If the empty string,
* defaults to a random integer.
* @clientHostnameIN: Optional. String. The hostname or IP address
of this machine or VM. See documentation for
<clientHostname> below.
*/
this.registerActivityLogger = registerActivityLogger;
function registerActivityLogger(activityLogServerIN, componentNameIN, componentVersionIN, sessionIdIN, clientHostnameIN)
{
activityLogServerURL = activityLogServerIN;
if(componentNameIN != "")
{
componentName= componentNameIN;
}
if(componentVersionIN != "")
{
componentVersion = componentVersionIN;
}
if(sessionIdIN != "")
{
sessionID = sessionIdIN;
}
if (clientHostnameIN)
{
clientHostname = clientHostnameIN;
clientsHostnameUpdated = true;
}
}
/* The fully-qualified address of the logging server that will
* collect messages dispatched by this library.
*
* Example: "http://example.com:1337"
*
* During XDATA Summer Camp 2013, contact James Remeika at
* jremeika@draper.com for the address of the Activity Log Server
*/
var activityLogServerURL;
/* The hostname of the computer or VM on which the software
* component using this library is runing. In the case of a web-
* based user interface component, this should be the host name of
* the computer on which the web browser displaying the UI is
* running. By default, this field will be populated with the
* IP address of the client machine as seen by the Logging Server.
*
* Ideally, this hostname should describe a physical terminal or
* experimental setup as persistently as possible.
*/
var clientHostname = "xdataClient";
// The name of the software component or application sending log
// messages from this library. Defaults to the location of the
// web app loading this library.
var componentName = location.host;
// The version number of the software component or application
// specified in <clientHostname> that is sending log messages from
// this library.
var componentVersion = "unknown";
/* The unique session ID used for communication between client and
* sever-side software components during use of this component.
* Defaults to a random integer.
*
* Ideally, this session ID will identify log messages from all
* software components used to execute a unique user session.
*/
var sessionID = Math.floor(Math.random() * 10,000 );
//========================END REGISTRATION==========================
/*====================DEVELOPMENT FUNCTIONALITY=====================
* The properties and function in this section allow developers to
* echo log messages to the console, and disable the generation and
* transmission of logging messages by this library.
*/
//Set to <true> to echo log messages to the console, even if they
//are sent sucessfully to the Logging Server.
var echoLogsToConsole = false;
this.echo = function(_) {
if (!arguments.length) return echoLogsToConsole;
echoLogsToConsole = _;
return this;
};
// var echoLogsToConsole = false;
//Set to <true> to disable System Activity log messages.
this.muteSystemActivityLogging = muteSystemActivityLogging;
var muteSystemActivityLogging = false;
//Set to <true> to disable User Activity log messages
this.muteUserActivityLogging = muteUserActivityLogging;
var muteUserActivityLogging = false;
//Set to <true> to disable UI Layout log messages
this.muteUILayoutLogging = muteUILayoutLogging;
var muteUILayoutLogging = true;
// Disable all log messages
this.muteAllLogging = muteAllLogging;
function muteAllLogging()
{
muteSystemActivityLogging = true;
muteUserActivityLogging = true;
muteUILayoutLogging = true;
}
//Enable all log messages
this.unmuteAllLogging = unmuteAllLogging;
function unmuteAllLogging()
{
muteSystemActivityLogging = false;
muteUserActivityLogging = false;
muteUILayoutLogging = false;
}
//=================END DEVELOPMENT FUNCTIONALITY====================
/*==================ACTIVITY LOGGING FUNCTIONS======================
* The 3 functions in this section are used to send Activity Log
* Mesages to an Activity Logging Server. Seperate functions are used
* to log System Activity, User Activity, and UI Layout Events. See
* the Activity Logging API by Draper Laboratory for more details
* about the use of these messages.
*/
/* Log a System Activity. <registerActivityLogger> MUST be
* called before calling this function. Use <logSystemActivity> to
* log software actions that are not explicitly invoked by the user.
* For example, if a software component refreshes a data store after
* a pre-determined time span, the refresh event should be logged as
* a system activity. However, if the datastore was refreshed in
* response to a user clicking a Reshresh UI element, that activity
* should NOT be logged as a System Activity, but rather as a User
* Activity.
*
* PARAMETERS:
* @actionDescription: String. A string describing the System
* Activity performed by the component. Example:
"BankAccountTableView component refreshed datasource"
* @softwareMetadata: JSON String. Optional. Any key/value
* pairs that will clarify or paramterize this system activity.
* Example: "{'rowsAdded':'3', 'dataSource':'CheckingAccounts'}"
*/
this.logSystemActivity = logSystemActivity;
function logSystemActivity(actionDescription, softwareMetadata)
{
var encodedSystemActivityMessage = "";
if(!muteSystemActivityLogging)
{
msg = writeHead();
msg.type = "SYSACTION";
msg.parms = {
desc: actionDescription
}
msg.meta = softwareMetadata;
sendHttpMsg(msg);
}
return msg;
}
/* Log a User Activity. <registerActivityLogger> MUST be
* called before calling this function. Use <logUserActivity> to
* log actions initiated by an explicit user action. For example,
* if a software component refreshes a data store when the user
* clicks a Reshresh UI element, that activity should be logged
* as a User Activity. However, if the datastore was refreshed
* automatically after a certain time span, that activity should
* NOT be logged as a User Activity, but rather as a System
* Activity.
*
* PARAMETERS:
* @actionDescription: String. A string describing the System
* Activity performed by the component. Example:
* "BankAccountTableView component refreshed datastore."
* @userActivity: String. A key word defined by each software
* component or application indicating which software-centric
* function is is most likely indicated by the this user
* activity. See the Activity Logging API for a standard
* set of user activity key words.
* @userWorkflowState: Integer. This value must be one of the
* Workflow Codes defined in this library. See the Activity
* Logging API for definitions of each workflow code.
* Example:
* var ac = new ActivityLogger();
* ...
* var userWorkflowState = ac.WF_SEARCH;
* @softwareMetadata: JSON String. Optional. Any key/value
* pairs that will clarify or paramterize this system activity.
* Example: "{'rowsAdded':'3', 'dataSource':'CheckingAccounts'}"
*/
this.logUserActivity = logUserActivity;
function logUserActivity(actionDescription, userActivity, userWorkflowState, softwareMetadata)
{
var encodedSystemActivityMessage = "";
if(!muteUserActivityLogging)
{
msg = writeHead();
msg.type = "USERACTION";
msg.parms = {
desc: actionDescription,
activity: userActivity,
wf_state: userWorkflowState,
wf_version: workflowCodingVersion
}
msg.meta = softwareMetadata;
sendHttpMsg(msg);
}
return msg;
}
//
/* Log the Layout of a UI Element. <registerActivityLogger>
* MUST be called before calling this function. Use
* <logUILayout> to record any changes to the position or
* visibility of User Interface elements on screen.
*
* PARAMETERS:
* @actionDescription: String. A string describing the System
* Activity performed by the component. Example:
* "BankAccountTableView moved in User_Dashboard"
* @uiElementName: String. The name of the UI component that
* has changed position or visibility.
* @visibility: Boolean. True if the element is currently
* visibile. False if the element is currently hidden.
* @leftBound: Integer. The absolute position on screen, in pixels
of the leftmost boundary of the UI element.
* @rightBound: Integer. The absolute position on screen, in pixels
of the rightmost boundary of the UI element.
* @topBound: Integer. The absolute position on screen, in pixels
of the top boundary of the UI element.
* @bottomBound: Integer. The absolute position on screen, in pixels
of the bottom boundary of the UI element.
* @softwareMetadata: JSON String. Optional. Any key/value
* pairs that will clarify or paramterize this system activity.
* Example: "{'currentDashboardRow':'3', 'movementMode':'Snap_To_Grid'}"
*/
this.logUILayout = logUILayout;
function logUILayout(actionDescription, uiElementName, visibility, leftBound, rightBound, topBound, bottomBound, softwareMetadata)
{
var encodedSystemActivityMessage = "";
if(!muteUILayoutLogging)
{
msg = writeHead();
msg.type = "UILAYOUT";
msg.parms = {
desc: actionDescription,
visibility: visibility,
leftBound: leftBound,
rightBound: rightBound,
topBound: topBound,
bottomBound: bottomBound
}
msg.meta = softwareMetadata;
sendHttpMsg(msg);
}
// return msg;
}
//=================END ACTIVITY LOGGING FUNCTIONS========================
/*=========================INTERNAL FUNCTIONS============================
* These functions are used internally by the Activity Logger helper
* library to generate RCF5424 Syslog messages, and transmit them via
* HTTP POST messages to an Activity Logging server.
*/
//basic class for sending HTTP messages to Activity Logging server
var httpConnection = new XMLHttpRequest();
httpConnection.timeout = 300;
httpConnection.addEventListener("load", doneAlert, false);
httpConnection.addEventListener("error", errorAlert, false);
httpConnection.addEventListener("abort", errorAlert, false);
var busy = false;
var nextPlaceInLine = 0;
var ticketServed = 0;
var waitTimeMS = 3
var currentXHRPayload = "";
function sendHttpMsg(encodedLogMessage, placeInLine)
{
if(!placeInLine)
{
placeInLine = nextPlaceInLine++;
}
// console.log(echoLogsToConsole)
if(echoLogsToConsole){
console.log(encodedLogMessage);
}
if(busy){
setTimeout(function(){sendHttpMsg(encodedLogMessage, placeInLine);}, waitTimeMS);
}else if (placeInLine>ticketServed)
{
setTimeout(function(){sendHttpMsg(encodedLogMessage, placeInLine);}, (placeInLine-ticketServed)*waitTimeMS );
}else{
if (activityLogServerURL) {
currentXHRPayload = encodedLogMessage;
ticketServed++;
busy = true;
httpConnection.open("POST", activityLogServerURL, true);
httpConnection.send(JSON.stringify(encodedLogMessage));
}
}
}
function doneAlert(evt)
{
busy = false;
if(!clientsHostnameUpdated)
{
var oldClientHostname = clientHostname;
clientHostname = this.responseText;
console.log(clientHostname);
clientsHostnameUpdated = true;
logSystemActivity("Client hostname changed from " + oldClientHostname + " to " + clientHostname);
}
}
function errorAlert(evt)
{
console.log(currentXHRPayload);
busy = false;
}
function writeHead() {
var msg = {}
msg.timestamp = new Date();
msg.client = clientHostname;
msg.component = {name:componentName, version:componentVersion};
msg.sessionID = sessionID;
msg.impLanguage = implementationLanguage;
msg.apiVersion = apiVersion;
return msg;
}
function writeHeader()
{
var currentTimestamp = new Date();
var encodedClientHostname = "-";
var encodedComponentName = "-";
var encodedSystemActivityMessage = "<134>1 "
+ currentTimestamp.toISOString() + " "
+ (clientHostname != "" ? removeWhiteSpace(clientHostname) : "-") + " "
+ (componentName != "" ? removeWhiteSpace(componentName) : "-") + " ";
return encodedSystemActivityMessage;
}
//Write the required API version structured data element
function writeVersionData()
{
var versionNumbers = new Object();
versionNumbers["componentVersion"] = componentVersion;
versionNumbers["apiVersion"] = apiVersion;
versionNumbers["implentationLanguage"] = implementationLanguage;
return writeSDE("versions", versionNumbers);
}
//Write the required Activity structured data element
function writeWorkflowCode(userActivity, userWorkflow)
{
var workflowCode = new Object();
workflowCode["USER_ACTIVITY"] = userActivity;
workflowCode["USER_WF"] = userWorkflow;
workflowCode["wfCodeVersion"] = workflowCodingVersion;
return writeSDE("USER_ACTION", workflowCode);
}
//Write the UI Layout structured data element
function writeUILayoutData(uiElementName, visibility, leftBound, rightBound, topBound, bottomBound)
{
var UILayout = new Object();
UILayout["uiElementName"] = uiElementName;
if(visibility){
UILayout["visibility"] = "true";
}
else{
UILayout["visibility"] = "false";
}
UILayout["leftBound"] = leftBound;
UILayout["rightBound"] = rightBound;
UILayout["topBound"] = topBound;
UILayout["bottomBound"] = bottomBound;
return writeSDE("uiLayoutData", UILayout);
}
//Write any metadata included by the software developer
function writeSWMetadata(swMetadata)
{
return writeSDE(removeWhiteSpace(componentName), swMetadata);
}
//Internal function to encode a single structured data element
function writeSDE(sdeName, metaData)
{
var sdeString = "";
if (sdeName && metaData)
{
sdeString += "[" + sdeName + "@" + structuredDataDomain;
for (var i in Object.keys(metaData))
{
var keyName = Object.keys(metaData)[i];
sdeString += " " + removeWhiteSpace(keyName) + "=\"" + metaData[keyName] + "\"";
}
sdeString += "]";
}
return sdeString;
}
//If the action description string is not empty, append a UTF-8 BOM.
//Otherwise return the empty string.
function appendActionDescription(actionDescription) {
var encodedActionDescription = "";
if (actionDescription && actionDescription != "") {
encodedActionDescription += ' \357\273\277';
encodedActionDescription += actionDescription;
}
return encodedActionDescription;
}
//Internal function to remove white space from an incoming value
function removeWhiteSpace(inputString)
{
if(typeof inputString == 'string' || inputString instanceof String)
{
return inputString.replace(/\s/g,"");
} else
{
return inputString;
}
}
//=======================END INTERNAL FUNCTIONS==========================
}