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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'debugbundle/evidence_wire'
4
+
3
5
  module DebugBundle
4
6
  class Config
5
7
  DEFAULT_ENDPOINT = 'https://api.debugbundle.com/v1/events'
@@ -40,7 +42,10 @@ module DebugBundle
40
42
  :max_probe_entries_per_label,
41
43
  :probe_flush_on_error,
42
44
  :probes_poll_interval,
43
- :before_send
45
+ :before_send,
46
+ :capture_exception_evidence,
47
+ :capture_recovery_observations,
48
+ :evidence_capabilities
44
49
 
45
50
  def initialize(
46
51
  project_token: nil,
@@ -63,7 +68,10 @@ module DebugBundle
63
68
  max_probe_entries_per_label: DEFAULT_MAX_PROBE_ENTRIES_PER_LABEL,
64
69
  probe_flush_on_error: DEFAULT_PROBE_FLUSH_ON_ERROR,
65
70
  probes_poll_interval: DEFAULT_PROBES_POLL_INTERVAL,
66
- before_send: nil
71
+ before_send: nil,
72
+ capture_exception_evidence: true,
73
+ capture_recovery_observations: false,
74
+ evidence_capabilities: nil
67
75
  )
68
76
  @project_token = project_token
69
77
  @enabled = enabled
@@ -92,6 +100,11 @@ module DebugBundle
92
100
  @probe_flush_on_error = probe_flush_on_error
93
101
  @probes_poll_interval = normalize_positive_number(probes_poll_interval, DEFAULT_PROBES_POLL_INTERVAL)
94
102
  @before_send = before_send.respond_to?(:call) ? before_send : nil
103
+ @capture_exception_evidence = !false.equal?(capture_exception_evidence)
104
+ @capture_recovery_observations = true.equal?(capture_recovery_observations)
105
+ @evidence_capabilities = EvidenceWire.descriptor(EvidenceValue.options(evidence_capabilities))
106
+ @evidence_capabilities['features'].freeze
107
+ @evidence_capabilities.freeze
95
108
  freeze
96
109
  end
