e11y 0.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 (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +69 -0
  4. data/CHANGELOG.md +26 -0
  5. data/CODE_OF_CONDUCT.md +64 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +179 -0
  8. data/Rakefile +37 -0
  9. data/benchmarks/run_all.rb +33 -0
  10. data/config/README.md +83 -0
  11. data/config/loki-local-config.yaml +35 -0
  12. data/config/prometheus.yml +15 -0
  13. data/docker-compose.yml +78 -0
  14. data/docs/00-ICP-AND-TIMELINE.md +483 -0
  15. data/docs/01-SCALE-REQUIREMENTS.md +858 -0
  16. data/docs/ADR-001-architecture.md +2617 -0
  17. data/docs/ADR-002-metrics-yabeda.md +1395 -0
  18. data/docs/ADR-003-slo-observability.md +3337 -0
  19. data/docs/ADR-004-adapter-architecture.md +2385 -0
  20. data/docs/ADR-005-tracing-context.md +1372 -0
  21. data/docs/ADR-006-security-compliance.md +4143 -0
  22. data/docs/ADR-007-opentelemetry-integration.md +1385 -0
  23. data/docs/ADR-008-rails-integration.md +1911 -0
  24. data/docs/ADR-009-cost-optimization.md +2993 -0
  25. data/docs/ADR-010-developer-experience.md +2166 -0
  26. data/docs/ADR-011-testing-strategy.md +1836 -0
  27. data/docs/ADR-012-event-evolution.md +958 -0
  28. data/docs/ADR-013-reliability-error-handling.md +2750 -0
  29. data/docs/ADR-014-event-driven-slo.md +1533 -0
  30. data/docs/ADR-015-middleware-order.md +1061 -0
  31. data/docs/ADR-016-self-monitoring-slo.md +1234 -0
  32. data/docs/API-REFERENCE-L28.md +914 -0
  33. data/docs/COMPREHENSIVE-CONFIGURATION.md +2366 -0
  34. data/docs/IMPLEMENTATION_NOTES.md +2804 -0
  35. data/docs/IMPLEMENTATION_PLAN.md +1971 -0
  36. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +586 -0
  37. data/docs/PLAN.md +148 -0
  38. data/docs/QUICK-START.md +934 -0
  39. data/docs/README.md +296 -0
  40. data/docs/design/00-memory-optimization.md +593 -0
  41. data/docs/guides/MIGRATION-L27-L28.md +692 -0
  42. data/docs/guides/PERFORMANCE-BENCHMARKS.md +434 -0
  43. data/docs/guides/README.md +44 -0
  44. data/docs/prd/01-overview-vision.md +440 -0
  45. data/docs/use_cases/README.md +119 -0
  46. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +813 -0
  47. data/docs/use_cases/UC-002-business-event-tracking.md +1953 -0
  48. data/docs/use_cases/UC-003-pattern-based-metrics.md +1627 -0
  49. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +728 -0
  50. data/docs/use_cases/UC-005-sentry-integration.md +759 -0
  51. data/docs/use_cases/UC-006-trace-context-management.md +905 -0
  52. data/docs/use_cases/UC-007-pii-filtering.md +2648 -0
  53. data/docs/use_cases/UC-008-opentelemetry-integration.md +1153 -0
  54. data/docs/use_cases/UC-009-multi-service-tracing.md +1043 -0
  55. data/docs/use_cases/UC-010-background-job-tracking.md +1018 -0
  56. data/docs/use_cases/UC-011-rate-limiting.md +1906 -0
  57. data/docs/use_cases/UC-012-audit-trail.md +2301 -0
  58. data/docs/use_cases/UC-013-high-cardinality-protection.md +2127 -0
  59. data/docs/use_cases/UC-014-adaptive-sampling.md +1940 -0
  60. data/docs/use_cases/UC-015-cost-optimization.md +735 -0
  61. data/docs/use_cases/UC-016-rails-logger-migration.md +785 -0
  62. data/docs/use_cases/UC-017-local-development.md +867 -0
  63. data/docs/use_cases/UC-018-testing-events.md +1081 -0
  64. data/docs/use_cases/UC-019-tiered-storage-migration.md +562 -0
  65. data/docs/use_cases/UC-020-event-versioning.md +708 -0
  66. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +956 -0
  67. data/docs/use_cases/UC-022-event-registry.md +648 -0
  68. data/docs/use_cases/backlog.md +226 -0
  69. data/e11y.gemspec +76 -0
  70. data/lib/e11y/adapters/adaptive_batcher.rb +207 -0
  71. data/lib/e11y/adapters/audit_encrypted.rb +239 -0
  72. data/lib/e11y/adapters/base.rb +580 -0
  73. data/lib/e11y/adapters/file.rb +224 -0
  74. data/lib/e11y/adapters/in_memory.rb +216 -0
  75. data/lib/e11y/adapters/loki.rb +333 -0
  76. data/lib/e11y/adapters/otel_logs.rb +203 -0
  77. data/lib/e11y/adapters/registry.rb +141 -0
  78. data/lib/e11y/adapters/sentry.rb +230 -0
  79. data/lib/e11y/adapters/stdout.rb +108 -0
  80. data/lib/e11y/adapters/yabeda.rb +370 -0
  81. data/lib/e11y/buffers/adaptive_buffer.rb +339 -0
  82. data/lib/e11y/buffers/base_buffer.rb +40 -0
  83. data/lib/e11y/buffers/request_scoped_buffer.rb +246 -0
  84. data/lib/e11y/buffers/ring_buffer.rb +267 -0
  85. data/lib/e11y/buffers.rb +14 -0
  86. data/lib/e11y/console.rb +122 -0
  87. data/lib/e11y/current.rb +48 -0
  88. data/lib/e11y/event/base.rb +894 -0
  89. data/lib/e11y/event/value_sampling_config.rb +84 -0
  90. data/lib/e11y/events/base_audit_event.rb +43 -0
  91. data/lib/e11y/events/base_payment_event.rb +33 -0
  92. data/lib/e11y/events/rails/cache/delete.rb +21 -0
  93. data/lib/e11y/events/rails/cache/read.rb +23 -0
  94. data/lib/e11y/events/rails/cache/write.rb +22 -0
  95. data/lib/e11y/events/rails/database/query.rb +45 -0
  96. data/lib/e11y/events/rails/http/redirect.rb +21 -0
  97. data/lib/e11y/events/rails/http/request.rb +26 -0
  98. data/lib/e11y/events/rails/http/send_file.rb +21 -0
  99. data/lib/e11y/events/rails/http/start_processing.rb +26 -0
  100. data/lib/e11y/events/rails/job/completed.rb +22 -0
  101. data/lib/e11y/events/rails/job/enqueued.rb +22 -0
  102. data/lib/e11y/events/rails/job/failed.rb +22 -0
  103. data/lib/e11y/events/rails/job/scheduled.rb +23 -0
  104. data/lib/e11y/events/rails/job/started.rb +22 -0
  105. data/lib/e11y/events/rails/log.rb +56 -0
  106. data/lib/e11y/events/rails/view/render.rb +23 -0
  107. data/lib/e11y/events.rb +18 -0
  108. data/lib/e11y/instruments/active_job.rb +201 -0
  109. data/lib/e11y/instruments/rails_instrumentation.rb +141 -0
  110. data/lib/e11y/instruments/sidekiq.rb +175 -0
  111. data/lib/e11y/logger/bridge.rb +205 -0
  112. data/lib/e11y/metrics/cardinality_protection.rb +172 -0
  113. data/lib/e11y/metrics/cardinality_tracker.rb +134 -0
  114. data/lib/e11y/metrics/registry.rb +234 -0
  115. data/lib/e11y/metrics/relabeling.rb +226 -0
  116. data/lib/e11y/metrics.rb +102 -0
  117. data/lib/e11y/middleware/audit_signing.rb +174 -0
  118. data/lib/e11y/middleware/base.rb +140 -0
  119. data/lib/e11y/middleware/event_slo.rb +167 -0
  120. data/lib/e11y/middleware/pii_filter.rb +266 -0
  121. data/lib/e11y/middleware/pii_filtering.rb +280 -0
  122. data/lib/e11y/middleware/rate_limiting.rb +214 -0
  123. data/lib/e11y/middleware/request.rb +163 -0
  124. data/lib/e11y/middleware/routing.rb +157 -0
  125. data/lib/e11y/middleware/sampling.rb +254 -0
  126. data/lib/e11y/middleware/slo.rb +168 -0
  127. data/lib/e11y/middleware/trace_context.rb +131 -0
  128. data/lib/e11y/middleware/validation.rb +118 -0
  129. data/lib/e11y/middleware/versioning.rb +132 -0
  130. data/lib/e11y/middleware.rb +12 -0
  131. data/lib/e11y/pii/patterns.rb +90 -0
  132. data/lib/e11y/pii.rb +13 -0
  133. data/lib/e11y/pipeline/builder.rb +155 -0
  134. data/lib/e11y/pipeline/zone_validator.rb +110 -0
  135. data/lib/e11y/pipeline.rb +12 -0
  136. data/lib/e11y/presets/audit_event.rb +65 -0
  137. data/lib/e11y/presets/debug_event.rb +34 -0
  138. data/lib/e11y/presets/high_value_event.rb +51 -0
  139. data/lib/e11y/presets.rb +19 -0
  140. data/lib/e11y/railtie.rb +138 -0
  141. data/lib/e11y/reliability/circuit_breaker.rb +216 -0
  142. data/lib/e11y/reliability/dlq/file_storage.rb +277 -0
  143. data/lib/e11y/reliability/dlq/filter.rb +117 -0
  144. data/lib/e11y/reliability/retry_handler.rb +207 -0
  145. data/lib/e11y/reliability/retry_rate_limiter.rb +117 -0
  146. data/lib/e11y/sampling/error_spike_detector.rb +225 -0
  147. data/lib/e11y/sampling/load_monitor.rb +161 -0
  148. data/lib/e11y/sampling/stratified_tracker.rb +92 -0
  149. data/lib/e11y/sampling/value_extractor.rb +82 -0
  150. data/lib/e11y/self_monitoring/buffer_monitor.rb +79 -0
  151. data/lib/e11y/self_monitoring/performance_monitor.rb +97 -0
  152. data/lib/e11y/self_monitoring/reliability_monitor.rb +146 -0
  153. data/lib/e11y/slo/event_driven.rb +150 -0
  154. data/lib/e11y/slo/tracker.rb +119 -0
  155. data/lib/e11y/version.rb +9 -0
  156. data/lib/e11y.rb +283 -0
  157. metadata +452 -0
@@ -0,0 +1,1081 @@
1
+ # UC-018: Testing Events
2
+
3
+ **Status:** MVP Feature
4
+ **Complexity:** Beginner
5
+ **Setup Time:** 10-15 minutes
6
+ **Target Users:** All Developers, QA Engineers
7
+
8
+ ---
9
+
10
+ ## 📋 Overview
11
+
12
+ ### Problem Statement
13
+
14
+ **The testing challenge:**
15
+ ```ruby
16
+ # ❌ BEFORE: Hard to test events
17
+ RSpec.describe OrdersController do
18
+ it 'creates order' do
19
+ post :create, params: { order: order_params }
20
+
21
+ # How do I test that Events::OrderCreated was tracked?
22
+ # - Can't easily check event was emitted
23
+ # - Can't verify event payload
24
+ # - Can't test metrics were updated
25
+ # - Events go to real adapters (slow!)
26
+ end
27
+ end
28
+ ```
29
+
30
+ ### E11y Solution
31
+
32
+ **Built-in RSpec support:**
33
+ ```ruby
34
+ # ✅ AFTER: Easy event testing
35
+ RSpec.describe OrdersController do
36
+ it 'creates order' do
37
+ # Expect event class to be tracked
38
+ expect {
39
+ post :create, params: { order: order_params }
40
+ }.to track_event(Events::OrderCreated)
41
+ .with(order_id: '123', user_id: '456')
42
+
43
+ # Automatic assertions:
44
+ # ✅ Event was tracked
45
+ # ✅ Payload matches
46
+ # ✅ Severity matches (from class definition)
47
+ end
48
+ end
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 🎯 Features
54
+
55
+ > **Implementation:** See [ADR-011: Testing Strategy](../ADR-011-testing-strategy.md) for complete testing architecture, including [Section 3: RSpec Matchers](../ADR-011-testing-strategy.md#3-rspec-matchers), [Section 4: Test Adapters](../ADR-011-testing-strategy.md#4-test-adapters), [Section 6: Snapshot Testing](../ADR-011-testing-strategy.md#6-snapshot-testing), and [Section 8: Integration Testing](../ADR-011-testing-strategy.md#8-integration-testing).
56
+
57
+ ### 1. RSpec Matchers
58
+
59
+ **Built-in custom matchers:**
60
+ ```ruby
61
+ # spec/support/e11y.rb
62
+ require 'e11y/rspec'
63
+
64
+ RSpec.configure do |config|
65
+ config.include E11y::RSpec::Matchers
66
+
67
+ config.before(:each) do
68
+ # Clear events before each test
69
+ E11y.reset!
70
+ end
71
+ end
72
+
73
+ # === MATCHER: track_event ===
74
+ # Basic usage (with event class)
75
+ expect { action }.to track_event(Events::OrderCreated)
76
+
77
+ # With payload matching
78
+ expect { action }.to track_event(Events::OrderCreated)
79
+ .with(order_id: '123')
80
+
81
+ # With hash_including (partial match)
82
+ expect { action }.to track_event(Events::OrderCreated)
83
+ .with(hash_including(order_id: '123'))
84
+
85
+ # Check severity (from event class definition)
86
+ expect { action }.to track_event(Events::PaymentFailed)
87
+ # severity already defined in Events::PaymentFailed class
88
+
89
+ # With count
90
+ expect { action }.to track_event(Events::OrderCreated).once
91
+ expect { action }.to track_event(Events::PaymentRetry).exactly(3).times
92
+ expect { action }.to track_event(Events::NotificationSent).at_least(1).times
93
+
94
+ # Negation
95
+ expect { action }.not_to track_event(Events::OrderCancelled)
96
+
97
+ # === MATCHER: track_events (multiple) ===
98
+ expect { action }.to track_events(
99
+ Events::OrderCreated,
100
+ Events::PaymentProcessing,
101
+ Events::ShipmentScheduled
102
+ ).in_order
103
+
104
+ # === MATCHER: update_metric ===
105
+ expect { action }.to update_metric('orders.total')
106
+ .by(1)
107
+ .with_tags(status: 'paid')
108
+
109
+ # === MATCHER: have_trace_id ===
110
+ event = E11y.last_event
111
+ expect(event).to have_trace_id('abc-123')
112
+
113
+ # === MATCHER: have_valid_schema ===
114
+ event = E11y.last_event
115
+ expect(event).to have_valid_schema
116
+ ```
117
+
118
+ ---
119
+
120
+ ### 2. Test Helpers
121
+
122
+ **Convenient helper methods:**
123
+ ```ruby
124
+ # spec/support/e11y.rb
125
+ RSpec.configure do |config|
126
+ config.include E11y::RSpec::Helpers
127
+ end
128
+
129
+ # === Helper: e11y_events ===
130
+ # Get all tracked events
131
+ events = e11y_events
132
+ # => [<Events::OrderCreated>, <Events::PaymentProcessing>]
133
+
134
+ # Filter by event class
135
+ events = e11y_events(Events::OrderCreated)
136
+ # => [<Events::OrderCreated>]
137
+
138
+ # Filter by pattern (for dynamic filtering)
139
+ events = e11y_events(/^order\./)
140
+ # => [<Events::OrderCreated>, <Events::OrderShipped>]
141
+
142
+ # Filter by severity
143
+ events = e11y_events(severity: :error)
144
+ # => [<Events::PaymentFailed>]
145
+
146
+ # === Helper: e11y_last_event ===
147
+ event = e11y_last_event
148
+ # => <Events::OrderCreated>
149
+
150
+ event = e11y_last_event(Events::PaymentProcessing)
151
+ # => <Events::PaymentProcessing>
152
+
153
+ # === Helper: e11y_event_classes ===
154
+ classes = e11y_event_classes
155
+ # => [Events::OrderCreated, Events::PaymentProcessing, Events::ShipmentScheduled]
156
+
157
+ # === Helper: e11y_reset! ===
158
+ e11y_reset! # Clear all tracked events
159
+
160
+ # === Helper: e11y_disable / e11y_enable ===
161
+ e11y_disable # Disable event tracking
162
+ # ... code ...
163
+ e11y_enable # Re-enable
164
+
165
+ # === Helper: e11y_with_config ===
166
+ e11y_with_config(severity: :debug) do
167
+ # Temporarily change config
168
+ Events::DebugEvent.track(...)
169
+ end
170
+ # Config restored after block
171
+ ```
172
+
173
+ ---
174
+
175
+ ### 3. Stub Events (Test Doubles)
176
+
177
+ **Mock event tracking for isolation:**
178
+ ```ruby
179
+ # === Stub specific event ===
180
+ allow(Events::OrderCreated).to receive(:track)
181
+
182
+ post :create, params: { order: order_params }
183
+
184
+ expect(Events::OrderCreated).to have_received(:track)
185
+ .with(hash_including(order_id: '123'))
186
+
187
+ # === Stub and return value ===
188
+ allow(Events::PaymentProcessing).to receive(:track).and_return(
189
+ E11y::TrackResult.success
190
+ )
191
+
192
+ # === Spy on events ===
193
+ event_spy = spy('Events::OrderCreated')
194
+ stub_const('Events::OrderCreated', event_spy)
195
+
196
+ post :create, params: { order: order_params }
197
+
198
+ expect(event_spy).to have_received(:track)
199
+
200
+ # === Partial stub (stub only track, keep schema validation) ===
201
+ allow(Events::OrderCreated).to receive(:track).and_call_original
202
+ # ... test ...
203
+ expect(Events::OrderCreated).to have_received(:track)
204
+ ```
205
+
206
+ ---
207
+
208
+ ### 4. Factory Support
209
+
210
+ **FactoryBot integration:**
211
+ ```ruby
212
+ # spec/factories/e11y_events.rb
213
+ FactoryBot.define do
214
+ factory :e11y_event, class: 'E11y::Event' do
215
+ event_name { 'test.event' }
216
+ severity { :info }
217
+ payload { {} }
218
+ context { { trace_id: SecureRandom.uuid } }
219
+ timestamp { Time.current }
220
+
221
+ trait :order_created do
222
+ event_name { 'order.created' }
223
+ payload do
224
+ {
225
+ order_id: '123',
226
+ user_id: '456',
227
+ amount: 99.99
228
+ }
229
+ end
230
+ end
231
+
232
+ trait :payment_failed do
233
+ event_name { 'payment.failed' }
234
+ severity { :error }
235
+ payload do
236
+ {
237
+ order_id: '123',
238
+ error: 'Card declined'
239
+ }
240
+ end
241
+ end
242
+
243
+ trait :with_trace do
244
+ context do
245
+ {
246
+ trace_id: 'abc-123-def',
247
+ request_id: 'req-789',
248
+ user_id: '456'
249
+ }
250
+ end
251
+ end
252
+ end
253
+ end
254
+
255
+ # Usage:
256
+ event = create(:e11y_event, :order_created)
257
+ event = build(:e11y_event, :payment_failed, :with_trace)
258
+ events = create_list(:e11y_event, 5, :order_created)
259
+ ```
260
+
261
+ ---
262
+
263
+ ### 5. Snapshot Testing
264
+
265
+ **Capture and compare event snapshots:**
266
+ ```ruby
267
+ # spec/support/e11y_snapshot.rb
268
+ RSpec.configure do |config|
269
+ config.include E11y::RSpec::Snapshot
270
+ end
271
+
272
+ # === Snapshot matcher ===
273
+ RSpec.describe OrdersController do
274
+ it 'creates order with expected events' do
275
+ expect {
276
+ post :create, params: { order: order_params }
277
+ }.to match_event_snapshot
278
+
279
+ # First run: creates snapshot file
280
+ # spec/fixtures/e11y_snapshots/orders_controller_creates_order.json
281
+
282
+ # Subsequent runs: compares against snapshot
283
+ # Fails if events changed!
284
+ end
285
+
286
+ it 'creates order', :update_snapshot do
287
+ # Use :update_snapshot tag to update snapshot
288
+ expect {
289
+ post :create, params: { order: order_params }
290
+ }.to match_event_snapshot
291
+ end
292
+ end
293
+
294
+ # Snapshot file format:
295
+ # spec/fixtures/e11y_snapshots/orders_controller_creates_order.json
296
+ {
297
+ "events": [
298
+ {
299
+ "event_name": "order.created",
300
+ "severity": "success",
301
+ "payload": {
302
+ "order_id": "123",
303
+ "user_id": "456",
304
+ "amount": 99.99
305
+ }
306
+ },
307
+ {
308
+ "event_name": "notification.sent",
309
+ "severity": "info",
310
+ "payload": {
311
+ "type": "order_confirmation",
312
+ "recipient": "user@example.com"
313
+ }
314
+ }
315
+ ]
316
+ }
317
+
318
+ # Benefits:
319
+ # ✅ Catch unintended changes
320
+ # ✅ Document expected behavior
321
+ # ✅ Easy to review in PR diffs
322
+ ```
323
+
324
+ ---
325
+
326
+ ### 6. Test Environment Configuration (C13)
327
+
328
+ > **Implementation:** See [ADR-011 Section 10: Test Environment Configuration](../ADR-011-testing-strategy.md#10-test-environment-configuration) for detailed rationale on disabling production features in tests.
329
+
330
+ **Critical: Production features MUST be disabled in tests for predictability.**
331
+
332
+ **Problem: Non-Deterministic Tests**
333
+
334
+ ```ruby
335
+ # ❌ BAD: Production config enabled in tests
336
+ # config/environments/test.rb (default Rails)
337
+ # → E11y inherits production config!
338
+
339
+ RSpec.describe OrdersController do
340
+ it 'creates order' do
341
+ post :create, params: { order: order_params }
342
+
343
+ # ❌ PROBLEM 1: Sampling enabled (10% rate)
344
+ # → Event randomly dropped! Test flaky!
345
+ # → Fails 90% of the time!
346
+
347
+ # ❌ PROBLEM 2: Buffering enabled (10s flush)
348
+ # → Event not visible yet! Test fails!
349
+ # → Must wait 10 seconds or call E11y.flush
350
+
351
+ # ❌ PROBLEM 3: Async adapters
352
+ # → Events sent in background threads
353
+ # → Race conditions!
354
+
355
+ expect {
356
+ # ... may pass or fail randomly!
357
+ }.to track_event(Events::OrderCreated)
358
+ end
359
+ end
360
+ ```
361
+
362
+ **Solution: Disable Production Features in Tests**
363
+
364
+ ```ruby
365
+ # spec/support/e11y_helper.rb (RECOMMENDED SETUP)
366
+ RSpec.configure do |config|
367
+ config.before(:suite) do
368
+ # Configure E11y for test environment
369
+ E11y.configure do |config|
370
+ # === CRITICAL: Disable sampling ===
371
+ # ✅ Ensures ALL events are tracked (100% predictability)
372
+ # ❌ Without this: Random test failures (sampling drops 90% of events)
373
+ config.sampling.enabled = false
374
+
375
+ # === CRITICAL: Disable buffering OR use flush helper ===
376
+ # Option 1: Disable buffering (immediate writes)
377
+ config.buffering.enabled = false
378
+
379
+ # Option 2: Keep buffering but flush after each test
380
+ # config.buffering.enabled = true # (if needed for performance tests)
381
+
382
+ # === CRITICAL: Synchronous adapters only ===
383
+ # ✅ Use TestAdapter (stores events in memory)
384
+ # ❌ No Loki, Sentry, S3 (slow, async, flaky)
385
+ config.adapters = [
386
+ E11y::Adapters::TestAdapter.new
387
+ ]
388
+
389
+ # === OPTIONAL: Disable rate limiting ===
390
+ config.rate_limiting.enabled = false
391
+
392
+ # === OPTIONAL: Disable PII filtering (test data is fake) ===
393
+ config.pii_filtering.enabled = false
394
+ end
395
+ end
396
+
397
+ config.before(:each) do
398
+ # Clear events before each test
399
+ E11y.reset!
400
+
401
+ # If buffering enabled, flush before test
402
+ # E11y.flush
403
+ end
404
+
405
+ config.after(:each) do
406
+ # If buffering enabled, flush after test (for assertions)
407
+ E11y.flush if E11y.config.buffering.enabled
408
+ end
409
+ end
410
+ ```
411
+
412
+ **Key Configuration Options:**
413
+
414
+ | Feature | Test Value | Production Value | Why Different? |
415
+ |---------|-----------|------------------|----------------|
416
+ | **Sampling** | ❌ Disabled (100%) | ✅ Enabled (10-50%) | Tests need ALL events, no randomness |
417
+ | **Buffering** | ❌ Disabled (immediate) | ✅ Enabled (10s batches) | Tests need synchronous assertions |
418
+ | **Adapters** | `TestAdapter` (in-memory) | `LokiAdapter`, `SentryAdapter` | Tests need fast, synchronous, no network |
419
+ | **Rate Limiting** | ❌ Disabled | ✅ Enabled (1000/min) | Tests may emit many events rapidly |
420
+ | **PII Filtering** | ❌ Disabled (optional) | ✅ Enabled (GDPR) | Test data is fake (no real PII) |
421
+
422
+ **Manual Flush Helper (for Buffering Tests):**
423
+
424
+ If you need to test buffering behavior, use `E11y.flush`:
425
+
426
+ ```ruby
427
+ RSpec.describe 'Buffering behavior' do
428
+ before do
429
+ E11y.configure do |config|
430
+ config.buffering do
431
+ enabled true
432
+ flush_interval 10.seconds
433
+ max_buffer_size 100
434
+ end
435
+ end
436
+ end
437
+
438
+ it 'buffers events until flush' do
439
+ # Event tracked but NOT sent yet (buffered)
440
+ Events::OrderCreated.track(order_id: '123')
441
+
442
+ # Buffer has 1 event
443
+ expect(E11y.buffer.size).to eq(1)
444
+
445
+ # Adapter has 0 events (not flushed yet)
446
+ expect(E11y::Adapters::TestAdapter.events.size).to eq(0)
447
+
448
+ # === CRITICAL: Manual flush for synchronous testing ===
449
+ E11y.flush
450
+
451
+ # Now buffer is empty
452
+ expect(E11y.buffer.size).to eq(0)
453
+
454
+ # Adapter has 1 event (flushed)
455
+ expect(E11y::Adapters::TestAdapter.events.size).to eq(1)
456
+ end
457
+
458
+ it 'auto-flushes when buffer full' do
459
+ # Track 101 events (exceeds max_buffer_size: 100)
460
+ 101.times { |i| Events::OrderCreated.track(order_id: i) }
461
+
462
+ # Buffer auto-flushed when full (at 100)
463
+ expect(E11y::Adapters::TestAdapter.events.size).to eq(100)
464
+
465
+ # 1 event still in buffer
466
+ expect(E11y.buffer.size).to eq(1)
467
+
468
+ # Flush remaining
469
+ E11y.flush
470
+ expect(E11y::Adapters::TestAdapter.events.size).to eq(101)
471
+ end
472
+ end
473
+ ```
474
+
475
+ **Deterministic Sampling (for Sampling Tests):**
476
+
477
+ If you need to test sampling behavior, use a **fixed random seed**:
478
+
479
+ ```ruby
480
+ RSpec.describe 'Sampling behavior' do
481
+ before do
482
+ # === CRITICAL: Set fixed random seed for deterministic sampling ===
483
+ srand(12345) # ← Same seed = same random sequence
484
+
485
+ E11y.configure do |config|
486
+ config.sampling do
487
+ enabled true
488
+ strategy :random
489
+ rate 0.5 # Keep 50% of events
490
+ end
491
+ end
492
+ end
493
+
494
+ it 'samples events deterministically with fixed seed' do
495
+ # With seed=12345, first 10 events sampled as:
496
+ # [KEEP, DROP, KEEP, DROP, KEEP, KEEP, DROP, DROP, KEEP, DROP]
497
+ # (this sequence is deterministic with seed=12345)
498
+
499
+ 10.times do |i|
500
+ Events::OrderCreated.track(order_id: i)
501
+ end
502
+
503
+ # With seed=12345 and rate=0.5, expect 5 events kept
504
+ expect(E11y::Adapters::TestAdapter.events.size).to eq(5)
505
+
506
+ # Verify specific events kept (deterministic!)
507
+ kept_order_ids = E11y::Adapters::TestAdapter.events.map { |e| e.payload[:order_id] }
508
+ expect(kept_order_ids).to eq([0, 2, 4, 5, 8]) # Deterministic with seed=12345
509
+ end
510
+
511
+ it 'can test different sampling rates' do
512
+ E11y.configure do |config|
513
+ config.sampling.rate = 0.1 # Keep 10%
514
+ end
515
+
516
+ 100.times { |i| Events::OrderCreated.track(order_id: i) }
517
+
518
+ # With seed=12345 and rate=0.1, expect ~10 events
519
+ expect(E11y::Adapters::TestAdapter.events.size).to be_within(2).of(10)
520
+ end
521
+ end
522
+ ```
523
+
524
+ **RSpec Shared Context (Reusable Config):**
525
+
526
+ ```ruby
527
+ # spec/support/shared_contexts/e11y_test_config.rb
528
+ RSpec.shared_context 'e11y_test_config' do
529
+ before do
530
+ E11y.configure do |config|
531
+ config.sampling.enabled = false
532
+ config.buffering.enabled = false
533
+ config.adapters = [E11y::Adapters::TestAdapter.new]
534
+ end
535
+ end
536
+
537
+ after do
538
+ E11y.flush
539
+ E11y.reset!
540
+ end
541
+ end
542
+
543
+ # Usage:
544
+ RSpec.describe OrdersController do
545
+ include_context 'e11y_test_config'
546
+
547
+ it 'creates order' do
548
+ # ... test with clean E11y config
549
+ end
550
+ end
551
+ ```
552
+
553
+ **Trade-offs:**
554
+
555
+ | Decision | Pro | Con | Recommendation |
556
+ |----------|-----|-----|----------------|
557
+ | **Disable sampling** | 100% predictable tests | Doesn't test prod sampling | ✅ Always disable (test sampling separately with fixed seed) |
558
+ | **Disable buffering** | Synchronous assertions | Doesn't test buffering behavior | ✅ Disable by default (test buffering separately with `E11y.flush`) |
559
+ | **Use TestAdapter** | Fast, in-memory, no network | Doesn't test real adapters | ✅ Always use (test real adapters in integration tests) |
560
+ | **Fixed random seed** | Deterministic sampling tests | Non-obvious magic number | ⚠️ Use only for sampling tests, document seed choice |
561
+
562
+ **Common Pitfalls:**
563
+
564
+ ```ruby
565
+ # ❌ PITFALL 1: Forgot to disable sampling
566
+ RSpec.describe 'Feature' do
567
+ it 'tracks event' do
568
+ # Sampling enabled (10% rate) → 90% chance of failure!
569
+ expect {
570
+ Events::OrderCreated.track(order_id: '123')
571
+ }.to track_event(Events::OrderCreated) # ← Flaky!
572
+ end
573
+ end
574
+
575
+ # ✅ FIX: Disable sampling in spec/support/e11y_helper.rb
576
+
577
+ # ❌ PITFALL 2: Forgot to flush buffer
578
+ RSpec.describe 'Feature' do
579
+ before do
580
+ E11y.configure { |c| c.buffering.enabled = true }
581
+ end
582
+
583
+ it 'tracks event' do
584
+ Events::OrderCreated.track(order_id: '123')
585
+ # Buffer not flushed → event not in adapter yet!
586
+ expect(E11y::Adapters::TestAdapter.events.size).to eq(1) # ← Fails!
587
+ end
588
+ end
589
+
590
+ # ✅ FIX: Call E11y.flush before assertions
591
+ it 'tracks event' do
592
+ Events::OrderCreated.track(order_id: '123')
593
+ E11y.flush # ← Force flush
594
+ expect(E11y::Adapters::TestAdapter.events.size).to eq(1) # ← Passes!
595
+ end
596
+
597
+ # ❌ PITFALL 3: Used production adapters in tests
598
+ RSpec.describe 'Feature' do
599
+ before do
600
+ E11y.configure do |config|
601
+ config.adapters = [
602
+ E11y::Adapters::LokiAdapter.new(...) # ← Slow! Network calls!
603
+ ]
604
+ end
605
+ end
606
+
607
+ it 'tracks event' do
608
+ # Test takes 5 seconds per event! ❌
609
+ Events::OrderCreated.track(order_id: '123')
610
+ end
611
+ end
612
+
613
+ # ✅ FIX: Use TestAdapter
614
+ config.adapters = [E11y::Adapters::TestAdapter.new]
615
+ ```
616
+
617
+ **Key Takeaways:**
618
+
619
+ 1. **Always disable sampling in tests** (non-deterministic = flaky tests)
620
+ 2. **Disable buffering OR call `E11y.flush`** (synchronous assertions)
621
+ 3. **Use TestAdapter only** (fast, in-memory, no network)
622
+ 4. **Fixed random seed for sampling tests** (deterministic behavior)
623
+ 5. **Flush buffer in `after(:each)` hook** (if buffering enabled)
624
+
625
+ ---
626
+
627
+ ## 💻 Implementation Examples
628
+
629
+ ### Example 1: Controller Tests
630
+
631
+ ```ruby
632
+ # spec/controllers/orders_controller_spec.rb
633
+ RSpec.describe OrdersController do
634
+ describe 'POST #create' do
635
+ let(:order_params) do
636
+ {
637
+ items: [{ product_id: '456', quantity: 2 }],
638
+ payment_method: 'stripe'
639
+ }
640
+ end
641
+
642
+ it 'tracks order creation' do
643
+ expect {
644
+ post :create, params: { order: order_params }
645
+ }.to track_event(Events::OrderCreated)
646
+ .with(hash_including(
647
+ user_id: current_user.id,
648
+ amount: 99.99
649
+ ))
650
+ end
651
+
652
+ it 'tracks all order flow events' do
653
+ expect {
654
+ post :create, params: { order: order_params }
655
+ }.to track_events(
656
+ Events::OrderValidationStarted,
657
+ Events::OrderValidationCompleted,
658
+ Events::OrderCreated,
659
+ Events::PaymentInitiated
660
+ ).in_order
661
+ end
662
+
663
+ it 'updates order metrics' do
664
+ expect {
665
+ post :create, params: { order: order_params }
666
+ }.to update_metric('orders.total').by(1)
667
+ end
668
+
669
+ context 'with invalid params' do
670
+ let(:order_params) { { items: [] } }
671
+
672
+ it 'tracks validation error' do
673
+ expect {
674
+ post :create, params: { order: order_params }
675
+ }.to track_event(Events::OrderValidationFailed)
676
+ end
677
+
678
+ it 'does not create order event' do
679
+ expect {
680
+ post :create, params: { order: order_params }
681
+ }.not_to track_event(Events::OrderCreated)
682
+ end
683
+ end
684
+ end
685
+ end
686
+ ```
687
+
688
+ ---
689
+
690
+ ### Example 2: Service Tests
691
+
692
+ ```ruby
693
+ # spec/services/payment_service_spec.rb
694
+ RSpec.describe PaymentService do
695
+ subject(:service) { described_class.new }
696
+
697
+ describe '#charge' do
698
+ let(:order) { create(:order, total: 99.99) }
699
+
700
+ context 'when payment succeeds' do
701
+ before do
702
+ allow(StripeGateway).to receive(:charge).and_return(
703
+ OpenStruct.new(id: 'tx_123', amount: 99.99)
704
+ )
705
+ end
706
+
707
+ it 'tracks payment success' do
708
+ expect {
709
+ service.charge(order)
710
+ }.to track_event(Events::PaymentSucceeded)
711
+ .with(
712
+ order_id: order.id,
713
+ transaction_id: 'tx_123',
714
+ amount: 99.99
715
+ )
716
+ end
717
+
718
+ it 'includes trace context' do
719
+ service.charge(order)
720
+
721
+ event = e11y_last_event(Events::PaymentSucceeded)
722
+ expect(event).to have_trace_id
723
+ expect(event.trace_id).not_to be_nil
724
+ end
725
+ end
726
+
727
+ context 'when payment fails' do
728
+ before do
729
+ allow(StripeGateway).to receive(:charge).and_raise(
730
+ StripeGateway::CardDeclined.new('Insufficient funds')
731
+ )
732
+ end
733
+
734
+ it 'tracks payment failure' do
735
+ expect {
736
+ service.charge(order) rescue nil
737
+ }.to track_event(Events::PaymentFailed)
738
+ .with(
739
+ order_id: order.id,
740
+ error: 'Insufficient funds'
741
+ )
742
+ end
743
+ end
744
+ end
745
+ end
746
+ ```
747
+
748
+ ---
749
+
750
+ ### Example 3: Job Tests
751
+
752
+ ```ruby
753
+ # spec/jobs/process_order_job_spec.rb
754
+ RSpec.describe ProcessOrderJob do
755
+ include ActiveJob::TestHelper
756
+
757
+ describe '#perform' do
758
+ let(:order) { create(:order) }
759
+
760
+ it 'tracks job execution' do
761
+ # E11y auto-tracks job lifecycle (UC-010)
762
+ # Just test business events!
763
+
764
+ expect {
765
+ perform_enqueued_jobs do
766
+ ProcessOrderJob.perform_later(order.id)
767
+ end
768
+ }.to track_events(
769
+ Events::OrderProcessingStarted,
770
+ Events::InventoryChecked,
771
+ Events::PaymentCaptured,
772
+ Events::ShipmentScheduled,
773
+ Events::OrderProcessingCompleted
774
+ ).in_order
775
+ end
776
+
777
+ it 'preserves trace context' do
778
+ # Set trace context before enqueuing
779
+ E11y::TraceContext.with_context(trace_id: 'abc-123') do
780
+ ProcessOrderJob.perform_later(order.id)
781
+ end
782
+
783
+ perform_enqueued_jobs
784
+
785
+ # All events should have same trace_id
786
+ events = e11y_events # All events
787
+ trace_ids = events.map(&:trace_id).uniq
788
+ expect(trace_ids).to eq(['abc-123'])
789
+ end
790
+
791
+ it 'tracks errors' do
792
+ allow_any_instance_of(Order).to receive(:process!).and_raise(
793
+ StandardError.new('Processing failed')
794
+ )
795
+
796
+ expect {
797
+ perform_enqueued_jobs do
798
+ ProcessOrderJob.perform_later(order.id)
799
+ end rescue nil
800
+ }.to track_event(Events::OrderProcessingFailed)
801
+ .with(
802
+ order_id: order.id.to_s,
803
+ error: 'Processing failed'
804
+ )
805
+ end
806
+ end
807
+ end
808
+ ```
809
+
810
+ ---
811
+
812
+ ### Example 4: Integration Tests
813
+
814
+ ```ruby
815
+ # spec/integration/order_flow_spec.rb
816
+ RSpec.describe 'Order Flow', type: :request do
817
+ it 'tracks complete order lifecycle' do
818
+ # Capture all events during full flow
819
+ e11y_reset!
820
+
821
+ # 1. Create order
822
+ post '/api/orders', params: { order: order_params }
823
+ expect(response).to have_http_status(:created)
824
+
825
+ # 2. Process payment
826
+ order_id = JSON.parse(response.body)['id']
827
+ post "/api/orders/#{order_id}/payment", params: { payment: payment_params }
828
+ expect(response).to have_http_status(:ok)
829
+
830
+ # 3. Ship order
831
+ post "/api/orders/#{order_id}/ship"
832
+ expect(response).to have_http_status(:ok)
833
+
834
+ # Verify complete event flow
835
+ expect(e11y_event_classes).to match_array([
836
+ Events::OrderCreated,
837
+ Events::PaymentProcessing,
838
+ Events::PaymentSucceeded,
839
+ Events::ShipmentRequested,
840
+ Events::ShipmentCreated,
841
+ Events::NotificationSent
842
+ ])
843
+
844
+ # Verify all events share same trace_id
845
+ trace_ids = e11y_events.map(&:trace_id).uniq
846
+ expect(trace_ids.size).to eq(1)
847
+
848
+ # Take snapshot for regression testing
849
+ expect {
850
+ # Re-run full flow
851
+ }.to match_event_snapshot
852
+ end
853
+ end
854
+ ```
855
+
856
+ ---
857
+
858
+ ### Example 5: Event Schema Tests
859
+
860
+ ```ruby
861
+ # spec/events/order_created_spec.rb
862
+ RSpec.describe Events::OrderCreated do
863
+ describe '.track' do
864
+ it 'validates required fields' do
865
+ expect {
866
+ described_class.track(order_id: '123') # Missing user_id
867
+ }.to raise_error(E11y::ValidationError, /user_id is missing/)
868
+ end
869
+
870
+ it 'validates field types' do
871
+ expect {
872
+ described_class.track(
873
+ order_id: '123',
874
+ user_id: '456',
875
+ amount: 'not-a-number' # Wrong type
876
+ )
877
+ }.to raise_error(E11y::ValidationError, /amount must be a decimal/)
878
+ end
879
+
880
+ it 'tracks valid event' do
881
+ expect {
882
+ described_class.track(
883
+ order_id: '123',
884
+ user_id: '456',
885
+ amount: 99.99,
886
+ currency: 'USD'
887
+ )
888
+ }.to track_event(Events::OrderCreated)
889
+ end
890
+
891
+ it 'has valid schema definition' do
892
+ event = build(:e11y_event, :order_created)
893
+ expect(event).to have_valid_schema
894
+ end
895
+ end
896
+ end
897
+ ```
898
+
899
+ ---
900
+
901
+ ## 🔧 Configuration
902
+
903
+ ### Test Configuration
904
+
905
+ ```ruby
906
+ # spec/rails_helper.rb
907
+ require 'e11y/rspec'
908
+
909
+ RSpec.configure do |config|
910
+ # Include E11y helpers
911
+ config.include E11y::RSpec::Matchers
912
+ config.include E11y::RSpec::Helpers
913
+
914
+ # Setup E11y for tests
915
+ config.before(:suite) do
916
+ E11y.configure do |c|
917
+ # Use memory adapter (fast!)
918
+ c.adapters = [E11y::Adapters::MemoryAdapter.new]
919
+
920
+ # Disable features that slow down tests
921
+ c.rate_limiting.enabled = false
922
+ c.sampling.enabled = false
923
+ c.buffering.enabled = false
924
+
925
+ # Enable test mode
926
+ c.test_mode = true
927
+ end
928
+ end
929
+
930
+ # Clear events before each test
931
+ config.before(:each) do
932
+ E11y.reset!
933
+ end
934
+
935
+ # Snapshot testing
936
+ config.include E11y::RSpec::Snapshot, type: :request
937
+ config.before(:each, :update_snapshot) do
938
+ E11y::Snapshot.update_mode = true
939
+ end
940
+ config.after(:each, :update_snapshot) do
941
+ E11y::Snapshot.update_mode = false
942
+ end
943
+ end
944
+ ```
945
+
946
+ ---
947
+
948
+ ## 💡 Best Practices
949
+
950
+ ### ✅ DO
951
+
952
+ **1. Test event tracking, not implementation**
953
+ ```ruby
954
+ # ✅ GOOD: Test behavior (event class)
955
+ expect {
956
+ service.call
957
+ }.to track_event(Events::OrderCreated)
958
+
959
+ # ❌ BAD: Test implementation details (mocking)
960
+ expect(Events::OrderCreated).to receive(:track)
961
+ # (unless you specifically need a stub for isolation)
962
+ ```
963
+
964
+ **2. Use partial matching for flexible tests**
965
+ ```ruby
966
+ # ✅ GOOD: Test important fields only
967
+ expect {
968
+ service.call
969
+ }.to track_event(Events::OrderCreated)
970
+ .with(hash_including(order_id: '123'))
971
+
972
+ # ❌ BAD: Test every field (brittle!)
973
+ expect {
974
+ service.call
975
+ }.to track_event(Events::OrderCreated)
976
+ .with(
977
+ order_id: '123',
978
+ user_id: '456',
979
+ created_at: Time.current,
980
+ ... # 20 more fields
981
+ )
982
+ ```
983
+
984
+ **3. Test event order when it matters**
985
+ ```ruby
986
+ # ✅ GOOD: Test critical sequences
987
+ expect {
988
+ service.call
989
+ }.to track_events(
990
+ Events::PaymentAuthorized,
991
+ Events::PaymentCaptured # Must be after authorized!
992
+ ).in_order
993
+ ```
994
+
995
+ **4. Clear events between tests**
996
+ ```ruby
997
+ # ✅ GOOD: Isolated tests
998
+ config.before(:each) do
999
+ E11y.reset!
1000
+ end
1001
+ ```
1002
+
1003
+ ---
1004
+
1005
+ ### ❌ DON'T
1006
+
1007
+ **1. Don't test E11y internals**
1008
+ ```ruby
1009
+ # ❌ BAD: Testing E11y, not your code
1010
+ expect(E11y::Buffer).to receive(:push)
1011
+ expect(E11y::Adapters::LokiAdapter).to receive(:write)
1012
+
1013
+ # ✅ GOOD: Test your events
1014
+ expect { action }.to track_event(Events::OrderCreated)
1015
+ ```
1016
+
1017
+ **2. Don't use real adapters in tests**
1018
+ ```ruby
1019
+ # ❌ BAD: Slow tests!
1020
+ config.adapters = [
1021
+ E11y::Adapters::LokiAdapter.new(...) # Real HTTP calls!
1022
+ ]
1023
+
1024
+ # ✅ GOOD: Memory adapter
1025
+ config.adapters = [
1026
+ E11y::Adapters::MemoryAdapter.new # Fast!
1027
+ ]
1028
+ ```
1029
+
1030
+ **3. Don't forget to reset between tests**
1031
+ ```ruby
1032
+ # ❌ BAD: Events leak between tests
1033
+ it 'test 1' do
1034
+ # tracks event A
1035
+ end
1036
+
1037
+ it 'test 2' do
1038
+ events = e11y_events
1039
+ # Still contains event A! 💥
1040
+ end
1041
+
1042
+ # ✅ GOOD: Reset before each
1043
+ config.before(:each) { E11y.reset! }
1044
+ ```
1045
+
1046
+ ---
1047
+
1048
+ ## 📚 Related Use Cases
1049
+
1050
+ - **[UC-017: Local Development](./UC-017-local-development.md)** - Development setup
1051
+ - **[UC-016: Rails Logger Migration](./UC-016-rails-logger-migration.md)** - Migration testing
1052
+
1053
+ ---
1054
+
1055
+ ## 🎯 Summary
1056
+
1057
+ ### Testing Features
1058
+
1059
+ | Feature | Description | Benefit |
1060
+ |---------|-------------|---------|
1061
+ | **RSpec Matchers** | `track_event`, `update_metric` | Expressive tests |
1062
+ | **Helpers** | `e11y_events`, `e11y_last_event` | Easy assertions |
1063
+ | **Stubs** | Mock event tracking | Isolation |
1064
+ | **Factories** | FactoryBot integration | Test data |
1065
+ | **Snapshots** | Capture event flow | Regression testing |
1066
+
1067
+ **Test Speed:**
1068
+ - Memory adapter: <1ms per event
1069
+ - No network calls
1070
+ - No external dependencies
1071
+
1072
+ **Developer Experience:**
1073
+ - Familiar RSpec matchers
1074
+ - Clear error messages
1075
+ - Easy to debug
1076
+
1077
+ ---
1078
+
1079
+ **Document Version:** 1.0
1080
+ **Last Updated:** January 12, 2026
1081
+ **Status:** ✅ Complete