debugbundle 2.0.1 → 2.1.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +131 -1
  3. data/lib/debugbundle/before_send.rb +58 -30
  4. data/lib/debugbundle/bounded_http_response.rb +24 -0
  5. data/lib/debugbundle/canonical_event_schema.rb +137 -0
  6. data/lib/debugbundle/client.rb +50 -67
  7. data/lib/debugbundle/client_event_support.rb +11 -12
  8. data/lib/debugbundle/client_evidence_support.rb +169 -0
  9. data/lib/debugbundle/client_exception_support.rb +64 -0
  10. data/lib/debugbundle/client_process_support.rb +5 -12
  11. data/lib/debugbundle/client_queue_support.rb +59 -4
  12. data/lib/debugbundle/config.rb +15 -2
  13. data/lib/debugbundle/diagnostic_budget.rb +155 -0
  14. data/lib/debugbundle/event-envelope.schema.json +4904 -0
  15. data/lib/debugbundle/event_batch.rb +48 -0
  16. data/lib/debugbundle/evidence_delivery.rb +50 -0
  17. data/lib/debugbundle/evidence_graph.rb +73 -0
  18. data/lib/debugbundle/evidence_json.rb +55 -0
  19. data/lib/debugbundle/evidence_resources.rb +60 -0
  20. data/lib/debugbundle/evidence_value.rb +107 -0
  21. data/lib/debugbundle/evidence_wire.rb +193 -0
  22. data/lib/debugbundle/exception_evidence.rb +179 -0
  23. data/lib/debugbundle/exception_identity.rb +42 -0
  24. data/lib/debugbundle/framework_operation.rb +13 -0
  25. data/lib/debugbundle/logging.rb +2 -1
  26. data/lib/debugbundle/operation_context.rb +90 -0
  27. data/lib/debugbundle/rack/middleware.rb +24 -21
  28. data/lib/debugbundle/rack/relay_middleware.rb +4 -1
  29. data/lib/debugbundle/rails/railtie.rb +4 -1
  30. data/lib/debugbundle/rails/relay_endpoint.rb +2 -1
  31. data/lib/debugbundle/relay/forwarder.rb +83 -0
  32. data/lib/debugbundle/relay/handler.rb +83 -19
  33. data/lib/debugbundle/remote_config.rb +5 -0
  34. data/lib/debugbundle/sidekiq/server_middleware.rb +22 -18
  35. data/lib/debugbundle/telemetry_privacy.rb +43 -5
  36. data/lib/debugbundle/transport.rb +43 -17
  37. data/lib/debugbundle/version.rb +1 -1
  38. data/lib/debugbundle.rb +11 -1
  39. metadata +20 -2
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
  require 'uri'
5
+ require 'debugbundle/relay/forwarder'
5
6
 
6
7
  module DebugBundle
7
8
  module Relay
@@ -12,6 +13,7 @@ module DebugBundle
12
13
  request_event
13
14
  probe_event
14
15
  analytics_event
16
+ diagnostic_context
15
17
  ].freeze
16
18
  BROWSER_SDK_NAME = '@debugbundle/sdk-browser'
17
19
  DEFAULT_MAX_BODY_BYTES = 262_144
@@ -20,6 +22,8 @@ module DebugBundle
20
22
  Response = Struct.new(:status, :body, :headers, keyword_init: true)
21
23
 
22
24
  class Handler
25
+ attr_reader :max_body_bytes
26
+
23
27
  def initialize(
24
28
  project_mode: :connected,
25
29
  project_token: nil,
@@ -33,7 +37,8 @@ module DebugBundle
33
37
  max_body_bytes: DEFAULT_MAX_BODY_BYTES,
34
38
  rate_limit_per_minute: DEFAULT_RATE_LIMIT_PER_MINUTE,
35
39
  rate_limit_store: nil,
36
- forward_transport: nil
40
+ forward_transport: nil,
41
+ evidence_capabilities: nil
37
42
  )
38
43
  @project_mode = project_mode.to_sym
39
44
  @project_token = project_token
@@ -48,16 +53,24 @@ module DebugBundle
48
53
  @rate_limit_per_minute = rate_limit_per_minute
