CrackedRuby logo

CrackedRuby

pry-byebug

Overview

Pry-byebug integrates the pry REPL with byebug's debugging capabilities, creating a debugging environment that supports breakpoints, step-through execution, and interactive code evaluation. The gem extends pry with additional commands for controlling program execution flow, examining stack frames, and inspecting variable states during runtime.

The integration operates through several core components. The Pry class gains debugging commands like step, next, continue, and finish. The PryByebug module manages debugger state and coordinates between pry sessions and byebug's execution control. The Breakpoints class handles breakpoint creation, modification, and removal across the debugging session.

require 'pry-byebug'

def calculate_total(items)
  binding.pry  # Debugger entry point
  total = 0
  items.each do |item|
    total += item[:price] * item[:quantity]
  end
  total
end

items = [
  { name: 'Widget', price: 10.50, quantity: 2 },
  { name: 'Gadget', price: 25.00, quantity: 1 }
]

result = calculate_total(items)
# => Drops into pry-byebug session at binding.pry

When execution reaches binding.pry, the program pauses and opens an interactive session. The debugger displays the current line, surrounding context, and provides access to all variables and methods in the current scope. The session accepts both regular Ruby code and debugging commands.

class OrderProcessor
  def initialize(orders)
    @orders = orders
    @processed = []
  end

  def process_all
    @orders.each_with_index do |order, index|
      binding.pry if order[:amount] > 1000  # Conditional debugging
      processed_order = process_order(order)
      @processed << processed_order
    end
  end

  private

  def process_order(order)
    # Processing logic here
    order.merge(processed_at: Time.now)
  end
end

The debugger maintains full access to instance variables, local variables, and method definitions. Commands execute within the current binding, allowing inspection of object state and method invocation during program execution.

# During debugging session, all these work:
self                    # => Current object instance
@orders                 # => Instance variable access
local_variables         # => List of local variables
order                   # => Current local variable value
process_order(order)    # => Method invocation

Basic Usage

Pry-byebug requires adding binding.pry statements to source code at points where debugging should begin. The debugger activates when Ruby execution encounters these statements, opening an interactive session with access to the current execution context.

require 'pry-byebug'

def fibonacci(n)
  return n if n <= 1
  
  binding.pry  # Debug session starts here
  
  a, b = 0, 1
  (2..n).each do |i|
    a, b = b, a + b
    binding.pry if i == n - 1  # Conditional breakpoint
  end
  b
end

fibonacci(8)

Inside the debugging session, execution control commands manage program flow. The step command advances one line of execution, entering method calls when encountered. The next command advances one line without entering method calls. The continue command resumes normal execution until the next breakpoint or program termination.

# Debugging session commands:
step        # Execute next line, step into methods
next        # Execute next line, step over methods  
continue    # Resume execution until next breakpoint
finish      # Complete current method and return to caller
up          # Move up one frame in call stack
down        # Move down one frame in call stack

The debugger provides stack frame navigation for examining the call hierarchy. Each frame represents a method invocation with its own variable scope and execution context. The up and down commands navigate between frames while maintaining access to frame-specific variables and methods.

def method_a(value)
  processed = value * 2
  method_b(processed)
end

def method_b(input)
  binding.pry  # When this activates:
  result = input + 10
  method_c(result)
end

def method_c(final)
  final.to_s
end

method_a(5)
# In debugging session:
# up      # Navigate to method_a frame, access 'processed' variable
# down    # Return to method_b frame

Variable inspection works through direct reference or helper methods. Local variables, instance variables, and constants remain accessible during debugging. The ls command lists available methods and variables for the current object or class.

class DataProcessor
  VALID_FORMATS = ['json', 'xml', 'csv'].freeze

  def initialize(format)
    @format = format
    @errors = []
  end

  def process(data)
    binding.pry
    validate_format
    # During debugging:
    # @format          # Instance variable access
    # @errors          # Instance variable access  
    # VALID_FORMATS    # Constant access
    # data             # Parameter access
    # ls               # List methods and variables
    # ls DataProcessor # List class methods
  end

  private

  def validate_format
    unless VALID_FORMATS.include?(@format)
      @errors << "Invalid format: #{@format}"
    end
  end
end

Code evaluation during debugging accepts any valid Ruby expression. Method calls, variable assignments, and object modifications execute within the current binding, allowing runtime experimentation and state manipulation.

def calculate_discount(price, discount_rate)
  binding.pry
  discounted_price = price * (1 - discount_rate)
  discounted_price.round(2)
end

