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,759 @@
|
|
|
1
|
+
# UC-005: Sentry Integration
|
|
2
|
+
|
|
3
|
+
**Status:** โ
Implemented (2026-01-19)
|
|
4
|
+
**Complexity:** Low
|
|
5
|
+
**Setup Time:** 10 minutes
|
|
6
|
+
**Target Users:** All developers
|
|
7
|
+
|
|
8
|
+
**Implementation:**
|
|
9
|
+
- โ
`E11y::Adapters::Sentry` - Implemented with full Sentry SDK integration
|
|
10
|
+
- โ
Automatic error reporting (severity-based filtering)
|
|
11
|
+
- โ
Breadcrumb tracking for context
|
|
12
|
+
- โ
Trace context propagation (trace_id, span_id)
|
|
13
|
+
- โ
User context support
|
|
14
|
+
- โ
39 comprehensive tests
|
|
15
|
+
- ๐ See [ADR-004 ยง4.4](../ADR-004-adapter-architecture.md#44-sentry-adapter) for technical details
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ๐ Overview
|
|
20
|
+
|
|
21
|
+
### Problem Statement
|
|
22
|
+
|
|
23
|
+
**Current Approach (Separate Systems):**
|
|
24
|
+
```ruby
|
|
25
|
+
# โ Duplication and disconnected systems
|
|
26
|
+
begin
|
|
27
|
+
process_payment(order)
|
|
28
|
+
rescue => e
|
|
29
|
+
# Log to Rails logger
|
|
30
|
+
Rails.logger.error "Payment failed: #{e.message}"
|
|
31
|
+
|
|
32
|
+
# Send to Sentry
|
|
33
|
+
Sentry.capture_exception(e, extra: { order_id: order.id })
|
|
34
|
+
|
|
35
|
+
# Track business event
|
|
36
|
+
Events::PaymentFailed.track(order_id: order.id, error: e.class.name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Problems:
|
|
40
|
+
# - 3 separate calls (verbose)
|
|
41
|
+
# - No correlation between systems
|
|
42
|
+
# - Can't see event context in Sentry
|
|
43
|
+
# - Can't jump from Sentry to logs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### E11y Solution
|
|
47
|
+
|
|
48
|
+
**Unified error tracking with automatic Sentry integration:**
|
|
49
|
+
```ruby
|
|
50
|
+
# โ
One call, automatic Sentry integration
|
|
51
|
+
begin
|
|
52
|
+
process_payment(order)
|
|
53
|
+
rescue => e
|
|
54
|
+
Events::PaymentFailed.track(
|
|
55
|
+
order_id: order.id,
|
|
56
|
+
error_class: e.class.name,
|
|
57
|
+
error_message: e.message,
|
|
58
|
+
severity: :error # โ Automatically sends to Sentry!
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Result:
|
|
63
|
+
# โ
Event tracked in E11y
|
|
64
|
+
# โ
Exception in Sentry (with breadcrumbs)
|
|
65
|
+
# โ
Correlated via trace_id
|
|
66
|
+
# โ
Full event context in Sentry extras
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ๐ฏ Features
|
|
72
|
+
|
|
73
|
+
### 1. Automatic Exception Capture
|
|
74
|
+
|
|
75
|
+
> **Implementation:** See [ADR-004 Section 4.4: Sentry Adapter](../ADR-004-adapter-architecture.md#44-sentry-adapter) for technical details.
|
|
76
|
+
|
|
77
|
+
**Configuration (2026-01-19 - Actual Implementation):**
|
|
78
|
+
```ruby
|
|
79
|
+
# config/initializers/e11y.rb
|
|
80
|
+
require 'e11y'
|
|
81
|
+
|
|
82
|
+
# Register Sentry adapter
|
|
83
|
+
E11y::Adapters::Registry.register(
|
|
84
|
+
:sentry,
|
|
85
|
+
E11y::Adapters::Sentry.new(
|
|
86
|
+
dsn: ENV['SENTRY_DSN'],
|
|
87
|
+
environment: Rails.env,
|
|
88
|
+
severity_threshold: :warn, # Send :warn, :error, :fatal to Sentry
|
|
89
|
+
breadcrumbs: true # Track all events as breadcrumbs
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Use in events
|
|
94
|
+
class Events::PaymentFailed < E11y::Event::Base
|
|
95
|
+
schema do
|
|
96
|
+
required(:order_id).filled(:string)
|
|
97
|
+
required(:error_message).filled(:string)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
severity :error
|
|
101
|
+
adapters [:sentry, :loki] # Send to both Sentry and Loki
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Usage:**
|
|
106
|
+
```ruby
|
|
107
|
+
# ANY event with severity :error automatically goes to Sentry
|
|
108
|
+
Events::PaymentFailed.track(
|
|
109
|
+
order_id: '123',
|
|
110
|
+
amount: 99.99,
|
|
111
|
+
error_message: 'Card declined',
|
|
112
|
+
severity: :error # โ Triggers Sentry capture
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# In Sentry UI you'll see:
|
|
116
|
+
# - Event name: "payment.failed"
|
|
117
|
+
# - Message: "Card declined"
|
|
118
|
+
# - Context: { order_id: '123', amount: 99.99 }
|
|
119
|
+
# - Trace ID: abc-123-def (for correlation)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### 2. Breadcrumbs Trail
|
|
125
|
+
|
|
126
|
+
**All E11y events become Sentry breadcrumbs:**
|
|
127
|
+
```ruby
|
|
128
|
+
# These events create breadcrumb trail
|
|
129
|
+
Events::CartViewed.track(user_id: '123', items: 3)
|
|
130
|
+
Events::CheckoutStarted.track(user_id: '123', cart_total: 299.99)
|
|
131
|
+
Events::PaymentAttempted.track(user_id: '123', payment_method: 'stripe')
|
|
132
|
+
Events::PaymentFailed.track(user_id: '123', error: 'Card declined', severity: :error)
|
|
133
|
+
|
|
134
|
+
# In Sentry, you'll see breadcrumb trail:
|
|
135
|
+
# 1. [info] cart.viewed - { user_id: '123', items: 3 }
|
|
136
|
+
# 2. [info] checkout.started - { user_id: '123', cart_total: 299.99 }
|
|
137
|
+
# 3. [info] payment.attempted - { user_id: '123', payment_method: 'stripe' }
|
|
138
|
+
# 4. [error] payment.failed - { user_id: '123', error: 'Card declined' } โ Exception
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Configuration:**
|
|
142
|
+
```ruby
|
|
143
|
+
E11y.configure do |config|
|
|
144
|
+
config.sentry do
|
|
145
|
+
# Enable breadcrumbs
|
|
146
|
+
breadcrumbs true
|
|
147
|
+
|
|
148
|
+
# Which severities become breadcrumbs
|
|
149
|
+
breadcrumb_severities [:debug, :info, :warn, :error]
|
|
150
|
+
|
|
151
|
+
# Max breadcrumbs (Sentry default is 100)
|
|
152
|
+
max_breadcrumbs 100
|
|
153
|
+
|
|
154
|
+
# Breadcrumb data limit
|
|
155
|
+
max_breadcrumb_size 1.kilobyte
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### 3. Trace Correlation
|
|
163
|
+
|
|
164
|
+
**Link Sentry errors to E11y logs:**
|
|
165
|
+
```ruby
|
|
166
|
+
# E11y automatically adds trace_id to all events
|
|
167
|
+
Events::PaymentFailed.track(
|
|
168
|
+
order_id: '123',
|
|
169
|
+
error: 'Card declined',
|
|
170
|
+
severity: :error
|
|
171
|
+
)
|
|
172
|
+
# โ Sentry tag: trace_id = abc-123-def
|
|
173
|
+
|
|
174
|
+
# In your observability stack:
|
|
175
|
+
# 1. See error in Sentry with trace_id = abc-123-def
|
|
176
|
+
# 2. Search Loki/ELK: trace_id:"abc-123-def"
|
|
177
|
+
# 3. See FULL context (all events in request)
|
|
178
|
+
|
|
179
|
+
# Grafana query:
|
|
180
|
+
# {trace_id="abc-123-def"} |= ""
|
|
181
|
+
# โ Shows complete timeline of request
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### 4. Custom Fingerprinting
|
|
187
|
+
|
|
188
|
+
> **Implementation:** See [ADR-004 Section 4.4: Sentry Adapter](../ADR-004-adapter-architecture.md#44-sentry-adapter) for technical details.
|
|
189
|
+
|
|
190
|
+
**Group similar errors in Sentry:**
|
|
191
|
+
```ruby
|
|
192
|
+
E11y.configure do |config|
|
|
193
|
+
config.sentry do
|
|
194
|
+
# Custom fingerprint for better grouping
|
|
195
|
+
fingerprint_extractor ->(event_data) {
|
|
196
|
+
if event_data[:event_name] == 'payment.failed'
|
|
197
|
+
# Group by payment_method + error_code (not full error message)
|
|
198
|
+
[
|
|
199
|
+
event_data[:event_name],
|
|
200
|
+
event_data[:payload][:payment_method],
|
|
201
|
+
event_data[:payload][:error_code]
|
|
202
|
+
]
|
|
203
|
+
else
|
|
204
|
+
# Default: group by event name
|
|
205
|
+
[event_data[:event_name]]
|
|
206
|
+
end
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Result in Sentry:
|
|
212
|
+
# - "payment.failed + stripe + card_declined" (100 occurrences)
|
|
213
|
+
# - "payment.failed + paypal + insufficient_funds" (50 occurrences)
|
|
214
|
+
# Instead of 150 separate issues
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### 5. Sampling Control
|
|
220
|
+
|
|
221
|
+
> **Implementation:** See [ADR-004 Section 4.4: Sentry Adapter](../ADR-004-adapter-architecture.md#44-sentry-adapter) for technical details.
|
|
222
|
+
|
|
223
|
+
**Avoid Sentry quota exhaustion:**
|
|
224
|
+
```ruby
|
|
225
|
+
E11y.configure do |config|
|
|
226
|
+
config.sentry do
|
|
227
|
+
# Sample rate for Sentry (0.0 - 1.0)
|
|
228
|
+
sample_rate 1.0 # 100% (default)
|
|
229
|
+
|
|
230
|
+
# OR: Dynamic sampling per event
|
|
231
|
+
sample_rate_for 'payment.failed', 1.0 # Always capture
|
|
232
|
+
sample_rate_for 'api.slow_request', 0.1 # 10% (too noisy)
|
|
233
|
+
sample_rate_for 'user.action', 0.01 # 1% (very noisy)
|
|
234
|
+
|
|
235
|
+
# OR: Conditional sampling
|
|
236
|
+
sampler ->(event_data) {
|
|
237
|
+
if event_data[:severity] == :fatal
|
|
238
|
+
1.0 # Always capture fatal
|
|
239
|
+
elsif event_data[:context][:user_segment] == 'enterprise'
|
|
240
|
+
1.0 # Always capture enterprise users
|
|
241
|
+
else
|
|
242
|
+
0.1 # 10% for others
|
|
243
|
+
end
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## ๐ป Implementation Examples
|
|
252
|
+
|
|
253
|
+
### Example 1: Payment Processing
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# app/services/process_payment_service.rb
|
|
257
|
+
class ProcessPaymentService
|
|
258
|
+
def call(order)
|
|
259
|
+
# Track attempt
|
|
260
|
+
Events::PaymentAttempted.track(
|
|
261
|
+
order_id: order.id,
|
|
262
|
+
amount: order.total,
|
|
263
|
+
payment_method: order.payment_method
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
begin
|
|
267
|
+
# Process payment
|
|
268
|
+
result = PaymentGateway.charge(
|
|
269
|
+
amount: order.total,
|
|
270
|
+
card: order.card_token
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Track success
|
|
274
|
+
Events::PaymentSucceeded.track(
|
|
275
|
+
order_id: order.id,
|
|
276
|
+
transaction_id: result.id,
|
|
277
|
+
amount: order.total,
|
|
278
|
+
severity: :success # โ Positive signal (not error)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
rescue PaymentGateway::CardDeclined => e
|
|
282
|
+
# Track failure (automatically goes to Sentry)
|
|
283
|
+
Events::PaymentFailed.track(
|
|
284
|
+
order_id: order.id,
|
|
285
|
+
amount: order.total,
|
|
286
|
+
payment_method: order.payment_method,
|
|
287
|
+
error_class: e.class.name,
|
|
288
|
+
error_message: e.message,
|
|
289
|
+
error_code: e.code,
|
|
290
|
+
severity: :error # โ Automatically captured in Sentry
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
raise # Re-raise for caller to handle
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# In Sentry, you'll see:
|
|
299
|
+
# - Full breadcrumb trail (attempted โ failed)
|
|
300
|
+
# - Order context (ID, amount, payment method)
|
|
301
|
+
# - Error details (class, message, code)
|
|
302
|
+
# - Trace ID (to correlate with logs)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### Example 2: Background Jobs
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
# app/jobs/send_email_job.rb
|
|
311
|
+
class SendEmailJob < ApplicationJob
|
|
312
|
+
def perform(user_id, template)
|
|
313
|
+
user = User.find(user_id)
|
|
314
|
+
|
|
315
|
+
# Track start
|
|
316
|
+
Events::EmailSending.track(
|
|
317
|
+
user_id: user.id,
|
|
318
|
+
template: template
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
begin
|
|
322
|
+
# Send email
|
|
323
|
+
UserMailer.with(user: user).send(template).deliver_now
|
|
324
|
+
|
|
325
|
+
# Track success
|
|
326
|
+
Events::EmailSent.track(
|
|
327
|
+
user_id: user.id,
|
|
328
|
+
template: template,
|
|
329
|
+
severity: :success
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
rescue Net::SMTPError => e
|
|
333
|
+
# Track failure (goes to Sentry)
|
|
334
|
+
Events::EmailFailed.track(
|
|
335
|
+
user_id: user.id,
|
|
336
|
+
template: template,
|
|
337
|
+
error_class: e.class.name,
|
|
338
|
+
error_message: e.message,
|
|
339
|
+
severity: :error # โ Sentry capture
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Retry job (Sidekiq will handle)
|
|
343
|
+
raise
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Sentry shows:
|
|
349
|
+
# - Job context (user_id, template)
|
|
350
|
+
# - Retry attempts (Sidekiq integration)
|
|
351
|
+
# - Full error trace
|
|
352
|
+
# - Breadcrumbs (sending โ failed)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
### Example 3: API Integration Failures
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# app/services/sync_with_external_api_service.rb
|
|
361
|
+
class SyncWithExternalApiService
|
|
362
|
+
def call
|
|
363
|
+
Events::ApiSyncStarted.track(api: 'external_crm')
|
|
364
|
+
|
|
365
|
+
begin
|
|
366
|
+
response = HTTP.timeout(10).get('https://api.example.com/sync')
|
|
367
|
+
|
|
368
|
+
if response.status.success?
|
|
369
|
+
Events::ApiSyncSucceeded.track(
|
|
370
|
+
api: 'external_crm',
|
|
371
|
+
records_synced: response.parse['count'],
|
|
372
|
+
severity: :success
|
|
373
|
+
)
|
|
374
|
+
else
|
|
375
|
+
Events::ApiSyncFailed.track(
|
|
376
|
+
api: 'external_crm',
|
|
377
|
+
http_status: response.code,
|
|
378
|
+
response_body: response.body.to_s[0..500], # First 500 chars
|
|
379
|
+
severity: :error # โ Sentry capture
|
|
380
|
+
)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
rescue HTTP::TimeoutError => e
|
|
384
|
+
Events::ApiSyncTimeout.track(
|
|
385
|
+
api: 'external_crm',
|
|
386
|
+
timeout_seconds: 10,
|
|
387
|
+
error_message: e.message,
|
|
388
|
+
severity: :error # โ Sentry capture
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
rescue => e
|
|
392
|
+
Events::ApiSyncError.track(
|
|
393
|
+
api: 'external_crm',
|
|
394
|
+
error_class: e.class.name,
|
|
395
|
+
error_message: e.message,
|
|
396
|
+
severity: :fatal # โ Sentry capture (high priority)
|
|
397
|
+
)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## ๐ง Advanced Configuration
|
|
406
|
+
|
|
407
|
+
### Sentry Adapter (Custom Implementation)
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
# lib/e11y/adapters/sentry_adapter.rb
|
|
411
|
+
module E11y
|
|
412
|
+
module Adapters
|
|
413
|
+
class SentryAdapter < Base
|
|
414
|
+
def initialize(
|
|
415
|
+
capture_severities: [:error, :fatal],
|
|
416
|
+
breadcrumb_severities: [:debug, :info, :warn, :error],
|
|
417
|
+
include_payload: true,
|
|
418
|
+
max_payload_size: 10.kilobytes
|
|
419
|
+
)
|
|
420
|
+
@capture_severities = capture_severities
|
|
421
|
+
@breadcrumb_severities = breadcrumb_severities
|
|
422
|
+
@include_payload = include_payload
|
|
423
|
+
@max_payload_size = max_payload_size
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def send_batch(events)
|
|
427
|
+
events.each do |event|
|
|
428
|
+
# Add breadcrumb for ALL events
|
|
429
|
+
add_breadcrumb(event) if should_breadcrumb?(event)
|
|
430
|
+
|
|
431
|
+
# Capture exception for error events
|
|
432
|
+
capture_event(event) if should_capture?(event)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
Result.success
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
private
|
|
439
|
+
|
|
440
|
+
def should_breadcrumb?(event)
|
|
441
|
+
@breadcrumb_severities.include?(event[:severity])
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def should_capture?(event)
|
|
445
|
+
@capture_severities.include?(event[:severity])
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def add_breadcrumb(event)
|
|
449
|
+
Sentry.add_breadcrumb(
|
|
450
|
+
Sentry::Breadcrumb.new(
|
|
451
|
+
category: 'e11y',
|
|
452
|
+
message: event[:event_name],
|
|
453
|
+
data: truncate_payload(event[:payload]),
|
|
454
|
+
level: sentry_level(event[:severity]),
|
|
455
|
+
timestamp: event[:timestamp].to_i
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def capture_event(event)
|
|
461
|
+
Sentry.capture_message(
|
|
462
|
+
"#{event[:event_name]}: #{event[:payload][:error_message] || 'Event'}",
|
|
463
|
+
level: sentry_level(event[:severity]),
|
|
464
|
+
extra: build_extra(event),
|
|
465
|
+
tags: build_tags(event),
|
|
466
|
+
fingerprint: build_fingerprint(event)
|
|
467
|
+
)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def build_extra(event)
|
|
471
|
+
extra = {
|
|
472
|
+
event_name: event[:event_name],
|
|
473
|
+
event_id: event[:event_id],
|
|
474
|
+
trace_id: event[:trace_id],
|
|
475
|
+
timestamp: event[:timestamp].iso8601
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if @include_payload
|
|
479
|
+
extra[:payload] = truncate_payload(event[:payload])
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
extra.merge!(event[:context]) if event[:context]
|
|
483
|
+
extra
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def build_tags(event)
|
|
487
|
+
{
|
|
488
|
+
event_name: event[:event_name],
|
|
489
|
+
trace_id: event[:trace_id],
|
|
490
|
+
severity: event[:severity],
|
|
491
|
+
env: event[:context][:env],
|
|
492
|
+
service: event[:context][:service]
|
|
493
|
+
}.compact
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def build_fingerprint(event)
|
|
497
|
+
if E11y.config.sentry.fingerprint_extractor
|
|
498
|
+
E11y.config.sentry.fingerprint_extractor.call(event)
|
|
499
|
+
else
|
|
500
|
+
[event[:event_name]]
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def sentry_level(severity)
|
|
505
|
+
case severity
|
|
506
|
+
when :debug then :debug
|
|
507
|
+
when :info, :success then :info
|
|
508
|
+
when :warn then :warning
|
|
509
|
+
when :error then :error
|
|
510
|
+
when :fatal then :fatal
|
|
511
|
+
else :info
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def truncate_payload(payload)
|
|
516
|
+
json = payload.to_json
|
|
517
|
+
if json.bytesize > @max_payload_size
|
|
518
|
+
truncated = json[0...@max_payload_size]
|
|
519
|
+
JSON.parse(truncated + '...')
|
|
520
|
+
else
|
|
521
|
+
payload
|
|
522
|
+
end
|
|
523
|
+
rescue JSON::ParserError
|
|
524
|
+
{ _truncated: true, _size: json.bytesize }
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## ๐ Monitoring
|
|
534
|
+
|
|
535
|
+
### Sentry Quota Management
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
# Track Sentry events sent (self-monitoring)
|
|
539
|
+
E11y.configure do |config|
|
|
540
|
+
config.self_monitoring do
|
|
541
|
+
counter :sentry_events_sent_total,
|
|
542
|
+
tags: [:event_name, :severity]
|
|
543
|
+
|
|
544
|
+
counter :sentry_events_sampled_out_total,
|
|
545
|
+
tags: [:event_name]
|
|
546
|
+
|
|
547
|
+
gauge :sentry_quota_used_pct,
|
|
548
|
+
comment: 'Percentage of Sentry quota used'
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Alert on high Sentry usage
|
|
553
|
+
# sentry_events_sent_total > 1000/min โ alert
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## ๐งช Testing
|
|
559
|
+
|
|
560
|
+
```ruby
|
|
561
|
+
# spec/e11y/sentry_integration_spec.rb
|
|
562
|
+
RSpec.describe 'Sentry Integration' do
|
|
563
|
+
before do
|
|
564
|
+
# Mock Sentry
|
|
565
|
+
allow(Sentry).to receive(:capture_message)
|
|
566
|
+
allow(Sentry).to receive(:add_breadcrumb)
|
|
567
|
+
|
|
568
|
+
E11y.configure do |config|
|
|
569
|
+
config.sentry do
|
|
570
|
+
enabled true
|
|
571
|
+
capture_severities [:error, :fatal]
|
|
572
|
+
breadcrumb_severities [:info, :error]
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
it 'captures error events in Sentry' do
|
|
578
|
+
Events::PaymentFailed.track(
|
|
579
|
+
order_id: '123',
|
|
580
|
+
error_message: 'Card declined',
|
|
581
|
+
severity: :error
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
expect(Sentry).to have_received(:capture_message).with(
|
|
585
|
+
'payment.failed: Card declined',
|
|
586
|
+
hash_including(
|
|
587
|
+
level: :error,
|
|
588
|
+
extra: hash_including(event_name: 'payment.failed'),
|
|
589
|
+
tags: hash_including(event_name: 'payment.failed')
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
it 'adds breadcrumbs for all events' do
|
|
595
|
+
Events::CartViewed.track(user_id: '123', items: 3, severity: :info)
|
|
596
|
+
|
|
597
|
+
expect(Sentry).to have_received(:add_breadcrumb).with(
|
|
598
|
+
an_instance_of(Sentry::Breadcrumb)
|
|
599
|
+
)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
it 'does not capture info events' do
|
|
603
|
+
Events::OrderPaid.track(order_id: '123', amount: 99.99, severity: :info)
|
|
604
|
+
|
|
605
|
+
expect(Sentry).not_to have_received(:capture_message)
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## ๐ก Best Practices
|
|
613
|
+
|
|
614
|
+
### โ
DO
|
|
615
|
+
|
|
616
|
+
**1. Use :error/:fatal for exceptions only**
|
|
617
|
+
```ruby
|
|
618
|
+
# โ
GOOD: Real errors
|
|
619
|
+
Events::PaymentFailed.track(error: e.message, severity: :error)
|
|
620
|
+
Events::DatabaseConnectionLost.track(severity: :fatal)
|
|
621
|
+
|
|
622
|
+
# โ BAD: Business logic (not errors)
|
|
623
|
+
Events::UserLoggedOut.track(severity: :error) # โ NOT an error!
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**2. Include error details**
|
|
627
|
+
```ruby
|
|
628
|
+
# โ
GOOD: Full context
|
|
629
|
+
Events::ApiCallFailed.track(
|
|
630
|
+
api: 'external_crm',
|
|
631
|
+
error_class: e.class.name,
|
|
632
|
+
error_message: e.message,
|
|
633
|
+
error_code: e.code,
|
|
634
|
+
http_status: response.code,
|
|
635
|
+
severity: :error
|
|
636
|
+
)
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
**3. Use fingerprinting for grouping**
|
|
640
|
+
```ruby
|
|
641
|
+
# Group similar errors together
|
|
642
|
+
fingerprint_extractor ->(event) {
|
|
643
|
+
[event[:event_name], event[:payload][:error_code]]
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
### โ DON'T
|
|
650
|
+
|
|
651
|
+
**1. Don't send PII to Sentry**
|
|
652
|
+
```ruby
|
|
653
|
+
# โ BAD: PII in error message
|
|
654
|
+
Events::LoginFailed.track(
|
|
655
|
+
email: 'user@example.com', # โ PII!
|
|
656
|
+
password: '***', # โ Even worse!
|
|
657
|
+
severity: :error
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
# โ
GOOD: Filtered
|
|
661
|
+
Events::LoginFailed.track(
|
|
662
|
+
user_id: '123', # IDs are OK
|
|
663
|
+
error: 'Invalid credentials',
|
|
664
|
+
severity: :error
|
|
665
|
+
)
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**2. Don't overload Sentry with noisy events**
|
|
669
|
+
```ruby
|
|
670
|
+
# โ BAD: Too noisy
|
|
671
|
+
Events::ApiSlowRequest.track(duration: 501, severity: :error) # Every slow request!
|
|
672
|
+
|
|
673
|
+
# โ
GOOD: Sample or use higher threshold
|
|
674
|
+
sample_rate_for 'api.slow_request', 0.1 # 10%
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## ๐ Related Use Cases
|
|
680
|
+
|
|
681
|
+
- **[UC-002: Business Event Tracking](./UC-002-business-event-tracking.md)** - Event definitions
|
|
682
|
+
- **[UC-007: PII Filtering](./UC-007-pii-filtering.md)** - Prevent PII leaks to Sentry
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## ๐ฆ Implementation Details (2026-01-19)
|
|
687
|
+
|
|
688
|
+
### Actual SentryAdapter Implementation
|
|
689
|
+
|
|
690
|
+
The implemented `E11y::Adapters::Sentry` provides:
|
|
691
|
+
|
|
692
|
+
**Features:**
|
|
693
|
+
- โ
**Severity-based filtering**: Configurable `severity_threshold` (default: `:warn`)
|
|
694
|
+
- โ
**Error reporting**: Automatic `Sentry.capture_message` for `:error` and `:fatal` events
|
|
695
|
+
- โ
**Exception handling**: Direct `Sentry.capture_exception` when exception object provided
|
|
696
|
+
- โ
**Breadcrumbs**: All non-error events tracked as `Sentry.add_breadcrumb`
|
|
697
|
+
- โ
**Context propagation**: Tags, extras, user context, and trace context
|
|
698
|
+
- โ
**Severity mapping**: E11y severities โ Sentry levels (debug/info/warning/error/fatal)
|
|
699
|
+
|
|
700
|
+
**Usage Example:**
|
|
701
|
+
```ruby
|
|
702
|
+
# Register adapter
|
|
703
|
+
E11y::Adapters::Registry.register(
|
|
704
|
+
:sentry,
|
|
705
|
+
E11y::Adapters::Sentry.new(
|
|
706
|
+
dsn: ENV['SENTRY_DSN'],
|
|
707
|
+
environment: 'production',
|
|
708
|
+
severity_threshold: :warn,
|
|
709
|
+
breadcrumbs: true
|
|
710
|
+
)
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
# Track error event
|
|
714
|
+
Events::PaymentFailed.track(
|
|
715
|
+
order_id: 'ORD-123',
|
|
716
|
+
error_message: 'Card declined',
|
|
717
|
+
user: { id: 456, email: 'user@example.com' },
|
|
718
|
+
trace_id: 'trace-abc-123',
|
|
719
|
+
span_id: 'span-def-456'
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
# Result in Sentry:
|
|
723
|
+
# - Message: "Card declined"
|
|
724
|
+
# - Tags: { event_name: "payment.failed", severity: "error" }
|
|
725
|
+
# - Extras: { order_id: "ORD-123", ... }
|
|
726
|
+
# - User: { id: 456, email: "user@example.com" }
|
|
727
|
+
# - Context: { trace: { trace_id: "trace-abc-123", span_id: "span-def-456" } }
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**Testing:**
|
|
731
|
+
```ruby
|
|
732
|
+
# spec/e11y/adapters/sentry_spec.rb - 39 tests
|
|
733
|
+
RSpec.describe E11y::Adapters::Sentry do
|
|
734
|
+
it 'sends errors to Sentry' do
|
|
735
|
+
expect(::Sentry).to receive(:capture_message).with(
|
|
736
|
+
"Payment processing failed",
|
|
737
|
+
level: :error
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
adapter.write(error_event)
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
it 'sends breadcrumbs for non-error events' do
|
|
744
|
+
expect(::Sentry).to receive(:add_breadcrumb)
|
|
745
|
+
adapter.write(warn_event)
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
**See Also:**
|
|
751
|
+
- Implementation: `lib/e11y/adapters/sentry.rb` (211 lines)
|
|
752
|
+
- Tests: `spec/e11y/adapters/sentry_spec.rb` (39 tests)
|
|
753
|
+
- ADR: [ADR-004 ยง4.4](../ADR-004-adapter-architecture.md#44-sentry-adapter)
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
**Document Version:** 2.0
|
|
758
|
+
**Last Updated:** January 19, 2026
|
|
759
|
+
**Status:** โ
Implemented & Tested
|