49
54
  @rate_limit_store = rate_limit_store
50
55
  @forward_transport = forward_transport || Transport::HttpTransport.new(@endpoint)
56
+ @evidence = EvidenceDelivery.new(evidence_capabilities)
57
+ @forwarder = Forwarder.new(@forward_transport, @project_token, @evidence)
51
58
  @rate_limit_state = Hash.new { |hash, key| hash[key] = [] }
52
59
  end
53
60
 
54
61
  def handle(request)
55
62
  method = request.fetch(:method, 'POST').to_s.upcase
56
63
  headers = normalize_headers(request[:headers] || {})
64
+ discover = requests_evidence?(request, headers)
57
65
  raw_body = request[:body].to_s
58
66
  return Response.new(status: 403, body: nil, headers: {}) unless origin_allowed?(headers)
59
67
 
60
- response_headers = source_origin(headers) ? cors_headers(source_origin(headers)) : {}
68
+ response_headers = if source_origin(headers)
69
+ cors_headers(source_origin(headers),
70
+ headers['access-control-request-headers'])
71
+ else
72
+ {}
73
+ end
61
74
  early_response = request_validation_response(
62
75
  method: method,
63
76
  headers: headers,
@@ -66,11 +79,17 @@ module DebugBundle
66
79
  )
67
80
  return with_headers(early_response, response_headers) if early_response
68
81
 
69
- accepted, errors = sanitize_batch(raw_body)
82
+ accepted, errors, indexed = sanitize_batch(raw_body)
70
83
 
71
- deliver(accepted) unless accepted.empty?
84
+ deliver(accepted, discover: discover) unless accepted.empty?
72
85
 
73
- with_headers(batch_response(accepted, errors), response_headers)
86
+ response = batch_response(accepted, errors)
87
+ if discover
88
+ response.status = 202
89
+ response.body['evidence_capabilities'] = @evidence.descriptor
90
+ response.body['evidence_errors'] = indexed unless indexed.empty?
91
+ end
92
+ with_headers(response, response_headers)
74
93
  rescue JSON::ParserError
75
94
  with_headers(invalid_json_response, response_headers || {})
76
95
  rescue KeyError
@@ -130,27 +149,44 @@ module DebugBundle
130
149
  )
131
150
  end
132
151
 
133
- def cors_headers(origin)
152
+ def cors_headers(origin, requested = nil)
153
+ allow_evidence = requested.to_s.downcase.split(',').map(&:strip).include?('x-debugbundle-evidence')
134
154
  {
135
155
  'Access-Control-Allow-Origin' => origin,
136
156
  'Access-Control-Allow-Methods' => 'POST, OPTIONS',
137
- 'Access-Control-Allow-Headers' => 'content-type',
157
+ 'Access-Control-Allow-Headers' => allow_evidence ? 'content-type, x-debugbundle-evidence' : 'content-type',
138
158
  'Access-Control-Max-Age' => '600',
139
- 'Vary' => 'Origin'
159
+ 'Vary' => allow_evidence ? 'Origin, Access-Control-Request-Headers' : 'Origin'
140
160
  }
141
161
  end
142
162
 
143
- def deliver(events)
163
+ def requests_evidence?(request, headers)
164
+ return true if headers['x-debugbundle-evidence'] == '1'
165
+
166
+ url = request[:url]
167
+ return false unless url.is_a?(String) && url.bytesize <= 4096
168
+
169
+ URI.decode_www_form(URI.parse(url).query || '').include?(%w[debugbundle_evidence 1])
170
+ rescue URI::InvalidURIError, ArgumentError
171
+ false
172
+ end
173
+
174
+ def deliver(events, discover:)
144
175
  service_name = @service || events.first.dig('service', 'name') || 'service'
145
176
 
146
177
  case @project_mode
147
178
  when :local_only
148
- Transport::FileTransport.new(@local_events_dir).call(service_name: service_name, events: events)
179
+ result = Transport::FileTransport.new(@local_events_dir).call(service_name: service_name, events: events)
180
+ raise 'relay_write_failed' unless result.status_code == 202
149
181
  when :connected
