This repository has been archived by the owner. It is now read-only.
Permalink
Show file tree
Hide file tree
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Userale.pyqt beta version. Tracks mouse, keyboard, and drag and drop …
…events.
- Loading branch information
Showing
3 changed files
with
316 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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__', ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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): | ||
""" | ||
""" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
|