bug_bunny 4.7.0 → 4.8.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/documentation-writer/SKILL.md +45 -0
  3. data/.agents/skills/gem-release/SKILL.md +114 -0
  4. data/.agents/skills/quality-code/SKILL.md +51 -0
  5. data/.agents/skills/rabbitmq-expert/SKILL.md +1555 -0
  6. data/.agents/skills/sentry/SKILL.md +135 -0
  7. data/.agents/skills/sentry/references/api-endpoints.md +147 -0
  8. data/.agents/skills/sentry/scripts/sentry.rb +194 -0
  9. data/.agents/skills/skill-builder/SKILL.md +232 -0
  10. data/.agents/skills/skill-manager/SKILL.md +172 -0
  11. data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
  12. data/.agents/skills/yard/SKILL.md +311 -0
  13. data/.agents/skills/yard/references/tipos.md +144 -0
  14. data/CHANGELOG.md +21 -0
  15. data/CLAUDE.md +29 -220
  16. data/lib/bug_bunny/client.rb +4 -3
  17. data/lib/bug_bunny/controller.rb +10 -14
  18. data/lib/bug_bunny/exception.rb +1 -1
  19. data/lib/bug_bunny/middleware/raise_error.rb +3 -3
  20. data/lib/bug_bunny/observability.rb +5 -5
  21. data/lib/bug_bunny/producer.rb +11 -13
  22. data/lib/bug_bunny/railtie.rb +8 -7
  23. data/lib/bug_bunny/request.rb +3 -11
  24. data/lib/bug_bunny/resource.rb +51 -21
  25. data/lib/bug_bunny/routing/route_set.rb +32 -21
  26. data/lib/bug_bunny/version.rb +1 -1
  27. data/lib/bug_bunny.rb +3 -2
  28. data/lib/generators/bug_bunny/install/install_generator.rb +45 -5
  29. data/lib/tasks/bug_bunny.rake +50 -0
  30. data/skill/SKILL.md +230 -0
  31. data/skill/references/client-middleware.md +144 -0
  32. data/skill/references/consumer.md +104 -0
  33. data/skill/references/controller.md +105 -0
  34. data/skill/references/errores.md +97 -0
  35. data/skill/references/resource.md +116 -0
  36. data/skill/references/routing.md +82 -0
  37. data/skill/references/testing.md +138 -0
  38. data/skills-lock.json +10 -0
  39. data/skills.lock +24 -0
  40. data/skills.yml +19 -0
  41. metadata +27 -17
  42. data/.claude/commands/pr.md +0 -42
  43. data/.claude/commands/release.md +0 -41
  44. data/.claude/commands/rubocop.md +0 -22
  45. data/.claude/commands/test.md +0 -28
  46. data/.claude/commands/yard.md +0 -46
  47. data/docs/concepts.md +0 -140
  48. data/docs/howto/controller.md +0 -194
  49. data/docs/howto/middleware_client.md +0 -119
  50. data/docs/howto/middleware_consumer.md +0 -127
  51. data/docs/howto/rails.md +0 -214
  52. data/docs/howto/resource.md +0 -200
  53. data/docs/howto/routing.md +0 -133
  54. data/docs/howto/testing.md +0 -259
  55. data/docs/howto/tracing.md +0 -119
  56. data/mejoras.md +0 -33
