dontbugme 0.1.5 → 0.1.7
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 +4 -4
- data/README.md +20 -0
- data/app/helpers/dontbugme/traces_helper.rb +38 -0
- data/app/views/dontbugme/traces/index.html.erb +1 -0
- data/app/views/dontbugme/traces/show.html.erb +9 -1
- data/app/views/layouts/dontbugme/application.html.erb +3 -0
- data/lib/dontbugme/configuration.rb +4 -0
- data/lib/dontbugme/middleware/rack.rb +4 -1
- data/lib/dontbugme/railtie.rb +1 -0
- data/lib/dontbugme/recorder.rb +1 -0
- data/lib/dontbugme/store/json_safe.rb +41 -0
- data/lib/dontbugme/store/postgresql.rb +4 -4
- data/lib/dontbugme/store/sqlite.rb +17 -4
- data/lib/dontbugme/variable_tracker.rb +140 -0
- data/lib/dontbugme/version.rb +1 -1
- data/lib/dontbugme.rb +12 -0
- data/lib/generators/dontbugme/install/templates/dontbugme.rb +3 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9433798398dd8fdefaec811b7f52b310fcbb57febcc03a44fc2eaf94acd4ab2c
|
|
4
|
+
data.tar.gz: a513ab24f0d08c91ffa941dbbc81b326a6c240052fbac9b1c8b4a0111a0d4257
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10dd8c9e2263610d647a66d92eabb0d56760f524e684b7f9c9984465b398f48391d43caac9ff7caba3684a5d3e22bf0b3fa1f14fa65f2afba45ddc7ec7114360
|
|
7
|
+
data.tar.gz: 9dc17d45129c997152ac4d842b337a813a3ed184dea61598ecb496cbd9a732c391e6c2424a32e94dfdae221eafff208c1899560f27f326982477907ed9726c85
|
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 = {
|
data/lib/dontbugme/railtie.rb
CHANGED
data/lib/dontbugme/recorder.rb
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Store
|
|
5
|
+
# Sanitizes data for JSON encoding by replacing invalid UTF-8 sequences.
|
|
6
|
+
# Trace data can contain binary from SQL binds, HTTP bodies, Redis, etc.
|
|
7
|
+
module JsonSafe
|
|
8
|
+
REPLACEMENT = "\uFFFD".freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def sanitize(obj)
|
|
13
|
+
case obj
|
|
14
|
+
when String
|
|
15
|
+
sanitize_string(obj)
|
|
16
|
+
when Hash
|
|
17
|
+
obj.transform_values { |v| sanitize(v) }
|
|
18
|
+
when Array
|
|
19
|
+
obj.map { |v| sanitize(v) }
|
|
20
|
+
when Symbol
|
|
21
|
+
sanitize_string(obj.to_s)
|
|
22
|
+
when Numeric, TrueClass, FalseClass, NilClass
|
|
23
|
+
obj
|
|
24
|
+
when Time
|
|
25
|
+
obj.respond_to?(:iso8601) ? obj.iso8601(3) : obj.to_s
|
|
26
|
+
else
|
|
27
|
+
sanitize_string(obj.to_s)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sanitize_string(str)
|
|
32
|
+
return str if str.nil?
|
|
33
|
+
return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
|
|
34
|
+
|
|
35
|
+
str.encode('UTF-8', invalid: :replace, undef: :replace, replace: REPLACEMENT)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
REPLACEMENT
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -17,14 +17,14 @@ module Dontbugme
|
|
|
17
17
|
params = [
|
|
18
18
|
data[:id],
|
|
19
19
|
data[:kind].to_s,
|
|
20
|
-
data[:identifier],
|
|
20
|
+
JsonSafe.sanitize_string(data[:identifier].to_s),
|
|
21
21
|
data[:status].to_s,
|
|
22
22
|
data[:started_at],
|
|
23
23
|
data[:duration_ms],
|
|
24
24
|
correlation_id,
|
|
25
|
-
data[:metadata].to_json,
|
|
26
|
-
data[:spans].to_json,
|
|
27
|
-
data[:error]
|
|
25
|
+
JsonSafe.sanitize(data[:metadata]).to_json,
|
|
26
|
+
JsonSafe.sanitize(data[:spans]).to_json,
|
|
27
|
+
data[:error] ? JsonSafe.sanitize(data[:error]).to_json : nil
|
|
28
28
|
]
|
|
29
29
|
exec_params(<<~SQL, params)
|
|
30
30
|
INSERT INTO dontbugme_traces
|
|
@@ -15,18 +15,21 @@ module Dontbugme
|
|
|
15
15
|
def save_trace(trace)
|
|
16
16
|
data = trace.to_h
|
|
17
17
|
correlation_id = data[:correlation_id] || data[:metadata]&.dig(:correlation_id)
|
|
18
|
+
metadata_json = json_safe(data[:metadata])
|
|
19
|
+
spans_json = json_safe(data[:spans])
|
|
20
|
+
error_json = data[:error] ? json_safe(data[:error]) : nil
|
|
18
21
|
db.execute(
|
|
19
22
|
'INSERT OR REPLACE INTO traces (id, kind, identifier, status, started_at, duration_ms, correlation_id, metadata_json, spans_json, error_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
20
23
|
data[:id],
|
|
21
24
|
data[:kind].to_s,
|
|
22
|
-
data[:identifier],
|
|
25
|
+
sanitize_identifier(data[:identifier]),
|
|
23
26
|
data[:status].to_s,
|
|
24
27
|
data[:started_at],
|
|
25
28
|
data[:duration_ms],
|
|
26
29
|
correlation_id,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
metadata_json,
|
|
31
|
+
spans_json,
|
|
32
|
+
error_json
|
|
30
33
|
)
|
|
31
34
|
end
|
|
32
35
|
|
|
@@ -108,6 +111,16 @@ module Dontbugme
|
|
|
108
111
|
db.execute('CREATE INDEX IF NOT EXISTS idx_traces_correlation_id ON traces(correlation_id)')
|
|
109
112
|
end
|
|
110
113
|
|
|
114
|
+
def json_safe(obj)
|
|
115
|
+
JsonSafe.sanitize(obj).to_json
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def sanitize_identifier(str)
|
|
119
|
+
return str if str.nil?
|
|
120
|
+
|
|
121
|
+
JsonSafe.sanitize_string(str.to_s)
|
|
122
|
+
end
|
|
123
|
+
|
|
111
124
|
def migrate_add_correlation_id
|
|
112
125
|
return if db.execute("PRAGMA table_info(traces)").any? { |col| col[1] == 'correlation_id' }
|
|
113
126
|
|
|
@@ -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
|
data/lib/dontbugme/version.rb
CHANGED
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'
|
|
@@ -149,6 +160,7 @@ require 'dontbugme/subscribers/cache'
|
|
|
149
160
|
require 'dontbugme/subscribers/action_mailer'
|
|
150
161
|
require 'dontbugme/subscribers/active_job'
|
|
151
162
|
require 'dontbugme/store/base'
|
|
163
|
+
require 'dontbugme/store/json_safe'
|
|
152
164
|
require 'dontbugme/store/memory'
|
|
153
165
|
require 'dontbugme/store/sqlite'
|
|
154
166
|
require 'dontbugme/store/postgresql'
|
|
@@ -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.
|
|
4
|
+
version: 0.1.7
|
|
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-
|
|
11
|
+
date: 2026-03-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -149,6 +149,7 @@ files:
|
|
|
149
149
|
- lib/dontbugme/span_collection.rb
|
|
150
150
|
- lib/dontbugme/store/async.rb
|
|
151
151
|
- lib/dontbugme/store/base.rb
|
|
152
|
+
- lib/dontbugme/store/json_safe.rb
|
|
152
153
|
- lib/dontbugme/store/memory.rb
|
|
153
154
|
- lib/dontbugme/store/postgresql.rb
|
|
154
155
|
- lib/dontbugme/store/sqlite.rb
|
|
@@ -160,6 +161,7 @@ files:
|
|
|
160
161
|
- lib/dontbugme/subscribers/net_http.rb
|
|
161
162
|
- lib/dontbugme/subscribers/redis.rb
|
|
162
163
|
- lib/dontbugme/trace.rb
|
|
164
|
+
- lib/dontbugme/variable_tracker.rb
|
|
163
165
|
- lib/dontbugme/version.rb
|
|
164
166
|
- lib/generators/dontbugme/install/install_generator.rb
|
|
165
167
|
- lib/generators/dontbugme/install/templates/dontbugme.rb
|