Logging And Replay

A key feature of CASMACAT is the extensive logging, that allows the replay of a user session from log data.

This page describes what needs to be done to add logging and replay to a new feature in the workbench.

Logging and replay is handled by Javascript code in public/js/casmacat/logging. The web server backend in lib/controller and lib/model handles storing log events into the database, retrieving it from the database and exporting it as XML.

We use the example of the floating prediction functionality as an example. This functionality displays a small text box with suggested continuation of the current translation.

Logging

The display of the floating prediction text box is handled by the function drawTextBox in itp-visualization.js. Your functionality will have similar code that displays the action on the screen.

To support logging, we add a trigger action on $target, i.e., the textbox in which the target translation is types, onto which a lot of functionality is attached.

public/js/casmacat/itp-visualization.js

  function drawTextBox( text, visible, x, y) {
    elFloatPred.innerHTML = text;
    elFloatPred.className = 'floating-prediction'
       + (visible ? '' : ' floating-prediction-hidden');
    elFloatPred.style.left = x;
    elFloatPred.style.top  = y;
    $target.trigger('floatPredictionShow',[ [text, visible, x, y]]);
  }

The triggered event floatPredictionShow is caught by the logging module.

public/js/casmacat/logging/jquery.casmacat.logging.js

     $(window).on("floatPredictionShow." + pluginName, 
                  floatPredictionShow);
     [...]
     var floatPredictionShow = function(e, data) {
        storeLogEvent(logEventFactory.newLogEvent(
           logEventFactory.FLOAT_PREDICTION_SHOW, 
           e.timeStamp, e.target, data));
    }

This calls the general logEventFactory code to store events. So, the logging event needs to be registered there. In case of the floating prediction event, we also store additional data - the contents and location of the text box, and a Boolean indicator if the text box is visible.

public/js/casmacat/logging/casmacat.logevent.js

    this.FLOAT_PREDICTION_SHOW = "floatPredictionShow";
 [...] 
        case this.FLOAT_PREDICTION_SHOW:
            logEvent.text    = arguments[3][0];
            logEvent.visible = arguments[3][1];
            logEvent.x = arguments[3][2];
            logEvent.y = arguments[3][3];
            break;

This extended log event will be passed as JSON object to the backend, which stores it in the database. Similarly, during replay this event is retrieved, so we can just access it as a Javascript object.

Replay

To replay the event, the user interface has to call the display function. For this, we first define a ITP event:

public/js/casmacat/itp-events.js

  // function to prompt display of floating prediction, used by replay
  itp.on('floatingPredictionShow', function(data, err) {
    var conf = userCfg();
     if (conf.mode == 'ITP' && window.config.floatPredictions) {
      self.vis.FloatingPrediction.drawTextBox
           (data[0],data[1],data[2],data[3]);
    }
  });

This ITP event is triggered by the replay module.

public/js/casmacat/logging/jquery.casmacat.replay.js

  case logEventFactory.FLOAT_PREDICTION_SHOW:
    vsWindow.$("#" + event.elementId).editableItp(
         'trigger', 
         'floatingPredictionShow', 
         {errors: [], data: [event.text, event.visible, 
                              event.x, event.y]});
    break;

Back-end support

Since the example event has additional information, this needs to be stored in the database. We create a special database table for the event.

lib/model/casmacat.sql

 CREATE TABLE IF NOT EXISTS `float_prediction_show_event` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `header_id` int(11) NOT NULL,
  `text` text COLLATE utf8_unicode_ci NOT NULL,
  `visible` tinyint(1) COLLATE utf8_unicode_ci NOT NULL,
  `x` varchar(8) COLLATE utf8_unicode_ci NOT NULL,
  `y` varchar(8) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  KEY `id` (`id`),
  KEY `header_id` (`header_id`)
 ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 
   COLLATE=utf8_unicode_ci AUTO_INCREMENT=28 ;

lib/model/deleteFromAll.sql

 DELETE FROM `float_prediction_show_event` WHERE 1;

The core code to fetch and insert the additional event information is in casQueries.php

