debugmate 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eb9597a1a66b25a3f9b7eb95f5bb9eb23bd9a64b19a59e90746ab6c53566f722
4
+ data.tar.gz: 62f5458450e7a11831b5389f66a0590265909e27ec07d80bf22e4dc20c48314f
5
+ SHA512:
6
+ metadata.gz: e40d35a1bdd587f03d451ef5421babd974e5e1f04ee7e0c615706da47ac6c0779717613e4776b972007e91a2cc9d701d780c3712025b2a47139f52de3d44f6bf
7
+ data.tar.gz: 4b46cf9f78c1773157a58f6a685b564706743ca95654fe6189775d27f65796944f5d03f81828e999b53164c2f401dc1454bc057dc4af690edb966bce45e823c9
data/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # DebugMate
2
+ Exception handling for Ruby on Rails
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem "debugmate"
9
+ ```
10
+
11
+ While the gem is not published in RubyGems, you can install from this repo directly:
12
+
13
+ ```ruby
14
+ gem 'debugmate', :git => 'https://github.com/DebugMate/rails', :branch => 'main'
15
+ ```
16
+
17
+ You can also clone this repo and point the path to the gem if you want to contribute or test locally:
18
+
19
+ `../debugmate` is relative to the project and the folder name is the one you decided when cloning the repo.
20
+
21
+ ```ruby
22
+ gem "debugmate", :path => "../debugmate"
23
+ ```
24
+
25
+ And then execute:
26
+ ```bash
27
+ bundle
28
+ ```
29
+
30
+ Or install it yourself as:
31
+ ```bash
32
+ gem install debugmate
33
+ ```
34
+
35
+ ## Usage
36
+ Create `config/debugmate.yml` in your rails application and provide the following values:
37
+
38
+ ```yaml
39
+ data:
40
+ domain: http://debugmate.test
41
+ enabled: true
42
+ token: 9dd70ed8-f3b2-4244-890e-c4f666d108ed
43
+ ```
44
+
45
+ Create `config/initializers/debugmate.rb` with the following line:
46
+
47
+ ```ruby
48
+ DEBUGMATE_CONFIG = YAML.load_file("#{Rails.root}/config/debugmate.yml")
49
+ ```
50
+
51
+ ## Get the last executed query with the exception
52
+
53
+ To be able to get the query, you must add the following boilerplate code to the `debugmate.rb` initializer.
54
+
55
+ This is the only way to get Rails to intercept the call for the moment:
56
+
57
+ ```ruby
58
+ module ActiveRecord
59
+ class LogSubscriber < ActiveSupport::LogSubscriber
60
+ # this allows us to call the original sql method. Ruby does copy the original method on the fly
61
+ alias_method :original_sql, :sql
62
+
63
+ def sql(event)
64
+ Debugmate::ExceptionHandler.last_executed_query = event.payload[:sql] if DEBUGMATE_CONFIG['data']['enabled'] === true
65
+ Debugmate::ExceptionHandler.last_executed_binds = event.payload[:binds] if DEBUGMATE_CONFIG['data']['enabled'] === true
66
+ original_sql(event)
67
+ end
68
+ end
69
+ end
70
+ ```
71
+
72
+ Edit `config/application.rb` to use Debugmate.
73
+
74
+ Add the middleware inside the Application class, ideally as the last line, following the example:
75
+
76
+ ```ruby
77
+ module Blog
78
+ class Application < Rails::Application
79
+ # --- Here is the exiting code ---
80
+
81
+ config.middleware.use Debugmate::ExceptionHandler
82
+ end
83
+ end
84
+ ```
85
+
86
+ You can check that your setup is correct by issuing the test command as follows:
87
+
88
+ ```bash
89
+ rails debugmate:test
90
+ ```
91
+
92
+ ## Get the current User
93
+
94
+ As there are a lot of ways an user can be retrieved and maybe even the application has no user at all, if you want
95
+ to know the current user that caused the exception, you must provide a public method called `current_user` in your
96
+ `ApplicationController`.
97
+
98
+ This method must return a Hash with the `id, name and email`.
99
+
100
+ You can follow the example. Debugmate only needs the Hash:
101
+
102
+ ```ruby
103
+ class ApplicationController < ActionController::Base
104
+
105
+ def current_user
106
+ # --- some logic that gets the user for your application ---
107
+
108
+ user = {
109
+ id: 99,
110
+ name: "User from Rails",
111
+ email: "user@fromrails.com"
112
+ }
113
+ end
114
+
115
+ end
116
+ ```
117
+
118
+ ## Sending exceptions manually to Debugmate
119
+
120
+ If you want to catch an exception manually, you can do so using the `Publish` class. Just pass the exception and call
121
+ `execute`
122
+
123
+ ```ruby
124
+ def index
125
+ @articles = Article.test
126
+ rescue => my_error
127
+ begin
128
+ Debugmate::Publish.new(my_error).execute
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### Sending information about the request
134
+
135
+ If you want Debugmate to register more details about the exception, just pass the request along:
136
+
137
+ ```ruby
138
+ def index
139
+ @articles = Article.test
140
+ rescue => my_error
141
+ begin
142
+ Debugmate::Publish.new(my_error, request).execute
143
+ end
144
+ end
145
+ ```
146
+
147
+ ### Sending extra info to Debugmate
148
+
149
+ If you want to send some extra info about the exception, you can make use of the `extra_data` parameter. Debugmate will
150
+ display it in its `Context` tab.
151
+
152
+ ```ruby
153
+ def index
154
+ @articles = Article.test
155
+ rescue => my_error
156
+ begin
157
+ extra = {hello: 'world'}
158
+ Debugmate::Publish.new(my_error, request, extra).execute
159
+ end
160
+ end
161
+ ```
162
+
163
+ ## Running tests
164
+
165
+ Go to the gem folder and run bundler to install the gem dependencies locally
166
+ ```bash
167
+ bundle
168
+ ```
169
+
170
+ To run the available tests, go to the gem folder and execute
171
+ ```bash
172
+ bundle exec rspec
173
+ ```
174
+
175
+ You can get the list of passing tests with the doc format:
176
+ ```bash
177
+ bundle exec rspec --format doc
178
+ ```
179
+
180
+ ## Contributing
181
+ Contribution directions go here.
182
+
183
+ ## License
184
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,140 @@
1
+ require 'debugmate/curl'
2
+
3
+ module Debugmate
4
+ class Context
5
+ attr_accessor :env, :extra_data
6
+
7
+ def initialize(request, exception, extra_data = nil)
8
+ @env = request if request.is_a? Hash
9
+ @env = request.env if defined? (request.env)
10
+
11
+ @exception = exception
12
+ @extra_data = extra_data
13
+ end
14
+
15
+ def app
16
+ controller = @env['action_dispatch.request.parameters']['controller'] if @env['action_dispatch.request.parameters'] && @env['action_dispatch.request.parameters']['controller']
17
+ action = @env['action_dispatch.request.parameters']['action'] if @env['action_dispatch.request.parameters'] && @env['action_dispatch.request.parameters']['action']
18
+
19
+ app = {
20
+ controller: controller,
21
+ route: {
22
+ name: action,
23
+ parameters: route_params
24
+ },
25
+ middlewares: {},
26
+ view: {
27
+ name: nil,
28
+ data: nil
29
+ }
30
+ }
31
+
32
+ app
33
+ end
34
+
35
+ def route_params
36
+ clean_params = {}
37
+
38
+ # based on https://github.com/rails/rails/blob/7-0-stable/actionpack/lib/action_dispatch/middleware/debug_view.rb
39
+ params = @env['action_dispatch.request.parameters'] if @env['action_dispatch.request.parameters']
40
+
41
+ clean_params = params.clone if params
42
+ clean_params.delete("action")
43
+ clean_params.delete("controller")
44
+ clean_params
45
+ end
46
+
47
+ def request
48
+ url = "#{@env['HTTP_HOST']}#{@env['PATH_INFO']}" if @env['HTTP_HOST'] && @env['PATH_INFO']
49
+ method = "#{@env['REQUEST_METHOD']}" if @env['REQUEST_METHOD']
50
+ url_params = @env['QUERY_STRING'] if @env['QUERY_STRING']
51
+
52
+ # query string to hash: https://stackoverflow.com/a/16695721/2465086
53
+ query_string = Hash[*url_params.split(/=|&/)] if url_params.is_a? String
54
+
55
+ body = route_params if method == 'POST'
56
+
57
+ curl = Curl.new(url_headers, route_params, url, method)
58
+
59
+ request_info = {
60
+ request: {
61
+ url: url,
62
+ method: method,
63
+ curl: curl.parsed
64
+ },
65
+ headers: url_headers,
66
+ query_string: query_string ||= nil,
67
+ body: body ||= nil,
68
+ files: nil,
69
+ session: session,
70
+ cookies: {}
71
+ }
72
+
73
+ request_info
74
+ end
75
+
76
+ def url_headers
77
+ headers = {}
78
+
79
+ # Based on https://github.com/rails/rails/blob/7-0-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
80
+ request = ActionDispatch::Request.new @env
81
+
82
+ # Headers are extracted using splat operator. This is based on https://github.com/rails/rails/blob/7-0-stable/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
83
+ headers = request.env.slice(*request.class::ENV_METHODS) if @env.is_a? Hash
84
+
85
+ headers
86
+ end
87
+
88
+ def session
89
+ session = {}
90
+
91
+ request = ActionDispatch::Request.new @env
92
+
93
+ session = request.session.to_h
94
+
95
+ session
96
+ end
97
+
98
+ def environment
99
+ environment = [
100
+ {
101
+ group: 'Ruby on Rails',
102
+ variables: {
103
+ version: Rails.version,
104
+ locale: I18n.locale.to_s
105
+ }
106
+ },
107
+ {
108
+ group: 'App',
109
+ variables: {
110
+ environment: Rails.env,
111
+ date_time: Rails.configuration.time_zone
112
+ }
113
+ },
114
+ {
115
+ group: 'System',
116
+ variables: {
117
+ ruby: RUBY_VERSION,
118
+ os: RUBY_PLATFORM,
119
+ server_software: @env['SERVER_SOFTWARE'],
120
+ database_version: Rails.configuration.database_configuration[Rails.env]['adapter'],
121
+ browser_version: @env['HTTP_USER_AGENT']
122
+ }
123
+ }
124
+ ]
125
+
126
+ environment
127
+ end
128
+
129
+ def user
130
+ begin
131
+ user_controller = @env['action_controller.instance'] if @env['action_controller.instance']
132
+ user = user_controller.send(:current_user) if user_controller
133
+
134
+ user
135
+ rescue NoMethodError
136
+ nil
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,48 @@
1
+ module Debugmate
2
+ class Curl
3
+ def initialize(headers, params, url, method)
4
+ @headers = headers
5
+ @params = params
6
+ @url = url
7
+ @method = method
8
+ end
9
+
10
+ def parsed
11
+ headers_clean = @headers.clone
12
+ headers_clean.delete("GATEWAY_INTERFACE")
13
+ headers_clean.delete("REMOTE_ADDR")
14
+ headers_clean.delete("SERVER_NAME")
15
+ headers_clean.delete("SERVER_PROTOCOL")
16
+ headers_clean.delete("ORIGINAL_SCRIPT_NAME")
17
+ headers_clean.delete("HTTP_VERSION")
18
+
19
+ headers_line = ""
20
+ headers_clean.each do |header|
21
+ value = header[1]
22
+ value = value.downcase unless header[0].downcase.include? 'token'
23
+
24
+ headers_line = headers_line + "-H '#{header[0].gsub(/HTTP_/, '').downcase}: #{value}' \\\n"
25
+ end
26
+
27
+ post_fields = ""
28
+ @params.each do |param|
29
+ if param[1].is_a? Hash
30
+ param[1].each do |label, val|
31
+ post_fields = post_fields + "-d '#{param[0]}[#{label}]=#{val}' \\\n"
32
+ end
33
+ else
34
+ post_fields = post_fields + "-d '#{param[0]}=#{param[1]}' \\\n"
35
+ end
36
+ end
37
+
38
+ curl = <<~CURL
39
+ curl '#{@url}' \\
40
+ -X #{@method} \\
41
+ #{headers_line.chop}
42
+ #{post_fields.chop}
43
+ CURL
44
+
45
+ curl
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,26 @@
1
+ require 'debugmate/publish'
2
+
3
+ module Debugmate
4
+ class Handler
5
+
6
+ def initialize(env, exception)
7
+ @env = env
8
+ @exception = exception
9
+ end
10
+
11
+ def capture
12
+
13
+ query_data = {
14
+ query: Debugmate::ExceptionHandler.last_executed_query.tr('"',"'"),
15
+ binds: Debugmate::ExceptionHandler.last_executed_binds
16
+ }
17
+
18
+ publish = Publish.new(@exception, @env, query_data).execute
19
+
20
+ Debugmate::ExceptionHandler.last_executed_query = ''
21
+ Debugmate::ExceptionHandler.last_executed_binds = {}
22
+
23
+ publish
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ require 'net/http'
2
+ require 'debugmate/context'
3
+ require 'debugmate/trace'
4
+
5
+ module Debugmate
6
+ class Publish
7
+ def initialize(exception, request = {}, extra_data = nil)
8
+ @exception = exception
9
+ @request = request
10
+ @extra_data = extra_data
11
+ end
12
+
13
+ def execute
14
+ uri = URI("#{DEBUGMATE_CONFIG['data']['domain']}/api/capture")
15
+
16
+ headers = {
17
+ 'X-DEBUGMATE-TOKEN' => DEBUGMATE_CONFIG['data']['token'],
18
+ 'Content-Type' => 'application/json',
19
+ 'Accept' => 'application/json'
20
+ }
21
+
22
+ result = Net::HTTP.post(uri, payload.to_json, headers)
23
+
24
+ result
25
+ end
26
+
27
+ def payload
28
+ context = Context.new(@request, @exception, @extra_data)
29
+
30
+ trace = Trace.new(@exception, @request)
31
+
32
+ data = {
33
+ exception: @exception.class.to_s,
34
+ message: @exception.message,
35
+ file: trace.parsed[0][:file],
36
+ type: 'web',
37
+ trace: trace.parsed,
38
+ app: context.app,
39
+ request: context.request,
40
+ environment: context.environment,
41
+ user: context.user || [],
42
+ context: context.extra_data || []
43
+ }
44
+
45
+ data
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ module Debugmate
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ load 'tasks/debugmate_tasks.rake'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,54 @@
1
+ module Debugmate
2
+ class Trace
3
+ def initialize(exception, request = {})
4
+ @exception = exception
5
+ @env = request.is_a?(Hash) ? request : (request.env if request.respond_to?(:env))
6
+ end
7
+
8
+ def parsed
9
+ wrapped = ActionDispatch::ExceptionWrapper.new(ActiveSupport::BacktraceCleaner.new, @exception)
10
+
11
+ function = @env.dig('action_dispatch.request.parameters', 'action')
12
+
13
+ source_to_show_id = wrapped.source_to_show_id
14
+ backtrace = @exception.backtrace
15
+
16
+ trace = if source_to_show_id && backtrace
17
+ [{
18
+ file: backtrace[source_to_show_id].to_s,
19
+ line: wrapped.source_extracts.dig(source_to_show_id, :line_number),
20
+ function: function,
21
+ class: extract_class(backtrace[source_to_show_id]),
22
+ preview: wrapped.source_extracts.dig(source_to_show_id, :code)
23
+ }]
24
+ else
25
+ [{
26
+ file: backtrace&.first.to_s,
27
+ line: nil,
28
+ function: function,
29
+ class: extract_class(backtrace&.first),
30
+ preview: nil
31
+ }]
32
+ end
33
+
34
+ trace
35
+ end
36
+
37
+ private
38
+
39
+ def extract_class(backtrace_line)
40
+ return "Unknown" if backtrace_line.nil?
41
+
42
+ # Tenta extrair o nome da classe do backtrace
43
+ match = backtrace_line.match(/^(.+?):(\d+):in/)
44
+ if match
45
+ file_path = match[1]
46
+ class_name = File.basename(file_path, ".*").split("_").map(&:capitalize).join
47
+ class_name.empty? ? "Unknown" : class_name
48
+ else
49
+ "Unknown"
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,3 @@
1
+ module Debugmate
2
+ VERSION = "0.1.0"
3
+ end
data/lib/debugmate.rb ADDED
@@ -0,0 +1,52 @@
1
+ require "debugmate/version"
2
+ require "debugmate/railtie"
3
+ require "debugmate/handler"
4
+
5
+ require "net/http"
6
+
7
+ module Debugmate
8
+ class ExceptionHandler
9
+ mattr_reader :last_executed_binds
10
+ mattr_accessor :last_executed_query
11
+
12
+ @@last_executed_query = ""
13
+ @@last_executed_binds = {}
14
+
15
+ def initialize(app)
16
+ @app = app
17
+ end
18
+
19
+ def call(env)
20
+ @app.call(env)
21
+ rescue StandardError => raised
22
+ Handler.new(env, raised).capture if DEBUGMATE_CONFIG['data']['enabled'] === true
23
+ raise
24
+ end
25
+
26
+ def self.send_test
27
+ puts "Please check that you have your initializer defined with the DEBUGMATE_CONFIG constant" unless defined? DEBUGMATE_CONFIG
28
+ raise TestException
29
+ rescue StandardError => raised
30
+ response = Handler.new({'REQUEST_METHOD' => "GET"}, raised).capture if DEBUGMATE_CONFIG['data']['enabled'] === true
31
+ puts "We couldn't reach Debugmate Server at #{DEBUGMATE_CONFIG['data']['domain']}" unless response.is_a? Net::HTTPSuccess
32
+ puts "Debugmate reached successfully. We sent a test Exception that has been registered." if response.is_a? Net::HTTPSuccess
33
+ end
34
+
35
+ def self.last_executed_binds=(binds)
36
+ if binds.is_a? Array
37
+ binds.each do |bind|
38
+ name = bind.name
39
+ value = bind.value
40
+ @@last_executed_binds[name] = value
41
+ end
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ class TestException < StandardError
48
+ def message
49
+ "Test generated by the debugmate:test rails command"
50
+ end
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: debugmate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - DevSquad
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.4.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.4.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: DebugMate provides powerful error tracking and debugging tools for Rails
42
+ applications.
43
+ email:
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.md
49
+ - Rakefile
50
+ - lib/debugmate.rb
51
+ - lib/debugmate/context.rb
52
+ - lib/debugmate/curl.rb
53
+ - lib/debugmate/handler.rb
54
+ - lib/debugmate/publish.rb
55
+ - lib/debugmate/railtie.rb
56
+ - lib/debugmate/trace.rb
57
+ - lib/debugmate/version.rb
58
+ homepage: https://github.com/DebugMate/debugmate
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ homepage_uri: https://github.com/DebugMate/debugmate
63
+ source_code_uri: https://github.com/DebugMate/debugmate
64
+ changelog_uri: https://github.com/DebugMate/debugmate/CHANGELOG.md
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.5.11
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Error tracking and monitoring for Ruby on Rails applications.
84
+ test_files: []