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.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +69 -0
- data/CHANGELOG.md +26 -0
- data/CODE_OF_CONDUCT.md +64 -0
- data/LICENSE.txt +21 -0
- data/README.md +179 -0
- data/Rakefile +37 -0
- data/benchmarks/run_all.rb +33 -0
- data/config/README.md +83 -0
- data/config/loki-local-config.yaml +35 -0
- data/config/prometheus.yml +15 -0
- data/docker-compose.yml +78 -0
- data/docs/00-ICP-AND-TIMELINE.md +483 -0
- data/docs/01-SCALE-REQUIREMENTS.md +858 -0
- data/docs/ADR-001-architecture.md +2617 -0
- data/docs/ADR-002-metrics-yabeda.md +1395 -0
- data/docs/ADR-003-slo-observability.md +3337 -0
- data/docs/ADR-004-adapter-architecture.md +2385 -0
- data/docs/ADR-005-tracing-context.md +1372 -0
- data/docs/ADR-006-security-compliance.md +4143 -0
- data/docs/ADR-007-opentelemetry-integration.md +1385 -0
- data/docs/ADR-008-rails-integration.md +1911 -0
- data/docs/ADR-009-cost-optimization.md +2993 -0
- data/docs/ADR-010-developer-experience.md +2166 -0
- data/docs/ADR-011-testing-strategy.md +1836 -0
- data/docs/ADR-012-event-evolution.md +958 -0
- data/docs/ADR-013-reliability-error-handling.md +2750 -0
- data/docs/ADR-014-event-driven-slo.md +1533 -0
- data/docs/ADR-015-middleware-order.md +1061 -0
- data/docs/ADR-016-self-monitoring-slo.md +1234 -0
- data/docs/API-REFERENCE-L28.md +914 -0
- data/docs/COMPREHENSIVE-CONFIGURATION.md +2366 -0
- data/docs/IMPLEMENTATION_NOTES.md +2804 -0
- data/docs/IMPLEMENTATION_PLAN.md +1971 -0
- data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +586 -0
- data/docs/PLAN.md +148 -0
- data/docs/QUICK-START.md +934 -0
- data/docs/README.md +296 -0
- data/docs/design/00-memory-optimization.md +593 -0
- data/docs/guides/MIGRATION-L27-L28.md +692 -0
- data/docs/guides/PERFORMANCE-BENCHMARKS.md +434 -0
- data/docs/guides/README.md +44 -0
- data/docs/prd/01-overview-vision.md +440 -0
- data/docs/use_cases/README.md +119 -0
- data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +813 -0
- data/docs/use_cases/UC-002-business-event-tracking.md +1953 -0
- data/docs/use_cases/UC-003-pattern-based-metrics.md +1627 -0
- data/docs/use_cases/UC-004-zero-config-slo-tracking.md +728 -0
- data/docs/use_cases/UC-005-sentry-integration.md +759 -0
- data/docs/use_cases/UC-006-trace-context-management.md +905 -0
- data/docs/use_cases/UC-007-pii-filtering.md +2648 -0
- data/docs/use_cases/UC-008-opentelemetry-integration.md +1153 -0
- data/docs/use_cases/UC-009-multi-service-tracing.md +1043 -0
- data/docs/use_cases/UC-010-background-job-tracking.md +1018 -0
- data/docs/use_cases/UC-011-rate-limiting.md +1906 -0
- data/docs/use_cases/UC-012-audit-trail.md +2301 -0
- data/docs/use_cases/UC-013-high-cardinality-protection.md +2127 -0
- data/docs/use_cases/UC-014-adaptive-sampling.md +1940 -0
- data/docs/use_cases/UC-015-cost-optimization.md +735 -0
- data/docs/use_cases/UC-016-rails-logger-migration.md +785 -0
- data/docs/use_cases/UC-017-local-development.md +867 -0
- data/docs/use_cases/UC-018-testing-events.md +1081 -0
- data/docs/use_cases/UC-019-tiered-storage-migration.md +562 -0
- data/docs/use_cases/UC-020-event-versioning.md +708 -0
- data/docs/use_cases/UC-021-error-handling-retry-dlq.md +956 -0
- data/docs/use_cases/UC-022-event-registry.md +648 -0
- data/docs/use_cases/backlog.md +226 -0
- data/e11y.gemspec +76 -0
- data/lib/e11y/adapters/adaptive_batcher.rb +207 -0
- data/lib/e11y/adapters/audit_encrypted.rb +239 -0
- data/lib/e11y/adapters/base.rb +580 -0
- data/lib/e11y/adapters/file.rb +224 -0
- data/lib/e11y/adapters/in_memory.rb +216 -0
- data/lib/e11y/adapters/loki.rb +333 -0
- data/lib/e11y/adapters/otel_logs.rb +203 -0
- data/lib/e11y/adapters/registry.rb +141 -0
- data/lib/e11y/adapters/sentry.rb +230 -0
- data/lib/e11y/adapters/stdout.rb +108 -0
- data/lib/e11y/adapters/yabeda.rb +370 -0
- data/lib/e11y/buffers/adaptive_buffer.rb +339 -0
- data/lib/e11y/buffers/base_buffer.rb +40 -0
- data/lib/e11y/buffers/request_scoped_buffer.rb +246 -0
- data/lib/e11y/buffers/ring_buffer.rb +267 -0
- data/lib/e11y/buffers.rb +14 -0
- data/lib/e11y/console.rb +122 -0
- data/lib/e11y/current.rb +48 -0
- data/lib/e11y/event/base.rb +894 -0
- data/lib/e11y/event/value_sampling_config.rb +84 -0
- data/lib/e11y/events/base_audit_event.rb +43 -0
- data/lib/e11y/events/base_payment_event.rb +33 -0
- data/lib/e11y/events/rails/cache/delete.rb +21 -0
- data/lib/e11y/events/rails/cache/read.rb +23 -0
- data/lib/e11y/events/rails/cache/write.rb +22 -0
- data/lib/e11y/events/rails/database/query.rb +45 -0
- data/lib/e11y/events/rails/http/redirect.rb +21 -0
- data/lib/e11y/events/rails/http/request.rb +26 -0
- data/lib/e11y/events/rails/http/send_file.rb +21 -0
- data/lib/e11y/events/rails/http/start_processing.rb +26 -0
- data/lib/e11y/events/rails/job/completed.rb +22 -0
- data/lib/e11y/events/rails/job/enqueued.rb +22 -0
- data/lib/e11y/events/rails/job/failed.rb +22 -0
- data/lib/e11y/events/rails/job/scheduled.rb +23 -0
- data/lib/e11y/events/rails/job/started.rb +22 -0
- data/lib/e11y/events/rails/log.rb +56 -0
- data/lib/e11y/events/rails/view/render.rb +23 -0
- data/lib/e11y/events.rb +18 -0
- data/lib/e11y/instruments/active_job.rb +201 -0
- data/lib/e11y/instruments/rails_instrumentation.rb +141 -0
- data/lib/e11y/instruments/sidekiq.rb +175 -0
- data/lib/e11y/logger/bridge.rb +205 -0
- data/lib/e11y/metrics/cardinality_protection.rb +172 -0
- data/lib/e11y/metrics/cardinality_tracker.rb +134 -0
- data/lib/e11y/metrics/registry.rb +234 -0
- data/lib/e11y/metrics/relabeling.rb +226 -0
- data/lib/e11y/metrics.rb +102 -0
- data/lib/e11y/middleware/audit_signing.rb +174 -0
- data/lib/e11y/middleware/base.rb +140 -0
- data/lib/e11y/middleware/event_slo.rb +167 -0
- data/lib/e11y/middleware/pii_filter.rb +266 -0
- data/lib/e11y/middleware/pii_filtering.rb +280 -0
- data/lib/e11y/middleware/rate_limiting.rb +214 -0
- data/lib/e11y/middleware/request.rb +163 -0
- data/lib/e11y/middleware/routing.rb +157 -0
- data/lib/e11y/middleware/sampling.rb +254 -0
- data/lib/e11y/middleware/slo.rb +168 -0
- data/lib/e11y/middleware/trace_context.rb +131 -0
- data/lib/e11y/middleware/validation.rb +118 -0
- data/lib/e11y/middleware/versioning.rb +132 -0
- data/lib/e11y/middleware.rb +12 -0
- data/lib/e11y/pii/patterns.rb +90 -0
- data/lib/e11y/pii.rb +13 -0
- data/lib/e11y/pipeline/builder.rb +155 -0
- data/lib/e11y/pipeline/zone_validator.rb +110 -0
- data/lib/e11y/pipeline.rb +12 -0
- data/lib/e11y/presets/audit_event.rb +65 -0
- data/lib/e11y/presets/debug_event.rb +34 -0
- data/lib/e11y/presets/high_value_event.rb +51 -0
- data/lib/e11y/presets.rb +19 -0
- data/lib/e11y/railtie.rb +138 -0
- data/lib/e11y/reliability/circuit_breaker.rb +216 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +277 -0
- data/lib/e11y/reliability/dlq/filter.rb +117 -0
- data/lib/e11y/reliability/retry_handler.rb +207 -0
- data/lib/e11y/reliability/retry_rate_limiter.rb +117 -0
- data/lib/e11y/sampling/error_spike_detector.rb +225 -0
- data/lib/e11y/sampling/load_monitor.rb +161 -0
- data/lib/e11y/sampling/stratified_tracker.rb +92 -0
- data/lib/e11y/sampling/value_extractor.rb +82 -0
- data/lib/e11y/self_monitoring/buffer_monitor.rb +79 -0
- data/lib/e11y/self_monitoring/performance_monitor.rb +97 -0
- data/lib/e11y/self_monitoring/reliability_monitor.rb +146 -0
- data/lib/e11y/slo/event_driven.rb +150 -0
- data/lib/e11y/slo/tracker.rb +119 -0
- data/lib/e11y/version.rb +9 -0
- data/lib/e11y.rb +283 -0
- 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
|