Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
Browse files
Userale.pyqt beta version. Tracks mouse, keyboard, and drag and drop …
…events.
  • Loading branch information
mooshu1x2 committed Jul 15, 2016
1 parent 3012750 commit bdb9b8ce4eee51d3dbbf123addbcb7b9b22af495
Showing 3 changed files with 316 additions and 0 deletions.
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
#
# Copyright 2016 The Charles Stark Draper Laboratory, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""UserAle"""

from .version import __version__

__all__ = ('__version__', )
@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
#
# Copyright 2016 The Charles Stark Draper Laboratory, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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 *
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 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

# Store logs
self.logs = []

# Drag/Drop - track duration
self.dd = datetime.datetime.now ()

def eventFilter(self, receiver, event):
'''
Event filter for capturing all events from a QApplication
'''
data = {}

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

if data:
Logger.stdout (data)

return super(Ale, self).eventFilter(receiver, event)
# return True

def getSelector (self, element):
"""
Get target object's name (element defined by user or object class name)
"""
return element.objectName()

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

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 ()

print (p)
# return p.metaObject().className () + ': ' + meta.className ()

def getClientTime (self, element):
"""
Time event was triggered
"""
return str (datetime.datetime.now ())

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'
}

return data

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'
}

return data

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

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'
}

return data

def handleMoveEvents (self, event_type, event, receiver):
"""
"""

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2016 The Charles Stark Draper Laboratory, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from requests.auth import AuthBase

import json

# Logger will need to optinally handle authorization
# through basic http auth, proxy, and SOCKS

class Logger (object):
"""
"""

# def __init__(self, url="", data={}):
# self.url = url
# self.data = data

# write to file

# send over url
# @staticmethod
# def emit (url, data):
# """
# """
# # url = self.url
# # payload = self.data
# r = requests.post (url, json=data)
# r.status_code

# print to stdout
@staticmethod
def stdout (data):
print (json.dumps (data, sort_keys=False, indent=4))

0 comments on commit bdb9b8c

Please sign in to comment.