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