calculate_discount(100, 0.15)

# In debugging session:
# price                           # => 100
# discount_rate                   # => 0.15
# price * discount_rate          # => 15.0  
# discount_rate = 0.20           # Modify variable
# discounted_price = price * 0.8 # Create new variable

Advanced Usage

Pry-byebug supports remote debugging for applications running in different processes or containers. The pry-remote functionality creates debugging servers that accept connections from external pry clients, enabling debugging of deployed applications or background processes.

require 'pry-byebug'
require 'pry-remote'

class BackgroundWorker
  def process_queue
    loop do
      job = fetch_next_job
      if job && job[:debug]
        binding.remote_pry  # Remote debugging entry point
      end
      process_job(job) if job
      sleep 1
    end
  rescue => e
    binding.remote_pry  # Debug exceptions remotely
    raise
  end

  private

  def fetch_next_job
    # Queue polling logic
  end

  def process_job(job)
    # Job processing logic
  end
end

# Start remote debugging server
worker = BackgroundWorker.new
worker.process_queue

Remote debugging sessions maintain full debugging capabilities while running in separate processes. The debugging server accepts multiple client connections, allowing collaborative debugging sessions with shared breakpoints and execution control.

Conditional breakpoints provide fine-grained debugging control by activating only when specified conditions evaluate to true. Complex conditions can examine variable states, method return values, or object relationships before triggering debugging sessions.

class OrderAnalyzer
  def analyze_orders(orders)
    orders.each_with_index do |order, index|
      # Multiple conditional breakpoint strategies
      binding.pry if order[:total] > 5000                    # Value threshold
      binding.pry if order[:customer_type] == 'premium'       # String matching
      binding.pry if index > 100 && order[:total] < 50       # Complex conditions
      binding.pry if suspicious_order?(order)                # Method-based conditions
      
      process_order(order)
    end
  end

  private

  def suspicious_order?(order)
    order[:total] > 10000 && 
    order[:items].length < 3 && 
    order[:customer_id].start_with?('temp_')
  end

  def process_order(order)
    # Order processing logic
  end
end

The debugger supports breakpoint management through programmatic interfaces. Breakpoints can be set, listed, disabled, and removed during debugging sessions or through application code. The Pry::Byebug::Breakpoints class provides methods for breakpoint manipulation.

# Programmatic breakpoint management
def setup_debugging
  # Set breakpoint at specific file and line
  Pry::Byebug::Breakpoints.add_file(__FILE__, 45)
  
  # Set method breakpoint
  Pry::Byebug::Breakpoints.add_method(OrderProcessor, :process_order)
  
  # Conditional breakpoint with custom logic
  Pry::Byebug::Breakpoints.add_file(__FILE__, 50) do |binding|
    binding.local_variable_get(:user_id) == 'admin'
  end
end

# During debugging session:
# breakpoints        # List all active breakpoints
# break --condition  # Add conditional breakpoint
# break --delete 1   # Remove breakpoint by number
# break --disable 2  # Disable breakpoint without deletion

Thread debugging requires special consideration since pry-byebug operates within single thread contexts. Multi-threaded applications need coordination mechanisms to avoid debugging session conflicts and ensure thread-safe debugging practices.

require 'thread'

class ThreadedProcessor
  def initialize
    @queue = Queue.new
    @debug_mutex = Mutex.new
    @threads = []
  end

  def start_workers(count)
    count.times do |i|
      @threads << Thread.new do
        loop do
          job = @queue.pop
          break if job == :shutdown
          
          # Thread-safe debugging
          if job[:debug] && @debug_mutex.try_lock
            begin
              binding.pry  # Only one thread debugs at a time
            ensure
              @debug_mutex.unlock
            end
          end
          
          process_job(job)
        end
      end
    end
  end

  def add_job(job)
    @queue.push(job)
  end

  def shutdown
    @threads.length.times { @queue.push(:shutdown) }
    @threads.each(&:join)
  end

  private

  def process_job(job)
    # Job processing implementation
  end
end

Exception debugging intercepts raised exceptions before they propagate, providing debugging access at the exact point of failure. The rescue clause can include debugging statements to examine variables and execution state when exceptions occur.

