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,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