@@ -1,259 +0,0 @@
1
- # Testing
2
-
3
- BugBunny applications can be tested at two levels: **unit tests** (with Bunny doubles) and **integration tests** (with a full mocked AMQP stack). No real RabbitMQ server is required.
4
-
5
- ## Setup
6
-
7
- ```ruby
8
- # spec/spec_helper.rb
9
- require 'bug_bunny'
10
- require 'rspec'
11
-
12
- Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
13
-
14
- RSpec.configure do |config|
15
- config.before(:each) do
16
- # Reset global state between tests
17
- BugBunny.instance_variable_set(:@consumer_middlewares, nil)
18
- BugBunny.instance_variable_set(:@routes, nil)
19
- end
20
- end
21
- ```
22
-
23
- ---
24
-
25
- ## Bunny Doubles
26
-
27
- Create lightweight doubles for Bunny objects so tests never touch the network:
28
-
29
- ```ruby
30
- # spec/support/bunny_mocks.rb
31
-
32
- def build_bunny_channel(opts = {})
33
- channel = instance_double(Bunny::Channel)
34
- allow(channel).to receive(:open?).and_return(true)
35
- allow(channel).to receive(:prefetch)
36
- allow(channel).to receive(:confirm_select)
37
- allow(channel).to receive(:ack)
38
- allow(channel).to receive(:reject)
39
- allow(channel).to receive(:close)
40
- allow(channel).to receive(:default_exchange).and_return(build_bunny_exchange)
41
- allow(channel).to receive(:topic).and_return(build_bunny_exchange)
42
- allow(channel).to receive(:direct).and_return(build_bunny_exchange)
43
- channel
44
- end
45
-
46
- def build_bunny_connection(opts = {})
47
- conn = instance_double(Bunny::Session)
48
- allow(conn).to receive(:open?).and_return(true)
49
- allow(conn).to receive(:start)
50
- allow(conn).to receive(:create_channel).and_return(build_bunny_channel)
51
- conn
52
- end
53
-
54
- def build_bunny_exchange
55
- exchange = instance_double(Bunny::Exchange)
56
- allow(exchange).to receive(:publish)
57
- exchange
58
- end
59
- ```
60
-
61
- ---
62
-
63
- ## Unit Testing Controllers
64
-
65
- Test controller actions directly via `Controller.call`, bypassing AMQP entirely:
66
-
67
- ```ruby
68
- RSpec.describe NodesController do
69
- let(:node) { Node.new(id: '42', name: 'web-01', status: 'active') }
70
-
71
- describe '#show' do
72
- it 'returns the node as JSON' do
73
- allow(Node).to receive(:find).with('42').and_return(node)
74
-
75
- response = NodesController.call(
76
- headers: { type: 'nodes/42', 'x-http-method' => 'GET' },
77
- body: ''
78
- )
79
-
80
- expect(response[:status]).to eq(200)
81
- expect(response[:body][:name]).to eq('web-01')
82
- end
83
- end
84
-
85
- describe '#show with missing node' do
86
- it 'returns 404' do
87
- allow(Node).to receive(:find).with('99').and_return(nil)
88
-
89
- response = NodesController.call(
90
- headers: { type: 'nodes/99', 'x-http-method' => 'GET' },
91
- body: ''
92
- )
93
-
94
- expect(response[:status]).to eq(404)
95
- end
96
- end
97
- end
98
- ```
99
-
100
- ---
101
-
102
- ## Unit Testing before_action / after_action
103
-
104
- ```ruby
105
- RSpec.describe NodesController do
106
- describe 'before_action :authenticate!' do
107
- it 'returns 401 when token is missing' do
108
- response = NodesController.call(
109
- headers: { type: 'nodes', 'x-http-method' => 'GET' },
110
- body: ''
111
- # no X-Service-Token header
112
- )
113
-
114
- expect(response[:status]).to eq(401)
115
- end
116
- end
117
-
118
- describe 'after_action :emit_audit_event' do
119
- it 'emits an audit event after create' do
120
- expect(AuditLog).to receive(:record).with(hash_including(action: 'create'))
121
-
122
- NodesController.call(
123
- headers: { type: 'nodes', 'x-http-method' => 'POST', 'X-Service-Token' => 'valid' },
124
- body: '{"node":{"name":"web-01","status":"pending"}}'
125
- )
126
- end
127
- end
128
- end
129
- ```
130
-
131
- ---
132
-
133
- ## Unit Testing Consumer Middleware
134
-
135
- ```ruby
136
- RSpec.describe TracingMiddleware do
137
- subject(:middleware) { described_class.new(terminal) }
138
-
139
- let(:terminal) { ->(di, props, body) { :processed } }
140
- let(:delivery_info) { instance_double(Bunny::DeliveryInfo, routing_key: 'nodes') }
141
- let(:properties) do
142
- instance_double(Bunny::MessageProperties,
143
- headers: { 'X-Trace-Id' => 'trace-123' },
144
- correlation_id: 'corr-456'
145
- )
146
- end
147
-
148
- it 'extracts the trace header and sets context' do
149
- expect(MyTracer).to receive(:with_trace).with('trace-123').and_yield
150
-
151
- middleware.call(delivery_info, properties, '{}')
152
- end
153
-
154
- it 'generates a new trace id when header is absent' do
155
- allow(properties).to receive(:headers).and_return({})
156
- expect(MyTracer).to receive(:with_trace).with(be_a(String)).and_yield
157
-
158
- middleware.call(delivery_info, properties, '{}')
159
- end
160
- end
161
- ```
162
-
163
- ---
164
-
165
- ## Integration Testing with a Mock Consumer
166
-
167
- For tests that exercise the full routing + controller stack, use an in-process integration helper:
168
-
169
- ```ruby
170
- # spec/support/integration_helper.rb
171
-
172
- module IntegrationHelper
173
- def process_message(method:, path:, body: '', headers: {})
174
- delivery_info = instance_double(Bunny::DeliveryInfo,
175
- delivery_tag: 1,
176
- routing_key: 'test'
177
- )
178
-
179
- properties = instance_double(Bunny::MessageProperties,
180
- type: path,
181
- reply_to: nil,
182
- correlation_id: SecureRandom.uuid,
183
- headers: { 'x-http-method' => method.to_s.upcase }.merge(headers)
184
- )
185
-
186
- consumer = BugBunny::Consumer.new
187
- # Allow channel operations on the mock session
188
- allow(consumer.session.channel).to receive(:ack)
189
- allow(consumer.session.channel).to receive(:reject)
190
- allow(consumer.session.channel).to receive(:default_exchange).and_return(build_bunny_exchange)
191
-
192
- consumer.send(:process_message, delivery_info, properties, body.to_json)
193
- end
194
- end
195
-
196
- RSpec.configure do |config|
197
- config.include IntegrationHelper, type: :integration
198
- end
199
- ```
200
-
201
- ```ruby
202
- # spec/integration/nodes_spec.rb, type: :integration
203
- RSpec.describe 'Nodes API', type: :integration do
204
- before do
205
- BugBunny.routes.draw { resources :nodes }
206
- end
207
-
208
- it 'routes GET nodes/:id to NodesController#show' do
209
- allow(Node).to receive(:find).with('42').and_return(Node.new(id: 42, name: 'web-01'))
210
-
211
- process_message(method: :get, path: 'nodes/42')
212
-
213
- expect(Node).to have_received(:find).with('42')
214
- end
215
-
216
- it 'returns 404 for unregistered routes' do
217
- response = process_message(method: :get, path: 'unknown/path')
218
- # response is captured via the consumer's handle_fatal_error path
219
- end
220
- end
221
- ```
222
-
223
- ---
224
-
225
- ## Unit Testing Configuration Validation
226
-
227
- ```ruby
228
- RSpec.describe BugBunny::Configuration do
229
- describe '#validate!' do
230
- it 'raises ConfigurationError when host is blank' do
231
- config = described_class.new
232
- config.host = ''
233
-
234
- expect { config.validate! }.to raise_error(BugBunny::ConfigurationError, /host is required/)
235
- end
236
-
237
- it 'raises ConfigurationError when port is out of range' do
238
- config = described_class.new
239
- config.host = 'localhost'
240
- config.port = 99_999
241
-
242
- expect { config.validate! }.to raise_error(BugBunny::ConfigurationError, /port must be in/)
243
- end
244
- end
245
- end
246
- ```
247
-
248
- ---
249
-
250
- ## Running Tests
251
-
252
- ```bash
253
- source /opt/homebrew/opt/chruby/share/chruby/chruby.sh && chruby ruby-3.3.8
254
-
255
- bundle exec rspec # all tests
256
- bundle exec rspec spec/unit/ # unit tests only
257
- bundle exec rspec spec/integration/ # integration tests only
258
- bundle exec rspec spec/unit/consumer_spec.rb # single file
259
- ```
@@ -1,119 +0,0 @@
1
- # Distributed Tracing
2
-
3
- BugBunny propagates trace context through the full RPC cycle, from the producer to the consumer and back. The mechanism is tracer-agnostic: BugBunny provides hooks and you supply the tracer-specific logic.
4
-
5
- ## What BugBunny Propagates by Default
6
-
7
- The `correlation_id` AMQP property travels automatically from producer to consumer on every request. The Consumer wraps the entire execution in a `logger.tagged(correlation_id)` block when the logger supports tagged logging (Rails' `ActiveSupport::TaggedLogging`).
8
-
9
- This alone connects producer and consumer log lines without any configuration.
10
-
11
- ---
12
-
13
- ## Full Bidirectional Propagation
14
-
15
- For distributed tracing systems (OpenTelemetry, AWS X-Ray, Datadog APM, etc.) you need to:
16
-
17
- 1. **Inject** the current trace header into outgoing requests (producer side).
18
- 2. **Extract** the trace header from incoming messages (consumer side) — done via Consumer Middleware.
19
- 3. **Re-inject** the updated trace header into the RPC reply (consumer side).
20
- 4. **Hydrate** the trace context from the reply headers back in the calling thread (producer side).
21
-
22
- Steps 3 and 4 handle the case where the consumer creates a child span — the parent needs to know about it.
23
-
24
- ---
25
-
26
- ## Configuration Hooks
27
-
28
- ### `rpc_reply_headers`
29
-
30
- A `Proc` called in the consumer thread just before sending the RPC reply. Its return value is merged into the reply AMQP headers.
31
-
32
- ```ruby
33
- BugBunny.configure do |config|
34
- config.rpc_reply_headers = -> {
35
- { 'X-Trace-Header' => MyTracer.outgoing_header }
36
- }
37
- end
38
- ```
39
-
40
- Zero overhead when not set.
41
-
42
- ### `on_rpc_reply`
43
-
44
- A `Proc` called in the producer thread after the RPC reply arrives, with the reply headers as argument. Use it to hydrate the trace context in the calling thread.
45
-
46
- ```ruby
47
- BugBunny.configure do |config|
48
- config.on_rpc_reply = ->(headers) {
49
- MyTracer.hydrate(headers['X-Trace-Header'])
50
- }
51
- end
52
- ```
53
-
54
- ---
55
-
56
- ## Full Example (tracer-agnostic)
57
-
58
- ```ruby
59
- # config/initializers/bug_bunny.rb
60
-
61
- BugBunny.configure do |config|
62
- # ... connection config ...
63
-
64
- # Step 3: inject updated trace header into RPC reply
65
- config.rpc_reply_headers = -> {
66
- { 'X-Trace-Header' => MyTracer.generate_outgoing_header }
67
- }
68
-
69
- # Step 4: hydrate trace context in the producer thread after reply
70
- config.on_rpc_reply = ->(headers) {
71
- MyTracer.hydrate_from_header(headers['X-Trace-Header'])
72
- }
73
- end
74
-
75
- # Step 1: inject trace header into outgoing requests (client middleware)
76
- class TraceInjectionMiddleware < BugBunny::Middleware::Base
77
- def call(request)
78
- request.headers['X-Trace-Header'] = MyTracer.generate_outgoing_header
79
- app.call(request)
80
- end
81
- end
82
-
83
- # Step 2: extract trace header from incoming messages (consumer middleware)
84
- class TraceExtractionMiddleware < BugBunny::ConsumerMiddleware::Base
85
- def call(delivery_info, properties, body)
86
- incoming_header = properties.headers&.dig('X-Trace-Header')
87
- MyTracer.with_trace_from_header(incoming_header) do
88
- @app.call(delivery_info, properties, body)
89
- end
90
- end
91
- end
92
-
93
- # Register both
94
- BugBunny::Resource.client_middleware { |s| s.use TraceInjectionMiddleware }
95
- BugBunny.consumer_middlewares.use TraceExtractionMiddleware
96
- ```
97
-
98
- ---
99
-
100
- ## Fire-and-Forget Tracing
101
-
102
- For `:publish` (fire-and-forget) calls, there is no reply, so `on_rpc_reply` and `rpc_reply_headers` do not apply. Use only the client middleware to inject the outgoing trace header:
103
-
104
- ```ruby
105
- class TraceInjectionMiddleware < BugBunny::Middleware::Base
106
- def call(request)
107
- request.headers['X-Trace-Header'] = MyTracer.generate_outgoing_header
108
- app.call(request)
109
- end
110
- end
111
- ```
112
-
113
- The consumer middleware extracts it on the other side regardless of whether the call was RPC or fire-and-forget.
114
-
115
- ---
116
-
117
- ## correlation_id
118
-
119
- BugBunny sets `correlation_id` automatically on every RPC request (used internally to match replies). It is also forwarded as a log tag. If your tracer uses a separate header (e.g., `traceparent`), use the `X-Trace-Header` pattern above. If you want to use `correlation_id` as the trace ID, read it from `properties.correlation_id` in your consumer middleware.
data/mejoras.md DELETED
@@ -1,33 +0,0 @@
1
- 1. Robustez en el Manejo de Conexiones
2
- Actualmente, el Producer y el Consumer dependen de que la sesión esté abierta al momento de instanciarse.
3
-
4
- Mejora sugerida: Implementar una lógica de "Reconexión Transparente". Si el socket de RabbitMQ se cierra por un problema de red, la gema debería intentar restablecer la conexión automáticamente antes de lanzar una excepción.
5
-
6
- Lazy Loading de Canales: Actualmente se crea el canal al inicializar la sesión. Podrías retrasar la creación del canal hasta el primer publish o subscribe para evitar consumir recursos si la gema está cargada pero no se usa inmediatamente.
7
-
8
- 2. Estandarización de BugBunny::Resource
9
- Durante los tests de Resource, notamos que los atributos son puramente dinámicos vía method_missing.
10
-
11
- Mejora sugerida: Integrar formalmente ActiveModel::Attributes (que ya incluyes parcialmente) para permitir la definición explícita de tipos.
12
-
13
- Ejemplo: attribute :price, :decimal.
14
-
15
- Esto permitiría que la gema realice conversiones de tipo (coerción) automáticamente antes de enviar el JSON, evitando errores de tipo en el microservicio de destino.
16
-
17
- 3. Observabilidad y Debugging
18
- Los tests de integración mostraron que identificar fallos en el flujo RPC puede ser difícil sin los logs adecuados.
19
-
20
- Mejora sugerida: Tracing ID (Correlation ID) automático. * Aunque ya usas correlation_id para el emparejamiento de respuestas, podrías integrar un middleware que inyecte este ID en todos los logs de Rails.
21
-
22
- Esto permitiría seguir el rastro de una petición desde que sale del Client hasta que el Consumer la procesa en otro servidor.
23
-
24
- 4. Soporte para Namespaces en Controladores
25
- El Consumer busca controladores en el namespace rígido Rabbit::Controllers.
26
-
27
- Mejora sugerida: Permitir configurar el namespace base. Si el usuario quiere organizar sus controladores en Messaging::Handlers en lugar de Rabbit::Controllers, la gema debería permitirlo mediante una opción en el inicializador.
28
-
29
- 5. No veo que en el controller que hicimos sea facil manipular el header
30
-
31
-
32
- Vamos a ir resolviendo este en etapas. del punto 1 al 5. Solo pasamos al proximo punto si quedo el actual resuelto.
33
- Importante: Todas las modificaciones que sean completadas, robustas y claramente documentadas con YARD.