A confusing programmer front a computrt

Understanding complex Rails projects – Ruby on rails stack trace made easy

The challenge of understanding complex systems

Imagine a scenario where a project is so large and its features so complex that tracking and understanding the entire flow can take days. Working in such a scenario, especially if test coverage is low and the code lacks an organized architecture, can be a significant challenge. To simplify this process and help understand how a feature behaves, the rails_tracepoint_stack gem was created, dissecting the applications stack trace.

How the Rails backtrace can help us

Debugging applications without a tool to facilitate this process can be challenging, and one alternative is to use the debug gem along with the backtrace command. The backtrace command is capable of bringing the logs about executed code directly to the console of the debug gem. Using a scenario close to a real one, let’s use the Forem project, applying the backtrace command from the debug gem on the article creation action:

article = Articles::Creator.call(@user, article_params_json)
debugger

When executing the a request test:

rspec ./spec/requests/articles/articles_create_spec.rb:12

and the execution reaches the debugger method, a console will be opened, and we can execute the backtrace command:


(rdbg) backtrace    # command
=>#0    ArticlesController#create at /app/app/controllers/articles_controller.rb:151
  #1    ActionController::BasicImplicitRender#send_action(method="create", args=[]) at /usr/local/bundle/gems/actionpack-7.0.8.4/lib/action_controller/metal/basic_implicit_render.rb:6
  #2    AbstractController::Base#process_action at /usr/local/bundle/gems/actionpack-7.0.8.4/lib/abstract_controller/base.rb:215
  #3    Datadog::Tracing::Contrib::ActionPack::ActionController::Instrumentation::Metal#process_action(args=["create"]) at /usr/local/bundle/gems/ddtrace-1.16.2/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb:105
  #4    ActionController::Rendering#process_action at /usr/local/bundle/gems/actionpack-7.0.8.4/lib/action_controller/metal/rendering.rb:165

  < ... >

  #195  [C] Kernel#load at /usr/local/bundle/bin/bundle:25
  #196  <main> at /usr/local/bundle/bin/bundle:25

We can see that, although quite detailed, the output does not provide much insight into how the Articles::Creator class is functioning. Among the large quantity of logs provided, one alternative is to filter using regex. However, it is a challenge to define the ideal pattern to apply. Let’s try filtering the class name using regex:

(rdbg) backtrace /Articles/    # command
=>#0    ArticlesController#create at /app/app/controllers/articles_controller.rb:151

Again, the information returned by the backtrace feature provides few details, not focusing on the class’s internal implementation. In this article, we shall present the rails_tracepoint_stack gem and compare its backtrace to this one.

Automating the code exploration with RailsTracepointStack

This ruby library captures and filters the stack of method calls. By default, it shows only the stack of code created by the developer, along with the parameters passed to the source class or module and its full path. This helps the developer visualize the entire execution flow of the code during feature execution.

This gem has two basic configurations in Rails applications: globally enabling it by setting the RAILS_TRACEPOINT_STACK_ENABLED environment variable to true, which captures and displays the call stack of all code created by the developer until the application finishes; or inline by passing code blocks to the Tracer. Let’s consider a pretty simple scenario, where we have a module with the method, which is used by a class that includes it:

module Foo
    def puts_message(message)
        puts message
    end
end
class Bar
    include Foo

    def initialize(message:)
        @message = message
    end

    def perform
        RailsTracepointStack.enable_trace do
            puts_message(@message)
        end
    end
end

By adding your code inside the block

RailsTracepointStack.enable_trace do
    puts_message(@message)
end

and calling it

 Bar.new(message: "Hello World").perform

The gem will capture the entire call stack and filter by default only the stack related to the code implemented by the developer, ignoring code executed by possible gems or even modules of the language itself, providing a formatted output like:

called: Foo#puts_message in COMPLETE_PATH_TO_YOUR_FILE/app/services/foo:METHOD_LINE with params: {:message=>"Hello World"}

In this straightforward example, it’s easy to follow the code execution flow without using the gem’s assistance. However, in a larger project with numerous modules, callbacks, and conditionals, having an automated return and a well-formatted call stack is highly beneficial.

Let’s use the same scenario, using the Forem project, applying the RailsTracePoint.enable_trace function around the Articles::Creator call :

RailsTracepointStack.enable_trace do
    article = Articles::Creator.call(@user, article_params_json)
end

and let’s run the same request test again to generate info about the article creation:

rspec ./spec/requests/articles/articles_create_spec.rb:12

Examining the logs from the rails_tracepoint_stack.log file reveals a detailed execution of the programmer’s code implementation, providing a detailed execution of what was executed by the class Articles::Creator call, showing services, models, validators, etc called during the execution:

called: Articles::Creator#initialize in /app/app/services/articles/creator.rb:21 with params: {:user=>#<User id: 21, apple_username: nil, articles_count: 0, badge_achievements_count: 0, blocked_by_count: 0, blocking_others_count: 0, checked_code_of_conduct: true, checked_terms_and_conditions: true, comments_count: 0, created_at: "2024-08-01 20:57:59.911703109 -0230", credits_count: 0, email: "person1@example.com", export_requested: false, exported_at: nil, facebook_username: nil, feed_fetched_at: "2017-01-01 01:30:00.000000000 -0330", following_orgs_count: 0, following_tags_count: 0, following_users_count: 0, forem_username: nil, github_repos_updated_at: "2017-01-01 01:30:00.000000000 -0330", github_username: "github1", google_oauth2_created_at: nil, google_oauth2_username: nil, last_article_at: "2017-01-01 01:30:00.000000000 -0330", last_comment_at: "2017-01-01 01:30:00.000000000 -0330", last_followed_at: nil, last_moderation_notification: "2017-01-01 01:30:00.000000000 -0330", last_notification_activity: nil, last_onboarding_page: nil, last_reacted_at: nil, latest_article_updated_at: nil, name: "Margery \"Lester\" \\:/ Hansen", old_old_username: nil, old_username: nil, onboarding_package_requested: false, organization_info_updated_at: nil, payment_pointer: nil, profile_image: "16042b3c-131e-4639-9ce4-88e63620d770.jpeg", profile_updated_at: "2017-01-01 01:30:00.000000000 -0330", rating_votes_count: 0, reactions_count: 0, registered: true, registered_at: "2024-08-01 20:57:59.868828302 -0230", reputation_modifier: 1.0, saw_onboarding: true, score: 0, secret: nil, signup_cta_variant: "navbar_basic", spent_credits_count: 0, stripe_id_code: nil, subscribed_to_user_subscriptions_count: 0, twitter_username: "twitter1", unspent_credits_count: 0, updated_at: "2024-08-01 20:58:00.361210945 -0230", username: "username1">, :article_params=>#<ActionController::Parameters {"title"=>"NEW TITLE 61", "body_markdown"=>"Yo ho ho31", "tag_list"=>"yo", "published_at"=>nil} permitted: true>}
<...>
called: ApplicationRecord#decorate in /app/app/models/application_record.rb:108 with params: {}
<...> 
called: Article#evaluate_markdown in /app/app/models/article.rb:730 with params: {:content_renderer=>nil, :result=>nil, :front_matter=>nil, :e=>nil}
<...>
called: ##call in /app/app/services/markdown_processor/fixer/base.rb:16 with params: {:markdown=>"Yo ho ho65"}
<...>
called: ##processor in /app/app/services/content_renderer.rb:5 with params: {:value=>MarkdownProcessor::Parser}
<...>
called: Taggable#validate_tag_name in /app/app/models/concerns/taggable.rb:50 with params: {:tag_list=>["yo"]}
called: Tag#validate_name in /app/app/models/tag.rb:206 with params: {}
<...>

Using this same stack logs, we have another good example of useful information: the presence of model callbacks, as the ones called after creating an article (also there are other callbacks called before the saving):

 after_save :create_conditional_autovomits
 after_save :bust_cache
 after_save :collection_cleanup
 after_save :generate_social_image

which produced the logs:

<...>
 called: Article#create_conditional_autovomits in /app/app/models/article.rb:1028 with params: {}
 <...>
 called: Article#bust_cache in /app/app/models/article.rb:1006 with params: {:destroying=>false, :cache_bust=>nil}
 <...>
 called: Article#collection_cleanup in /app/app/models/article.rb:669 with params: {:collection=>nil}
 <...>
 called: Article#generate_social_image in /app/app/models/article.rb:1015 with params: {:change=>nil}
 <...>

Among others, these logs provide a detailed look at the code executed during the operation of this feature, including template callbacks (an interesting post by Codeminer42 here), which can sometimes run without the developer noticing.

Customizing RailsTracepointStack for your necessities

rails_tracepoint_stack also provides a configuration interface where you can:

  • Observe only stack calls from files where the path matches a specific pattern, providing a more targeted view of the code execution:
RailsTracepointStack.configure do |config|
    config.file_path_to_filter_patterns << /services\/foo.rb/
end
  • Ignore a predefined list of files based on patterns, such as
RailsTracepointStack.configure do |config|
    config.ignore_patterns << /services\/foo.rb/
end
  • Return the entire stack trace without filtering, including code from gems and the framework itself:
RailsTracepointStack.configure do |config|
    config.log_external_sources = false
end

Why use RailsTracePointStack instead of Debug#backtrace

RailsTracePointStack compiles information in a clear and organized manner while offering advanced filtering options that are easy to use. Compared to the Rails backtrace function, RailsTracePointStack provides:

  • By default, only shows code created by the developer, excluding code from libraries, modules from Ruby, etc.

  • Advanced filtering for excluding specific files and folders of the stack tracing;

  • Intuitive way to configure the tracer to catch traces just from specific folders;

  • Different trace output formats;

  • Intuitive way to trace specific blocks of code or even the entire application.

With the rails_tracepoint_stack gem and a few configurations, you should be able to understand how a complex system works, spend less time debugging and more time being assertive and effective with your project. Thank you for following along, and feel free to add rails_tracepoint_stack to your arsenal of gems!

We want to work with you. Check out our "What We Do" section!