dontbugme 0.1.5 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b8559d5544650453a07a48f7a123270fa34537e4980167b96c5391cd21bd081
4
- data.tar.gz: 8e87d2da3525e8def1b4fbd7d8f7c8e0c5941cc176c835a0cedd8f9635ead888
3
+ metadata.gz: 6f71bd0ef418ea0d47ca94a5242d3677a00d82cf36e2fd0d8b69f3f4ce323f9c
4
+ data.tar.gz: c9692d06855f24fc322e218588958881a0a8d9ee4b278125961143b7527fdf3a
5
5
  SHA512:
6
- metadata.gz: cc02e325d106483e274c53cd77258edc868bbd03241ec20aaa3e4962b5bfeea8f9345bb2e2d5b0d1fc04155e2f6caaa60a540211392f1c7a9a54c6c0ad4864f3
7
- data.tar.gz: 45642d97da0b241ffd31a3b0585227195b17320e675ac9f2af671f07d38f7af11620ae9e11141a76aa8f4ae220505ddf2d1638b9f112e859ae0351e134a89038
6
+ metadata.gz: 85e2c35aa316839754b10c22353ef1b89970c893f78f537708fce9a5af7653c4532219a98bd035c833bfdc9e3f3d4fbd3c61a6ad8fb9f60a6d8bff2f1a301511
7
+ data.tar.gz: 646af28b63c0f177fcc68b48c0892f3a92c0b1dd102aad33b70d2c54915b85fa325bdcf5b6661c4e2b8c5b13af735d10a6b088643df9335f8967ab8569e918b9
data/README.md CHANGED
@@ -128,6 +128,26 @@ end
128
128
 
129
129
  Use `capture_output: false` to skip capturing the return value for sensitive data.
130
130
 
131
+ ### Automatic Variable Tracking
132
+
133
+ Dontbugme automatically captures local variable changes between lines in your app code. No manual instrumentation needed — when a variable like `token` changes from `abc123` to `abc124`, you'll see an observe span with Input and Output in the UI.
134
+
135
+ ```ruby
136
+ token = Member.find(1).confirmation_token
137
+ token += 1
138
+ Member.find(1).update(confirmation_token: token)
139
+ ```
140
+
141
+ The UI will show both **Input** and **Output** for the transformation. Enabled by default in development; disable with `config.capture_variable_changes = false`. Only tracks simple types (String, Integer, Float, etc.) to avoid noise.
142
+
143
+ ### Manual Observe (optional)
144
+
145
+ For explicit control, use `Dontbugme.observe`:
146
+
147
+ ```ruby
148
+ token = Dontbugme.observe('token increment', token) { token + 1 }
149
+ ```
150
+
131
151
  ### Span Categories
132
152
 
133
153
  Access spans by category for assertions or analysis:
@@ -17,6 +17,15 @@ module Dontbugme
17
17
  end
18
18
 
19
19
  OUTPUT_KEYS = %w[output result response_body].freeze
20
+ DEDICATED_INPUT_KEYS = %w[input].freeze
21
+
22
+ def span_input(span)
23
+ return nil if span.payload.blank?
24
+
25
+ payload = span.payload.is_a?(Hash) ? span.payload : {}
26
+ key = DEDICATED_INPUT_KEYS.find { |k| payload[k.to_sym] || payload[k] }
27
+ key ? (payload[key.to_sym] || payload[key]) : nil
28
+ end
20
29
 
21
30
  def span_output(span)
22
31
  return nil if span.payload.blank?
@@ -33,6 +42,7 @@ module Dontbugme
33
42
  payload.map do |key, value|
34
43
  next if value.nil?
35
44
  next if OUTPUT_KEYS.include?(key.to_s)
45
+ next if DEDICATED_INPUT_KEYS.include?(key.to_s)
36
46
 
37
47
  display_value = case value
38
48
  when Array then value.map { |v| v.is_a?(String) ? v : v.inspect }.join(', ')
@@ -54,5 +64,33 @@ module Dontbugme
54
64
  return str if str.length <= max_len
55
65
  "#{str[0, max_len - 3]}..."
56
66
  end
