callstacking-rails 0.1.0

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.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Callstacking::Rails
2
+
3
+ Callstacking is a rolling, checkpoint debugger for Rails. It records all of the critical method calls within your app, along with their important context (param/argument/return/local variable values).
4
+
5
+ You no longer need to debug with `binding.pry` or `puts` statements, as the entire callstack for a given request is captured.
6
+
7
+ Demo video:
8
+ [![Watch the video](https://user-images.githubusercontent.com/4600/190929740-fc68e18f-9572-41be-9719-cc6a8077e97f.png)](https://www.youtube.com/watch?v=NGqnwcNWv_k)
9
+
10
+ Class method calls are labeled. Return values for those calls are denoted with ↳
11
+
12
+ Arguments for a method will be listed along with their calling values.
13
+
14
+ For method returns ↳, the final values of the local variables will be listed when you hover over the entry.
15
+
16
+ <img width="1695" alt="CleanShot 2022-09-17 at 21 10 32@2x" src="https://user-images.githubusercontent.com/4600/190882603-a99e9358-9754-4cbf-ac68-a41d53afe747.png">
17
+
18
+ Subsequent calls within a method are visibly nested.
19
+
20
+ Callstacking is a Rails engine that you mount within your Rails app.
21
+
22
+ Here's a sample debugging sessions recorded from a Jumpstart Rails based app I've been working on. This is a request for the main page ( https://smartk.id/ ).
23
+
24
+ ![image](https://user-images.githubusercontent.com/4600/190882432-58092e38-7ee2-4138-b13a-f45ff2b09227.png)
25
+
26
+ Callstacking Rails records all of the critical method calls within your app, along with their important context (param/argument/return/local variable values).
27
+
28
+ All in a rolling panel, so that you can debug your call chains from any point in the stack.
29
+
30
+ You'll never have to debug with `puts` statements ever again.
31
+
32
+ Calls are visibly nested so that it's easy to see which calls are issued from which parent methods.
33
+
34
+ ## Installation
35
+ Add this line to your application's Gemfile:
36
+
37
+ ```ruby
38
+ gem "callstacking-rails"
39
+ ```
40
+
41
+ And then execute:
42
+ ```bash
43
+ $ bundle
44
+ ```
45
+
46
+ Register an account at Callstacking.com
47
+ ```bash
48
+ callstacking-rails register
49
+ ```
50
+
51
+ Authenticate to your newly created account.
52
+
53
+ ```bash
54
+ callstacking-rails setup
55
+ ```
56
+
57
+ You're now ready to start tracing.
58
+
59
+ ## Usage
60
+ Usage:
61
+
62
+ > callstacking-rails enable
63
+
64
+ Enables the callstacking tracing.
65
+
66
+ > callstacking-rails disable
67
+
68
+ Disables the callstacking tracing.
69
+
70
+ > callstacking-rails register
71
+
72
+ Opens a browser window to register as a callstacking.com user.
73
+
74
+ > callstacking-rails setup
75
+
76
+ Interactively prompts you for your callstacking.com username/password.
77
+ Stores auth details in `~/.callstacking-rails`.
78
+
79
+ You can have multiple environments.
80
+ The default is `development`.
81
+
82
+ The `development:` section in the `~/.callstacking-rails` config contains your credentials.
83
+
84
+ By setting the RAILS_ENV environment you can maintain multiple settings.
85
+
86
+ Questions? Create an issue: https://github.com/callstacking/callstacking-rails/issues
87
+
88
+
89
+ ## Trace Output
90
+ When you open a page for your app, once the page has rendered, you will see an `<<` arrow on the right hand side.
91
+
92
+ Click the arrows, and observe the full callstack context.
93
+
94
+ ## License
95
+ The gem is available as open source under the terms of the [GPLv3 License](https://www.gnu.org/licenses/gpl-3.0.en.html).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/callstacking/rails .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,6 @@
1
+ module Callstacking
2
+ module Rails
3
+ class ApplicationController < ActionController::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Callstacking
2
+ module Rails
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Callstacking
2
+ module Rails
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Callstacking
2
+ module Rails
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: "from@example.com"
5
+ layout "mailer"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Callstacking
2
+ module Rails
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Callstacking::Rails::Engine.routes.draw do
2
+ resources :traces
3
+ root to: "traces#index"
4
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "callstacking/rails/setup"
5
+ require "callstacking/rails/settings"
6
+
7
+ include Callstacking::Rails::Settings
8
+
9
+ action = ARGV[0]&.downcase&.strip
10
+
11
+ if action.nil?
12
+ Callstacking::Rails::Setup.instructions
13
+ exit!(1)
14
+ end
15
+
16
+ read_settings
17
+
18
+ case action
19
+ when 'register'
20
+ `open #{settings.url || Callstacking::Rails::Settings::PRODUCTION_URL}/users/sign_up`
21
+
22
+ when 'setup'
23
+ Callstacking::Rails::Setup.new.start
24
+
25
+ when 'enable'
26
+ Callstacking::Rails::Setup.new.enable_disable
27
+ puts "Callstacking tracing enabled (#{Callstacking::Rails::Env.environment})"
28
+
29
+ when 'disable'
30
+ Callstacking::Rails::Setup.new.enable_disable(enabled: false)
31
+ puts "Callstacking tracing disabled (#{Callstacking::Rails::Env.environment})"
32
+ end
@@ -0,0 +1,23 @@
1
+ require 'json'
2
+ require "callstacking/rails/client/base"
3
+
4
+ module Callstacking
5
+ module Rails
6
+ module Client
7
+ class Error < StandardError; end
8
+
9
+ class Authenticate < Base
10
+ URL = "/api/v1/auth.json"
11
+
12
+ def login(email, password)
13
+ resp = post(URL, email: email, password: password)
14
+
15
+ raise Faraday::UnauthorizedError if resp&.body.nil?
16
+
17
+ body = resp&.body || {}
18
+ body["token"]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,56 @@
1
+ require 'faraday'
2
+ require 'faraday/follow_redirects'
3
+ require "callstacking/rails/settings"
4
+
5
+ module Callstacking
6
+ module Rails
7
+ module Client
8
+ class Error < StandardError; end
9
+
10
+ class Base
11
+ include Callstacking::Rails::Settings
12
+
13
+ def initialize
14
+ read_settings
15
+ end
16
+
17
+ def connection
18
+ # https://github.com/lostisland/awesome-faraday
19
+ @connection ||= Faraday.new(url) do |c|
20
+ c.response :json
21
+ c.use Faraday::Response::Logger, Logger.new('/tmp/callstacking-rails.log')
22
+ # c.use Faraday::Response::Logger, nil, { headers: false, bodies: false }
23
+ c.response :follow_redirects
24
+ c.use Faraday::Response::RaiseError # raise exceptions on 40x, 50x responses
25
+ c.request :json # This will set the "Content-Type" header to application/json and call .to_json on the body
26
+ c.adapter Faraday.default_adapter
27
+
28
+ if auth_token?
29
+ c.request :authorization, :Bearer, auth_token
30
+ end
31
+ end
32
+ end
33
+
34
+ def get(url, params = {})
35
+ connection.get(url, params, headers)
36
+ end
37
+
38
+ def post(url, params = {}, body = {}, headers_ext = {})
39
+ r(:post, url, params, body, headers_ext)
40
+ end
41
+
42
+ def patch(url, params = {}, body = {}, headers_ext = {})
43
+ r(:patch, url, params, body, headers_ext)
44
+ end
45
+
46
+ def r(action, url, params = {}, body = {}, _headers_ext = {})
47
+ connection.send(action, url) do |req|
48
+ req.params.merge!(params)
49
+ req.body = body
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ require "callstacking/rails/client/base"
2
+
3
+ module Callstacking
4
+ module Rails
5
+ module Client
6
+ class Trace < Base
7
+ CREATE_URL = "/api/v1/traces.json"
8
+ UPDATE_URL = "/api/v1/traces/:id.json"
9
+
10
+ def create(method_name, klass, action_name, format_name, root_path, url, request_id, headers, params)
11
+ resp = post(CREATE_URL,
12
+ {},
13
+ { method_name: method_name,
14
+ klass: klass,
15
+ action_name: action_name,
16
+ format_name: format_name,
17
+ root_path: root_path,
18
+ url: url,
19
+ request_id: request_id,
20
+ h: headers.to_h,
21
+ p: params.to_h,
22
+ })
23
+
24
+ return resp.body["trace_id"], resp.body["pulse_interval"]
25
+ end
26
+
27
+ def upsert(trace_id, traces)
28
+ resp = patch(UPDATE_URL.gsub(':id', trace_id), {}, traces)
29
+
30
+ resp.status
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ require "rails"
2
+ require "callstacking/rails/trace"
3
+ require "callstacking/rails/instrument"
4
+ require 'callstacking/rails/spans'
5
+ require "callstacking/rails/setup"
6
+ require "callstacking/rails/settings"
7
+ require "callstacking/rails/loader"
8
+ require "callstacking/rails/client/base"
9
+ require "callstacking/rails/client/authenticate"
10
+ require "callstacking/rails/client/trace"
11
+ require "callstacking/rails/traces_helper"
12
+
13
+ module Callstacking
14
+ module Rails
15
+ class Engine < ::Rails::Engine
16
+ include Settings
17
+ isolate_namespace Callstacking::Rails
18
+
19
+ read_settings
20
+
21
+ spans = Spans.new
22
+ trace = Trace.new(spans)
23
+
24
+ initializer "engine_name.assets.precompile" do |app|
25
+ app.config.assets.precompile << "checkpoint_rails_manifest.js"
26
+ end
27
+
28
+ initializer 'local_helper.action_controller' do
29
+ ActiveSupport.on_load :action_controller do
30
+ helper Callstacking::Rails::TracesHelper
31
+ include Callstacking::Rails::TracesHelper
32
+ end
33
+
34
+ Callstacking::Rails::Loader.new.on_load(spans) if enabled?
35
+ end
36
+
37
+ initializer :append_before_action do
38
+ ActionController::Base.send :after_action, :inject_hud
39
+ end
40
+
41
+ if enabled?
42
+ puts "Callstacking enabled (#{Callstacking::Rails::Env.environment})"
43
+ trace.tracing
44
+ else
45
+ puts "Callstacking disabled (#{Callstacking::Rails::Env.environment})"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ require "active_support/inflector"
2
+
3
+ module Callstacking
4
+ module Rails
5
+ class Env
6
+ DEFAULT_ENVIRONMENT = "development"
7
+
8
+ cattr_accessor :environment
9
+
10
+ @@environment = (ENV['RAILS_ENV'] || DEFAULT_ENVIRONMENT).parameterize(separator: '_').to_sym
11
+
12
+ def self.production?
13
+ @@environment == DEFAULT_ENVIRONMENT.parameterize(separator: '_').to_sym
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,88 @@
1
+ require 'rails'
2
+
3
+ # https://stackoverflow.com/q/52932516
4
+ module Callstacking
5
+ module Rails
6
+ class Instrument
7
+ attr_accessor :spans, :klass
8
+ attr_reader :root
9
+
10
+ def initialize(spans, klass)
11
+ @spans = spans
12
+ @klass = klass
13
+ @root = Regexp.new(::Rails.root.to_s)
14
+ end
15
+
16
+ def instrument_method(method_name, application_level: true)
17
+ method_path = (klass.instance_method(method_name).source_location.first rescue nil) ||
18
+ (klass.method(method_name).source_location.first rescue nil)
19
+
20
+ # Application level method definitions
21
+ return unless method_path =~ root if application_level
22
+
23
+ tmp_module = find_or_initialize_module
24
+
25
+ return if tmp_module.instance_methods.include?(method_name) ||
26
+ tmp_module.singleton_methods.include?(method_name)
27
+
28
+ tmp_module.define_method(method_name) do |*args, &block|
29
+ method_name = __method__
30
+
31
+ path = method(__method__).super_method.source_location.first
32
+ line_no = method(__method__).super_method.source_location.last
33
+
34
+ p, l = caller.find { |c| c.to_s =~ /#{::Rails.root.to_s}/}&.split(':')
35
+
36
+ spans = tmp_module.instance_variable_get(:@spans)
37
+ klass = tmp_module.instance_variable_get(:@klass)
38
+
39
+ spans.call_entry(klass, method_name, p || path, l || line_no)
40
+ return_val = super(*args, &block)
41
+ spans.call_return(klass, method_name, p || path, l || line_no, return_val)
42
+
43
+ return_val
44
+ end
45
+ end
46
+
47
+ def find_or_initialize_module
48
+ module_name = "#{klass.name.gsub('::', '')}Span"
49
+ module_index = klass.ancestors.map(&:to_s).index(module_name)
50
+
51
+ unless module_index
52
+ new_module = Object.const_set(module_name, Module.new)
53
+
54
+ new_module.instance_variable_set("@klass", klass)
55
+ new_module.instance_variable_set("@spans", spans)
56
+
57
+ klass.prepend new_module
58
+ klass.singleton_class.prepend new_module if klass.class == Module
59
+
60
+ return find_or_initialize_module
61
+ end
62
+
63
+ klass.ancestors[module_index]
64
+ end
65
+
66
+ def instrument_klass(application_level: true)
67
+ relevant = all_methods - filtered
68
+ relevant.each { |method| instrument_method(method, application_level: application_level) }
69
+ end
70
+
71
+ private
72
+
73
+ def all_methods
74
+ @all_methods ||= (klass.instance_methods +
75
+ klass.private_instance_methods(false) +
76
+ klass.protected_instance_methods(false) +
77
+ klass.methods +
78
+ klass.singleton_methods).uniq
79
+ end
80
+
81
+ def filtered
82
+ @filtered ||= (Object.instance_methods + Object.private_instance_methods +
83
+ Object.protected_instance_methods + Object.methods(false)).uniq
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,29 @@
1
+ require "rails"
2
+
3
+ module Callstacking
4
+ module Rails
5
+ class Loader
6
+ attr_accessor :loader, :root, :once
7
+ def initialize
8
+ @root = Regexp.new(::Rails.root.to_s)
9
+ end
10
+
11
+ def on_load(spans)
12
+ trace = TracePoint.new(:end) do |tp|
13
+ klass = tp.self
14
+ path = tp.path
15
+
16
+ Instrument.new(spans, klass).instrument_klass if path =~ root
17
+ end
18
+
19
+ trace.enable
20
+
21
+ Instrument.new(spans, ActionView::PartialRenderer).instrument_method(:render,
22
+ application_level: false)
23
+
24
+ Instrument.new(spans, ActionView::TemplateRenderer).instrument_method( :render,
25
+ application_level: false)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,54 @@
1
+ require "active_support/concern"
2
+ require "active_support/core_ext/class/attribute_accessors"
3
+ require "callstacking/rails/env"
4
+
5
+ module Callstacking
6
+ module Rails
7
+ module Settings
8
+ extend ActiveSupport::Concern
9
+
10
+ included do |base|
11
+ attr_accessor :settings
12
+ end
13
+
14
+ SETTINGS_FILE = "#{Dir.home}/.callstacking-rails"
15
+ PRODUCTION_URL = "https://callstacking.com"
16
+
17
+ def url
18
+ settings[:url]
19
+ end
20
+
21
+ def auth_token
22
+ settings[:auth_token]
23
+ end
24
+
25
+ def auth_token?
26
+ auth_token.present?
27
+ end
28
+
29
+ def write_settings(new_settings)
30
+ File.write(SETTINGS_FILE, new_settings.to_yaml)
31
+ end
32
+
33
+ def enabled?
34
+ settings[:enabled]
35
+ end
36
+
37
+ def disabled?
38
+ !enabled?
39
+ end
40
+
41
+ def read_settings
42
+ @@settings = @settings = complete_settings.dig(::Callstacking::Rails::Env.environment, :settings)
43
+ rescue StandardError => e
44
+ puts e.full_message
45
+ puts e.backtrace.join("\n")
46
+ return {}
47
+ end
48
+
49
+ def complete_settings
50
+ YAML.load(File.read(SETTINGS_FILE)) rescue {}
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,115 @@
1
+ require 'yaml'
2
+ require "callstacking/rails/settings"
3
+ require "callstacking/rails/client/authenticate"
4
+ require "callstacking/rails/env"
5
+
6
+ module Callstacking
7
+ module Rails
8
+ class Setup
9
+ include ::Callstacking::Rails::Settings
10
+ extend ::Callstacking::Rails::Settings
11
+
12
+ attr_accessor :client
13
+
14
+ def initialize
15
+ read_settings
16
+ end
17
+
18
+ def client
19
+ @client = Callstacking::Rails::Client::Authenticate.new
20
+ end
21
+
22
+ def start
23
+ email = prompt("Enter email:")
24
+ password = prompt("Enter password:")
25
+
26
+ url = if Callstacking::Rails::Env.production? && ENV['CHECKPOINT_RAILS_LOCAL_TEST'].nil?
27
+ PRODUCTION_URL
28
+ else
29
+ prompt("Enter URL for #{Callstacking::Rails::Env.environment} API calls [#{PRODUCTION_URL}]:") || PRODUCTION_URL
30
+ end
31
+
32
+ save(email, password, url)
33
+
34
+ puts "Authentication successful."
35
+ puts "Settings saved to #{SETTINGS_FILE}"
36
+ rescue StandardError => e
37
+ puts "Problem authenticating: #{e.message}"
38
+ end
39
+
40
+ def enable_disable(enabled: true)
41
+ settings[:enabled] = enabled
42
+
43
+ props = { Callstacking::Rails::Env.environment => {
44
+ settings: settings
45
+ } }
46
+
47
+ write_settings(complete_settings.merge(props))
48
+ end
49
+
50
+ def prompt(label)
51
+ puts label
52
+ value = STDIN.gets.chomp
53
+ puts
54
+
55
+ return nil if value == ''
56
+ value
57
+ end
58
+
59
+ def token(email, password)
60
+ client.login(email, password)
61
+ end
62
+
63
+ def save(email, password, url)
64
+ props = { auth_token: '',
65
+ url: url,
66
+ enabled: true
67
+ }
68
+
69
+ props = { Callstacking::Rails::Env.environment => {
70
+ settings: props
71
+ } }
72
+
73
+ write_settings(complete_settings.merge(props))
74
+
75
+ props[Callstacking::Rails::Env.environment][:settings][:auth_token] = token(email, password)
76
+
77
+ write_settings(complete_settings.merge(props))
78
+
79
+ read_settings
80
+ end
81
+
82
+ def self.instructions
83
+ read_settings
84
+ puts "loading environment #{Callstacking::Rails::Env.environment}"
85
+ puts
86
+ puts "Usage: "
87
+ puts
88
+ puts " > callstacking-rails enable"
89
+ puts
90
+ puts " Enables the callstacking tracing."
91
+ puts
92
+ puts " > callstacking-rails disable"
93
+ puts
94
+ puts " Disables the callstacking tracing."
95
+ puts
96
+ puts " > callstacking-rails register"
97
+ puts
98
+ puts " Opens a browser window to register as a callstacking.com user."
99
+ puts
100
+ puts " > callstacking-rails setup"
101
+ puts
102
+ puts " Interactively prompts you for your callstacking.com username/password."
103
+ puts " Stores auth details in #{SETTINGS_FILE}"
104
+ puts
105
+ puts " You can have multiple environments."
106
+ puts " The default is #{Callstacking::Rails::Env::DEFAULT_ENVIRONMENT}."
107
+ puts
108
+ puts " The #{Callstacking::Rails::Env.environment}: section in the #{SETTINGS_FILE} contains your credentials."
109
+ puts " By setting the RAILS_ENV environment you can maintain multiple settings."
110
+ puts
111
+ puts "Questions? Create an issue: https://github.com/callstacking/callstacking-rails/issues"
112
+ end
113
+ end
114
+ end
115
+ end