callback_tracer 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: dc2f5bb8dbc7993f062b1a9278293e8927e3003b57ffd8393679e1fa378184e2
4
+ data.tar.gz: 12b226db470343333e74512e8c29ca39437eccdc75821ecd2516b1747a32bf83
5
+ SHA512:
6
+ metadata.gz: 8758991c2da82f688267e8b0bda7f029be6a74171aaa7b9d8050abd8b9e8dd7008669d40d5d81edabf866330f2844062ed9b25a76e210b61f3653d67137ccf95
7
+ data.tar.gz: fcfb5d5b1202b7183654a1d111c533ce7bd7a03b8a8bc968870051aca8965dfb183bbe880bcefee710e7eb63438cd554c65af6f0f20fb0af4fc9b9bd5c26d018
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-03-24)
4
+
5
+ - Initial release
6
+ - Trace all ActiveRecord callback chains (validation, save, create, update, destroy, commit, rollback, initialize, find, touch)
7
+ - Source location and timing for each callback
8
+ - Around callback enter/exit tracing
9
+ - Colorized terminal output
10
+ - Configurable: enable/disable, exclude models, custom logger
11
+ - Rails generator for initializer setup
12
+ - Automatic production safety (disabled in production)
13
+ - Rack middleware for per-request buffer flushing
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Shehab Mohamed
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # CallbackTracer
2
+
3
+ Trace ActiveRecord callback execution order with source locations and timing. See exactly which callbacks fire, in what order, and how long each takes.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "callback_tracer", group: [:development, :test]
11
+ ```
12
+
13
+ Run the install generator:
14
+
15
+ ```bash
16
+ rails generate callback_tracer:install
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ ```ruby
22
+ # config/initializers/callback_tracer.rb
23
+ CallbackTracer.configure do |config|
24
+ config.enabled = true
25
+ config.excluded_models = ["ApplicationRecord"]
26
+ config.colorize = true
27
+ config.logger = nil # defaults to puts
28
+ end
29
+ ```
30
+
31
+ ## Output
32
+
33
+ ```
34
+ [CallbackTracer] Post before_validation (app/models/post.rb:11) 0.02ms
35
+ [CallbackTracer] Post after_validation (app/models/post.rb:12) 0.01ms
36
+ [CallbackTracer] Post before_save (app/models/post.rb:16) 0.03ms
37
+ [CallbackTracer] Post before_create (app/models/post.rb:21) 0.15ms
38
+ [CallbackTracer] Post after_create (app/models/post.rb:23) 0.02ms
39
+ [CallbackTracer] Post after_save (app/models/post.rb:18) 0.01ms
40
+ [CallbackTracer] Post after_commit (app/models/post.rb:31) 0.04ms
41
+ ```
42
+
43
+ ## Requirements
44
+
45
+ - Ruby 3.1+
46
+ - Rails 7.0+
47
+
48
+ Automatically disabled in production.
@@ -0,0 +1,44 @@
1
+ module CallbackTracer
2
+ class Configuration
3
+ attr_reader :enabled, :excluded_models, :colorize, :logger
4
+
5
+ def initialize
6
+ @enabled = false
7
+ @excluded_models = []
8
+ @colorize = true
9
+ @logger = nil
10
+ end
11
+
12
+ def enabled=(value)
13
+ @enabled = !!value
14
+ end
15
+
16
+ def colorize=(value)
17
+ @colorize = !!value
18
+ end
19
+
20
+ def excluded_models=(value)
21
+ raise ArgumentError, "excluded_models must be an Array" unless value.is_a?(Array)
22
+
23
+ @excluded_models = value
24
+ end
25
+
26
+ def logger=(value)
27
+ if value && !value.respond_to?(:info)
28
+ raise ArgumentError, "logger must respond to #info"
29
+ end
30
+
31
+ @logger = value
32
+ end
33
+
34
+ def excluded?(model_class)
35
+ model_name = model_class.name
36
+ return false unless model_name
37
+
38
+ excluded_models.any? do |name|
39
+ model_name == name ||
40
+ model_class.ancestors.any? { |a| a.name == name && name != "ActiveRecord::Base" }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,78 @@
1
+ module CallbackTracer
2
+ module LogFormatter
3
+ COLORS = {
4
+ prefix: "\e[36m", # cyan
5
+ model: "\e[33m", # yellow
6
+ callback: "\e[32m", # green
7
+ location: "\e[90m", # gray
8
+ timing: "\e[35m", # magenta
9
+ around: "\e[34m", # blue
10
+ reset: "\e[0m"
11
+ }.freeze
12
+
13
+ # Control characters that could be used for log injection or terminal escape attacks
14
+ CONTROL_CHAR_PATTERN = /[\r\n\e\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/
15
+
16
+ module_function
17
+
18
+ def sanitize(str)
19
+ str.to_s.gsub(CONTROL_CHAR_PATTERN, "")
20
+ end
21
+
22
+ def format_callback(model_name:, callback_kind:, callback_type:, location:, duration_ms:, method_name: nil, colorize: true)
23
+ model_name = sanitize(model_name)
24
+ kind_label = sanitize("#{callback_type}_#{callback_kind}")
25
+ kind_label = "#{kind_label} :#{method_name}" if method_name
26
+ loc_str = location ? "(#{sanitize(location)})" : "(unknown)"
27
+ time_str = "%.2fms" % duration_ms
28
+
29
+ if colorize
30
+ "#{c(:prefix)}[CallbackTracer]#{c(:reset)} " \
31
+ "#{c(:model)}#{model_name}#{c(:reset)} " \
32
+ "#{c(:callback)}#{kind_label.ljust(25)}#{c(:reset)} " \
33
+ "#{c(:location)}#{loc_str}#{c(:reset)} " \
34
+ "#{c(:timing)}#{time_str}#{c(:reset)}"
35
+ else
36
+ "[CallbackTracer] #{model_name} #{kind_label.ljust(25)} #{loc_str} #{time_str}"
37
+ end
38
+ end
39
+
40
+ def format_around_enter(model_name:, callback_kind:, location:, method_name: nil, colorize: true)
41
+ model_name = sanitize(model_name)
42
+ loc_str = location ? "(#{sanitize(location)})" : "(unknown)"
43
+ label = sanitize("around_#{callback_kind} [enter]")
44
+ label = "#{label} :#{method_name}" if method_name
45
+
46
+ if colorize
47
+ "#{c(:prefix)}[CallbackTracer]#{c(:reset)} " \
48
+ "#{c(:model)}#{model_name}#{c(:reset)} " \
49
+ "#{c(:around)}#{label.ljust(25)}#{c(:reset)} " \
50
+ "#{c(:location)}#{loc_str}#{c(:reset)}"
51
+ else
52
+ "[CallbackTracer] #{model_name} #{label.ljust(25)} #{loc_str}"
53
+ end
54
+ end
55
+
56
+ def format_around_exit(model_name:, callback_kind:, location:, duration_ms:, method_name: nil, colorize: true)
57
+ model_name = sanitize(model_name)
58
+ loc_str = location ? "(#{sanitize(location)})" : "(unknown)"
59
+ time_str = "%.2fms" % duration_ms
60
+ label = sanitize("around_#{callback_kind} [exit]")
61
+ label = "#{label} :#{method_name}" if method_name
62
+
63
+ if colorize
64
+ "#{c(:prefix)}[CallbackTracer]#{c(:reset)} " \
65
+ "#{c(:model)}#{model_name}#{c(:reset)} " \
66
+ "#{c(:around)}#{label.ljust(25)}#{c(:reset)} " \
67
+ "#{c(:location)}#{loc_str}#{c(:reset)} " \
68
+ "#{c(:timing)}#{time_str}#{c(:reset)}"
69
+ else
70
+ "[CallbackTracer] #{model_name} #{label.ljust(25)} #{loc_str} #{time_str}"
71
+ end
72
+ end
73
+
74
+ def c(name)
75
+ COLORS[name]
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,15 @@
1
+ module CallbackTracer
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ CallbackTracer.clear_buffer!
9
+ response = @app.call(env)
10
+ response
11
+ ensure
12
+ CallbackTracer.flush_buffer!
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module CallbackTracer
2
+ class Railtie < Rails::Railtie
3
+ initializer "callback_tracer.setup" do
4
+ ActiveSupport.on_load(:active_record) do
5
+ CallbackTracer.setup! unless Rails.env.production?
6
+ end
7
+ end
8
+
9
+ initializer "callback_tracer.middleware" do |app|
10
+ app.middleware.use CallbackTracer::Middleware unless Rails.env.production?
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,59 @@
1
+ module CallbackTracer
2
+ module SourceLocator
3
+ module_function
4
+
5
+ def locate(filter, model_class)
6
+ location = extract_location(filter, model_class)
7
+ return nil unless location
8
+
9
+ file, line = location
10
+ shorten_path(file, line)
11
+ end
12
+
13
+ def extract_location(filter, model_class)
14
+ case filter
15
+ when Symbol
16
+ if model_class.method_defined?(filter, false) || model_class.private_method_defined?(filter, false)
17
+ model_class.instance_method(filter).source_location
18
+ elsif model_class.method_defined?(filter) || model_class.private_method_defined?(filter)
19
+ model_class.instance_method(filter).source_location
20
+ end
21
+ when Proc
22
+ filter.source_location
23
+ when Object
24
+ # Callable object — try common callback method names
25
+ [:before, :after, :around, :call].each do |m|
26
+ return filter.method(m).source_location if filter.respond_to?(m)
27
+ end
28
+ nil
29
+ end
30
+ rescue NameError, TypeError
31
+ nil
32
+ end
33
+
34
+ # Matches bundled gem paths like /gems/activerecord-7.1.0/ or /gems/railties-7.1.0/
35
+ BUNDLED_GEM_PATH_PATTERN = %r{/gems/[a-zA-Z0-9_-]+-\d+}
36
+
37
+ def framework_source?(filter, model_class)
38
+ location = extract_location(filter, model_class)
39
+ return false unless location
40
+
41
+ file = location[0].to_s
42
+ return true if file.match?(BUNDLED_GEM_PATH_PATTERN)
43
+ return true if defined?(Rails) && Rails.respond_to?(:root) && Rails.root && !file.start_with?(Rails.root.to_s)
44
+
45
+ false
46
+ end
47
+
48
+ def shorten_path(file, line)
49
+ shortened = if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
50
+ file.sub("#{Rails.root}/", "")
51
+ elsif defined?(Bundler) && Bundler.respond_to?(:root)
52
+ file.sub("#{Bundler.root}/", "")
53
+ else
54
+ File.basename(File.dirname(file)) + "/" + File.basename(file)
55
+ end
56
+ "#{shortened}:#{line}"
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,218 @@
1
+ require "logger"
2
+ require "monitor"
3
+
4
+ module CallbackTracer
5
+ module Tracer
6
+ TRACED_KINDS = %i[
7
+ validation save create update destroy
8
+ commit rollback initialize find touch
9
+ ].freeze
10
+
11
+ TRACE_MONITOR = Monitor.new
12
+
13
+ def run_callbacks(kind, &block)
14
+ unless CallbackTracer.enabled? &&
15
+ TRACED_KINDS.include?(kind) &&
16
+ !CallbackTracer.configuration.excluded?(self.class)
17
+ return super
18
+ end
19
+
20
+ callbacks = self.class.__callbacks[kind]
21
+ return super unless callbacks
22
+
23
+ chain = callbacks.send(:chain).dup
24
+ colorize = CallbackTracer.configuration.colorize
25
+ model_name = LogFormatter.sanitize(self.class.name.to_s)
26
+ logger = CallbackTracer.configuration.logger
27
+
28
+ # Build wrappers for each callback in the chain, skipping framework-internal ones
29
+ wrappers = chain.filter_map do |cb|
30
+ filter = cb.filter
31
+ next if SourceLocator.framework_source?(filter, self.class)
32
+
33
+ location = SourceLocator.locate(filter, self.class)
34
+ cb_kind = cb.kind # :before, :after, :around
35
+ method_name = filter.is_a?(Symbol) ? filter : nil
36
+
37
+ { callback: cb, filter: filter, location: location, cb_kind: cb_kind, method_name: method_name }
38
+ end
39
+
40
+ # Install temporary method wrappers to trace individual callbacks
41
+ install_method_wrappers(wrappers, model_name, kind, colorize, logger)
42
+
43
+ # For proc filters, wrap them thread-safely without mutating shared state
44
+ trace_proc_filters(wrappers, model_name, kind, colorize, logger) do
45
+ super(kind, &block)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def install_method_wrappers(wrappers, model_name, callback_kind, colorize, logger)
52
+ # Track which methods we've already wrapped on this instance to avoid accumulation
53
+ @_callback_tracer_wrapped ||= Set.new
54
+
55
+ wrappers.each do |w|
56
+ filter = w[:filter]
57
+ next unless filter.is_a?(Symbol)
58
+ next if @_callback_tracer_wrapped.include?(filter)
59
+ next unless self.class.method_defined?(filter, false) || self.class.private_method_defined?(filter, false)
60
+
61
+ location = w[:location]
62
+ cb_kind = w[:cb_kind]
63
+ method_name = w[:method_name]
64
+
65
+ wrapper_module = Module.new do
66
+ if cb_kind == :around
67
+ define_method(filter) do |*args, &blk|
68
+ output_enter = LogFormatter.format_around_enter(
69
+ model_name: model_name,
70
+ callback_kind: callback_kind,
71
+ location: location,
72
+ method_name: method_name,
73
+ colorize: colorize
74
+ )
75
+ log_trace(output_enter, logger)
76
+
77
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
78
+ result = super(*args, &blk)
79
+ duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
80
+
81
+ output_exit = LogFormatter.format_around_exit(
82
+ model_name: model_name,
83
+ callback_kind: callback_kind,
84
+ location: location,
85
+ method_name: method_name,
86
+ duration_ms: duration,
87
+ colorize: colorize
88
+ )
89
+ log_trace(output_exit, logger)
90
+ result
91
+ end
92
+ else
93
+ define_method(filter) do |*args, &blk|
94
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
95
+ result = super(*args, &blk)
96
+ duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
97
+
98
+ output = LogFormatter.format_callback(
99
+ model_name: model_name,
100
+ callback_kind: callback_kind,
101
+ callback_type: cb_kind,
102
+ location: location,
103
+ method_name: method_name,
104
+ duration_ms: duration,
105
+ colorize: colorize
106
+ )
107
+ log_trace(output, logger)
108
+ result
109
+ end
110
+ end
111
+ end
112
+
113
+ self.singleton_class.prepend(wrapper_module)
114
+ @_callback_tracer_wrapped.add(filter)
115
+ end
116
+ end
117
+
118
+ def trace_proc_filters(wrappers, model_name, callback_kind, colorize, logger)
119
+ proc_wrappers = wrappers.select { |w| w[:filter].is_a?(Proc) }
120
+
121
+ if proc_wrappers.empty?
122
+ return yield
123
+ end
124
+
125
+ # Thread-safe approach: duplicate the callback objects before mutating,
126
+ # then swap the entire chain under a mutex so no shared state is modified.
127
+ callbacks_set = self.class.__callbacks[callback_kind]
128
+ original_chain = callbacks_set.send(:chain)
129
+
130
+ # Deep-copy the callback objects we need to wrap
131
+ cloned_cbs = {}
132
+ proc_wrappers.each do |w|
133
+ cb = w[:callback]
134
+ cloned = cb.dup
135
+ cloned_cbs[cb.object_id] = { original: cb, clone: cloned }
136
+ end
137
+
138
+ # Build wrapped procs on the cloned copies (no shared state mutation)
139
+ cloned_cbs.each_value do |entry|
140
+ cloned_cb = entry[:clone]
141
+ w = proc_wrappers.find { |pw| pw[:callback].object_id == entry[:original].object_id }
142
+ filter = w[:filter]
143
+ location = w[:location]
144
+ cb_kind = w[:cb_kind]
145
+
146
+ wrapped_proc = build_wrapped_proc(filter, cb_kind, model_name, callback_kind, location, colorize, logger)
147
+ cloned_cb.instance_variable_set(:@filter, wrapped_proc)
148
+ end
149
+
150
+ # Hold mutex for the entire swap-yield-restore cycle to prevent
151
+ # TOCTOU race conditions between concurrent threads.
152
+ TRACE_MONITOR.synchronize do
153
+ cloned_cbs.each_value do |entry|
154
+ idx = original_chain.index(entry[:original])
155
+ original_chain[idx] = entry[:clone] if idx
156
+ end
157
+
158
+ begin
159
+ yield
160
+ ensure
161
+ cloned_cbs.each_value do |entry|
162
+ idx = original_chain.index(entry[:clone])
163
+ original_chain[idx] = entry[:original] if idx
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ def build_wrapped_proc(filter, cb_kind, model_name, callback_kind, location, colorize, logger)
170
+ if cb_kind == :around
171
+ proc do |record, &blk|
172
+ output_enter = LogFormatter.format_around_enter(
173
+ model_name: model_name,
174
+ callback_kind: callback_kind,
175
+ location: location,
176
+ colorize: colorize
177
+ )
178
+ record.send(:log_trace, output_enter, logger)
179
+
180
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
181
+ result = filter.call(record, &blk)
182
+ duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
183
+
184
+ output_exit = LogFormatter.format_around_exit(
185
+ model_name: model_name,
186
+ callback_kind: callback_kind,
187
+ location: location,
188
+ duration_ms: duration,
189
+ colorize: colorize
190
+ )
191
+ record.send(:log_trace, output_exit, logger)
192
+ result
193
+ end
194
+ else
195
+ proc do |record, &blk|
196
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
197
+ result = record.instance_exec(&filter)
198
+ duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
199
+
200
+ output = LogFormatter.format_callback(
201
+ model_name: model_name,
202
+ callback_kind: callback_kind,
203
+ callback_type: cb_kind,
204
+ location: location,
205
+ duration_ms: duration,
206
+ colorize: colorize
207
+ )
208
+ record.send(:log_trace, output, logger)
209
+ result
210
+ end
211
+ end
212
+ end
213
+
214
+ def log_trace(message, _logger = nil)
215
+ CallbackTracer.buffer_message(message)
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,3 @@
1
+ module CallbackTracer
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,72 @@
1
+ require "active_support"
2
+ require "active_record"
3
+
4
+ require_relative "callback_tracer/version"
5
+ require_relative "callback_tracer/configuration"
6
+ require_relative "callback_tracer/source_locator"
7
+ require_relative "callback_tracer/log_formatter"
8
+ require_relative "callback_tracer/tracer"
9
+ require_relative "callback_tracer/middleware"
10
+ require_relative "callback_tracer/railtie" if defined?(Rails::Railtie)
11
+
12
+ module CallbackTracer
13
+ SETUP_MUTEX = Mutex.new
14
+
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield(configuration)
22
+ end
23
+
24
+ def setup!
25
+ SETUP_MUTEX.synchronize do
26
+ return if @setup_done
27
+
28
+ if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
29
+ warn "[CallbackTracer] WARNING: CallbackTracer.setup! called in production. Tracing is disabled in production by default."
30
+ return
31
+ end
32
+
33
+ ActiveRecord::Base.prepend(Tracer)
34
+ @setup_done = true
35
+ end
36
+ end
37
+
38
+ def reset!
39
+ SETUP_MUTEX.synchronize do
40
+ @configuration = Configuration.new
41
+ @setup_done = false
42
+ end
43
+ end
44
+
45
+ def enabled?
46
+ configuration.enabled
47
+ end
48
+
49
+ def buffer
50
+ Thread.current[:callback_tracer_buffer] ||= []
51
+ end
52
+
53
+ def buffer_message(message)
54
+ buffer << message
55
+ end
56
+
57
+ def clear_buffer!
58
+ Thread.current[:callback_tracer_buffer] = []
59
+ end
60
+
61
+ def flush_buffer!
62
+ messages = buffer
63
+ return if messages.empty?
64
+
65
+ logger = configuration.logger
66
+ output_logger = logger || Logger.new($stdout, formatter: proc { |_, _, _, msg| "#{msg}\n" })
67
+ messages.each { |msg| output_logger.info(msg) }
68
+ ensure
69
+ clear_buffer!
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,27 @@
1
+ require "rails/generators"
2
+
3
+ module CallbackTracer
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ desc "Creates a CallbackTracer initializer file"
7
+
8
+ def create_initializer
9
+ create_file "config/initializers/callback_tracer.rb", <<~RUBY
10
+ CallbackTracer.configure do |config|
11
+ # Enable or disable tracing (automatically disabled in production)
12
+ # config.enabled = true
13
+
14
+ # Models to exclude from tracing
15
+ # config.excluded_models = ["ApplicationRecord", "ActiveRecord::SchemaMigration"]
16
+
17
+ # Enable colorized output
18
+ # config.colorize = true
19
+
20
+ # Custom logger (defaults to puts)
21
+ # config.logger = Rails.logger
22
+ end
23
+ RUBY
24
+ end
25
+ end
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: callback_tracer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shehab Mohamed
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '9'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '7.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9'
53
+ - !ruby/object:Gem::Dependency
54
+ name: railties
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '7.0'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '9'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '7.0'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '9'
73
+ description: Instruments all ActiveRecord callbacks and prints their execution order,
74
+ source location, and timing to the terminal during development and test.
75
+ email:
76
+ - shehab.mohamed2104@gmail.com
77
+ executables: []
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - CHANGELOG.md
82
+ - LICENSE.txt
83
+ - README.md
84
+ - lib/callback_tracer.rb
85
+ - lib/callback_tracer/configuration.rb
86
+ - lib/callback_tracer/log_formatter.rb
87
+ - lib/callback_tracer/middleware.rb
88
+ - lib/callback_tracer/railtie.rb
89
+ - lib/callback_tracer/source_locator.rb
90
+ - lib/callback_tracer/tracer.rb
91
+ - lib/callback_tracer/version.rb
92
+ - lib/generators/callback_tracer/install_generator.rb
93
+ homepage: https://github.com/ShehabMohamed21/callback_tracer_gem
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ homepage_uri: https://github.com/ShehabMohamed21/callback_tracer_gem
98
+ source_code_uri: https://github.com/ShehabMohamed21/callback_tracer_gem
99
+ changelog_uri: https://github.com/ShehabMohamed21/callback_tracer_gem/blob/main/CHANGELOG.md
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 3.1.0
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.4.19
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Trace ActiveRecord callback execution order with source locations and timing
119
+ test_files: []