class ErrorProneService
  def risky_operation(data)
    begin
      validate_data(data)
      transform_data(data)
      save_data(data)
    rescue StandardError => e
      # Debug at exception point with full context
      binding.pry
      
      # Examine exception details and local state
      # e.class           # Exception type
      # e.message         # Exception message  
      # e.backtrace       # Full stack trace
      # data              # Original input data
      # local_variables   # All local variables
      
      # Decide whether to re-raise or handle
      raise if data[:critical]
      log_error(e, data)
    end
  end

  private

  def validate_data(data)
    raise ArgumentError, "Invalid data format" unless data.is_a?(Hash)
  end

  def transform_data(data)
    # Transformation that might fail
  end

  def save_data(data)
    # Persistence that might fail  
  end

  def log_error(error, context)
    # Error logging implementation
  end
end

Testing Strategies

Testing pry-byebug integration requires careful isolation to prevent debugging sessions from interrupting test execution. Test environments need configuration that disables interactive debugging while maintaining code coverage and functionality testing.

# spec/spec_helper.rb
require 'pry-byebug'

RSpec.configure do |config|
  # Disable pry in test environment
  config.before(:each) do
    allow_any_instance_of(Binding).to receive(:pry).and_return(nil)
    allow_any_instance_of(Binding).to receive(:remote_pry).and_return(nil)
  end
  
  # Alternative: conditional debugging based on environment
  config.before(:each) do
    ENV['ENABLE_PRY_IN_TESTS'] ||= 'false'
  end
end

# Test with conditional debugging
def debug_enabled?
  ENV['ENABLE_PRY_IN_TESTS'] == 'true'
end

def conditional_pry
  binding.pry if debug_enabled?
end

Mock debugging scenarios test application behavior under debugging conditions without triggering actual debugging sessions. Stubbing binding.pry calls allows testing of debugging-related logic and ensuring applications handle debugging gracefully.

class DebugAwareService
  def process_with_debug(data, debug: false)
    validate_input(data)
    
    binding.pry if debug
    
    result = transform_data(data)
    
    binding.pry if debug && result.nil?
    
    save_result(result) unless result.nil?
    result
  end

  private

  def validate_input(data); end
  def transform_data(data); end  
  def save_result(result); end
end

# Testing debugging integration
RSpec.describe DebugAwareService do
  let(:service) { described_class.new }
  let(:test_data) { { id: 1, value: 'test' } }

  describe '#process_with_debug' do
    context 'when debugging enabled' do
      it 'calls binding.pry at appropriate points' do
        expect_any_instance_of(Binding).to receive(:pry).twice
        service.process_with_debug(test_data, debug: true)
      end

      it 'processes data correctly even with debug calls' do
        allow_any_instance_of(Binding).to receive(:pry)
        result = service.process_with_debug(test_data, debug: true)
        expect(result).not_to be_nil
      end
    end

    context 'when debugging disabled' do
      it 'does not call binding.pry' do
        expect_any_instance_of(Binding).not_to receive(:pry)
        service.process_with_debug(test_data, debug: false)
      end
    end
  end
end

Integration testing with debugging requires test isolation strategies that prevent debugging sessions from blocking test execution while ensuring debugging functionality works correctly in development environments.

# Integration test for debugging-enabled controllers
RSpec.describe OrdersController, type: :controller do
  describe 'debugging integration' do
    context 'in development environment' do
      before do
        allow(Rails.env).to receive(:development?).and_return(true)
        allow_any_instance_of(Binding).to receive(:pry).and_return(nil)
      end

      it 'processes orders with debugging support' do
        order_params = { 
          items: [{ name: 'Test', price: 10.99, quantity: 2 }],
          debug: true 
        }
        
        post :create, params: { order: order_params }
        
        expect(response).to have_http_status(:created)
        expect(Order.count).to eq(1)
      end
    end

    context 'in production environment' do  
      before do
        allow(Rails.env).to receive(:production?).and_return(true)
      end

      it 'ignores debug parameters in production' do
        order_params = {
          items: [{ name: 'Test', price: 10.99, quantity: 2 }],
          debug: true
        }
        
        # Should not attempt debugging in production
        expect_any_instance_of(Binding).not_to receive(:pry)
        
        post :create, params: { order: order_params }
        expect(response).to have_http_status(:created)
      end
    end
  end
end

Testing debugging utilities requires creating controlled scenarios where debugging behavior can be verified without interrupting test flow. Custom debugging helpers and debugging state management need comprehensive testing coverage.

class DebuggingHelper
  class << self
    def debug_if(condition)
      binding.pry if condition && debugging_enabled?
    end

    def debug_on_error
      yield
    rescue StandardError => e
      binding.pry if debugging_enabled?
      raise
    end

    def debugging_enabled?
      ENV['DEBUG'] == 'true' && !Rails.env.test?
    end

    def capture_debug_context(binding_context)
      {
        local_variables: binding_context.local_variables,
        instance_variables: binding_context.receiver.instance_variables,
        method_name: binding_context.frame_description
      }
    end
  end
