Added support for path generation.
diff --git a/userale/ale.py b/userale/ale.py
index eba2dd9..2f4ff13 100644
--- a/userale/ale.py
+++ b/userale/ale.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
# Copyright 2016 The Charles Stark Draper Laboratory, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,231 +12,278 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# app reference to PyQT5 application (widget, application, desktop)
-# should developers be allowed to turn off global event (ignore hover/blur events, only track click events?)
-
-# Only support events, not signals (which are system level or signals emitted from various connected QtWidgets)
-# blur event
-
from userale.logger import Logger
-
-from PyQt5.QtCore import *
-from PyQt5.QtWidgets import *
+from userale.version import __version__
+from PyQt5.QtCore import QObject, QEvent
import datetime
-
class Ale (QObject):
- """
- UserAle.pyqt5 is one of the Software As A Sensor™ products.
- The goal of Software As A Sensor™ is to develop understanding of your users through their
- interactions with your software product. You can then apply that understanding to improve your
- product's design and functionality. UserAle.pyqt5 provides an easy way to generate highly detailed
- log streams from a PyQt5 application. UserAle.pyqt5 intercepts all application events by letting
- the developer install an event filter in their application to generate detailed user logs. UserAle does
- not capture system level logs or events generated by a non-user (a.k.a. system and signals sent between QObjects).
- """
- def __init__(self,
- url="http://localhost:8000/logs",
- autostart=True,
- interval=5000,
- threshold=5,
- user=None,
- version=None,
- details=False,
- resolution=500):
- """
- :param url: [string] The URL to which logs will be sent (can either be file:// or http://)
- :param autorstart: [boolean] Should UserAle start auotmatically on app rendering
- :param interval: [int] The minimum time interval in ms betweeen batch transmission of logs
- :param user: [string] Identifier for the user of the application
- :param version: [string] The application version
- :param log_details: [string] Should detailed logs (key strokes, input/change values) be recorded
- :param resolution: [int] Delay in ms between instances of high frequency logs like mouseovers, scrolls, etc
- :param shutoff: [string] Turn off logging for specific events. For example, to ignore mousedown events, ['mousedown']
-
- A log will appear like this:
- {
- 'target': ,
- 'path': ,
- 'clientTime': ,
- 'location': ,
- 'type': ,
- 'userAction': 'true',
- 'details' : [ ],
- 'userId': null,
- 'toolVersion': '1.0.0 alpha',
- 'useraleVersion': '1.0.0 alpha'
- }
- """
- QObject.__init__(self)
+ """
+ UserALE intercepts all application events by letting the developer install an event filter in their PyQT5 application to
+ generate detailed user logs. UserAle does not capture system level logs or events generated by a non-user
+ (a.k.a. system and signals sent between QObjects).
+ """
+ def __init__(self,
+ url="",
+ autostart=True,
+ interval=5000,
+ user=None,
+ version=None,
+ details=False,
+ resolution=500,
+ shutoff=[]):
+ """
+ :param url: [str] The URL to which logs will be sent (can either be file:// or http://)
+ :param autostart: [bool] Should UserAle start auotmatically on app rendering
+ :param interval: [int] The minimum time interval in ms betweeen batch transmission of logs
+ :param user: [str] Identifier for the user of the application
+ :param version: [str] The application version
+ :param details: [bool] Should detailed logs (key strokes, input/change values) be recorded
+ :param resolution: [int] Delay in ms between instances of high frequency logs like mouseovers, scrolls, etc
+ :param shutoff: [list] Turn off logging for specific events. For example, to ignore mousedown events, ['mousedown']
+
+ An example log will appear like this:
- # UserAle Configuration
- self.url = url
- self.autostart = autostart
- self.interval = interval
- self.threshold = threshold
- self.user = user
- self.version = version
- self.details = details
- self.resolution = resolution
+ .. code-block:: python
- # Store logs
- self.logs = []
+ {
+ 'target': ,
+ 'path': ['Example', 'testLineEdit'],
+ 'clientTime': ,
+ 'location': {'x': 82, 'y': 0},
+ 'type': 'dragstart',
+ 'userAction': 'true',
+ 'details' : [ ],
+ 'userId': 'userABC1234',
+ 'toolVersion': 'myApplication',
+ 'useraleVersion': '1.0.0 alpha'
+ }
+ """
- # Drag/Drop - track duration
- self.dd = datetime.datetime.now ()
+ QObject.__init__(self)
- def eventFilter(self, receiver, event):
- '''
- Event filter for capturing all events from a QApplication
- '''
- data = {}
+ # UserAle Configuration
+ self.url = url
+ self.autostart = autostart
+ self.interval = interval
+ self.user = user
+ self.version = version
+ self.details = details
+ self.resolution = resolution
- if (event.type () == QEvent.MouseButtonDblClick):
- # self.handleMouseEvents ("dblclick", event, receiver)
- pass
- elif (event.type () == QEvent.MouseButtonPress):
- data = self.handleMouseEvents ("mousedown", event, receiver)
- elif (event.type () == QEvent.MouseButtonRelease):
- data = self.handleMouseEvents ("mouseup", event, receiver)
- elif (event.type () == QEvent.MouseMove):
- data = self.handleMouseEvents ("mousemove", event, receiver)
- elif (event.type () == QEvent.KeyPress):
- data = self.handleKeyEvents ("keypress", event, receiver)
- elif (event.type () == QEvent.KeyRelease):
- data = self.handleKeyEvents ("keyrelease", event, receiver)
- elif (event.type () == QEvent.Leave):
- pass
- elif (event.type () == QEvent.Move):
- pass
- elif (event.type () == QEvent.Resize):
- pass
- elif (event.type () == QEvent.Scroll):
- pass
- elif (event.type () == QEvent.DragEnter):
- data = self.handleDragEvents ("dragstart", event, receiver)
- elif (event.type () == QEvent.DragLeave):
- data = self.handleDragEvents ("dragleave", event, receiver)
- elif (event.type () == QEvent.DragMove):
- data = self.handleDragEvents ("dragmove", event, receiver)
- elif (event.type () == QEvent.Drop):
- data = self.handleDragEvents ("dragdrop", event, receiver)
- else:
- pass
+ # Store logs
+ self.logs = []
- if data:
- Logger.stdout (data)
+ # Drag/Drop - track duration
+ self.dd = datetime.datetime.now ()
- return super(Ale, self).eventFilter(receiver, event)
- # return True
+ def eventFilter(self, object, event):
+ '''
+ :param object: [QObject] The object being watched.
+ :param event: [QEvent]
+ :return: [bool] Return true in order to filter the event out (stop it from being handled further). Otherwise return false.
+
+ Filters events for the watched object (in this case, QApplication)
+ '''
- def getSelector (self, element):
- """
- Get target object's name (element defined by user or object class name)
- """
- return element.objectName()
+ data = {}
+
+ if (event.type () == QEvent.MouseButtonPress):
+ data = self.handleMouseEvents ("mousedown", event, object)
+ elif (event.type () == QEvent.MouseButtonRelease):
+ data = self.handleMouseEvents ("mouseup", event, object)
+ elif (event.type () == QEvent.MouseMove):
+ data = self.handleMouseEvents ("mousemove", event, object)
+ elif (event.type () == QEvent.KeyPress):
+ data = self.handleKeyEvents ("keypress", event, object)
+ elif (event.type () == QEvent.KeyRelease):
+ data = self.handleKeyEvents ("keyrelease", event, object)
+ elif (event.type () == QEvent.Leave):
+ data = self.handleLeaveEvents ("keyrelease", event, object)
+ elif (event.type () == QEvent.Move):
+ data = self.handleMoveEvents ("keyrelease", event, object)
+ elif (event.type () == QEvent.Resize):
+ data = self.handleResizeEvents ("keyrelease", event, object)
+ elif (event.type () == QEvent.Scroll):
+ data = self.handleScrollEvents ("keyrelease", event, object)
+ elif (event.type () == QEvent.DragEnter):
+ data = self.handleDragEvents ("dragstart", event, object)
+ elif (event.type () == QEvent.DragLeave):
+ data = self.handleDragEvents ("dragleave", event, object)
+ elif (event.type () == QEvent.DragMove):
+ data = self.handleDragEvents ("dragmove", event, object)
+ elif (event.type () == QEvent.Drop):
+ data = self.handleDragEvents ("dragdrop", event, object)
+ else:
+ pass
- def getLocation (self, event):
- """
- Grab the x and y position of the mouse cursor, relative to the widget that received the event.
- """
- try:
- pos = event.pos ()
- loc = {"x" : pos.x (), "y" : pos.y ()}
- except:
- loc = None
- return loc
+ if data:
+ Logger.stdout (data)
+
+ # return super(Ale, self).eventFilter(object, event)
+ return False
- def getPath (self, element):
- """
- How to encode path for elements. Is it DOM hierachy? Or it is path of movement?
- DragnDrop Event:
- Distance?
- """
- # Fetch parent
- #meta = element.metaObject ()
- p = element.parent ()
+ def getSelector (self, object):
+ """
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [str] The Qt object's name
- print (p)
- # return p.metaObject().className () + ': ' + meta.className ()
+ Get target object's name (object defined by user or object's meta class name)
+ """
- def getClientTime (self, element):
- """
- Time event was triggered
- """
- return str (datetime.datetime.now ())
+ return object.objectName() if object.objectName() else object.staticMetaObject.className()
- def handleMouseEvents (self, event_type, event, receiver):
- """
- Detailed log for a mouse event.
- """
- data = {
- 'target': self.getSelector (receiver) ,
- 'path': self.getPath (receiver),
- 'clientTime': self.getClientTime (event),
- 'location': self.getLocation(event),
- 'type': event_type ,
- 'userAction': 'true',
- 'details' : [ ],
- 'userId': self.user,
- 'toolVersion': self.version,
- 'useraleVersion': '1.0.0 alpha'
- }
+ def getLocation (self, event):
+ """
+ :param event: [QEvent] The base class for all event classes.
+ :return: [dict] A dictionary representation of the x and y positions of the mouse cursor.
- return data
+ Grab the x and y position of the mouse cursor, relative to the widget that received the event.
+ """
- def handleKeyEvents (self, event_type, event, receiver):
- """
- Detailed log for a key event. Key name and code are tracked.
- """
- data = {
- 'target': self.getSelector (receiver) ,
- 'path': self.getPath (receiver),
- 'clientTime': self.getClientTime (event),
- 'location': self.getLocation(event),
- 'type': event_type ,
- 'userAction': 'true',
- 'details' : {'key' : event.text (), 'keycode' : event.key ()},
- 'userId': self.user,
- 'toolVersion': self.version,
- 'useraleVersion': '1.0.0 alpha'
- }
+ try:
+ return {"x" : event.pos ().x (), "y" : event.pos ().y ()}
+ except:
+ return None
- return data
+ def getPath (self, object):
+ """
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [list] List of QObjects up to the child object.
- def handleDragEvents (self, event_type, event, receiver):
- """
- Detailed log for a drag event. When a user attempts a dragenter/drag move event, a timer is generated
- to track duraction. Duraction will be stored in the drag drop event message.
- """
- res = {}
- if event_type == 'dragstart':
- # start timer
- self.dd = datetime.datetime.now ()
- elif event_type == 'dragdrop' or event_type == 'dragleave':
- res = {"elapsed" : str (datetime.datetime.now () - self.dd)}
- self.dd = datetime.datetime.now ()
- else:
- # drag move event - ignore
- pass
+ Fetch the entire path up the root of the tree for a leaf node object.
+ Recursive operation.
+ """
- data = {
- 'target': self.getSelector (receiver) ,
- 'path': self.getPath (receiver),
- 'clientTime': self.getClientTime (event),
- 'location': self.getLocation(event),
- 'type': event_type ,
- 'userAction': 'true',
- 'details' : res,
- 'userId': self.user,
- 'toolVersion': self.version,
- 'useraleVersion': '1.0.0 alpha'
- }
+ if object.parent() is not None:
+ return self.getPath (object.parent()) + [self.getSelector (object)]
+ else:
+ return [self.getSelector (object)]
- return data
+ def getClientTime (self):
+ """
+ :return: [str] String representation of the time the event was triggered.
+
+ Capture the time the event was captured.
+ """
- def handleMoveEvents (self, event_type, event, receiver):
- """
+ return str (datetime.datetime.now ())
- """
+ def handleMouseEvents (self, event_type, event, object):
+ """
+ :param event_type: [str] The string representation of the type of event being triggered by the user.
+ :param event: [QEvent] The base class for all event classes.
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [dict] A userale log describing a mouse event.
+ Returns the userale log representing all mouse event data.
+
+ .. code-block:: python
+
+ """
+
+ return self.__create_msg (event_type, event, object)
+
+ def handleKeyEvents (self, event_type, event, object):
+ """
+ :param event_type: [str] The string representation of the type of event being triggered by the user.
+ :param event: [QEvent] The base class for all event classes.
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [dict] A userale log describing a key event.
+
+ Returns the userale log representing all key events, including key name and key code.
+ """
+
+ details = {'key' : event.text (), 'keycode' : event.key ()}
+ return self.__create_msg (event_type, event, object, details=details)
+
+ def handleDragEvents (self, event_type, event, object):
+ """
+ :param event_type: [str] The string representation of the type of event being triggered by the user.
+ :param event: [QEvent] The base class for all event classes.
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [dict] A userale log describing a drag event.
+
+ Returns the userale log representing all drag events.
+ """
+
+ details = {}
+ if event_type == 'dragstart':
+ # start timer
+ self.dd = datetime.datetime.now ()
+ elif event_type == 'dragdrop' or event_type == 'dragleave':
+ details = {"elapsed" : str (datetime.datetime.now () - self.dd)}
+ self.dd = datetime.datetime.now ()
+ else:
+ # drag move event - ignore
+ pass
+
+ return self.__create_msg (event_type, event, object, details=details)
+
+ def handleMoveEvents (self, event_type, event, object):
+ """
+ :param event_type: [str] The string representation of the type of event being triggered by the user.
+ :param event: [QEvent] The base class for all event classes.
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [dict] A userale log describing a drag event.
+
+ Returns the userale log representing all move events.
+ """
+
+ pass
+
+ def handleLeaveEvents (self, event_type, event, object):
+ """
+ :param event_type: [str] The string representation of the type of event being triggered by the user.
+ :param event: [QEvent] The base class for all event classes.
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [dict] A userale log describing a leave event.
+
+ Returns the userale log representing all leave events.
+ """
+
+ pass
+
+ def handleResizeEvents (self, event_type, event, object):
+ """
+ :param event_type: [str] The string representation of the type of event being triggered by the user.
+ :param event: [QEvent] The base class for all event classes.
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [dict] A userale log describing a resize event.
+
+ Returns the userale log representing all resize events.
+ """
+
+ pass
+
+ def handleScrollEvents (self, event_type, event, object):
+ """
+ :param event_type: [str] The string representation of the type of event being triggered by the user.
+ :param event: [QEvent] The base class for all event classes.
+ :param object: [QObject] The base class for all Qt objects.
+ :return: [dict] A userale log describing a scroll event.
+
+ Returns the userale log representing all scroll events.
+ """
+
+ pass
+
+ def __create_msg (self, event_type, event, object, details={}):
+ """
+ Geneate UserAle log describing an event.
+ """
+
+ data = {
+ 'target': self.getSelector (object) ,
+ 'path': self.getPath (object),
+ 'clientTime': self.getClientTime (),
+ 'location': self.getLocation(event),
+ 'type': event_type ,
+ 'userAction': 'true',
+ 'details' : details,
+ 'userId': self.user,
+ 'toolVersion': self.version,
+ 'useraleVersion': __version__
+ }
+
+ return data