e11y 0.2.0 → 1.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 (288) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +130 -10
  3. data/CHANGELOG.md +80 -1
  4. data/CLAUDE.md +168 -0
  5. data/CONTRIBUTING.md +640 -0
  6. data/README.md +165 -701
  7. data/RELEASE.md +41 -12
  8. data/Rakefile +249 -57
  9. data/config/README.md +1 -1
  10. data/config/loki-local-config.yaml +12 -0
  11. data/config/otel-collector-config.yaml +44 -0
  12. data/cucumber.yml +1 -0
  13. data/docker-compose.yml +18 -2
  14. data/docs/ADAPTERS.md +76 -0
  15. data/docs/ADAPTIVE_SAMPLING.md +59 -0
  16. data/docs/COMPARISON.md +104 -0
  17. data/docs/CONFIGURATION.md +52 -0
  18. data/docs/DISTRIBUTED_TRACING.md +44 -0
  19. data/docs/LIMITATIONS.md +13 -0
  20. data/docs/METRICS_DSL.md +84 -0
  21. data/docs/PERFORMANCE.md +60 -0
  22. data/docs/PII_FILTERING.md +40 -0
  23. data/docs/PRESETS.md +65 -0
  24. data/docs/QUICK-START.md +546 -587
  25. data/docs/RAILS_INTEGRATION.md +79 -0
  26. data/docs/SCHEMA_VALIDATION.md +63 -0
  27. data/docs/SLO-PROMQL-ALERTS.md +161 -0
  28. data/docs/TESTING.md +69 -0
  29. data/docs/{ADR-001-architecture.md → architecture/ADR-001-architecture.md} +36 -65
  30. data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
  31. data/docs/architecture/ADR-003-slo-observability.md +1402 -0
  32. data/docs/{ADR-004-adapter-architecture.md → architecture/ADR-004-adapter-architecture.md} +163 -146
  33. data/docs/{ADR-005-tracing-context.md → architecture/ADR-005-tracing-context.md} +10 -9
  34. data/docs/{ADR-006-security-compliance.md → architecture/ADR-006-security-compliance.md} +184 -191
  35. data/docs/{ADR-007-opentelemetry-integration.md → architecture/ADR-007-opentelemetry-integration.md} +3 -21
  36. data/docs/{ADR-008-rails-integration.md → architecture/ADR-008-rails-integration.md} +182 -743
  37. data/docs/{ADR-009-cost-optimization.md → architecture/ADR-009-cost-optimization.md} +45 -54
  38. data/docs/architecture/ADR-010-developer-experience.md +522 -0
  39. data/docs/{ADR-011-testing-strategy.md → architecture/ADR-011-testing-strategy.md} +44 -86
  40. data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +11 -11
  41. data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
  42. data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
  43. data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +43 -59
  44. data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +58 -355
  45. data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
  46. data/docs/architecture/ADR-018-memory-optimization.md +366 -0
  47. data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
  48. data/docs/plans/2026-03-20-browser-overlay-svelte.md +281 -0
  49. data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
  50. data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
  51. data/docs/prd/01-overview-vision.md +19 -14
  52. data/docs/use_cases/README.md +22 -23
  53. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
  54. data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
  55. data/docs/use_cases/UC-003-event-metrics.md +66 -0
  56. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +33 -684
  57. data/docs/use_cases/UC-005-sentry-integration.md +13 -15
  58. data/docs/use_cases/UC-006-trace-context-management.md +30 -28
  59. data/docs/use_cases/UC-007-pii-filtering.md +35 -87
  60. data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
  61. data/docs/use_cases/UC-009-multi-service-tracing.md +30 -178
  62. data/docs/use_cases/UC-010-background-job-tracking.md +24 -91
  63. data/docs/use_cases/UC-011-rate-limiting.md +95 -168
  64. data/docs/use_cases/UC-012-audit-trail.md +21 -46
  65. data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
  66. data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
  67. data/docs/use_cases/UC-015-cost-optimization.md +46 -99
  68. data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
  69. data/docs/use_cases/UC-017-local-development.md +203 -777
  70. data/docs/use_cases/UC-018-testing-events.md +3 -3
  71. data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
  72. data/docs/use_cases/UC-020-event-versioning.md +8 -9
  73. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
  74. data/docs/use_cases/UC-022-event-registry.md +15 -21
  75. data/docs/use_cases/backlog.md +119 -87
  76. data/e11y.gemspec +2 -2
  77. data/gems/e11y-devtools/README.md +158 -0
  78. data/gems/e11y-devtools/config/routes.rb +15 -0
  79. data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
  80. data/gems/e11y-devtools/exe/e11y +34 -0
  81. data/gems/e11y-devtools/frontend/.gitignore +24 -0
  82. data/gems/e11y-devtools/frontend/README.md +51 -0
  83. data/gems/e11y-devtools/frontend/index.html +14 -0
  84. data/gems/e11y-devtools/frontend/package-lock.json +3707 -0
  85. data/gems/e11y-devtools/frontend/package.json +28 -0
  86. data/gems/e11y-devtools/frontend/public/mocks/v1/events/recent.json +4205 -0
  87. data/gems/e11y-devtools/frontend/public/mocks/v1/interactions.json +194 -0
  88. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0a2e04027cfa22d014bc22e8b27cd913/events.json +86 -0
  89. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0e1543af6a630fb3af6b52283154b3e0/events.json +169 -0
  90. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/1838b691faa49564f97db8592ff3978d/events.json +78 -0
  91. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/29f198f6588dacffb687777eb5f8f118/events.json +197 -0
  92. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/34bc3c9c0097de28a7a6f99b90a8e7bc/events.json +194 -0
  93. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/3ba6c20d068ab9cee00e51b180e66444/events.json +184 -0
  94. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/435bfd8f17b9009146a79812d7c3726d/events.json +144 -0
  95. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/4c7676e3fe668e99edb2b94d7d5678a9/events.json +222 -0
  96. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/6daf0d47974bedfc55d5de7004a3ea9f/events.json +194 -0
  97. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8a81ada42834d15f287bb40010043605/events.json +194 -0
  98. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8c0a98900edaae105469df8daedccf02/events.json +198 -0
  99. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8e4f645180f8a7d1dce426b07380466b/events.json +222 -0
  100. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/93db346fa5d44a032605a13b627f4b80/events.json +128 -0
  101. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/98ff6146faf7bd9be8bd03a8275817ba/events.json +223 -0
  102. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/9997ddd0247bc7e25f2ca7a5c415c93d/events.json +197 -0
  103. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/99e35f8ef3baedd798cc4fd085980ad9/events.json +194 -0
  104. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b4f3095c1909924cbc98889a86c83d6d/events.json +131 -0
  105. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b54b7fc32b7575a7110de809d11ccda0/events.json +128 -0
  106. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c0b48033fa06746bcc5886745e053cff/events.json +169 -0
  107. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c44649ac76701b4558927cd2305ab535/events.json +169 -0
  108. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/d601ae3320057580a39dbdac2edfdf4a/events.json +248 -0
  109. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e67e724bab422d2b52eeb49635e512e1/events.json +194 -0
  110. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e6c72765a28f158a8485b35fa63f73da/events.json +194 -0
  111. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/f541b87405c9a54819b18ebe529f6419/events.json +194 -0
  112. data/gems/e11y-devtools/frontend/scripts/generate_mocks.rb +397 -0
  113. data/gems/e11y-devtools/frontend/src/App.svelte +827 -0
  114. data/gems/e11y-devtools/frontend/src/components/Fab.svelte +19 -0
  115. data/gems/e11y-devtools/frontend/src/components/FilterBar.svelte +38 -0
  116. data/gems/e11y-devtools/frontend/src/components/FullscreenPanel.svelte +82 -0
  117. data/gems/e11y-devtools/frontend/src/components/InteractionsTimeline.svelte +264 -0
  118. data/gems/e11y-devtools/frontend/src/components/RecentHistogram.svelte +354 -0
  119. data/gems/e11y-devtools/frontend/src/lib/api.ts +37 -0
  120. data/gems/e11y-devtools/frontend/src/lib/eventIdentity.ts +12 -0
  121. data/gems/e11y-devtools/frontend/src/lib/format.ts +37 -0
  122. data/gems/e11y-devtools/frontend/src/lib/listFilter.ts +43 -0
  123. data/gems/e11y-devtools/frontend/src/lib/recentVolume.ts +80 -0
  124. data/gems/e11y-devtools/frontend/src/lib/router.ts +12 -0
  125. data/gems/e11y-devtools/frontend/src/lib/transitions.ts +34 -0
  126. data/gems/e11y-devtools/frontend/src/lib/viewportOrigin.ts +25 -0
  127. data/gems/e11y-devtools/frontend/src/main.ts +8 -0
  128. data/gems/e11y-devtools/frontend/src/overlay-entry.ts +24 -0
  129. data/gems/e11y-devtools/frontend/src/overlay.css +1080 -0
  130. data/gems/e11y-devtools/frontend/svelte.config.js +2 -0
  131. data/gems/e11y-devtools/frontend/test_puppeteer.js +41 -0
  132. data/gems/e11y-devtools/frontend/test_scale.js +3 -0
  133. data/gems/e11y-devtools/frontend/tsconfig.app.json +21 -0
  134. data/gems/e11y-devtools/frontend/tsconfig.json +7 -0
  135. data/gems/e11y-devtools/frontend/tsconfig.node.json +26 -0
  136. data/gems/e11y-devtools/frontend/vite.config.ts +36 -0
  137. data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
  138. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
  139. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
  140. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
  141. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
  142. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
  143. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
  144. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
  145. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
  146. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
  147. data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +20 -0
  148. data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +94 -0
  149. data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
  150. data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
  151. data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +67 -0
  152. data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
  153. data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
  154. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
  155. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
  156. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
  157. data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
  158. data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
  159. data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
  160. data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +91 -0
  161. data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
  162. data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
  163. data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
  164. data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
  165. data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
  166. data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
  167. data/lib/e11y/adapters/audit_encrypted.rb +53 -11
  168. data/lib/e11y/adapters/base.rb +33 -34
  169. data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
  170. data/lib/e11y/adapters/dev_log/query.rb +219 -0
  171. data/lib/e11y/adapters/dev_log.rb +118 -0
  172. data/lib/e11y/adapters/file.rb +3 -6
  173. data/lib/e11y/adapters/in_memory.rb +52 -5
  174. data/lib/e11y/adapters/in_memory_test.rb +29 -0
  175. data/lib/e11y/adapters/loki.rb +58 -23
  176. data/lib/e11y/adapters/null.rb +82 -0
  177. data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
  178. data/lib/e11y/adapters/otel_logs.rb +136 -23
  179. data/lib/e11y/adapters/sentry.rb +4 -7
  180. data/lib/e11y/adapters/stdout.rb +73 -7
  181. data/lib/e11y/adapters/yabeda.rb +153 -29
  182. data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
  183. data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
  184. data/lib/e11y/buffers/ring_buffer.rb +3 -16
  185. data/lib/e11y/configuration.rb +272 -0
  186. data/lib/e11y/console.rb +10 -17
  187. data/lib/e11y/current.rb +53 -1
  188. data/lib/e11y/debug/pipeline_inspector.rb +96 -0
  189. data/lib/e11y/documentation/generator.rb +48 -0
  190. data/lib/e11y/event/base.rb +176 -82
  191. data/lib/e11y/event/value_sampling_config.rb +1 -5
  192. data/lib/e11y/events/rails/database/query.rb +1 -4
  193. data/lib/e11y/events/rails/job/failed.rb +2 -0
  194. data/lib/e11y/instruments/active_job.rb +44 -12
  195. data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
  196. data/lib/e11y/instruments/sidekiq.rb +135 -31
  197. data/lib/e11y/linters/base.rb +11 -0
  198. data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
  199. data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
  200. data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
  201. data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
  202. data/lib/e11y/logger/bridge.rb +26 -7
  203. data/lib/e11y/metrics/cardinality_protection.rb +10 -15
  204. data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
  205. data/lib/e11y/metrics/registry.rb +3 -5
  206. data/lib/e11y/metrics/test_backend.rb +62 -0
  207. data/lib/e11y/metrics.rb +56 -10
  208. data/lib/e11y/middleware/adapter_resolver.rb +40 -0
  209. data/lib/e11y/middleware/audit_signing.rb +43 -6
  210. data/lib/e11y/middleware/baggage_protection.rb +75 -0
  211. data/lib/e11y/middleware/dev_log_source.rb +24 -0
  212. data/lib/e11y/middleware/event_slo.rb +23 -9
  213. data/lib/e11y/middleware/otel_span.rb +23 -0
  214. data/lib/e11y/middleware/pii_filter.rb +104 -75
  215. data/lib/e11y/middleware/rate_limiting.rb +54 -27
  216. data/lib/e11y/middleware/request.rb +70 -23
  217. data/lib/e11y/middleware/routing.rb +78 -21
  218. data/lib/e11y/middleware/sampling.rb +66 -17
  219. data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
  220. data/lib/e11y/middleware/trace_context.rb +45 -10
  221. data/lib/e11y/middleware/track_latency.rb +34 -0
  222. data/lib/e11y/middleware/validation.rb +7 -16
  223. data/lib/e11y/middleware/versioning.rb +26 -22
  224. data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
  225. data/lib/e11y/opentelemetry/span_creator.rb +142 -0
  226. data/lib/e11y/pii/patterns.rb +12 -1
  227. data/lib/e11y/pipeline/builder.rb +4 -4
  228. data/lib/e11y/presets/audit_event.rb +13 -2
  229. data/lib/e11y/railtie.rb +52 -14
  230. data/lib/e11y/registry.rb +306 -0
  231. data/lib/e11y/reliability/circuit_breaker.rb +19 -21
  232. data/lib/e11y/reliability/dlq/base.rb +71 -0
  233. data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
  234. data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
  235. data/lib/e11y/reliability/dlq/filter.rb +37 -54
  236. data/lib/e11y/reliability/retry_handler.rb +26 -29
  237. data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
  238. data/lib/e11y/sampling/error_spike_detector.rb +0 -2
  239. data/lib/e11y/sampling/load_monitor.rb +5 -9
  240. data/lib/e11y/sampling/stratified_tracker.rb +18 -0
  241. data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
  242. data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
  243. data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
  244. data/lib/e11y/slo/config_loader.rb +40 -0
  245. data/lib/e11y/slo/config_validator.rb +58 -0
  246. data/lib/e11y/slo/dashboard_generator.rb +122 -0
  247. data/lib/e11y/slo/event_driven.rb +8 -0
  248. data/lib/e11y/slo/tracker.rb +31 -4
  249. data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
  250. data/lib/e11y/testing/rspec_matchers.rb +21 -0
  251. data/lib/e11y/testing/snapshot_matcher.rb +86 -0
  252. data/lib/e11y/trace_context/sampler.rb +35 -0
  253. data/lib/e11y/tracing/faraday_middleware.rb +31 -0
  254. data/lib/e11y/tracing/net_http_patch.rb +33 -0
  255. data/lib/e11y/tracing/propagator.rb +144 -0
  256. data/lib/e11y/tracing.rb +47 -0
  257. data/lib/e11y/version.rb +1 -1
  258. data/lib/e11y/versioning/version_extractor.rb +32 -0
  259. data/lib/e11y.rb +123 -266
  260. data/lib/generators/e11y/event/event_generator.rb +22 -0
  261. data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
  262. data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
  263. data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
  264. data/lib/generators/e11y/install/install_generator.rb +34 -0
  265. data/lib/generators/e11y/install/templates/e11y.rb +239 -0
  266. data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
  267. data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
  268. data/lib/tasks/e11y_docs.rake +30 -0
  269. data/lib/tasks/e11y_events.rake +71 -0
  270. data/lib/tasks/e11y_lint.rake +91 -0
  271. data/lib/tasks/e11y_slo.rake +29 -0
  272. metadata +186 -39
  273. data/docs/ADR-003-slo-observability.md +0 -3337
  274. data/docs/ADR-010-developer-experience.md +0 -2166
  275. data/docs/API-REFERENCE-L28.md +0 -914
  276. data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
  277. data/docs/CONTRIBUTING.md +0 -312
  278. data/docs/IMPLEMENTATION_NOTES.md +0 -2804
  279. data/docs/IMPLEMENTATION_PLAN.md +0 -1971
  280. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
  281. data/docs/PLAN.md +0 -148
  282. data/docs/README.md +0 -296
  283. data/docs/design/00-memory-optimization.md +0 -593
  284. data/docs/guides/MIGRATION-L27-L28.md +0 -692
  285. data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
  286. data/docs/guides/README.md +0 -44
  287. data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
  288. data/lib/e11y/adapters/registry.rb +0 -141
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ratatui_ruby"
4
+
5
+ module E11y
6
+ module Devtools
7
+ module Tui
8
+ module Widgets
9
+ # Renders a table of events for the selected trace.
10
+ class EventList
11
+ SEVERITY_COLORS = {
12
+ "debug" => :dark_gray,
13
+ "info" => :white,
14
+ "warn" => :yellow,
15
+ "error" => :red,
16
+ "fatal" => :red
17
+ }.freeze
18
+
19
+ def initialize(events:, trace_id:, selected_index: 0)
20
+ @events = events
21
+ @trace_id = trace_id
22
+ @selected_index = selected_index
23
+ end
24
+
25
+ def render(tui, frame, area)
26
+ frame.render_widget(
27
+ tui.table(
28
+ header: ["#", "Severity", "Event Name", "Duration", "At"],
29
+ rows: build_rows(tui),
30
+ row_highlight_style: { bg: :dark_gray },
31
+ selected_row: @selected_index,
32
+ block: tui.block(title: " #{@trace_id} ", borders: :all)
33
+ ),
34
+ area
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def build_rows(tui)
41
+ @events.each_with_index.map do |e, i|
42
+ sev = e["severity"] || "info"
43
+ color = SEVERITY_COLORS.fetch(sev, :white)
44
+ [
45
+ (i + 1).to_s,
46
+ tui.span(content: sev.upcase, style: { fg: color }),
47
+ e["event_name"].to_s,
48
+ duration_str(e),
49
+ timestamp_short(e["timestamp"])
50
+ ]
51
+ end
52
+ end
53
+
54
+ def duration_str(event)
55
+ ms = event.dig("metadata", "duration_ms")
56
+ ms ? "#{ms}ms" : "—"
57
+ end
58
+
59
+ def timestamp_short(timestamp)
60
+ return "—" unless timestamp
61
+
62
+ Time.parse(timestamp).strftime(".%L")
63
+ rescue ArgumentError
64
+ "—"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ratatui_ruby"
4
+ require_relative "../grouping"
5
+
6
+ module E11y
7
+ module Devtools
8
+ module Tui
9
+ module Widgets
10
+ # Renders a scrollable list of interaction groups.
11
+ # Each row shows: bullet (● error / ○ ok), time, trace count.
12
+ class InteractionList
13
+ def initialize(interactions:, selected_index: 0, source_filter: :all)
14
+ @interactions = interactions
15
+ @selected_index = selected_index
16
+ @source_filter = source_filter
17
+ end
18
+
19
+ def render(tui, frame, area)
20
+ rows = @interactions.map do |ix|
21
+ bullet = ix.has_error? ? "●" : "○"
22
+ bullet_fg = ix.has_error? ? :red : :gray
23
+ time_str = ix.started_at.strftime("%H:%M:%S")
24
+ count_str = "#{ix.trace_ids.size} req"
25
+ error_str = ix.has_error? ? " ● err" : ""
26
+
27
+ tui.line(spans: [
28
+ tui.span(content: bullet, style: { fg: bullet_fg }),
29
+ tui.span(content: " #{time_str} #{count_str}#{error_str}")
30
+ ])
31
+ end
32
+
33
+ frame.render_widget(
34
+ tui.list(
35
+ items: rows,
36
+ highlight_style: { bg: :dark_gray },
37
+ selected_index: @selected_index,
38
+ block: tui.block(title: " INTERACTIONS ", borders: :all)
39
+ ),
40
+ area
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Devtools
5
+ VERSION = "0.1.1"
6
+ CORE_VERSION = "1.0" # compatible e11y gem version
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y"
4
+ require_relative "devtools/version"
5
+
6
+ module E11y
7
+ # Developer tooling for E11y: TUI, Browser Overlay, and MCP Server.
8
+ module Devtools
9
+ autoload :Tui, "e11y/devtools/tui"
10
+ autoload :Overlay, "e11y/devtools/overlay"
11
+ autoload :Mcp, "e11y/devtools/mcp"
12
+ end
13
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+ require "json"
6
+ require "securerandom"
7
+ require "time"
8
+ require "fileutils"
9
+ require "e11y/adapters/dev_log/query"
10
+
11
+ # Load tool files (guarded in case mcp gem is absent)
12
+ %w[
13
+ recent_events events_by_trace search stats errors clear
14
+ ].each do |name|
15
+ require "e11y/devtools/mcp/tools/#{name}"
16
+ end
17
+
18
+ RSpec.describe E11y::Devtools::Mcp::Tools do
19
+ let(:error_severities) { %w[error fatal] }
20
+ let(:dir) { Dir.mktmpdir("e11y_mcp") }
21
+ let(:path) { File.join(dir, "e11y_dev.jsonl") }
22
+ let(:store) { E11y::Adapters::DevLog::Query.new(path) }
23
+ let(:ctx) { { store: store } }
24
+
25
+ after { FileUtils.remove_entry(dir) }
26
+
27
+ def write_event(overrides = {})
28
+ data = {
29
+ "id" => SecureRandom.uuid,
30
+ "timestamp" => Time.now.iso8601(3),
31
+ "event_name" => "test.event",
32
+ "severity" => "info",
33
+ "trace_id" => "t1",
34
+ "payload" => {},
35
+ "metadata" => {}
36
+ }.merge(overrides)
37
+ File.open(path, "a") { |f| f.puts(JSON.generate(data)) }
38
+ end
39
+
40
+ describe E11y::Devtools::Mcp::Tools::RecentEvents do
41
+ it "returns recent events as array" do
42
+ write_event
43
+ result = described_class.call(limit: 10, server_context: ctx)
44
+ expect(result).to be_an(Array)
45
+ expect(result.first["event_name"]).to eq("test.event")
46
+ end
47
+
48
+ it "respects limit" do
49
+ 5.times { write_event }
50
+ result = described_class.call(limit: 2, server_context: ctx)
51
+ expect(result.size).to eq(2)
52
+ end
53
+
54
+ it "filters by severity" do
55
+ write_event("severity" => "info")
56
+ write_event("severity" => "error")
57
+ result = described_class.call(limit: 10, severity: "error", server_context: ctx)
58
+ expect(result.all? { |e| e["severity"] == "error" }).to be true
59
+ end
60
+ end
61
+
62
+ describe E11y::Devtools::Mcp::Tools::EventsByTrace do
63
+ it "returns events for given trace_id" do
64
+ write_event("trace_id" => "abc", "event_name" => "a")
65
+ write_event("trace_id" => "xyz", "event_name" => "b")
66
+ result = described_class.call(trace_id: "abc", server_context: ctx)
67
+ expect(result.size).to eq(1)
68
+ expect(result.first["event_name"]).to eq("a")
69
+ end
70
+ end
71
+
72
+ describe E11y::Devtools::Mcp::Tools::Search do
73
+ it "finds events matching query" do
74
+ write_event("event_name" => "payment.failed")
75
+ write_event("event_name" => "order.created")
76
+ result = described_class.call(query: "payment", limit: 10, server_context: ctx)
77
+ expect(result.size).to eq(1)
78
+ end
79
+ end
80
+
81
+ describe E11y::Devtools::Mcp::Tools::Stats do
82
+ it "returns stats hash" do
83
+ write_event
84
+ result = described_class.call(server_context: ctx)
85
+ expect(result).to be_a(Hash)
86
+ expect(result).to have_key(:total_events)
87
+ end
88
+ end
89
+
90
+ describe E11y::Devtools::Mcp::Tools::Errors do
91
+ it "returns only error/fatal events" do
92
+ write_event("severity" => "info")
93
+ write_event("severity" => "error")
94
+ result = described_class.call(limit: 10, server_context: ctx)
95
+ expect(result.all? { |e| error_severities.include?(e["severity"]) }).to be true
96
+ end
97
+ end
98
+
99
+ describe E11y::Devtools::Mcp::Tools::Clear do
100
+ it "clears the log and returns confirmation string" do
101
+ write_event
102
+ result = described_class.call(server_context: ctx)
103
+ expect(result).to include("cleared")
104
+ expect(store.stored_events).to be_empty
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+ require "json"
6
+ require "securerandom"
7
+ require "e11y/adapters/dev_log/query"
8
+ require "e11y/devtools/overlay/controller"
9
+
10
+ RSpec.describe E11y::Devtools::Overlay::Controller do
11
+ let(:dir) { Dir.mktmpdir("e11y_ctrl") }
12
+ let(:controller) { described_class.new(query) }
13
+ let(:path) { File.join(dir, "e11y_dev.jsonl") }
14
+ let(:query) { E11y::Adapters::DevLog::Query.new(path) }
15
+
16
+ after { FileUtils.remove_entry(dir) }
17
+
18
+ def write_event(name: "test.event", severity: "info", trace_id: "t1")
19
+ data = {
20
+ "id" => SecureRandom.uuid, "timestamp" => Time.now.iso8601(3),
21
+ "event_name" => name, "severity" => severity,
22
+ "trace_id" => trace_id, "payload" => {}, "metadata" => {}
23
+ }
24
+ File.open(path, "a") { |f| f.puts(JSON.generate(data)) }
25
+ end
26
+
27
+ describe "#events_for" do
28
+ it "returns events_by_trace when trace_id given" do
29
+ write_event(trace_id: "abc")
30
+ result = controller.events_for(trace_id: "abc")
31
+ expect(result).to be_an(Array)
32
+ expect(result.first["trace_id"]).to eq("abc")
33
+ end
34
+
35
+ it "returns recent events when no trace_id" do
36
+ write_event
37
+ result = controller.events_for(trace_id: nil)
38
+ expect(result).to be_an(Array)
39
+ expect(result.size).to eq(1)
40
+ end
41
+ end
42
+
43
+ describe "#recent_events" do
44
+ it "returns limited recent events" do
45
+ 3.times { write_event }
46
+ result = controller.recent_events(limit: 2)
47
+ expect(result.size).to eq(2)
48
+ end
49
+ end
50
+
51
+ describe "#clear_log!" do
52
+ it "removes the log file" do
53
+ write_event
54
+ controller.clear_log!
55
+ expect(File.exist?(path)).to be false
56
+ end
57
+ end
58
+
59
+ describe "#v1_interactions" do
60
+ it "returns hashes with trace_ids and traces_count" do
61
+ write_event(name: "a", trace_id: "t1", severity: "info")
62
+ write_event(name: "b", trace_id: "t2", severity: "error")
63
+ rows = controller.v1_interactions(source: nil, limit: 10, window_ms: 500)
64
+ expect(rows).to be_an(Array)
65
+ expect(rows.first).to include("started_at", "trace_ids", "has_error", "source", "traces_count")
66
+ expect(rows.first["trace_ids"]).to be_an(Array)
67
+ expect(rows.first["traces_count"]).to eq(rows.first["trace_ids"].size)
68
+ end
69
+ end
70
+
71
+ describe "#v1_trace_events" do
72
+ it "returns events for trace in order" do
73
+ write_event(name: "first", trace_id: "tx")
74
+ write_event(name: "second", trace_id: "tx")
75
+ rows = controller.v1_trace_events("tx")
76
+ expect(rows.map { |e| e["event_name"] }).to eq(%w[first second])
77
+ end
78
+
79
+ it "returns empty array for blank trace id" do
80
+ expect(controller.v1_trace_events("")).to eq([])
81
+ end
82
+ end
83
+
84
+ describe "#v1_recent_events" do
85
+ it "respects limit clamp" do
86
+ 5.times { write_event }
87
+ rows = controller.v1_recent_events(limit: 2)
88
+ expect(rows.size).to eq(2)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "rack/test"
5
+ require "rack/mock"
6
+ require "e11y/devtools/overlay/middleware"
7
+
8
+ RSpec.describe E11y::Devtools::Overlay::Middleware do
9
+ include Rack::Test::Methods
10
+
11
+ let(:html_body) { "<html><body><h1>Hello</h1></body></html>" }
12
+ let(:base_app) do
13
+ ->(_env) { [200, { "Content-Type" => "text/html" }, [html_body]] }
14
+ end
15
+
16
+ def app = described_class.new(base_app)
17
+
18
+ it "injects overlay script before </body>" do
19
+ get "/"
20
+ expect(last_response.body).to include("e11y-overlay")
21
+ expect(last_response.body).to include("</body>")
22
+ end
23
+
24
+ it "does not inject into non-HTML responses" do
25
+ json_app = ->(_env) { [200, { "Content-Type" => "application/json" }, ['{"ok":true}']] }
26
+ response = described_class.new(json_app).call(Rack::MockRequest.env_for("/"))
27
+ body = response[2].join
28
+ expect(body).not_to include("e11y-overlay")
29
+ end
30
+
31
+ it "does not inject into XHR requests" do
32
+ get "/", {}, { "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" }
33
+ expect(last_response.body).not_to include("e11y-overlay")
34
+ end
35
+
36
+ it "does not inject into asset paths" do
37
+ get "/assets/application.js"
38
+ expect(last_response.body).not_to include("e11y-overlay")
39
+ end
40
+
41
+ it "preserves Content-Length consistency after injection" do
42
+ get "/"
43
+ content_length = last_response.headers["Content-Length"]
44
+ expect(content_length.to_i).to eq(last_response.body.bytesize) if content_length
45
+ end
46
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+ require "e11y/adapters/dev_log/query"
6
+ require "e11y/devtools/tui/grouping"
7
+ require "e11y/devtools/tui/app"
8
+
9
+ RSpec.describe E11y::Devtools::Tui::App do
10
+ subject(:app) { described_class.new(log_path: "/dev/null") }
11
+
12
+ describe "#initialize" do
13
+ it "starts in :interactions view" do
14
+ expect(app.current_view).to eq(:interactions)
15
+ end
16
+
17
+ it "starts with source_filter :web" do
18
+ expect(app.source_filter).to eq(:web)
19
+ end
20
+ end
21
+
22
+ describe "#handle_key" do
23
+ context "when in :interactions view" do
24
+ it "drills into :events on Enter" do
25
+ query = instance_double(E11y::Adapters::DevLog::Query, events_by_trace: [])
26
+ interaction = E11y::Devtools::Tui::Grouping::Interaction.new(
27
+ Time.now, ["t1"], false, "web"
28
+ )
29
+ app.instance_variable_set(:@query, query)
30
+ app.instance_variable_set(:@interactions, [interaction])
31
+ app.instance_variable_set(:@selected_ix, 0)
32
+ app.handle_key("enter")
33
+ expect(app.current_view).to eq(:events)
34
+ end
35
+
36
+ it "toggles source to :job on 'j'" do
37
+ app.handle_key("j")
38
+ expect(app.source_filter).to eq(:job)
39
+ end
40
+
41
+ it "toggles source to :all on 'a'" do
42
+ app.handle_key("a")
43
+ expect(app.source_filter).to eq(:all)
44
+ end
45
+
46
+ it "toggles source back to :web on 'w'" do
47
+ app.handle_key("a")
48
+ app.handle_key("w")
49
+ expect(app.source_filter).to eq(:web)
50
+ end
51
+ end
52
+
53
+ context "when in :events view" do
54
+ before do
55
+ app.instance_variable_set(:@current_view, :events)
56
+ app.instance_variable_set(:@current_trace_id, "t1")
57
+ app.instance_variable_set(:@events, [{ "id" => "e1", "event_name" => "x" }])
58
+ end
59
+
60
+ it "goes back to :interactions on Esc" do
61
+ app.handle_key("esc")
62
+ expect(app.current_view).to eq(:interactions)
63
+ end
64
+
65
+ it "drills into :detail on Enter" do
66
+ app.handle_key("enter")
67
+ expect(app.current_view).to eq(:detail)
68
+ end
69
+ end
70
+
71
+ context "when in :detail view" do
72
+ before { app.instance_variable_set(:@current_view, :detail) }
73
+
74
+ it "goes back to :events on Esc" do
75
+ app.handle_key("esc")
76
+ expect(app.current_view).to eq(:events)
77
+ end
78
+
79
+ it "goes back to :events on 'b'" do
80
+ app.handle_key("b")
81
+ expect(app.current_view).to eq(:events)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "time"
5
+ require "e11y/devtools/tui/grouping"
6
+
7
+ RSpec.describe E11y::Devtools::Tui::Grouping do
8
+ def make_trace(id, offset_ms, severity: "info", source: "web")
9
+ {
10
+ trace_id: id,
11
+ started_at: Time.now + (offset_ms / 1000.0),
12
+ severity: severity,
13
+ source: source
14
+ }
15
+ end
16
+
17
+ describe ".group" do
18
+ it "returns empty array for empty input" do
19
+ expect(described_class.group([])).to eq([])
20
+ end
21
+
22
+ it "places single trace in one interaction" do
23
+ groups = described_class.group([make_trace("t1", 0)])
24
+ expect(groups.size).to eq(1)
25
+ expect(groups.first.trace_ids).to eq(["t1"])
26
+ end
27
+
28
+ it "groups traces within window into one interaction" do
29
+ traces = [
30
+ make_trace("t1", 0),
31
+ make_trace("t2", 300), # 300ms after t1 — within 500ms window
32
+ make_trace("t3", 1200) # 1200ms after t1 — outside window
33
+ ]
34
+ groups = described_class.group(traces, window_ms: 500)
35
+ expect(groups.size).to eq(2)
36
+ # newest-first: groups[0] = t3 group, groups[1] = t1+t2 group
37
+ expect(groups.last.trace_ids.sort).to eq(%w[t1 t2].sort)
38
+ expect(groups.first.trace_ids).to eq(["t3"])
39
+ end
40
+
41
+ it "marks interaction has_error? when any trace has error severity" do
42
+ traces = [
43
+ make_trace("t1", 0, severity: "error"),
44
+ make_trace("t2", 100, severity: "info")
45
+ ]
46
+ groups = described_class.group(traces, window_ms: 500)
47
+ expect(groups.first.has_error?).to be true
48
+ end
49
+
50
+ it "marks interaction clean when no errors" do
51
+ groups = described_class.group([make_trace("t1", 0, severity: "info")])
52
+ expect(groups.first.has_error?).to be false
53
+ end
54
+
55
+ it "returns groups newest-first" do
56
+ traces = [
57
+ make_trace("old", 0),
58
+ make_trace("new", 2000)
59
+ ]
60
+ groups = described_class.group(traces, window_ms: 500)
61
+ expect(groups.first.trace_ids).to eq(["new"])
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "e11y/devtools/version"
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "time"
5
+
6
+ unless defined?(RATATUI_AVAILABLE)
7
+ begin
8
+ require "minitest"
9
+ require "ratatui_ruby"
10
+ require "ratatui_ruby/test_helper"
11
+ RATATUI_AVAILABLE = true
12
+ rescue LoadError
13
+ RATATUI_AVAILABLE = false
14
+ end
15
+ end
16
+
17
+ RSpec.describe "E11y::Devtools::Tui::Widgets::EventList" do
18
+ include RatatuiRuby::TestHelper if RATATUI_AVAILABLE
19
+
20
+ before do
21
+ skip "ratatui_ruby not available" unless RATATUI_AVAILABLE
22
+ require "e11y/devtools/tui/widgets/event_list"
23
+ end
24
+
25
+ let(:events) do
26
+ [
27
+ { "severity" => "error", "event_name" => "order.failed",
28
+ "timestamp" => Time.now.iso8601, "metadata" => {} },
29
+ { "severity" => "info", "event_name" => "order.created",
30
+ "timestamp" => Time.now.iso8601, "metadata" => { "duration_ms" => 42 } }
31
+ ]
32
+ end
33
+
34
+ it "renders without raising" do
35
+ widget = E11y::Devtools::Tui::Widgets::EventList.new(
36
+ events: events, trace_id: "trace-1", selected_index: 0
37
+ )
38
+ tui = RatatuiRuby::TUI.new
39
+ with_test_terminal(80, 10) do
40
+ expect { RatatuiRuby.draw { |frame| widget.render(tui, frame, frame.area) } }
41
+ .not_to raise_error
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "time"
5
+ require "e11y/devtools/tui/grouping"
6
+
7
+ unless defined?(RATATUI_AVAILABLE)
8
+ begin
9
+ require "minitest"
10
+ require "ratatui_ruby"
11
+ require "ratatui_ruby/test_helper"
12
+ RATATUI_AVAILABLE = true
13
+ rescue LoadError
14
+ RATATUI_AVAILABLE = false
15
+ end
16
+ end
17
+
18
+ RSpec.describe "E11y::Devtools::Tui::Widgets::InteractionList" do
19
+ include RatatuiRuby::TestHelper if RATATUI_AVAILABLE
20
+
21
+ before do
22
+ skip "ratatui_ruby not available" unless RATATUI_AVAILABLE
23
+ require "e11y/devtools/tui/widgets/interaction_list"
24
+ end
25
+
26
+ let(:t0) { Time.now }
27
+
28
+ def make_interaction(trace_ids:, has_error: false)
29
+ E11y::Devtools::Tui::Grouping::Interaction.new(
30
+ started_at: t0,
31
+ trace_ids: trace_ids,
32
+ has_error?: has_error,
33
+ source: "web"
34
+ )
35
+ end
36
+
37
+ it "renders bullet as ● red when interaction has error" do
38
+ widget = E11y::Devtools::Tui::Widgets::InteractionList.new(
39
+ interactions: [make_interaction(trace_ids: ["t1"], has_error: true)],
40
+ selected_index: 0
41
+ )
42
+ tui = RatatuiRuby::TUI.new
43
+ with_test_terminal(40, 5) do
44
+ expect { RatatuiRuby.draw { |frame| widget.render(tui, frame, frame.area) } }
45
+ .not_to raise_error
46
+ expect(buffer_content.join).to include("●")
47
+ end
48
+ end
49
+
50
+ it "renders bullet as ○ when interaction is clean" do
51
+ widget = E11y::Devtools::Tui::Widgets::InteractionList.new(
52
+ interactions: [make_interaction(trace_ids: ["t1"], has_error: false)],
53
+ selected_index: 0
54
+ )
55
+ tui = RatatuiRuby::TUI.new
56
+ with_test_terminal(40, 5) do
57
+ expect { RatatuiRuby.draw { |frame| widget.render(tui, frame, frame.area) } }
58
+ .not_to raise_error
59
+ expect(buffer_content.join).to include("○")
60
+ end
61
+ end
62
+ end