150
- Transport::FileTransport.new(@spool_dir).call(service_name: service_name, events: events) if @durable_write
151
-
152
- result = Transport.coerce_result(@forward_transport.call(project_token: @project_token, events: events))
153
- raise 'relay_forward_failed' unless result.status_code.between?(200, 299)
182
+ if @durable_write
183
+ spool = Transport::FileTransport.new(@spool_dir)
184
+ written = spool.call(service_name: service_name, events: events)
185
+ raise 'relay_write_failed' unless written.status_code == 202
186
+ end
187
+ success, complete = @forwarder.send(events, discover: discover)
188
+ spool.mark_delivered(written.written_file_path) if complete && spool && written.written_file_path
189
+ raise 'relay_forward_failed' unless success || @durable_write
154
190
  else
155
191
  raise ArgumentError, 'unsupported relay project mode'
156
192
  end
@@ -161,12 +197,16 @@ module DebugBundle
161
197
  batch = decoded.fetch('batch')
162
198
  raise KeyError unless batch.is_a?(Array)
163
199
 
164
- batch.each_with_index.with_object([[], []]) do |(candidate, index), (accepted, errors)|
200
+ batch.each_with_index.with_object([[], [], []]) do |(candidate, index), (accepted, errors, indexed)|
165
201
  sanitized = sanitize_event(candidate)
202
+ if sanitized && sanitized['event_type'] != 'analytics_event'
203
+ sanitized = EvidenceWire.supported(sanitized, @evidence.features)
204
+ end
166
205
  if sanitized
167
206
  accepted << sanitized
168
207
  else
169
208
  errors << "batch[#{index}]: Invalid browser relay event payload."
209
+ indexed << { 'index' => index, 'reason' => 'invalid_event' }
170
210
  end
171
211
  end
172
212
  end
@@ -194,19 +234,43 @@ module DebugBundle
194
234
  'environment' => @environment || service['environment'].to_s
195
235
  },
196
236
  'correlation' => sanitize_correlation(correlation, event_type),
197
- 'payload' => payload,
198
- 'project_token' => @project_token
237
+ 'payload' => payload
199
238
  }
239
+ sanitized['project_token'] = @project_token if @project_token.is_a?(String) && !@project_token.empty?
240
+ sanitized['context'] = candidate['context'] if candidate['context'].is_a?(Hash)
241
+ return nil unless EvidenceWire.valid_optional?(sanitized)
242
+
243
+ new_contract = uses_evidence_contract?(sanitized)
244
+ return nil if new_contract && !BeforeSend.valid?(sanitized)
245
+
246
+ protect_relay_event(sanitized, new_contract)
247
+ rescue StandardError
248
+ nil
249
+ end
250
+
251
+ def protect_relay_event(sanitized, new_contract)
200
252
  return nil unless TelemetryPrivacy.safe_event_identity?(sanitized)
201
253
 
202
- fields = TelemetryPrivacy.protect({ 'service' => sanitized['service'], 'payload' => sanitized['payload'] })
254
+ fields = TelemetryPrivacy.protect(sanitized.slice('service', 'payload', 'context'))
203
255
  return nil unless fields['service'].is_a?(Hash) && fields['payload'].is_a?(Hash)
204
256
 
205
- sanitized.merge('service' => fields['service'], 'payload' => fields['payload'])
257
+ protected = sanitized.merge(fields)
258
+ if EvidenceWire.valid_optional?(protected) && (!new_contract || BeforeSend.valid?(protected))
259
+ return EventBatch.fit(protected)
260
+ end
261
+
262
+ reduced = EvidenceWire.without_optional(protected)
263
+ EventBatch.fit(reduced) if reduced && BeforeSend.valid?(reduced)
206
264
  rescue StandardError
207
265
  nil
208
266
  end
209
267
 
268
+ def uses_evidence_contract?(event)
269
+ event['event_type'] == 'diagnostic_context' ||
270
+ %w[exception_evidence resource_observations execution].any? { |field| event['payload'].key?(field) } ||
271
+ !EvidenceWire.reference(event.dig('context', 'evidence_reference')).nil?
272
+ end
273
+
210
274
  def sanitize_correlation(value, event_type)
