dontbugme 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +174 -0
  4. data/app/controllers/dontbugme/traces_controller.rb +45 -0
  5. data/app/views/dontbugme/traces/diff.html.erb +18 -0
  6. data/app/views/dontbugme/traces/index.html.erb +30 -0
  7. data/app/views/dontbugme/traces/show.html.erb +15 -0
  8. data/app/views/layouts/dontbugme/application.html.erb +56 -0
  9. data/bin/dontbugme +5 -0
  10. data/lib/dontbugme/cleanup_job.rb +17 -0
  11. data/lib/dontbugme/cli.rb +171 -0
  12. data/lib/dontbugme/config/routes.rb +7 -0
  13. data/lib/dontbugme/configuration.rb +147 -0
  14. data/lib/dontbugme/context.rb +25 -0
  15. data/lib/dontbugme/correlation.rb +25 -0
  16. data/lib/dontbugme/engine.rb +11 -0
  17. data/lib/dontbugme/formatters/diff.rb +187 -0
  18. data/lib/dontbugme/formatters/json.rb +11 -0
  19. data/lib/dontbugme/formatters/timeline.rb +119 -0
  20. data/lib/dontbugme/middleware/rack.rb +37 -0
  21. data/lib/dontbugme/middleware/sidekiq.rb +31 -0
  22. data/lib/dontbugme/middleware/sidekiq_client.rb +14 -0
  23. data/lib/dontbugme/railtie.rb +47 -0
  24. data/lib/dontbugme/recorder.rb +70 -0
  25. data/lib/dontbugme/source_location.rb +44 -0
  26. data/lib/dontbugme/span.rb +70 -0
  27. data/lib/dontbugme/span_collection.rb +40 -0
  28. data/lib/dontbugme/store/async.rb +45 -0
  29. data/lib/dontbugme/store/base.rb +23 -0
  30. data/lib/dontbugme/store/memory.rb +61 -0
  31. data/lib/dontbugme/store/postgresql.rb +186 -0
  32. data/lib/dontbugme/store/sqlite.rb +148 -0
  33. data/lib/dontbugme/subscribers/action_mailer.rb +53 -0
  34. data/lib/dontbugme/subscribers/active_job.rb +44 -0
  35. data/lib/dontbugme/subscribers/active_record.rb +81 -0
  36. data/lib/dontbugme/subscribers/base.rb +19 -0
  37. data/lib/dontbugme/subscribers/cache.rb +54 -0
  38. data/lib/dontbugme/subscribers/net_http.rb +87 -0
  39. data/lib/dontbugme/subscribers/redis.rb +63 -0
  40. data/lib/dontbugme/trace.rb +142 -0
  41. data/lib/dontbugme/version.rb +5 -0
  42. data/lib/dontbugme.rb +118 -0
  43. data/lib/generators/dontbugme/install/install_generator.rb +17 -0
  44. data/lib/generators/dontbugme/install/templates/dontbugme.rb +17 -0
  45. metadata +164 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fb0afce7d9aa5e4a8f372a7b7b3b2e24ecfd3e5c7cb7e9c05afbb3dee2deb635
