debugbundle 2.0.1 → 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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +127 -0
  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 +41 -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 +54 -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/framework_operation.rb +13 -0
  24. data/lib/debugbundle/operation_context.rb +90 -0
  25. data/lib/debugbundle/rack/middleware.rb +24 -21
  26. data/lib/debugbundle/rack/relay_middleware.rb +4 -1
  27. data/lib/debugbundle/rails/railtie.rb +4 -1
  28. data/lib/debugbundle/rails/relay_endpoint.rb +2 -1
  29. data/lib/debugbundle/relay/forwarder.rb +83 -0
  30. data/lib/debugbundle/relay/handler.rb +83 -19
  31. data/lib/debugbundle/remote_config.rb +5 -0
  32. data/lib/debugbundle/sidekiq/server_middleware.rb +22 -18
  33. data/lib/debugbundle/telemetry_privacy.rb +9 -1
  34. data/lib/debugbundle/transport.rb +43 -17
  35. data/lib/debugbundle/version.rb +1 -1
  36. data/lib/debugbundle.rb +11 -1
  37. 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
 
@@ -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.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.1
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-26 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