enhanced_errors 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: 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: []