97
110
 
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module DebugBundle
6
+ # Caller holds the destination's buffer mutex. Metadata and pending serialized
7
+ # ownership share a 64 KiB budget; ACK/retry cannot renew an expired window.
8
+ class DiagnosticBudget
9
+ def initialize
10
+ @windows = {}
11
+ @pending = {}
12
+ @held = 0
13
+ end
14
+
15
+ def accounted_bytes = @held + @windows.values.sum { |window| window[:cost] }
16
+
17
+ def key(payload)
18
+ scope = payload.fetch('scope')
19
+ JSON.generate([payload.fetch('failure_ref'), scope.fetch('kind'), scope['operation_id'] || scope['page_id']])
20
+ end
21
+
22
+ def metadata_cost(scope, window) = JSON.generate([scope, window.except(:cost)]).bytesize
23
+
24
+ def prune(now)
25
+ @windows.delete_if { |_key, window| window[:pending].zero? && now >= window[:starts] + 300 }
26
+ end
27
+
28
+ def window(now)
29
+ { starts: now, failure_at: nil, failure_id: nil, attempts: [], observations: 0,
30
+ resource: false, ambiguous: false, pending: 0, cost: 0 }
31
+ end
32
+
33
+ def register_failure(payload, event_id, now)
34
+ return false unless now.is_a?(Numeric) && now.finite?
35
+
36
+ prune(now)
37
+ scope = key(payload)
38
+ previous = @windows[scope]
39
+ if previous && previous[:failure_id] && previous[:failure_id] != event_id
40
+ previous[:ambiguous] = true
41
+ previous[:cost] = metadata_cost(scope, previous)
42
+ return false
43
+ end
44
+ return false if previous.nil? && @windows.length >= 50
45
+
46
+ value = previous ? previous.dup : window(now)
47
+ value[:failure_id] = event_id
48
+ value[:failure_at] ||= now
49
+ store(scope, previous, value)
50
+ end
51
+
52
+ def store(scope, previous, value, extra = 0)
53
+ value[:cost] = metadata_cost(scope, value)
54
+ return false if accounted_bytes - (previous ? previous[:cost] : 0) + value[:cost] + extra > 65_536
55
+
56
+ @windows[scope] = value
57
+ !value[:ambiguous]
58
+ end
59
+
60
+ def reserve(event_id, payload, bytes, now)
61
+ return false unless valid_size?(bytes) && now.is_a?(Numeric) && now.finite? && !@pending.key?(event_id)
62
+
63
+ prune(now)
64
+ scope = key(payload)
65
+ previous = @windows[scope]
66
+ return false if (previous.nil? && @windows.length >= 50) || (previous && previous[:ambiguous])
67
+
68
+ value = previous ? previous.merge(attempts: previous[:attempts].dup) : window(now)
69
+ return false if now < value[:starts] || now > (value[:failure_at] || value[:starts]) + 300
70
+ return false if value[:observations] >= 20 || !admit_kind?(value, payload, now)
71
+
72
+ value[:pending] += 1
73
+ value[:observations] += 1
74
+ cost = bytes + event_id.bytesize + scope.bytesize + 64
75
+ return false unless store(scope, previous, value, cost)
76
+
77
+ @pending[event_id] =
78
+ { scope: scope, kind: payload['kind'], attempt: payload['attempt_id'], size: bytes, cost: cost }
79
+ @held += cost
80
+ true
81
+ end
82
+
83
+ def admit_kind?(value, payload, now)
84
+ if payload['kind'] == 'recovery'
85
+ attempt = payload['attempt_id']
86
+ return false if !value[:attempts].include?(attempt) && value[:attempts].length >= 5
87
+
88
+ value[:attempts] |= [attempt]
89
+ else
90
+ return false if value[:resource] || value[:failure_id] != payload['source_event_id']
91
+ return false unless value[:failure_at] && now <= value[:failure_at] + 30
92
+
93
+ value[:resource] = true
94
+ end
95
+ true
96
+ end
97
+
98
+ def replace(original_id, event_id, payload, bytes, now)
99
+ entry = @pending[original_id]
100
+ same_scope = entry && entry[:scope] == key(payload)
101
+ unless same_scope && entry[:kind] == payload['kind'] && entry[:attempt] == payload['attempt_id']
102
+ release([original_id])
103
+ return reserve(event_id, payload, bytes, now)
104
+ end
105
+ if event_id != original_id
106
+ if @pending.key?(event_id)
107
+ release([original_id])
108
+ return false
109
+ end
110
+ @pending[event_id] = @pending.delete(original_id)
111
+ delta = event_id.bytesize - original_id.bytesize
112
+ entry[:cost] += delta
113
+ @held += delta
114
+ end
115
+ if !adjust(event_id, bytes) || !eligible(event_id, payload)
116
+ release([event_id])
117
+ return false
118
+ end
119
+ true
120
+ end
121
+
122
+ def adjust(event_id, bytes)
123
+ entry = @pending[event_id]
124
+ return false unless entry && valid_size?(bytes) && accounted_bytes + bytes - entry[:size] <= 65_536
125
+
126
+ @held += bytes - entry[:size]
127
+ entry[:cost] += bytes - entry[:size]
128
+ entry[:size] = bytes
129
+ true
130
+ end
131
+
132
+ def eligible(event_id, payload)
133
+ entry = @pending[event_id]
134
+ value = @windows[key(payload)]
135
+ entry && entry[:scope] == key(payload) && value && !value[:ambiguous] &&
136
+ (payload['kind'] == 'recovery' || value[:failure_id] == payload['source_event_id'])
137
+ end
138
+
139
+ def release(event_ids)
140
+ event_ids.compact.uniq.each do |id|
141
+ entry = @pending.delete(id)
142
+ next unless entry
143
+
144
+ @held -= entry[:cost]
145
+ value = @windows[entry[:scope]]
146
+ next unless value
147
+
148
+ value[:pending] = [value[:pending] - 1, 0].max
149
+ value[:cost] = metadata_cost(entry[:scope], value)
150
+ end
151
+ end
152
+
153
+ def valid_size?(size) = size.is_a?(Integer) && size.between?(0, 65_536)
154
+ end
155
+ end