67
+
68
+ def trace_started_at_formatted(trace)
69
+ return nil unless trace.respond_to?(:started_at_utc) && trace.started_at_utc
70
+
71
+ trace.started_at_utc.respond_to?(:strftime) ? trace.started_at_utc.strftime('%Y-%m-%d %H:%M:%S.%3N UTC') : trace.started_at_utc.to_s
72
+ end
73
+
74
+ def trace_started_at_short(trace)
75
+ return nil unless trace.respond_to?(:started_at_utc) && trace.started_at_utc
76
+
77
+ trace.started_at_utc.respond_to?(:strftime) ? trace.started_at_utc.strftime('%Y-%m-%d %H:%M:%S') : trace.started_at_utc.to_s
78
+ end
79
+
80
+ def trace_finished_at_formatted(trace)
81
+ return nil unless trace.respond_to?(:duration_ms) && trace.duration_ms
82
+
83
+ finished = trace.started_at_utc + (trace.duration_ms / 1000.0)
84
+ finished.respond_to?(:strftime) ? finished.strftime('%Y-%m-%d %H:%M:%S.%3N UTC') : finished.to_s
85
+ end
86
+
87
+ def span_timestamp_formatted(trace, span)
88
+ return nil unless trace.respond_to?(:started_at_utc) && trace.started_at_utc
89
+ return nil unless span.respond_to?(:started_at) && span.started_at
90
+
91
+ offset_sec = (span.started_at.to_f / 1000.0)
92
+ at = trace.started_at_utc + offset_sec
93
+ at.respond_to?(:strftime) ? at.strftime('%H:%M:%S.%3N') : at.to_s
94
+ end
57
95
  end
58
96
  end
@@ -22,6 +22,7 @@
22
22
  <span class="trace-identifier"><%= trace.identifier %></span>
23
23
  <span class="badge <%= trace.status == :success ? 'badge-success' : 'badge-error' %>"><%= trace.status %></span>
24
24
  <span class="trace-duration"><%= trace.duration_ms ? "#{trace.duration_ms.round}ms" : '-' %></span>
25
+ <span class="trace-timestamp" title="<%= trace_started_at_formatted(trace) %>"><%= trace_started_at_short(trace) %></span>
25
26
  <% end %>
26
27
  <% end %>
27
28
  <% else %>
@@ -4,6 +4,7 @@
4
4
  <div class="trace-meta">
5
5
  <span class="badge <%= @trace.status == :success ? 'badge-success' : 'badge-error' %>"><%= @trace.status %></span>
6
6
  <span class="trace-meta-item"><%= @trace.duration_ms ? "#{@trace.duration_ms.round}ms" : 'N/A' %></span>
7
+ <span class="trace-meta-item trace-timestamp" title="<%= trace_started_at_formatted(@trace) %>"><%= trace_started_at_formatted(@trace) %></span>
7
8
  <span class="trace-meta-item"><%= @trace.id %></span>
8
9
  <% if @trace.correlation_id.present? %>
9
10
  <%= link_to "Follow chain", root_path(correlation_id: @trace.correlation_id), class: 'trace-link' %>
@@ -35,6 +36,7 @@
35
36
  <% spans_to_show.each_with_index do |span, idx| %>
36
37
  <details class="span-card card" data-category="<%= span.category %>">
37
38
  <summary class="span-summary">
39
+ <span class="span-timestamp" title="<%= trace_started_at_formatted(@trace) %>"><%= span_timestamp_formatted(@trace, span) %></span>
38
40
  <span class="span-offset"><%= span.started_at.to_f.round(1) %>ms</span>
39
41
  <span class="span-duration"><%= span.duration_ms ? "#{span.duration_ms.round(1)}ms" : '-' %></span>
40
42
  <span class="badge <%= span_category_badge_class(span.category) %>"><%= span.category.to_s.upcase %></span>
@@ -42,6 +44,12 @@
42
44
  <span class="span-detail-preview"><%= truncate_detail(span.detail, 80) %></span>
43
45
  </summary>
44
46
  <div class="span-details">
47
+ <% if span_input(span).present? %>
48
+ <div class="span-section span-input-section">
49
+ <h4>Input <button type="button" class="copy-btn" data-copy-target="input-<%= idx %>" title="Copy">Copy</button></h4>
50
+ <pre class="code-block input-block" id="input-<%= idx %>"><code><%= h(span_input(span)) %></code></pre>
51
+ </div>
52
+ <% end %>
45
53
  <% if span_output(span).present? %>
46
54
  <div class="span-section span-output-section">
47
55
  <h4>Output <button type="button" class="copy-btn" data-copy-target="output-<%= idx %>" title="Copy">Copy</button></h4>
@@ -81,7 +89,7 @@
81
89
  <% end %>
