debugbundle 1.2.0 → 1.3.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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/debugbundle/acknowledgement.rb +59 -0
- data/lib/debugbundle/before_send.rb +172 -0
- data/lib/debugbundle/client.rb +99 -225
- data/lib/debugbundle/client_event_support.rb +247 -0
- data/lib/debugbundle/config.rb +5 -2
- data/lib/debugbundle/rails.rb +4 -0
- data/lib/debugbundle/transport.rb +13 -3
- data/lib/debugbundle/version.rb +1 -1
- data/lib/debugbundle.rb +2 -0
- data/spec/before_send_spec.rb +74 -0
- data/spec/client_spec.rb +196 -0
- data/spec/remote_config_spec.rb +63 -385
- data/spec/spec_helper.rb +4 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c33eb05bc99ae435c838069ade718b123f897eae489769a3aca8969289cdd5b2
|
|
4
|
+
data.tar.gz: 13628e1335f3597e46c1457a20b8f242972e5da2f54971cf69c9f5fef08ecf3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c0e7b78a867a41fb326300ca20f816760cc43d4534109824c1ced09072a025eb54efaaceb3c16cc0f8d8ef686ecf065e99ee3a2c8b4f99ac50922d267184c25
|
|
7
|
+
data.tar.gz: 5d16ab5d4bddf01eb39fb65269c624d941ab5f9820b89e253c8c9131c528fff935f48d5d56f14b06a1e8d015a173f7412c937ba37476583eb1c32cd367643b21
|
data/README.md
CHANGED
|
@@ -314,7 +314,7 @@ This repository also ships a clean-install app-driven smoke harness that validat
|
|
|
314
314
|
|
|
315
315
|
```sh
|
|
316
316
|
make smoke
|
|
317
|
-
make smoke-published VERSION=1.
|
|
317
|
+
make smoke-published VERSION=1.3.0
|
|
318
318
|
```
|
|
319
319
|
|
|
320
320
|
`make smoke` builds the gem, installs it into a fresh RubyGems home, drives a Rack request plus a browser relay batch through the public SDK surface, validates event envelope shape, and confirms the mock ingestion endpoint receives the expected service, environment, SDK metadata, and correlation fields.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DebugBundle
|
|
4
|
+
module Acknowledgement
|
|
5
|
+
RETRYABLE_REASONS = %w[
|
|
6
|
+
rate_limited
|
|
7
|
+
monthly_quota_exceeded
|
|
8
|
+
analytics_quota_exceeded
|
|
9
|
+
].freeze
|
|
10
|
+
FIELDS = %w[accepted rejected errors].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def decide(body, batch_length)
|
|
15
|
+
return { kind: :legacy } unless body.is_a?(Hash) && FIELDS.any? { |field| body.key?(field) }
|
|
16
|
+
return protocol_failure unless FIELDS.all? { |field| body.key?(field) }
|
|
17
|
+
|
|
18
|
+
accepted = body['accepted']
|
|
19
|
+
rejected = body['rejected']
|
|
20
|
+
errors = body['errors']
|
|
21
|
+
return protocol_failure unless count?(accepted) && count?(rejected) && errors.is_a?(Array)
|
|
22
|
+
return protocol_failure unless accepted + rejected == batch_length && errors.length == rejected
|
|
23
|
+
|
|
24
|
+
seen = {}
|
|
25
|
+
retryable_indices = []
|
|
26
|
+
terminal_errors = []
|
|
27
|
+
errors.each do |error|
|
|
28
|
+
return protocol_failure unless error.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
index = error['index']
|
|
31
|
+
reason = error['reason']
|
|
32
|
+
return protocol_failure unless index.is_a?(Integer) && index.between?(0, batch_length - 1)
|
|
33
|
+
return protocol_failure unless reason.is_a?(String) && !reason.empty? && !seen[index]
|
|
34
|
+
|
|
35
|
+
seen[index] = true
|
|
36
|
+
if RETRYABLE_REASONS.include?(reason)
|
|
37
|
+
retryable_indices << index
|
|
38
|
+
else
|
|
39
|
+
terminal_errors << [index, reason]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
kind: :acknowledged,
|
|
45
|
+
accepted: accepted,
|
|
46
|
+
retryable_indices: retryable_indices,
|
|
47
|
+
terminal_errors: terminal_errors
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def count?(value)
|
|
52
|
+
value.is_a?(Integer) && value >= 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def protocol_failure
|
|
56
|
+
{ kind: :protocol_failure }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module DebugBundle
|
|
6
|
+
module BeforeSend
|
|
7
|
+
REQUIRED_PAYLOAD_FIELDS = {
|
|
8
|
+
'backend_exception' => %w[name message stack handled request response runtime],
|
|
9
|
+
'request_event' => %w[method path query headers response_status duration_ms],
|
|
10
|
+
'log_event' => %w[level message attributes],
|
|
11
|
+
'frontend_breadcrumb' => %w[breadcrumb_type data],
|
|
12
|
+
'frontend_exception' => %w[name message stack],
|
|
13
|
+
'deploy_metadata' => %w[commit_sha version branch environment deployed_at],
|
|
14
|
+
'error_suppressed' => %w[fingerprint suppressed_count window_seconds first_seen last_seen],
|
|
15
|
+
'probe_event' => %w[label data activation_id probe_label_pattern]
|
|
16
|
+
}.freeze
|
|
17
|
+
ALLOWED_PAYLOAD_FIELDS = {
|
|
18
|
+
'backend_exception' => %w[name message stack handled request response runtime probe_data],
|
|
19
|
+
'request_event' => %w[
|
|
20
|
+
method path query headers body response_status duration_ms route_template
|
|
21
|
+
response_headers response_body device
|
|
22
|
+
],
|
|
23
|
+
'log_event' => %w[level message attributes device],
|
|
24
|
+
'frontend_breadcrumb' => %w[breadcrumb_type route data device],
|
|
25
|
+
'frontend_exception' => %w[
|
|
26
|
+
name message stack route browser breadcrumbs device browser_event
|
|
27
|
+
rejection_reason dom_context probe_data
|
|
28
|
+
],
|
|
29
|
+
'deploy_metadata' => %w[commit_sha version branch environment deployed_at],
|
|
30
|
+
'error_suppressed' => %w[fingerprint suppressed_count window_seconds first_seen last_seen device],
|
|
31
|
+
'probe_event' => %w[label data activation_id probe_label_pattern device]
|
|
32
|
+
}.freeze
|
|
33
|
+
ROOT_FIELDS = %w[
|
|
34
|
+
schema_version event_id event_type project_token project_id sdk_name sdk_version
|
|
35
|
+
service occurred_at correlation context payload
|
|
36
|
+
].freeze
|
|
37
|
+
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
def apply(event, hook)
|
|
42
|
+
return event unless hook
|
|
43
|
+
|
|
44
|
+
result = hook.call(Marshal.load(Marshal.dump(event)))
|
|
45
|
+
return nil if result.nil?
|
|
46
|
+
|
|
47
|
+
valid?(result) ? result : event
|
|
48
|
+
rescue StandardError
|
|
49
|
+
event
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def valid?(event)
|
|
53
|
+
return false unless event.is_a?(Hash)
|
|
54
|
+
return false unless (event.keys - ROOT_FIELDS).empty?
|
|
55
|
+
return false unless %w[schema_version event_id event_type occurred_at sdk_name sdk_version].all? do |field|
|
|
56
|
+
event[field].is_a?(String) && !event[field].empty?
|
|
57
|
+
end
|
|
58
|
+
return false unless UUID_PATTERN.match?(event['event_id'])
|
|
59
|
+
|
|
60
|
+
Time.iso8601(event['occurred_at'])
|
|
61
|
+
service = event['service']
|
|
62
|
+
payload = event['payload']
|
|
63
|
+
fields = REQUIRED_PAYLOAD_FIELDS[event['event_type']]
|
|
64
|
+
allowed_fields = ALLOWED_PAYLOAD_FIELDS[event['event_type']]
|
|
65
|
+
service.is_a?(Hash) &&
|
|
66
|
+
service['name'].is_a?(String) && !service['name'].empty? &&
|
|
67
|
+
service['environment'].is_a?(String) && !service['environment'].empty? &&
|
|
68
|
+
payload.is_a?(Hash) &&
|
|
69
|
+
fields &&
|
|
70
|
+
allowed_fields &&
|
|
71
|
+
(payload.keys - allowed_fields).empty? &&
|
|
72
|
+
fields.all? { |field| payload.key?(field) } &&
|
|
73
|
+
valid_payload_shape?(event['event_type'], payload)
|
|
74
|
+
rescue ArgumentError
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def valid_payload_shape?(event_type, payload)
|
|
79
|
+
case event_type
|
|
80
|
+
when 'backend_exception'
|
|
81
|
+
valid_backend_exception?(payload)
|
|
82
|
+
when 'request_event'
|
|
83
|
+
valid_request_event?(payload)
|
|
84
|
+
when 'log_event'
|
|
85
|
+
non_empty_strings?(payload, 'level', 'message') && payload['attributes'].is_a?(Hash)
|
|
86
|
+
when 'frontend_breadcrumb'
|
|
87
|
+
non_empty_strings?(payload, 'breadcrumb_type') && payload['data'].is_a?(Hash)
|
|
88
|
+
when 'frontend_exception'
|
|
89
|
+
valid_frontend_exception?(payload)
|
|
90
|
+
when 'deploy_metadata'
|
|
91
|
+
valid_deploy_metadata?(payload)
|
|
92
|
+
when 'error_suppressed'
|
|
93
|
+
valid_error_suppressed?(payload)
|
|
94
|
+
when 'probe_event'
|
|
95
|
+
valid_probe_event?(payload)
|
|
96
|
+
else
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def valid_backend_exception?(payload)
|
|
102
|
+
non_empty_strings?(payload, 'name', 'message', 'stack') &&
|
|
103
|
+
[true, false].include?(payload['handled']) &&
|
|
104
|
+
%w[request response runtime].all? { |field| payload[field].is_a?(Hash) } &&
|
|
105
|
+
optional_hash?(payload, 'probe_data')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def valid_request_event?(payload)
|
|
109
|
+
non_empty_strings?(payload, 'method', 'path') &&
|
|
110
|
+
payload['query'].is_a?(Hash) &&
|
|
111
|
+
payload['headers'].is_a?(Hash) &&
|
|
112
|
+
non_negative_number?(payload['response_status']) &&
|
|
113
|
+
non_negative_number?(payload['duration_ms']) &&
|
|
114
|
+
optional_hash?(payload, 'response_headers')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def valid_frontend_exception?(payload)
|
|
118
|
+
non_empty_strings?(payload, 'name', 'message', 'stack') &&
|
|
119
|
+
(!payload.key?('breadcrumbs') || payload['breadcrumbs'].is_a?(Array)) &&
|
|
120
|
+
optional_hash?(payload, 'probe_data')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def valid_deploy_metadata?(payload)
|
|
124
|
+
non_empty_strings?(payload, 'commit_sha', 'version', 'branch', 'environment') &&
|
|
125
|
+
timestamp?(payload['deployed_at'])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def valid_error_suppressed?(payload)
|
|
129
|
+
non_empty_strings?(payload, 'fingerprint') &&
|
|
130
|
+
non_negative_integer?(payload['suppressed_count']) &&
|
|
131
|
+
positive_integer?(payload['window_seconds']) &&
|
|
132
|
+
timestamp?(payload['first_seen']) &&
|
|
133
|
+
timestamp?(payload['last_seen'])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def valid_probe_event?(payload)
|
|
137
|
+
non_empty_strings?(payload, 'label', 'probe_label_pattern') &&
|
|
138
|
+
payload['data'].is_a?(Hash) &&
|
|
139
|
+
nullable_uuid?(payload['activation_id'])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def non_empty_strings?(payload, *fields)
|
|
143
|
+
fields.all? { |field| payload[field].is_a?(String) && !payload[field].strip.empty? }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def optional_hash?(payload, field)
|
|
147
|
+
!payload.key?(field) || payload[field].is_a?(Hash)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def non_negative_number?(value)
|
|
151
|
+
value.is_a?(Numeric) && value.finite? && value >= 0
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def non_negative_integer?(value)
|
|
155
|
+
value.is_a?(Integer) && value >= 0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def positive_integer?(value)
|
|
159
|
+
value.is_a?(Integer) && value.positive?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def timestamp?(value)
|
|
163
|
+
value.is_a?(String) && Time.iso8601(value)
|
|
164
|
+
rescue ArgumentError
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def nullable_uuid?(value)
|
|
169
|
+
value.nil? || (value.is_a?(String) && UUID_PATTERN.match?(value))
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
data/lib/debugbundle/client.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'digest'
|
|
|
4
4
|
require 'time'
|
|
5
5
|
require 'uri'
|
|
6
6
|
|
|
7
|
+
require 'debugbundle/client_event_support'
|
|
7
8
|
require 'debugbundle/runtime'
|
|
8
9
|
|
|
9
10
|
module DebugBundle
|
|
@@ -96,6 +97,7 @@ module DebugBundle
|
|
|
96
97
|
@last_event_at = nil
|
|
97
98
|
@retry_at = nil
|
|
98
99
|
@consecutive_failures = 0
|
|
100
|
+
@acknowledgement_state = nil
|
|
99
101
|
@at_exit_registered = false
|
|
100
102
|
@thread_exception_registered = false
|
|
101
103
|
@logger_bindings = {}
|
|
@@ -110,6 +112,10 @@ module DebugBundle
|
|
|
110
112
|
end
|
|
111
113
|
|
|
112
114
|
def capture_exception(error, context: nil, handled: true)
|
|
115
|
+
capture_exception_internal(error, context: context, handled: handled, run_before_send: true)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def capture_exception_internal(error, context:, handled:, run_before_send:)
|
|
113
119
|
return unless capture_enabled?
|
|
114
120
|
|
|
115
121
|
poll_remote_config_if_due!
|
|
@@ -133,11 +139,22 @@ module DebugBundle
|
|
|
133
139
|
extra_context = merged_context.except('request', 'response', 'correlation')
|
|
134
140
|
extra_context['causes'] = causes unless causes.empty?
|
|
135
141
|
|
|
136
|
-
|
|
142
|
+
event = base_event('backend_exception', payload, extra_context)
|
|
143
|
+
event = apply_before_send(event) if run_before_send
|
|
144
|
+
return if event.nil?
|
|
145
|
+
|
|
146
|
+
event_payload = event.fetch('payload')
|
|
147
|
+
suppression_key = [
|
|
148
|
+
event['event_type'],
|
|
149
|
+
event_payload['name'],
|
|
150
|
+
event_payload['message'],
|
|
151
|
+
event_payload['stack']
|
|
152
|
+
].join(':')
|
|
137
153
|
return unless @suppression.should_capture(suppression_key, now: monotonic_now)
|
|
138
154
|
|
|
139
|
-
enqueue_event(
|
|
155
|
+
enqueue_event(event)
|
|
140
156
|
end
|
|
157
|
+
private :capture_exception_internal
|
|
141
158
|
|
|
142
159
|
def capture_error(error, context: nil, handled: true) = capture_exception(error, context: context, handled: handled)
|
|
143
160
|
|
|
@@ -147,7 +164,6 @@ module DebugBundle
|
|
|
147
164
|
poll_remote_config_if_due!
|
|
148
165
|
|
|
149
166
|
normalized_level = normalize_level(level || :warning)
|
|
150
|
-
return unless level_enabled?(normalized_level)
|
|
151
167
|
|
|
152
168
|
merged_context = merge_context(context)
|
|
153
169
|
payload = {
|
|
@@ -155,7 +171,10 @@ module DebugBundle
|
|
|
155
171
|
'message' => message.to_s,
|
|
156
172
|
'attributes' => merged_context
|
|
157
173
|
}
|
|
158
|
-
|
|
174
|
+
event = apply_before_send(base_event('log_event', payload, merged_context))
|
|
175
|
+
return if event.nil? || !level_enabled?(normalized_level)
|
|
176
|
+
|
|
177
|
+
enqueue_event(event)
|
|
159
178
|
end
|
|
160
179
|
|
|
161
180
|
def capture_request(request, response, context: nil)
|
|
@@ -167,7 +186,6 @@ module DebugBundle
|
|
|
167
186
|
sanitized_request = request_payload(request)
|
|
168
187
|
sanitized_response = response_payload(response)
|
|
169
188
|
response_status = (sanitized_response['status_code'] || 0).to_i
|
|
170
|
-
return unless capture_request_event?(response_status, sanitized_request)
|
|
171
189
|
|
|
172
190
|
payload = {
|
|
173
191
|
'method' => sanitized_request['method'],
|
|
@@ -181,7 +199,12 @@ module DebugBundle
|
|
|
181
199
|
'response_headers' => sanitized_response['headers'],
|
|
182
200
|
'response_body' => sanitized_response['body']
|
|
183
201
|
}
|
|
184
|
-
|
|
202
|
+
event = apply_before_send(
|
|
203
|
+
base_event('request_event', payload, merged_context.merge('request' => sanitized_request))
|
|
204
|
+
)
|
|
205
|
+
return if event.nil? || !capture_request_event?(response_status, sanitized_request)
|
|
206
|
+
|
|
207
|
+
enqueue_event(event)
|
|
185
208
|
end
|
|
186
209
|
|
|
187
210
|
def capture_message(message, level: nil, context: nil)
|
|
@@ -203,18 +226,22 @@ module DebugBundle
|
|
|
203
226
|
if heavy
|
|
204
227
|
return if matching_directives.empty?
|
|
205
228
|
|
|
206
|
-
raw_value =
|
|
207
|
-
|
|
229
|
+
resolved, raw_value = resolve_probe_value(data, block)
|
|
230
|
+
return unless resolved
|
|
231
|
+
|
|
232
|
+
emit_probe_events(label.to_s, normalize_probe_data(raw_value), matching_directives)
|
|
208
233
|
return
|
|
209
234
|
end
|
|
210
235
|
|
|
211
236
|
return if !@probe_buffers.key?(label) && @probe_buffers.size >= config.max_probe_labels
|
|
212
237
|
|
|
213
|
-
raw_value =
|
|
238
|
+
resolved, raw_value = resolve_probe_value(data, block)
|
|
239
|
+
return unless resolved
|
|
240
|
+
|
|
214
241
|
entry = {
|
|
215
242
|
'label' => label.to_s,
|
|
216
|
-
'data' =>
|
|
217
|
-
'
|
|
243
|
+
'data' => normalize_probe_data(raw_value),
|
|
244
|
+
'timestamp' => now.iso8601
|
|
218
245
|
}
|
|
219
246
|
|
|
220
247
|
bucket = (@probe_buffers[label.to_s] ||= [])
|
|
@@ -240,7 +267,13 @@ module DebugBundle
|
|
|
240
267
|
error = $ERROR_INFO
|
|
241
268
|
next unless error.is_a?(Exception)
|
|
242
269
|
|
|
243
|
-
client.
|
|
270
|
+
client.__send__(
|
|
271
|
+
:capture_exception_internal,
|
|
272
|
+
error,
|
|
273
|
+
context: nil,
|
|
274
|
+
handled: false,
|
|
275
|
+
run_before_send: false
|
|
276
|
+
)
|
|
244
277
|
client.flush
|
|
245
278
|
end
|
|
246
279
|
true
|
|
@@ -329,11 +362,7 @@ module DebugBundle
|
|
|
329
362
|
|
|
330
363
|
case result.status_code
|
|
331
364
|
when 200..299
|
|
332
|
-
|
|
333
|
-
@retry_at = nil
|
|
334
|
-
@consecutive_failures = 0
|
|
335
|
-
@last_event_at = now
|
|
336
|
-
true
|
|
365
|
+
handle_successful_result(result, batch)
|
|
337
366
|
when 429
|
|
338
367
|
@consecutive_failures += 1
|
|
339
368
|
retry_after_seconds = (result.retry_after_seconds || 1).clamp(1, RETRY_AFTER_CAP_SECONDS)
|
|
@@ -358,6 +387,7 @@ module DebugBundle
|
|
|
358
387
|
def status
|
|
359
388
|
return :disconnected unless config.enabled?
|
|
360
389
|
return :degraded unless config.configured?
|
|
390
|
+
return @acknowledgement_state if @acknowledgement_state
|
|
361
391
|
return :disconnected if @consecutive_failures >= 3
|
|
362
392
|
return :degraded if rate_limited?
|
|
363
393
|
|
|
@@ -368,6 +398,48 @@ module DebugBundle
|
|
|
368
398
|
|
|
369
399
|
private
|
|
370
400
|
|
|
401
|
+
def handle_successful_result(result, batch)
|
|
402
|
+
decision = Acknowledgement.decide(result.body, batch.length)
|
|
403
|
+
return handle_protocol_failure if decision[:kind] == :protocol_failure
|
|
404
|
+
|
|
405
|
+
if decision[:kind] == :legacy
|
|
406
|
+
remove_buffered_events(batch)
|
|
407
|
+
record_success
|
|
408
|
+
return true
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
retryable_indices = decision.fetch(:retryable_indices)
|
|
412
|
+
retryable_events = retryable_indices.map { |index| batch.fetch(index) }
|
|
413
|
+
remove_buffered_events(batch - retryable_events)
|
|
414
|
+
@last_event_at = now if decision.fetch(:accepted).positive?
|
|
415
|
+
|
|
416
|
+
if retryable_events.any?
|
|
417
|
+
@consecutive_failures += 1
|
|
418
|
+
@retry_at = now + 1
|
|
419
|
+
@acknowledgement_state = :degraded
|
|
420
|
+
false
|
|
421
|
+
else
|
|
422
|
+
@retry_at = nil
|
|
423
|
+
@consecutive_failures = 0
|
|
424
|
+
@acknowledgement_state = decision.fetch(:accepted).positive? ? nil : :disconnected
|
|
425
|
+
decision.fetch(:accepted).positive?
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def handle_protocol_failure
|
|
430
|
+
@consecutive_failures += 1
|
|
431
|
+
@retry_at = now + 1
|
|
432
|
+
@acknowledgement_state = :degraded
|
|
433
|
+
false
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def record_success
|
|
437
|
+
@retry_at = nil
|
|
438
|
+
@consecutive_failures = 0
|
|
439
|
+
@acknowledgement_state = nil
|
|
440
|
+
@last_event_at = now
|
|
441
|
+
end
|
|
442
|
+
|
|
371
443
|
def build_default_transport
|
|
372
444
|
return nil unless config.enabled?
|
|
373
445
|
|
|
@@ -391,204 +463,6 @@ module DebugBundle
|
|
|
391
463
|
)
|
|
392
464
|
end
|
|
393
465
|
|
|
394
|
-
def capture_enabled? = config.enabled? && config.configured?
|
|
395
|
-
|
|
396
|
-
def merge_context(context)
|
|
397
|
-
merged = @context.merge(stringify_hash(context || {}))
|
|
398
|
-
@redactor.redact_value(merged)
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def stringify_hash(value)
|
|
402
|
-
return {} unless value.is_a?(Hash)
|
|
403
|
-
|
|
404
|
-
value.each_with_object({}) do |(key, nested_value), result|
|
|
405
|
-
result[key.to_s] = nested_value
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def request_payload(request)
|
|
410
|
-
source = object_to_hash(request)
|
|
411
|
-
{
|
|
412
|
-
'method' => source['method'] || 'UNKNOWN',
|
|
413
|
-
'path' => source['path'] || '/',
|
|
414
|
-
'query' => @redactor.redact_value(source['query'] || {}),
|
|
415
|
-
'headers' => sanitized_headers(source['headers'] || {}),
|
|
416
|
-
'body' => @redactor.redact_value(source['body'] || {})
|
|
417
|
-
}
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def response_payload(response)
|
|
421
|
-
source = object_to_hash(response)
|
|
422
|
-
{
|
|
423
|
-
'status_code' => source['status_code'] || source['status'] || 0,
|
|
424
|
-
'headers' => sanitized_headers(source['headers'] || {}),
|
|
425
|
-
'body' => @redactor.redact_value(source['body'] || {})
|
|
426
|
-
}
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def runtime_payload = Runtime.payload
|
|
430
|
-
|
|
431
|
-
def exception_causes(error)
|
|
432
|
-
causes = []
|
|
433
|
-
current = error.cause
|
|
434
|
-
|
|
435
|
-
while current
|
|
436
|
-
causes << {
|
|
437
|
-
'name' => current.class.name,
|
|
438
|
-
'message' => current.message.to_s,
|
|
439
|
-
'stack' => Array(current.backtrace).join("\n")
|
|
440
|
-
}
|
|
441
|
-
current = current.cause
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
causes
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
def probe_snapshot
|
|
448
|
-
items = @probe_buffers.values.flatten.map do |entry|
|
|
449
|
-
entry.merge('activation_id' => nil)
|
|
450
|
-
end
|
|
451
|
-
return {} if items.empty?
|
|
452
|
-
|
|
453
|
-
{ 'version' => 1, 'items' => items }
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def enqueue_event(event)
|
|
457
|
-
return unless sampled_in?
|
|
458
|
-
|
|
459
|
-
@buffer_mutex.synchronize do
|
|
460
|
-
@buffer << event
|
|
461
|
-
@buffer.shift while @buffer.length > MAX_BUFFER_SIZE
|
|
462
|
-
end
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
def buffered_batch
|
|
466
|
-
@buffer_mutex.synchronize { @buffer.dup }
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
def remove_buffered_events(events)
|
|
470
|
-
event_ids = events.map { |event| event['event_id'] }
|
|
471
|
-
@buffer_mutex.synchronize do
|
|
472
|
-
@buffer.reject! { |event| event_ids.include?(event['event_id']) }
|
|
473
|
-
end
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def sampled_in?
|
|
477
|
-
return false if config.sample_rate <= 0.0
|
|
478
|
-
return true if config.sample_rate >= 1.0
|
|
479
|
-
|
|
480
|
-
@random_provider.call.to_f < config.sample_rate
|
|
481
|
-
rescue StandardError
|
|
482
|
-
true
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def append_suppression_aggregates
|
|
486
|
-
@suppression.drain_aggregates(now: monotonic_now).each do |aggregate|
|
|
487
|
-
enqueue_event(base_event('error_suppressed', aggregate, {}))
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def base_event(event_type, payload, context)
|
|
492
|
-
event = {
|
|
493
|
-
'schema_version' => SCHEMA_VERSION,
|
|
494
|
-
'event_id' => SecureRandom.uuid,
|
|
495
|
-
'event_type' => event_type,
|
|
496
|
-
'project_token' => config.project_token,
|
|
497
|
-
'sdk_name' => SDK_NAME,
|
|
498
|
-
'sdk_version' => DebugBundle::VERSION,
|
|
499
|
-
'service' => {
|
|
500
|
-
'name' => service_name,
|
|
501
|
-
'runtime' => 'ruby',
|
|
502
|
-
'framework' => context['framework'],
|
|
503
|
-
'environment' => environment_name
|
|
504
|
-
},
|
|
505
|
-
'occurred_at' => now.iso8601,
|
|
506
|
-
'correlation' => correlation_payload(context),
|
|
507
|
-
'payload' => @redactor.redact_value(payload)
|
|
508
|
-
}
|
|
509
|
-
envelope_context = event_context(context)
|
|
510
|
-
event['context'] = envelope_context unless envelope_context.empty?
|
|
511
|
-
event
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
def service_name = config.service || DEFAULT_SERVICE_NAME
|
|
515
|
-
|
|
516
|
-
def environment_name = config.environment || DEFAULT_ENVIRONMENT
|
|
517
|
-
|
|
518
|
-
def correlation_payload(context)
|
|
519
|
-
request = object_to_hash(context['request'])
|
|
520
|
-
correlation = object_to_hash(context['correlation'])
|
|
521
|
-
{
|
|
522
|
-
'request_id' => correlation['request_id'] || request['request_id'] || context['request_id'],
|
|
523
|
-
'trace_id' => correlation['trace_id'] || request['trace_id'] || context['trace_id'],
|
|
524
|
-
'session_id' => correlation['session_id'] || context['session_id'],
|
|
525
|
-
'user_id_hash' => correlation['user_id_hash'] || context['user_id_hash']
|
|
526
|
-
}
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
def event_context(context)
|
|
530
|
-
object_to_hash(context).except(
|
|
531
|
-
'request',
|
|
532
|
-
'response',
|
|
533
|
-
'correlation',
|
|
534
|
-
'request_id',
|
|
535
|
-
'trace_id',
|
|
536
|
-
'session_id',
|
|
537
|
-
'user_id_hash'
|
|
538
|
-
)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def object_to_hash(value)
|
|
542
|
-
case value
|
|
543
|
-
when Hash
|
|
544
|
-
stringify_hash(value)
|
|
545
|
-
else
|
|
546
|
-
if value.respond_to?(:to_h)
|
|
547
|
-
stringify_hash(value.to_h)
|
|
548
|
-
elsif value.respond_to?(:to_hash)
|
|
549
|
-
stringify_hash(value.to_hash)
|
|
550
|
-
else
|
|
551
|
-
{}
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
rescue StandardError
|
|
555
|
-
{}
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
def sanitized_headers(headers)
|
|
559
|
-
stringify_hash(headers).each_with_object({}) do |(key, value), result|
|
|
560
|
-
normalized_key = key.to_s.downcase
|
|
561
|
-
next unless DEFAULT_HEADER_ALLOWLIST.include?(normalized_key)
|
|
562
|
-
|
|
563
|
-
result[normalized_key] = @redactor.redact_value(value)
|
|
564
|
-
end
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
def normalize_level(level)
|
|
568
|
-
candidate = level.to_s.strip.downcase.to_sym
|
|
569
|
-
return candidate if LOG_LEVEL_RANKS.key?(candidate)
|
|
570
|
-
|
|
571
|
-
:warning
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
def level_enabled?(level)
|
|
575
|
-
threshold = [normalize_level(config.log_level), policy_log_level].max_by { |entry| LOG_LEVEL_RANKS.fetch(entry) }
|
|
576
|
-
LOG_LEVEL_RANKS.fetch(level) >= LOG_LEVEL_RANKS.fetch(threshold)
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
def policy_log_level
|
|
580
|
-
case @capture_policy.capture_logs
|
|
581
|
-
when 'off'
|
|
582
|
-
:fatal
|
|
583
|
-
when 'error'
|
|
584
|
-
:error
|
|
585
|
-
when 'info'
|
|
586
|
-
:info
|
|
587
|
-
else
|
|
588
|
-
:warning
|
|
589
|
-
end
|
|
590
|
-
end
|
|
591
|
-
|
|
592
466
|
def capture_request_event?(status_code, request)
|
|
593
467
|
mode = @capture_policy.capture_request_events
|
|
594
468
|
|
|
@@ -667,15 +541,12 @@ module DebugBundle
|
|
|
667
541
|
end
|
|
668
542
|
|
|
669
543
|
def emit_probe_events(label, data, matching_directives)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
allowed_directives.each do |directive|
|
|
678
|
-
enqueue_event(
|
|
544
|
+
request_directives = matching_request_trigger_directives(label)
|
|
545
|
+
candidate_directives = (matching_directives + request_directives).uniq(&:id)
|
|
546
|
+
return if candidate_directives.empty?
|
|
547
|
+
|
|
548
|
+
candidate_directives.each do |directive|
|
|
549
|
+
event = apply_before_send(
|
|
679
550
|
base_event(
|
|
680
551
|
'probe_event',
|
|
681
552
|
{
|
|
@@ -687,6 +558,9 @@ module DebugBundle
|
|
|
687
558
|
{}
|
|
688
559
|
)
|
|
689
560
|
)
|
|
561
|
+
allowed = request_directives.include?(directive) ||
|
|
562
|
+
@capture_policy.capture_probe_events == 'standalone_when_activated'
|
|
563
|
+
enqueue_event(event) if event && allowed
|
|
690
564
|
end
|
|
691
565
|
end
|
|
692
566
|
|
|
@@ -729,7 +603,7 @@ module DebugBundle
|
|
|
729
603
|
end
|
|
730
604
|
|
|
731
605
|
def capture_thread_exception(error)
|
|
732
|
-
|
|
606
|
+
capture_exception_internal(error, context: nil, handled: false, run_before_send: false)
|
|
733
607
|
flush
|
|
734
608
|
rescue StandardError
|
|
735
609
|
nil
|