Sanyam0385 commited on
Commit Β·
49d46a6
1
Parent(s): 0f55048
Fix own-UI click leakage, S/E firing on the report page, add X kill switch
Browse filesMouse tracker now excludes its own panel/pill/canvas from every
measurement (trail, heatmap, dwell, clicks) instead of logging interactions
with its own controls as website activity. S/E are now gated to actual
website pages (landing and report pages are marked non-trackable, with a
Python-side URL check as a backstop). X is a new global hotkey that kills
the process outright via Api.quit_app().
- analysis.py +1 -0
- browser_session.py +76 -30
analysis.py
CHANGED
|
@@ -273,6 +273,7 @@ _TEMPLATE = r"""<!DOCTYPE html>
|
|
| 273 |
<html>
|
| 274 |
<head>
|
| 275 |
<meta charset="utf-8">
|
|
|
|
| 276 |
<title>InsightUX Session Report</title>
|
| 277 |
<style>
|
| 278 |
:root {
|
|
|
|
| 273 |
<html>
|
| 274 |
<head>
|
| 275 |
<meta charset="utf-8">
|
| 276 |
+
<meta name="insightux-report" content="true">
|
| 277 |
<title>InsightUX Session Report</title>
|
| 278 |
<style>
|
| 279 |
:root {
|
browser_session.py
CHANGED
|
@@ -225,34 +225,40 @@ class OneEuroFilter:
|
|
| 225 |
CONTROL_JS = r"""
|
| 226 |
(function(){
|
| 227 |
if (window.__insightuxControl) { return; }
|
| 228 |
-
// The local landing page is already fully InsightUX-branded β don't
|
| 229 |
-
// stack a second banner on top of it.
|
| 230 |
-
if (document.querySelector('meta[name="insightux-landing"]')) { return; }
|
| 231 |
window.__insightuxControl = true;
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
const statusEl = () => document.getElementById('__insightux_status');
|
| 258 |
window.insightuxSetStatus = function(text){
|
|
@@ -270,6 +276,15 @@ CONTROL_JS = r"""
|
|
| 270 |
document.addEventListener('keydown', function(e){
|
| 271 |
const typingBlocked = isTypingTarget(e.target) || isTypingTarget(document.activeElement);
|
| 272 |
const apiReady = !!(window.pywebview && window.pywebview.api);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
if (e.key === 's' || e.key === 'S' || e.key === 'e' || e.key === 'E') {
|
| 274 |
console.log('[insightux] key=' + e.key, 'typingBlocked=' + typingBlocked, 'apiReady=' + apiReady, 'focusedEl=' + (document.activeElement && document.activeElement.tagName));
|
| 275 |
}
|
|
@@ -368,9 +383,9 @@ TRACKING_JS = r"""
|
|
| 368 |
function consider(el){
|
| 369 |
if (full) return;
|
| 370 |
// Never track InsightUX's own injected UI (the "press S/E" pill,
|
| 371 |
-
// the gaze-dot canvas) as if it were page content.
|
| 372 |
-
if (el.id === '__insightux_pill' || el.id === '__insightux_canvas') return;
|
| 373 |
-
if (el.closest && el.closest('#__insightux_pill, #__insightux_canvas')) return;
|
| 374 |
const tag = el.tagName.toLowerCase();
|
| 375 |
const explicit = !!(el.dataset && el.dataset.aoi);
|
| 376 |
if (!explicit){
|
|
@@ -646,8 +661,18 @@ MOUSE_JS = r"""
|
|
| 646 |
updatePanel();
|
| 647 |
}, 2000);
|
| 648 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
// ---- mouse position + hover tracking ----
|
| 650 |
function updateMousePos(e){
|
|
|
|
| 651 |
lastClientX = e.clientX; lastClientY = e.clientY;
|
| 652 |
if (lastPageX === 0) lastPageX = e.pageX;
|
| 653 |
if (lastPageY === 0) lastPageY = e.pageY;
|
|
@@ -665,6 +690,7 @@ MOUSE_JS = r"""
|
|
| 665 |
document.addEventListener('click', function(e){
|
| 666 |
if (!isTracking) return;
|
| 667 |
if (e.target === canvas) return;
|
|
|
|
| 668 |
if (!isInteractable(e.target)) return;
|
| 669 |
|
| 670 |
const x = e.pageX, y = e.pageY;
|
|
@@ -962,9 +988,15 @@ MOUSE_JS = r"""
|
|
| 962 |
|
| 963 |
|
| 964 |
# =============================================================================
|
| 965 |
-
# API exposed to JS β start/stop are the
|
| 966 |
# =============================================================================
|
| 967 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 968 |
class Api:
|
| 969 |
def __init__(self):
|
| 970 |
self.window = None
|
|
@@ -979,6 +1011,14 @@ class Api:
|
|
| 979 |
if self.tracking:
|
| 980 |
print("[browser_session] already tracking, ignoring")
|
| 981 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 982 |
if not os.path.exists(CALIBRATION_PATH):
|
| 983 |
print(f"[browser_session] no {CALIBRATION_PATH} found β run calibrate.py first")
|
| 984 |
self._set_status("No calibration.pkl found β run calibrate.py first.")
|
|
@@ -1021,6 +1061,12 @@ class Api:
|
|
| 1021 |
self.mouse_close_timer.start()
|
| 1022 |
return True
|
| 1023 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1024 |
def _inject_tracking_overlay(self):
|
| 1025 |
try:
|
| 1026 |
self.window.evaluate_js(TRACKING_JS)
|
|
|
|
| 225 |
CONTROL_JS = r"""
|
| 226 |
(function(){
|
| 227 |
if (window.__insightuxControl) { return; }
|
|
|
|
|
|
|
|
|
|
| 228 |
window.__insightuxControl = true;
|
| 229 |
|
| 230 |
+
// S/E (start/stop tracking) only make sense on an actual website β not on
|
| 231 |
+
// the InsightUX landing/search page and not on a generated insights
|
| 232 |
+
// report page. X (quit the app) always works, on every page.
|
| 233 |
+
const isLanding = !!document.querySelector('meta[name="insightux-landing"]');
|
| 234 |
+
const isReport = !!document.querySelector('meta[name="insightux-report"]');
|
| 235 |
+
const isTrackable = !isLanding && !isReport;
|
| 236 |
+
|
| 237 |
+
if (isTrackable) {
|
| 238 |
+
const banner = document.createElement('div');
|
| 239 |
+
banner.id = '__insightux_pill';
|
| 240 |
+
banner.style.cssText = `
|
| 241 |
+
position:fixed; top:0; left:0; right:0; z-index:2147483647;
|
| 242 |
+
display:flex; align-items:center; justify-content:space-between;
|
| 243 |
+
padding:7px 16px; font:12px -apple-system,Arial;
|
| 244 |
+
background:linear-gradient(90deg, rgba(20,18,26,0.95), rgba(30,20,40,0.95));
|
| 245 |
+
border-bottom:2px solid; border-image:linear-gradient(90deg,#7B2FBE,#FF2DF0) 1;
|
| 246 |
+
box-shadow:0 3px 14px rgba(0,0,0,0.35); pointer-events:none;
|
| 247 |
+
`;
|
| 248 |
+
banner.innerHTML = `
|
| 249 |
+
<span style="display:flex;align-items:center;gap:7px;">
|
| 250 |
+
<span style="width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#7B2FBE,#FF2DF0);display:inline-block;"></span>
|
| 251 |
+
<b style="color:#f0ecf7;letter-spacing:0.02em;">InsightUX</b>
|
| 252 |
+
<span style="color:#7a7288;">Eye + Mouse Research Browser</span>
|
| 253 |
+
</span>
|
| 254 |
+
<span id="__insightux_status" style="color:#c9a6f5;">Press S to start tracking · E to stop · H heatmap · M mouse panel · X quit</span>
|
| 255 |
+
`;
|
| 256 |
+
document.documentElement.appendChild(banner);
|
| 257 |
+
|
| 258 |
+
setInterval(function(){
|
| 259 |
+
if (!document.documentElement.contains(banner)) document.documentElement.appendChild(banner);
|
| 260 |
+
}, 1000);
|
| 261 |
+
}
|
| 262 |
|
| 263 |
const statusEl = () => document.getElementById('__insightux_status');
|
| 264 |
window.insightuxSetStatus = function(text){
|
|
|
|
| 276 |
document.addEventListener('keydown', function(e){
|
| 277 |
const typingBlocked = isTypingTarget(e.target) || isTypingTarget(document.activeElement);
|
| 278 |
const apiReady = !!(window.pywebview && window.pywebview.api);
|
| 279 |
+
|
| 280 |
+
if (e.key === 'x' || e.key === 'X') {
|
| 281 |
+
if (typingBlocked || !apiReady) return;
|
| 282 |
+
window.pywebview.api.quit_app();
|
| 283 |
+
return;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
if (!isTrackable) return; // S/E do nothing on the landing page or a report page
|
| 287 |
+
|
| 288 |
if (e.key === 's' || e.key === 'S' || e.key === 'e' || e.key === 'E') {
|
| 289 |
console.log('[insightux] key=' + e.key, 'typingBlocked=' + typingBlocked, 'apiReady=' + apiReady, 'focusedEl=' + (document.activeElement && document.activeElement.tagName));
|
| 290 |
}
|
|
|
|
| 383 |
function consider(el){
|
| 384 |
if (full) return;
|
| 385 |
// Never track InsightUX's own injected UI (the "press S/E" pill,
|
| 386 |
+
// the gaze-dot canvas, the mouse tracker panel) as if it were page content.
|
| 387 |
+
if (el.id === '__insightux_pill' || el.id === '__insightux_canvas' || el.id === '__insightux_mouse_panel') return;
|
| 388 |
+
if (el.closest && el.closest('#__insightux_pill, #__insightux_canvas, #__insightux_mouse_panel')) return;
|
| 389 |
const tag = el.tagName.toLowerCase();
|
| 390 |
const explicit = !!(el.dataset && el.dataset.aoi);
|
| 391 |
if (!explicit){
|
|
|
|
| 661 |
updatePanel();
|
| 662 |
}, 2000);
|
| 663 |
|
| 664 |
+
// ---- exclude InsightUX's own injected UI from every measurement ----
|
| 665 |
+
// (the status pill, the mouse-tracker panel, the heatmap canvas) β only
|
| 666 |
+
// real website elements should ever show up in trail/heatmap/dwell/clicks.
|
| 667 |
+
function isOwnUI(el){
|
| 668 |
+
if (!el) return false;
|
| 669 |
+
if (el.id === '__insightux_pill' || el.id === '__insightux_canvas' || el.id === '__insightux_mouse_panel') return true;
|
| 670 |
+
return !!(el.closest && el.closest('#__insightux_pill, #__insightux_canvas, #__insightux_mouse_panel'));
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
// ---- mouse position + hover tracking ----
|
| 674 |
function updateMousePos(e){
|
| 675 |
+
if (isOwnUI(e.target)) return;
|
| 676 |
lastClientX = e.clientX; lastClientY = e.clientY;
|
| 677 |
if (lastPageX === 0) lastPageX = e.pageX;
|
| 678 |
if (lastPageY === 0) lastPageY = e.pageY;
|
|
|
|
| 690 |
document.addEventListener('click', function(e){
|
| 691 |
if (!isTracking) return;
|
| 692 |
if (e.target === canvas) return;
|
| 693 |
+
if (isOwnUI(e.target)) return;
|
| 694 |
if (!isInteractable(e.target)) return;
|
| 695 |
|
| 696 |
const x = e.pageX, y = e.pageY;
|
|
|
|
| 988 |
|
| 989 |
|
| 990 |
# =============================================================================
|
| 991 |
+
# API exposed to JS β start/stop/quit are the entry points
|
| 992 |
# =============================================================================
|
| 993 |
|
| 994 |
+
# Pages tracking must never start on, checked against window.get_current_url()
|
| 995 |
+
# as a Python-side backstop to CONTROL_JS's isTrackable check (belt and
|
| 996 |
+
# suspenders β the JS guard can't run before the JS bridge is up).
|
| 997 |
+
_NON_TRACKABLE_URL_MARKERS = ("insightux_landing.html", "analysis_report.html")
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
class Api:
|
| 1001 |
def __init__(self):
|
| 1002 |
self.window = None
|
|
|
|
| 1011 |
if self.tracking:
|
| 1012 |
print("[browser_session] already tracking, ignoring")
|
| 1013 |
return False
|
| 1014 |
+
try:
|
| 1015 |
+
current_url = self.window.get_current_url() or ""
|
| 1016 |
+
except Exception:
|
| 1017 |
+
current_url = ""
|
| 1018 |
+
if any(marker in current_url for marker in _NON_TRACKABLE_URL_MARKERS):
|
| 1019 |
+
print(f"[browser_session] refusing to start on non-website page: {current_url}")
|
| 1020 |
+
self._set_status("Start tracking only works on a website β search or open a page first.")
|
| 1021 |
+
return False
|
| 1022 |
if not os.path.exists(CALIBRATION_PATH):
|
| 1023 |
print(f"[browser_session] no {CALIBRATION_PATH} found β run calibrate.py first")
|
| 1024 |
self._set_status("No calibration.pkl found β run calibrate.py first.")
|
|
|
|
| 1061 |
self.mouse_close_timer.start()
|
| 1062 |
return True
|
| 1063 |
|
| 1064 |
+
def quit_app(self):
|
| 1065 |
+
# X is an emergency kill switch, not a graceful shutdown β the user
|
| 1066 |
+
# wants the terminal process gone immediately, camera/threads and all.
|
| 1067 |
+
print("[browser_session] quit_app() called from JS -- terminating process")
|
| 1068 |
+
os._exit(0)
|
| 1069 |
+
|
| 1070 |
def _inject_tracking_overlay(self):
|
| 1071 |
try:
|
| 1072 |
self.window.evaluate_js(TRACKING_JS)
|