debugbundle 2.0.0 → 2.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +133 -0
  3. data/lib/debugbundle/acknowledgement.rb +6 -2
  4. data/lib/debugbundle/before_send.rb +58 -30
  5. data/lib/debugbundle/bounded_http_response.rb +24 -0
  6. data/lib/debugbundle/canonical_event_schema.rb +137 -0
  7. data/lib/debugbundle/client.rb +55 -75
  8. data/lib/debugbundle/client_event_support.rb +11 -12
  9. data/lib/debugbundle/client_evidence_support.rb +169 -0
  10. data/lib/debugbundle/client_exception_support.rb +54 -0
  11. data/lib/debugbundle/client_process_support.rb +5 -12
  12. data/lib/debugbundle/client_queue_support.rb +59 -4
  13. data/lib/debugbundle/config.rb +15 -2
  14. data/lib/debugbundle/diagnostic_budget.rb +155 -0
  15. data/lib/debugbundle/event-envelope.schema.json +4904 -0
  16. data/lib/debugbundle/event_batch.rb +48 -0
  17. data/lib/debugbundle/evidence_delivery.rb +50 -0
  18. data/lib/debugbundle/evidence_graph.rb +73 -0
  19. data/lib/debugbundle/evidence_json.rb +55 -0
  20. data/lib/debugbundle/evidence_resources.rb +60 -0
  21. data/lib/debugbundle/evidence_value.rb +107 -0
  22. data/lib/debugbundle/evidence_wire.rb +193 -0
  23. data/lib/debugbundle/exception_evidence.rb +179 -0
  24. data/lib/debugbundle/framework_operation.rb +13 -0
  25. data/lib/debugbundle/operation_context.rb +90 -0
  26. data/lib/debugbundle/rack/middleware.rb +24 -21
  27. data/lib/debugbundle/rack/relay_middleware.rb +4 -1
  28. data/lib/debugbundle/rails/railtie.rb +4 -1
  29. data/lib/debugbundle/rails/relay_endpoint.rb +2 -1
  30. data/lib/debugbundle/relay/forwarder.rb +83 -0
  31. data/lib/debugbundle/relay/handler.rb +83 -19
  32. data/lib/debugbundle/remote_config.rb +5 -0
  33. data/lib/debugbundle/sidekiq/server_middleware.rb +22 -18
  34. data/lib/debugbundle/telemetry_privacy.rb +9 -1
  35. data/lib/debugbundle/transport.rb +49 -19
  36. data/lib/debugbundle/version.rb +1 -1
  37. data/lib/debugbundle.rb +11 -1
  38. metadata +19 -2
@@ -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
@@ -29,7 +29,11 @@ module DebugBundle
29
29
  end
30
30
 
31
31
  work = { nodes: 0, bytes: 0, seen: {}.compare_by_identity,
32
- fields: (FIELDS + additional_fields).map { |field| canonical(field) },
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
 
@@ -4,13 +4,15 @@ require 'json'
4
4
  require 'net/http'
5
5
  require 'securerandom'
6
6
  require 'uri'
7
+ require 'time'
7
8
  require 'fileutils'
9
+ require 'debugbundle/bounded_http_response'
8
10
 
9
11
  module DebugBundle
10
12
  module Transport
11
13
  RETRY_AFTER_CAP_SECONDS = 300
12
14
 
13
- 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)
14
16
 
15
17
  def self.sdk_config_endpoint(events_endpoint)
16
18
  uri = URI.parse(events_endpoint)
@@ -47,24 +49,35 @@ module DebugBundle
47
49
  end
48
50
 
49
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
+
50
59
  http = Net::HTTP.new(@uri.host, @uri.port)
51
60
  http.use_ssl = @uri.scheme == 'https'
52
- http.open_timeout = 5
53
- http.read_timeout = 5
54
-
55
- response = http.post(
56
- @uri.request_uri,
57
- JSON.generate(events: request.fetch(:events)),
58
- {
59
- 'Authorization' => "Bearer #{request.fetch(:project_token)}",
60
- 'Content-Type' => 'application/json'
61
- }
62
- )
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
63
76
 
64
77
  Result.new(
65
78
  status_code: response.code.to_i,
66
79
  retry_after_seconds: parse_retry_after(response['Retry-After']),
67
- body: parse_body(response.body)
80
+ body: parse_body(raw)
68
81
  )
69
82
  rescue StandardError
70
83
  Result.new(status_code: 500)
@@ -83,8 +96,11 @@ module DebugBundle
83
96
  def parse_retry_after(value)
84
97
  return nil if value.nil? || value.strip.empty?
85
98
 
86
- seconds = Integer(Float(value))
87
- seconds.clamp(0, RETRY_AFTER_CAP_SECONDS)
99
+ seconds = Float(value, exception: false)
100
+ seconds = Time.httpdate(value) - Time.now if seconds.nil?
101
+ return nil unless seconds.finite?
102
+
103
+ seconds.clamp(0, RETRY_AFTER_CAP_SECONDS).to_i
88
104
  rescue ArgumentError, TypeError
89
105
  nil
90
106
  end
@@ -104,15 +120,18 @@ module DebugBundle
104
120
  http.open_timeout = 5
105
121
  http.read_timeout = 5
106
122
 
107
- request = Net::HTTP::Get.new(@uri.request_uri)
123
+ request = Net::HTTP::Get.new(@uri.request_uri, 'Accept-Encoding' => 'identity')
108
124
  request['Authorization'] = "Bearer #{@project_token}"
109
125
  request['Accept'] = 'application/json'
110
126
  request['X-DebugBundle-SDK'] = @sdk_name
111
127
  request['X-DebugBundle-SDK-Version'] = @sdk_version
112
128
  request['If-None-Match'] = etag if etag
129
+ request['X-DebugBundle-Evidence'] = '1'
113
130
 
114
- response = http.request(request)
115
- 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)
116
135
  { status_code: response.code.to_i, etag: response['ETag'], body: body }
117
136
  rescue StandardError
118
137
  { status_code: 500, body: {} }
@@ -146,13 +165,24 @@ module DebugBundle
146
165
  File.rename(temp_path, final_path)
147
166
  File.chmod(FILE_MODE, final_path)
148
167
 
149
- Result.new(status_code: 202)
168
+ Result.new(status_code: 202, written_file_path: final_path)
150
169
  rescue StandardError
151
170
  Result.new(status_code: 500)
152
171
  ensure
153
172
  File.delete(temp_path) if defined?(temp_path) && temp_path && File.exist?(temp_path)
154
173
  end
155
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
+
156
186
  private
157
187
 
158
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.0'
4
+ VERSION = '2.1.0'
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.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DebugBundle
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-25 00:00:00.000000000 Z
10
+ date: 2026-11-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -49,14 +49,30 @@ 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/framework_operation.rb
59
74
  - lib/debugbundle/logging.rb
75
+ - lib/debugbundle/operation_context.rb
60
76
  - lib/debugbundle/rack/middleware.rb
61
77
  - lib/debugbundle/rack/relay_middleware.rb
62
78
  - lib/debugbundle/rails.rb
@@ -64,6 +80,7 @@ files:
64
80
  - lib/debugbundle/rails/relay_endpoint.rb
65
81
  - lib/debugbundle/redaction.rb
66
82
  - lib/debugbundle/relay.rb
83
+ - lib/debugbundle/relay/forwarder.rb
67
84
  - lib/debugbundle/relay/handler.rb
68
85
  - lib/debugbundle/remote_config.rb
69
86
  - lib/debugbundle/runtime.rb