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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1bab13c641c7fb30659e5ed19ef54331355ae7239d722a585e094115b47a0a9
4
- data.tar.gz: ab863ecd3c380117ae77c731b9a917795c0fa4bc89c0a6cbe2f0d780b5597b59
3
+ metadata.gz: 5b8559d5544650453a07a48f7a123270fa34537e4980167b96c5391cd21bd081
4
+ data.tar.gz: 8e87d2da3525e8def1b4fbd7d8f7c8e0c5941cc176c835a0cedd8f9635ead888
5
5
  SHA512:
6
- metadata.gz: a1b4ee5fdbd1a36898a712ce07ceca1016b487d9362a61745116172250eb9cecb905919d1b327f4c2ae4e3a6e0c66cf4c97e53b4f91e8247de658cc9621e5059
7
- data.tar.gz: e033607a2dad279edd380e60ab2633fa940a8d04189bcb2c7b0289e99bf599b2d6ecada08b590b39f137717179d03576f289e6c3e375fda634eab84b36a75878
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: 200px;">
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
- <div class="trace-row">
21
- <span class="trace-id"><%= link_to trace.id, trace_path(trace.id) %></span>
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
- </div>
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
- <h2 style="margin: 0 0 8px; font-size: 16px;"><%= @trace.identifier %></h2>
3
- <p style="margin: 0 0 16px; color: #6b7280; font-size: 12px;">
4
- <%= @trace.id %> · <%= @trace.status %> · <%= @trace.duration_ms ? "#{@trace.duration_ms.round}ms" : 'N/A' %>
5
- <% if @trace.correlation_id.present? %>
6
- · <%= link_to "Follow chain", root_path(correlation_id: @trace.correlation_id) %>
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
- </p>
9
- <div class="timeline"><%= Dontbugme::Formatters::Timeline.format(@trace) %></div>
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
- <p style="margin-top: 16px;">
13
- <%= link_to '← Back to traces', root_path %> ·
14
- <%= link_to 'Compare with another trace', diff_path(a: @trace.id) %>
15
- </p>
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: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-size: 13px; line-height: 1.5; color: #1a1a1a; background: #f8f9fa; margin: 0; padding: 0; }
11
- .container { max-width: 1000px; margin: 0 auto; padding: 24px; }
12
- header { background: #fff; border-bottom: 1px solid #e5e7eb; padding: 16px 24px; margin-bottom: 24px; }
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: #4b5563; text-decoration: none; margin-right: 16px; }
16
- header a:hover { color: #111; }
17
- header a.active { color: #111; font-weight: 600; }
18
- .card { background: #fff; border-radius: 6px; border: 1px solid #e5e7eb; padding: 16px; margin-bottom: 16px; }
19
- .trace-row { display: flex; align-items: center; gap: 16px; padding: 0 0 12px; border-bottom: 1px solid #f3f4f6; }
20
- .trace-row:last-child { border-bottom: none; padding-bottom: 0; }
21
- .trace-row:hover { background: #f9fafb; margin: 0 -16px; padding: 0 16px 12px; }
22
- .trace-id { font-family: monospace; font-size: 12px; color: #6b7280; min-width: 140px; }
23
- .trace-id a { color: #2563eb; text-decoration: none; }
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: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; }
27
- .badge-success { background: #d1fae5; color: #065f46; }
28
- .badge-error { background: #fee2e2; color: #991b1b; }
29
- .trace-duration { color: #6b7280; min-width: 50px; }
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: 8px 12px; border: 1px solid #e5e7eb; border-radius: 4px; font-size: 13px; }
32
- .filters button { padding: 8px 16px; background: #111; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; }
33
- .filters button:hover { background: #333; }
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: 8px 12px; border: 1px solid #e5e7eb; border-radius: 4px; }
39
- .diff-output { white-space: pre-wrap; font-size: 12px; background: #1f2937; color: #e5e7eb; padding: 16px; border-radius: 6px; overflow-x: auto; }
40
- .empty { color: #6b7280; padding: 24px; text-align: center; }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dontbugme
4
- VERSION = '0.1.4'
4
+ VERSION = '0.1.5'
5
5
  end
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: 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
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