e11y 0.2.0 → 1.0.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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +130 -10
  3. data/CHANGELOG.md +56 -1
  4. data/CLAUDE.md +168 -0
  5. data/CONTRIBUTING.md +640 -0
  6. data/README.md +134 -702
  7. data/RELEASE.md +18 -3
  8. data/Rakefile +108 -29
  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 +29 -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} +35 -64
  30. data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
  31. data/docs/{ADR-003-slo-observability.md → architecture/ADR-003-slo-observability.md} +27 -466
  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} +209 -339
  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} +41 -83
  40. data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
  41. data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
  42. data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +23 -41
  43. data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +52 -349
  44. data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
  45. data/docs/architecture/ADR-018-memory-optimization.md +366 -0
  46. data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
  47. data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
  48. data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
  49. data/docs/prd/01-overview-vision.md +19 -14
  50. data/docs/use_cases/README.md +22 -23
  51. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
  52. data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
  53. data/docs/use_cases/UC-003-event-metrics.md +66 -0
  54. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +42 -101
  55. data/docs/use_cases/UC-005-sentry-integration.md +13 -15
  56. data/docs/use_cases/UC-006-trace-context-management.md +30 -28
  57. data/docs/use_cases/UC-007-pii-filtering.md +35 -87
  58. data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
  59. data/docs/use_cases/UC-009-multi-service-tracing.md +4 -4
  60. data/docs/use_cases/UC-010-background-job-tracking.md +5 -5
  61. data/docs/use_cases/UC-011-rate-limiting.md +95 -168
  62. data/docs/use_cases/UC-012-audit-trail.md +21 -46
  63. data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
  64. data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
  65. data/docs/use_cases/UC-015-cost-optimization.md +46 -99
  66. data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
  67. data/docs/use_cases/UC-017-local-development.md +203 -777
  68. data/docs/use_cases/UC-018-testing-events.md +3 -3
  69. data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
  70. data/docs/use_cases/UC-020-event-versioning.md +8 -9
  71. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
  72. data/docs/use_cases/UC-022-event-registry.md +15 -21
  73. data/docs/use_cases/backlog.md +119 -87
  74. data/e11y.gemspec +2 -2
  75. data/gems/e11y-devtools/README.md +136 -0
  76. data/gems/e11y-devtools/config/routes.rb +8 -0
  77. data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
  78. data/gems/e11y-devtools/exe/e11y +34 -0
  79. data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
  80. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
  81. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
  82. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
  83. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
  84. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
  85. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
  86. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
  87. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
  88. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
  89. data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +115 -0
  90. data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +54 -0
  91. data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
  92. data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
  93. data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +42 -0
  94. data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
  95. data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
  96. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
  97. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
  98. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
  99. data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
  100. data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
  101. data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
  102. data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +58 -0
  103. data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
  104. data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
  105. data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
  106. data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
  107. data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
  108. data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
  109. data/lib/e11y/adapters/audit_encrypted.rb +53 -11
  110. data/lib/e11y/adapters/base.rb +33 -34
  111. data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
  112. data/lib/e11y/adapters/dev_log/query.rb +219 -0
  113. data/lib/e11y/adapters/dev_log.rb +118 -0
  114. data/lib/e11y/adapters/file.rb +3 -6
  115. data/lib/e11y/adapters/in_memory.rb +52 -5
  116. data/lib/e11y/adapters/in_memory_test.rb +29 -0
  117. data/lib/e11y/adapters/loki.rb +58 -23
  118. data/lib/e11y/adapters/null.rb +82 -0
  119. data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
  120. data/lib/e11y/adapters/otel_logs.rb +136 -23
  121. data/lib/e11y/adapters/sentry.rb +4 -7
  122. data/lib/e11y/adapters/stdout.rb +73 -7
  123. data/lib/e11y/adapters/yabeda.rb +153 -29
  124. data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
  125. data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
  126. data/lib/e11y/buffers/ring_buffer.rb +3 -16
  127. data/lib/e11y/configuration.rb +272 -0
  128. data/lib/e11y/console.rb +10 -17
  129. data/lib/e11y/current.rb +53 -1
  130. data/lib/e11y/debug/pipeline_inspector.rb +96 -0
  131. data/lib/e11y/documentation/generator.rb +48 -0
  132. data/lib/e11y/event/base.rb +176 -82
  133. data/lib/e11y/event/value_sampling_config.rb +1 -5
  134. data/lib/e11y/events/rails/database/query.rb +1 -4
  135. data/lib/e11y/events/rails/job/failed.rb +2 -0
  136. data/lib/e11y/instruments/active_job.rb +46 -12
  137. data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
  138. data/lib/e11y/instruments/sidekiq.rb +137 -31
  139. data/lib/e11y/linters/base.rb +11 -0
  140. data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
  141. data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
  142. data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
  143. data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
  144. data/lib/e11y/logger/bridge.rb +26 -7
  145. data/lib/e11y/metrics/cardinality_protection.rb +10 -15
  146. data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
  147. data/lib/e11y/metrics/registry.rb +3 -5
  148. data/lib/e11y/metrics/test_backend.rb +62 -0
  149. data/lib/e11y/metrics.rb +56 -10
  150. data/lib/e11y/middleware/adapter_resolver.rb +40 -0
  151. data/lib/e11y/middleware/audit_signing.rb +43 -6
  152. data/lib/e11y/middleware/baggage_protection.rb +75 -0
  153. data/lib/e11y/middleware/dev_log_source.rb +24 -0
  154. data/lib/e11y/middleware/event_slo.rb +23 -9
  155. data/lib/e11y/middleware/otel_span.rb +23 -0
  156. data/lib/e11y/middleware/pii_filter.rb +104 -75
  157. data/lib/e11y/middleware/rate_limiting.rb +54 -27
  158. data/lib/e11y/middleware/request.rb +70 -23
  159. data/lib/e11y/middleware/routing.rb +78 -21
  160. data/lib/e11y/middleware/sampling.rb +66 -17
  161. data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
  162. data/lib/e11y/middleware/trace_context.rb +45 -10
  163. data/lib/e11y/middleware/track_latency.rb +34 -0
  164. data/lib/e11y/middleware/validation.rb +7 -16
  165. data/lib/e11y/middleware/versioning.rb +26 -22
  166. data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
  167. data/lib/e11y/opentelemetry/span_creator.rb +142 -0
  168. data/lib/e11y/pii/patterns.rb +12 -1
  169. data/lib/e11y/pipeline/builder.rb +1 -1
  170. data/lib/e11y/presets/audit_event.rb +13 -2
  171. data/lib/e11y/railtie.rb +52 -15
  172. data/lib/e11y/registry.rb +306 -0
  173. data/lib/e11y/reliability/circuit_breaker.rb +19 -21
  174. data/lib/e11y/reliability/dlq/base.rb +71 -0
  175. data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
  176. data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
  177. data/lib/e11y/reliability/dlq/filter.rb +37 -54
  178. data/lib/e11y/reliability/retry_handler.rb +26 -29
  179. data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
  180. data/lib/e11y/sampling/error_spike_detector.rb +0 -2
  181. data/lib/e11y/sampling/load_monitor.rb +5 -9
  182. data/lib/e11y/sampling/stratified_tracker.rb +18 -0
  183. data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
  184. data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
  185. data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
  186. data/lib/e11y/slo/config_loader.rb +40 -0
  187. data/lib/e11y/slo/config_validator.rb +58 -0
  188. data/lib/e11y/slo/dashboard_generator.rb +122 -0
  189. data/lib/e11y/slo/event_driven.rb +8 -0
  190. data/lib/e11y/slo/tracker.rb +31 -4
  191. data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
  192. data/lib/e11y/testing/rspec_matchers.rb +21 -0
  193. data/lib/e11y/testing/snapshot_matcher.rb +86 -0
  194. data/lib/e11y/trace_context/sampler.rb +35 -0
  195. data/lib/e11y/tracing/faraday_middleware.rb +31 -0
  196. data/lib/e11y/tracing/net_http_patch.rb +33 -0
  197. data/lib/e11y/tracing/propagator.rb +116 -0
  198. data/lib/e11y/tracing.rb +47 -0
  199. data/lib/e11y/version.rb +1 -1
  200. data/lib/e11y/versioning/version_extractor.rb +32 -0
  201. data/lib/e11y.rb +141 -265
  202. data/lib/generators/e11y/event/event_generator.rb +22 -0
  203. data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
  204. data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
  205. data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
  206. data/lib/generators/e11y/install/install_generator.rb +34 -0
  207. data/lib/generators/e11y/install/templates/e11y.rb +239 -0
  208. data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
  209. data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
  210. data/lib/tasks/e11y_docs.rake +30 -0
  211. data/lib/tasks/e11y_events.rake +71 -0
  212. data/lib/tasks/e11y_lint.rake +91 -0
  213. data/lib/tasks/e11y_slo.rake +29 -0
  214. metadata +129 -39
  215. data/docs/ADR-010-developer-experience.md +0 -2166
  216. data/docs/API-REFERENCE-L28.md +0 -914
  217. data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
  218. data/docs/CONTRIBUTING.md +0 -312
  219. data/docs/IMPLEMENTATION_NOTES.md +0 -2804
  220. data/docs/IMPLEMENTATION_PLAN.md +0 -1971
  221. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
  222. data/docs/PLAN.md +0 -148
  223. data/docs/README.md +0 -296
  224. data/docs/design/00-memory-optimization.md +0 -593
  225. data/docs/guides/MIGRATION-L27-L28.md +0 -692
  226. data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
  227. data/docs/guides/README.md +0 -44
  228. data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
  229. data/lib/e11y/adapters/registry.rb +0 -141
  230. /data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +0 -0
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "mcp"
5
+ rescue LoadError
6
+ # mcp gem not available — ToolBase will use plain class
7
+ end
8
+ require_relative "../tool_base"
9
+
10
+ module E11y
11
+ module Devtools
12
+ module Mcp
13
+ module Tools
14
+ # Returns all events for a specific trace ID in chronological order.
15
+ class EventsByTrace < ToolBase
16
+ description "Get all events for a specific trace ID in chronological order"
17
+
18
+ input_schema(
19
+ type: :object,
20
+ required: ["trace_id"],
21
+ properties: {
22
+ trace_id: { type: :string, description: "Trace ID" }
23
+ }
24
+ )
25
+
26
+ def self.call(trace_id:, server_context:)
27
+ server_context[:store].events_by_trace(trace_id)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "mcp"
5
+ rescue LoadError
6
+ # mcp gem not available — ToolBase will use plain class
7
+ end
8
+ require_relative "../tool_base"
9
+
10
+ module E11y
11
+ module Devtools
12
+ module Mcp
13
+ module Tools
14
+ # Returns time-grouped interactions (parallel requests from one user action).
15
+ class Interactions < ToolBase
16
+ description "Get time-grouped interactions (parallel requests from one user action)"
17
+
18
+ input_schema(
19
+ type: :object,
20
+ properties: {
21
+ limit: { type: :integer, description: "Max interactions", default: 20 },
22
+ window_ms: { type: :integer, description: "Grouping window in ms", default: 500 }
23
+ }
24
+ )
25
+
26
+ def self.call(server_context:, limit: 20, window_ms: 500)
27
+ server_context[:store].interactions(limit: limit, window_ms: window_ms).map do |ix|
28
+ {
29
+ started_at: ix.started_at.iso8601(3),
30
+ trace_ids: ix.trace_ids,
31
+ has_error: ix.has_error?,
32
+ traces_count: ix.traces_count
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "mcp"
5
+ rescue LoadError
6
+ # mcp gem not available — ToolBase will use plain class
7
+ end
8
+ require_relative "../tool_base"
9
+
10
+ module E11y
11
+ module Devtools
12
+ module Mcp
13
+ module Tools
14
+ # Returns the most recent events from the dev log.
15
+ class RecentEvents < ToolBase
16
+ description "Get recent E11y events from the development log"
17
+
18
+ input_schema(
19
+ type: :object,
20
+ properties: {
21
+ limit: { type: :integer, description: "Max events to return (default 50)", default: 50 },
22
+ severity: { type: :string, description: "Filter by severity",
23
+ enum: %w[debug info warn error fatal] }
24
+ }
25
+ )
26
+
27
+ def self.call(server_context:, limit: 50, severity: nil)
28
+ server_context[:store].stored_events(limit: limit, severity: severity)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "mcp"
5
+ rescue LoadError
6
+ # mcp gem not available — ToolBase will use plain class
7
+ end
8
+ require_relative "../tool_base"
9
+
10
+ module E11y
11
+ module Devtools
12
+ module Mcp
13
+ module Tools
14
+ # Full-text search across event names and payload content.
15
+ class Search < ToolBase
16
+ description "Full-text search across event names and payload content"
17
+
18
+ input_schema(
19
+ type: :object,
20
+ required: ["query"],
21
+ properties: {
22
+ query: { type: :string, description: "Search term" },
23
+ limit: { type: :integer, description: "Max results", default: 50 }
24
+ }
25
+ )
26
+
27
+ def self.call(query:, server_context:, limit: 50)
28
+ server_context[:store].search(query, limit: limit)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "mcp"
5
+ rescue LoadError
6
+ # mcp gem not available — ToolBase will use plain class
7
+ end
8
+ require_relative "../tool_base"
9
+
10
+ module E11y
11
+ module Devtools
12
+ module Mcp
13
+ module Tools
14
+ # Returns aggregate statistics about the E11y development log.
15
+ class Stats < ToolBase
16
+ description "Get aggregate statistics about the E11y development log"
17
+
18
+ input_schema(
19
+ type: :object,
20
+ properties: {}
21
+ )
22
+
23
+ def self.call(server_context:, **_opts)
24
+ server_context[:store].stats
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,115 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ const POLL_INTERVAL = 2000;
5
+ const API_BASE = '/_e11y';
6
+
7
+ class E11yOverlay extends HTMLElement {
8
+ connectedCallback() {
9
+ const shadow = this.attachShadow({ mode: 'open' });
10
+ shadow.innerHTML = `
11
+ <style>
12
+ :host { position: fixed; bottom: 16px; right: 16px; z-index: 99999; font-family: monospace; }
13
+ .badge { background: #1a1a2e; color: #e0e0e0; border-radius: 6px; padding: 6px 12px;
14
+ cursor: pointer; font-size: 12px; border: 1px solid #333; }
15
+ .badge.has-error { border-color: #e53e3e; color: #fc8181; }
16
+ .panel { display: none; position: fixed; right: 16px; bottom: 60px; width: 420px;
17
+ max-height: 70vh; background: #1a1a2e; border: 1px solid #444;
18
+ border-radius: 8px; overflow: hidden; flex-direction: column; }
19
+ .panel.open { display: flex; }
20
+ .panel-header { padding: 10px 14px; background: #16213e; border-bottom: 1px solid #333;
21
+ display: flex; justify-content: space-between; align-items: center;
22
+ font-size: 12px; color: #a0aec0; }
23
+ .panel-title { color: #e0e0e0; font-weight: bold; }
24
+ .close-btn { cursor: pointer; color: #718096; }
25
+ .events { overflow-y: auto; flex: 1; padding: 8px; }
26
+ .event-row { padding: 4px 8px; border-radius: 4px; margin-bottom: 2px;
27
+ font-size: 11px; cursor: pointer; display: flex; gap: 8px; }
28
+ .event-row:hover { background: #2d3748; }
29
+ .sev-error { color: #fc8181; }
30
+ .sev-warn { color: #f6ad55; }
31
+ .sev-info { color: #68d391; }
32
+ .footer { padding: 8px 14px; border-top: 1px solid #333; display: flex;
33
+ gap: 12px; font-size: 11px; }
34
+ .footer a { color: #63b3ed; cursor: pointer; text-decoration: none; }
35
+ .footer a:hover { text-decoration: underline; }
36
+ </style>
37
+ <div class="badge" id="badge">e11y</div>
38
+ <div class="panel" id="panel">
39
+ <div class="panel-header">
40
+ <span class="panel-title" id="panel-title">e11y devtools</span>
41
+ <span class="close-btn" id="close-btn">x</span>
42
+ </div>
43
+ <div class="events" id="events-list"></div>
44
+ <div class="footer">
45
+ <a id="clear-btn">clear log</a>
46
+ <a id="copy-trace-btn">copy trace_id</a>
47
+ </div>
48
+ </div>
49
+ `;
50
+
51
+ this._shadow = shadow;
52
+ this._panelOpen = false;
53
+ this._traceId = window.__E11Y_TRACE_ID__ || null;
54
+ this._events = [];
55
+
56
+ shadow.getElementById('badge').addEventListener('click', () => this.togglePanel());
57
+ shadow.getElementById('close-btn').addEventListener('click', () => this.closePanel());
58
+ shadow.getElementById('clear-btn').addEventListener('click', () => this.clearLog());
59
+ shadow.getElementById('copy-trace-btn').addEventListener('click', () => this.copyTrace());
60
+
61
+ this.loadEvents();
62
+ this._pollTimer = setInterval(() => this.loadEvents(), POLL_INTERVAL);
63
+ }
64
+
65
+ disconnectedCallback() { clearInterval(this._pollTimer); }
66
+
67
+ togglePanel() { this._panelOpen ? this.closePanel() : this.openPanel(); }
68
+ openPanel() { this._panelOpen = true; this._shadow.getElementById('panel').classList.add('open'); }
69
+ closePanel() { this._panelOpen = false; this._shadow.getElementById('panel').classList.remove('open'); }
70
+
71
+ loadEvents() {
72
+ const url = this._traceId
73
+ ? `${API_BASE}/events?trace_id=${encodeURIComponent(this._traceId)}`
74
+ : `${API_BASE}/events/recent?limit=20`;
75
+ fetch(url)
76
+ .then(r => r.json())
77
+ .then(events => { this._events = events; this.renderBadge(); this.renderEvents(); })
78
+ .catch(() => {});
79
+ }
80
+
81
+ renderBadge() {
82
+ const badge = this._shadow.getElementById('badge');
83
+ const errCount = this._events.filter(e => e.severity === 'error' || e.severity === 'fatal').length;
84
+ badge.textContent = errCount > 0
85
+ ? `e11y ${this._events.length} * ${errCount}`
86
+ : `e11y ${this._events.length}`;
87
+ badge.className = errCount > 0 ? 'badge has-error' : 'badge';
88
+ }
89
+
90
+ renderEvents() {
91
+ const list = this._shadow.getElementById('events-list');
92
+ list.innerHTML = this._events.map(e => `
93
+ <div class="event-row">
94
+ <span class="sev-${e.severity}">${(e.severity || 'info').toUpperCase().slice(0,4)}</span>
95
+ <span>${e.event_name}</span>
96
+ <span style="color:#718096;margin-left:auto">${(e.metadata && e.metadata.duration_ms) || ''}ms</span>
97
+ </div>`).join('');
98
+ }
99
+
100
+ clearLog() {
101
+ fetch(`${API_BASE}/events`, { method: 'DELETE' })
102
+ .then(() => { this._events = []; this.renderBadge(); this.renderEvents(); });
103
+ }
104
+
105
+ copyTrace() {
106
+ if (this._traceId) { navigator.clipboard && navigator.clipboard.writeText(this._traceId); }
107
+ }
108
+ }
109
+
110
+ customElements.define('e11y-overlay', E11yOverlay);
111
+
112
+ if (!document.querySelector('e11y-overlay')) {
113
+ document.body.appendChild(document.createElement('e11y-overlay'));
114
+ }
115
+ })();
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/adapters/dev_log/query"
4
+ require "e11y/adapters/dev_log"
5
+
6
+ module E11y
7
+ module Devtools
8
+ module Overlay
9
+ # Plain Ruby controller logic — testable without Rails.
10
+ # Used by the Rails route handlers (see config/routes.rb).
11
+ class Controller
12
+ def initialize(query = nil)
13
+ @query = query || resolve_query
14
+ end
15
+
16
+ def events_for(trace_id: nil, limit: 50)
17
+ if trace_id && !trace_id.empty?
18
+ @query.events_by_trace(trace_id)
19
+ else
20
+ @query.stored_events(limit: limit)
21
+ end
22
+ end
23
+
24
+ def recent_events(limit: 50)
25
+ clamped = limit.to_i.clamp(1, 500)
26
+ @query.stored_events(limit: clamped)
27
+ end
28
+
29
+ def clear_log!
30
+ @query.clear!
31
+ end
32
+
33
+ def stats
34
+ @query.stats
35
+ end
36
+
37
+ private
38
+
39
+ def resolve_query
40
+ if defined?(E11y) && E11y.respond_to?(:configuration)
41
+ adapter = E11y.configuration.adapters[:dev_log]
42
+ return adapter if adapter.respond_to?(:stored_events)
43
+ end
44
+ default_path = if defined?(Rails) && Rails.respond_to?(:root)
45
+ Rails.root.join("log", "e11y_dev.jsonl").to_s
46
+ else
47
+ "log/e11y_dev.jsonl"
48
+ end
49
+ E11y::Adapters::DevLog::Query.new(default_path)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module E11y
6
+ module Devtools
7
+ module Overlay
8
+ # Rails Engine that mounts JSON endpoints at /_e11y/
9
+ # and injects the overlay badge via Rack middleware.
10
+ class Engine < Rails::Engine
11
+ isolate_namespace E11y::Devtools::Overlay
12
+
13
+ initializer "e11y_devtools.overlay.middleware" do |app|
14
+ next unless Rails.env.development? || Rails.env.test?
15
+
16
+ require "e11y/devtools/overlay/middleware"
17
+ app.middleware.use E11y::Devtools::Overlay::Middleware
18
+ end
19
+
20
+ config.generators do |g|
21
+ g.test_framework :rspec
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Devtools
5
+ module Overlay
6
+ # Rack middleware that injects the e11y overlay badge into HTML responses.
7
+ #
8
+ # Skips injection for:
9
+ # - XHR requests (X-Requested-With: XMLHttpRequest)
10
+ # - Asset paths (/assets/, /packs/, /_e11y/)
11
+ # - Non-HTML responses
12
+ class Middleware
13
+ OVERLAY_SNIPPET = <<~HTML
14
+
15
+ <!-- e11y-overlay -->
16
+ <script id="e11y-overlay-loader">
17
+ (function() {
18
+ var s = document.createElement('script');
19
+ s.src = '/_e11y/overlay.js';
20
+ s.defer = true;
21
+ document.head.appendChild(s);
22
+ })();
23
+ </script>
24
+ HTML
25
+
26
+ def initialize(app)
27
+ @app = app
28
+ end
29
+
30
+ def call(env)
31
+ status, headers, body = @app.call(env)
32
+ return [status, headers, body] unless injectable?(env, headers)
33
+
34
+ new_body = inject_overlay(body, env["e11y.trace_id"])
35
+ [status, update_content_length(headers, new_body), [new_body]]
36
+ end
37
+
38
+ private
39
+
40
+ def injectable?(env, headers)
41
+ !xhr?(env) && !asset_path?(env) && html_response?(headers)
42
+ end
43
+
44
+ def xhr?(env)
45
+ env["HTTP_X_REQUESTED_WITH"]&.downcase == "xmlhttprequest"
46
+ end
47
+
48
+ def asset_path?(env)
49
+ path = env["PATH_INFO"] || ""
50
+ path.start_with?("/assets/", "/packs/", "/_e11y/")
51
+ end
52
+
53
+ def html_response?(headers)
54
+ ct = headers["Content-Type"] || headers["content-type"] || ""
55
+ ct.include?("text/html")
56
+ end
57
+
58
+ def inject_overlay(body, trace_id)
59
+ full = body.respond_to?(:join) ? body.join : body.to_s
60
+ snippet = trace_id_script(trace_id) + OVERLAY_SNIPPET
61
+ full.sub("</body>", "#{snippet}</body>")
62
+ end
63
+
64
+ def trace_id_script(trace_id)
65
+ return "" unless trace_id
66
+
67
+ "<script>window.__E11Y_TRACE_ID__ = '#{trace_id}';</script>\n"
68
+ end
69
+
70
+ def update_content_length(headers, new_body)
71
+ h = headers.dup
72
+ h.delete("Content-Length")
73
+ h.delete("content-length")
74
+ h["Content-Length"] = new_body.bytesize.to_s
75
+ h
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "controller"
4
+
5
+ module E11y
6
+ module Devtools
7
+ module Overlay
8
+ # Thin Rails controller — delegates to plain Controller for testability.
9
+ # Only available in development/test.
10
+ class RailsController < ActionController::Base
11
+ before_action :development_only!
12
+
13
+ def events
14
+ render json: overlay_ctrl.events_for(trace_id: params[:trace_id])
15
+ end
16
+
17
+ def recent
18
+ render json: overlay_ctrl.recent_events(limit: params[:limit])
19
+ end
20
+
21
+ def clear
22
+ overlay_ctrl.clear_log!
23
+ head :no_content
24
+ end
25
+
26
+ def stats
27
+ render json: overlay_ctrl.stats
28
+ end
29
+
30
+ private
31
+
32
+ def overlay_ctrl
33
+ @overlay_ctrl ||= Controller.new
34
+ end
35
+
36
+ def development_only!
37
+ head :not_found unless Rails.env.development? || Rails.env.test?
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end