end

# Testing debugging utilities
RSpec.describe DebuggingHelper do
  describe '.debug_if' do
    context 'when condition is true and debugging enabled' do
      before do
        allow(described_class).to receive(:debugging_enabled?).and_return(true)
        allow_any_instance_of(Binding).to receive(:pry)
      end

      it 'triggers debugging session' do
        expect_any_instance_of(Binding).to receive(:pry)
        described_class.debug_if(true)
      end
    end

    context 'when condition is false' do
      it 'does not trigger debugging' do
        expect_any_instance_of(Binding).not_to receive(:pry)
        described_class.debug_if(false)
      end
    end
  end

  describe '.debug_on_error' do
    context 'when block raises error and debugging enabled' do
      before do
        allow(described_class).to receive(:debugging_enabled?).and_return(true)
        allow_any_instance_of(Binding).to receive(:pry)
      end

      it 'triggers debugging before re-raising' do
        expect_any_instance_of(Binding).to receive(:pry)
        
        expect {
          described_class.debug_on_error { raise StandardError, 'test error' }
        }.to raise_error(StandardError, 'test error')
      end
    end
  end

  describe '.capture_debug_context' do
    it 'captures binding context information' do
      test_var = 'local_value'
      context = described_class.capture_debug_context(binding)
      
      expect(context[:local_variables]).to include(:test_var)
      expect(context[:method_name]).to include('capture_debug_context')
    end
  end
end

Production Patterns

Production debugging requires careful implementation to avoid security vulnerabilities and performance degradation. Remote debugging capabilities should include authentication, connection limits, and automatic session timeouts to prevent unauthorized access and resource exhaustion.

class SecureRemoteDebugger
  AUTHORIZED_IPS = ['10.0.0.0/24', '192.168.1.0/24'].freeze
  MAX_CONCURRENT_SESSIONS = 3
  SESSION_TIMEOUT = 300 # 5 minutes

  class << self
    def enable_if_authorized(ip_address, user_credentials)
      return false unless authorized_ip?(ip_address)
      return false unless valid_credentials?(user_credentials)
      return false if session_limit_reached?

      enable_remote_debugging
      schedule_session_cleanup
      true
    end

    def secure_breakpoint(condition: nil, user: nil)
      return unless production_debugging_enabled?
      return unless authorized_user?(user)
      
      if condition.nil? || condition.call
        binding.remote_pry
      end
    rescue => e
      Rails.logger.error "Remote debugging error: #{e.message}"
    end

    private

    def authorized_ip?(ip_address)
      AUTHORIZED_IPS.any? { |range| IPAddr.new(range).include?(ip_address) }
    end

    def valid_credentials?(credentials)
      # Implement secure credential validation
      BCrypt::Password.new(ENV['DEBUG_PASSWORD_HASH']) == credentials
    end

    def session_limit_reached?
      active_remote_sessions.count >= MAX_CONCURRENT_SESSIONS
    end

    def active_remote_sessions
      # Track active debugging sessions
      @sessions ||= []
    end

    def enable_remote_debugging
      require 'pry-remote'
      # Configure secure remote debugging
    end

    def schedule_session_cleanup
      Thread.new do
        sleep SESSION_TIMEOUT
        cleanup_expired_sessions
      end
    end

    def production_debugging_enabled?
      ENV['PRODUCTION_DEBUG'] == 'true' && Rails.env.production?
    end

    def authorized_user?(user)
      user&.admin? || user&.developer?
    end
  end
end

Application monitoring integration captures debugging session activity, performance metrics, and security events. Debugging sessions should log access attempts, duration, and actions performed for audit compliance and security monitoring.