82
90
 
83
91
  <div class="trace-footer">
84
- <p>Filter by category: <%= link_to 'All', trace_path(@trace.id) %> · <%= link_to 'SQL', trace_path(@trace.id, only: 'sql') %> · <%= link_to 'HTTP', trace_path(@trace.id, only: 'http') %> · <%= link_to 'Redis', trace_path(@trace.id, only: 'redis') %> · <%= link_to 'Cache', trace_path(@trace.id, only: 'cache') %> · <%= link_to 'Mailer', trace_path(@trace.id, only: 'mailer') %> · <%= link_to 'Enqueue', trace_path(@trace.id, only: 'enqueue') %></p>
92
+ <p>Filter by category: <%= link_to 'All', trace_path(@trace.id) %> · <%= link_to 'SQL', trace_path(@trace.id, only: 'sql') %> · <%= link_to 'HTTP', trace_path(@trace.id, only: 'http') %> · <%= link_to 'Redis', trace_path(@trace.id, only: 'redis') %> · <%= link_to 'Cache', trace_path(@trace.id, only: 'cache') %> · <%= link_to 'Mailer', trace_path(@trace.id, only: 'mailer') %> · <%= link_to 'Enqueue', trace_path(@trace.id, only: 'enqueue') %> · <%= link_to 'Custom', trace_path(@trace.id, only: 'custom') %> · <%= link_to 'Snapshot', trace_path(@trace.id, only: 'snapshot') %></p>
85
93
  </div>
86
94
 
87
95
  <% if @trace.error.present? %>
