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
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ class Configuration
5
+ attr_accessor :store,
6
+ :sqlite_path,
7
+ :postgresql_connection,
8
+ :enable_web_ui,
9
+ :web_ui_mount_path,
10
+ :recording_mode,
11
+ :record_on_error,
12
+ :record_jobs,
13
+ :record_requests,
14
+ :sample_rate,
15
+ :capture_sql_binds,
16
+ :capture_http_headers,
17
+ :capture_http_body,
18
+ :capture_redis_values,
19
+ :source_mode,
20
+ :source_filter,
21
+ :source_depth,
22
+ :source_stack_limit,
23
+ :max_spans_per_trace,
24
+ :span_overflow_strategy,
25
+ :max_sql_bind_size,
26
+ :max_http_body_size,
27
+ :max_redis_value_size,
28
+ :max_span_detail_size,
29
+ :max_trace_buffer_bytes,
30
+ :retention,
31
+ :max_traces,
32
+ :async_store
33
+
34
+ def initialize
35
+ apply_environment_defaults
36
+ end
37
+
38
+ def apply_environment_defaults
39
+ env = defined?(Rails) ? Rails.env.to_s : 'development'
40
+
41
+ case env
42
+ when 'test'
43
+ apply_test_defaults
44
+ when 'production'
45
+ apply_production_defaults
46
+ else
47
+ apply_development_defaults
48
+ end
49
+ end
50
+
51
+ def apply_development_defaults
52
+ self.store = :sqlite
53
+ self.sqlite_path = 'tmp/inspector/inspector.db'
54
+ self.postgresql_connection = nil
55
+ self.enable_web_ui = true
56
+ self.web_ui_mount_path = '/inspector'
57
+ self.recording_mode = :always
58
+ self.record_on_error = true
59
+ self.record_jobs = :all
60
+ self.record_requests = :all
61
+ self.sample_rate = 1.0
62
+ self.capture_sql_binds = true
63
+ self.capture_http_headers = []
64
+ self.capture_http_body = false
65
+ self.capture_redis_values = false
66
+ self.source_mode = :full
67
+ self.source_filter = %w[app/ lib/]
68
+ self.source_depth = 3
69
+ self.source_stack_limit = 50
70
+ self.max_spans_per_trace = 1_000
71
+ self.span_overflow_strategy = :count
72
+ self.max_sql_bind_size = 4_096
73
+ self.max_http_body_size = 8_192
74
+ self.max_redis_value_size = 512
75
+ self.max_span_detail_size = 8_192
76
+ self.max_trace_buffer_bytes = 10 * 1024 * 1024 # 10 MB
77
+ self.retention = retention_seconds(72, :hours)
78
+ self.max_traces = 10_000
79
+ self.async_store = false
80
+ end
81
+
82
+ def apply_test_defaults
83
+ apply_development_defaults
84
+ self.store = :memory
85
+ self.recording_mode = :off
86
+ self.enable_web_ui = false
87
+ self.record_on_error = false
88
+ self.max_trace_buffer_bytes = 5 * 1024 * 1024 # 5 MB
89
+ end
90
+
91
+ def apply_production_defaults
92
+ apply_development_defaults
93
+ self.store = :postgresql
94
+ self.enable_web_ui = false
95
+ self.recording_mode = :selective
96
+ self.record_on_error = true
97
+ self.capture_sql_binds = false
98
+ self.source_mode = :shallow
99
+ self.source_depth = 1
100
+ self.source_stack_limit = 30
101
+ self.max_spans_per_trace = 500
102
+ self.max_sql_bind_size = 512
103
+ self.max_http_body_size = 1_024
104
+ self.max_span_detail_size = 2_048
105
+ self.max_trace_buffer_bytes = 5 * 1024 * 1024 # 5 MB
106
+ self.retention = retention_seconds(24, :hours)
107
+ self.async_store = true
108
+ end
109
+
110
+ def recording?
111
+ return false if recording_mode == :off
112
+ return true if recording_mode == :always
113
+
114
+ # :selective — caller (Recorder) decides based on record_jobs, record_requests, etc.
115
+ true
116
+ end
117
+
118
+ def should_record_job?(job_class)
119
+ return false if record_jobs == :none
120
+ return true if record_jobs == :all
121
+ return record_jobs.include?(job_class.to_s) if record_jobs.is_a?(Array)
122
+
123
+ false
124
+ end
125
+
126
+ def should_record_request?(env)
127
+ return false if record_requests == :none
128
+ return true if record_requests == :all
129
+ return record_requests.call(env) if record_requests.respond_to?(:call)
130
+
131
+ false
132
+ end
133
+
134
+ private
135
+
136
+ def retention_seconds(value, unit)
137
+ return value if value.is_a?(Integer)
138
+ return value.to_i if defined?(ActiveSupport) && value.respond_to?(:to_i)
139
+
140
+ case unit
141
+ when :hours then value * 3600
142
+ when :days then value * 86400
143
+ else value
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ class Context
5
+ KEY = :dontbugme_trace
6
+
7
+ class << self
8
+ def current
9
+ Thread.current[KEY]
10
+ end
11
+
12
+ def current=(trace)
13
+ Thread.current[KEY] = trace
14
+ end
15
+
16
+ def active?
17
+ !current.nil?
18
+ end
19
+
20
+ def clear!
21
+ Thread.current[KEY] = nil
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ module Correlation
5
+ KEY = :dontbugme_correlation_id
6
+
7
+ class << self
8
+ def current
9
+ Thread.current[KEY]
10
+ end
11
+
12
+ def current=(id)
13
+ Thread.current[KEY] = id
14
+ end
15
+
16
+ def generate
17
+ "corr_#{SecureRandom.hex(8)}"
18
+ end
19
+
20
+ def clear!
21
+ Thread.current[KEY] = nil
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Dontbugme
6
+
7
+ # Mount in config/routes.rb:
8
+ # mount Dontbugme::Engine, at: '/inspector'
9
+ # Enable/disable via config.enable_web_ui (default: true in dev, false in prod)
10
+ end
11
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module Dontbugme
6
+ module Formatters
7
+ class Diff
8
+ def self.format(trace_a, trace_b)
9
+ new(trace_a, trace_b).format
10
+ end
11
+
12
+ def initialize(trace_a, trace_b)
13
+ @trace_a = trace_a
14
+ @trace_b = trace_b
15
+ @alignment = align_spans
16
+ end
17
+
18
+ def format
19
+ lines = []
20
+ lines << ''
21
+ lines << " #{@trace_a.identifier} — Execution Diff"
22
+ lines << ' ' + ('─' * 60)
23
+ lines << ''
24
+ lines << " Trace A: #{@trace_a.id} (#{@trace_a.status}, #{duration_str(@trace_a)})"
25
+ lines << " Trace B: #{@trace_b.id} (#{@trace_b.status}, #{duration_str(@trace_b)})"
26
+ lines << ''
27
+
28
+ @alignment.each do |result|
29
+ lines << format_result(result)
30
+ end
31
+
32
+ lines << ''
33
+ lines.join("\n")
34
+ end
35
+
36
+ private
37
+
38
+ def align_spans
39
+ spans_a = spans_with_error(@trace_a)
40
+ spans_b = spans_with_error(@trace_b)
41
+ results = []
42
+ i = j = 0
43
+
44
+ while i < spans_a.size || j < spans_b.size
45
+ span_a = spans_a[i]
46
+ span_b = spans_b[j]
47
+
48
+ if span_a.nil?
49
+ results << { type: :new, span_a: nil, span_b: span_b }
50
+ j += 1
51
+ elsif span_b.nil?
52
+ results << { type: :missing, span_a: span_a, span_b: nil }
53
+ i += 1
54
+ else
55
+ key_a = span_key(span_a)
56
+ key_b = span_key(span_b)
57
+
58
+ if key_a == key_b
59
+ if span_equal?(span_a, span_b)
60
+ results << { type: :identical, span_a: span_a, span_b: span_b }
61
+ else
62
+ results << { type: :changed, span_a: span_a, span_b: span_b }
63
+ end
64
+ i += 1
65
+ j += 1
66
+ elsif key_a < key_b
67
+ results << { type: :missing, span_a: span_a, span_b: nil }
68
+ i += 1
69
+ else
70
+ results << { type: :new, span_a: nil, span_b: span_b }
71
+ j += 1
72
+ end
73
+ end
74
+ end
75
+
76
+ results
77
+ end
78
+
79
+ def spans_with_error(trace)
80
+ spans = trace.raw_spans.dup
81
+ if trace.error
82
+ error_span = OpenStruct.new(
83
+ category: :custom,
84
+ operation: 'EXCEPTION',
85
+ detail: "#{trace.error[:class]}: #{trace.error[:message]}",
86
+ payload: trace.error
87
+ )
88
+ spans << error_span
89
+ end
90
+ spans
91
+ end
92
+
93
+ def span_key(span)
94
+ detail = normalize_detail(span.detail)
95
+ "#{span.category}:#{span.operation}:#{detail}"
96
+ end
97
+
98
+ def normalize_detail(detail)
99
+ return '' if detail.nil?
100
+ # Normalize SQL: collapse whitespace, remove exact values for comparison
101
+ detail = detail.to_s.strip.gsub(/\s+/, ' ')
102
+ # Truncate for alignment (compare structure, not values)
103
+ detail[0, 80]
104
+ end
105
+
106
+ def span_equal?(span_a, span_b)
107
+ return false unless span_a.category == span_b.category
108
+ return false unless span_a.operation == span_b.operation
109
+
110
+ case span_a.category
111
+ when :http
112
+ (span_a.payload[:status] || span_a.payload['status']) == (span_b.payload[:status] || span_b.payload['status'])
113
+ when :sql
114
+ normalize_detail(span_a.detail) == normalize_detail(span_b.detail)
115
+ else
116
+ normalize_detail(span_a.detail) == normalize_detail(span_b.detail)
117
+ end
118
+ end
119
+
120
+ def format_result(result)
121
+ case result[:type]
122
+ when :identical
123
+ " IDENTICAL #{format_span_short(result[:span_a])}"
124
+ when :changed
125
+ lines = [" CHANGED #{format_span_short(result[:span_a])}"]
126
+ lines << " A: #{format_span_detail(result[:span_a])}"
127
+ lines << " B: #{format_span_detail(result[:span_b])} ←"
128
+ lines.join("\n")
129
+ when :missing
130
+ " MISSING #{format_span_short(result[:span_a])} ← never reached in B"
131
+ when :new
132
+ span_b = result[:span_b]
133
+ if span_b.operation == 'EXCEPTION'
134
+ err = span_b.payload
135
+ detail = err ? "#{err[:class]}: #{truncate(err[:message].to_s, 40)}" : 'EXCEPTION'
136
+ " NEW EXCEPTION #{detail} ← only in B"
137
+ else
138
+ " NEW #{format_span_short(span_b)} ← only in B"
139
+ end
140
+ else
141
+ ''
142
+ end
143
+ end
144
+
145
+ def format_span_short(span)
146
+ return '' unless span
147
+
148
+ detail = truncate(span.detail, 50)
149
+ "#{span.category.to_s.upcase} #{span.operation} #{detail}"
150
+ end
151
+
152
+ def format_span_detail(span)
153
+ case span.category
154
+ when :http
155
+ status = span.payload[:status] || span.payload['status']
156
+ duration = span.duration_ms ? "#{span.duration_ms.round}ms" : ''
157
+ "#{status} #{status_label(status)} (#{duration})".strip
158
+ when :sql
159
+ truncate(span.detail, 60)
160
+ else
161
+ truncate(span.detail, 60)
162
+ end
163
+ end
164
+
165
+ def status_label(code)
166
+ return '' unless code
167
+ return 'OK' if code.to_i.between?(200, 299)
168
+ return 'Timeout' if code.to_i == 408 || code.to_i == 504
169
+ return 'Error' if code.to_i >= 400
170
+
171
+ ''
172
+ end
173
+
174
+ def duration_str(trace)
175
+ ms = trace.duration_ms
176
+ ms ? "#{ms.round}ms" : 'N/A'
177
+ end
178
+
179
+ def truncate(str, max)
180
+ return '' if str.nil? || str.empty?
181
+ return str if str.length <= max
182
+
183
+ "#{str[0, max - 3]}..."
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ module Formatters
5
+ class Json
6
+ def self.format(trace)
7
+ trace.to_h.to_json
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ module Formatters
5
+ class Timeline
6
+ def self.format(trace, only: nil, slow: nil)
7
+ new(trace, only: only, slow: slow).format
8
+ end
9
+
10
+ def initialize(trace, only: nil, slow: nil)
11
+ @trace = trace
12
+ @only = only&.to_sym
13
+ @slow = slow
14
+ end
15
+
16
+ def format
17
+ lines = []
18
+ lines << ''
19
+ lines << " #{@trace.identifier}"
20
+ lines << " Status: #{@trace.status} | Duration: #{duration_str} | Recorded: #{time_ago}"
21
+ lines << ''
22
+ lines << ' TIMELINE'
23
+ lines << ' ' + ('─' * 60)
24
+ lines << ''
25
+ lines << format_start_span
26
+ lines << ''
27
+
28
+ spans_to_show.each do |span|
29
+ lines << format_span(span)
30
+ end
31
+
32
+ lines << ''
33
+ lines << format_finish_span
34
+
35
+ if @trace.truncated_spans_count.to_i.positive?
36
+ lines << ''
37
+ lines << " ... #{@trace.truncated_spans_count} additional spans truncated"
38
+ end
39
+
40
+ lines << ''
41
+ lines.join("\n")
42
+ end
43
+
44
+ private
45
+
46
+ def format_start_span
47
+ label = case @trace.kind
48
+ when :sidekiq then 'Job started'
49
+ when :request then 'Request started'
50
+ else 'Started'
51
+ end
52
+ " 0.0ms ▸ #{label}"
53
+ end
54
+
55
+ def format_finish_span
56
+ label = case @trace.kind
57
+ when :sidekiq then 'Job finished'
58
+ when :request then 'Request finished'
59
+ else 'Finished'
60
+ end
61
+ duration = @trace.duration_ms ? "#{@trace.duration_ms.round(1)}ms" : '0ms'
62
+ " #{duration} ▸ #{label}"
63
+ end
64
+
65
+ def spans_to_show
66
+ spans = @trace.raw_spans
67
+ spans = spans.select { |s| s.category == @only } if @only
68
+ spans = spans.select { |s| s.duration_ms.to_f >= @slow } if @slow
69
+ spans
70
+ end
71
+
72
+ def format_span(span)
73
+ offset = span.started_at.to_f.round(1)
74
+ duration = span.duration_ms ? "(#{span.duration_ms.round(1)}ms)" : ''
75
+ detail = format_span_detail(span)
76
+ line = " #{offset}ms ▸ #{span.category.to_s.upcase} #{span.operation} #{detail} #{duration}".strip
77
+ lines = [line]
78
+ lines << " → #{span.source}" if span.source && !span.source.empty?
79
+ lines.join("\n")
80
+ end
81
+
82
+ def format_span_detail(span)
83
+ case span.category
84
+ when :http
85
+ status = span.payload[:status] || span.payload['status']
86
+ status_str = status ? " → #{status}" : ''
87
+ truncate_detail(span.detail + status_str, 55)
88
+ else
89
+ truncate_detail(span.detail, 50)
90
+ end
91
+ end
92
+
93
+ def truncate_detail(str, max_len)
94
+ return '' if str.nil? || str.empty?
95
+
96
+ str = str.strip
97
+ return str if str.length <= max_len
98
+
99
+ "#{str[0, max_len - 3]}..."
100
+ end
101
+
102
+ def duration_str
103
+ ms = @trace.duration_ms
104
+ ms ? "#{ms.round}ms" : 'N/A'
105
+ end
106
+
107
+ def time_ago
108
+ return 'N/A' unless @trace.started_at_utc
109
+
110
+ sec = Time.now - @trace.started_at_utc
111
+ if sec < 60 then "#{sec.round} sec ago"
112
+ elsif sec < 3600 then "#{(sec / 60).round} min ago"
113
+ elsif sec < 86400 then "#{(sec / 3600).round} hours ago"
114
+ else "#{(sec / 86400).round} days ago"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ module Middleware
5
+ class Rack
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ return @app.call(env) unless Dontbugme.config.recording?
12
+
13
+ request = ::Rack::Request.new(env)
14
+ request_id = env['action_dispatch.request_id'] || request.get_header('HTTP_X_REQUEST_ID') || SecureRandom.uuid
15
+ correlation_id = env['HTTP_X_CORRELATION_ID'] || Correlation.generate
16
+ Correlation.current = correlation_id
17
+
18
+ method = request.request_method
19
+ path = request.path
20
+ identifier = "#{method} #{path}"
21
+
22
+ metadata = {
23
+ request_id: request_id,
24
+ correlation_id: correlation_id,
25
+ method: method,
26
+ path: path
27
+ }
28
+
29
+ Recorder.record(kind: :request, identifier: identifier, metadata: metadata) do
30
+ @app.call(env)
31
+ end
32
+ ensure
33
+ Correlation.clear!
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ module Middleware
5
+ class Sidekiq
6
+ def call(_worker, job, _queue)
7
+ return yield unless Dontbugme.config.recording?
8
+
9
+ job_class = job['class'] || job[:class] || 'Unknown'
10
+ jid = job['jid'] || job[:jid] || SecureRandom.hex(8)
11
+ correlation_id = job['correlation_id'] || job[:correlation_id] || Correlation.current
12
+ Correlation.current = correlation_id
13
+
14
+ identifier = "#{job_class} (jid=#{jid})"
15
+
16
+ metadata = {
17
+ jid: jid,
18
+ queue: job['queue'] || job[:queue],
19
+ args: job['args'] || job[:args],
20
+ correlation_id: correlation_id
21
+ }
22
+
23
+ Recorder.record(kind: :sidekiq, identifier: identifier, metadata: metadata) do
24
+ yield
25
+ end
26
+ ensure
27
+ Correlation.clear!
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dontbugme
4
+ module Middleware
5
+ class SidekiqClient
6
+ def call(_worker_class, job, _queue, _redis_pool)
7
+ if Correlation.current
8
+ job['correlation_id'] = Correlation.current
9
+ end
10
+ yield
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dontbugme'
4
+ require 'dontbugme/engine'
5
+
6
+ module Dontbugme
7
+ class Railtie < ::Rails::Railtie
8
+ config.dontbugme = ActiveSupport::OrderedOptions.new
9
+
10
+ initializer 'dontbugme.configure' do |app|
11
+ # Configuration defaults are applied in Configuration#initialize
12
+ # Merge any app-level config (e.g. config.dontbugme.store = :sqlite)
13
+ config = Dontbugme.config
14
+ app.config.dontbugme.each { |k, v| config.send("#{k}=", v) }
15
+ end
16
+
17
+ initializer 'dontbugme.subscribers' do
18
+ Dontbugme::Subscribers::ActiveRecord.subscribe
19
+ Dontbugme::Subscribers::NetHttp.subscribe
20
+ Dontbugme::Subscribers::Redis.subscribe
21
+ Dontbugme::Subscribers::Cache.subscribe
22
+ Dontbugme::Subscribers::ActionMailer.subscribe
23
+ Dontbugme::Subscribers::ActiveJob.subscribe
24
+ end
25
+
26
+ config.after_initialize do
27
+ # Sidekiq middleware
28
+ if defined?(Sidekiq)
29
+ Sidekiq.configure_server do |config|
30
+ config.server_middleware do |chain|
31
+ chain.add Dontbugme::Middleware::Sidekiq
32
+ end
33
+ end
34
+ Sidekiq.configure_client do |config|
35
+ config.client_middleware do |chain|
36
+ chain.add Dontbugme::Middleware::SidekiqClient
37
+ end
38
+ end
39
+ end
40
+
41
+ # Rack middleware
42
+ if defined?(Rails::Application)
43
+ Rails.application.config.middleware.insert 0, Dontbugme::Middleware::Rack
44
+ end
45
+ end
46
+ end
47
+ end