class DebugMonitor
  include Singleton

  def initialize
    @sessions = []
    @metrics = Hash.new(0)
  end

  def track_session_start(user:, ip:, context:)
    session = {
      id: SecureRandom.uuid,
      user: user,
      ip: ip,
      context: context,
      started_at: Time.current,
      commands: []
    }
    
    @sessions << session
    @metrics[:total_sessions] += 1
    
    log_session_event(:started, session)
    notify_monitoring_system(:debug_session_started, session)
    
    session[:id]
  end

  def track_command(session_id, command, result)
    session = find_session(session_id)
    return unless session

    command_record = {
      command: command,
      executed_at: Time.current,
      result_type: result.class.name,
      success: !result.is_a?(Exception)
    }

    session[:commands] << command_record
    @metrics[:total_commands] += 1
    @metrics[:failed_commands] += 1 if result.is_a?(Exception)

    log_command_execution(session, command_record)
  end

  def track_session_end(session_id)
    session = find_session(session_id)
    return unless session

    session[:ended_at] = Time.current
    session[:duration] = session[:ended_at] - session[:started_at]

    log_session_event(:ended, session)
    generate_session_report(session)
    cleanup_session(session_id)
  end

  def current_metrics
    {
      active_sessions: @sessions.count { |s| s[:ended_at].nil? },
      total_sessions: @metrics[:total_sessions],
      total_commands: @metrics[:total_commands],
      failed_commands: @metrics[:failed_commands],
      average_session_duration: calculate_average_duration
    }
  end

  private

  def find_session(session_id)
    @sessions.find { |s| s[:id] == session_id }
  end

  def log_session_event(event, session)
    Rails.logger.info({
      event: "debug_session_#{event}",
      session_id: session[:id],
      user: session[:user]&.id,
      ip: session[:ip],
      context: session[:context],
      timestamp: Time.current
    }.to_json)
  end

  def log_command_execution(session, command_record)
    Rails.logger.debug({
      event: 'debug_command_executed',
      session_id: session[:id],
      command: command_record[:command],
      success: command_record[:success],
      timestamp: command_record[:executed_at]
    }.to_json)
  end

  def notify_monitoring_system(event, data)
    # Integration with monitoring tools (DataDog, New Relic, etc.)
    Metrics.increment("debug.#{event}")
    Metrics.histogram('debug.session_duration', data[:duration]) if data[:duration]
  end

  def generate_session_report(session)
    # Generate detailed session report for audit purposes
    report = {
      session_summary: session.except(:commands),
      command_summary: {
        total_commands: session[:commands].length,
        successful_commands: session[:commands].count { |c| c[:success] },
        failed_commands: session[:commands].count { |c| !c[:success] }
      },
      security_flags: detect_security_concerns(session)
    }

    store_audit_report(report)
  end

  def detect_security_concerns(session)
    concerns = []
    
    # Detect suspicious patterns
    concerns << :excessive_commands if session[:commands].length > 100
    concerns << :long_duration if session[:duration] > 1800 # 30 minutes
    concerns << :system_access if session[:commands].any? { |c| 
      dangerous_patterns = ['system', 'exec', '`', 'File.', 'Dir.']
      dangerous_patterns.any? { |pattern| c[:command].include?(pattern) }
    }
    
    concerns
  end

  def cleanup_session(session_id)
    @sessions.reject! { |s| s[:id] == session_id }
  end

  def calculate_average_duration
    completed_sessions = @sessions.select { |s| s[:duration] }
    return 0 if completed_sessions.empty?
    
    total_duration = completed_sessions.sum { |s| s[:duration] }
    total_duration / completed_sessions.length
  end

  def store_audit_report(report)
    # Store in audit database or send to compliance system
    AuditLog.create!(
      event_type: 'debug_session_completed',
      details: report,
      created_at: Time.current
    )
  end
end

Performance optimization for debugging-enabled applications requires careful consideration of debugging overhead and resource usage. Conditional debugging, lazy evaluation, and resource cleanup prevent debugging infrastructure from impacting production performance.

class PerformantDebugger
  class << self
    def smart_debug(threshold: 0.1, &block)
      start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      
      result = block.call
      
      execution_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
      
      if execution_time > threshold
        binding.pry  # Debug slow operations
      end
      
      result
    end

    def memory_aware_debug(max_objects: 1000)
      initial_objects = ObjectSpace.count_objects[:T_OBJECT]
      
      yield
      
      final_objects = ObjectSpace.count_objects[:T_OBJECT]
      objects_created = final_objects - initial_objects
      
      if objects_created > max_objects
        GC.start  # Force garbage collection before debugging
        binding.pry
      end
    end

    def conditional_remote_debug(condition_proc)
      # Avoid remote debugging overhead unless condition met
      return unless condition_proc.call
      return unless remote_debugging_available?
      
      # Lazy load remote debugging dependencies
      require 'pry-remote' unless defined?(PryRemote)
      
      binding.remote_pry
    rescue LoadError => e
      Rails.logger.warn "Remote debugging unavailable: #{e.message}"
    end

    def resource_limited_debug(max_memory_mb: 100)
      memory_usage = get_memory_usage_mb
      
      if memory_usage < max_memory_mb
        binding.pry
      else
        Rails.logger.warn "Debugging skipped: memory usage #{memory_usage}MB exceeds limit"
      end
    end

    private

    def remote_debugging_available?
      # Check if remote debugging infrastructure is ready
      ENV['REMOTE_DEBUG_HOST'] && ENV['REMOTE_DEBUG_PORT']
    end

    def get_memory_usage_mb
      # Get current process memory usage
      status_file = "/proc/#{Process.pid}/status"
      return 0 unless File.exist?(status_file)
      
      File.readlines(status_file).each do |line|
        if line.start_with?('VmRSS:')
          return line.split[1].to_i / 1024  # Convert KB to MB
        end
      end
      
      0
    end
  end
