enhanced_errors 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1381112bfa16b295aa6916a6248fba5951ae7362984fd7741d8cef0c81d60769
4
+ data.tar.gz: e60c4a237637dae49192c670ed291d1b6d90d97ea868572b834df2ec17c0e048
5
+ SHA512:
6
+ metadata.gz: 387e0660918b06a32515f2ef928d9caacf4394640bce877438b4d25d278664999d831aef693bb128b24efbed490e66991b32a1663927b099fa50fb1d77d1e620
7
+ data.tar.gz: fbfdee8971f463d7697321b4873fd0bc4a48ca39acd59f172ca81e64b8e52bde4c5a83d3c56599e772bfdc61eae034b179fbd7bec42dee1442685fd23e00919d
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2019-2024 Eric Beland
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,367 @@
1
+ # EnhancedErrors
2
+
3
+ ## Overview
4
+
5
+ **EnhancedErrors** is a pure Ruby gem that enhances exception messages by capturing and appending variables and their values from the scope where the error was raised.
6
+
7
+ **EnhancedErrors** leverages Ruby's built-in [TracePoint](https://ruby-doc.org/core-3.1.0/TracePoint.html) feature to provide detailed context for exceptions, making debugging easier without significant performance overhead.
8
+
9
+ When an exception is raised, EnhancedErrors captures the surrounding context. It works like this:
10
+
11
+ ```ruby
12
+
13
+ require './lib/enhanced_errors'
14
+ require 'awesome_print' # Optional, for better output
15
+
16
+ EnhancedErrors.enhance!
17
+
18
+ def foo
19
+ begin
20
+ myvar = 0
21
+ @myinstance = 10
22
+ foo = @myinstance / myvar
23
+ rescue => e
24
+ puts e.message
25
+ end
26
+ end
27
+
28
+ foo
29
+
30
+ ```
31
+
32
+ #### Enhanced Exception In Code:
33
+
34
+ <img src="./doc/images/enhanced-error.png" style="height: 171px; width: 440px;"></img>
35
+
36
+
37
+ ```ruby
38
+ describe 'attains enlightenment' do
39
+ let(:the_matrix) { 'code rains, dramatically' }
40
+
41
+ before(:each) do
42
+ @spoon = 'there is no spoon'
43
+ end
44
+
45
+ it 'in the matrix' do
46
+ #activate memoized item
47
+ the_matrix
48
+ stop = 'bullets'
49
+ raise 'No!'
50
+ end
51
+ end
52
+ ```
53
+
54
+ #### Enhanced Exception In Specs:
55
+
56
+ <img src="./doc/images/enhanced-spec.png" style="height: 426px; width: 712px;"></img>
57
+
58
+
59
+ ## Features
60
+
61
+ - **Pure Ruby**: No external dependencies or C extensions.
62
+ - **Standalone**: Does not rely on any external libraries.
63
+ - **Lightweight**: Minimal performance impact, as tracing is only active during exception raising.
64
+ - **Customizable Output**: Supports multiple output formats (`:json`, `:plaintext`, `:terminal`).
65
+ - **Flexible Hooks**: Redact or modifying captured data via the `on_capture` hook.
66
+ - **Environment-Based Defaults**: For Rails apps, automatically adjusts settings based on the environment (`development`, `test`, `production`, `ci`).
67
+ - **Pre-Populated Skip List**: Comes with predefined skip lists to exclude irrelevant variables from being captured.
68
+ - **Capture Levels**: Supports `info` and `debug` levels, where `debug` level ignores the skip lists for more comprehensive data capture.
69
+ - **Capture Types**: Captures variables from the first `raise` and the last `rescue` for an exception by default.
70
+ - **No dependencies**: EnhancedErrors does not ___require___ any dependencies--it uses [awesome_print](https://github.com/awesome-print/awesome_print) for nicer output if it is installed and available.
71
+
72
+ EnhancedErrors has a few big use-cases:
73
+
74
+ * Data-driven bugs. For example, if, while processing a 10 gig file, you get an error, you can't just re-run the code with a debugger.
75
+ You also can't just print out all the data, because it's too big. You want to know what the data was the cause of the error.
76
+ Ideally, without long instrument-re-run-fix loops.
77
+
78
+ If your logging didn't capture the data, normally, you'd be stuck.
79
+
80
+ * Debug a complex application erroring deep in the stack when you can't tell where the error originates
81
+
82
+ * Faster TDD - Often, you won't have to re-run to see an error--you can go straight to the fix.
83
+
84
+ * Faster CI -> Dev fixes. When a bug happens in CI, usually there's a step where you first reproduce it locally.
85
+ EnhancedErrors can help you skip that step.
86
+
87
+ * Faster debugging. In general, you can skip the add-instrumentation step and jump to the fix.
88
+
89
+ * Heisenbugs - bugs that disappear when you try to debug them. EnhancedErrors can help you capture the data that causes the bug before it disappears.
90
+
91
+ * "Unknown Unknowns" - you can't pre-emptively log variables from failure cases you never imagined.
92
+
93
+ * Cron jobs and daemons - when it fails for unknown reasons at 4am, check the log and fix--it probably has what you need.
94
+
95
+ ## Installation
96
+
97
+ Add this line to your `Gemfile`:
98
+
99
+ ```ruby
100
+ gem 'enhanced_errors'
101
+ ```
102
+
103
+ Then execute:
104
+
105
+ ```shell
106
+ $ bundle install
107
+ ```
108
+
109
+ Or install it yourself with:
110
+
111
+ ```shell
112
+ $ gem install enhanced_errors
113
+ ```
114
+
115
+ ## Basic Usage
116
+
117
+ To enable EnhancedErrors, call the `enhance!` method:
118
+
119
+ ```ruby
120
+ EnhancedErrors.enhance!
121
+ ```
122
+
123
+ This activates the TracePoint to start capturing exceptions and their surrounding context.
124
+
125
+ ### Configuration Options
126
+
127
+ You can pass configuration options to `enhance!`:
128
+
129
+ ```ruby
130
+ EnhancedErrors.enhance!(enabled: true, max_length: 2000) do
131
+ # Additional configuration here
132
+ add_to_skip_list :@instance_variable_to_skip, :local_to_skip
133
+ end
134
+
135
+ ```
136
+ - `enabled`: Enables or disables the enhancement (default: `true`).
137
+ - `max_length`: Sets the maximum length of the enhanced message (default: `2500`).
138
+
139
+ ### Environment-Based Defaults
140
+
141
+ EnhancedErrors adjusts its default settings based on the environment:
142
+
143
+ - **Development/Test**:
144
+ - Default Output format: `:terminal`
145
+ - Terminal Color output: Enabled
146
+ - **Production**:
147
+ - Output format: `:json`
148
+ - Terminal Color output: Disabled
149
+ - **CI Environment**:
150
+ - Output format: `:plaintext`
151
+ - Color output: Disabled
152
+
153
+ The environment is determined by `ENV['RAILS_ENV']`, `ENV['RACK_ENV']`, or detected CI environment variables like:
154
+ - `CI=true`
155
+
156
+ ### Output Formats
157
+
158
+ You can customize the output format:
159
+
160
+ - **`:json`**: Outputs the captured data in JSON format.
161
+ - **`:plaintext`**: Outputs plain text without color codes.
162
+ - **`:terminal`**: Outputs text with terminal color codes.
163
+
164
+ Example:
165
+
166
+ ```ruby
167
+ EnhancedErrors.format(captured_bindings, :json)
168
+ ```
169
+
170
+ ### Customizing Data Capture
171
+
172
+ #### Using `on_capture`
173
+
174
+ The `on_capture` hook allows you to modify or redact data as it is captured. For each captured binding
175
+ it yields out a hash with the structure below. Modify it as needed and return the modified hash.
176
+
177
+ ```ruby
178
+ {
179
+ source: source_location,
180
+ object: Object source of error,
181
+ library: true or false,
182
+ method_and_args: method_and_args,
183
+ variables: {
184
+ locals: locals,
185
+ instances: instances,
186
+ lets: lets,
187
+ globals: globals
188
+ },
189
+ exception: exception.class.name,
190
+ capture_type: capture_type # 'raise' or 'rescue'
191
+ }
192
+ ```
193
+
194
+
195
+ ```ruby
196
+ EnhancedErrors.on_capture do |binding_info|
197
+ # Redact sensitive data
198
+ if binding_info[:variables][:locals][:password]
199
+ binding_info[:variables][:locals][:password] = '[REDACTED]'
200
+ end
201
+ binding_info # Return the modified binding_info
202
+ end
203
+ ```
204
+
205
+
206
+ #### Using `eligible_for_capture`
207
+
208
+ The `eligible_for_capture` hook yields an Exception, and allows you to decide whether you want to capture it or not.
209
+ By default, all exceptions are captured. When the block result is true, the error will be captured.
210
+ Error capture is relatively cheap, but ignoring errors you don't care about makes it almost totally free.
211
+ One use-case for eligible_for_capture is to run a string or regexp off a setting flag, which
212
+ lets you turn on and off what you capture without redeploying.
213
+
214
+ ```ruby
215
+ EnhancedErrors.eligible_for_capture do |exception|
216
+ exception.class.name == 'ExceptionIWantTOCatch'
217
+ end
218
+
219
+
220
+ ```
221
+
222
+ #### Using `on_format`
223
+
224
+ `on_format` is the last stop for the message string that will be appended to `exception.message`.
225
+
226
+ Here it can be encrypted, rewritten, or otherwise modified.
227
+
228
+
229
+ ```ruby
230
+ EnhancedErrors.on_format do |formatted_string|
231
+ "---whatever--- #{formatted_string} ---whatever---"
232
+ end
233
+
234
+ ```
235
+
236
+
237
+ #### Applying a Variable Skip List
238
+
239
+ EnhancedErrors comes with predefined skip lists to exclude sensitive or irrelevant variables.
240
+ By default, the skip list is used to remove a lot of framework noise from Rails and RSpec.
241
+ You can add additional variables to the skip list as needed:
242
+
243
+ ```ruby
244
+
245
+ EnhancedErrors.enhance! do
246
+ add_to_skip_list :@variable_to_skip
247
+ end
248
+
249
+ ```
250
+
251
+ The skip list is pre-populated with common variables to exclude and can be extended based on your application's requirements.
252
+
253
+
254
+
255
+
256
+ ### Capture Levels
257
+
258
+ EnhancedErrors supports different capture levels to control the verbosity of the captured data:
259
+
260
+ - **Info Level**: Respects the skip list, excluding predefined sensitive or irrelevant variables. Global variables are ignored.
261
+ - **Debug Level**: Ignores the skip lists, capturing all variables including those typically excluded and global variables.
262
+ Global variables,
263
+
264
+ **Default Behavior**: By default, `info` level is used, which excludes variables in the skip list to protect sensitive information. In `debug` mode, the skip lists are ignored to provide more comprehensive data, which is useful during development but should be used cautiously to avoid exposing sensitive data.
265
+ The info mode is recommended.
266
+
267
+
268
+
269
+ ### Capture Types
270
+
271
+ EnhancedErrors differentiates between two types of capture events:
272
+
273
+ - **`raise`**: Captures the context when an exception is initially raised.
274
+ - **`rescue`**: Captures the context when an exception is last rescued.
275
+
276
+ **Default Behavior**: By default, EnhancedErrors returns the first `raise` and the last `rescue` event for each exception.
277
+ This provides a clear picture of where and how the exception was handled.
278
+
279
+
280
+ ### Example: Redacting Sensitive Information
281
+
282
+ ```ruby
283
+ EnhancedErrors.on_capture do |binding_info|
284
+ sensitive_keys = [:password, :ssn, :health_info]
285
+ sensitive_keys.each do |key|
286
+ if binding_info[:variables][:locals][key]
287
+ binding_info[:variables][:locals][key] = '[REDACTED]'
288
+ end
289
+ end
290
+ binding_info
291
+ end
292
+ ```
293
+
294
+ ### Example: Encrypting Data in Custom Format
295
+
296
+
297
+ ```ruby
298
+ # config/initializers/encryption.rb
299
+
300
+ require 'active_support'
301
+
302
+ # Retrieve the encryption key from Rails credentials or environment variables
303
+ ENCRYPTION_KEY = Rails.application.credentials.encryption_key || ENV['ENCRYPTION_KEY']
304
+
305
+ # It's recommended to use a 256-bit key (32 bytes)
306
+ # If your key is in hex or another format, ensure it's properly decoded
307
+ key = ActiveSupport::KeyGenerator.new(ENCRYPTION_KEY).generate_key('enhanced_errors', 32)
308
+ ENCRYPTOR = ActiveSupport::MessageEncryptor.new(key)
309
+ ```
310
+
311
+ ```ruby
312
+
313
+ require_relative 'path_to/enhanced_errors' # Adjust the path accordingly
314
+ require 'active_support/message_encryptor'
315
+
316
+ # Ensure the encryptor is initialized
317
+ encryptor = ENCRYPTOR
318
+
319
+ EnhancedErrors.on_format = lambda do |formatted_string|
320
+ encrypted_data = encryptor.encrypt_and_sign(formatted_string)
321
+ encrypted_base64 = Base64.strict_encode64(encrypted_data)
322
+ "ENCRYPTED[#{encrypted_data}]"
323
+ end
324
+ ```
325
+
326
+
327
+ ## How It Works
328
+
329
+ EnhancedErrors uses Ruby's `TracePoint` to listen for `:raise` and `:rescue` events.
330
+ When an exception is raised or rescued, it captures:
331
+
332
+ - **Local Variables**: Variables local to the scope where the exception occurred.
333
+ - **Instance Variables**: Instance variables of the object.
334
+ - **Method and Arguments**: The method name and its arguments.
335
+ - **Let Variables**: RSpec let variables, if applicable. Only memoized (evaluated) let variables are captured.
336
+ - **Global Variables**: Global variables, in debug mode.
337
+
338
+ The captured data includes a `capture_type` field indicating whether the data was captured during a `raise` or `rescue` event. By default, EnhancedErrors returns the first `raise` and the last `rescue` event for each exception, providing a clear trace of the exception lifecycle.
339
+
340
+ The captured data is then appended to the exception's message, providing rich context for debugging.
341
+
342
+
343
+ ## Awesome Print
344
+
345
+ EnhancedErrors uses the [awesome_print](https://github.com/awesome-print/awesome_print)
346
+ gem to format the captured data, if it is installed and available.
347
+ If not, errors should still work, but the output may be less readable. AwesomePrint is not
348
+ required directly by EnhancedErrors, so you will need to add it to your Gemfile if you want to use it.
349
+
350
+ ```ruby
351
+ gem 'awesome_print'
352
+ ```
353
+
354
+
355
+ ## Performance Considerations
356
+
357
+ - **Minimal Overhead**: Since TracePoint is only activated during exception raising and rescuing, the performance impact is negligible during normal operation.
358
+ - **Production Safe**: The gem is designed to be safe for production use, giving you valuable insights without compromising performance.
359
+
360
+ ## Contributing
361
+
362
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/your_username/enhanced_errors](https://github.com/your_username/enhanced_errors).
363
+
364
+ ## License
365
+
366
+ The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
367
+
@@ -0,0 +1,88 @@
1
+ require 'benchmark'
2
+ require_relative '../lib/enhanced_errors' # Adjust the path if necessary
3
+
4
+ # Define the number of iterations
5
+ ITERATIONS = 10_000
6
+ EXCEPTIONS_PER_BATCH = 100
7
+
8
+ class Boo < StandardError; end
9
+
10
+ def calculate_cost(time_in_seconds)
11
+ milliseconds = time_in_seconds * 1000
12
+ (milliseconds / (ITERATIONS / EXCEPTIONS_PER_BATCH)).round(2)
13
+ end
14
+
15
+ def with_enhanced_errors
16
+ EnhancedErrors.enhance!(debug: false)
17
+ ITERATIONS.times do
18
+ begin
19
+ foo = 'bar'
20
+ @boo = 'baz'
21
+ raise 'Test exception with EnhancedErrors'
22
+ rescue => e
23
+ e.message
24
+ end
25
+ end
26
+ end
27
+
28
+ def without_enhanced_errors
29
+ ITERATIONS.times do
30
+ begin
31
+ foo = 'bar'
32
+ @boo = 'baz'
33
+ raise 'Test exception without EnhancedErrors'
34
+ rescue => e
35
+ e.message
36
+ end
37
+ end
38
+ end
39
+
40
+ def when_capture_only_regexp_matched
41
+ EnhancedErrors.enhance!(debug: false) do
42
+ eligible_for_capture { |exception| !!/Boo/.match(exception.class.to_s) }
43
+ end
44
+
45
+ ITERATIONS.times do
46
+ begin
47
+ foo = 'bar'
48
+ @boo = 'baz'
49
+ raise Boo.new('Test exception with EnhancedErrors')
50
+ rescue => e
51
+ e.message
52
+ end
53
+ end
54
+ end
55
+
56
+ def when_capture_only_regexp_did_not_match
57
+ EnhancedErrors.enhance!(debug: false) do
58
+ eligible_for_capture { |exception| !!/Baz/.match(exception.class.to_s) }
59
+ end
60
+
61
+ ITERATIONS.times do
62
+ begin
63
+ foo = 'bar'
64
+ @boo = 'baz'
65
+ raise Boo.new('Test exception with EnhancedErrors')
66
+ rescue => e
67
+ e.message
68
+ end
69
+ end
70
+ end
71
+
72
+ puts "Cost Exploration\n"
73
+ Benchmark.bm(35) do |x|
74
+ without_time = x.report('10k Without EnhancedErrors:') { without_enhanced_errors }
75
+ with_time = x.report('10k With EnhancedErrors:') { with_enhanced_errors }
76
+
77
+ puts "\nCost per 100 exceptions (Without EnhancedErrors): #{calculate_cost(without_time.real)} ms"
78
+ puts "Cost per 100 exceptions (With EnhancedErrors): #{calculate_cost(with_time.real)} ms"
79
+ end
80
+
81
+ puts "\nProof that if you only match the classes you care about, the cost is nominal\n"
82
+ Benchmark.bm(35) do |x|
83
+ matched_time = x.report('10k With capture_only_regexp match:') { when_capture_only_regexp_matched }
84
+ not_matched_time = x.report('10k Without capture_only_regexp match:') { when_capture_only_regexp_did_not_match }
85
+
86
+ puts "\nCost per 100 exceptions (Capture Only Match): #{calculate_cost(matched_time.real)} ms"
87
+ puts "Cost per 100 exceptions (No Match): #{calculate_cost(not_matched_time.real)} ms"
88
+ end
@@ -0,0 +1,31 @@
1
+ require 'stackprof'
2
+ require_relative '../lib/enhanced_errors' # Adjust the path if necessary
3
+
4
+ # gem install stackprof
5
+
6
+ # adjust path as needed
7
+ # ruby ./lib/core_ext/enhanced_errors/benchmark/stackprofile.rb
8
+ # dumps to current folder. read the stackprof dump:
9
+ # stackprof stackprof.dump
10
+
11
+ # Define the number of iterations
12
+ ITERATIONS = 10_000
13
+
14
+ def run_with_enhanced_errors
15
+ EnhancedErrors.enhance!(debug: false)
16
+ ITERATIONS.times do
17
+ begin
18
+ raise 'Test exception with EnhancedErrors'
19
+ rescue => _e
20
+ # Exception handled with EnhancedErrors.
21
+ end
22
+ end
23
+ end
24
+
25
+ def stackprofile
26
+ StackProf.run(mode: :wall, out: 'stackprof.dump') do
27
+ run_with_enhanced_errors
28
+ end
29
+ end
30
+
31
+ stackprofile
Binary file
Binary file
Binary file
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "enhanced_errors"
3
+ spec.version = "0.1.0"
4
+ spec.authors = ["Eric Beland"]
5
+
6
+ spec.summary = "Automatically enhance your errors with messages containing variable values from the moment they were raised."
7
+ spec.description = "With no extra dependencies, and using only Ruby's built-in TracePoint, EnhancedErrors will automatically enhance your errors with messages containing variable values from the moment they were raised."
8
+ spec.homepage = "https://github.com/ericbeland/enhanced_errors."
9
+ spec.required_ruby_version = ">= 3.0.0"
10
+
11
+ spec.metadata["homepage_uri"] = spec.homepage
12
+ spec.metadata["source_code_uri"] = "https://github.com/ericbeland/enhanced_errors"
13
+
14
+ # Specify which files should be added to the gem when it is released.
15
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
16
+ spec.files = Dir.chdir(__dir__) do
17
+ `git ls-files -z`.split("\x0").reject do |f|
18
+ (File.expand_path(f) == __FILE__) ||
19
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
20
+ end
21
+ end
22
+ spec.require_paths = ["lib"]
23
+ spec.add_development_dependency "rspec", "> 3.4.0"
24
+ end
@@ -0,0 +1,16 @@
1
+ require './lib/enhanced_errors'
2
+ require 'awesome_print' # Optional, for better output
3
+
4
+ EnhancedErrors.enhance!
5
+
6
+ def foo
7
+ begin
8
+ myvar = 0
9
+ @myinstance = 10
10
+ foo = @myinstance / myvar
11
+ rescue => e
12
+ puts e.message
13
+ end
14
+ end
15
+
16
+ foo
@@ -0,0 +1,27 @@
1
+ require 'rspec'
2
+ require_relative '../lib/enhanced_errors'
3
+
4
+ # INSTRUCTIONS: Install rspec
5
+ # gem install rspec
6
+ # rspec examples/example_spec.rb
7
+
8
+ RSpec.describe 'Neo' do
9
+ before(:each) do
10
+ EnhancedErrors.enhance!
11
+ end
12
+
13
+ describe 'attains enlightenment' do
14
+ let(:the_matrix) { 'code rains, dramatically' }
15
+
16
+ before(:each) do
17
+ @spoon = 'there is no spoon'
18
+ end
19
+
20
+ it 'in the matrix' do
21
+ #activate memoized item
22
+ the_matrix
23
+ stop = 'bullets'
24
+ raise 'No!'
25
+ end
26
+ end
27
+ end
data/lib/binding.rb ADDED
@@ -0,0 +1,11 @@
1
+ module Debugging
2
+ def let_vars_hash
3
+ memoized_values = self.receiver.instance_variable_get(:@__memoized)&.instance_variable_get(:@memoized)
4
+ memoized_values && !memoized_values.empty? ? memoized_values.dup : {}
5
+ end
6
+ end
7
+
8
+ class Binding
9
+ include Debugging
10
+ end
11
+
data/lib/colors.rb ADDED
@@ -0,0 +1,27 @@
1
+ class Colors
2
+ COLORS = { red: 31, green: 32, yellow: 33, blue: 34, purple: 35, cyan: 36, white: 0 }
3
+
4
+ class << self
5
+ def enabled?
6
+ @enabled
7
+ end
8
+
9
+ def enabled=(value)
10
+ @enabled = value
11
+ end
12
+
13
+ def color(num, string)
14
+ @enabled ? "#{code(num)}#{string}#{code(0)}" : string
15
+ end
16
+
17
+ def code(num)
18
+ "\e[#{num}m"
19
+ end
20
+
21
+ COLORS.each do |color, code|
22
+ define_method(color) do |str|
23
+ color(COLORS[color], str)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,583 @@
1
+ require 'set'
2
+ require 'json'
3
+
4
+ require_relative 'colors'
5
+ require_relative 'error_enhancements'
6
+ require_relative 'binding'
7
+
8
+ # The EnhancedErrors class provides mechanisms to enhance exception handling by capturing
9
+ # additional context such as binding information, variables, and method arguments when exceptions are raised.
10
+ # It offers customization options for formatting and filtering captured data.
11
+ class EnhancedErrors
12
+ class << self
13
+ # @!attribute [rw] enabled
14
+ # @return [Boolean] Indicates whether EnhancedErrors is enabled.
15
+ attr_accessor :enabled
16
+
17
+ # @!attribute [rw] trace
18
+ # @return [TracePoint, nil] The TracePoint object used for tracing exceptions.
19
+ attr_accessor :trace
20
+
21
+ # @!attribute [rw] config_block
22
+ # @return [Proc, nil] The configuration block provided during enhancement.
23
+ attr_accessor :config_block
24
+
25
+ # @!attribute [rw] max_length
26
+ # @return [Integer] The maximum length of the formatted exception message.
27
+ attr_accessor :max_length
28
+
29
+ # @!attribute [rw] on_capture_hook
30
+ # @return [Proc, nil] Hook to modify binding information upon capture.
31
+ attr_accessor :on_capture_hook
32
+
33
+ # @!attribute [rw] capture_let_variables
34
+ # @return [Boolean] Determines whether RSpec `let` variables are captured.
35
+ attr_accessor :capture_let_variables
36
+
37
+ # @!attribute [rw] eligible_for_capture
38
+ # @return [Proc, nil] A proc that determines if an exception is eligible for capture.
39
+ attr_accessor :eligible_for_capture
40
+
41
+ # @!attribute [rw] skip_list
42
+ # @return [Set<Symbol>] A set of variable names to exclude from binding information.
43
+ attr_accessor :skip_list
44
+
45
+ # @!constant GEMS_REGEX
46
+ # @return [Regexp] Regular expression to identify gem paths.
47
+ GEMS_REGEX = %r{[\/\\]gems[\/\\]}
48
+
49
+ # @!constant DEFAULT_MAX_LENGTH
50
+ # @return [Integer] The default maximum length for formatted exception messages.
51
+ DEFAULT_MAX_LENGTH = 2500
52
+
53
+ # @!constant RSPEC_SKIP_LIST
54
+ # @return [Set<Symbol>] A set of RSpec-specific instance variables to skip.
55
+ RSPEC_SKIP_LIST = Set.new([
56
+ :@fixture_cache,
57
+ :@fixture_cache_key,
58
+ :@fixture_connection_pools,
59
+ :@connection_subscriber,
60
+ :@saved_pool_configs,
61
+ :@loaded_fixtures,
62
+ :@matcher_definitions,
63
+ ])
64
+
65
+ # @!constant RAILS_SKIP_LIST
66
+ # @return [Set<Symbol>] A set of Rails-specific instance variables to skip.
67
+ RAILS_SKIP_LIST = Set.new([
68
+ :@new_record,
69
+ :@attributes,
70
+ :@association_cache,
71
+ :@readonly,
72
+ :@previously_new_record,
73
+ :@destroyed,
74
+ :@marked_for_destruction,
75
+ :@destroyed_by_association,
76
+ :@primary_key,
77
+ :@strict_loading,
78
+ :@strict_loading_mode,
79
+ :@mutations_before_last_save,
80
+ :@mutations_from_database
81
+ ])
82
+
83
+ # Gets or sets the maximum length for the formatted exception message.
84
+ #
85
+ # @param value [Integer, nil] The desired maximum length. If `nil`, returns the current value.
86
+ # @return [Integer] The maximum length for the formatted message.
87
+ def max_length(value = nil)
88
+ if value.nil?
89
+ @max_length ||= DEFAULT_MAX_LENGTH
90
+ else
91
+ @max_length = value
92
+ end
93
+ @max_length
94
+ end
95
+
96
+ # Gets or sets whether to capture RSpec `let` variables.
97
+ #
98
+ # @param value [Boolean, nil] The desired state. If `nil`, returns the current value.
99
+ # @return [Boolean] Whether RSpec `let` variables are being captured.
100
+ def capture_let_variables(value = nil)
101
+ if value.nil?
102
+ @capture_let_variables = @capture_let_variables.nil? ? true : @capture_let_variables
103
+ else
104
+ @capture_let_variables = value
105
+ end
106
+ @capture_let_variables
107
+ end
108
+
109
+ # Retrieves the current skip list, initializing it with default values if not already set.
110
+ #
111
+ # @return [Set<Symbol>] The current skip list.
112
+ def skip_list
113
+ @skip_list ||= default_skip_list
114
+ end
115
+
116
+ # Initializes the default skip list by merging Rails and RSpec specific variables.
117
+ #
118
+ # @return [Set<Symbol>] The default skip list.
119
+ def default_skip_list
120
+ Set.new(RAILS_SKIP_LIST).merge(RSPEC_SKIP_LIST)
121
+ end
122
+
123
+ # Adds variables to the skip list to exclude them from binding information.
124
+ #
125
+ # @param vars [Symbol] The variable names to add to the skip list.
126
+ # @return [Set<Symbol>] The updated skip list.
127
+ def add_to_skip_list(*vars)
128
+ skip_list.merge(vars)
129
+ end
130
+
131
+ # Enhances the exception handling by setting up tracing and configuration options.
132
+ #
133
+ # @param enabled [Boolean] Whether to enable EnhancedErrors.
134
+ # @param debug [Boolean] Whether to enable debug mode.
135
+ # @param options [Hash] Additional configuration options.
136
+ # @yield [void] A block for additional configuration.
137
+ # @return [void]
138
+ def enhance!(enabled: true, debug: false, **options, &block)
139
+ @output_format = nil
140
+ @eligible_for_capture = nil
141
+ @original_global_variables = nil
142
+ if enabled == false
143
+ @original_global_variables = nil
144
+ @enabled = false
145
+ @trace.disable if @trace
146
+ else
147
+ @enabled = true
148
+ @debug = debug
149
+ @original_global_variables = global_variables
150
+
151
+ options.each do |key, value|
152
+ setter_method = "#{key}="
153
+ if respond_to?(setter_method)
154
+ send(setter_method, value)
155
+ elsif respond_to?(key)
156
+ send(key, value)
157
+ else
158
+ # Ignore unknown options or handle as needed
159
+ end
160
+ end
161
+
162
+ @config_block = block_given? ? block : nil
163
+ instance_eval(&@config_block) if @config_block
164
+
165
+ start_tracing
166
+ end
167
+ end
168
+
169
+ # Sets or retrieves the eligibility criteria for capturing exceptions.
170
+ #
171
+ # @yieldparam exception [Exception] The exception to evaluate.
172
+ # @return [Proc] The current eligibility proc.
173
+ def eligible_for_capture(&block)
174
+ if block_given?
175
+ @eligible_for_capture = block
176
+ else
177
+ @eligible_for_capture ||= method(:default_eligible_for_capture)
178
+ end
179
+ end
180
+
181
+ # Sets or retrieves the hook to modify binding information upon capture.
182
+ #
183
+ # @yieldparam binding_info [Hash] The binding information captured.
184
+ # @return [Proc] The current on_capture hook.
185
+ def on_capture(&block)
186
+ if block_given?
187
+ @on_capture_hook = block
188
+ else
189
+ @on_capture_hook ||= method(:default_on_capture)
190
+ end
191
+ end
192
+
193
+ # Sets the on_capture hook.
194
+ #
195
+ # @param value [Proc] The proc to set as the on_capture hook.
196
+ # @return [Proc] The newly set on_capture hook.
197
+ def on_capture=(value)
198
+ self.on_capture_hook = value
199
+ end
200
+
201
+ # Sets or retrieves the hook to modify formatted exception messages.
202
+ #
203
+ # @yieldparam formatted_string [String] The formatted exception message.
204
+ # @return [Proc] The current on_format hook.
205
+ def on_format(&block)
206
+ if block_given?
207
+ @on_format_hook = block
208
+ else
209
+ @on_format_hook ||= method(:default_on_format)
210
+ end
211
+ end
212
+
213
+ # Sets the on_format hook.
214
+ #
215
+ # @param value [Proc] The proc to set as the on_format hook.
216
+ # @return [Proc] The newly set on_format hook.
217
+ def on_format=(value)
218
+ @on_format_hook = value
219
+ end
220
+
221
+ # Formats the captured binding information into a string based on the specified format.
222
+ #
223
+ # @param captured_bindings [Array<Hash>] The array of captured binding information.
224
+ # @param output_format [Symbol] The format to use for output (:json, :plaintext, :terminal).
225
+ # @return [String] The formatted exception message.
226
+ def format(captured_bindings = [], output_format = get_default_format_for_environment)
227
+ result = binding_infos_array_to_string(captured_bindings, output_format)
228
+ if @on_format_hook
229
+ result = @on_format_hook.call(result)
230
+ else
231
+ result = default_on_format(result)
232
+ end
233
+ result
234
+ end
235
+
236
+ # Converts an array of binding information hashes into a formatted string.
237
+ #
238
+ # @param captured_bindings [Array<Hash>] The array of binding information.
239
+ # @param format [Symbol] The format to use (:json, :plaintext, :terminal).
240
+ # @return [String] The formatted string representation of the binding information.
241
+ def binding_infos_array_to_string(captured_bindings, format = :terminal)
242
+ case format
243
+ when :json
244
+ Colors.enabled = false
245
+ JSON.pretty_generate(captured_bindings)
246
+ when :plaintext
247
+ Colors.enabled = false
248
+ captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
249
+ when :terminal
250
+ Colors.enabled = true
251
+ captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
252
+ else
253
+ Colors.enabled = false
254
+ captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
255
+ end
256
+ end
257
+
258
+ # Determines the default output format based on the current environment.
259
+ #
260
+ # @return [Symbol] The default format (:json, :plaintext, :terminal).
261
+ def get_default_format_for_environment
262
+ return @output_format unless @output_format.nil?
263
+ env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
264
+ @output_format = case env
265
+ when 'development', 'test'
266
+ if running_in_ci?
267
+ :plaintext
268
+ else
269
+ :terminal
270
+ end
271
+ when 'production'
272
+ :json
273
+ else
274
+ :terminal
275
+ end
276
+ end
277
+
278
+ # Checks if the code is running in a Continuous Integration (CI) environment.
279
+ #
280
+ # @return [Boolean] `true` if running in CI, otherwise `false`.
281
+ def running_in_ci?
282
+ return @running_in_ci if defined?(@running_in_ci)
283
+ ci_env_vars = {
284
+ 'CI' => ENV['CI'],
285
+ 'JENKINS' => ENV['JENKINS'],
286
+ 'GITHUB_ACTIONS' => ENV['GITHUB_ACTIONS'],
287
+ 'CIRCLECI' => ENV['CIRCLECI'],
288
+ 'TRAVIS' => ENV['TRAVIS'],
289
+ 'APPVEYOR' => ENV['APPVEYOR'],
290
+ 'GITLAB_CI' => ENV['GITLAB_CI']
291
+ }
292
+ @running_in_ci = ci_env_vars.any? { |_, value| value.to_s.downcase == 'true' }
293
+ end
294
+
295
+ # Applies the skip list to the captured binding information, excluding specified variables.
296
+ #
297
+ # @param binding_info [Hash] The binding information to filter.
298
+ # @return [Hash] The filtered binding information.
299
+ def apply_skip_list(binding_info)
300
+ unless @debug
301
+ variables = binding_info[:variables]
302
+ variables[:instances]&.reject! { |var, _| skip_list.include?(var) || var.to_s.start_with?('@__') }
303
+ variables[:locals]&.reject! { |var, _| skip_list.include?(var) }
304
+ variables[:globals]&.reject! { |var, _| skip_list.include?(var) }
305
+ end
306
+ binding_info
307
+ end
308
+
309
+ # Validates the format of the captured binding information.
310
+ #
311
+ # @param binding_info [Hash] The binding information to validate.
312
+ # @return [Hash, nil] The validated binding information or `nil` if invalid.
313
+ def validate_binding_format(binding_info)
314
+ unless binding_info.keys.include?(:capture_type) && binding_info[:variables].is_a?(Hash)
315
+ puts "Invalid binding_info format."
316
+ return nil
317
+ end
318
+ binding_info
319
+ end
320
+
321
+ # Formats a single binding information hash into a string with colorization.
322
+ #
323
+ # @param binding_info [Hash] The binding information to format.
324
+ # @return [String] The formatted string.
325
+ def binding_info_string(binding_info)
326
+ result = "\n#{Colors.green("#{binding_info[:capture_type].capitalize}: ")}#{Colors.blue(binding_info[:source])}"
327
+
328
+ result += method_and_args_desc(binding_info[:method_and_args])
329
+
330
+ variables = binding_info[:variables] || {}
331
+
332
+ if variables[:locals] && !variables[:locals].empty?
333
+ result += "\n#{Colors.green('Locals:')}\n#{variable_description(variables[:locals])}"
334
+ end
335
+
336
+ instance_vars_to_display = variables[:instances] || {}
337
+
338
+
339
+ if instance_vars_to_display && !instance_vars_to_display.empty?
340
+ result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
341
+ end
342
+
343
+ if variables[:lets] && !variables[:lets].empty?
344
+ result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
345
+ end
346
+
347
+ if variables[:globals] && !variables[:globals].empty?
348
+ result += "\n#{Colors.green('Globals:')}\n#{variable_description(variables[:globals])}"
349
+ end
350
+
351
+ if result.length > max_length
352
+ result = result[0...max_length] + "... (truncated)"
353
+ end
354
+ result + "\n\n"
355
+ end
356
+
357
+ private
358
+
359
+ # Starts the TracePoint for capturing exceptions based on configured events.
360
+ #
361
+ # @return [void]
362
+ def start_tracing
363
+ return if @trace && @trace.enabled?
364
+
365
+ events = [:raise]
366
+ events << :rescue if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
367
+
368
+ @trace = TracePoint.new(*events) do |tp|
369
+ exception = tp.raised_exception
370
+ capture_me = EnhancedErrors.eligible_for_capture.call(exception)
371
+
372
+ next unless capture_me
373
+
374
+ next if Thread.current[:enhanced_errors_processing]
375
+ Thread.current[:enhanced_errors_processing] = true
376
+
377
+ exception = tp.raised_exception
378
+ binding_context = tp.binding
379
+
380
+ unless exception.instance_variable_defined?(:@binding_infos)
381
+ exception.instance_variable_set(:@binding_infos, [])
382
+ exception.extend(ErrorEnhancements)
383
+ end
384
+
385
+ method_name = tp.method_id
386
+ method_and_args = {
387
+ object_name: determine_object_name(tp, method_name),
388
+ args: extract_arguments(tp, method_name)
389
+ }
390
+
391
+ locals = binding_context.local_variables.map { |var|
392
+ [var, binding_context.local_variable_get(var)]
393
+ }.to_h
394
+
395
+ instance_vars = binding_context.receiver.instance_variables
396
+
397
+ instances = instance_vars.map { |var|
398
+ [var, (binding_context.receiver.instance_variable_get(var) rescue "#<Error getting instance variable: #{$!.message}>")]
399
+ }.to_h
400
+
401
+ # Extract 'let' variables from :@__memoized (RSpec specific)
402
+ lets = {}
403
+ if capture_let_variables && instance_vars.include?(:@__memoized)
404
+ outer_memoized = binding_context.receiver.instance_variable_get(:@__memoized)
405
+ memoized = outer_memoized.instance_variable_get(:@memoized) if outer_memoized.respond_to?(:instance_variable_get)
406
+ if memoized.is_a?(Hash)
407
+ lets = memoized&.transform_keys(&:to_sym)
408
+ end
409
+ end
410
+
411
+ globals = {}
412
+ # Capture global variables
413
+ if @debug
414
+ globals = (global_variables - @original_global_variables).map { |var|
415
+ [var, get_global_variable_value(var)]
416
+ }.to_h
417
+ puts "Global Variables: #{globals.inspect}"
418
+ end
419
+
420
+ capture_type = tp.event.to_s # 'raise' or 'rescue'
421
+ location = "#{tp.path}:#{tp.lineno}"
422
+
423
+ binding_info = {
424
+ source: location,
425
+ object: tp.self,
426
+ library: !!GEMS_REGEX.match?(location),
427
+ method_and_args: method_and_args,
428
+ test_name: test_name,
429
+ variables: {
430
+ locals: locals,
431
+ instances: instances,
432
+ lets: lets,
433
+ globals: globals
434
+ },
435
+ exception: exception.class.name,
436
+ capture_type: capture_type
437
+ }
438
+
439
+ if on_capture_hook
440
+ binding_info = on_capture_hook.call(binding_info)
441
+ else
442
+ binding_info = default_on_capture(binding_info)
443
+ end
444
+
445
+ binding_info = validate_binding_format(binding_info)
446
+
447
+ if binding_info
448
+ exception.instance_variable_get(:@binding_infos) << binding_info
449
+ else
450
+ puts "Invalid binding_info returned from on_capture, skipping."
451
+ end
452
+ ensure
453
+ Thread.current[:enhanced_errors_processing] = false
454
+ end
455
+
456
+ @trace.enable
457
+ end
458
+
459
+ def test_name
460
+ return RSpec&.current_example&.full_description if defined?(RSpec)
461
+ nil
462
+ end
463
+
464
+ # Extracts method arguments from the TracePoint binding.
465
+ #
466
+ # @param tp [TracePoint] The current TracePoint.
467
+ # @param method_name [Symbol] The name of the method.
468
+ # @return [String] A string representation of the method arguments.
469
+ def extract_arguments(tp, method_name)
470
+ return '' unless method_name
471
+ begin
472
+ bind = tp.binding
473
+ unbound_method = tp.defined_class.instance_method(method_name)
474
+ method_obj = unbound_method.bind(tp.self)
475
+ parameters = method_obj.parameters
476
+ locals = bind.local_variables
477
+
478
+ return parameters.map do |(type, name)|
479
+ value = locals.include?(name) ? bind.local_variable_get(name) : nil
480
+ "#{name}=#{value.inspect}"
481
+ rescue => e
482
+ "#{name}=#<Error getting argument: #{e.message}>"
483
+ end.join(", ")
484
+ end
485
+
486
+ rescue => e
487
+ "#<Error getting arguments: #{e.message}>"
488
+ end
489
+
490
+
491
+ # Determines the object name based on the TracePoint and method name.
492
+ #
493
+ # @param tp [TracePoint] The current TracePoint.
494
+ # @param method_name [Symbol] The name of the method.
495
+ # @return [String] The formatted object name.
496
+ def determine_object_name(tp, method_name)
497
+ if tp.self.is_a?(Class) && tp.self.singleton_class == tp.defined_class
498
+ "#{tp.self}.#{method_name}"
499
+ else
500
+ "#{tp.self.class.name}##{method_name}"
501
+ end
502
+ rescue => e
503
+ "#<Error inspecting value: #{e.message}>"
504
+ end
505
+
506
+ # Retrieves the value of a global variable by its name.
507
+ #
508
+ # @param var [Symbol] The name of the global variable.
509
+ # @return [Object, String] The value of the global variable or an error message.
510
+ def get_global_variable_value(var)
511
+ begin
512
+ var.is_a?(Symbol) ? eval("#{var}") : nil
513
+ rescue => e
514
+ "#<Error getting value: #{e.message}>"
515
+ end
516
+ end
517
+
518
+ # Generates a description for method and arguments.
519
+ #
520
+ # @param method_info [Hash] Information about the method and its arguments.
521
+ # @return [String] The formatted description.
522
+ def method_and_args_desc(method_info)
523
+ return '' unless method_info[:object_name] != '' || method_info[:args]&.length.to_i > 0
524
+ arg_str = method_info[:args]
525
+ arg_str = "(#{arg_str})" if arg_str != ""
526
+ str = method_info[:object_name] + arg_str
527
+ "\n#{Colors.green('Method: ')}#{Colors.blue(str)}\n"
528
+ end
529
+
530
+ # Generates a formatted description for a set of variables.
531
+ #
532
+ # @param vars_hash [Hash] A hash of variable names and their values.
533
+ # @return [String] The formatted variables description.
534
+ def variable_description(vars_hash)
535
+ vars_hash.map do |name, value|
536
+ " #{Colors.purple(name)}: #{format_variable(value)}\n"
537
+ end.join
538
+ end
539
+
540
+ # Formats a variable for display, using `awesome_print` if available and enabled.
541
+ #
542
+ # @param variable [Object] The variable to format.
543
+ # @return [String] The formatted variable.
544
+ def format_variable(variable)
545
+ (awesome_print_available? && Colors.enabled?) ? variable.ai : variable.inspect
546
+ end
547
+
548
+ # Checks if the `AwesomePrint` gem is available.
549
+ #
550
+ # @return [Boolean] `true` if `AwesomePrint` is available, otherwise `false`.
551
+ def awesome_print_available?
552
+ return @awesome_print_available unless @awesome_print_available.nil?
553
+ @awesome_print_available = defined?(AwesomePrint)
554
+ end
555
+
556
+ # Default implementation for the on_format hook.
557
+ #
558
+ # @param string [String] The formatted exception message.
559
+ # @return [String] The unmodified exception message.
560
+ def default_on_format(string)
561
+ string
562
+ end
563
+
564
+ # Default implementation for the on_capture hook, applying the skip list.
565
+ #
566
+ # @param binding_info [Hash] The captured binding information.
567
+ # @return [Hash] The filtered binding information.
568
+ def default_on_capture(binding_info)
569
+ # Use this to clean up the captured bindings
570
+ EnhancedErrors.apply_skip_list(binding_info)
571
+ end
572
+
573
+ # Default eligibility check for capturing exceptions.
574
+ #
575
+ # @param exception [Exception] The exception to evaluate.
576
+ # @return [Boolean] `true` if the exception should be captured, otherwise `false`.
577
+ def default_eligible_for_capture(exception)
578
+ true
579
+ end
580
+
581
+ @enabled = false
582
+ end
583
+ end
@@ -0,0 +1,53 @@
1
+ module ErrorEnhancements
2
+ def message
3
+ original_message = super()
4
+ "#{original_message}#{variables_message}"
5
+ rescue => e
6
+ puts "Error in message method: #{e.message}"
7
+ original_message
8
+ end
9
+
10
+ def variables_message
11
+ @variables_message ||= begin
12
+ bindings_of_interest = []
13
+ if defined?(@binding_infos) && @binding_infos && !@binding_infos.empty?
14
+ bindings_of_interest = select_binding_infos(@binding_infos)
15
+ end
16
+ EnhancedErrors.format(bindings_of_interest)
17
+ rescue => e
18
+ puts "Error in variables_message: #{e.message}"
19
+ ""
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def select_binding_infos(binding_infos)
26
+ # Preference:
27
+ # Grab the first raise binding that isn't a library (gem) binding.
28
+ # If there are only library bindings, grab the first one.
29
+ # Grab the last rescue binding if we have one
30
+
31
+ bindings_of_interest = []
32
+ binding_infos.each do |info|
33
+ if info[:capture_type] == 'raise' && !info[:library]
34
+ bindings_of_interest << info
35
+ break
36
+ end
37
+ end
38
+
39
+ if bindings_of_interest.empty?
40
+ bindings_of_interest << binding_infos.first if binding_infos.first
41
+ end
42
+
43
+ # find the last rescue binding if there is one
44
+ binding_infos.reverse.each do |info|
45
+ if info[:capture_type] == 'rescue'
46
+ bindings_of_interest << info
47
+ break
48
+ end
49
+ end
50
+ bindings_of_interest
51
+ end
52
+
53
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: enhanced_errors
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eric Beland
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-10-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.4.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.4.0
27
+ description: With no extra dependencies, and using only Ruby's built-in TracePoint,
28
+ EnhancedErrors will automatically enhance your errors with messages containing variable
29
+ values from the moment they were raised.
30
+ email:
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE
36
+ - README.md
37
+ - benchmark/benchmark.rb
38
+ - benchmark/stackprofile.rb
39
+ - doc/images/enhance.png
40
+ - doc/images/enhanced-error.png
41
+ - doc/images/enhanced-spec.png
42
+ - enhanced_errors.gemspec
43
+ - examples/division_by_zero_example.rb
44
+ - examples/example_spec.rb
45
+ - lib/binding.rb
46
+ - lib/colors.rb
47
+ - lib/enhanced_errors.rb
48
+ - lib/error_enhancements.rb
49
+ homepage: https://github.com/ericbeland/enhanced_errors.
50
+ licenses: []
51
+ metadata:
52
+ homepage_uri: https://github.com/ericbeland/enhanced_errors.
53
+ source_code_uri: https://github.com/ericbeland/enhanced_errors
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 3.0.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.3.26
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Automatically enhance your errors with messages containing variable values
73
+ from the moment they were raised.
74
+ test_files: []