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.
- checksums.yaml +4 -4
- data/.rubocop.yml +130 -10
- data/CHANGELOG.md +56 -1
- data/CLAUDE.md +168 -0
- data/CONTRIBUTING.md +640 -0
- data/README.md +134 -702
- data/RELEASE.md +18 -3
- data/Rakefile +108 -29
- data/config/README.md +1 -1
- data/config/loki-local-config.yaml +12 -0
- data/config/otel-collector-config.yaml +44 -0
- data/cucumber.yml +1 -0
- data/docker-compose.yml +18 -2
- data/docs/ADAPTERS.md +76 -0
- data/docs/ADAPTIVE_SAMPLING.md +59 -0
- data/docs/COMPARISON.md +104 -0
- data/docs/CONFIGURATION.md +52 -0
- data/docs/DISTRIBUTED_TRACING.md +44 -0
- data/docs/LIMITATIONS.md +13 -0
- data/docs/METRICS_DSL.md +84 -0
- data/docs/PERFORMANCE.md +60 -0
- data/docs/PII_FILTERING.md +40 -0
- data/docs/PRESETS.md +65 -0
- data/docs/QUICK-START.md +546 -587
- data/docs/RAILS_INTEGRATION.md +29 -0
- data/docs/SCHEMA_VALIDATION.md +63 -0
- data/docs/SLO-PROMQL-ALERTS.md +161 -0
- data/docs/TESTING.md +69 -0
- data/docs/{ADR-001-architecture.md → architecture/ADR-001-architecture.md} +35 -64
- data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
- data/docs/{ADR-003-slo-observability.md → architecture/ADR-003-slo-observability.md} +27 -466
- data/docs/{ADR-004-adapter-architecture.md → architecture/ADR-004-adapter-architecture.md} +163 -146
- data/docs/{ADR-005-tracing-context.md → architecture/ADR-005-tracing-context.md} +10 -9
- data/docs/{ADR-006-security-compliance.md → architecture/ADR-006-security-compliance.md} +184 -191
- data/docs/{ADR-007-opentelemetry-integration.md → architecture/ADR-007-opentelemetry-integration.md} +3 -21
- data/docs/{ADR-008-rails-integration.md → architecture/ADR-008-rails-integration.md} +209 -339
- data/docs/{ADR-009-cost-optimization.md → architecture/ADR-009-cost-optimization.md} +45 -54
- data/docs/architecture/ADR-010-developer-experience.md +522 -0
- data/docs/{ADR-011-testing-strategy.md → architecture/ADR-011-testing-strategy.md} +41 -83
- data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
- data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
- data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +23 -41
- data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +52 -349
- data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
- data/docs/architecture/ADR-018-memory-optimization.md +366 -0
- data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
- data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
- data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
- data/docs/prd/01-overview-vision.md +19 -14
- data/docs/use_cases/README.md +22 -23
- data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
- data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
- data/docs/use_cases/UC-003-event-metrics.md +66 -0
- data/docs/use_cases/UC-004-zero-config-slo-tracking.md +42 -101
- data/docs/use_cases/UC-005-sentry-integration.md +13 -15
- data/docs/use_cases/UC-006-trace-context-management.md +30 -28
- data/docs/use_cases/UC-007-pii-filtering.md +35 -87
- data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
- data/docs/use_cases/UC-009-multi-service-tracing.md +4 -4
- data/docs/use_cases/UC-010-background-job-tracking.md +5 -5
- data/docs/use_cases/UC-011-rate-limiting.md +95 -168
- data/docs/use_cases/UC-012-audit-trail.md +21 -46
- data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
- data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
- data/docs/use_cases/UC-015-cost-optimization.md +46 -99
- data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
- data/docs/use_cases/UC-017-local-development.md +203 -777
- data/docs/use_cases/UC-018-testing-events.md +3 -3
- data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
- data/docs/use_cases/UC-020-event-versioning.md +8 -9
- data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
- data/docs/use_cases/UC-022-event-registry.md +15 -21
- data/docs/use_cases/backlog.md +119 -87
- data/e11y.gemspec +2 -2
- data/gems/e11y-devtools/README.md +136 -0
- data/gems/e11y-devtools/config/routes.rb +8 -0
- data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
- data/gems/e11y-devtools/exe/e11y +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +115 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +54 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +42 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
- data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
- data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
- data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
- data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +58 -0
- data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
- data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
- data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
- data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
- data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
- data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
- data/lib/e11y/adapters/audit_encrypted.rb +53 -11
- data/lib/e11y/adapters/base.rb +33 -34
- data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
- data/lib/e11y/adapters/dev_log/query.rb +219 -0
- data/lib/e11y/adapters/dev_log.rb +118 -0
- data/lib/e11y/adapters/file.rb +3 -6
- data/lib/e11y/adapters/in_memory.rb +52 -5
- data/lib/e11y/adapters/in_memory_test.rb +29 -0
- data/lib/e11y/adapters/loki.rb +58 -23
- data/lib/e11y/adapters/null.rb +82 -0
- data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
- data/lib/e11y/adapters/otel_logs.rb +136 -23
- data/lib/e11y/adapters/sentry.rb +4 -7
- data/lib/e11y/adapters/stdout.rb +73 -7
- data/lib/e11y/adapters/yabeda.rb +153 -29
- data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
- data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
- data/lib/e11y/buffers/ring_buffer.rb +3 -16
- data/lib/e11y/configuration.rb +272 -0
- data/lib/e11y/console.rb +10 -17
- data/lib/e11y/current.rb +53 -1
- data/lib/e11y/debug/pipeline_inspector.rb +96 -0
- data/lib/e11y/documentation/generator.rb +48 -0
- data/lib/e11y/event/base.rb +176 -82
- data/lib/e11y/event/value_sampling_config.rb +1 -5
- data/lib/e11y/events/rails/database/query.rb +1 -4
- data/lib/e11y/events/rails/job/failed.rb +2 -0
- data/lib/e11y/instruments/active_job.rb +46 -12
- data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
- data/lib/e11y/instruments/sidekiq.rb +137 -31
- data/lib/e11y/linters/base.rb +11 -0
- data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
- data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
- data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
- data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
- data/lib/e11y/logger/bridge.rb +26 -7
- data/lib/e11y/metrics/cardinality_protection.rb +10 -15
- data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
- data/lib/e11y/metrics/registry.rb +3 -5
- data/lib/e11y/metrics/test_backend.rb +62 -0
- data/lib/e11y/metrics.rb +56 -10
- data/lib/e11y/middleware/adapter_resolver.rb +40 -0
- data/lib/e11y/middleware/audit_signing.rb +43 -6
- data/lib/e11y/middleware/baggage_protection.rb +75 -0
- data/lib/e11y/middleware/dev_log_source.rb +24 -0
- data/lib/e11y/middleware/event_slo.rb +23 -9
- data/lib/e11y/middleware/otel_span.rb +23 -0
- data/lib/e11y/middleware/pii_filter.rb +104 -75
- data/lib/e11y/middleware/rate_limiting.rb +54 -27
- data/lib/e11y/middleware/request.rb +70 -23
- data/lib/e11y/middleware/routing.rb +78 -21
- data/lib/e11y/middleware/sampling.rb +66 -17
- data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
- data/lib/e11y/middleware/trace_context.rb +45 -10
- data/lib/e11y/middleware/track_latency.rb +34 -0
- data/lib/e11y/middleware/validation.rb +7 -16
- data/lib/e11y/middleware/versioning.rb +26 -22
- data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
- data/lib/e11y/opentelemetry/span_creator.rb +142 -0
- data/lib/e11y/pii/patterns.rb +12 -1
- data/lib/e11y/pipeline/builder.rb +1 -1
- data/lib/e11y/presets/audit_event.rb +13 -2
- data/lib/e11y/railtie.rb +52 -15
- data/lib/e11y/registry.rb +306 -0
- data/lib/e11y/reliability/circuit_breaker.rb +19 -21
- data/lib/e11y/reliability/dlq/base.rb +71 -0
- data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
- data/lib/e11y/reliability/dlq/filter.rb +37 -54
- data/lib/e11y/reliability/retry_handler.rb +26 -29
- data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
- data/lib/e11y/sampling/error_spike_detector.rb +0 -2
- data/lib/e11y/sampling/load_monitor.rb +5 -9
- data/lib/e11y/sampling/stratified_tracker.rb +18 -0
- data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
- data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
- data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
- data/lib/e11y/slo/config_loader.rb +40 -0
- data/lib/e11y/slo/config_validator.rb +58 -0
- data/lib/e11y/slo/dashboard_generator.rb +122 -0
- data/lib/e11y/slo/event_driven.rb +8 -0
- data/lib/e11y/slo/tracker.rb +31 -4
- data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
- data/lib/e11y/testing/rspec_matchers.rb +21 -0
- data/lib/e11y/testing/snapshot_matcher.rb +86 -0
- data/lib/e11y/trace_context/sampler.rb +35 -0
- data/lib/e11y/tracing/faraday_middleware.rb +31 -0
- data/lib/e11y/tracing/net_http_patch.rb +33 -0
- data/lib/e11y/tracing/propagator.rb +116 -0
- data/lib/e11y/tracing.rb +47 -0
- data/lib/e11y/version.rb +1 -1
- data/lib/e11y/versioning/version_extractor.rb +32 -0
- data/lib/e11y.rb +141 -265
- data/lib/generators/e11y/event/event_generator.rb +22 -0
- data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
- data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
- data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
- data/lib/generators/e11y/install/install_generator.rb +34 -0
- data/lib/generators/e11y/install/templates/e11y.rb +239 -0
- data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
- data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
- data/lib/tasks/e11y_docs.rake +30 -0
- data/lib/tasks/e11y_events.rake +71 -0
- data/lib/tasks/e11y_lint.rake +91 -0
- data/lib/tasks/e11y_slo.rake +29 -0
- metadata +129 -39
- data/docs/ADR-010-developer-experience.md +0 -2166
- data/docs/API-REFERENCE-L28.md +0 -914
- data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
- data/docs/CONTRIBUTING.md +0 -312
- data/docs/IMPLEMENTATION_NOTES.md +0 -2804
- data/docs/IMPLEMENTATION_PLAN.md +0 -1971
- data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
- data/docs/PLAN.md +0 -148
- data/docs/README.md +0 -296
- data/docs/design/00-memory-optimization.md +0 -593
- data/docs/guides/MIGRATION-L27-L28.md +0 -692
- data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
- data/docs/guides/README.md +0 -44
- data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
- data/lib/e11y/adapters/registry.rb +0 -141
- /data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +0 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# ADR-010: Developer Experience
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** March 18, 2026
|
|
5
|
+
**Covers:** UC-017 (Local Development)
|
|
6
|
+
**Depends On:** ADR-001 (Core), ADR-004 (Adapter Architecture), ADR-008 (Rails Integration)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
1. [Context & Problem](#1-context--problem)
|
|
13
|
+
2. [Architecture: Hub-and-Spoke](#2-architecture-hub-and-spoke)
|
|
14
|
+
3. [DevLog Adapter](#3-devlog-adapter)
|
|
15
|
+
4. [TUI — Interactive Log Viewer](#4-tui--interactive-log-viewer)
|
|
16
|
+
5. [Browser Overlay](#5-browser-overlay)
|
|
17
|
+
6. [MCP Server](#6-mcp-server)
|
|
18
|
+
7. [CLI Entry Point](#7-cli-entry-point)
|
|
19
|
+
8. [Monorepo Structure](#8-monorepo-structure)
|
|
20
|
+
9. [Noise Reduction Philosophy](#9-noise-reduction-philosophy)
|
|
21
|
+
10. [Technology Choices & Alternatives](#10-technology-choices--alternatives)
|
|
22
|
+
11. [Trade-offs](#11-trade-offs)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. Context & Problem
|
|
27
|
+
|
|
28
|
+
### 1.1. Problem Statement
|
|
29
|
+
|
|
30
|
+
E11y routes events to production backends (Loki, Sentry, OpenTelemetry). During local development, those backends are unavailable or impractical to run. Developers need answers to:
|
|
31
|
+
|
|
32
|
+
- What events fired during this request?
|
|
33
|
+
- Which events contained errors?
|
|
34
|
+
- Did sampling or PII filtering suppress an event?
|
|
35
|
+
- How do parallel async traces relate to a single user interaction?
|
|
36
|
+
|
|
37
|
+
Before e11y-devtools, the only option was configuring a `StdoutAdapter` and scanning console output manually — a high-noise, low-signal workflow.
|
|
38
|
+
|
|
39
|
+
### 1.2. Goals
|
|
40
|
+
|
|
41
|
+
1. **Zero configuration** — works automatically in `development` and `test` environments.
|
|
42
|
+
2. **Zero production overhead** — the write-path adapter is production-safe (tiny, no UI code).
|
|
43
|
+
3. **Multiple access modes** — terminal (TUI), browser (overlay), AI assistant (MCP).
|
|
44
|
+
4. **Noise reduction** — show developers what matters, hide what does not.
|
|
45
|
+
|
|
46
|
+
### 1.3. Non-Goals
|
|
47
|
+
|
|
48
|
+
- Replace production observability backends (Loki, Sentry, OTel).
|
|
49
|
+
- Provide event schema registry or documentation generation (separate concern).
|
|
50
|
+
- Operate in production (all viewer code is in the opt-in `e11y-devtools` gem).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. Architecture: Hub-and-Spoke
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
┌─────────────────────────┐
|
|
58
|
+
│ E11y Event Pipeline │
|
|
59
|
+
│ (production gem code) │
|
|
60
|
+
└────────────┬────────────┘
|
|
61
|
+
│ DevLog adapter
|
|
62
|
+
▼
|
|
63
|
+
┌─────────────────────────┐
|
|
64
|
+
│ log/e11y_dev.jsonl │ ← single source of truth
|
|
65
|
+
│ (JSONL, gzip rotation) │
|
|
66
|
+
└──────┬────────┬──────────┘
|
|
67
|
+
│ │
|
|
68
|
+
┌────────────┘ └────────────────┐
|
|
69
|
+
▼ ▼
|
|
70
|
+
┌──────────────────┐ ┌───────────────────────────┐
|
|
71
|
+
│ TUI Viewer │ │ Browser Overlay │
|
|
72
|
+
│ bundle exec e11y │ │ Rails Engine /_e11y/ │
|
|
73
|
+
│ (ratatui_ruby) │ │ + injected JS <e11y- │
|
|
74
|
+
└──────────────────┘ │ overlay> custom element │
|
|
75
|
+
└───────────────────────────┘
|
|
76
|
+
│
|
|
77
|
+
┌──────────────┘
|
|
78
|
+
▼
|
|
79
|
+
┌─────────────────────┐
|
|
80
|
+
│ MCP Server │
|
|
81
|
+
│ bundle exec e11y mcp│
|
|
82
|
+
│ (stdio / HTTP) │
|
|
83
|
+
└─────────────────────┘
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Key design principle:** `log/e11y_dev.jsonl` is the single source of truth. The write path (DevLog adapter) lives in the production `e11y` gem — it is always available. All three viewers are independent and read the same file; they share no runtime state.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 3. DevLog Adapter
|
|
91
|
+
|
|
92
|
+
The DevLog adapter is split into three components under `lib/e11y/adapters/dev_log/`.
|
|
93
|
+
|
|
94
|
+
### 3.1. FileStore
|
|
95
|
+
|
|
96
|
+
`DevLog::FileStore` is the write path. It writes one JSON line per event and rotates the file when it exceeds configured limits.
|
|
97
|
+
|
|
98
|
+
Key implementation details:
|
|
99
|
+
- Thread-safety: `Mutex` + `File::LOCK_EX` around every write.
|
|
100
|
+
- Rotation: atomic rename to `.1`, `.2`, … up to `keep_rotated` numbered gzip files. Older files are deleted.
|
|
101
|
+
- Copy strategy: `IO.copy_stream` — no heap allocation for file content during rotation.
|
|
102
|
+
|
|
103
|
+
| Constant | Default | ENV override |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `DEFAULT_MAX_SIZE` | 50 MB | `E11Y_MAX_SIZE` |
|
|
106
|
+
| `DEFAULT_MAX_LINES` | 10 000 | `E11Y_MAX_EVENTS` |
|
|
107
|
+
| `DEFAULT_KEEP_ROTATED` | 5 | `E11Y_KEEP_ROTATED` |
|
|
108
|
+
|
|
109
|
+
### 3.2. Query
|
|
110
|
+
|
|
111
|
+
`DevLog::Query` is the read path, shared by all three viewers.
|
|
112
|
+
|
|
113
|
+
- **mtime-cached in-memory cache**: re-parses the JSONL file only when `File.mtime` changes.
|
|
114
|
+
- **Optional JSON accelerator**: uses `oj` when available, falls back to stdlib `JSON`.
|
|
115
|
+
- **Zero Rails dependency**: usable from the TUI process that has no Rails loaded.
|
|
116
|
+
|
|
117
|
+
Public API:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
query = E11y::Adapters::DevLog::Query.new("log/e11y_dev.jsonl")
|
|
121
|
+
|
|
122
|
+
query.stored_events(limit: 1000, severity: nil, source: nil) # → Array of event Hashes
|
|
123
|
+
query.search("checkout") # → Array of matching events (full-text)
|
|
124
|
+
query.events_by_trace(id) # → Array of events for one trace_id
|
|
125
|
+
query.interactions # → Array of Interaction structs (grouped traces)
|
|
126
|
+
query.stats # → Hash with counts, error rate, top events
|
|
127
|
+
query.find_event(id) # → single event Hash or nil
|
|
128
|
+
query.updated_since?(time) # → Boolean (used by polling viewers)
|
|
129
|
+
query.clear! # → truncates the JSONL file
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`Interaction` is a plain Struct:
|
|
133
|
+
```ruby
|
|
134
|
+
Interaction = Struct.new(:started_at, :trace_ids, :has_error?,
|
|
135
|
+
:source, keyword_init: true) do
|
|
136
|
+
def traces_count = trace_ids.size
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3.3. DevLog Adapter Facade
|
|
141
|
+
|
|
142
|
+
`E11y::Adapters::DevLog` wraps FileStore for writing and delegates all read calls to Query:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
adapter = E11y::Adapters::DevLog.new(
|
|
146
|
+
path: "log/e11y_dev.jsonl",
|
|
147
|
+
max_size: 50 * 1024 * 1024,
|
|
148
|
+
max_lines: 10_000,
|
|
149
|
+
keep_rotated: 5
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
adapter.write(event_data) # delegates to FileStore
|
|
153
|
+
adapter.recent_events(limit: 50) # delegates to Query
|
|
154
|
+
adapter.capabilities # → { dev_log: true, readable: true }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 3.4. DevLogSource Middleware
|
|
158
|
+
|
|
159
|
+
`E11y::Middleware::DevLogSource` is a Rack middleware that stamps request metadata before events are tracked:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
Thread.current[:e11y_source] = "web" # sets thread-local; downstream code (including DevLog#serialize) reads this
|
|
163
|
+
env["e11y.trace_id"] ||= Thread.current[:e11y_trace_id] # exposes trace ID to the Browser Overlay JS
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 3.5. Railtie Auto-Registration
|
|
167
|
+
|
|
168
|
+
The `E11y::Railtie` automatically registers the DevLog adapter in `development` and `test` environments when no `:dev_log` adapter is already configured. ENV vars control limits at boot:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
E11Y_MAX_EVENTS=5000 # override max lines
|
|
172
|
+
E11Y_MAX_SIZE=10485760 # override max file size (bytes)
|
|
173
|
+
E11Y_KEEP_ROTATED=3 # override number of rotated files kept
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 4. TUI — Interactive Log Viewer
|
|
179
|
+
|
|
180
|
+
### 4.1. Entry
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
bundle exec e11y # default: launches TUI
|
|
184
|
+
bundle exec e11y tui # explicit
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 4.2. Three-View Navigation
|
|
188
|
+
|
|
189
|
+
The TUI presents a drill-down hierarchy:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
:interactions → :events → :detail
|
|
193
|
+
(list of (events in (full JSON
|
|
194
|
+
interactions) one trace) of one event)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 4.3. Keyboard Map
|
|
198
|
+
|
|
199
|
+
| Key | View | Action |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `↓` / `↑` | interactions, events | Navigate down/up |
|
|
202
|
+
| `Enter` | interactions | Drill into events for selected interaction |
|
|
203
|
+
| `Enter` | events | Open detail overlay |
|
|
204
|
+
| `Esc` / `b` | events, detail | Go back |
|
|
205
|
+
| `w` | interactions | Source filter: web requests only |
|
|
206
|
+
| `j` | interactions | Source filter: background jobs only |
|
|
207
|
+
| `a` | interactions | Source filter: all sources |
|
|
208
|
+
| `r` | interactions | Force reload |
|
|
209
|
+
| `q` | any | Quit |
|
|
210
|
+
| `c` | detail | Copy event JSON to clipboard |
|
|
211
|
+
|
|
212
|
+
### 4.4. Interaction Grouping
|
|
213
|
+
|
|
214
|
+
The `Grouping.group` function converts a flat list of trace IDs into `Interaction` structs:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
E11y::Devtools::Tui::Grouping.group(traces, window_ms: 500)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Algorithm:
|
|
221
|
+
1. Sort traces by `started_at`.
|
|
222
|
+
2. Assign traces that start within `window_ms` of the group's start into the same `Interaction`.
|
|
223
|
+
3. Return interactions sorted newest-first.
|
|
224
|
+
|
|
225
|
+
This converts N parallel async traces into a single human-readable row without requiring any client-side coordination (no `X-Interaction-ID` header).
|
|
226
|
+
|
|
227
|
+
### 4.5. File Watcher
|
|
228
|
+
|
|
229
|
+
The TUI polls `File.mtime` every `POLL_INTERVAL_MS = 250` ms. No inotify/kqueue dependency — zero platform-specific code, cross-platform by default.
|
|
230
|
+
|
|
231
|
+
### 4.6. Widgets
|
|
232
|
+
|
|
233
|
+
| Widget | Description |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `InteractionList` | One row per interaction. Bullet: `●` red (has errors) / `○` gray (clean). |
|
|
236
|
+
| `EventList` | Table of events for a trace, colored by severity. |
|
|
237
|
+
| `EventDetail` | Popup overlay showing full JSON of a single event. |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 5. Browser Overlay
|
|
242
|
+
|
|
243
|
+
### 5.1. Rails Engine
|
|
244
|
+
|
|
245
|
+
The overlay is a Rails Engine with isolated namespace, mounted automatically at `/_e11y/`:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# config/routes.rb (added by Railtie)
|
|
249
|
+
mount E11y::Devtools::Overlay::Engine => "/_e11y"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
All controller actions return `404 Not Found` outside the `development` environment, making accidental production mount harmless.
|
|
253
|
+
|
|
254
|
+
### 5.2. Controller Endpoints
|
|
255
|
+
|
|
256
|
+
| Method | Path | Description |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `GET` | `/_e11y/events?trace_id=` | Events for a specific trace |
|
|
259
|
+
| `GET` | `/_e11y/events/recent?limit=` | Most recent N events |
|
|
260
|
+
| `DELETE` | `/_e11y/events` | Clear log; returns 204 No Content |
|
|
261
|
+
|
|
262
|
+
### 5.3. Rack Middleware — Script Injection
|
|
263
|
+
|
|
264
|
+
`E11y::Devtools::Overlay::Middleware` sits in the Rack stack and injects the overlay script into HTML responses:
|
|
265
|
+
|
|
266
|
+
- Skips: XHR requests, asset paths (`/assets/`, `.js`, `.css`, etc.), non-HTML content types.
|
|
267
|
+
- Injects `<script>` tag before `</body>`.
|
|
268
|
+
- Injects `window.__E11Y_TRACE_ID__` with the current request's trace ID.
|
|
269
|
+
- Recalculates and updates `Content-Length` header.
|
|
270
|
+
|
|
271
|
+
### 5.4. Custom Element
|
|
272
|
+
|
|
273
|
+
The injected script registers a vanilla JS Custom Element `<e11y-overlay>` using Shadow DOM:
|
|
274
|
+
|
|
275
|
+
- **Badge**: floating, bottom-right corner. Shows event count and error count.
|
|
276
|
+
- **Error indicator**: red border around the badge when any event in the current trace has severity `error` or `fatal`.
|
|
277
|
+
- **Panel**: click the badge to open a slide-in panel showing the current trace's events.
|
|
278
|
+
- **Polling**: queries `/_e11y/events?trace_id=...` every 2 seconds.
|
|
279
|
+
- **Footer actions**: `[clear log]` (DELETE) and `[copy trace_id]`.
|
|
280
|
+
|
|
281
|
+
No npm build step, no React, no webpack — the script is a single file of vanilla JS shipped with the gem.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 6. MCP Server
|
|
286
|
+
|
|
287
|
+
### 6.1. Entry
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
bundle exec e11y mcp # stdio transport (Claude Desktop, Cursor)
|
|
291
|
+
bundle exec e11y mcp --port 3099 # StreamableHTTP transport (WEBrick)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 6.2. Tools
|
|
295
|
+
|
|
296
|
+
The MCP server exposes 8 tools backed by `DevLog::Query`:
|
|
297
|
+
|
|
298
|
+
| Tool | Description |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `RecentEvents` | Most recent N events (default 50) |
|
|
301
|
+
| `EventsByTrace` | All events for a given `trace_id` |
|
|
302
|
+
| `Search` | Full-text search across event JSON |
|
|
303
|
+
| `Stats` | Summary: total count, error rate, top event types |
|
|
304
|
+
| `Interactions` | Grouped interaction list (same grouping as TUI) |
|
|
305
|
+
| `EventDetail` | Full data for a single event by ID |
|
|
306
|
+
| `Errors` | All events with severity `error` or `fatal` |
|
|
307
|
+
| `Clear` | Truncate the log file |
|
|
308
|
+
|
|
309
|
+
### 6.3 AI Tool Setup
|
|
310
|
+
|
|
311
|
+
**Cursor** (`.cursor/mcp.json`):
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
{
|
|
315
|
+
"mcpServers": {
|
|
316
|
+
"e11y": {
|
|
317
|
+
"command": "bundle",
|
|
318
|
+
"args": ["exec", "e11y", "mcp"],
|
|
319
|
+
"cwd": "/path/to/your/rails/app"
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Claude Code** (`.claude/mcp.json` or `claude_desktop_config.json`):
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"mcpServers": {
|
|
330
|
+
"e11y": {
|
|
331
|
+
"command": "bundle",
|
|
332
|
+
"args": ["exec", "e11y", "mcp"]
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Once connected, ask your AI assistant: *"What errors happened in the last request?"* or *"Show me all events for trace abc-123"*.
|
|
339
|
+
|
|
340
|
+
### 6.4. Server Context
|
|
341
|
+
|
|
342
|
+
The `server_context` passed to every tool handler contains:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
{ store: E11y::Adapters::DevLog::Query.new(log_path) }
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Tools call `context[:store]` directly — no shared mutable state between requests.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 7. CLI Entry Point
|
|
353
|
+
|
|
354
|
+
The `e11y` executable (`gems/e11y-devtools/exe/e11y`) dispatches subcommands:
|
|
355
|
+
|
|
356
|
+
| Subcommand | Behavior |
|
|
357
|
+
|---|---|
|
|
358
|
+
| `e11y` (no args) | Launches TUI (default) |
|
|
359
|
+
| `e11y tui` | Launches TUI explicitly |
|
|
360
|
+
| `e11y mcp` | Starts MCP server on stdio |
|
|
361
|
+
| `e11y mcp --port N` | Starts MCP server on port N over HTTP |
|
|
362
|
+
| `e11y tail` | Streams new events to stdout (like `tail -f`) |
|
|
363
|
+
| `e11y help` | Prints usage |
|
|
364
|
+
|
|
365
|
+
**Log path auto-detection**: the CLI walks up from `Dir.pwd` looking for `log/e11y_dev.jsonl`, stopping at the first directory that contains it (or a `Gemfile`). This makes it work correctly from any subdirectory of the project.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## 8. Monorepo Structure
|
|
370
|
+
|
|
371
|
+
e11y uses a two-gem monorepo layout:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
e11y/ ← root
|
|
375
|
+
├── e11y.gemspec ← production gem (v0.2.x)
|
|
376
|
+
├── lib/
|
|
377
|
+
│ └── e11y/
|
|
378
|
+
│ └── adapters/
|
|
379
|
+
│ └── dev_log/ ← DevLog adapter (production-safe)
|
|
380
|
+
│ ├── file_store.rb
|
|
381
|
+
│ ├── query.rb
|
|
382
|
+
│ └── dev_log.rb
|
|
383
|
+
└── gems/
|
|
384
|
+
└── e11y-devtools/ ← separate gem (dev-only)
|
|
385
|
+
├── e11y-devtools.gemspec
|
|
386
|
+
└── lib/
|
|
387
|
+
└── e11y/
|
|
388
|
+
└── devtools/
|
|
389
|
+
├── tui/ ← TUI viewer
|
|
390
|
+
├── overlay/ ← Rails Engine + Rack middleware + JS
|
|
391
|
+
└── mcp/ ← MCP server tools
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### 8.1 Why Two Gems?
|
|
395
|
+
|
|
396
|
+
The write-path (`DevLog` adapter, `FileStore`, `Query`, `DevLogSource` middleware) is
|
|
397
|
+
**production-safe**: it has zero viewer dependencies and runs in any environment.
|
|
398
|
+
It lives in `gem 'e11y'` so production apps can enable it if needed (e.g., for
|
|
399
|
+
log-based observability pipelines).
|
|
400
|
+
|
|
401
|
+
The viewers (TUI via ratatui_ruby, Browser Overlay, MCP Server) are **dev-only** by
|
|
402
|
+
design. `ratatui_ruby` ships Rust-compiled binaries; adding it to production gems
|
|
403
|
+
inflates deploy size and introduces native-extension compilation. Keeping viewers in
|
|
404
|
+
a separate gem makes the dependency opt-in:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
# Gemfile
|
|
408
|
+
gem "e11y" # always
|
|
409
|
+
gem "e11y-devtools", group: :development # never reaches production
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### 8.2. Gem Dependencies
|
|
413
|
+
|
|
414
|
+
**`e11y.gemspec`** (production gem — no devtools dependencies):
|
|
415
|
+
```ruby
|
|
416
|
+
# No ratatui_ruby, no mcp gem here
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**`gems/e11y-devtools/e11y-devtools.gemspec`** (opt-in dev gem):
|
|
420
|
+
```ruby
|
|
421
|
+
spec.add_dependency "e11y", "~> 0.2"
|
|
422
|
+
spec.add_dependency "ratatui_ruby", "~> 1.4"
|
|
423
|
+
spec.add_dependency "mcp", ">= 1.0"
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Developers add `e11y-devtools` only to the `:development` group in their Gemfile:
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
gem "e11y"
|
|
430
|
+
|
|
431
|
+
group :development do
|
|
432
|
+
gem "e11y-devtools"
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## 9. Noise Reduction Philosophy
|
|
439
|
+
|
|
440
|
+
Local development log noise is the primary usability concern. e11y-devtools applies noise reduction at three independent layers:
|
|
441
|
+
|
|
442
|
+
### Layer 1: Buffer Flush (inherited from e11y core)
|
|
443
|
+
|
|
444
|
+
Debug-level events accumulate in the request-scoped buffer (see ADR-001). They are written to `log/e11y_dev.jsonl` **only when the request fails**. A successful request produces zero debug-level entries in the devlog.
|
|
445
|
+
|
|
446
|
+
### Layer 2: Viewer Defaults
|
|
447
|
+
|
|
448
|
+
| Viewer | Default filter |
|
|
449
|
+
|---|---|
|
|
450
|
+
| TUI | `:web` source filter — shows only web request interactions, not background jobs |
|
|
451
|
+
| Browser Overlay | Current trace only — the panel shows only events from the active request |
|
|
452
|
+
| MCP `recent_events` | `limit: 50` — bounded by default |
|
|
453
|
+
|
|
454
|
+
### Layer 3: Interaction Grouping
|
|
455
|
+
|
|
456
|
+
A single user action typically spawns multiple async traces (background jobs, ActionCable, webhooks). The TUI groups traces within a 500 ms window into one `Interaction` row, reducing visual noise from N rows to 1.
|
|
457
|
+
|
|
458
|
+
The three layers are independent. Any one layer alone would reduce noise meaningfully; together they make the default view tractable even in busy development servers.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 10. Technology Choices & Alternatives
|
|
463
|
+
|
|
464
|
+
### 10.1. TUI Library
|
|
465
|
+
|
|
466
|
+
| Candidate | Chosen? | Rationale |
|
|
467
|
+
|---|---|---|
|
|
468
|
+
| `ratatui_ruby` | **Yes** | Built-in `TestHelper` for unit testing widgets; 44 published versions (stable); all needed widgets available; single GC (no subprocess) |
|
|
469
|
+
| `charm-ruby` | No | ~10 commits total at decision time; no built-in test support |
|
|
470
|
+
| Plain ANSI escape codes | No | Custom widget code duplication; no input handling |
|
|
471
|
+
|
|
472
|
+
### 10.2. MCP Library
|
|
473
|
+
|
|
474
|
+
| Candidate | Chosen? | Rationale |
|
|
475
|
+
|---|---|---|
|
|
476
|
+
| `mcp` (Anthropic+Shopify) | **Yes** | StreamableHTTP transport; full MCP spec compliance; actively maintained by protocol authors |
|
|
477
|
+
| `fast-mcp` | No | No StreamableHTTP support at decision time; community-maintained, not spec-complete |
|
|
478
|
+
| Custom JSON-RPC | No | Significant maintenance surface; no transport flexibility |
|
|
479
|
+
|
|
480
|
+
### 10.3. Trace Grouping Strategy
|
|
481
|
+
|
|
482
|
+
| Approach | Chosen? | Rationale |
|
|
483
|
+
|---|---|---|
|
|
484
|
+
| Time-window grouping (500 ms) | **Yes** | No client-side coordination; zero configuration; works without HTTP header support |
|
|
485
|
+
| `X-Interaction-ID` header | No | Requires all clients (background jobs, ActionCable) to propagate a custom header; breaks for third-party callers |
|
|
486
|
+
|
|
487
|
+
### 10.4. File Watch Strategy
|
|
488
|
+
|
|
489
|
+
| Approach | Chosen? | Rationale |
|
|
490
|
+
|---|---|---|
|
|
491
|
+
| Poll `File.mtime` every 250 ms | **Yes** | Zero native dependencies; works on macOS, Linux, Docker without kernel feature flags |
|
|
492
|
+
| `inotify` / `kqueue` | No | Platform-specific; adds C extension dependency; Docker volume mounts may not deliver events |
|
|
493
|
+
| `listen` gem | No | Pulls in `rb-fsevent` / `rb-inotify`; heavy for a dev-only tool |
|
|
494
|
+
|
|
495
|
+
### 10.5. Browser Overlay Build
|
|
496
|
+
|
|
497
|
+
| Approach | Chosen? | Rationale |
|
|
498
|
+
|---|---|---|
|
|
499
|
+
| Vanilla JS Custom Element | **Yes** | No npm build; zero-config; ships as a single `.js` file in the gem |
|
|
500
|
+
| React SPA | No | Requires npm build step; breaks zero-config install; 100 KB+ overhead |
|
|
501
|
+
| Separate dev server | No | Port management; firewall issues; second process to manage |
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## 11. Trade-offs
|
|
506
|
+
|
|
507
|
+
### 11.1. Accepted Trade-offs
|
|
508
|
+
|
|
509
|
+
**Polling instead of push**: The 250 ms poll interval means the TUI and overlay lag by up to 250 ms. This is imperceptible in practice for a developer tool and eliminates all platform-specific file-watch dependencies.
|
|
510
|
+
|
|
511
|
+
**JSONL over SQLite**: A flat JSONL file is simpler to rotate, inspect with standard tools (`tail`, `jq`), and ship without native dependencies. Random-access query performance at 10 000 events (the default limit) is acceptable with the mtime-cached in-memory parse.
|
|
512
|
+
|
|
513
|
+
**Interaction grouping is heuristic**: The 500 ms window is a heuristic — it may merge unrelated concurrent requests or split a single slow interaction. It is a viewer-level concern only; the raw JSONL contains full per-trace data for manual inspection.
|
|
514
|
+
|
|
515
|
+
**No structured query language**: The `search` method is full-text across serialized JSON. This is sufficient for local development workflows and avoids embedding a query parser.
|
|
516
|
+
|
|
517
|
+
### 11.2. Future Considerations
|
|
518
|
+
|
|
519
|
+
- Configurable `window_ms` for interaction grouping (currently hard-coded at 500).
|
|
520
|
+
- WebSocket push from the overlay controller to eliminate polling lag in the browser.
|
|
521
|
+
- Index file alongside JSONL for O(1) trace lookup at scale (relevant if `max_lines` is raised significantly).
|
|
522
|
+
- `e11y tail` output formats: JSON, pretty-print, structured table.
|