end

# Usage in performance-critical applications
class HighVolumeProcessor
  def process_batch(items)
    PerformantDebugger.smart_debug(threshold: 0.05) do
      items.each_slice(100) do |batch|
        PerformantDebugger.memory_aware_debug(max_objects: 500) do
          process_item_batch(batch)
        end
      end
    end
  end

  def handle_critical_error(error, context)
    PerformantDebugger.resource_limited_debug(max_memory_mb: 50) do
      # Debug critical errors with memory constraints
      analyze_error_context(error, context)
    end
  end

  private

  def process_item_batch(batch)
    # High-volume processing logic
  end

  def analyze_error_context(error, context)
    # Error analysis logic
  end
end

Common Pitfalls

Debugging session interference occurs when multiple debugging entry points activate simultaneously, creating nested debugging contexts that confuse execution flow and variable access. Nested binding.pry calls create stacked debugging sessions that require careful navigation to avoid losing execution context.

class ProblematicDebugging
  def outer_method(data)
    processed_data = preprocess(data)
    binding.pry  # First debugging entry point
    
    result = inner_method(processed_data)
    
    binding.pry  # Second debugging entry point - creates confusion
    result
  end

  def inner_method(data)
    binding.pry  # Third debugging entry point - nested debugging
    
    data.each do |item|
      binding.pry if item[:debug]  # Fourth potential entry point
      transform_item(item)
    end
  end

  private

  def preprocess(data); end
  def transform_item(item); end
end

# Better approach: Controlled debugging with flags
class ImprovedDebugging
  def initialize(debug_level: 0)
    @debug_level = debug_level
    @debug_stack = []
  end

  def outer_method(data)
    debug_context(:outer_method) do
      processed_data = preprocess(data)
      controlled_debug(level: 1)  # Only debug if level >= 1
      
      result = inner_method(processed_data)
      
      controlled_debug(level: 2)  # Only debug if level >= 2  
      result
    end
  end

  def inner_method(data)
    debug_context(:inner_method) do
      controlled_debug(level: 3)
      
      data.each_with_index do |item, index|
        conditional_debug(item[:debug] && index < 5)  # Limit debugging iterations
        transform_item(item)
      end
    end
  end

  private

  def debug_context(method_name)
    @debug_stack.push(method_name)
    yield
  ensure
    @debug_stack.pop
  end

  def controlled_debug(level:)
    return unless @debug_level >= level
    return if @debug_stack.length > 3  # Prevent deep nesting
    
    binding.pry
  end

  def conditional_debug(condition)
    return unless condition
    return if currently_debugging?
    
    binding.pry
  end

  def currently_debugging?
    # Check if already in a debugging session
    caller.any? { |frame| frame.include?('pry') }
  end
end

Variable modification during debugging sessions can create inconsistent program state that leads to unexpected behavior when execution resumes. Debugging sessions allow complete variable access, including modification of local variables, instance variables, and object state.

class StateModificationIssues
  def calculate_total(items, tax_rate)
    subtotal = 0
    
    items.each do |item|
      binding.pry  # Dangerous debugging point
      # During debugging, developer might modify variables:
      # subtotal = 1000    # Accidental modification
      # tax_rate = 0       # Testing different values
      # items.clear        # Destructive operation
      
      subtotal += item[:price] * item[:quantity]
    end
    
    total = subtotal * (1 + tax_rate)
    total.round(2)
  end
end