lib/model/casQueries.php

  case LogEvent::FLOAT_PREDICTION_SHOW:
   break;
 [...]
 case LogEvent::FLOAT_PREDICTION_SHOW:
   $eventRow = fetchEventRow($logEvent->id, 
                "float_prediction_show_event");
   $logEvent->floatPredictionShowData($eventRow);
   break;
 [...]
  function insertFloatPredictionShowEvent($event) {
    $headerId = insertLogEventHeader($event);
    $data = array();
    $data["id"] = "NULL";
    $data["header_id"] = $headerId;
    $data["text"] = $event->text;
    $data["visible"] = $event->visible;
    $data["x"] = $event->x;
    $data["y"] = $event->y;

    $db = Database::obtain();
    $db->insert("float_prediction_show_event", $data);

    $err = $db->get_error();
    $errno = $err["error_code"];
    if ($errno != 0) {
        log::doLog("CASMACAT: insertFloatPredictionShow(): "
               . print_r($err, true));
        throw new Exception("CASMACAT: insertFloatPredictionShow(): "
               . print_r($err, true));
    }

}

We also need to map JSON and PHP objects

lib/model/LogEvent.class.php

 const FLOAT_PREDICTION_SHOW = "floatPredictionShow";
 [...]
    public function floatPredictionShowData($object) {
        $this->text = $object->text;
        $this->visible = $object->visible;
        $this->x = $object->x;
        $this->y = $object->y;
    }

This basic functionality is called when the log entries are saved.

lib/controller/saveLogChunkController.php

 case LogEvent::FLOAT_PREDICTION_SHOW:
    $logEvent->floatPredictionShowData($value);
     insertFloatPredictionShowEvent($logEvent);
       break;

.. and exported

lib/model/exportLog.php

 //float_prediction_show_event 
 $queryId = $db->query("SELECT h.id as id, h.job_id, 
     h.file_id, h.element_id, h.x_path, h.time, h.type"
     . ", b.text, b.visible, b.x, b.y"
     . " FROM log_event_header h, float_prediction_show_event b 
       WHERE h.job_id = '$jobId' AND h.file_id = '$fileId' AND h.

 $err = $db->get_error();
 $errno = $err["error_code"];
 if ($errno != 0) {
   log::doLog("CASMACAT: fetchLogChunk(): "
             . print_r($err, true));
   throw new Exception("CASMACAT: fetchLogChunk(): "
             . print_r($err, true));
 }

 $floatPredictionShowRow = null;
 $floatPredictionShowEvents = array();
 while ( ($floatPredictionShowRow = $db->fetch($queryId)) != false ) {
   $floatPredictionShowRowAsObject 
     = snakeToCamel($floatPredictionShowRow);        
   $floatPredictionShowEvent 
     = new LogEvent($jobId, $fileId, $floatPredictionShowRowAsObject);
   $floatPredictionShowEvent->floatPredictionShowData
     ($floatPredictionShowRowAsObject);
   array_push($floatPredictionShowEvents, $floatPredictionShowEvent); 
 }

 if(!empty($floatPredictionShowEvents)) {
   $count_float_prediction_show = 0;
   $len_float_prediction_show = count($floatPredictionShowEvents);
 }
 else $len_float_prediction_show = 0;

 [...]

 elseif ($len_float_prediction_show != 0 && 
      $headerRowAsObject->id == 
        $floatPredictionShowEvents[$count_float_prediction_show]->id){
    foreach($floatPredictionShowEvents[$count_float_prediction_show] 
            as $attribute => $val){                
      if ($attribute != 'jobId' && $attribute != 'fileId' 
         && $attribute != 'type'){
       $writer->writeAttribute($attribute, $val);
      }
    }
    if ($count_float_prediction_show < $len_float_prediction_show-1){
      $count_float_prediction_show = $count_float_prediction_show + 1;
    }
  }

... and stored from uploaded XML

lib/model/uploadXML.php

 case LogEvent::FLOAT_PREDICTION_SHOW:
   $logEvent->floatPredictionShowData($value);
   insertFloatPredictionShowEvent($logEvent);
   break;