211
275
  correlation = value.is_a?(Hash) ? value : {}
212
276
  keys = if event_type == 'analytics_event'
@@ -10,6 +10,7 @@ module DebugBundle
10
10
  :capture_request_events,
11
11
  :capture_breadcrumbs,
12
12
  :capture_probe_events,
13
+ :capture_diagnostic_context,
13
14
  :immediate_client_error_statuses,
14
15
  :immediate_client_error_path_rules,
15
16
  keyword_init: true
@@ -72,6 +73,7 @@ module DebugBundle
72
73
  capture_request_events: 'failures_only',
73
74
  capture_breadcrumbs: 'local_only',
74
75
  capture_probe_events: 'buffer_only',
76
+ capture_diagnostic_context: false,
75
77
  immediate_client_error_statuses: [],
76
78
  immediate_client_error_path_rules: []
77
79
  )
@@ -84,6 +86,7 @@ module DebugBundle
84
86
  capture_request_events: 'failures_only',
85
87
  capture_breadcrumbs: 'exception_only',
86
88
  capture_probe_events: 'buffer_only',
89
+ capture_diagnostic_context: true,
87
90
  immediate_client_error_statuses: [],
88
91
  immediate_client_error_path_rules: []
89
92
  )
@@ -125,6 +128,8 @@ module DebugBundle
125
128
  payload['capture_request_events'] || payload[:capture_request_events] || 'failures_only',
126
129
  capture_breadcrumbs: payload['capture_breadcrumbs'] || payload[:capture_breadcrumbs] || 'exception_only',
127
130
  capture_probe_events: payload['capture_probe_events'] || payload[:capture_probe_events] || 'buffer_only',
131
+ capture_diagnostic_context: (payload['preset'] || payload[:preset]) != 'minimal' &&
132
+ payload['capture_diagnostic_context'] != false && payload[:capture_diagnostic_context] != false,
128
133
  immediate_client_error_statuses: Array(
129
134
  payload['immediate_client_error_statuses'] || payload[:immediate_client_error_statuses]
130
135
  ).grep(Integer),
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'debugbundle/framework_operation'
4
+
3
5
  module DebugBundle
4
6
  module Sidekiq
5
7
  class ServerMiddleware
@@ -9,25 +11,27 @@ module DebugBundle
9
11
  end
10
12
 
11
13
  def call(_worker, job, queue)