# Safer debugging approach with state protection
class ProtectedStateDebugging
  def calculate_total(items, tax_rate)
    # Create debugging snapshot
    original_state = {
      items_count: items.length,
      tax_rate: tax_rate,
      items_checksum: items.hash
    }
    
    subtotal = 0
    
    items.each do |item|
      debug_with_protection(original_state) do
        # Debugging within protected context
      end
      
      subtotal += item[:price] * item[:quantity]
    end
    
    validate_state_integrity(original_state, items, tax_rate)
    
    total = subtotal * (1 + tax_rate)
    total.round(2)
  end

  private

  def debug_with_protection(original_state)
    if debugging_enabled?
      puts "=== DEBUGGING SESSION START ==="
      puts "WARNING: Modifying variables may cause unexpected behavior"
      puts "Original state: #{original_state}"
      binding.pry
      puts "=== DEBUGGING SESSION END ==="
    end
  end

  def validate_state_integrity(original_state, items, tax_rate)
    inconsistencies = []
    
    inconsistencies << "items count changed" if items.length != original_state[:items_count]
    inconsistencies << "tax rate modified" if tax_rate != original_state[:tax_rate]  
    inconsistencies << "items content modified" if items.hash != original_state[:items_checksum]
    
    if inconsistencies.any?
      Rails.logger.warn "State modified during debugging: #{inconsistencies.join(', ')}"
    end
  end

  def debugging_enabled?
    defined?(Pry) && ENV['DEBUG'] == 'true'
  end
end

Thread safety violations occur when debugging multi-threaded applications without proper synchronization. Debugging sessions operate within single thread contexts, but shared state modifications during debugging can create race conditions and data corruption in concurrent environments.

class ThreadSafetyProblems
  def initialize
    @shared_counter = 0
    @shared_data = {}
  end

  def concurrent_processing(items)
    threads = items.each_slice(10).map do |batch|
      Thread.new do
        batch.each do |item|
          binding.pry if item[:debug]  # DANGEROUS: No thread synchronization
          
          # These operations are not thread-safe during debugging:
          @shared_counter += 1           # Race condition
          @shared_data[item[:id]] = item # Concurrent hash modification
        end
      end
    end
    
    threads.each(&:join)
  end
end

# Thread-safe debugging implementation
class ThreadSafeDebugging
  def initialize
    @shared_counter = Concurrent::AtomicFixnum.new(0)
    @shared_data = Concurrent::Map.new
    @debug_mutex = Mutex.new
    @debug_semaphore = Mutex.new
  end

  def concurrent_processing(items)
    threads = items.each_slice(10).map.with_index do |batch, thread_id|
      Thread.new do
        Thread.current[:id] = thread_id
        
        batch.each do |item|
          thread_safe_debug(item) if item[:debug]
          
          # Thread-safe operations
          @shared_counter.increment
          @shared_data[item[:id]] = item
        end
      end
    end
    
    threads.each(&:join)
  end

  private

  def thread_safe_debug(item)
    # Ensure only one thread debugs at a time
    return unless @debug_semaphore.try_lock
    
    begin
      thread_info = {
        thread_id: Thread.current[:id],
        thread_object_id: Thread.current.object_id,
        shared_counter_value: @shared_counter.value,
        shared_data_size: @shared_data.size
      }
      
      puts "=== THREAD-SAFE DEBUGGING ==="
      puts "Thread info: #{thread_info}"
      puts "Item: #{item}"
      puts "WARNING: Shared state modifications may cause race conditions"
      
      # Create thread-local copy for safe modification
      local_item = item.dup
      binding.pry
      
    ensure
      @debug_semaphore.unlock
    end
  rescue ThreadError => e
    Rails.logger.warn "Thread debugging conflict: #{e.message}"
  end
end

Performance degradation from debugging overhead becomes problematic in high-throughput applications when debugging statements remain active in production code. Each binding.pry call incurs method call overhead and string evaluation costs even when debugging is disabled.

class PerformanceDegradation
  def high_frequency_method(data)
    # These debugging calls execute even when not debugging
    binding.pry if Rails.env.development?     # Environment check overhead
    binding.pry if data[:debug]               # Hash key lookup overhead
    binding.pry if should_debug?(data)        # Method call overhead
    
    # Performance-critical processing
    process_data(data)
  end

  def should_debug?(data)
    # Complex condition evaluation happens every call
    data[:user_id] == 'admin' && 
    data[:created_at] > 1.hour.ago &&
    Rails.cache.read("debug_mode_#{data[:user_id]}")
  end
end

