debugger_replay 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: 05e538193555af9b38c46655099ed40a6cf70a789acd8a13267b69018d8e8b24
4
+ data.tar.gz: dd054040cf7f6f70c2c598e0ecd18823a18f6f05366cd662132d72be8122b97f
5
+ SHA512:
6
+ metadata.gz: e517f19aa71fc87d26f8012b74f0bc54d8f737aba7c6daad00e9b29f61f39fc72a917cf4b54f62ed2872d253e3496dcb4dc6e2a3af28e02c586e75e4de1a6b16
7
+ data.tar.gz: d4bfd7c7f913cd30e440b7f84c5dcb451c05069d8b310048c79e4525dd87aa401feaf486ed68b59a818c4e704bd348b13672fa634ad3063db36866c1bfdc0496
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ted Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # debugger_replay
2
+
3
+ Hot-reload and replay requests during debugging. Edit code, type `r`, see changes instantly.
4
+
5
+ https://github.com/user-attachments/assets/8908892d-ed77-48eb-9fd7-d6b4f0743185
6
+
7
+ ## Compare
8
+
9
+ | Steps | Without debugger_replay | With debugger_replay |
10
+ |-------|----------------------------|----------------------|
11
+ | 1 | Hit breakpoint | Hit breakpoint |
12
+ | 2 | Edit code | Edit code |
13
+ | 3 | Type `c` to continue | Type `r` |
14
+ | 4 | Switch to browser, refresh | Breakpoint hit again |
15
+ | 5 | Breakpoint hit again | |
16
+
17
+ ## Use cases
18
+
19
+ Replays HTTP requests, ActiveJob jobs, ActionMailer deliveries, Rake tasks
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ group :development do
25
+ gem "debugger_replay"
26
+ end
27
+ ```
28
+
29
+ ## Commands
30
+
31
+ | Command | Description |
32
+ |---------|--------------------------------------|
33
+ | `r` | Reload code + auto-continue + replay |
34
+
35
+ ## How it works
36
+
37
+ Middleware captures request details. `r` triggers `Rails.application.reloader.reload!`, queues replay, auto-continues. After current request completes, replays with fresh code.
38
+
39
+ ## License
40
+
41
+ MIT
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebuggerReplay
4
+ class << self
5
+ attr_accessor :last_request, :last_job, :last_mailer, :last_rake
6
+ attr_accessor :current_context
7
+
8
+ def capture_request!(env)
9
+ @current_context = :request
10
+ @last_request = {
11
+ method: env["REQUEST_METHOD"],
12
+ path: env["PATH_INFO"],
13
+ query_string: env["QUERY_STRING"],
14
+ content_type: env["CONTENT_TYPE"],
15
+ body: read_body(env),
16
+ headers: extract_headers(env),
17
+ cookies: env["HTTP_COOKIE"],
18
+ host: env["HTTP_HOST"],
19
+ timestamp: defined?(Time.current) ? Time.current : Time.now
20
+ }
21
+ end
22
+
23
+ def capture_job!(job)
24
+ @current_context = :job
25
+ @last_job = {
26
+ class: job.class,
27
+ arguments: job.arguments,
28
+ job_id: job.job_id,
29
+ queue_name: job.queue_name,
30
+ timestamp: defined?(Time.current) ? Time.current : Time.now
31
+ }
32
+ end
33
+
34
+ def capture_mailer!(mailer_class, action, args)
35
+ @current_context = :mailer
36
+ @last_mailer = {
37
+ class: mailer_class.to_s,
38
+ action: action.to_s,
39
+ arguments: args,
40
+ timestamp: defined?(Time.current) ? Time.current : Time.now
41
+ }
42
+ end
43
+
44
+ def capture_rake!(task_name, args)
45
+ @current_context = :rake
46
+ @last_rake = {
47
+ task_name: task_name,
48
+ arguments: args,
49
+ timestamp: defined?(Time.current) ? Time.current : Time.now
50
+ }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebuggerReplay
4
+ class << self
5
+ attr_accessor :pending_replay, :pending_job, :pending_mailer, :pending_rake
6
+
7
+ def replay!
8
+ case current_context
9
+ when :job then last_job ? replay_job! : puts("No job captured.")
10
+ when :mailer then last_mailer ? replay_mailer! : puts("No mailer captured.")
11
+ when :rake then last_rake ? replay_rake! : puts("No rake task captured.")
12
+ when :request then last_request ? replay_request! : puts("No request captured.")
13
+ else puts "No request, job, mailer, or rake task captured."
14
+ end
15
+ nil
16
+ end
17
+
18
+ def replay_request!
19
+ @pending_replay = last_request.dup
20
+ reload_and_queue!("request")
21
+ end
22
+
23
+ def replay_job!
24
+ @pending_job = last_job.dup
25
+ reload_and_queue!("job")
26
+ end
27
+
28
+ def replay_mailer!
29
+ @pending_mailer = last_mailer.dup
30
+ reload_and_queue!("mailer")
31
+ end
32
+
33
+ def replay_rake!
34
+ @pending_rake = last_rake.dup
35
+ reload_and_queue!("rake")
36
+ end
37
+
38
+ def execute_pending_request!
39
+ return unless @pending_replay
40
+ req = @pending_replay
41
+ @pending_replay = nil
42
+ puts "\n=> Replaying: #{req[:method]} #{req[:path]}"
43
+ Rails.application.call(build_env(req))
44
+ end
45
+
46
+ def execute_pending_job!
47
+ return unless @pending_job
48
+ job = @pending_job
49
+ @pending_job = nil
50
+ puts "\n=> Replaying: #{job[:class]}#perform(#{format_args(job[:arguments])})"
51
+ job[:class].perform_now(*job[:arguments])
52
+ end
53
+
54
+ def execute_pending_mailer!
55
+ return unless @pending_mailer
56
+ mailer = @pending_mailer
57
+ @pending_mailer = nil
58
+ puts "\n=> Replaying: #{mailer[:class]}##{mailer[:action]}(#{format_args(mailer[:arguments])})"
59
+ mailer[:class].constantize.send(mailer[:action], *mailer[:arguments]).deliver_now
60
+ end
61
+
62
+ def execute_pending_rake!
63
+ return unless @pending_rake
64
+ rake = @pending_rake
65
+ @pending_rake = nil
66
+ puts "\n=> Replaying: rake #{rake[:task_name]}[#{rake[:arguments].join(',')}]"
67
+ task = Rake::Task[rake[:task_name]]
68
+ task.reenable
69
+ task.invoke(*rake[:arguments])
70
+ end
71
+
72
+ private
73
+
74
+ def reload_and_queue!(type)
75
+ puts "\n=> Reloading code..."
76
+ Rails.application.reloader.reload!
77
+ puts "=> #{type.capitalize} replay queued."
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebuggerReplay
4
+ class << self
5
+ private
6
+
7
+ def build_env(req)
8
+ url = req[:path]
9
+ url += "?#{req[:query_string]}" if req[:query_string]&.length&.positive?
10
+ env = Rack::MockRequest.env_for(url, method: req[:method], input: req[:body],
11
+ "CONTENT_TYPE" => req[:content_type], "HTTP_HOST" => req[:host] || "localhost")
12
+ req[:headers].each { |k, v| env["HTTP_#{k.upcase.tr('-', '_')}"] = v }
13
+ env["HTTP_COOKIE"] = req[:cookies] if req[:cookies]&.length&.positive?
14
+ env
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebuggerReplay
4
+ class << self
5
+ private
6
+
7
+ def format_args(args)
8
+ args.map(&:inspect).join(", ")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebuggerReplay
4
+ class << self
5
+ private
6
+
7
+ def read_body(env)
8
+ return nil unless env["rack.input"]
9
+ body = env["rack.input"].read
10
+ env["rack.input"].rewind
11
+ body&.length&.positive? ? body : nil
12
+ end
13
+
14
+ def extract_headers(env)
15
+ env.select { |k, _| k.start_with?("HTTP_") && !%w[HTTP_COOKIE HTTP_HOST].include?(k) }
16
+ .transform_keys { |k| k.delete_prefix("HTTP_").split("_").map(&:capitalize).join("-") }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebuggerReplay
4
+ class MailerSubscriber
5
+ def call(name, started, finished, unique_id, payload)
6
+ mailer_class = payload[:mailer].to_s.split("#").first
7
+ action = payload[:action].to_s
8
+ DebuggerReplay.capture_mailer!(mailer_class, action, payload[:args] || [])
9
+ end
10
+ end
11
+
12
+ module JobReplayCallback
13
+ def self.included(base)
14
+ base.class_eval do
15
+ after_perform do
16
+ DebuggerReplay.execute_pending_job!
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebuggerReplay
4
+ class RequestCapture
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ path = env["PATH_INFO"]
11
+ DebuggerReplay.capture_request!(env) unless IGNORED_PATHS.any? { |p| path.start_with?(p) }
12
+ @app.call(env)
13
+ end
14
+ end
15
+
16
+ class ReplayExecutor
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ result = @app.call(env)
23
+ DebuggerReplay.execute_pending_request!
24
+ result
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ module DebuggerReplay
2
+ class Railtie < Rails::Railtie
3
+ initializer "debugger_replay.setup", after: :load_config_initializers do
4
+ next unless Rails.env.development?
5
+
6
+ # Request middleware
7
+ Rails.application.config.middleware.insert_before(0, DebuggerReplay::RequestCapture)
8
+ Rails.application.config.middleware.use(DebuggerReplay::ReplayExecutor)
9
+
10
+ # Job support
11
+ DebuggerReplay.subscribe_to_jobs!
12
+ ActiveSupport.on_load(:active_job) do
13
+ include DebuggerReplay::JobReplayCallback
14
+ end
15
+
16
+ # Mailer support
17
+ DebuggerReplay.subscribe_to_mailers!
18
+
19
+ # Rake support
20
+ DebuggerReplay.instrument_rake_tasks!
21
+
22
+ # Define `r` shortcut at top level
23
+ Object.send(:define_method, :r) do
24
+ DebuggerReplay.replay!
25
+ if defined?(DEBUGGER__::SESSION)
26
+ DEBUGGER__::SESSION.add_preset_commands("replay", ["c"], kick: false, continue: true)
27
+ end
28
+ nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DebuggerReplay
4
+ class << self
5
+ def subscribe_to_jobs!
6
+ return unless defined?(ActiveSupport::Notifications)
7
+ ActiveSupport::Notifications.subscribe("perform_start.active_job") { |*, payload| capture_job!(payload[:job]) }
8
+ end
9
+
10
+ def subscribe_to_mailers!
11
+ return unless defined?(ActiveSupport::Notifications)
12
+ ActiveSupport::Notifications.subscribe("process.action_mailer", MailerSubscriber.new)
13
+ ActiveSupport::Notifications.subscribe("deliver.action_mailer") { |*, _| execute_pending_mailer! }
14
+ end
15
+
16
+ def instrument_rake_tasks!
17
+ return unless defined?(Rake::Task)
18
+ Rake::Task.class_eval do
19
+ alias_method :original_execute, :execute
20
+ def execute(args = nil)
21
+ DebuggerReplay.capture_rake!(name, args&.to_a || [])
22
+ original_execute(args).tap { DebuggerReplay.execute_pending_rake! }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module DebuggerReplay
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "debugger_replay/version"
4
+
5
+ # helpers/ - Private utility methods
6
+ require_relative "debugger_replay/helpers/env_builder"
7
+ require_relative "debugger_replay/helpers/request_parser"
8
+ require_relative "debugger_replay/helpers/formatter"
9
+
10
+ # core/ - Main logic
11
+ require_relative "debugger_replay/core/capture"
12
+ require_relative "debugger_replay/core/replay"
13
+
14
+ # setup/ - Rails integration
15
+ require_relative "debugger_replay/setup/subscriptions"
16
+ require_relative "debugger_replay/setup/middleware"
17
+ require_relative "debugger_replay/setup/callbacks"
18
+
19
+ module DebuggerReplay
20
+ IGNORED_PATHS = %w[/cable /assets /rails/ /logo /avatar /banner].freeze
21
+ end
22
+
23
+ require_relative "debugger_replay/setup/railtie" if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: debugger_replay
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nguyen Vu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-01-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ description: Replay HTTP requests, ActiveJob jobs, ActionMailer deliveries, and Rake
28
+ tasks with hot-reloaded code at debugger breakpoints.
29
+ email:
30
+ - ''
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE
36
+ - README.md
37
+ - lib/debugger_replay.rb
38
+ - lib/debugger_replay/core/capture.rb
39
+ - lib/debugger_replay/core/replay.rb
40
+ - lib/debugger_replay/helpers/env_builder.rb
41
+ - lib/debugger_replay/helpers/formatter.rb
42
+ - lib/debugger_replay/helpers/request_parser.rb
43
+ - lib/debugger_replay/setup/callbacks.rb
44
+ - lib/debugger_replay/setup/middleware.rb
45
+ - lib/debugger_replay/setup/railtie.rb
46
+ - lib/debugger_replay/setup/subscriptions.rb
47
+ - lib/debugger_replay/version.rb
48
+ homepage: https://github.com/tednguyendev/debugger_replay
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/tednguyendev/debugger_replay
53
+ source_code_uri: https://github.com/tednguyendev/debugger_replay
54
+ changelog_uri: https://github.com/tednguyendev/debugger_replay/blob/main/CHANGELOG.md
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.0.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.5.22
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Hot-reload and replay requests during debugging
74
+ test_files: []