12
- yield
13
- rescue StandardError => e
14
- @client.capture_exception(
15
- e,
16
- context: {
17
- queue: queue,
18
- job: {
19
- class: job['class'],
20
- queue: job['queue'] || queue,
21
- jid: job['jid'],
22
- retry_count: job['retry_count'] || job['retry'],
23
- args_summary: Array(job['args']).first(5).map { |value| value.class.name }
14
+ FrameworkOperation.run(@client, kind: 'job', correlation: { trace_id: job['trace_id'] }) do
15
+ yield
16
+ rescue StandardError => e
17
+ @client.capture_exception(
18
+ e,
19
+ context: {
20
+ queue: queue,
21
+ job: {
22
+ class: job['class'],
23
+ queue: job['queue'] || queue,
24
+ jid: job['jid'],
25
+ retry_count: job['retry_count'] || job['retry'],
26
+ args_summary: Array(job['args']).first(5).map { |value| value.class.name }
27
+ },
28
+ job_id: job['jid'],
29
+ trace_id: job['trace_id']
24
30
  },
25
- job_id: job['jid'],
26
- trace_id: job['trace_id']
27
- },
28
- handled: false
29
- )
30
- raise
31
+ handled: false
32
+ )
33
+ raise
34
+ end
31
35
  end
32
36
  end
33
37
  end
@@ -28,8 +28,12 @@ module DebugBundle
28
28
  raise ArgumentError, 'unsafe_input'
29
29
  end
30
30
 
31
- work = { nodes: 0, bytes: 0, seen: {}.compare_by_identity,
32
- fields: (FIELDS + additional_fields).map { |field| canonical(field) },
31
+ work = { nodes: 0, bytes: 0, seen: {}.compare_by_identity, key_policy: {}, safe_text: {},
32
+ fields: if additional_fields.empty?
33
+ CANONICAL_FIELDS
34
+ else
35
+ CANONICAL_FIELDS + additional_fields.map { |field| canonical(field) }
36
+ end,
33
37
  extra: additional_fields,
34
38
  assignment_pattern: assignment_pattern(additional_fields) }
35
39
  result = visit(value, work, 0, true)
@@ -54,6 +58,10 @@ module DebugBundle
54
58
  value.downcase.gsub(/[^a-z0-9]/, '')
55
59
  end
56
60
 
61
+ # Mandatory terms are immutable policy. Reuse their normalised form while
62
+ # each projection still owns its counters, seen set and configured keys.
63
+ CANONICAL_FIELDS = FIELDS.map { |field| canonical(field).freeze }.freeze
64
+
57
65
  def assignment_pattern(additional_fields)
58
66
  return DEFAULT_ASSIGNMENT_PATTERN if additional_fields.empty?
59
67
 
@@ -72,6 +80,23 @@ module DebugBundle
72
80
  end
73
81
  end
74
82
 
83
+ def key_policy(name, work)
84
+ cached = work[:key_policy][name]
85
+ return cached if cached
86
+
87
+ policy = if scrub_text(name, work) != name
88
+ :omit
89
+ elsif sensitive_key?(name, work[:fields])
90
+ :redact
91
+ else
92
+ :retain
93
+ end
94
+ # Stack frames repeat the same keys. Cache decisions only in this bounded
95
+ # projection; values, byte accounting and later policies remain independent.
96
+ work[:key_policy][name] = policy if work[:key_policy].size < 256
97
+ policy
98
+ end
99
+
75
100
  def visit(value, work, depth, structured)
76
101
  work[:nodes] += 1
77
102
  raise ArgumentError, 'budget_exceeded' if work[:nodes] > 4096
@@ -107,9 +132,10 @@ module DebugBundle
107
132
  next if name.bytesize > 128
108
133
 
109
134
  count_bytes(work, name)
110
- next if scrub_text(name, work) != name
135
+ policy = key_policy(name, work)
136
+ next if policy == :omit
111
137
 
112
- result[name] = sensitive_key?(name, work[:fields]) ? REDACTED : visit(nested, work, depth + 1, structured)
138
+ result[name] = policy == :redact ? REDACTED : visit(nested, work, depth + 1, structured)
113
139
  end
114
140
  end
115
141
  ensure
@@ -134,13 +160,25 @@ module DebugBundle
134
160
  # Malformed structured content with a credential label is withheld below.
135
161
  end
136
162
  end
137
- result = scrub_text(value, work)
163
+ result = scrub_value_text(value, work)
138
164
  return REDACTED if result == value && value.start_with?('{', '[') &&
139
165
  /(?:password|token|secret|authorization|cookie)["']?\s*[:=]/i.match?(value)
140
166
 
141
167
  result
142
168
  end
143
169
 
170
+ def scrub_value_text(value, work)
171
+ # Only short, unchanged scalar text is reusable. Structured containers and
172
+ # redacted values always take the full path; every return owns its string.
173
+ cacheable = value.bytesize <= 128 && !value.start_with?('{', '[')
174
+ cached = work[:safe_text][value] if cacheable
175
+ return cached.dup if cached
176
+
177
+ result = scrub_text(value, work)
178
+ work[:safe_text][value] = result.dup.freeze if cacheable && result == value && work[:safe_text].size < 256
179
+ result
180
+ end
181
+
144
182
  def valid_card?(candidate)
145
183
  digits = candidate.delete(' -')
146
184
  return false unless digits.length.between?(13, 19) && digits.chars.uniq.length > 1
@@ -6,12 +6,13 @@ require 'securerandom'
6
6
  require 'uri'
7
7
  require 'time'
8
8
  require 'fileutils'
9
+ require 'debugbundle/bounded_http_response'
9
10
 
10
11
  module DebugBundle
11
12
  module Transport
12
13
  RETRY_AFTER_CAP_SECONDS = 300
13
14
 
14
- Result = Struct.new(:status_code, :retry_after_seconds, :body, keyword_init: true)
15
+ Result = Struct.new(:status_code, :retry_after_seconds, :body, :written_file_path, keyword_init: true)
15
16
 
16
17
  def self.sdk_config_endpoint(events_endpoint)
17
18
  uri = URI.parse(events_endpoint)
@@ -48,24 +49,35 @@ module DebugBundle
48
49
  end
49
50
 
50
51
  def call(request)
52
+ deadline = request.fetch(:deadline) { Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5 }
53
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ return Result.new(status_code: 500) unless remaining.positive?
55
+
56
+ body = JSON.generate(events: request.fetch(:events))
57
+ return Result.new(status_code: 413) if body.bytesize > 262_144
58
+
51
59
  http = Net::HTTP.new(@uri.host, @uri.port)
52
60
  http.use_ssl = @uri.scheme == 'https'
53
- http.open_timeout = 5
54
- http.read_timeout = 5
55
-
56
- response = http.post(
57
- @uri.request_uri,
58
- JSON.generate(events: request.fetch(:events)),
59
- {
60
- 'Authorization' => "Bearer #{request.fetch(:project_token)}",
61
- 'Content-Type' => 'application/json'
62
- }
63
- )
61
+ http.open_timeout = remaining
62
+ http.read_timeout = remaining
63
+
64
+ response, raw = BoundedHttpResponse.read(deadline: deadline) do |consume|
65
+ http.post(
66
+ @uri.request_uri,
67
+ body,
68
+ {
69
+ 'Authorization' => "Bearer #{request.fetch(:project_token)}",
70
+ 'Content-Type' => 'application/json',
71
+ 'Accept-Encoding' => 'identity',
72
+ 'X-DebugBundle-Evidence' => '1'
73
+ }, &consume
74
+ )
75
+ end
64
76
 
65
77
  Result.new(
66
78
  status_code: response.code.to_i,
67
79
  retry_after_seconds: parse_retry_after(response['Retry-After']),
68
- body: parse_body(response.body)
80
+ body: parse_body(raw)
69
81
  )
70
82
  rescue StandardError
71
83
  Result.new(status_code: 500)
@@ -108,15 +120,18 @@ module DebugBundle
108
120
  http.open_timeout = 5
109
121
  http.read_timeout = 5
110
122
 
111
- request = Net::HTTP::Get.new(@uri.request_uri)
123
+ request = Net::HTTP::Get.new(@uri.request_uri, 'Accept-Encoding' => 'identity')
112
124
  request['Authorization'] = "Bearer #{@project_token}"
113
125
  request['Accept'] = 'application/json'
114
126
  request['X-DebugBundle-SDK'] = @sdk_name
115
127
  request['X-DebugBundle-SDK-Version'] = @sdk_version
116
128
  request['If-None-Match'] = etag if etag
129
+ request['X-DebugBundle-Evidence'] = '1'
117
130
 
118
- response = http.request(request)
119
- body = response.body.to_s.empty? ? {} : JSON.parse(response.body)
131
+ response, raw = BoundedHttpResponse.read do |consume|
132
+ http.request(request) { |reply| reply.read_body(&consume) }
133
+ end
134
+ body = raw.empty? ? {} : JSON.parse(raw)
120
135
  { status_code: response.code.to_i, etag: response['ETag'], body: body }
121
136
  rescue StandardError
122
137
  { status_code: 500, body: {} }
@@ -150,13 +165,24 @@ module DebugBundle
150
165
  File.rename(temp_path, final_path)
151
166
  File.chmod(FILE_MODE, final_path)
152
167
 
153
- Result.new(status_code: 202)
168
+ Result.new(status_code: 202, written_file_path: final_path)
154
169
  rescue StandardError
155
170
  Result.new(status_code: 500)
156
171
  ensure
157
172
  File.delete(temp_path) if defined?(temp_path) && temp_path && File.exist?(temp_path)
158
173
  end
159
174
 
175
+ def mark_delivered(path)
176
+ ensure_secure_directory!(@directory)
177
+ return false unless File.dirname(File.expand_path(path)) == @directory && path.end_with?('.events.json')
178
+ return false unless File.file?(path) && !File.symlink?(path)
179
+
180
+ write_secure_file("#{path}.delivered", '')
181
+ true
182
+ rescue StandardError
183
+ false
184
+ end
185
+
160
186
  private
161
187
 
162
188
  def ensure_secure_directory!(directory)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DebugBundle
4
- VERSION = '2.0.1'
4
+ VERSION = '2.1.1'
5
5
  end
data/lib/debugbundle.rb CHANGED
@@ -22,7 +22,9 @@ require_relative 'debugbundle/rails' if defined?(Rails::Railtie)
22
22
  module DebugBundle
23
23
  class << self
24
24
  def init(**options)
25
- self.client = Client.new(**options)
25
+ replacement = Client.new(**options)
26
+ @client.dispose if @client.is_a?(Client)
27
+ self.client = replacement
26
28
  client.capture_exceptions
27
29
  client
28
30
  end
@@ -41,6 +43,14 @@ module DebugBundle
41
43
  client.capture_error(error, context: context)
42
44
  end
43
45
 
46
+ def capture_recovery(observation = nil, context: nil, **options)
47
+ client.capture_recovery(observation, context: context, **options)
48
+ end
49
+
50
+ def with_operation_context(...)
51
+ client.with_operation_context(...)
52
+ end
53
+
44
54
  def capture_log(message, level: :warning, context: nil)
45
55
  client.capture_log(message, level: level, context: context)
46
56
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: debugbundle
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - DebugBundle
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-26 00:00:00.000000000 Z
10
+ date: 2026-11-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -49,14 +49,31 @@ files:
49
49
  - lib/debugbundle.rb
50
50
  - lib/debugbundle/acknowledgement.rb
51
51
  - lib/debugbundle/before_send.rb
52
+ - lib/debugbundle/bounded_http_response.rb
53
+ - lib/debugbundle/canonical_event_schema.rb
52
54
  - lib/debugbundle/client.rb
53
55
  - lib/debugbundle/client_event_support.rb
56
+ - lib/debugbundle/client_evidence_support.rb
57
+ - lib/debugbundle/client_exception_support.rb
54
58
  - lib/debugbundle/client_process_support.rb
55
59
  - lib/debugbundle/client_queue_support.rb
56
60
  - lib/debugbundle/config.rb
57
61
  - lib/debugbundle/config_worker.rb
58
62
  - lib/debugbundle/delivery_worker.rb
63
+ - lib/debugbundle/diagnostic_budget.rb
64
+ - lib/debugbundle/event-envelope.schema.json
65
+ - lib/debugbundle/event_batch.rb
66
+ - lib/debugbundle/evidence_delivery.rb
67
+ - lib/debugbundle/evidence_graph.rb
68
+ - lib/debugbundle/evidence_json.rb
69
+ - lib/debugbundle/evidence_resources.rb
70
+ - lib/debugbundle/evidence_value.rb
71
+ - lib/debugbundle/evidence_wire.rb
72
+ - lib/debugbundle/exception_evidence.rb
73
+ - lib/debugbundle/exception_identity.rb
74
+ - lib/debugbundle/framework_operation.rb
59
75
  - lib/debugbundle/logging.rb
76
+ - lib/debugbundle/operation_context.rb
60
77
  - lib/debugbundle/rack/middleware.rb
61
78
  - lib/debugbundle/rack/relay_middleware.rb
62
79
  - lib/debugbundle/rails.rb
@@ -64,6 +81,7 @@ files:
64
81
  - lib/debugbundle/rails/relay_endpoint.rb
65
82
  - lib/debugbundle/redaction.rb
66
83
  - lib/debugbundle/relay.rb
84
+ - lib/debugbundle/relay/forwarder.rb
67
85
  - lib/debugbundle/relay/handler.rb
68
86
  - lib/debugbundle/remote_config.rb
69
87
  - lib/debugbundle/runtime.rb