# Optimized debugging approach
class OptimizedDebugging
  # Class-level debugging configuration
  DEBUG_ENABLED = Rails.env.development? && ENV['ENABLE_DEBUG'] == 'true'
  DEBUG_CACHE_TTL = 300 # 5 minutes
  
  def initialize
    @debug_conditions_cache = {}
    @cache_expires_at = {}
  end

  def high_frequency_method(data)
    # Fast path: skip debugging entirely if disabled globally
    return process_data(data) unless DEBUG_ENABLED
    
    # Cached condition evaluation
    if should_debug_cached?(data)
      binding.pry
    end
    
    process_data(data)
  end

  private

  def should_debug_cached?(data)
    cache_key = "debug_condition_#{data[:user_id]}"
    current_time = Time.current
    
    # Check cache expiration
    if @cache_expires_at[cache_key] && current_time > @cache_expires_at[cache_key]
      @debug_conditions_cache.delete(cache_key)
      @cache_expires_at.delete(cache_key)
    end
    
    # Return cached result if available
    return @debug_conditions_cache[cache_key] if @debug_conditions_cache.key?(cache_key)
    
    # Evaluate condition and cache result
    result = evaluate_debug_condition(data)
    @debug_conditions_cache[cache_key] = result
    @cache_expires_at[cache_key] = current_time + DEBUG_CACHE_TTL
    
    result
  end

  def evaluate_debug_condition(data)
    # Complex condition evaluation only when cache miss occurs
    data[:user_id] == 'admin' && 
    data[:created_at] > 1.hour.ago &&
    Rails.cache.read("debug_mode_#{data[:user_id]}")
  end

  def process_data(data)
    # Performance-critical processing implementation
  end
end

# Compile-time debugging removal for production
class ProductionOptimized
  if Rails.env.production?
    # Define no-op debugging methods for production
    def debug_breakpoint; end
    def conditional_debug(condition); end
  else
    def debug_breakpoint
      binding.pry
    end
    
    def conditional_debug(condition)
      binding.pry if condition
    end
  end

  def critical_method(data)
    debug_breakpoint  # No overhead in production
    
    # Critical processing
    process_data(data)
    
    conditional_debug(data[:suspicious])  # No overhead in production
  end
end

Reference

Core Debugging Commands

Command Parameters Returns Description
step None nil Execute next line, entering method calls
next None nil Execute next line, stepping over method calls
continue None nil Resume execution until next breakpoint
finish None nil Complete current method and return to caller
up [count] nil Move up call stack frames
down [count] nil Move down call stack frames
break file:line | Class#method Breakpoint Set breakpoint at location
breakpoints None Array<Breakpoint> List all active breakpoints

Binding Methods

Method Parameters Returns Description
binding.pry None nil Start debugging session at current location
binding.remote_pry host, port nil Start remote debugging session
local_variables None Array<Symbol> List local variables in current binding
local_variable_get(name) Symbol Object Get local variable value
local_variable_set(name, value) Symbol, Object Object Set local variable value

Breakpoint Management

Method Parameters Returns Description
Pry::Byebug::Breakpoints.add_file(file, line) String, Integer Breakpoint Add file breakpoint
Pry::Byebug::Breakpoints.add_method(klass, method) Class, Symbol Breakpoint Add method breakpoint
Pry::Byebug::Breakpoints.delete(id) Integer Boolean Remove breakpoint by ID
Pry::Byebug::Breakpoints.disable(id) Integer Boolean Disable breakpoint
Pry::Byebug::Breakpoints.enable(id) Integer Boolean Enable breakpoint

Configuration Options

Option Type Default Description
Pry.config.auto_indent Boolean true Automatic code indentation
Pry.config.correct_indent Boolean true Correct indentation on paste
Pry.config.collision_warning Boolean false Warn about method name collisions
Pry.config.should_load_rc Boolean true Load .pryrc configuration file
Pry.config.should_load_local_rc Boolean true Load local .pryrc files

Environment Variables

Variable Values Description
PRY 0, 1 Enable/disable pry loading
PRY_BYEBUG 0, 1 Enable/disable pry-byebug features
DISABLE_PRY 1 Disable all pry functionality
PRY_RESCUE 0, 1 Enable automatic exception debugging

Exception Classes

Exception Description
Pry::CommandError Invalid debugging command
Pry::SyntaxError Syntax error in debugging session
PryByebug::Breakpoints::FileNotFoundError Breakpoint file not found

Debugging Session States

State Description
running Normal program execution
suspended Debugging session active
stepping Step-by-step execution mode
breaking Stopped at breakpoint
finishing Completing current method

Remote Debugging Configuration

Setting Type Description
host String Remote debugging server host
port Integer Remote debugging server port
wait Boolean Wait for client connection
quiet Boolean Suppress connection messages

Stack Frame Information

Method Returns Description
frame_description String Current frame method and location
frame_type Symbol Frame type (:method, :block, :rescue)
source_location Array File path and line number