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.
- checksums.yaml +4 -4
- data/README.md +131 -1
- data/lib/debugbundle/before_send.rb +58 -30
- data/lib/debugbundle/bounded_http_response.rb +24 -0
- data/lib/debugbundle/canonical_event_schema.rb +137 -0
- data/lib/debugbundle/client.rb +50 -67
- data/lib/debugbundle/client_event_support.rb +11 -12
- data/lib/debugbundle/client_evidence_support.rb +169 -0
- data/lib/debugbundle/client_exception_support.rb +64 -0
- data/lib/debugbundle/client_process_support.rb +5 -12
- data/lib/debugbundle/client_queue_support.rb +59 -4
- data/lib/debugbundle/config.rb +15 -2
- data/lib/debugbundle/diagnostic_budget.rb +155 -0
- data/lib/debugbundle/event-envelope.schema.json +4904 -0
- data/lib/debugbundle/event_batch.rb +48 -0
- data/lib/debugbundle/evidence_delivery.rb +50 -0
- data/lib/debugbundle/evidence_graph.rb +73 -0
- data/lib/debugbundle/evidence_json.rb +55 -0
- data/lib/debugbundle/evidence_resources.rb +60 -0
- data/lib/debugbundle/evidence_value.rb +107 -0
- data/lib/debugbundle/evidence_wire.rb +193 -0
- data/lib/debugbundle/exception_evidence.rb +179 -0
- data/lib/debugbundle/exception_identity.rb +42 -0
- data/lib/debugbundle/framework_operation.rb +13 -0
- data/lib/debugbundle/logging.rb +2 -1
- data/lib/debugbundle/operation_context.rb +90 -0
- data/lib/debugbundle/rack/middleware.rb +24 -21
- data/lib/debugbundle/rack/relay_middleware.rb +4 -1
- data/lib/debugbundle/rails/railtie.rb +4 -1
- data/lib/debugbundle/rails/relay_endpoint.rb +2 -1
- data/lib/debugbundle/relay/forwarder.rb +83 -0
- data/lib/debugbundle/relay/handler.rb +83 -19
- data/lib/debugbundle/remote_config.rb +5 -0
- data/lib/debugbundle/sidekiq/server_middleware.rb +22 -18
- data/lib/debugbundle/telemetry_privacy.rb +43 -5
- data/lib/debugbundle/transport.rb +43 -17
- data/lib/debugbundle/version.rb +1 -1
- data/lib/debugbundle.rb +11 -1
- 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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
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
|
-
|
|
135
|
+
policy = key_policy(name, work)
|
|
136
|
+
next if policy == :omit
|
|
111
137
|
|
|
112
|
-
result[name] =
|
|
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 =
|
|
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 =
|
|
54
|
-
http.read_timeout =
|
|
55
|
-
|
|
56
|
-
response =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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 =
|
|
119
|
-
|
|
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)
|
data/lib/debugbundle/version.rb
CHANGED
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
|
-
|
|
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.
|
|
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-
|
|
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
|