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,1836 @@
1
+ # ADR-011: Testing Strategy
2
+
3
+ **Status:** Draft
4
+ **Date:** January 12, 2026
5
+ **Covers:** UC-018 (Testing Events)
6
+ **Depends On:** ADR-001 (Core), ADR-008 (Rails Integration)
7
+
8
+ ---
9
+
10
+ ## 📋 Table of Contents
11
+
12
+ 1. [Context & Problem](#1-context--problem)
13
+ 2. [Architecture Overview](#2-architecture-overview)
14
+ 3. [RSpec Matchers](#3-rspec-matchers)
15
+ 4. [Test Adapters](#4-test-adapters)
16
+ 5. [Factory Integration](#5-factory-integration)
17
+ 6. [Snapshot Testing](#6-snapshot-testing)
18
+ 7. [Performance Testing](#7-performance-testing)
19
+ 8. [Integration Testing](#8-integration-testing)
20
+ 9. [Contract Testing](#9-contract-testing)
21
+ 10. [Security & Compliance Testing](#10-security--compliance-testing)
22
+ 11. [Trade-offs](#11-trade-offs)
23
+ 12. [Complete Example](#12-complete-example)
24
+ 13. [Summary & Key Takeaways](#13-summary--key-takeaways)
25
+
26
+ ---
27
+
28
+ ## 1. Context & Problem
29
+
30
+ ### 1.1. Problem Statement
31
+
32
+ **Current Pain Points:**
33
+
34
+ 1. **No Test Helpers:**
35
+ ```ruby
36
+ # ❌ Manual verification, brittle
37
+ it 'tracks order created event' do
38
+ order = create(:order)
39
+
40
+ # How to verify event was tracked?
41
+ # Check logs? Mock adapters? 🤷
42
+ end
43
+ ```
44
+
45
+ 2. **Adapter Pollution:**
46
+ ```ruby
47
+ # ❌ Test events go to real adapters
48
+ RSpec.describe OrdersController do
49
+ it 'creates order' do
50
+ post :create
51
+ # Event sent to Loki, Sentry in test! 😱
52
+ end
53
+ end
54
+ ```
55
+
56
+ 3. **No Event Assertions:**
57
+ ```ruby
58
+ # ❌ Can't verify event payload
59
+ it 'tracks with correct data' do
60
+ Events::OrderCreated.track(order_id: 123)
61
+
62
+ # How to assert payload[:order_id] == 123?
63
+ end
64
+ ```
65
+
66
+ 4. **Hard to Test Edge Cases:**
67
+ ```ruby
68
+ # ❌ How to test retry logic? Circuit breaker?
69
+ it 'retries on failure' do
70
+ # Simulate adapter failure?
71
+ # Verify retry attempts?
72
+ end
73
+ ```
74
+
75
+ ### 1.2. Goals
76
+
77
+ **Primary Goals:**
78
+ - ✅ **RSpec matchers** for event assertions
79
+ - ✅ **InMemory test adapter** (zero I/O)
80
+ - ✅ **Factory integration** (FactoryBot)
81
+ - ✅ **Snapshot testing** for payloads
82
+ - ✅ **Performance benchmarks**
83
+ - ✅ **Contract testing** for adapters
84
+
85
+ **Non-Goals:**
86
+ - ❌ Support other test frameworks (Minitest, etc.)
87
+ - ❌ Full mocking of E11y internals
88
+ - ❌ Production-like test environment
89
+
90
+ ### 1.3. Success Metrics
91
+
92
+ | Metric | Target | Critical? |
93
+ |--------|--------|-----------|
94
+ | **Test execution speed** | <1ms per event assertion | ✅ Yes |
95
+ | **Setup complexity** | <5 lines in spec_helper | ✅ Yes |
96
+ | **Matcher coverage** | 100% of common patterns | ✅ Yes |
97
+ | **False positive rate** | <1% | ✅ Yes |
98
+
99
+ ---
100
+
101
+ ## 2. Architecture Overview
102
+
103
+ ### 2.1. System Context
104
+
105
+ ```mermaid
106
+ C4Context
107
+ title Testing Strategy Context
108
+
109
+ Person(dev, "Developer", "Writes tests")
110
+
111
+ System(rspec, "RSpec Suite", "Test suite")
112
+
113
+ System(e11y, "E11y Gem", "Event tracking")
114
+
115
+ System_Ext(test_adapter, "InMemory Adapter", "Test-only adapter")
116
+ System_Ext(factories, "FactoryBot", "Test data")
117
+
118
+ Rel(dev, rspec, "Runs tests", "bundle exec rspec")
119
+ Rel(rspec, e11y, "Uses matchers", "expect().to have_tracked_event")
120
+ Rel(e11y, test_adapter, "Stores events", "In-memory array")
121
+ Rel(rspec, factories, "Creates test data", "create(:order)")
122
+ ```
123
+
124
+ ### 2.2. Component Architecture
125
+
126
+ ```mermaid
127
+ graph TB
128
+ subgraph "RSpec Test Suite"
129
+ TestCase[Test Case] --> Matcher[E11y RSpec Matcher]
130
+ TestCase --> Factory[FactoryBot]
131
+ end
132
+
133
+ subgraph "E11y Testing Module"
134
+ Matcher --> TestAdapter[InMemory Test Adapter]
135
+ Matcher --> Assertions[Assertion Helpers]
136
+
137
+ TestAdapter --> EventStore[Event Store<br/>In-Memory Array]
138
+
139
+ Assertions --> PayloadMatcher[Payload Matcher]
140
+ Assertions --> SeverityMatcher[Severity Matcher]
141
+ Assertions --> CountMatcher[Count Matcher]
142
+ end
143
+
144
+ subgraph "E11y Core"
145
+ EventStore --> Pipeline[E11y Pipeline]
146
+ Pipeline --> EventClass[Event Classes]
147
+ end
148
+
149
+ subgraph "Test Helpers"
150
+ Factory --> EventFactory[Event Factories]
151
+ Factory --> TraceFactory[Trace Context Factory]
152
+ end
153
+
154
+ style TestAdapter fill:#d1ecf1
155
+ style Matcher fill:#fff3cd
156
+ style EventStore fill:#d4edda
157
+ ```
158
+
159
+ ### 2.3. Test Flow
160
+
161
+ ```mermaid
162
+ sequenceDiagram
163
+ participant Test as RSpec Test
164
+ participant Matcher as E11y Matcher
165
+ participant Adapter as InMemory Adapter
166
+ participant App as Application Code
167
+
168
+ Note over Test: Setup phase
169
+ Test->>Test: before(:each)
170
+ Test->>Adapter: Clear event store
171
+
172
+ Note over Test: Exercise phase
173
+ Test->>App: Trigger action
174
+ App->>E11y: Track event
175
+ E11y->>Adapter: Store event (in-memory)
176
+
177
+ Note over Test: Verify phase
178
+ Test->>Matcher: expect().to have_tracked_event
179
+ Matcher->>Adapter: Query event store
180
+ Adapter-->>Matcher: Return matching events
181
+ Matcher->>Matcher: Assert payload/severity/count
182
+ Matcher-->>Test: Pass/Fail
183
+
184
+ Note over Test: Teardown phase
185
+ Test->>Adapter: Clear event store
186
+ ```
187
+
188
+ ---
189
+
190
+ ## 3. RSpec Matchers
191
+
192
+ ### 3.1. Core Matchers
193
+
194
+ ```ruby
195
+ # lib/e11y/testing/rspec_matchers.rb
196
+ module E11y
197
+ module Testing
198
+ module RSpecMatchers
199
+ # Main matcher: have_tracked_event
200
+ def have_tracked_event(event_class_or_pattern)
201
+ HaveTrackedEventMatcher.new(event_class_or_pattern)
202
+ end
203
+
204
+ # Alias for readability
205
+ alias_method :track_event, :have_tracked_event
206
+
207
+ class HaveTrackedEventMatcher
208
+ def initialize(event_class_or_pattern)
209
+ @event_class_or_pattern = event_class_or_pattern
210
+ @payload_matchers = {}
211
+ @severity_matcher = nil
212
+ @count_matcher = nil
213
+ @trace_id_matcher = nil
214
+ end
215
+
216
+ # Chain: with payload
217
+ def with(payload_hash)
218
+ @payload_matchers = payload_hash
219
+ self
220
+ end
221
+
222
+ # Chain: with severity
223
+ def with_severity(severity)
224
+ @severity_matcher = severity
225
+ self
226
+ end
227
+
228
+ # Chain: exactly N times
229
+ def exactly(count)
230
+ @count_matcher = count
231
+ self
232
+ end
233
+
234
+ # Chain: at_least N times
235
+ def at_least(count)
236
+ @count_matcher = [:at_least, count]
237
+ self
238
+ end
239
+
240
+ # Chain: at_most N times
241
+ def at_most(count)
242
+ @count_matcher = [:at_most, count]
243
+ self
244
+ end
245
+
246
+ # Chain: once
247
+ def once
248
+ exactly(1)
249
+ end
250
+
251
+ # Chain: twice
252
+ def twice
253
+ exactly(2)
254
+ end
255
+
256
+ # Chain: with trace_id
257
+ def with_trace_id(trace_id)
258
+ @trace_id_matcher = trace_id
259
+ self
260
+ end
261
+
262
+ # Matcher implementation
263
+ def matches?(actual = nil)
264
+ @events = find_matching_events
265
+
266
+ return false if @events.empty?
267
+ return false unless count_matches?
268
+ return false unless payload_matches?
269
+ return false unless severity_matches?
270
+ return false unless trace_id_matches?
271
+
272
+ true
273
+ end
274
+
275
+ def failure_message
276
+ if @events.empty?
277
+ no_events_message
278
+ elsif !count_matches?
279
+ count_mismatch_message
280
+ elsif !payload_matches?
281
+ payload_mismatch_message
282
+ elsif !severity_matches?
283
+ severity_mismatch_message
284
+ else
285
+ trace_id_mismatch_message
286
+ end
287
+ end
288
+
289
+ def failure_message_when_negated
290
+ "expected not to have tracked #{event_name}, but it was tracked"
291
+ end
292
+
293
+ def supports_block_expectations?
294
+ true
295
+ end
296
+
297
+ private
298
+
299
+ def find_matching_events
300
+ E11y.test_adapter.find_events(event_pattern)
301
+ end
302
+
303
+ def event_pattern
304
+ case @event_class_or_pattern
305
+ when Class
306
+ @event_class_or_pattern.event_name
307
+ when String, Regexp
308
+ @event_class_or_pattern
309
+ else
310
+ raise ArgumentError, "Invalid event pattern: #{@event_class_or_pattern}"
311
+ end
312
+ end
313
+
314
+ def event_name
315
+ case @event_class_or_pattern
316
+ when Class
317
+ @event_class_or_pattern.name
318
+ else
319
+ @event_class_or_pattern.to_s
320
+ end
321
+ end
322
+
323
+ def count_matches?
324
+ return true unless @count_matcher
325
+
326
+ case @count_matcher
327
+ when Integer
328
+ @events.size == @count_matcher
329
+ when Array
330
+ operator, expected = @count_matcher
331
+ case operator
332
+ when :at_least
333
+ @events.size >= expected
334
+ when :at_most
335
+ @events.size <= expected
336
+ end
337
+ end
338
+ end
339
+
340
+ def payload_matches?
341
+ return true if @payload_matchers.empty?
342
+
343
+ @events.any? do |event|
344
+ @payload_matchers.all? do |key, expected_value|
345
+ actual_value = event[:payload][key]
346
+
347
+ case expected_value
348
+ when RSpec::Matchers::BuiltIn::BaseMatcher
349
+ expected_value.matches?(actual_value)
350
+ when Proc
351
+ expected_value.call(actual_value)
352
+ else
353
+ actual_value == expected_value
354
+ end
355
+ end
356
+ end
357
+ end
358
+
359
+ def severity_matches?
360
+ return true unless @severity_matcher
361
+
362
+ @events.any? { |event| event[:severity] == @severity_matcher }
363
+ end
364
+
365
+ def trace_id_matches?
366
+ return true unless @trace_id_matcher
367
+
368
+ @events.any? { |event| event[:trace_id] == @trace_id_matcher }
369
+ end
370
+
371
+ def no_events_message
372
+ all_events = E11y.test_adapter.all_events
373
+
374
+ if all_events.empty?
375
+ "expected to have tracked #{event_name}, but no events were tracked at all"
376
+ else
377
+ tracked_names = all_events.map { |e| e[:event_name] }.uniq.join(', ')
378
+ "expected to have tracked #{event_name}, but only tracked: #{tracked_names}"
379
+ end
380
+ end
381
+
382
+ def count_mismatch_message
383
+ expected_count = case @count_matcher
384
+ when Integer
385
+ "exactly #{@count_matcher}"
386
+ when Array
387
+ operator, count = @count_matcher
388
+ "#{operator.to_s.gsub('_', ' ')} #{count}"
389
+ end
390
+
391
+ "expected to track #{event_name} #{expected_count} times, but tracked #{@events.size} times"
392
+ end
393
+
394
+ def payload_mismatch_message
395
+ "expected #{event_name} with payload #{@payload_matchers.inspect}, " \
396
+ "but got:\n#{format_events(@events)}"
397
+ end
398
+
399
+ def severity_mismatch_message
400
+ severities = @events.map { |e| e[:severity] }.uniq.join(', ')
401
+ "expected #{event_name} with severity :#{@severity_matcher}, " \
402
+ "but got severities: #{severities}"
403
+ end
404
+
405
+ def trace_id_mismatch_message
406
+ trace_ids = @events.map { |e| e[:trace_id] }.uniq.join(', ')
407
+ "expected #{event_name} with trace_id #{@trace_id_matcher}, " \
408
+ "but got trace_ids: #{trace_ids}"
409
+ end
410
+
411
+ def format_events(events)
412
+ events.map.with_index do |event, index|
413
+ " [#{index + 1}] #{event[:payload].inspect}"
414
+ end.join("\n")
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
420
+ ```
421
+
422
+ ### 3.2. Usage Examples
423
+
424
+ ```ruby
425
+ RSpec.describe OrdersController, type: :controller do
426
+ describe 'POST #create' do
427
+ it 'tracks order created event' do
428
+ expect {
429
+ post :create, params: { order: { item: 'Book', price: 29.99 } }
430
+ }.to have_tracked_event(Events::OrderCreated)
431
+ end
432
+
433
+ it 'tracks with correct payload' do
434
+ post :create, params: { order: { item: 'Book', price: 29.99 } }
435
+
436
+ expect(Events::OrderCreated).to have_been_tracked
437
+ .with(item: 'Book', price: 29.99)
438
+ end
439
+
440
+ it 'tracks with correct severity' do
441
+ post :create
442
+
443
+ expect(Events::OrderCreated).to have_been_tracked
444
+ .with_severity(:info)
445
+ end
446
+
447
+ it 'tracks exactly once' do
448
+ post :create
449
+
450
+ expect(Events::OrderCreated).to have_been_tracked.once
451
+ end
452
+
453
+ it 'tracks multiple times' do
454
+ 3.times { post :create }
455
+
456
+ expect(Events::OrderCreated).to have_been_tracked
457
+ .exactly(3)
458
+ end
459
+
460
+ it 'uses RSpec matchers in payload' do
461
+ post :create, params: { order: { price: 29.99 } }
462
+
463
+ expect(Events::OrderCreated).to have_been_tracked
464
+ .with(
465
+ order_id: be_a(Integer),
466
+ price: be_within(0.01).of(29.99),
467
+ created_at: be_a(Time)
468
+ )
469
+ end
470
+
471
+ it 'uses custom matchers' do
472
+ post :create
473
+
474
+ expect(Events::OrderCreated).to have_been_tracked
475
+ .with(
476
+ amount: ->(val) { val > 0 }
477
+ )
478
+ end
479
+
480
+ it 'verifies trace context' do
481
+ post :create
482
+
483
+ expect(Events::OrderCreated).to have_been_tracked
484
+ .with_trace_id(E11y::Current.trace_id)
485
+ end
486
+ end
487
+ end
488
+ ```
489
+
490
+ ---
491
+
492
+ ## 4. Test Adapters
493
+
494
+ ### 4.1. InMemory Adapter
495
+
496
+ ```ruby
497
+ # lib/e11y/adapters/in_memory.rb
498
+ module E11y
499
+ module Adapters
500
+ class InMemory < Base
501
+ def initialize
502
+ super(name: :test)
503
+ @events = []
504
+ @mutex = Mutex.new
505
+ end
506
+
507
+ def send_batch(events)
508
+ @mutex.synchronize do
509
+ events.each do |event|
510
+ @events << event.dup
511
+ end
512
+ end
513
+
514
+ { success: true, sent: events.size }
515
+ end
516
+
517
+ def health_check
518
+ true
519
+ end
520
+
521
+ # Test helper methods
522
+ def all_events
523
+ @mutex.synchronize { @events.dup }
524
+ end
525
+
526
+ def find_events(pattern)
527
+ @mutex.synchronize do
528
+ @events.select do |event|
529
+ case pattern
530
+ when String
531
+ event[:event_name] == pattern
532
+ when Regexp
533
+ event[:event_name] =~ pattern
534
+ when Class
535
+ event[:event_name] == pattern.event_name
536
+ else
537
+ false
538
+ end
539
+ end
540
+ end
541
+ end
542
+
543
+ def find_event(pattern)
544
+ find_events(pattern).first
545
+ end
546
+
547
+ def event_count(pattern = nil)
548
+ if pattern
549
+ find_events(pattern).size
550
+ else
551
+ @mutex.synchronize { @events.size }
552
+ end
553
+ end
554
+
555
+ def clear!
556
+ @mutex.synchronize { @events.clear }
557
+ end
558
+
559
+ def tracked?(event_class_or_pattern)
560
+ find_events(event_class_or_pattern).any?
561
+ end
562
+
563
+ # Pretty print for debugging
564
+ def inspect
565
+ "#<E11y::Adapters::InMemory events=#{@events.size}>"
566
+ end
567
+
568
+ def to_s
569
+ inspect
570
+ end
571
+ end
572
+ end
573
+ end
574
+ ```
575
+
576
+ ### 4.2. Mock Adapter (For Failure Testing)
577
+
578
+ ```ruby
579
+ # lib/e11y/adapters/mock.rb
580
+ module E11y
581
+ module Adapters
582
+ class Mock < Base
583
+ attr_accessor :should_fail, :failure_error, :delay_ms
584
+
585
+ def initialize(config = {})
586
+ super(name: :mock)
587
+ @should_fail = config[:should_fail] || false
588
+ @failure_error = config[:failure_error] || StandardError.new('Mock failure')
589
+ @delay_ms = config[:delay_ms] || 0
590
+ @call_count = 0
591
+ end
592
+
593
+ def send_batch(events)
594
+ @call_count += 1
595
+
596
+ # Simulate delay
597
+ sleep(@delay_ms / 1000.0) if @delay_ms > 0
598
+
599
+ # Simulate failure
600
+ raise @failure_error if @should_fail
601
+
602
+ { success: true, sent: events.size }
603
+ end
604
+
605
+ def health_check
606
+ !@should_fail
607
+ end
608
+
609
+ def call_count
610
+ @call_count
611
+ end
612
+
613
+ def reset!
614
+ @call_count = 0
615
+ @should_fail = false
616
+ end
617
+ end
618
+ end
619
+ end
620
+ ```
621
+
622
+ ### 4.3. RSpec Configuration
623
+
624
+ ```ruby
625
+ # spec/support/e11y.rb
626
+ RSpec.configure do |config|
627
+ # Use InMemory adapter for all tests
628
+ config.before(:suite) do
629
+ E11y.configure do |e11y_config|
630
+ # Clear all adapters
631
+ e11y_config.adapters.clear
632
+
633
+ # Register only test adapter
634
+ e11y_config.adapters.register :test, E11y::Adapters::InMemory.new
635
+
636
+ # Disable rate limiting in tests
637
+ e11y_config.rate_limiting.enabled = false
638
+
639
+ # Disable sampling (track everything)
640
+ e11y_config.sampling.default_sample_rate = 1.0
641
+
642
+ # Fast buffer flush (no delays)
643
+ e11y_config.buffer.flush_interval = 0.milliseconds
644
+ end
645
+ end
646
+
647
+ # Clear events between tests
648
+ config.before(:each) do
649
+ E11y.test_adapter.clear!
650
+ end
651
+
652
+ # Reset trace context
653
+ config.after(:each) do
654
+ E11y::Current.reset
655
+ end
656
+
657
+ # Include matchers
658
+ config.include E11y::Testing::RSpecMatchers
659
+
660
+ # Alias for convenience
661
+ config.alias_example_group_to :describe_event, type: :event
662
+ end
663
+
664
+ # Convenience method to access test adapter
665
+ module E11y
666
+ def self.test_adapter
667
+ Adapters::Registry.get(:test)
668
+ end
669
+ end
670
+ ```
671
+
672
+ ---
673
+
674
+ ## 5. Factory Integration
675
+
676
+ ### 5.1. Event Factories
677
+
678
+ ```ruby
679
+ # spec/factories/events.rb
680
+ FactoryBot.define do
681
+ # Base event factory
682
+ factory :event, class: Hash do
683
+ transient do
684
+ event_class { Events::Generic }
685
+ end
686
+
687
+ event_name { event_class.event_name }
688
+ severity { :info }
689
+ timestamp { Time.now }
690
+ trace_id { E11y::TraceContext.generate_id }
691
+ payload { {} }
692
+
693
+ initialize_with { attributes }
694
+
695
+ # Specific event factories
696
+ factory :order_created_event do
697
+ event_class { Events::OrderCreated }
698
+ payload do
699
+ {
700
+ order_id: Faker::Number.number(digits: 10),
701
+ user_id: Faker::Number.number(digits: 5),
702
+ amount: Faker::Commerce.price,
703
+ currency: 'USD'
704
+ }
705
+ end
706
+ end
707
+
708
+ factory :payment_processed_event do
709
+ event_class { Events::PaymentProcessed }
710
+ severity { :success }
711
+ payload do
712
+ {
713
+ payment_id: Faker::Alphanumeric.alphanumeric(number: 20),
714
+ order_id: Faker::Number.number(digits: 10),
715
+ amount: Faker::Commerce.price,
716
+ status: 'completed'
717
+ }
718
+ end
719
+ end
720
+
721
+ factory :error_event do
722
+ event_class { Events::ErrorOccurred }
723
+ severity { :error }
724
+ payload do
725
+ {
726
+ error_class: 'StandardError',
727
+ error_message: Faker::Lorem.sentence,
728
+ backtrace: Faker::Lorem.paragraphs(number: 3)
729
+ }
730
+ end
731
+ end
732
+ end
733
+
734
+ # Trace context factory
735
+ factory :trace_context, class: Hash do
736
+ trace_id { E11y::TraceContext.generate_id }
737
+ span_id { E11y::TraceContext.generate_span_id }
738
+ sampled { true }
739
+ user_id { Faker::Number.number(digits: 5) }
740
+
741
+ initialize_with { attributes }
742
+ end
743
+ end
744
+ ```
745
+
746
+ ### 5.2. Usage with Factories
747
+
748
+ ```ruby
749
+ RSpec.describe 'Event tracking with factories' do
750
+ it 'builds event payload' do
751
+ payload = build(:order_created_event)[:payload]
752
+
753
+ Events::OrderCreated.track(payload)
754
+
755
+ expect(Events::OrderCreated).to have_been_tracked
756
+ .with(order_id: payload[:order_id])
757
+ end
758
+
759
+ it 'sets up trace context' do
760
+ context = build(:trace_context)
761
+
762
+ E11y::Current.set(context)
763
+
764
+ Events::OrderCreated.track(order_id: 123)
765
+
766
+ expect(Events::OrderCreated).to have_been_tracked
767
+ .with_trace_id(context[:trace_id])
768
+ end
769
+ end
770
+ ```
771
+
772
+ ---
773
+
774
+ ## 6. Snapshot Testing
775
+
776
+ ### 6.1. Snapshot Matcher
777
+
778
+ ```ruby
779
+ # lib/e11y/testing/snapshot_matcher.rb
780
+ module E11y
781
+ module Testing
782
+ module SnapshotMatcher
783
+ def match_snapshot(snapshot_name)
784
+ SnapshotMatcher.new(snapshot_name)
785
+ end
786
+
787
+ class SnapshotMatcher
788
+ def initialize(snapshot_name)
789
+ @snapshot_name = snapshot_name
790
+ @snapshot_dir = Rails.root.join('spec', 'snapshots', 'events')
791
+ @snapshot_file = @snapshot_dir.join("#{@snapshot_name}.yml")
792
+ end
793
+
794
+ def matches?(actual)
795
+ @actual = normalize_event(actual)
796
+
797
+ if File.exist?(@snapshot_file)
798
+ @expected = YAML.load_file(@snapshot_file)
799
+ @actual == @expected
800
+ else
801
+ # First run: save snapshot
802
+ save_snapshot(@actual)
803
+ true
804
+ end
805
+ end
806
+
807
+ def failure_message
808
+ "Expected event to match snapshot #{@snapshot_name}\n\n" \
809
+ "Expected:\n#{@expected.to_yaml}\n\n" \
810
+ "Actual:\n#{@actual.to_yaml}\n\n" \
811
+ "Run UPDATE_SNAPSHOTS=1 rspec to update snapshot"
812
+ end
813
+
814
+ private
815
+
816
+ def normalize_event(event)
817
+ event = event.deep_symbolize_keys if event.respond_to?(:deep_symbolize_keys)
818
+
819
+ # Remove volatile fields
820
+ event.except(:timestamp, :trace_id, :span_id)
821
+ end
822
+
823
+ def save_snapshot(data)
824
+ FileUtils.mkdir_p(@snapshot_dir)
825
+ File.write(@snapshot_file, data.to_yaml)
826
+ end
827
+ end
828
+ end
829
+ end
830
+ end
831
+ ```
832
+
833
+ ### 6.2. Snapshot Usage
834
+
835
+ ```ruby
836
+ RSpec.describe Events::OrderCreated do
837
+ it 'matches expected structure' do
838
+ Events::OrderCreated.track(
839
+ order_id: 123,
840
+ user_id: 456,
841
+ amount: 99.99,
842
+ currency: 'USD'
843
+ )
844
+
845
+ event = E11y.test_adapter.find_event(Events::OrderCreated)
846
+
847
+ expect(event).to match_snapshot('order_created_event')
848
+ end
849
+ end
850
+
851
+ # First run: creates spec/snapshots/events/order_created_event.yml
852
+ # Subsequent runs: compares against saved snapshot
853
+ # Update snapshots: UPDATE_SNAPSHOTS=1 bundle exec rspec
854
+ ```
855
+
856
+ ---
857
+
858
+ ## 7. Performance Testing
859
+
860
+ ### 7.1. Performance Benchmarks
861
+
862
+ ```ruby
863
+ # spec/performance/event_tracking_spec.rb
864
+ RSpec.describe 'Event tracking performance', type: :performance do
865
+ it 'tracks 1000 events in < 100ms' do
866
+ elapsed = Benchmark.realtime do
867
+ 1000.times do |i|
868
+ Events::OrderCreated.track(order_id: i)
869
+ end
870
+ end
871
+
872
+ expect(elapsed).to be < 0.1 # 100ms
873
+ end
874
+
875
+ it 'handles concurrent tracking' do
876
+ threads = 10.times.map do |thread_id|
877
+ Thread.new do
878
+ 100.times do |i|
879
+ Events::OrderCreated.track(
880
+ order_id: "#{thread_id}-#{i}"
881
+ )
882
+ end
883
+ end
884
+ end
885
+
886
+ threads.each(&:join)
887
+
888
+ expect(E11y.test_adapter.event_count).to eq(1000)
889
+ end
890
+
891
+ it 'has low memory overhead' do
892
+ before_memory = memory_usage
893
+
894
+ 1000.times { Events::OrderCreated.track(order_id: 1) }
895
+
896
+ after_memory = memory_usage
897
+ memory_increase = after_memory - before_memory
898
+
899
+ # Should use < 1MB for 1000 events
900
+ expect(memory_increase).to be < 1.megabyte
901
+ end
902
+
903
+ private
904
+
905
+ def memory_usage
906
+ `ps -o rss= -p #{Process.pid}`.to_i * 1024 # bytes
907
+ end
908
+ end
909
+ ```
910
+
911
+ ### 7.2. Benchmark Helpers
912
+
913
+ ```ruby
914
+ # spec/support/benchmark_helpers.rb
915
+ module BenchmarkHelpers
916
+ def benchmark(label = nil, &block)
917
+ label ||= caller_locations(1, 1).first.label
918
+
919
+ result = nil
920
+ elapsed = Benchmark.realtime { result = block.call }
921
+
922
+ puts "#{label}: #{(elapsed * 1000).round(2)}ms"
923
+
924
+ result
925
+ end
926
+
927
+ def profile_memory(&block)
928
+ before = memory_usage
929
+ result = block.call
930
+ after = memory_usage
931
+
932
+ increase = ((after - before) / 1024.0).round(2)
933
+ puts "Memory increase: #{increase} KB"
934
+
935
+ result
936
+ end
937
+ end
938
+
939
+ RSpec.configure do |config|
940
+ config.include BenchmarkHelpers, type: :performance
941
+ end
942
+ ```
943
+
944
+ ---
945
+
946
+ ## 8. Integration Testing
947
+
948
+ ### 8.1. Full Stack Integration Tests
949
+
950
+ ```ruby
951
+ # spec/integration/order_flow_spec.rb
952
+ RSpec.describe 'Order flow integration', type: :integration do
953
+ it 'tracks all events in order creation flow' do
954
+ user = create(:user)
955
+
956
+ # Simulate full order flow
957
+ order = nil
958
+
959
+ expect {
960
+ order = create_order_as(user, items: [{ name: 'Book', price: 29.99 }])
961
+ }.to have_tracked_event(Events::OrderCreated)
962
+ .and have_tracked_event(Events::PaymentProcessed)
963
+ .and have_tracked_event(Events::EmailQueued)
964
+
965
+ # Verify event sequence
966
+ events = E11y.test_adapter.all_events
967
+ event_names = events.map { |e| e[:event_name] }
968
+
969
+ expect(event_names).to eq([
970
+ 'Events::OrderCreated',
971
+ 'Events::PaymentProcessed',
972
+ 'Events::EmailQueued'
973
+ ])
974
+
975
+ # Verify trace context propagation
976
+ trace_ids = events.map { |e| e[:trace_id] }.uniq
977
+ expect(trace_ids.size).to eq(1) # All events share same trace_id
978
+ end
979
+
980
+ it 'propagates trace to background jobs' do
981
+ order = create(:order)
982
+
983
+ expect {
984
+ SendOrderEmailJob.perform_later(order.id)
985
+ }.to have_tracked_event(Events::Rails::Job::Enqueued)
986
+
987
+ # Execute job
988
+ perform_enqueued_jobs
989
+
990
+ # Verify job tracked event with same trace_id
991
+ enqueued_event = E11y.test_adapter.find_event(Events::Rails::Job::Enqueued)
992
+ completed_event = E11y.test_adapter.find_event(Events::Rails::Job::Completed)
993
+
994
+ expect(completed_event[:trace_id]).to eq(enqueued_event[:trace_id])
995
+ end
996
+ end
997
+ ```
998
+
999
+ ### 8.2. Adapter Integration Tests
1000
+
1001
+ ```ruby
1002
+ # spec/integration/adapters_spec.rb
1003
+ RSpec.describe 'Adapter integration', type: :integration do
1004
+ it 'sends events to multiple adapters' do
1005
+ # Reconfigure with multiple test adapters
1006
+ adapter1 = E11y::Adapters::InMemory.new
1007
+ adapter2 = E11y::Adapters::InMemory.new
1008
+
1009
+ E11y.configure do |config|
1010
+ config.adapters.clear
1011
+ config.adapters.register :adapter1, adapter1
1012
+ config.adapters.register :adapter2, adapter2
1013
+ end
1014
+
1015
+ Events::OrderCreated.track(order_id: 123)
1016
+
1017
+ # Verify both adapters received event
1018
+ expect(adapter1.event_count).to eq(1)
1019
+ expect(adapter2.event_count).to eq(1)
1020
+ end
1021
+
1022
+ it 'handles adapter failures gracefully' do
1023
+ failing_adapter = E11y::Adapters::Mock.new(should_fail: true)
1024
+ working_adapter = E11y::Adapters::InMemory.new
1025
+
1026
+ E11y.configure do |config|
1027
+ config.adapters.clear
1028
+ config.adapters.register :failing, failing_adapter
1029
+ config.adapters.register :working, working_adapter
1030
+ end
1031
+
1032
+ # Should not raise, working adapter should receive event
1033
+ expect {
1034
+ Events::OrderCreated.track(order_id: 123)
1035
+ }.not_to raise_error
1036
+
1037
+ expect(working_adapter.event_count).to eq(1)
1038
+ end
1039
+ end
1040
+ ```
1041
+
1042
+ ---
1043
+
1044
+ ## 9. Contract Testing
1045
+
1046
+ ### 9.1. Adapter Contract Tests
1047
+
1048
+ ```ruby
1049
+ # spec/contracts/adapter_contract_spec.rb
1050
+ RSpec.shared_examples 'an E11y adapter' do
1051
+ let(:adapter) { described_class.new }
1052
+
1053
+ describe '#send_batch' do
1054
+ it 'accepts array of events' do
1055
+ events = [build(:event)]
1056
+
1057
+ expect { adapter.send_batch(events) }.not_to raise_error
1058
+ end
1059
+
1060
+ it 'returns success hash' do
1061
+ events = [build(:event)]
1062
+ result = adapter.send_batch(events)
1063
+
1064
+ expect(result).to be_a(Hash)
1065
+ expect(result).to have_key(:success)
1066
+ expect(result[:success]).to be_in([true, false])
1067
+ end
1068
+
1069
+ it 'handles empty batch' do
1070
+ expect { adapter.send_batch([]) }.not_to raise_error
1071
+ end
1072
+
1073
+ it 'raises on invalid input' do
1074
+ expect { adapter.send_batch(nil) }.to raise_error(ArgumentError)
1075
+ end
1076
+ end
1077
+
1078
+ describe '#health_check' do
1079
+ it 'returns boolean' do
1080
+ result = adapter.health_check
1081
+ expect(result).to be_in([true, false])
1082
+ end
1083
+ end
1084
+
1085
+ describe '#capabilities' do
1086
+ it 'returns array of symbols' do
1087
+ capabilities = adapter.capabilities
1088
+
1089
+ expect(capabilities).to be_an(Array)
1090
+ expect(capabilities).to all(be_a(Symbol))
1091
+ end
1092
+ end
1093
+ end
1094
+
1095
+ # Use in adapter specs
1096
+ RSpec.describe E11y::Adapters::Loki do
1097
+ it_behaves_like 'an E11y adapter'
1098
+ end
1099
+
1100
+ RSpec.describe E11y::Adapters::Sentry do
1101
+ it_behaves_like 'an E11y adapter'
1102
+ end
1103
+ ```
1104
+
1105
+ ### 9.2. Event Contract Tests
1106
+
1107
+ ```ruby
1108
+ # spec/contracts/event_contract_spec.rb
1109
+ RSpec.shared_examples 'an E11y event' do
1110
+ describe '.track' do
1111
+ it 'accepts payload hash' do
1112
+ expect { described_class.track({}) }.not_to raise_error
1113
+ end
1114
+
1115
+ it 'validates schema' do
1116
+ invalid_payload = { invalid_key: 'value' }
1117
+
1118
+ expect {
1119
+ described_class.track(invalid_payload)
1120
+ }.to raise_error(E11y::SchemaValidationError)
1121
+ end
1122
+
1123
+ it 'returns nil' do
1124
+ result = described_class.track(valid_payload)
1125
+ expect(result).to be_nil
1126
+ end
1127
+ end
1128
+
1129
+ describe '.event_name' do
1130
+ it 'returns string' do
1131
+ expect(described_class.event_name).to be_a(String)
1132
+ end
1133
+
1134
+ it 'matches class name pattern' do
1135
+ expect(described_class.event_name).to match(/^Events::/)
1136
+ end
1137
+ end
1138
+
1139
+ describe '.schema' do
1140
+ it 'is defined' do
1141
+ expect(described_class.schema).to be_present
1142
+ end
1143
+ end
1144
+ end
1145
+
1146
+ # Use in event specs
1147
+ RSpec.describe Events::OrderCreated do
1148
+ let(:valid_payload) do
1149
+ {
1150
+ order_id: 123,
1151
+ user_id: 456,
1152
+ amount: 99.99
1153
+ }
1154
+ end
1155
+
1156
+ it_behaves_like 'an E11y event'
1157
+ end
1158
+ ```
1159
+
1160
+ ---
1161
+
1162
+ ## 10. Security & Compliance Testing
1163
+
1164
+ ### 10.1. Problem: Critical Features Need User Verification
1165
+
1166
+ **Why This Matters:**
1167
+
1168
+ ```ruby
1169
+ # ❌ GDPR violation if PII filtering broken:
1170
+ # Company: $50M fine for data breach
1171
+ # Reputation: Destroyed
1172
+
1173
+ # ❌ Cost overrun if routing broken:
1174
+ # Expected: $100/month (debug → file)
1175
+ # Actual: $10,000/month (debug → Sentry)
1176
+
1177
+ # Users MUST be able to verify these features work!
1178
+ ```
1179
+
1180
+ ### 10.2. Architecture: Multiple InMemory Adapters
1181
+
1182
+ **Design Decision:** Use separate `InMemory` adapters per destination to verify:
1183
+ 1. ✅ PII filtering per-adapter
1184
+ 2. ✅ Event routing to correct adapters
1185
+ 3. ✅ Rate limiting behavior
1186
+ 4. ✅ Sampling behavior
1187
+
1188
+ ```mermaid
1189
+ graph TB
1190
+ subgraph "Test Setup"
1191
+ TestCase[RSpec Test] --> Config[E11y Config]
1192
+ Config --> Register1[Register :sentry adapter]
1193
+ Config --> Register2[Register :file_audit adapter]
1194
+ Config --> Register3[Register :file adapter]
1195
+ end
1196
+
1197
+ subgraph "Event Tracking"
1198
+ TestCase --> TrackEvent[Track Event]
1199
+ TrackEvent --> Pipeline[E11y Pipeline]
1200
+ Pipeline --> PiiFilter[PII Filtering]
1201
+ PiiFilter --> Router[Adapter Router]
1202
+ end
1203
+
1204
+ subgraph "Separate InMemory Adapters"
1205
+ Router --> SentryAdapter[InMemory :sentry<br/>Email: HASHED<br/>Password: MASKED]
1206
+ Router --> AuditAdapter[InMemory :file_audit<br/>Email: ORIGINAL<br/>Password: MASKED]
1207
+ Router --> FileAdapter[InMemory :file<br/>No event]
1208
+ end
1209
+
1210
+ subgraph "Verification Phase"
1211
+ TestCase --> VerifySentry[Assert Sentry got hashed email]
1212
+ TestCase --> VerifyAudit[Assert Audit got original email]
1213
+ TestCase --> VerifyFile[Assert File got nothing]
1214
+
1215
+ VerifySentry --> SentryAdapter
1216
+ VerifyAudit --> AuditAdapter
1217
+ VerifyFile --> FileAdapter
1218
+ end
1219
+
1220
+ style PiiFilter fill:#f8d7da
1221
+ style Router fill:#fff3cd
1222
+ style SentryAdapter fill:#d1ecf1
1223
+ style AuditAdapter fill:#d1ecf1
1224
+ style FileAdapter fill:#d1ecf1
1225
+ ```
1226
+
1227
+ ### 10.3. PII Filtering Test Helpers
1228
+
1229
+ ```ruby
1230
+ # lib/e11y/testing/pii_helpers.rb
1231
+ module E11y
1232
+ module Testing
1233
+ module PiiHelpers
1234
+ # Verify field was filtered with specific strategy
1235
+ def have_field_filtered(field, strategy, options = {})
1236
+ PiiFilterMatcher.new(field, strategy, options)
1237
+ end
1238
+
1239
+ class PiiFilterMatcher
1240
+ def initialize(field, strategy, options)
1241
+ @field = field
1242
+ @strategy = strategy
1243
+ @adapter = options[:for_adapter]
1244
+ @algorithm = options[:algorithm]
1245
+ end
1246
+
1247
+ def matches?(event)
1248
+ @actual_value = event.dig(:payload, @field)
1249
+
1250
+ case @strategy
1251
+ when :mask
1252
+ @actual_value == '[FILTERED]'
1253
+ when :hash
1254
+ verify_hash
1255
+ when :skip
1256
+ !event[:payload].key?(@field)
1257
+ when :truncate
1258
+ @actual_value.length <= (event[:metadata][:pii_truncate_length] || 100)
1259
+ when :redact
1260
+ @actual_value == '[REDACTED]'
1261
+ else
1262
+ false
1263
+ end
1264
+ end
1265
+
1266
+ def failure_message
1267
+ "expected field '#{@field}' to be filtered with strategy :#{@strategy}, " \
1268
+ "but got: #{@actual_value.inspect}"
1269
+ end
1270
+
1271
+ def failure_message_when_negated
1272
+ "expected field '#{@field}' NOT to be filtered with :#{@strategy}, " \
1273
+ "but it was"
1274
+ end
1275
+
1276
+ private
1277
+
1278
+ def verify_hash
1279
+ return false unless @actual_value.is_a?(String)
1280
+
1281
+ case @algorithm || :sha256
1282
+ when :sha256
1283
+ @actual_value =~ /^[a-f0-9]{64}$/
1284
+ when :md5
1285
+ @actual_value =~ /^[a-f0-9]{32}$/
1286
+ else
1287
+ false
1288
+ end
1289
+ end
1290
+ end
1291
+
1292
+ # Verify event was sent to specific adapters only
1293
+ def be_routed_to(*adapter_names)
1294
+ AdapterRoutingMatcher.new(adapter_names)
1295
+ end
1296
+
1297
+ class AdapterRoutingMatcher
1298
+ def initialize(expected_adapters)
1299
+ @expected_adapters = expected_adapters.map(&:to_sym).sort
1300
+ @only = false
1301
+ end
1302
+
1303
+ def only
1304
+ @only = true
1305
+ self
1306
+ end
1307
+
1308
+ def matches?(event_class)
1309
+ @actual_adapters = event_class.adapters.sort
1310
+
1311
+ if @only
1312
+ @actual_adapters == @expected_adapters
1313
+ else
1314
+ (@expected_adapters - @actual_adapters).empty?
1315
+ end
1316
+ end
1317
+
1318
+ def failure_message
1319
+ if @only
1320
+ "expected event to be routed to ONLY #{@expected_adapters.inspect}, " \
1321
+ "but configured for: #{@actual_adapters.inspect}"
1322
+ else
1323
+ "expected event to be routed to #{@expected_adapters.inspect}, " \
1324
+ "but only configured for: #{@actual_adapters.inspect}"
1325
+ end
1326
+ end
1327
+ end
1328
+
1329
+ # Helper to setup multiple test adapters
1330
+ def setup_test_adapters(*adapter_names)
1331
+ adapters = {}
1332
+
1333
+ adapter_names.each do |name|
1334
+ adapters[name] = E11y::Adapters::InMemory.new
1335
+ end
1336
+
1337
+ E11y.configure do |config|
1338
+ config.adapters.clear
1339
+ adapters.each do |name, adapter|
1340
+ config.adapters.register name, adapter
1341
+ end
1342
+ end
1343
+
1344
+ adapters
1345
+ end
1346
+ end
1347
+ end
1348
+ end
1349
+ ```
1350
+
1351
+ ### 10.4. PII Filtering Test Examples
1352
+
1353
+ ```ruby
1354
+ # spec/events/user_login_spec.rb
1355
+ RSpec.describe Events::UserLogin, type: :event do
1356
+ # Setup separate adapters to verify per-adapter PII filtering
1357
+ let(:adapters) { setup_test_adapters(:sentry, :file_audit, :file) }
1358
+
1359
+ describe 'PII filtering' do
1360
+ context 'password field' do
1361
+ it 'masks password for ALL adapters' do
1362
+ Events::UserLogin.track(
1363
+ email: 'user@example.com',
1364
+ password: 'secret123',
1365
+ ip_address: '192.168.1.1'
1366
+ )
1367
+
1368
+ # ✅ Sentry: password masked
1369
+ sentry_event = adapters[:sentry].find_event(Events::UserLogin)
1370
+ expect(sentry_event).to have_field_filtered(:password, :mask)
1371
+
1372
+ # ✅ Audit: password masked (sensitive data)
1373
+ audit_event = adapters[:file_audit].find_event(Events::UserLogin)
1374
+ expect(audit_event).to have_field_filtered(:password, :mask)
1375
+
1376
+ # ✅ File: password masked
1377
+ file_event = adapters[:file].find_event(Events::UserLogin)
1378
+ expect(file_event).to have_field_filtered(:password, :mask)
1379
+ end
1380
+ end
1381
+
1382
+ context 'email field (per-adapter rules)' do
1383
+ it 'hashes email for Sentry, keeps original for audit' do
1384
+ Events::UserLogin.track(
1385
+ email: 'user@example.com',
1386
+ password: 'secret123'
1387
+ )
1388
+
1389
+ # ✅ Sentry: email hashed (compliance + privacy)
1390
+ sentry_event = adapters[:sentry].find_event(Events::UserLogin)
1391
+ expect(sentry_event).to have_field_filtered(:email, :hash, algorithm: :sha256)
1392
+ expect(sentry_event[:payload][:email]).not_to eq('user@example.com')
1393
+
1394
+ # ✅ Audit: email ORIGINAL (for investigation)
1395
+ audit_event = adapters[:file_audit].find_event(Events::UserLogin)
1396
+ expect(audit_event[:payload][:email]).to eq('user@example.com')
1397
+ expect(audit_event).not_to have_field_filtered(:email, :hash)
1398
+ end
1399
+
1400
+ it 'uses consistent hash for same email' do
1401
+ # Track twice with same email
1402
+ 2.times do
1403
+ Events::UserLogin.track(
1404
+ email: 'user@example.com',
1405
+ password: 'secret123'
1406
+ )
1407
+ end
1408
+
1409
+ events = adapters[:sentry].find_events(Events::UserLogin)
1410
+ hashes = events.map { |e| e[:payload][:email] }
1411
+
1412
+ # ✅ Same email → same hash (for correlation)
1413
+ expect(hashes.uniq.size).to eq(1)
1414
+ end
1415
+ end
1416
+
1417
+ context 'ip_address field (conditional filtering)' do
1418
+ it 'masks IP for non-admin users, skips for audit' do
1419
+ Events::UserLogin.track(
1420
+ email: 'user@example.com',
1421
+ password: 'secret123',
1422
+ ip_address: '192.168.1.1',
1423
+ is_admin: false
1424
+ )
1425
+
1426
+ # ✅ Sentry: IP masked (PII for regular users)
1427
+ sentry_event = adapters[:sentry].find_event(Events::UserLogin)
1428
+ expect(sentry_event).to have_field_filtered(:ip_address, :mask)
1429
+
1430
+ # ✅ Audit: IP kept (security investigation)
1431
+ audit_event = adapters[:file_audit].find_event(Events::UserLogin)
1432
+ expect(audit_event[:payload][:ip_address]).to eq('192.168.1.1')
1433
+ end
1434
+ end
1435
+ end
1436
+
1437
+ describe 'GDPR compliance' do
1438
+ it 'never leaks PII to non-audit adapters' do
1439
+ Events::UserLogin.track(
1440
+ email: 'user@example.com',
1441
+ password: 'secret123',
1442
+ session_id: 'sess_abc123'
1443
+ )
1444
+
1445
+ sentry_event = adapters[:sentry].find_event(Events::UserLogin)
1446
+
1447
+ # ✅ No plain-text PII in Sentry
1448
+ expect(sentry_event[:payload].values).not_to include('user@example.com')
1449
+ expect(sentry_event[:payload].values).not_to include('secret123')
1450
+ end
1451
+
1452
+ it 'logs PII filtering activity (for audit trail)' do
1453
+ Events::UserLogin.track(
1454
+ email: 'user@example.com',
1455
+ password: 'secret123'
1456
+ )
1457
+
1458
+ # ✅ Metadata tracks PII filtering
1459
+ sentry_event = adapters[:sentry].find_event(Events::UserLogin)
1460
+ expect(sentry_event[:metadata][:pii_filtered]).to be true
1461
+ expect(sentry_event[:metadata][:pii_fields]).to include(:email, :password)
1462
+ end
1463
+ end
1464
+ end
1465
+ ```
1466
+
1467
+ ### 10.5. Adapter Routing Test Examples
1468
+
1469
+ ```ruby
1470
+ # spec/events/routing_spec.rb
1471
+ RSpec.describe 'Event routing', type: :integration do
1472
+ let(:adapters) { setup_test_adapters(:sentry, :loki, :file, :pagerduty) }
1473
+
1474
+ describe Events::DebugQuery do
1475
+ it 'routes ONLY to file adapter (cost optimization)' do
1476
+ Events::DebugQuery.track(sql: 'SELECT * FROM users')
1477
+
1478
+ # ✅ File got event (cheap)
1479
+ expect(adapters[:file].event_count).to eq(1)
1480
+
1481
+ # ✅ Sentry did NOT get event (expensive!)
1482
+ expect(adapters[:sentry].event_count).to eq(0)
1483
+ expect(adapters[:loki].event_count).to eq(0)
1484
+ expect(adapters[:pagerduty].event_count).to eq(0)
1485
+ end
1486
+
1487
+ it 'uses routing matcher for cleaner assertions' do
1488
+ expect(Events::DebugQuery).to be_routed_to(:file).only
1489
+ end
1490
+ end
1491
+
1492
+ describe Events::PaymentFraudDetected do
1493
+ it 'routes to critical adapters (Sentry, PagerDuty, Audit)' do
1494
+ Events::PaymentFraudDetected.track(
1495
+ transaction_id: 'tx_123',
1496
+ fraud_score: 0.95
1497
+ )
1498
+
1499
+ # ✅ Critical adapters got event
1500
+ expect(adapters[:sentry].event_count).to eq(1)
1501
+ expect(adapters[:pagerduty].event_count).to eq(1)
1502
+
1503
+ # ✅ Low-priority adapters did NOT get event
1504
+ expect(adapters[:file].event_count).to eq(0)
1505
+ end
1506
+ end
1507
+
1508
+ describe Events::HealthCheckPassed do
1509
+ it 'routes NOWHERE (excluded from all adapters)' do
1510
+ Events::HealthCheckPassed.track
1511
+
1512
+ # ✅ No adapters got event (noise reduction)
1513
+ expect(adapters.values.sum(&:event_count)).to eq(0)
1514
+ end
1515
+ end
1516
+
1517
+ context 'cost verification' do
1518
+ it 'prevents expensive routing mistakes' do
1519
+ # Track 1000 debug events
1520
+ 1000.times { Events::DebugQuery.track(sql: 'SELECT 1') }
1521
+
1522
+ # ✅ If routed to Sentry: $10/1000 events = $10 cost
1523
+ # ✅ If routed to File: $0 cost
1524
+ expect(adapters[:sentry].event_count).to eq(0),
1525
+ 'Debug events leaked to Sentry! Cost impact: ~$10'
1526
+ end
1527
+ end
1528
+ end
1529
+ ```
1530
+
1531
+ ### 10.6. Rate Limiting Test Examples
1532
+
1533
+ ```ruby
1534
+ # spec/features/rate_limiting_spec.rb
1535
+ RSpec.describe 'Rate limiting', type: :integration do
1536
+ let(:adapters) { setup_test_adapters(:file) }
1537
+
1538
+ before do
1539
+ E11y.configure do |config|
1540
+ config.rate_limiting.enabled = true
1541
+ config.rate_limiting.global_limit = 10 # 10 events/sec
1542
+ end
1543
+ end
1544
+
1545
+ it 'drops events exceeding global rate limit' do
1546
+ # Track 15 events (exceeds limit of 10)
1547
+ 15.times { Events::DebugQuery.track(sql: 'SELECT 1') }
1548
+
1549
+ # ✅ Only 10 events tracked
1550
+ expect(adapters[:file].event_count).to eq(10)
1551
+
1552
+ # ✅ 5 events dropped
1553
+ expect(E11y::Metrics.get('e11y.rate_limit.dropped_total')).to eq(5)
1554
+ end
1555
+
1556
+ it 'never drops critical events (bypass rate limit)' do
1557
+ E11y.config.rate_limiting.global_limit = 1 # Extremely low
1558
+
1559
+ # Track 10 critical events
1560
+ 10.times { Events::PaymentFailed.track(payment_id: 'p123') }
1561
+
1562
+ # ✅ ALL critical events tracked (bypass)
1563
+ expect(adapters[:file].event_count).to eq(10)
1564
+ end
1565
+
1566
+ it 'applies per-event rate limits' do
1567
+ E11y.config.rate_limiting.per_event_limits = {
1568
+ 'Events::DebugQuery' => 5 # Max 5 per second
1569
+ }
1570
+
1571
+ 10.times { Events::DebugQuery.track(sql: 'SELECT 1') }
1572
+
1573
+ # ✅ Only 5 DebugQuery events tracked
1574
+ debug_events = adapters[:file].find_events(Events::DebugQuery)
1575
+ expect(debug_events.size).to eq(5)
1576
+ end
1577
+ end
1578
+ ```
1579
+
1580
+ ### 10.7. Sampling Test Examples
1581
+
1582
+ ```ruby
1583
+ # spec/features/sampling_spec.rb
1584
+ RSpec.describe 'Adaptive sampling', type: :integration do
1585
+ let(:adapters) { setup_test_adapters(:file) }
1586
+
1587
+ before do
1588
+ E11y.configure do |config|
1589
+ config.sampling.enabled = true
1590
+ config.sampling.default_sample_rate = 0.1 # 10%
1591
+ end
1592
+ end
1593
+
1594
+ it 'samples debug events at configured rate' do
1595
+ # Track 1000 debug events
1596
+ 1000.times { Events::DebugQuery.track(sql: 'SELECT 1') }
1597
+
1598
+ # ✅ ~100 events tracked (10% ± margin of error)
1599
+ expect(adapters[:file].event_count).to be_within(30).of(100)
1600
+ end
1601
+
1602
+ it 'NEVER samples error/fatal events (always 100%)' do
1603
+ E11y.config.sampling.default_sample_rate = 0.01 # 1%
1604
+
1605
+ # Track 100 error events
1606
+ 100.times { Events::ErrorOccurred.track(error: 'Boom!') }
1607
+
1608
+ # ✅ ALL 100 error events tracked (no sampling)
1609
+ expect(adapters[:file].event_count).to eq(100)
1610
+ end
1611
+
1612
+ it 'NEVER samples audit events (compliance requirement)' do
1613
+ E11y.config.sampling.default_sample_rate = 0.01 # 1%
1614
+
1615
+ # Track 50 audit events
1616
+ 50.times { Events::GdprDeletion.audit(user_id: 123) }
1617
+
1618
+ # ✅ ALL 50 audit events tracked (never sampled)
1619
+ expect(adapters[:file].event_count).to eq(50)
1620
+ end
1621
+
1622
+ it 'uses trace-consistent sampling (same trace_id)' do
1623
+ E11y::Current.set(trace_id: 'trace-123')
1624
+
1625
+ # Track 100 events with same trace_id
1626
+ sampled = 0
1627
+ 100.times do
1628
+ Events::DebugQuery.track(sql: 'SELECT 1')
1629
+ sampled += 1 if adapters[:file].event_count > 0
1630
+ adapters[:file].clear! # Reset for next iteration
1631
+ end
1632
+
1633
+ # ✅ Either ALL sampled or NONE sampled (consistent)
1634
+ expect([0, 100]).to include(sampled)
1635
+ end
1636
+ end
1637
+ ```
1638
+
1639
+ ### 10.8. Configuration for Security Tests
1640
+
1641
+ ```ruby
1642
+ # spec/support/e11y_security.rb
1643
+ RSpec.configure do |config|
1644
+ # Include PII helpers
1645
+ config.include E11y::Testing::PiiHelpers, type: :event
1646
+ config.include E11y::Testing::PiiHelpers, type: :integration
1647
+
1648
+ # Global before hook for security tests
1649
+ config.before(:each, type: :event) do
1650
+ # Enable strict PII validation in tests
1651
+ E11y.configure do |e11y_config|
1652
+ e11y_config.pii.strict_mode = true # Fail on unfiltered PII
1653
+ e11y_config.pii.validate_on_track = true # Validate immediately
1654
+ end
1655
+ end
1656
+
1657
+ # Verify no PII leaks in ANY test (global check)
1658
+ config.after(:each) do
1659
+ next unless E11y.config.pii.strict_mode
1660
+
1661
+ # Get all events from all adapters
1662
+ all_events = E11y.config.adapters.all.flat_map do |adapter|
1663
+ adapter.respond_to?(:all_events) ? adapter.all_events : []
1664
+ end
1665
+
1666
+ # Verify no plain-text PII (if event declares contains_pii)
1667
+ all_events.each do |event|
1668
+ event_class = E11y::Registry.get(event[:event_name])
1669
+
1670
+ if event_class&.contains_pii?
1671
+ # Check for common PII patterns
1672
+ payload_str = event[:payload].values.join(' ')
1673
+
1674
+ # ❌ Email pattern
1675
+ if payload_str =~ /@.+\./
1676
+ raise "SECURITY: Plain-text email detected in #{event[:event_name]}"
1677
+ end
1678
+
1679
+ # ❌ Credit card pattern
1680
+ if payload_str =~ /\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}/
1681
+ raise "SECURITY: Credit card detected in #{event[:event_name]}"
1682
+ end
1683
+ end
1684
+ end
1685
+ end
1686
+ end
1687
+ ```
1688
+
1689
+ ### 10.9. Trade-offs: Security Testing
1690
+
1691
+ | Decision | Pro | Con | Rationale |
1692
+ |----------|-----|-----|-----------|
1693
+ | **Multiple InMemory adapters** | Real per-adapter verification | More test setup | Critical for PII/routing |
1694
+ | **Strict mode in tests** | Catches PII leaks early | Slower tests | Security > speed |
1695
+ | **Per-adapter assertions** | Precise verification | Verbose tests | Compliance requirement |
1696
+ | **Global PII leak detector** | Safety net | False positives | Better safe than sorry |
1697
+ | **Routing matchers** | Prevents cost overruns | Extra DSL | Clear intent |
1698
+
1699
+ ---
1700
+
1701
+ ## 11. Trade-offs
1702
+
1703
+ ### 11.1. Key Decisions
1704
+
1705
+ | Decision | Pro | Con | Rationale |
1706
+ |----------|-----|-----|-----------|
1707
+ | **InMemory adapter** | Fast, zero I/O | Not production-like | Test speed > realism |
1708
+ | **RSpec-only** | Deep integration | No Minitest support | Focus on one framework |
1709
+ | **Snapshot testing** | Easy updates | Brittle on schema changes | Opt-in feature |
1710
+ | **No auto-mocking** | Explicit tests | More setup | Clarity > magic |
1711
+ | **Event factories** | Reusable | Extra maintenance | DRY test data |
1712
+ | **Contract tests** | Enforces API | Boilerplate | Adapter quality |
1713
+
1714
+ ### 11.2. Alternatives Considered
1715
+
1716
+ **A) VCR for adapter recording**
1717
+ - ❌ Rejected: Too complex for unit tests
1718
+
1719
+ **B) Minitest support**
1720
+ - ❌ Rejected: Limited by time, RSpec is majority
1721
+
1722
+ **C) Automatic mocking**
1723
+ - ❌ Rejected: Hides test intent
1724
+
1725
+ **D) Database-backed test adapter**
1726
+ - ❌ Rejected: Too slow
1727
+
1728
+ **E) Spy pattern (track all calls)**
1729
+ - ✅ Partially adopted: InMemory adapter tracks calls
1730
+
1731
+ ---
1732
+
1733
+ ## 12. Complete Example
1734
+
1735
+ ```ruby
1736
+ # spec/models/order_spec.rb
1737
+ RSpec.describe Order, type: :model do
1738
+ describe '#complete!' do
1739
+ let(:order) { create(:order, status: 'pending') }
1740
+
1741
+ it 'tracks order completed event' do
1742
+ expect {
1743
+ order.complete!
1744
+ }.to have_tracked_event(Events::OrderCompleted)
1745
+ .with(
1746
+ order_id: order.id,
1747
+ total_amount: order.total_amount,
1748
+ items_count: order.items.count
1749
+ )
1750
+ .with_severity(:success)
1751
+ .once
1752
+ end
1753
+
1754
+ it 'includes trace context' do
1755
+ E11y::Current.set(trace_id: 'test-trace-123')
1756
+
1757
+ order.complete!
1758
+
1759
+ expect(Events::OrderCompleted).to have_been_tracked
1760
+ .with_trace_id('test-trace-123')
1761
+ end
1762
+
1763
+ it 'matches expected payload structure' do
1764
+ order.complete!
1765
+
1766
+ event = E11y.test_adapter.find_event(Events::OrderCompleted)
1767
+
1768
+ expect(event[:payload]).to match_snapshot('order_completed')
1769
+ end
1770
+ end
1771
+ end
1772
+ ```
1773
+
1774
+ ---
1775
+
1776
+ ## 13. Summary & Key Takeaways
1777
+
1778
+ ### 13.1. What We Achieved
1779
+
1780
+ ✅ **Comprehensive testing strategy**: RSpec matchers, test adapters, factories, snapshots
1781
+ ✅ **Performance testing**: Benchmarks for <1ms p99 latency
1782
+ ✅ **Integration testing**: Full Rails stack with RSpec & factories
1783
+ ✅ **Contract testing**: Shared examples for custom adapters
1784
+ ✅ **Security & compliance testing**: PII filtering, adapter routing, rate limiting verification
1785
+
1786
+ ### 13.2. Critical Testing Priorities
1787
+
1788
+ **Tier 1 (Must Test - Security/Cost Impact):**
1789
+ 1. ✅ PII filtering per-adapter (GDPR compliance)
1790
+ 2. ✅ Adapter routing (cost optimization)
1791
+ 3. ✅ Rate limiting (system protection)
1792
+ 4. ✅ Sampling (critical events never dropped)
1793
+
1794
+ **Tier 2 (Should Test - Functionality):**
1795
+ 5. ✅ Event tracking & payload validation
1796
+ 6. ✅ Trace context propagation
1797
+ 7. ✅ Schema validation
1798
+
1799
+ **Tier 3 (Nice to Test - Performance):**
1800
+ 8. ✅ Performance benchmarks (<1ms p99)
1801
+ 9. ✅ Memory allocation (zero-allocation pattern)
1802
+
1803
+ ### 13.3. Testing Architecture Decision
1804
+
1805
+ **✅ Multiple InMemory Adapters** - Winner for security/compliance testing:
1806
+ - ✅ Real per-adapter verification
1807
+ - ✅ Separate event stores per destination
1808
+ - ✅ Verifies PII filtering independently
1809
+ - ✅ Verifies routing correctness
1810
+ - ✅ Simple to setup & use
1811
+
1812
+ ### 13.4. Key Testing Helpers
1813
+
1814
+ ```ruby
1815
+ # PII filtering verification
1816
+ expect(event).to have_field_filtered(:password, :mask)
1817
+ expect(event).to have_field_filtered(:email, :hash, for_adapter: :sentry)
1818
+
1819
+ # Adapter routing verification
1820
+ expect(Events::DebugQuery).to be_routed_to(:file).only
1821
+ expect(Events::PaymentFailed).to be_routed_to(:sentry, :pagerduty)
1822
+
1823
+ # Setup test adapters
1824
+ adapters = setup_test_adapters(:sentry, :file_audit, :file)
1825
+ ```
1826
+
1827
+ ---
1828
+
1829
+ **Status:** ✅ Complete (with Security & Compliance Testing)
1830
+ **Next:** Implementation + Integration with CI/CD
1831
+ **Estimated Implementation:** 2 weeks
1832
+ **Impact:**
1833
+ - GDPR compliance verification (critical!)
1834
+ - Cost overrun prevention (routing tests)
1835
+ - Developer confidence (comprehensive testing)
1836
+ - Security audit trail (PII filtering proof)