dontbugme 0.1.4 → 0.1.5
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 +9 -2
- data/app/helpers/dontbugme/traces_helper.rb +58 -0
- data/app/views/dontbugme/traces/diff.html.erb +1 -1
- data/app/views/dontbugme/traces/index.html.erb +5 -5
- data/app/views/dontbugme/traces/show.html.erb +93 -12
- data/app/views/layouts/dontbugme/application.html.erb +96 -28
- data/lib/dontbugme/configuration.rb +6 -0
- data/lib/dontbugme/subscribers/net_http.rb +4 -0
- data/lib/dontbugme/subscribers/redis.rb +12 -2
- data/lib/dontbugme/version.rb +1 -1
- data/lib/dontbugme.rb +53 -2
- metadata +3 -2
- /data/{lib/dontbugme/config → config}/routes.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b8559d5544650453a07a48f7a123270fa34537e4980167b96c5391cd21bd081
|
|
4
|
+
data.tar.gz: 8e87d2da3525e8def1b4fbd7d8f7c8e0c5941cc176c835a0cedd8f9635ead888
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc02e325d106483e274c53cd77258edc868bbd03241ec20aaa3e4962b5bfeea8f9345bb2e2d5b0d1fc04155e2f6caaa60a540211392f1c7a9a54c6c0ad4864f3
|
|
7
|
+
data.tar.gz: 45642d97da0b241ffd31a3b0585227195b17320e675ac9f2af671f07d38f7af11620ae9e11141a76aa8f4ae220505ddf2d1638b9f112e859ae0351e134a89038
|
data/README.md
CHANGED
|
@@ -114,18 +114,20 @@ puts trace.to_timeline
|
|
|
114
114
|
|
|
115
115
|
### Manual Spans and Snapshots
|
|
116
116
|
|
|
117
|
-
Add custom spans and snapshots within a trace:
|
|
117
|
+
Add custom spans and snapshots within a trace. Spans capture the **return value** by default so you can see outputs in the UI:
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
120
|
trace = Dontbugme.trace("checkout flow") do
|
|
121
121
|
Dontbugme.span("Calculate tax") do
|
|
122
|
-
tax = order.calculate_tax
|
|
122
|
+
tax = order.calculate_tax # output shown in UI
|
|
123
123
|
end
|
|
124
124
|
Dontbugme.snapshot(user: user.attributes.slice("id", "email"), total: order.total)
|
|
125
125
|
Dontbugme.tag(customer_tier: "enterprise")
|
|
126
126
|
end
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
Use `capture_output: false` to skip capturing the return value for sensitive data.
|
|
130
|
+
|
|
129
131
|
### Span Categories
|
|
130
132
|
|
|
131
133
|
Access spans by category for assertions or analysis:
|
|
@@ -185,6 +187,11 @@ Dontbugme.configure do |config|
|
|
|
185
187
|
config.recording_mode = :always
|
|
186
188
|
config.capture_sql_binds = true
|
|
187
189
|
config.source_mode = :full
|
|
190
|
+
|
|
191
|
+
# Capture outputs for debugging (development only)
|
|
192
|
+
config.capture_span_output = true # return values from Dontbugme.span
|
|
193
|
+
config.capture_http_body = true # HTTP response bodies
|
|
194
|
+
config.capture_redis_return_values = true # Redis command return values
|
|
188
195
|
end
|
|
189
196
|
```
|
|
190
197
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module TracesHelper
|
|
5
|
+
def span_category_badge_class(category)
|
|
6
|
+
case category.to_sym
|
|
7
|
+
when :sql then 'badge-sql'
|
|
8
|
+
when :http then 'badge-http'
|
|
9
|
+
when :redis then 'badge-redis'
|
|
10
|
+
when :cache then 'badge-cache'
|
|
11
|
+
when :mailer then 'badge-mailer'
|
|
12
|
+
when :enqueue then 'badge-enqueue'
|
|
13
|
+
when :custom then 'badge-custom'
|
|
14
|
+
when :snapshot then 'badge-snapshot'
|
|
15
|
+
else 'badge-default'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
OUTPUT_KEYS = %w[output result response_body].freeze
|
|
20
|
+
|
|
21
|
+
def span_output(span)
|
|
22
|
+
return nil if span.payload.blank?
|
|
23
|
+
|
|
24
|
+
payload = span.payload.is_a?(Hash) ? span.payload : {}
|
|
25
|
+
key = OUTPUT_KEYS.find { |k| payload[k.to_sym] || payload[k] }
|
|
26
|
+
key ? (payload[key.to_sym] || payload[key]) : nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def format_span_payload(span)
|
|
30
|
+
return [] if span.payload.blank?
|
|
31
|
+
|
|
32
|
+
payload = span.payload.is_a?(Hash) ? span.payload : {}
|
|
33
|
+
payload.map do |key, value|
|
|
34
|
+
next if value.nil?
|
|
35
|
+
next if OUTPUT_KEYS.include?(key.to_s)
|
|
36
|
+
|
|
37
|
+
display_value = case value
|
|
38
|
+
when Array then value.map { |v| v.is_a?(String) ? v : v.inspect }.join(', ')
|
|
39
|
+
when Hash then value.inspect
|
|
40
|
+
else value.to_s
|
|
41
|
+
end
|
|
42
|
+
display_value = "#{display_value[0, 500]}..." if display_value.length > 500
|
|
43
|
+
[key.to_s.tr('_', ' ').capitalize, display_value]
|
|
44
|
+
end.compact
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def format_span_detail_for_display(span)
|
|
48
|
+
span.detail
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def truncate_detail(str, max_len = 80)
|
|
52
|
+
return '' if str.blank?
|
|
53
|
+
str = str.to_s.strip
|
|
54
|
+
return str if str.length <= max_len
|
|
55
|
+
"#{str[0, max_len - 3]}..."
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
<p class="empty">Enter two trace IDs above to see what changed between two executions.</p>
|
|
16
16
|
<% end %>
|
|
17
17
|
|
|
18
|
-
<p style="margin-top: 16px;"><%= link_to '← Back to traces', root_path %></p>
|
|
18
|
+
<p style="margin-top: 16px;"><%= link_to '← Back to traces', root_path, class: 'btn btn-secondary', style: 'display: inline-block;' %></p>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%= form_with url: root_path, method: :get, local: true, class: 'filters' do |f| %>
|
|
2
|
-
<input type="text" name="q" value="<%= params[:q] %>" placeholder="Search identifier..." style="min-width:
|
|
2
|
+
<input type="text" name="q" value="<%= params[:q] %>" placeholder="Search identifier..." style="min-width: 220px;">
|
|
3
3
|
<select name="status">
|
|
4
4
|
<option value="">All statuses</option>
|
|
5
5
|
<option value="success" <%= params[:status] == 'success' ? 'selected' : '' %>>Success</option>
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
<button type="submit">Filter</button>
|
|
15
15
|
<% end %>
|
|
16
16
|
|
|
17
|
-
<div class="card">
|
|
17
|
+
<div class="card traces-list">
|
|
18
18
|
<% if @traces.any? %>
|
|
19
19
|
<% @traces.each do |trace| %>
|
|
20
|
-
|
|
21
|
-
<span class="trace-id"><%=
|
|
20
|
+
<%= link_to trace_path(trace.id), class: 'trace-row trace-row-link' do %>
|
|
21
|
+
<span class="trace-id"><%= trace.id %></span>
|
|
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
|
-
|
|
25
|
+
<% end %>
|
|
26
26
|
<% end %>
|
|
27
27
|
<% else %>
|
|
28
28
|
<p class="empty">No traces found. Run your app and execute some jobs or requests.</p>
|
|
@@ -1,15 +1,96 @@
|
|
|
1
|
-
<div class="card">
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<div class="trace-header card">
|
|
2
|
+
<div class="trace-header-top">
|
|
3
|
+
<h2 class="trace-title"><%= @trace.identifier %></h2>
|
|
4
|
+
<div class="trace-meta">
|
|
5
|
+
<span class="badge <%= @trace.status == :success ? 'badge-success' : 'badge-error' %>"><%= @trace.status %></span>
|
|
6
|
+
<span class="trace-meta-item"><%= @trace.duration_ms ? "#{@trace.duration_ms.round}ms" : 'N/A' %></span>
|
|
7
|
+
<span class="trace-meta-item"><%= @trace.id %></span>
|
|
8
|
+
<% if @trace.correlation_id.present? %>
|
|
9
|
+
<%= link_to "Follow chain", root_path(correlation_id: @trace.correlation_id), class: 'trace-link' %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="trace-actions">
|
|
14
|
+
<%= link_to '← Back to traces', root_path, class: 'btn btn-secondary' %>
|
|
15
|
+
<%= link_to 'Compare with another', diff_path(a: @trace.id), class: 'btn btn-secondary' %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<% if params[:only].present? %>
|
|
20
|
+
<p class="filter-hint">Filtering by: <strong><%= params[:only] %></strong> · <%= link_to 'Show all', trace_path(@trace.id) %></p>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<div class="timeline-viz">
|
|
24
|
+
<div class="timeline-bar" style="--total-ms: <%= [@trace.duration_ms || 1, 1].max %>;">
|
|
25
|
+
<% @trace.raw_spans.each do |span| %>
|
|
26
|
+
<% next if params[:only].present? && span.category.to_s != params[:only] %>
|
|
27
|
+
<% width = span.duration_ms.to_f > 0 ? [(span.duration_ms / (@trace.duration_ms || 1) * 100).round(1), 2].max : 2 %>
|
|
28
|
+
<span class="timeline-segment badge-<%= span.category %>" style="width: <%= width %>%;" title="<%= span.category %> <%= span.duration_ms.round(1) %>ms"></span>
|
|
7
29
|
<% end %>
|
|
8
|
-
</
|
|
9
|
-
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="spans-list">
|
|
34
|
+
<% spans_to_show = params[:only].present? ? @trace.raw_spans.select { |s| s.category.to_s == params[:only] } : @trace.raw_spans %>
|
|
35
|
+
<% spans_to_show.each_with_index do |span, idx| %>
|
|
36
|
+
<details class="span-card card" data-category="<%= span.category %>">
|
|
37
|
+
<summary class="span-summary">
|
|
38
|
+
<span class="span-offset"><%= span.started_at.to_f.round(1) %>ms</span>
|
|
39
|
+
<span class="span-duration"><%= span.duration_ms ? "#{span.duration_ms.round(1)}ms" : '-' %></span>
|
|
40
|
+
<span class="badge <%= span_category_badge_class(span.category) %>"><%= span.category.to_s.upcase %></span>
|
|
41
|
+
<span class="span-operation"><%= span.operation %></span>
|
|
42
|
+
<span class="span-detail-preview"><%= truncate_detail(span.detail, 80) %></span>
|
|
43
|
+
</summary>
|
|
44
|
+
<div class="span-details">
|
|
45
|
+
<% if span_output(span).present? %>
|
|
46
|
+
<div class="span-section span-output-section">
|
|
47
|
+
<h4>Output <button type="button" class="copy-btn" data-copy-target="output-<%= idx %>" title="Copy">Copy</button></h4>
|
|
48
|
+
<pre class="code-block output-block" id="output-<%= idx %>"><code><%= h(span_output(span)) %></code></pre>
|
|
49
|
+
</div>
|
|
50
|
+
<% end %>
|
|
51
|
+
<div class="span-section">
|
|
52
|
+
<h4>Detail <button type="button" class="copy-btn" data-copy-target="detail-<%= idx %>" title="Copy">Copy</button></h4>
|
|
53
|
+
<pre class="code-block" id="detail-<%= idx %>"><code><%= h(format_span_detail_for_display(span)) %></code></pre>
|
|
54
|
+
</div>
|
|
55
|
+
<% if format_span_payload(span).any? %>
|
|
56
|
+
<div class="span-section">
|
|
57
|
+
<h4>Inputs</h4>
|
|
58
|
+
<dl class="payload-list">
|
|
59
|
+
<% format_span_payload(span).each do |key, value| %>
|
|
60
|
+
<div class="payload-row">
|
|
61
|
+
<dt><%= key %></dt>
|
|
62
|
+
<dd><pre class="payload-value"><%= h(value) %></pre></dd>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
65
|
+
</dl>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% if span.source.present? %>
|
|
69
|
+
<div class="span-section">
|
|
70
|
+
<h4>Source</h4>
|
|
71
|
+
<code class="source-location"><%= h(span.source) %></code>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
</details>
|
|
76
|
+
<% end %>
|
|
10
77
|
</div>
|
|
11
78
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
79
|
+
<% if @trace.truncated_spans_count.to_i.positive? %>
|
|
80
|
+
<p class="truncated-notice">+ <%= @trace.truncated_spans_count %> additional spans truncated</p>
|
|
81
|
+
<% end %>
|
|
82
|
+
|
|
83
|
+
<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>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<% if @trace.error.present? %>
|
|
88
|
+
<div class="card error-card">
|
|
89
|
+
<h3>Error</h3>
|
|
90
|
+
<pre class="code-block error-detail"><%= h(@trace.error[:message] || @trace.error['message']) %></pre>
|
|
91
|
+
<% bt = @trace.error[:backtrace] || @trace.error['backtrace'] %>
|
|
92
|
+
<% if bt.present? %>
|
|
93
|
+
<pre class="code-block backtrace"><%= Array(bt).map { |l| h(l) }.join("\n") %></pre>
|
|
94
|
+
<% end %>
|
|
95
|
+
</div>
|
|
96
|
+
<% end %>
|
|
@@ -7,38 +7,106 @@
|
|
|
7
7
|
<title>Dontbugme</title>
|
|
8
8
|
<style>
|
|
9
9
|
* { box-sizing: border-box; }
|
|
10
|
-
body { font-family:
|
|
11
|
-
.container { max-width:
|
|
12
|
-
header { background: #
|
|
13
|
-
header h1 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
10
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; line-height: 1.5; color: #1a1a1a; background: #0f172a; margin: 0; padding: 0; }
|
|
11
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
|
12
|
+
header { background: #1e293b; border-bottom: 1px solid #334155; padding: 16px 24px; margin-bottom: 24px; }
|
|
13
|
+
header h1 { margin: 0; font-size: 18px; font-weight: 600; color: #f8fafc; }
|
|
14
14
|
header nav { margin-top: 12px; }
|
|
15
|
-
header a { color: #
|
|
16
|
-
header a:hover { color: #
|
|
17
|
-
header a.active { color: #
|
|
18
|
-
.card { background: #
|
|
19
|
-
.trace-row { display: flex; align-items: center; gap: 16px; padding:
|
|
20
|
-
.trace-row:
|
|
21
|
-
.trace-row
|
|
22
|
-
.trace-id { font-family: monospace; font-size: 12px; color: #
|
|
23
|
-
.trace-id a { color: #
|
|
15
|
+
header a { color: #94a3b8; text-decoration: none; margin-right: 16px; }
|
|
16
|
+
header a:hover { color: #f8fafc; }
|
|
17
|
+
header a.active { color: #38bdf8; font-weight: 600; }
|
|
18
|
+
.card { background: #1e293b; border-radius: 8px; border: 1px solid #334155; padding: 20px; margin-bottom: 16px; }
|
|
19
|
+
.trace-row { display: flex; align-items: center; gap: 16px; padding: 12px 16px; border-radius: 6px; transition: background 0.15s; }
|
|
20
|
+
.trace-row:hover { background: #334155; }
|
|
21
|
+
.trace-row-link { text-decoration: none; color: inherit; display: flex; }
|
|
22
|
+
.trace-id { font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 12px; color: #64748b; min-width: 140px; }
|
|
23
|
+
.trace-id a { color: #38bdf8; text-decoration: none; }
|
|
24
24
|
.trace-id a:hover { text-decoration: underline; }
|
|
25
|
-
.trace-identifier { flex: 1; }
|
|
26
|
-
.badge { display: inline-block; padding:
|
|
27
|
-
.badge-success { background: #
|
|
28
|
-
.badge-error { background: #
|
|
29
|
-
.
|
|
25
|
+
.trace-identifier { flex: 1; color: #e2e8f0; }
|
|
26
|
+
.badge { display: inline-block; padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; }
|
|
27
|
+
.badge-success { background: #064e3b; color: #34d399; }
|
|
28
|
+
.badge-error { background: #7f1d1d; color: #f87171; }
|
|
29
|
+
.badge-sql { background: #1e3a5f; color: #7dd3fc; }
|
|
30
|
+
.badge-http { background: #422006; color: #fbbf24; }
|
|
31
|
+
.badge-redis { background: #4c1d95; color: #c4b5fd; }
|
|
32
|
+
.badge-cache { background: #14532d; color: #86efac; }
|
|
33
|
+
.badge-mailer { background: #701a75; color: #f0abfc; }
|
|
34
|
+
.badge-enqueue { background: #1e293b; color: #94a3b8; }
|
|
35
|
+
.badge-custom { background: #0f172a; color: #cbd5e1; }
|
|
36
|
+
.badge-snapshot { background: #312e81; color: #a5b4fc; }
|
|
37
|
+
.badge-default { background: #334155; color: #94a3b8; }
|
|
38
|
+
.trace-duration { color: #64748b; min-width: 60px; font-family: monospace; }
|
|
30
39
|
.filters { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
31
|
-
.filters input, .filters select { padding:
|
|
32
|
-
.filters button { padding:
|
|
33
|
-
.filters button:hover { background: #
|
|
34
|
-
.timeline { font-size: 12px; white-space: pre-wrap; font-family: ui-monospace, monospace; }
|
|
35
|
-
.span-line { padding: 4px 0; }
|
|
36
|
-
.span-source { color: #6b7280; padding-left: 12px; font-size: 11px; }
|
|
40
|
+
.filters input, .filters select { padding: 10px 14px; border: 1px solid #334155; border-radius: 6px; font-size: 13px; background: #1e293b; color: #e2e8f0; }
|
|
41
|
+
.filters button { padding: 10px 20px; background: #38bdf8; color: #0f172a; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; }
|
|
42
|
+
.filters button:hover { background: #7dd3fc; }
|
|
37
43
|
.diff-form { display: flex; gap: 12px; align-items: center; margin-bottom: 24px; }
|
|
38
|
-
.diff-form input { flex: 1; padding:
|
|
39
|
-
.diff-output { white-space: pre-wrap; font-size: 12px; background: #
|
|
40
|
-
.empty { color: #
|
|
44
|
+
.diff-form input { flex: 1; padding: 10px 14px; border: 1px solid #334155; border-radius: 6px; background: #1e293b; color: #e2e8f0; }
|
|
45
|
+
.diff-output { white-space: pre-wrap; font-size: 12px; background: #0f172a; color: #e2e8f0; padding: 20px; border-radius: 8px; overflow-x: auto; font-family: monospace; }
|
|
46
|
+
.empty { color: #64748b; padding: 24px; text-align: center; }
|
|
47
|
+
.trace-header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-start; gap: 16px; }
|
|
48
|
+
.trace-title { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #f8fafc; }
|
|
49
|
+
.trace-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
50
|
+
.trace-meta-item { color: #64748b; font-size: 13px; }
|
|
51
|
+
.trace-link { color: #38bdf8; text-decoration: none; }
|
|
52
|
+
.trace-link:hover { text-decoration: underline; }
|
|
53
|
+
.trace-actions { display: flex; gap: 8px; }
|
|
54
|
+
.btn { padding: 8px 16px; border-radius: 6px; font-size: 13px; text-decoration: none; font-weight: 500; }
|
|
55
|
+
.btn-secondary { background: #334155; color: #e2e8f0; }
|
|
56
|
+
.btn-secondary:hover { background: #475569; }
|
|
57
|
+
.timeline-viz { margin-bottom: 24px; }
|
|
58
|
+
.timeline-bar { display: flex; height: 24px; border-radius: 6px; overflow: hidden; background: #334155; }
|
|
59
|
+
.timeline-segment { min-width: 2px; transition: opacity 0.2s; }
|
|
60
|
+
.timeline-segment:hover { opacity: 0.8; }
|
|
61
|
+
.spans-list { display: flex; flex-direction: column; gap: 8px; }
|
|
62
|
+
.span-card { padding: 0; overflow: hidden; }
|
|
63
|
+
.span-card summary { padding: 14px 20px; cursor: pointer; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; list-style: none; }
|
|
64
|
+
.span-card summary::-webkit-details-marker { display: none; }
|
|
65
|
+
.span-card summary::after { content: '▶'; color: #64748b; font-size: 10px; margin-left: auto; transition: transform 0.2s; }
|
|
66
|
+
.span-card[open] summary::after { transform: rotate(90deg); }
|
|
67
|
+
.span-card summary:hover { background: #334155; }
|
|
68
|
+
.span-offset { font-family: monospace; font-size: 12px; color: #64748b; min-width: 50px; }
|
|
69
|
+
.span-duration { font-family: monospace; font-size: 12px; color: #38bdf8; min-width: 55px; }
|
|
70
|
+
.span-operation { color: #94a3b8; font-size: 12px; }
|
|
71
|
+
.span-detail-preview { color: #cbd5e1; font-size: 13px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
72
|
+
.span-details { padding: 0 20px 20px; border-top: 1px solid #334155; }
|
|
73
|
+
.span-section { margin-top: 16px; }
|
|
74
|
+
.span-section h4 { margin: 0 0 8px; font-size: 11px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
75
|
+
.code-block { background: #0f172a; color: #e2e8f0; padding: 16px; border-radius: 6px; overflow-x: auto; font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 12px; line-height: 1.6; margin: 0; white-space: pre-wrap; word-break: break-all; }
|
|
76
|
+
.payload-list { margin: 0; }
|
|
77
|
+
.payload-row { display: grid; grid-template-columns: 140px 1fr; gap: 12px; margin-bottom: 12px; align-items: start; }
|
|
78
|
+
.payload-row dt { margin: 0; color: #64748b; font-size: 12px; font-weight: 500; }
|
|
79
|
+
.payload-value { margin: 0; padding: 8px 12px; background: #0f172a; border-radius: 4px; font-size: 12px; color: #e2e8f0; overflow-x: auto; white-space: pre-wrap; font-family: monospace; }
|
|
80
|
+
.source-location { display: block; padding: 8px 12px; background: #0f172a; border-radius: 4px; font-size: 11px; color: #94a3b8; }
|
|
81
|
+
.filter-hint { color: #94a3b8; margin-bottom: 16px; font-size: 13px; }
|
|
82
|
+
.filter-hint a { color: #38bdf8; }
|
|
83
|
+
.trace-footer { margin-top: 24px; }
|
|
84
|
+
.trace-footer p { color: #64748b; font-size: 13px; }
|
|
85
|
+
.trace-footer a { color: #38bdf8; }
|
|
86
|
+
.truncated-notice { color: #64748b; font-size: 13px; margin-top: 8px; }
|
|
87
|
+
.error-card { border-color: #7f1d1d; }
|
|
88
|
+
.error-card h3 { color: #f87171; margin: 0 0 12px; }
|
|
89
|
+
.error-detail { color: #f87171; }
|
|
90
|
+
.backtrace { font-size: 11px; color: #94a3b8; }
|
|
91
|
+
.span-section h4 { display: flex; align-items: center; gap: 8px; }
|
|
92
|
+
.span-output-section .output-block { border-left: 3px solid #38bdf8; }
|
|
93
|
+
.copy-btn { background: #334155; color: #94a3b8; border: none; padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
|
|
94
|
+
.copy-btn:hover { background: #475569; color: #e2e8f0; }
|
|
41
95
|
</style>
|
|
96
|
+
<script>
|
|
97
|
+
document.querySelectorAll('.copy-btn').forEach(function(btn) {
|
|
98
|
+
btn.addEventListener('click', function() {
|
|
99
|
+
var target = document.getElementById(this.dataset.copyTarget);
|
|
100
|
+
if (!target) return;
|
|
101
|
+
var text = target.textContent;
|
|
102
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
103
|
+
var orig = btn.textContent;
|
|
104
|
+
btn.textContent = 'Copied!';
|
|
105
|
+
setTimeout(function() { btn.textContent = orig; }, 1500);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
</script>
|
|
42
110
|
</head>
|
|
43
111
|
<body>
|
|
44
112
|
<header>
|
|
@@ -50,7 +118,7 @@
|
|
|
50
118
|
</nav>
|
|
51
119
|
</div>
|
|
52
120
|
</header>
|
|
53
|
-
<main class="container">
|
|
121
|
+
<main class="container" style="color: #e2e8f0;">
|
|
54
122
|
<%= yield %>
|
|
55
123
|
</main>
|
|
56
124
|
</body>
|
|
@@ -16,6 +16,8 @@ module Dontbugme
|
|
|
16
16
|
:capture_http_headers,
|
|
17
17
|
:capture_http_body,
|
|
18
18
|
:capture_redis_values,
|
|
19
|
+
:capture_redis_return_values,
|
|
20
|
+
:capture_span_output,
|
|
19
21
|
:source_mode,
|
|
20
22
|
:source_filter,
|
|
21
23
|
:source_depth,
|
|
@@ -63,6 +65,8 @@ module Dontbugme
|
|
|
63
65
|
self.capture_http_headers = []
|
|
64
66
|
self.capture_http_body = false
|
|
65
67
|
self.capture_redis_values = false
|
|
68
|
+
self.capture_redis_return_values = true
|
|
69
|
+
self.capture_span_output = true
|
|
66
70
|
self.source_mode = :full
|
|
67
71
|
self.source_filter = %w[app/ lib/]
|
|
68
72
|
self.source_depth = 3
|
|
@@ -95,6 +99,8 @@ module Dontbugme
|
|
|
95
99
|
self.recording_mode = :selective
|
|
96
100
|
self.record_on_error = true
|
|
97
101
|
self.capture_sql_binds = false
|
|
102
|
+
self.capture_redis_return_values = false
|
|
103
|
+
self.capture_span_output = false
|
|
98
104
|
self.source_mode = :shallow
|
|
99
105
|
self.source_depth = 1
|
|
100
106
|
self.source_stack_limit = 30
|
|
@@ -41,6 +41,10 @@ module Dontbugme
|
|
|
41
41
|
}
|
|
42
42
|
payload[:error] = error.message if error
|
|
43
43
|
|
|
44
|
+
if config.capture_http_body && response&.body
|
|
45
|
+
payload[:response_body] = truncate(response.body, config.max_http_body_size)
|
|
46
|
+
end
|
|
47
|
+
|
|
44
48
|
if config.capture_http_headers&.any?
|
|
45
49
|
payload[:request_headers] = capture_headers(req, config.capture_http_headers)
|
|
46
50
|
end
|
|
@@ -20,7 +20,7 @@ module Dontbugme
|
|
|
20
20
|
result = super
|
|
21
21
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round(2)
|
|
22
22
|
|
|
23
|
-
record_span(command, start_wall, duration_ms)
|
|
23
|
+
record_span(command, start_wall, duration_ms, result: result)
|
|
24
24
|
result
|
|
25
25
|
rescue StandardError => e
|
|
26
26
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round(2)
|
|
@@ -30,7 +30,7 @@ module Dontbugme
|
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
|
-
def record_span(command, start_wall, duration_ms, error: nil)
|
|
33
|
+
def record_span(command, start_wall, duration_ms, result: nil, error: nil)
|
|
34
34
|
cmd = Array(command).map(&:to_s)
|
|
35
35
|
operation = cmd.first&.upcase || 'UNKNOWN'
|
|
36
36
|
detail = cmd.join(' ')
|
|
@@ -40,6 +40,9 @@ module Dontbugme
|
|
|
40
40
|
if config.capture_redis_values && cmd.size > 1
|
|
41
41
|
payload[:args] = cmd[1..].map { |a| truncate(a, config.max_redis_value_size) }
|
|
42
42
|
end
|
|
43
|
+
if config.capture_redis_return_values && result && !error
|
|
44
|
+
payload[:output] = truncate_value(result, config.max_redis_value_size)
|
|
45
|
+
end
|
|
43
46
|
payload[:error] = error.message if error
|
|
44
47
|
|
|
45
48
|
Dontbugme::Recorder.add_span(
|
|
@@ -57,6 +60,13 @@ module Dontbugme
|
|
|
57
60
|
|
|
58
61
|
"#{str.to_s.byteslice(0, max)}[truncated]"
|
|
59
62
|
end
|
|
63
|
+
|
|
64
|
+
def truncate_value(val, max)
|
|
65
|
+
str = val.to_s
|
|
66
|
+
return str if str.bytesize <= max
|
|
67
|
+
|
|
68
|
+
"#{str.byteslice(0, max)}[truncated]"
|
|
69
|
+
end
|
|
60
70
|
end
|
|
61
71
|
end
|
|
62
72
|
end
|
data/lib/dontbugme/version.rb
CHANGED
data/lib/dontbugme.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Dontbugme
|
|
|
24
24
|
Recorder.record(kind: :custom, identifier: identifier, metadata: metadata, return_trace: true, &block)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def span(name, payload: {}, &block)
|
|
27
|
+
def span(name, payload: {}, capture_output: true, &block)
|
|
28
28
|
return yield unless Context.active?
|
|
29
29
|
|
|
30
30
|
start_mono = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
@@ -32,11 +32,16 @@ module Dontbugme
|
|
|
32
32
|
result = yield
|
|
33
33
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_mono).round(2)
|
|
34
34
|
|
|
35
|
+
span_payload = payload.dup
|
|
36
|
+
if capture_output && config.capture_span_output
|
|
37
|
+
span_payload[:output] = format_output_value(result)
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
Recorder.add_span(
|
|
36
41
|
category: :custom,
|
|
37
42
|
operation: 'span',
|
|
38
43
|
detail: name.to_s,
|
|
39
|
-
payload:
|
|
44
|
+
payload: span_payload,
|
|
40
45
|
duration_ms: duration_ms,
|
|
41
46
|
started_at: start_wall
|
|
42
47
|
)
|
|
@@ -65,6 +70,52 @@ module Dontbugme
|
|
|
65
70
|
|
|
66
71
|
private
|
|
67
72
|
|
|
73
|
+
def format_output_value(val)
|
|
74
|
+
return nil if val.nil?
|
|
75
|
+
|
|
76
|
+
max = config.max_span_detail_size
|
|
77
|
+
str = if val.is_a?(Array)
|
|
78
|
+
format_array(val)
|
|
79
|
+
elsif val.is_a?(Hash)
|
|
80
|
+
format_hash(val)
|
|
81
|
+
elsif defined?(ActiveRecord::Base) && val.is_a?(ActiveRecord::Base)
|
|
82
|
+
val.inspect
|
|
83
|
+
elsif defined?(ActiveRecord::Relation) && val.is_a?(ActiveRecord::Relation)
|
|
84
|
+
"#{val.to_sql} (relation)"
|
|
85
|
+
else
|
|
86
|
+
val.to_s
|
|
87
|
+
end
|
|
88
|
+
str.bytesize > max ? "#{str.byteslice(0, max)}...[truncated]" : str
|
|
89
|
+
rescue StandardError
|
|
90
|
+
val.class.name
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def format_array(ary)
|
|
94
|
+
return '[]' if ary.empty?
|
|
95
|
+
|
|
96
|
+
preview = ary.first(5).map { |v| format_single_value(v) }.join(', ')
|
|
97
|
+
ary.size > 5 ? "[#{preview}, ... (#{ary.size} total)]" : "[#{preview}]"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def format_hash(hash)
|
|
101
|
+
return '{}' if hash.empty?
|
|
102
|
+
|
|
103
|
+
preview = hash.first(5).map { |k, v| "#{k}: #{format_single_value(v)}" }.join(', ')
|
|
104
|
+
hash.size > 5 ? "{#{preview}, ...}" : "{#{preview}}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def format_single_value(v)
|
|
108
|
+
if defined?(ActiveRecord::Base) && v.is_a?(ActiveRecord::Base)
|
|
109
|
+
v.respond_to?(:id) ? "#<#{v.class.name} id=#{v.id}>" : "#<#{v.class.name}>"
|
|
110
|
+
elsif v.is_a?(Hash)
|
|
111
|
+
'{...}'
|
|
112
|
+
elsif v.is_a?(Array)
|
|
113
|
+
'[...]'
|
|
114
|
+
else
|
|
115
|
+
v.to_s
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
68
119
|
def build_store
|
|
69
120
|
store = case config.store
|
|
70
121
|
when :sqlite
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Inspector Contributors
|
|
@@ -122,15 +122,16 @@ files:
|
|
|
122
122
|
- LICENSE
|
|
123
123
|
- README.md
|
|
124
124
|
- app/controllers/dontbugme/traces_controller.rb
|
|
125
|
+
- app/helpers/dontbugme/traces_helper.rb
|
|
125
126
|
- app/views/dontbugme/traces/diff.html.erb
|
|
126
127
|
- app/views/dontbugme/traces/index.html.erb
|
|
127
128
|
- app/views/dontbugme/traces/show.html.erb
|
|
128
129
|
- app/views/layouts/dontbugme/application.html.erb
|
|
129
130
|
- bin/dontbugme
|
|
131
|
+
- config/routes.rb
|
|
130
132
|
- lib/dontbugme.rb
|
|
131
133
|
- lib/dontbugme/cleanup_job.rb
|
|
132
134
|
- lib/dontbugme/cli.rb
|
|
133
|
-
- lib/dontbugme/config/routes.rb
|
|
134
135
|
- lib/dontbugme/configuration.rb
|
|
135
136
|
- lib/dontbugme/context.rb
|
|
136
137
|
- lib/dontbugme/correlation.rb
|
|
File without changes
|