Skip to content
Joey Wang
Menu

Search

Full Stack Development

How to Set a Breakpoint on Devise's current_user Method

Why byebug can't break on Devise's metaprogrammed current_user method, and several ways to debug or patch a method that isn't in your source.

· 4 min read

rails #rails#ruby#debugging#metaprogramming#devise

Audio summary

Devise is a Rails authentication framework built on Warden. It gives you current_user (and current_admin, or current_whatever for any model you configure) without writing that method yourself, because it generates it dynamically for every model you set up for authentication.

That’s convenient right up until you need to set a breakpoint inside current_user and byebug refuses to stop there.

Why byebug can’t find the method

current_user doesn’t exist in Devise’s source the way you’d expect. What’s actually there is a template:

def #{mapping}_signed_in?
  !!current_#{mapping}
end

def current_#{mapping}
  @current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end

Devise interpolates mapping (user, admin, whatever you configured) and defines the real method at runtime with define_method. The file and line byebug is looking at is the template, not the generated method, so a breakpoint set there never fires.

The debug gem handles this correctly

Ruby 3.1 made debug the standard replacement for byebug, and it can set breakpoints on methods defined at runtime, which byebug cannot. It’s also faster and supports remote debugging out of the box.

# Gemfile
gem 'debug', '>= 1.0.0'
def some_method
  debugger
  # rest of the method
end

Or start it from the command line with rails server --debugger, then break on the dynamically generated method directly:

debugger.break MyController, :current_user

The trade-off is mostly familiarity: if your muscle memory is byebug commands, there’s a short relearning curve, and as of early Rails 7 some byebug-specific integrations hadn’t fully caught up.

Wrapping the method yourself when a debugger won’t do

Sometimes you want more than a breakpoint: you want to log every call, or trace what’s calling current_user and from where. Ruby’s metaprogramming gives you a few ways to swap the method temporarily and put it back.

Dynamic replacement, from outside the method:

# Get the original method
original_method = method(:current_user)

# Define a new method with debugging
new_method = proc do |*args|
  puts "Entering current_user"
  result = original_method.call(*args)
  puts "Result: #{result.inspect}"
  debugger # or binding.pry
  result
end

# Replace the original method
define_method(:current_user, new_method)

# ... debugging ...

# Restore the original method
define_method(:current_user, original_method)

From inside the method itself, __method__ gets you the name without hardcoding it:

# Assuming we're inside the original current_user method

# Get the current method object
current_method = method(__method__)

# Define the new method
new_method = proc do |*args|
  puts "Entering modified current_user method"
  result = current_method.call(*args)
  puts "current_user result: #{result.inspect}"
  # debugger
  result
end

# Replace the method
self.class.send(:define_method, __method__, new_method)
self.class.send(:define_method, "old_#{__method__}", current_method)

# Restore the old method
old_method = method("old_#{__method__}")
self.class.send(:define_method, __method__, old_method)

self.class.send(:undef_method, "old_#{__method__}")

# Call the new method to continue execution
new_method.call

To restore the original afterward, either keep a reference to it as above, or pull it back from the superclass with superclass.instance_method.

Using alias_method

alias_method is the least ceremony for a quick, reversible wrap:

class ApplicationController < ActionController::Base
  # Create an alias for the original method
  alias_method :original_current_user, :current_user

  # Redefine current_user with debugging
  def current_user
    puts "Entering current_user method"
    result = original_current_user
    puts "current_user result: #{result.inspect}"
    debugger # or binding.pry
    result
  end
end

Restore it the same way you set it up:

class ApplicationController < ActionController::Base
  alias_method :current_user, :original_current_user
  remove_method :original_current_user
end

Or, using the instance method captured beforehand:

class ApplicationController < ActionController::Base
  original_method = instance_method(:current_user)
  undef_method :current_user
  define_method :current_user, original_method
end

alias_method keeps the method’s visibility (public, protected, or private), has a negligible performance cost, and only affects the class you define it in, not a superclass. Set the alias before any code calls the original method, or you’ll wrap nothing.

Using prepend and refine

prepend adds a module ahead of the class in the method lookup chain, so it modifies behavior across the whole hierarchy and cleans up with super:

module DebuggingModule
  def current_user
    puts "Entering current_user method"
    result = super
    puts "current_user result: #{result.inspect}"
    debugger # or binding.pry
    result
  end
end

class ApplicationController < ActionController::Base
  prepend DebuggingModule
end

refine scopes the change to wherever you using it, which makes it the safest option when you only want the modified behavior in one file:

module DebuggingRefinements
  refine ApplicationController do
    def current_user
      puts "Entering current_user method"
      result = super
      puts "current_user result: #{result.inspect}"
      debugger # or binding.pry
      result
    end
  end
end

# In the file or context where you want to use the debugging version:
using DebuggingRefinements

# The refined version of current_user will only be active in this file or block

Which one to reach for

alias_method for a quick, reversible check. prepend when you want the change everywhere the class is used. refine when you want it nowhere else. Dynamic define_method replacement when you need the most control and are willing to manage restoring the original method yourself.

All four exist because Devise, and plenty of other gems, generate real methods at runtime. Understanding that is what makes the rest of Ruby’s metaprogramming toolbox make sense, not just debugging Devise.