claude-mpm 4.2.43__py3-none-any.whl → 4.2.44__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,451 +0,0 @@
1
- """
2
- Browser Console Handler for Unified Monitor
3
- ===========================================
4
-
5
- WHY: This handler manages browser console events from web pages that have
6
- the browser console monitor script injected. It provides centralized logging
7
- and debugging capabilities for client-side applications.
8
-
9
- DESIGN DECISIONS:
10
- - Creates separate log files per browser session for easy tracking
11
- - Stores logs in .claude-mpm/logs/client/ directory
12
- - Handles multiple concurrent browser connections
13
- - Tracks active browser sessions with metadata
14
- - Provides structured logging with timestamps and context
15
- - Integrates with the unified monitor architecture
16
- """
17
-
18
- import json
19
- from datetime import datetime
20
- from pathlib import Path
21
- from typing import Dict, Set
22
-
23
- import socketio
24
-
25
- from ....core.logging_config import get_logger
26
-
27
-
28
- class BrowserHandler:
29
- """Event handler for browser console monitoring functionality.
30
-
31
- WHY: Manages browser console events from injected monitoring scripts
32
- and provides centralized logging for client-side debugging.
33
- """
34
-
35
- def __init__(self, sio: socketio.AsyncServer):
36
- """Initialize the browser handler.
37
-
38
- Args:
39
- sio: Socket.IO server instance
40
- """
41
- self.sio = sio
42
- self.logger = get_logger(__name__)
43
-
44
- # Browser session management
45
- self.active_browsers: Set[str] = set()
46
- self.browser_info: Dict[str, Dict] = {}
47
-
48
- # Logging configuration
49
- self.log_dir = Path.cwd() / ".claude-mpm" / "logs" / "client"
50
- self.log_files: Dict[str, Path] = {}
51
-
52
- # Ensure log directory exists
53
- self._ensure_log_directory()
54
-
55
- def register(self):
56
- """Register Socket.IO event handlers."""
57
- try:
58
- # Browser connection events
59
- self.sio.on("browser:connect", self.handle_browser_connect)
60
- self.sio.on("browser:disconnect", self.handle_browser_disconnect)
61
- self.sio.on("browser:hide", self.handle_browser_hide)
62
-
63
- # Console events
64
- self.sio.on("browser:console", self.handle_console_event)
65
-
66
- # Browser management events
67
- self.sio.on("browser:list", self.handle_browser_list)
68
- self.sio.on("browser:info", self.handle_browser_info)
69
-
70
- self.logger.info("Browser event handlers registered")
71
-
72
- except Exception as e:
73
- self.logger.error(f"Error registering browser handlers: {e}")
74
- raise
75
-
76
- def _ensure_log_directory(self):
77
- """Ensure the client logs directory exists."""
78
- try:
79
- self.log_dir.mkdir(parents=True, exist_ok=True)
80
- self.logger.debug(f"Client logs directory ensured: {self.log_dir}")
81
- except Exception as e:
82
- self.logger.error(f"Error creating client logs directory: {e}")
83
- raise
84
-
85
- def _get_log_file_path(self, browser_id: str) -> Path:
86
- """Get log file path for a browser session.
87
-
88
- Args:
89
- browser_id: Unique browser identifier
90
-
91
- Returns:
92
- Path to the log file
93
- """
94
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
95
- log_filename = f"{browser_id}_{timestamp}.log"
96
- return self.log_dir / log_filename
97
-
98
- def _write_log_entry(
99
- self,
100
- browser_id: str,
101
- level: str,
102
- message: str,
103
- timestamp: str = None,
104
- extra_data: Dict = None,
105
- ):
106
- """Write a log entry to the browser's log file.
107
-
108
- Args:
109
- browser_id: Browser identifier
110
- level: Log level (INFO, ERROR, etc.)
111
- message: Log message
112
- timestamp: Event timestamp (ISO format)
113
- extra_data: Additional event data
114
- """
115
- try:
116
- # Get or create log file for this browser
117
- if browser_id not in self.log_files:
118
- self.log_files[browser_id] = self._get_log_file_path(browser_id)
119
-
120
- log_file = self.log_files[browser_id]
121
-
122
- # Format timestamp
123
- if not timestamp:
124
- timestamp = datetime.now().isoformat()
125
-
126
- # Format log entry
127
- log_entry = f"[{timestamp}] [{level}] [{browser_id}] {message}"
128
-
129
- # Add extra data if provided
130
- if extra_data:
131
- filtered_data = {
132
- k: v
133
- for k, v in extra_data.items()
134
- if k not in ["browser_id", "level", "timestamp", "message"]
135
- }
136
- if filtered_data:
137
- log_entry += f"\n Data: {json.dumps(filtered_data, indent=2)}"
138
-
139
- # Write to file
140
- with open(log_file, "a", encoding="utf-8") as f:
141
- f.write(log_entry + "\n")
142
-
143
- self.logger.debug(f"Log entry written for {browser_id}: {level}")
144
-
145
- except Exception as e:
146
- self.logger.error(f"Error writing log entry for {browser_id}: {e}")
147
-
148
- async def handle_browser_connect(self, sid: str, data: Dict):
149
- """Handle browser connection event.
150
-
151
- Args:
152
- sid: Socket.IO session ID
153
- data: Browser connection data
154
- """
155
- try:
156
- browser_id = data.get("browser_id")
157
- if not browser_id:
158
- self.logger.warning(f"Browser connect without ID from {sid}")
159
- return
160
-
161
- # Track browser session
162
- self.active_browsers.add(browser_id)
163
-
164
- # Store browser info
165
- browser_info = {
166
- "browser_id": browser_id,
167
- "socket_id": sid,
168
- "connected_at": datetime.now().isoformat(),
169
- "user_agent": data.get("user_agent", "Unknown"),
170
- "url": data.get("url", "Unknown"),
171
- "last_activity": datetime.now().isoformat(),
172
- }
173
- self.browser_info[browser_id] = browser_info
174
-
175
- # Log connection
176
- self._write_log_entry(
177
- browser_id,
178
- "INFO",
179
- f'Browser connected from {data.get("url", "unknown URL")}',
180
- data.get("timestamp"),
181
- browser_info,
182
- )
183
-
184
- self.logger.info(f"Browser connected: {browser_id} from {data.get('url')}")
185
-
186
- # Send acknowledgment
187
- await self.sio.emit(
188
- "browser:connected",
189
- {
190
- "browser_id": browser_id,
191
- "status": "connected",
192
- "message": "Console monitoring active",
193
- },
194
- room=sid,
195
- )
196
-
197
- # Broadcast browser count update to dashboard
198
- await self._broadcast_browser_stats()
199
-
200
- except Exception as e:
201
- self.logger.error(f"Error handling browser connect: {e}")
202
-
203
- async def handle_browser_disconnect(self, sid: str, data: Dict):
204
- """Handle browser disconnection event.
205
-
206
- Args:
207
- sid: Socket.IO session ID
208
- data: Browser disconnection data
209
- """
210
- try:
211
- browser_id = data.get("browser_id")
212
- if not browser_id:
213
- return
214
-
215
- # Log disconnection
216
- self._write_log_entry(
217
- browser_id, "INFO", "Browser disconnected", data.get("timestamp")
218
- )
219
-
220
- # Update browser info
221
- if browser_id in self.browser_info:
222
- self.browser_info[browser_id][
223
- "disconnected_at"
224
- ] = datetime.now().isoformat()
225
- self.browser_info[browser_id]["status"] = "disconnected"
226
-
227
- self.logger.info(f"Browser disconnected: {browser_id}")
228
-
229
- # Broadcast browser count update
230
- await self._broadcast_browser_stats()
231
-
232
- except Exception as e:
233
- self.logger.error(f"Error handling browser disconnect: {e}")
234
-
235
- async def handle_browser_hide(self, sid: str, data: Dict):
236
- """Handle browser hide event (tab hidden/mobile app backgrounded).
237
-
238
- Args:
239
- sid: Socket.IO session ID
240
- data: Browser hide data
241
- """
242
- try:
243
- browser_id = data.get("browser_id")
244
- if not browser_id:
245
- return
246
-
247
- # Log hide event
248
- self._write_log_entry(
249
- browser_id,
250
- "INFO",
251
- "Browser tab hidden/backgrounded",
252
- data.get("timestamp"),
253
- )
254
-
255
- # Update browser info
256
- if browser_id in self.browser_info:
257
- self.browser_info[browser_id]["hidden_at"] = datetime.now().isoformat()
258
- self.browser_info[browser_id]["status"] = "hidden"
259
-
260
- self.logger.debug(f"Browser hidden: {browser_id}")
261
-
262
- except Exception as e:
263
- self.logger.error(f"Error handling browser hide: {e}")
264
-
265
- async def handle_console_event(self, sid: str, data: Dict):
266
- """Handle console event from browser.
267
-
268
- Args:
269
- sid: Socket.IO session ID
270
- data: Console event data
271
- """
272
- try:
273
- browser_id = data.get("browser_id")
274
- level = data.get("level", "LOG")
275
- message = data.get("message", "")
276
- timestamp = data.get("timestamp")
277
-
278
- if not browser_id:
279
- self.logger.warning(f"Console event without browser ID from {sid}")
280
- return
281
-
282
- # Update last activity
283
- if browser_id in self.browser_info:
284
- self.browser_info[browser_id][
285
- "last_activity"
286
- ] = datetime.now().isoformat()
287
-
288
- # Log console event
289
- self._write_log_entry(browser_id, level, message, timestamp, data)
290
-
291
- # Log to main logger based on level
292
- log_message = f"[{browser_id}] {message}"
293
- if level == "ERROR":
294
- self.logger.error(log_message)
295
- elif level == "WARN":
296
- self.logger.warning(log_message)
297
- else:
298
- self.logger.debug(log_message)
299
-
300
- # Format log entry for dashboard
301
- log_entry = {
302
- "browser_id": browser_id,
303
- "level": level,
304
- "message": message,
305
- "timestamp": timestamp,
306
- "url": data.get("url"),
307
- "line_info": data.get("line_info"),
308
- }
309
-
310
- # Forward event to dashboard clients using both event names for compatibility
311
- await self.sio.emit("dashboard:browser:console", log_entry)
312
- await self.sio.emit("browser_log", log_entry)
313
-
314
- except Exception as e:
315
- self.logger.error(f"Error handling console event: {e}")
316
-
317
- async def handle_browser_list(self, sid: str, data: Dict):
318
- """Handle browser list request.
319
-
320
- Args:
321
- sid: Socket.IO session ID
322
- data: Request data
323
- """
324
- try:
325
- browser_list = []
326
- for browser_id, info in self.browser_info.items():
327
- browser_list.append(
328
- {
329
- "browser_id": browser_id,
330
- "url": info.get("url", "Unknown"),
331
- "user_agent": info.get("user_agent", "Unknown"),
332
- "connected_at": info.get("connected_at"),
333
- "last_activity": info.get("last_activity"),
334
- "status": info.get("status", "active"),
335
- }
336
- )
337
-
338
- await self.sio.emit(
339
- "browser:list:response",
340
- {
341
- "browsers": browser_list,
342
- "total": len(browser_list),
343
- "active": len(self.active_browsers),
344
- },
345
- room=sid,
346
- )
347
-
348
- except Exception as e:
349
- self.logger.error(f"Error getting browser list: {e}")
350
- await self.sio.emit(
351
- "browser:error", {"error": f"Browser list error: {e!s}"}, room=sid
352
- )
353
-
354
- async def handle_browser_info(self, sid: str, data: Dict):
355
- """Handle browser info request.
356
-
357
- Args:
358
- sid: Socket.IO session ID
359
- data: Request data containing browser_id
360
- """
361
- try:
362
- browser_id = data.get("browser_id")
363
- if not browser_id or browser_id not in self.browser_info:
364
- await self.sio.emit(
365
- "browser:error", {"error": "Browser not found"}, room=sid
366
- )
367
- return
368
-
369
- info = self.browser_info[browser_id]
370
-
371
- # Add log file info
372
- log_file_path = self.log_files.get(browser_id)
373
- if log_file_path and log_file_path.exists():
374
- info["log_file"] = str(log_file_path)
375
- info["log_size"] = log_file_path.stat().st_size
376
-
377
- await self.sio.emit("browser:info:response", info, room=sid)
378
-
379
- except Exception as e:
380
- self.logger.error(f"Error getting browser info: {e}")
381
- await self.sio.emit(
382
- "browser:error", {"error": f"Browser info error: {e!s}"}, room=sid
383
- )
384
-
385
- async def _broadcast_browser_stats(self):
386
- """Broadcast browser statistics to all dashboard clients."""
387
- try:
388
- stats = {
389
- "total_browsers": len(self.browser_info),
390
- "active_browsers": len(self.active_browsers),
391
- "connected_browsers": len(
392
- [
393
- info
394
- for info in self.browser_info.values()
395
- if info.get("status") != "disconnected"
396
- ]
397
- ),
398
- }
399
-
400
- await self.sio.emit("dashboard:browser:stats", stats)
401
-
402
- except Exception as e:
403
- self.logger.error(f"Error broadcasting browser stats: {e}")
404
-
405
- def cleanup_old_sessions(self, max_age_hours: int = 24):
406
- """Clean up old browser sessions and log files.
407
-
408
- Args:
409
- max_age_hours: Maximum age in hours before cleanup
410
- """
411
- try:
412
- from datetime import timedelta
413
-
414
- cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
415
-
416
- # Remove old browser info
417
- to_remove = []
418
- for browser_id, info in self.browser_info.items():
419
- try:
420
- last_activity = datetime.fromisoformat(
421
- info.get("last_activity", "")
422
- )
423
- if last_activity < cutoff_time:
424
- to_remove.append(browser_id)
425
- except (ValueError, TypeError):
426
- # Invalid timestamp, mark for removal
427
- to_remove.append(browser_id)
428
-
429
- for browser_id in to_remove:
430
- self.browser_info.pop(browser_id, None)
431
- self.active_browsers.discard(browser_id)
432
- self.log_files.pop(browser_id, None)
433
-
434
- if to_remove:
435
- self.logger.info(f"Cleaned up {len(to_remove)} old browser sessions")
436
-
437
- except Exception as e:
438
- self.logger.error(f"Error cleaning up browser sessions: {e}")
439
-
440
- def get_stats(self) -> Dict:
441
- """Get handler statistics.
442
-
443
- Returns:
444
- Dictionary with handler stats
445
- """
446
- return {
447
- "total_browsers": len(self.browser_info),
448
- "active_browsers": len(self.active_browsers),
449
- "log_files": len(self.log_files),
450
- "log_directory": str(self.log_dir),
451
- }