@@ -36,6 +36,8 @@
36
36
  .badge-snapshot { background: #312e81; color: #a5b4fc; }
37
37
  .badge-default { background: #334155; color: #94a3b8; }
38
38
  .trace-duration { color: #64748b; min-width: 60px; font-family: monospace; }
39
+ .trace-timestamp { font-family: monospace; font-size: 12px; color: #64748b; }
40
+ .span-timestamp { font-family: monospace; font-size: 11px; color: #64748b; min-width: 90px; }
39
41
  .filters { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
40
42
  .filters input, .filters select { padding: 10px 14px; border: 1px solid #334155; border-radius: 6px; font-size: 13px; background: #1e293b; color: #e2e8f0; }
41
43
  .filters button { padding: 10px 20px; background: #38bdf8; color: #0f172a; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; }
@@ -90,6 +92,7 @@
90
92
  .backtrace { font-size: 11px; color: #94a3b8; }
91
93
  .span-section h4 { display: flex; align-items: center; gap: 8px; }
92
94
  .span-output-section .output-block { border-left: 3px solid #38bdf8; }
95
+ .span-input-section .input-block { border-left: 3px solid #94a3b8; }
93
96
  .copy-btn { background: #334155; color: #94a3b8; border: none; padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
94
97
  .copy-btn:hover { background: #475569; color: #e2e8f0; }
95
98
  </style>
@@ -18,6 +18,7 @@ module Dontbugme
18
18
  :capture_redis_values,
19
19
  :capture_redis_return_values,
20
20
  :capture_span_output,
21
+ :capture_variable_changes,
21
22
  :source_mode,
22
23
  :source_filter,
23
24
  :source_depth,
@@ -67,6 +68,7 @@ module Dontbugme
67
68
  self.capture_redis_values = false
68
69
  self.capture_redis_return_values = true
69
70
  self.capture_span_output = true
71
+ self.capture_variable_changes = true
70
72
  self.source_mode = :full
71
73
  self.source_filter = %w[app/ lib/]
72
74
  self.source_depth = 3
@@ -89,6 +91,7 @@ module Dontbugme
89
91
  self.recording_mode = :off
90
92
  self.enable_web_ui = false
91
93
  self.record_on_error = false
94
+ self.capture_variable_changes = false
92
95
  self.max_trace_buffer_bytes = 5 * 1024 * 1024 # 5 MB
93
96
  end
94
97
 
@@ -101,6 +104,7 @@ module Dontbugme
101
104
  self.capture_sql_binds = false
102
105
  self.capture_redis_return_values = false
103
106
  self.capture_span_output = false
107
+ self.capture_variable_changes = false
104
108
  self.source_mode = :shallow
105
109
  self.source_depth = 1
106
110
  self.source_stack_limit = 30
@@ -12,12 +12,15 @@ module Dontbugme
12
12
  return @app.call(env) unless Dontbugme.config.should_record_request?(env)
13
13
 
14
14
  request = ::Rack::Request.new(env)
15
+ path = request.path.to_s
16
+ mount_path = Dontbugme.config.web_ui_mount_path.to_s.chomp('/')
17
+ return @app.call(env) if mount_path.to_s != '' && (path == mount_path || path.start_with?("#{mount_path}/"))
18
+
15
19
  request_id = env['action_dispatch.request_id'] || request.get_header('HTTP_X_REQUEST_ID') || SecureRandom.uuid
16
20
  correlation_id = env['HTTP_X_CORRELATION_ID'] || Correlation.generate
17
21
  Correlation.current = correlation_id
18
22
 
19
23
  method = request.request_method
20
- path = request.path
21
24
  identifier = "#{method} #{path}"
22
25
 
23
26
  metadata = {
@@ -23,6 +23,7 @@ module Dontbugme
23
23
  Dontbugme::Subscribers::Cache.subscribe
24
24
  Dontbugme::Subscribers::ActionMailer.subscribe
25
25
  Dontbugme::Subscribers::ActiveJob.subscribe
26
+ Dontbugme::VariableTracker.subscribe
26
27
  end
27
28
 
28
29
  config.after_initialize do
@@ -13,6 +13,7 @@ module Dontbugme
13
13
 
14
14
  trace = Trace.new(kind: kind, identifier: identifier, metadata: metadata)
15
15
  Context.current = trace
16
+ VariableTracker.clear_state! if defined?(VariableTracker)
16
17
 
17
18
  result = yield
18
19
  trace.finish!
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ # Automatically captures local variable changes between lines using TracePoint.
5
+ # Emits observe-style spans when variables change, so you can inspect value
6
+ # transformations without manual Dontbugme.observe calls.
7
+ class VariableTracker
8
+ THREAD_KEY = :dontbugme_variable_tracker_state
9
+ THREAD_PATH_KEY = :dontbugme_variable_tracker_path
10
+ IN_TRACKER_KEY = :dontbugme_variable_tracker_in_callback
11
+ SKIP_VARS = %w[_ result trace e ex].freeze
12
+ TRACKABLE_CLASSES = [String, Integer, Float, Symbol, TrueClass, FalseClass, NilClass].freeze
13
+
14
+ class << self
15
+ def subscribe
16
+ return if @subscribed
17
+
18
+ @trace_point = TracePoint.new(:line) { |tp| handle_line(tp) }
19
+ @trace_point.enable
20
+ @subscribed = true
21
+ end
22
+
23
+ def unsubscribe
24
+ return unless @subscribed
25
+
26
+ @trace_point&.disable
27
+ @trace_point = nil
28
+ @subscribed = false
29
+ end
30
+
31
+ def handle_line(tp)
32
+ return if Thread.current[IN_TRACKER_KEY]
33
+ return unless Dontbugme.config.recording?
34
+ return unless Dontbugme.config.capture_variable_changes
35
+ return unless Context.active?
36
+
37
+ path = tp.path.to_s
38
+ return if path.include?('dontbugme') || path.include?('/gems/') || path.include?('bundler')
39
+ return unless Dontbugme.config.source_filter.any? { |f| path.include?(f) }
40
+
41
+ binding = tp.binding
42
+ return unless binding
43
+
44
+ Thread.current[IN_TRACKER_KEY] = true
45
+ begin
46
+ current = extract_locals(binding)
47
+ prev = Thread.current[THREAD_KEY]
48
+ prev_path = Thread.current[THREAD_PATH_KEY]
49
+ # Only diff when we're in the same file (avoid cross-scope false positives)
50
+ if prev && prev_path == path
51
+ diff_and_emit(prev, current, tp)
52
+ end
53
+ Thread.current[THREAD_KEY] = current
54
+ Thread.current[THREAD_PATH_KEY] = path
55
+ ensure
56
+ Thread.current[IN_TRACKER_KEY] = false
57
+ end
58
+ end
59
+
60
+ def clear_state!
61
+ Thread.current[THREAD_KEY] = nil
62
+ Thread.current[THREAD_PATH_KEY] = nil
63
+ end
64
+
65
+ private
66
+
67
+ def extract_locals(binding)
68
+ return {} unless binding.respond_to?(:local_variables)
69
+
70
+ binding.local_variables.each_with_object({}) do |name, h|
71
+ next if SKIP_VARS.include?(name.to_s)
72
+ next if name.to_s.start_with?('_')
73
+
74
+ begin
75
+ h[name] = binding.local_variable_get(name)
76
+ rescue StandardError
77
+ # Some vars (e.g. from C extensions) may not be readable
78
+ end
79
+ end
80
+ end
81
+
82
+ def diff_and_emit(prev, current, tp)
83
+ changed = current.select do |name, new_val|
84
+ prev_val = prev[name]
85
+ !values_equal?(prev_val, new_val)
86
+ end
87
+ return if changed.empty?
88
+
89
+ changed.each do |name, new_val|
90
+ prev_val = prev[name]
91
+ emit_observe(name, prev_val, new_val, tp)
92
+ end
93
+ end
94
+
95
+ def values_equal?(a, b)
96
+ return true if a.equal?(b)
97
+ return a == b if a.nil? || b.nil?
98
+
99
+ a == b
100
+ rescue StandardError
101
+ false
102
+ end
103
+
104
+ def emit_observe(name, input, output, tp)
105
+ return unless (input.nil? || trackable_value?(input)) && (output.nil? || trackable_value?(output))
106
+
107
+ detail = "#{name} changed"
108
+ payload = {
109
+ input: format_value(input),
110
+ output: format_value(output)
111
+ }
112
+ Recorder.add_span(
113
+ category: :custom,
114
+ operation: 'observe',
115
+ detail: detail,
116
+ payload: payload,
117
+ duration_ms: 0,
118
+ started_at: Time.now
119
+ )
120
+ end
121
+
122
+ def format_value(val)
123
+ return nil if val.nil?
124
+
125
+ Dontbugme.send(:format_output_value, val)
126
+ rescue StandardError
127
+ val.class.name
128
+ end
129
+
130
+ def trackable_value?(val)
131
+ return true if val.nil?
132
+ return true if TRACKABLE_CLASSES.any? { |c| val.is_a?(c) }
133
+ return true if val.is_a?(Array) && val.size <= 10 && val.all? { |v| trackable_value?(v) }
134
+ return true if val.is_a?(Hash) && val.size <= 10 && val.values.all? { |v| trackable_value?(v) }
135
+
136
+ false
137
+ end
138
+ end
139
+ end
140
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dontbugme
4
- VERSION = '0.1.5'
4
+ VERSION = '0.1.6'
5
5
  end
data/lib/dontbugme.rb CHANGED
@@ -48,6 +48,16 @@ module Dontbugme
48
48
  result
49
49
  end
50
50
 
51
+ # Captures input and output of in-house calculations for value transformation inspection.
52
+ # Example: token = Dontbugme.observe('token increment', token) { token + 1 }
53
+ def observe(name, input = nil, &block)
54
+ return yield unless Context.active?
55
+
56
+ payload = {}
57
+ payload[:input] = format_output_value(input) unless input.nil?
58
+ span(name, payload: payload, capture_output: true, &block)
59
+ end
60
+
51
61
  def snapshot(data)
52
62
  return unless Context.active?
53
63
 
@@ -136,6 +146,7 @@ end
136
146
  require 'dontbugme/version'
137
147
  require 'dontbugme/configuration'
138
148
  require 'dontbugme/span'
149
+ require 'dontbugme/variable_tracker'
139
150
  require 'dontbugme/span_collection'
140
151
  require 'dontbugme/trace'
141
152
  require 'dontbugme/context'
@@ -8,6 +8,9 @@ Dontbugme.configure do |config|
8
8
  # config.enable_web_ui = true
9
9
  # config.web_ui_mount_path = "/inspector"
10
10
 
11
+ # Automatic variable tracking (dev only): captures input/output for local var changes
12
+ # config.capture_variable_changes = true
13
+
11
14
  # Production: use PostgreSQL, async writes, selective recording
12
15
  # config.store = :postgresql
13
16
  # config.async_store = true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dontbugme
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Inspector Contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-28 00:00:00.000000000 Z
11
+ date: 2026-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -160,6 +160,7 @@ files:
160
160
  - lib/dontbugme/subscribers/net_http.rb
161
161
  - lib/dontbugme/subscribers/redis.rb
162
162
  - lib/dontbugme/trace.rb
163
+ - lib/dontbugme/variable_tracker.rb
163
164
  - lib/dontbugme/version.rb
164
165
  - lib/generators/dontbugme/install/install_generator.rb
165
166
  - lib/generators/dontbugme/install/templates/dontbugme.rb