@@ -281,6 +281,8 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
281281 "command_type" ,
282282 "error_type" ,
283283 "error_message" ,
284+ "shutdown_type" ,
285+ "exit_code" ,
284286 }
285287 for key in safe_keys :
286288 if key in properties :
@@ -326,26 +328,98 @@ def track_command(
326328 track_event (f"cli_{ command } " , properties )
327329
328330
329- def _sanitize_error_message (message : str ) -> str :
330- """Sanitize error message to remove sensitive information.
331+ def track_detailed_error (
332+ event_name : str ,
333+ error : Exception ,
334+ context : str | None = None ,
335+ operation : str | None = None ,
336+ additional_props : dict [str , Any ] | None = None ,
337+ ) -> None :
338+ """Track a detailed error with enhanced debugging information.
331339
332340 Args:
333- message: Raw error message
334-
335- Returns:
336- Sanitized error message
341+ event_name: Name of the error event (e.g., "cli_run_failed", "cli_build_failed")
342+ error: The exception that occurred
343+ context: Additional context about where the error occurred
344+ operation: The specific operation that failed
345+ additional_props: Additional properties to include
337346 """
347+ import traceback
348+ import time
349+
350+ properties = {
351+ "success" : False ,
352+ "error_type" : type (error ).__name__ ,
353+ "error_message" : _sanitize_error_message (str (error )),
354+ "timestamp" : int (time .time ()),
355+ }
356+
357+ # Add operation context
358+ if operation :
359+ properties ["operation" ] = operation
360+ if context :
361+ properties ["context" ] = context
362+
363+ # Add sanitized stack trace for debugging
364+ try :
365+ tb_lines = traceback .format_exception (type (error ), error , error .__traceback__ )
366+ # Get the last few frames (most relevant) and sanitize them
367+ relevant_frames = tb_lines [- 3 :] if len (tb_lines ) > 3 else tb_lines
368+ sanitized_trace = []
369+
370+ for frame in relevant_frames :
371+ # Sanitize file paths in stack trace
372+ sanitized_frame = _sanitize_error_message (frame .strip ())
373+ # Further sanitize common traceback patterns
374+ sanitized_frame = sanitized_frame .replace ('File "[PATH]' , 'File "[PATH]' )
375+ sanitized_trace .append (sanitized_frame )
376+
377+ properties ["stack_trace" ] = " | " .join (sanitized_trace )
378+
379+ # Add the specific line that caused the error if available
380+ if hasattr (error , '__traceback__' ) and error .__traceback__ :
381+ tb = error .__traceback__
382+ while tb .tb_next :
383+ tb = tb .tb_next
384+ properties ["error_line" ] = tb .tb_lineno
385+
386+ except Exception :
387+ # Don't fail if we can't capture stack trace
388+ pass
389+
390+ # Add system context for debugging
391+ try :
392+ properties ["python_executable" ] = _sanitize_error_message (platform .python_implementation ())
393+ properties ["platform_detail" ] = platform .platform ()[:50 ] # Limit length
394+ except Exception :
395+ pass
396+
397+ # Merge additional properties
398+ if additional_props :
399+ # Only include safe additional properties
400+ safe_additional_keys = {
401+ "exit_code" , "shutdown_type" , "environment" , "template" ,
402+ "build_env" , "transport" , "component_count" , "file_path" ,
403+ "component_type" , "validation_error" , "config_error"
404+ }
405+ for key , value in additional_props .items ():
406+ if key in safe_additional_keys :
407+ properties [key ] = value
408+
409+ track_event (event_name , properties )
410+
411+ def _sanitize_error_message (message : str ) -> str :
412+ """Sanitize error messages to remove sensitive information."""
338413 import re
339414
340- # Remove absolute file paths but keep the filename
341- # Unix-style paths
342- message = re .sub (
343- r'/(?:Users|home|var|tmp|opt|usr|etc)/[^\s"\']+/([^/\s"\']+)' , r"\1" , message
344- )
345- # Windows-style paths
346- message = re .sub (r'[A-Za-z]:\\[^\s"\']+\\([^\\s"\']+)' , r"\1" , message )
347- # Generic path pattern (catches remaining paths)
348- message = re .sub (r'(?:^|[\s"])(/[^\s"\']+/)+([^/\s"\']+)' , r"\2" , message )
415+ # Remove file paths but preserve filenames
416+ # Match paths with directories and capture the filename
417+ # Unix style: /path/to/file.py -> file.py
418+ message = re .sub (r"(/[^/\s]+)+/([^/\s]+)" , r"\2" , message )
419+ # Windows style: C:\path\to\file.py -> file.py
420+ message = re .sub (r"([A-Za-z]:\\[^\\]+\\)+([^\\]+)" , r"\2" , message )
421+ # Remaining absolute paths without filename
422+ message = re .sub (r"[/\\][^\s]*[/\\]" , "[PATH]/" , message )
349423
350424 # Remove potential API keys or tokens (common patterns)
351425 # Generic API keys (20+ alphanumeric with underscores/hyphens)
0 commit comments