4
+ data.tar.gz: 462510e9da9e18d236a65a46e9c25a0b4c8ada876d059e5209b05c63d041bddb
5
+ SHA512:
6
+ metadata.gz: '09be1265d9bd33cf45291cb52c7085e2c3f00f95de5f6a7e54f5dcd22522ecf9264413824aaa7d9c4b4eceef8a5edce099f294dfee0d94bc27661bb025e6c070'
7
+ data.tar.gz: e8690b9f9d2937f18751ba50903130e143dc6e4148808ca2f10dba299cf72ad28bbf021e4e9c0d1d247d51894a9c7692067da9165c69c14fea660f9e511aeac7
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # Dontbugme
2
+
3
+ A flight recorder for Rails applications. Reconstruct the full execution story of Sidekiq jobs and HTTP requests — see exactly what database queries ran, what HTTP services were called, what exceptions were raised, with source locations pointing to your code.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'dontbugme'
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Automatic Recording
22
+
23
+ Dontbugme automatically records:
24
+
25
+ - **Sidekiq jobs** — via Sidekiq server middleware
26
+ - **HTTP requests** — via Rack middleware
27
+ - **SQL queries** — via ActiveSupport::Notifications
28
+ - **HTTP calls** — Net::HTTP (including Faraday, which uses it)
29
+ - **Redis** — Redis gem operations
30
+ - **Cache** — Rails cache read/write/delete
31
+ - **Mailer** — ActionMailer deliveries
32
+ - **Job enqueue** — Active Job enqueues
33
+
34
+ In development, recording is on by default. Run your app normally, then inspect:
35
+
36
+ ```bash
37
+ # List recent traces
38
+ bundle exec dontbugme list
39
+
40
+ # Show a specific trace
41
+ bundle exec dontbugme show tr_abc123
42
+
43
+ # Filter spans
44
+ bundle exec dontbugme show tr_abc123 --only=sql
45
+ bundle exec dontbugme show tr_abc123 --slow=10
46
+
47
+ # JSON output
48
+ bundle exec dontbugme show tr_abc123 --json
49
+ ```
50
+
51
+ ### Manual Tracing
52
+
53
+ Wrap any block to capture a trace:
54
+
55
+ ```ruby
56
+ trace = Dontbugme.trace("my debug session") do
57
+ User.find(42)
58
+ Order.where(user_id: 42).count
59
+ end
60
+
61
+ puts trace.to_timeline
62
+ # Or: trace.spans, trace.status, trace.duration_ms
63
+ ```
64
+
65
+ ### Manual Spans and Snapshots
66
+
67
+ Add custom spans and snapshots within a trace:
68
+
69
+ ```ruby
70
+ trace = Dontbugme.trace("checkout flow") do
71
+ Dontbugme.span("Calculate tax") do
72
+ tax = order.calculate_tax
73
+ end
74
+ Dontbugme.snapshot(user: user.attributes.slice("id", "email"), total: order.total)
75
+ Dontbugme.tag(customer_tier: "enterprise")
76
+ end
77
+ ```
78
+
79
+ ### Span Categories
80
+
81
+ Access spans by category for assertions or analysis:
82
+
83
+ ```ruby
84
+ trace.spans.sql # SQL queries
85
+ trace.spans.http # HTTP calls
86
+ trace.spans.redis # Redis operations
87
+ trace.spans.category(:mailer) # Any category
88
+ ```
89
+
90
+ ### Search
91
+
92
+ ```bash
93
+ bundle exec dontbugme search --status=error --class=SendInvoiceJob --limit=10
94
+ ```
95
+
96
+ ### Trace Diff
97
+
98
+ Compare two executions to see what changed:
99
+
100
+ ```bash
101
+ bundle exec dontbugme diff tr_success tr_failed
102
+ ```
103
+
104
+ Shows IDENTICAL, CHANGED, MISSING, and NEW spans between the two traces.
105
+
106
+ ### Correlation Chain
107
+
108
+ When a request enqueues jobs, they share a `correlation_id`. Follow the full chain:
109
+
110
+ ```bash
111
+ bundle exec dontbugme trace tr_request_id --follow
112
+ ```
113
+
114
+ Shows all traces (request + enqueued jobs) with the same correlation ID. Correlation IDs are automatically propagated from HTTP requests to Sidekiq jobs (and from job to child job) when using the Rails integration.
115
+
116
+ ### Web UI (optional)
117
+
118
+ A lightweight web interface to browse traces. **Disabled by default in production.** Enable in development:
119
+
120
+ ```ruby
121
+ # config/initializers/dontbugme.rb
122
+ Dontbugme.configure do |config|
123
+ config.enable_web_ui = true # default: true in dev, false in prod
124
+ config.web_ui_mount_path = '/inspector'
125
+ end
126
+ ```
127
+
128
+ Add to `config/routes.rb`:
129
+
130
+ ```ruby
131
+ mount Dontbugme::Engine, at: '/inspector' if Dontbugme.config.enable_web_ui
132
+ ```
133
+
134
+ Then visit `/inspector` to browse traces, search, and compare.
135
+
136
+ ### Configuration
137
+
138
+ Create `config/initializers/dontbugme.rb`:
139
+
140
+ ```ruby
141
+ Dontbugme.configure do |config|
142
+ config.store = :sqlite
143
+ config.sqlite_path = "tmp/inspector/inspector.db"
144
+ config.recording_mode = :always
145
+ config.capture_sql_binds = true
146
+ config.source_mode = :full
147
+ end
148
+ ```
149
+
150
+ ## Storage
151
+
152
+ - **SQLite** (default): Zero config. Data at `tmp/inspector/inspector.db`
153
+ - **PostgreSQL**: Uses your Rails DB. Set `config.store = :postgresql`
154
+ - **Memory**: For tests. Traces lost on process exit.
155
+
156
+ ## Cleanup
157
+
158
+ Traces are ephemeral. Run cleanup to enforce retention:
159
+
160
+ ```ruby
161
+ Dontbugme::CleanupJob.perform
162
+ ```
163
+
164
+ Schedule via cron or Sidekiq to run periodically.
165
+
166
+ ## Requirements
167
+
168
+ - Ruby >= 3.0
169
+ - Rails >= 7.0 (for full integration)
170
+ - Sidekiq >= 7.0 (optional, for job tracing)
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ class TracesController < ActionController::Base
5
+ layout 'dontbugme/application'
6
+
7
+ before_action :ensure_enabled
8
+
9
+ def index
10
+ filters = { limit: 50 }
11
+ filters[:status] = params[:status] if params[:status].present?
12
+ filters[:kind] = params[:kind] if params[:kind].present?
13
+ filters[:identifier] = params[:q] if params[:q].present?
14
+ filters[:correlation_id] = params[:correlation_id] if params[:correlation_id].present?
15
+ @traces = store.search(filters)
16
+ end
17
+
18
+ def show
19
+ @trace = store.find_trace(params[:id])
20
+ return render plain: 'Trace not found', status: :not_found unless @trace
21
+ end
22
+
23
+ def diff
24
+ @trace_a = params[:a].present? ? store.find_trace(params[:a]) : nil
25
+ @trace_b = params[:b].present? ? store.find_trace(params[:b]) : nil
26
+ @diff_output = if @trace_a && @trace_b
27
+ Formatters::Diff.format(@trace_a, @trace_b)
28
+ else
29
+ nil
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def store
36
+ Dontbugme.store
37
+ end
38
+
39
+ def ensure_enabled
40
+ return if Dontbugme.config.enable_web_ui
41
+
42
+ render plain: 'Dontbugme Web UI is disabled. Set config.enable_web_ui = true to enable.', status: :forbidden
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ <%= form_with url: diff_path, method: :get, local: true, class: 'diff-form' do %>
2
+ <input type="text" name="a" value="<%= params[:a] %>" placeholder="Trace A ID (e.g. tr_abc123)">
3
+ <span>vs</span>
4
+ <input type="text" name="b" value="<%= params[:b] %>" placeholder="Trace B ID (e.g. tr_def456)">
5
+ <button type="submit">Compare</button>
6
+ <% end %>
7
+
8
+ <% if @diff_output %>
9
+ <div class="card">
10
+ <div class="diff-output"><%= @diff_output %></div>
11
+ </div>
12
+ <% elsif params[:a].present? || params[:b].present? %>
13
+ <p class="empty">Enter two trace IDs to compare. Use the trace list to find IDs.</p>
14
+ <% else %>
15
+ <p class="empty">Enter two trace IDs above to see what changed between two executions.</p>
16
+ <% end %>
17
+
18
+ <p style="margin-top: 16px;"><%= link_to '← Back to traces', root_path %></p>
@@ -0,0 +1,30 @@
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;">
3
+ <select name="status">
4
+ <option value="">All statuses</option>
5
+ <option value="success" <%= params[:status] == 'success' ? 'selected' : '' %>>Success</option>
6
+ <option value="error" <%= params[:status] == 'error' ? 'selected' : '' %>>Error</option>
7
+ </select>
8
+ <select name="kind">
9
+ <option value="">All kinds</option>
10
+ <option value="sidekiq" <%= params[:kind] == 'sidekiq' ? 'selected' : '' %>>Sidekiq</option>
11
+ <option value="request" <%= params[:kind] == 'request' ? 'selected' : '' %>>Request</option>
12
+ <option value="custom" <%= params[:kind] == 'custom' ? 'selected' : '' %>>Custom</option>
13
+ </select>
14
+ <button type="submit">Filter</button>
15
+ <% end %>
16
+
17
+ <div class="card">
18
+ <% if @traces.any? %>
19
+ <% @traces.each do |trace| %>
20
+ <div class="trace-row">
21
+ <span class="trace-id"><%= link_to trace.id, trace_path(trace.id) %></span>
22
+ <span class="trace-identifier"><%= trace.identifier %></span>
23
+ <span class="badge <%= trace.status == :success ? 'badge-success' : 'badge-error' %>"><%= trace.status %></span>
24
+ <span class="trace-duration"><%= trace.duration_ms ? "#{trace.duration_ms.round}ms" : '-' %></span>
25
+ </div>
26
+ <% end %>
27
+ <% else %>
28
+ <p class="empty">No traces found. Run your app and execute some jobs or requests.</p>
29
+ <% end %>
30
+ </div>
@@ -0,0 +1,15 @@
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) %>
7
+ <% end %>
8
+ </p>
9
+ <div class="timeline"><%= Formatters::Timeline.format(@trace) %></div>
10
+ </div>
11
+
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>
@@ -0,0 +1,56 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Dontbugme</title>
7
+ <style>
8
+ * { box-sizing: border-box; }
9
+ 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; }
10
+ .container { max-width: 1000px; margin: 0 auto; padding: 24px; }
11
+ header { background: #fff; border-bottom: 1px solid #e5e7eb; padding: 16px 24px; margin-bottom: 24px; }
12
+ header h1 { margin: 0; font-size: 18px; font-weight: 600; }
13
+ header nav { margin-top: 12px; }
14
+ header a { color: #4b5563; text-decoration: none; margin-right: 16px; }
15
+ header a:hover { color: #111; }
16
+ header a.active { color: #111; font-weight: 600; }
17
+ .card { background: #fff; border-radius: 6px; border: 1px solid #e5e7eb; padding: 16px; margin-bottom: 16px; }
18
+ .trace-row { display: flex; align-items: center; gap: 16px; padding: 0 0 12px; border-bottom: 1px solid #f3f4f6; }
19
+ .trace-row:last-child { border-bottom: none; padding-bottom: 0; }
20
+ .trace-row:hover { background: #f9fafb; margin: 0 -16px; padding: 0 16px 12px; }
21
+ .trace-id { font-family: monospace; font-size: 12px; color: #6b7280; min-width: 140px; }
22
+ .trace-id a { color: #2563eb; text-decoration: none; }
23
+ .trace-id a:hover { text-decoration: underline; }
24
+ .trace-identifier { flex: 1; }
25
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; }
26
+ .badge-success { background: #d1fae5; color: #065f46; }
27
+ .badge-error { background: #fee2e2; color: #991b1b; }
28
+ .trace-duration { color: #6b7280; min-width: 50px; }
29
+ .filters { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
30
+ .filters input, .filters select { padding: 8px 12px; border: 1px solid #e5e7eb; border-radius: 4px; font-size: 13px; }
31
+ .filters button { padding: 8px 16px; background: #111; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; }
32
+ .filters button:hover { background: #333; }
33
+ .timeline { font-size: 12px; white-space: pre-wrap; font-family: ui-monospace, monospace; }
34
+ .span-line { padding: 4px 0; }
35
+ .span-source { color: #6b7280; padding-left: 12px; font-size: 11px; }
36
+ .diff-form { display: flex; gap: 12px; align-items: center; margin-bottom: 24px; }
37
+ .diff-form input { flex: 1; padding: 8px 12px; border: 1px solid #e5e7eb; border-radius: 4px; }
38
+ .diff-output { white-space: pre-wrap; font-size: 12px; background: #1f2937; color: #e5e7eb; padding: 16px; border-radius: 6px; overflow-x: auto; }
39
+ .empty { color: #6b7280; padding: 24px; text-align: center; }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <header>
44
+ <div class="container">
45
+ <h1>Dontbugme</h1>
46
+ <nav>
47
+ <%= link_to 'Traces', root_path, class: (controller_name == 'traces' && action_name == 'index' ? 'active' : '') %>
48
+ <%= link_to 'Diff', diff_path, class: (action_name == 'diff' ? 'active' : '') %>
49
+ </nav>
50
+ </div>
51
+ </header>
52
+ <main class="container">
53
+ <%= yield %>
54
+ </main>
55
+ </body>
56
+ </html>
data/bin/dontbugme ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'dontbugme'
5
+ Dontbugme::CLI.start
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ class CleanupJob
5
+ def self.perform
6
+ store = Dontbugme.store
7
+ return unless store
8
+
9
+ config = Dontbugme.config
10
+ retention = config.retention
11
+ return unless retention
12
+
13
+ cutoff = Time.now - retention
14
+ store.cleanup(before: cutoff)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Dontbugme
6
+ class CLI < Thor
7
+ desc 'list', 'List recent traces'
8
+ option :limit, type: :numeric, default: 20, aliases: '-n'
9
+ def list
10
+ ensure_loaded
11
+ traces = store.search(limit: options[:limit])
12
+ display_list(traces)
13
+ end
14
+
15
+ desc 'show TRACE_ID', 'Show a single trace'
16
+ option :only, type: :string, desc: 'Filter spans by category (sql, http, etc.)'
17
+ option :slow, type: :numeric, desc: 'Only show spans slower than N ms'
18
+ option :json, type: :boolean, default: false
19
+ def show(trace_id)
20
+ ensure_loaded
21
+ trace = store.find_trace(trace_id)
22
+ if trace.nil?
23
+ puts "Trace not found: #{trace_id}"
24
+ exit 1
25
+ end
26
+
27
+ if options[:json]
28
+ puts Formatters::Json.format(trace)
29
+ else
30
+ puts Formatters::Timeline.format(trace, only: options[:only], slow: options[:slow])
31
+ end
32
+ end
33
+
34
+ desc 'diff TRACE_A TRACE_B', 'Compare two traces'
35
+ def diff(trace_id_a, trace_id_b)
36
+ ensure_loaded
37
+ trace_a = store.find_trace(trace_id_a)
38
+ trace_b = store.find_trace(trace_id_b)
39
+ if trace_a.nil?
40
+ puts "Trace not found: #{trace_id_a}"
41
+ exit 1
42
+ end
43
+ if trace_b.nil?
44
+ puts "Trace not found: #{trace_id_b}"
45
+ exit 1
46
+ end
47
+ puts Formatters::Diff.format(trace_a, trace_b)
48
+ end
49
+
50
+ desc 'trace TRACE_ID', 'Show trace and follow correlation chain'
51
+ option :follow, type: :boolean, default: false, aliases: '-f'
52
+ def trace(trace_id)
53
+ ensure_loaded
54
+ root = store.find_trace(trace_id)
55
+ if root.nil?
56
+ puts "Trace not found: #{trace_id}"
57
+ exit 1
58
+ end
59
+
60
+ if options[:follow]
61
+ cid = root.correlation_id || root.metadata[:correlation_id]
62
+ if cid.nil? || cid.to_s.empty?
63
+ puts "No correlation ID for trace #{trace_id}. Showing single trace."
64
+ puts Formatters::Timeline.format(root)
65
+ return
66
+ end
67
+ traces = store.search(correlation_id: cid, limit: 100)
68
+ puts format_correlation_tree(traces, root)
69
+ else
70
+ puts Formatters::Timeline.format(root)
71
+ end
72
+ end
73
+
74
+ desc 'search', 'Search traces'
75
+ option :status, type: :string, desc: 'Filter by status (success, error)'
76
+ option :kind, type: :string, desc: 'Filter by kind (sidekiq, request, custom)'
77
+ option :identifier, type: :string, desc: 'Filter by identifier (partial match)'
78
+ option :class, type: :string, desc: 'Filter by job/request class (e.g. SendInvoiceJob)'
79
+ option :limit, type: :numeric, default: 20
80
+ def search
81
+ ensure_loaded
82
+ filters = { limit: options[:limit] }
83
+ filters[:status] = options[:status] if options[:status]
84
+ filters[:kind] = options[:kind] if options[:kind]
85
+ filters[:identifier] = options[:identifier] || options[:class]
86
+ traces = store.search(filters)
87
+ display_list(traces)
88
+ end
89
+
90
+ default_task :list
91
+
92
+ private
93
+
94
+ def ensure_loaded
95
+ # Gem is loaded by bin/dontbugme; this is a no-op when used as a gem
96
+ require 'dontbugme' unless defined?(Dontbugme)
97
+ end
98
+
99
+ def store
100
+ Dontbugme.store
101
+ end
102
+
103
+ def display_list(traces)
104
+ return puts('No traces found.') if traces.empty?
105
+
106
+ rows = traces.map do |trace|
107
+ [
108
+ trace.id,
109
+ truncate(trace.identifier, 24),
110
+ status_icon(trace.status),
111
+ duration_str(trace)
112
+ ]
113
+ end
114
+
115
+ # Simple table
116
+ col_widths = [12, 26, 6, 10]
117
+ puts ''
118
+ puts row_str(['Trace ID', 'Identifier', 'Status', 'Duration'], col_widths)
119
+ puts row_str(['-' * 12, '-' * 24, '-' * 4, '-' * 8], col_widths)
120
+ rows.each { |r| puts row_str(r, col_widths) }
121
+ puts ''
122
+ end
123
+
124
+ def row_str(cols, widths)
125
+ cols.each_with_index.map { |c, i| c.to_s.ljust(widths[i]) }.join(' ')
126
+ end
127
+
128
+ def truncate(str, max)
129
+ return str if str.length <= max
130
+
131
+ "#{str[0, max - 3]}..."
132
+ end
133
+
134
+ def status_icon(status)
135
+ status.to_s == 'success' ? '✓' : '✗'
136
+ end
137
+
138
+ def duration_str(trace)
139
+ ms = trace.duration_ms
140
+ ms ? "#{ms.round}ms" : '-'
141
+ end
142
+
143
+ def format_correlation_tree(traces, root)
144
+ lines = []
145
+ lines << ''
146
+ lines << " Correlation: #{root.correlation_id || root.metadata[:correlation_id]}"
147
+ lines << ' ' + ('─' * 60)
148
+ lines << ''
149
+
150
+ # Order: request first, then jobs by started_at
151
+ sorted = traces.sort_by do |t|
152
+ kind_order = { request: 0, sidekiq: 1, custom: 2 }
153
+ [kind_order[t.kind] || 3, t.started_at_utc.to_s]
154
+ end
155
+
156
+ display_trace = lambda do |t|
157
+ icon = t.status == :success ? '✓' : '✗'
158
+ duration = t.duration_ms ? "#{t.duration_ms.round}ms" : '-'
159
+ "#{t.identifier} (#{t.id}, #{duration}, #{t.status})"
160
+ end
161
+
162
+ sorted.each_with_index do |t, i|
163
+ prefix = i.zero? ? '' : (i == sorted.size - 1 ? '└── ' : '├── ')
164
+ lines << " #{prefix}#{display_trace.call(t)}"
165
+ end
166
+
167
+ lines << ''
168
+ lines.join("\n")
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ root to: 'traces#index', as: :root
5
+ get 'diff', to: 'traces#diff', as: :diff
6
+ get ':id', to: 'traces#show', as: :trace, constraints: { id: /tr_[a-f0-9]+/ }
7
+ end