e11y 0.2.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +130 -10
- data/CHANGELOG.md +80 -1
- data/CLAUDE.md +168 -0
- data/CONTRIBUTING.md +640 -0
- data/README.md +165 -701
- data/RELEASE.md +41 -12
- data/Rakefile +249 -57
- 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 +79 -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} +36 -65
- data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
- data/docs/architecture/ADR-003-slo-observability.md +1402 -0
- 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} +182 -743
- 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} +44 -86
- data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +11 -11
- 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} +43 -59
- data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +58 -355
- 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/plans/2026-03-20-browser-overlay-svelte.md +281 -0
- 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 +33 -684
- 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 +30 -178
- data/docs/use_cases/UC-010-background-job-tracking.md +24 -91
- 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 +158 -0
- data/gems/e11y-devtools/config/routes.rb +15 -0
- data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
- data/gems/e11y-devtools/exe/e11y +34 -0
- data/gems/e11y-devtools/frontend/.gitignore +24 -0
- data/gems/e11y-devtools/frontend/README.md +51 -0
- data/gems/e11y-devtools/frontend/index.html +14 -0
- data/gems/e11y-devtools/frontend/package-lock.json +3707 -0
- data/gems/e11y-devtools/frontend/package.json +28 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/events/recent.json +4205 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/interactions.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0a2e04027cfa22d014bc22e8b27cd913/events.json +86 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0e1543af6a630fb3af6b52283154b3e0/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/1838b691faa49564f97db8592ff3978d/events.json +78 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/29f198f6588dacffb687777eb5f8f118/events.json +197 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/34bc3c9c0097de28a7a6f99b90a8e7bc/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/3ba6c20d068ab9cee00e51b180e66444/events.json +184 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/435bfd8f17b9009146a79812d7c3726d/events.json +144 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/4c7676e3fe668e99edb2b94d7d5678a9/events.json +222 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/6daf0d47974bedfc55d5de7004a3ea9f/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8a81ada42834d15f287bb40010043605/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8c0a98900edaae105469df8daedccf02/events.json +198 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8e4f645180f8a7d1dce426b07380466b/events.json +222 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/93db346fa5d44a032605a13b627f4b80/events.json +128 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/98ff6146faf7bd9be8bd03a8275817ba/events.json +223 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/9997ddd0247bc7e25f2ca7a5c415c93d/events.json +197 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/99e35f8ef3baedd798cc4fd085980ad9/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b4f3095c1909924cbc98889a86c83d6d/events.json +131 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b54b7fc32b7575a7110de809d11ccda0/events.json +128 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c0b48033fa06746bcc5886745e053cff/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c44649ac76701b4558927cd2305ab535/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/d601ae3320057580a39dbdac2edfdf4a/events.json +248 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e67e724bab422d2b52eeb49635e512e1/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e6c72765a28f158a8485b35fa63f73da/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/f541b87405c9a54819b18ebe529f6419/events.json +194 -0
- data/gems/e11y-devtools/frontend/scripts/generate_mocks.rb +397 -0
- data/gems/e11y-devtools/frontend/src/App.svelte +827 -0
- data/gems/e11y-devtools/frontend/src/components/Fab.svelte +19 -0
- data/gems/e11y-devtools/frontend/src/components/FilterBar.svelte +38 -0
- data/gems/e11y-devtools/frontend/src/components/FullscreenPanel.svelte +82 -0
- data/gems/e11y-devtools/frontend/src/components/InteractionsTimeline.svelte +264 -0
- data/gems/e11y-devtools/frontend/src/components/RecentHistogram.svelte +354 -0
- data/gems/e11y-devtools/frontend/src/lib/api.ts +37 -0
- data/gems/e11y-devtools/frontend/src/lib/eventIdentity.ts +12 -0
- data/gems/e11y-devtools/frontend/src/lib/format.ts +37 -0
- data/gems/e11y-devtools/frontend/src/lib/listFilter.ts +43 -0
- data/gems/e11y-devtools/frontend/src/lib/recentVolume.ts +80 -0
- data/gems/e11y-devtools/frontend/src/lib/router.ts +12 -0
- data/gems/e11y-devtools/frontend/src/lib/transitions.ts +34 -0
- data/gems/e11y-devtools/frontend/src/lib/viewportOrigin.ts +25 -0
- data/gems/e11y-devtools/frontend/src/main.ts +8 -0
- data/gems/e11y-devtools/frontend/src/overlay-entry.ts +24 -0
- data/gems/e11y-devtools/frontend/src/overlay.css +1080 -0
- data/gems/e11y-devtools/frontend/svelte.config.js +2 -0
- data/gems/e11y-devtools/frontend/test_puppeteer.js +41 -0
- data/gems/e11y-devtools/frontend/test_scale.js +3 -0
- data/gems/e11y-devtools/frontend/tsconfig.app.json +21 -0
- data/gems/e11y-devtools/frontend/tsconfig.json +7 -0
- data/gems/e11y-devtools/frontend/tsconfig.node.json +26 -0
- data/gems/e11y-devtools/frontend/vite.config.ts +36 -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 +20 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +94 -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 +67 -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 +91 -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 +44 -12
- data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
- data/lib/e11y/instruments/sidekiq.rb +135 -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 +4 -4
- data/lib/e11y/presets/audit_event.rb +13 -2
- data/lib/e11y/railtie.rb +52 -14
- 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 +144 -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 +123 -266
- 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 +186 -39
- data/docs/ADR-003-slo-observability.md +0 -3337
- 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
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
# @return [Array<Hash>] JSON-ready rows, newest interaction first (matches TUI ordering).
|
|
38
|
+
def v1_interactions(source: nil, limit: 50, window_ms: 500)
|
|
39
|
+
src = normalize_v1_source(source)
|
|
40
|
+
lim = limit.to_i.clamp(1, 500)
|
|
41
|
+
wm = window_ms.to_i.clamp(50, 10_000)
|
|
42
|
+
list = @query.interactions(window_ms: wm, limit: lim, source: src)
|
|
43
|
+
list.reverse.map { |row| v1_interaction_hash(row) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Array<Hash>] events for trace, chronological (same as DevLog::Query).
|
|
47
|
+
def v1_trace_events(trace_id)
|
|
48
|
+
return [] if trace_id.nil? || trace_id.to_s.empty?
|
|
49
|
+
|
|
50
|
+
@query.events_by_trace(trace_id.to_s)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Array<Hash>] newest-first flat list for badge / pulse.
|
|
54
|
+
def v1_recent_events(limit: 100)
|
|
55
|
+
lim = limit.to_i.clamp(1, 500)
|
|
56
|
+
@query.stored_events(limit: lim)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def normalize_v1_source(source)
|
|
62
|
+
s = source.to_s
|
|
63
|
+
return "web" if s == "web"
|
|
64
|
+
return "job" if s == "job"
|
|
65
|
+
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def v1_interaction_hash(row)
|
|
70
|
+
{
|
|
71
|
+
"started_at" => row.started_at.iso8601(3),
|
|
72
|
+
"trace_ids" => row.trace_ids,
|
|
73
|
+
"has_error" => row.has_error?,
|
|
74
|
+
"source" => row.source,
|
|
75
|
+
"traces_count" => row.traces_count
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_query
|
|
80
|
+
if defined?(E11y) && E11y.respond_to?(:configuration)
|
|
81
|
+
adapter = E11y.configuration.adapters[:dev_log]
|
|
82
|
+
return adapter if adapter.respond_to?(:stored_events)
|
|
83
|
+
end
|
|
84
|
+
default_path = if defined?(Rails) && Rails.respond_to?(:root)
|
|
85
|
+
Rails.root.join("log", "e11y_dev.jsonl").to_s
|
|
86
|
+
else
|
|
87
|
+
"log/e11y_dev.jsonl"
|
|
88
|
+
end
|
|
89
|
+
E11y::Adapters::DevLog::Query.new(default_path)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
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,67 @@
|
|
|
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
|
+
def overlay_js
|
|
31
|
+
path = E11y::Devtools::Overlay::Engine.root.join(
|
|
32
|
+
"lib/e11y/devtools/overlay/assets/overlay.js"
|
|
33
|
+
)
|
|
34
|
+
return head :not_found unless path.file?
|
|
35
|
+
|
|
36
|
+
send_file path, type: "application/javascript", disposition: "inline"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def v1_interactions
|
|
40
|
+
render json: overlay_ctrl.v1_interactions(
|
|
41
|
+
source: params[:source],
|
|
42
|
+
limit: params[:limit],
|
|
43
|
+
window_ms: params[:window_ms]
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def v1_trace_events
|
|
48
|
+
render json: overlay_ctrl.v1_trace_events(params[:trace_id])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def v1_events_recent
|
|
52
|
+
render json: overlay_ctrl.v1_recent_events(limit: params[:limit])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def overlay_ctrl
|
|
58
|
+
@overlay_ctrl ||= Controller.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def development_only!
|
|
62
|
+
head :not_found unless Rails.env.development? || Rails.env.test?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "e11y/adapters/dev_log/query"
|
|
6
|
+
require_relative "grouping"
|
|
7
|
+
|
|
8
|
+
module E11y
|
|
9
|
+
module Devtools
|
|
10
|
+
module Tui
|
|
11
|
+
# Top-level TUI application.
|
|
12
|
+
#
|
|
13
|
+
# Manages navigation state (:interactions | :events | :detail),
|
|
14
|
+
# handles keyboard events, and reloads data when the log file changes.
|
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
|
16
|
+
class App
|
|
17
|
+
attr_reader :current_view, :source_filter
|
|
18
|
+
|
|
19
|
+
POLL_INTERVAL_MS = 250
|
|
20
|
+
|
|
21
|
+
def initialize(log_path: nil)
|
|
22
|
+
@log_path = log_path || auto_detect_log_path
|
|
23
|
+
@query = E11y::Adapters::DevLog::Query.new(@log_path)
|
|
24
|
+
@current_view = :interactions
|
|
25
|
+
@source_filter = :web
|
|
26
|
+
@selected_ix = 0
|
|
27
|
+
@interactions = []
|
|
28
|
+
@events = []
|
|
29
|
+
@current_trace_id = nil
|
|
30
|
+
@current_event = nil
|
|
31
|
+
@last_mtime = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Start the TUI event loop (blocks until user quits).
|
|
35
|
+
def run
|
|
36
|
+
require "ratatui_ruby"
|
|
37
|
+
require_relative "widgets/interaction_list"
|
|
38
|
+
require_relative "widgets/event_list"
|
|
39
|
+
require_relative "widgets/event_detail"
|
|
40
|
+
RatatuiRuby.run do |tui|
|
|
41
|
+
loop do
|
|
42
|
+
reload_if_changed!
|
|
43
|
+
tui.draw { |frame| render(tui, frame) }
|
|
44
|
+
event = tui.poll_event(timeout_ms: POLL_INTERVAL_MS)
|
|
45
|
+
break if quit_event?(event)
|
|
46
|
+
|
|
47
|
+
handle_key(key_from(event)) if key_event?(event)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Handle a single key press (public for testability).
|
|
53
|
+
def handle_key(key)
|
|
54
|
+
case @current_view
|
|
55
|
+
when :interactions then handle_interactions_key(key)
|
|
56
|
+
when :events then handle_events_key(key)
|
|
57
|
+
when :detail then handle_detail_key(key)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return the currently highlighted interaction (or nil).
|
|
62
|
+
def selected_interaction
|
|
63
|
+
@interactions[@selected_ix]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# --- Rendering ---
|
|
69
|
+
|
|
70
|
+
def render(tui, frame)
|
|
71
|
+
case @current_view
|
|
72
|
+
when :interactions then render_interactions(tui, frame)
|
|
73
|
+
when :events then render_events(tui, frame)
|
|
74
|
+
when :detail
|
|
75
|
+
render_events(tui, frame)
|
|
76
|
+
Widgets::EventDetail.new(event: @current_event).render(tui, frame, frame.area)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_interactions(tui, frame)
|
|
81
|
+
Widgets::InteractionList.new(
|
|
82
|
+
interactions: @interactions,
|
|
83
|
+
selected_index: @selected_ix
|
|
84
|
+
).render(tui, frame, frame.area)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def render_events(tui, frame)
|
|
88
|
+
Widgets::EventList.new(
|
|
89
|
+
events: @events,
|
|
90
|
+
trace_id: @current_trace_id || "",
|
|
91
|
+
selected_index: @selected_ix
|
|
92
|
+
).render(tui, frame, frame.area)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# --- Key handlers per view ---
|
|
96
|
+
|
|
97
|
+
def handle_interactions_key(key)
|
|
98
|
+
case key
|
|
99
|
+
when "enter" then drill_into_events
|
|
100
|
+
when "j" then @source_filter = :job
|
|
101
|
+
reload!
|
|
102
|
+
when "w" then @source_filter = :web
|
|
103
|
+
reload!
|
|
104
|
+
when "a" then @source_filter = :all
|
|
105
|
+
reload!
|
|
106
|
+
when "down" then move_down(@interactions.size)
|
|
107
|
+
when "up" then move_up
|
|
108
|
+
when "r" then reload!
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def handle_events_key(key)
|
|
113
|
+
case key
|
|
114
|
+
when "esc", "b" then back_to_interactions
|
|
115
|
+
when "enter" then drill_into_detail
|
|
116
|
+
when "down" then move_down(@events.size)
|
|
117
|
+
when "up" then move_up
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def handle_detail_key(key)
|
|
122
|
+
case key
|
|
123
|
+
when "esc", "b" then @current_view = :events
|
|
124
|
+
when "c" then copy_to_clipboard(::JSON.generate(@current_event))
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# --- Navigation helpers ---
|
|
129
|
+
|
|
130
|
+
def drill_into_events
|
|
131
|
+
ix = selected_interaction
|
|
132
|
+
return unless ix
|
|
133
|
+
|
|
134
|
+
@current_trace_id = ix.trace_ids.first
|
|
135
|
+
@events = @query.events_by_trace(@current_trace_id)
|
|
136
|
+
@selected_ix = 0
|
|
137
|
+
@current_view = :events
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def drill_into_detail
|
|
141
|
+
event = @events[@selected_ix]
|
|
142
|
+
return unless event
|
|
143
|
+
|
|
144
|
+
@current_event = event
|
|
145
|
+
@current_view = :detail
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def back_to_interactions
|
|
149
|
+
@current_view = :interactions
|
|
150
|
+
@selected_ix = 0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def move_down(size)
|
|
154
|
+
@selected_ix = [@selected_ix + 1, size - 1].min
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def move_up
|
|
158
|
+
@selected_ix = [@selected_ix - 1, 0].max
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# --- Data loading ---
|
|
162
|
+
|
|
163
|
+
def reload_if_changed!
|
|
164
|
+
mtime = file_mtime
|
|
165
|
+
return if mtime == @last_mtime
|
|
166
|
+
|
|
167
|
+
@last_mtime = mtime
|
|
168
|
+
reload!
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def reload!
|
|
172
|
+
source = @source_filter == :all ? nil : @source_filter.to_s
|
|
173
|
+
traces = build_traces(source)
|
|
174
|
+
@interactions = Grouping.group(traces, window_ms: 500)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def build_traces(source)
|
|
178
|
+
events = @query.stored_events(limit: 5000, source: source)
|
|
179
|
+
trace_map = {}
|
|
180
|
+
events.each { |e| accumulate_trace(trace_map, e) }
|
|
181
|
+
trace_map.values
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def accumulate_trace(trace_map, event)
|
|
185
|
+
tid = event["trace_id"]
|
|
186
|
+
return unless tid
|
|
187
|
+
|
|
188
|
+
entry = trace_map[tid] ||= {
|
|
189
|
+
trace_id: tid,
|
|
190
|
+
started_at: parse_time(event.dig("metadata", "started_at") || event["timestamp"]),
|
|
191
|
+
severity: event["severity"],
|
|
192
|
+
source: event.dig("metadata", "source") || "web"
|
|
193
|
+
}
|
|
194
|
+
entry[:severity] = "error" if %w[error fatal].include?(event["severity"])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def file_mtime
|
|
198
|
+
::File.mtime(@log_path)
|
|
199
|
+
rescue Errno::ENOENT
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# --- Utilities ---
|
|
204
|
+
|
|
205
|
+
def auto_detect_log_path
|
|
206
|
+
dir = Pathname.new(Dir.pwd)
|
|
207
|
+
loop do
|
|
208
|
+
candidate = dir.join("log", "e11y_dev.jsonl")
|
|
209
|
+
return candidate.to_s if candidate.exist?
|
|
210
|
+
|
|
211
|
+
parent = dir.parent
|
|
212
|
+
break if parent == dir
|
|
213
|
+
|
|
214
|
+
dir = parent
|
|
215
|
+
end
|
|
216
|
+
"log/e11y_dev.jsonl"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def parse_time(str)
|
|
220
|
+
::Time.parse(str.to_s)
|
|
221
|
+
rescue ArgumentError, TypeError
|
|
222
|
+
::Time.now
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def quit_event?(event)
|
|
226
|
+
return false unless event
|
|
227
|
+
return false unless event[:type] == :key
|
|
228
|
+
|
|
229
|
+
event[:code] == "q" ||
|
|
230
|
+
(event[:code] == "c" && event[:modifiers]&.include?("ctrl"))
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def key_event?(event)
|
|
234
|
+
event && event[:type] == :key
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def key_from(event)
|
|
238
|
+
event&.dig(:code)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def copy_to_clipboard(text)
|
|
242
|
+
copy_macos(text) || copy_linux(text)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def copy_macos(text)
|
|
246
|
+
::IO.popen("pbcopy", "w") { |f| f.write(text) }
|
|
247
|
+
true
|
|
248
|
+
rescue StandardError
|
|
249
|
+
false
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def copy_linux(text)
|
|
253
|
+
::IO.popen("xclip -selection clipboard", "w") { |f| f.write(text) }
|
|
254
|
+
true
|
|
255
|
+
rescue StandardError
|
|
256
|
+
false
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
# rubocop:enable Metrics/ClassLength
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Devtools
|
|
5
|
+
module Tui
|
|
6
|
+
# Pure-function time-window grouping for traces → interactions.
|
|
7
|
+
# Shared by TUI widgets, Overlay, and MCP interactions tool.
|
|
8
|
+
module Grouping
|
|
9
|
+
# Severities that count as errors for interaction flagging.
|
|
10
|
+
ERROR_SEVERITIES = %w[error fatal].freeze
|
|
11
|
+
|
|
12
|
+
# Value object representing one interaction group.
|
|
13
|
+
Interaction = Struct.new(:started_at, :trace_ids, :has_error?,
|
|
14
|
+
:source)
|
|
15
|
+
|
|
16
|
+
# Group an array of trace hashes into Interaction bands.
|
|
17
|
+
#
|
|
18
|
+
# @param traces [Array<Hash>] Each hash must have :trace_id,
|
|
19
|
+
# :started_at (Time), :severity
|
|
20
|
+
# @param window_ms [Integer] Grouping window in milliseconds
|
|
21
|
+
# @return [Array<Interaction>] Newest-first
|
|
22
|
+
def self.group(traces, window_ms: 500)
|
|
23
|
+
return [] if traces.empty?
|
|
24
|
+
|
|
25
|
+
build_interactions(accumulate_groups(traces, window_ms))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.accumulate_groups(traces, window_ms)
|
|
29
|
+
sorted = traces.sort_by { |t| t[:started_at] }
|
|
30
|
+
groups = []
|
|
31
|
+
current = nil
|
|
32
|
+
sorted.each { |trace| current = append_trace(groups, current, trace, window_ms) }
|
|
33
|
+
groups
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.append_trace(groups, current, trace, window_ms)
|
|
37
|
+
if current.nil? || outside_window?(trace, current, window_ms)
|
|
38
|
+
current = new_group(trace)
|
|
39
|
+
groups << current
|
|
40
|
+
end
|
|
41
|
+
current[:trace_ids] << trace[:trace_id]
|
|
42
|
+
current[:has_error] ||= ERROR_SEVERITIES.include?(trace[:severity])
|
|
43
|
+
current
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.outside_window?(trace, current, window_ms)
|
|
47
|
+
(trace[:started_at] - current[:anchor]) * 1000 > window_ms
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.new_group(trace)
|
|
51
|
+
{ anchor: trace[:started_at], started_at: trace[:started_at],
|
|
52
|
+
trace_ids: [], has_error: false, source: trace[:source] }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.build_interactions(groups)
|
|
56
|
+
groups.reverse.map do |g|
|
|
57
|
+
Interaction.new(g[:started_at], g[:trace_ids], g[:has_error], g[:source])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private_class_method :accumulate_groups, :append_trace,
|
|
62
|
+
:outside_window?, :new_group, :build_interactions
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ratatui_ruby"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Devtools
|
|
8
|
+
module Tui
|
|
9
|
+
module Widgets
|
|
10
|
+
# Full-screen popup overlay showing event payload + metadata.
|
|
11
|
+
class EventDetail
|
|
12
|
+
def initialize(event:)
|
|
13
|
+
@event = event
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render(tui, frame, area)
|
|
17
|
+
popup_area = centered_rect(area, percent_x: 80, percent_y: 70)
|
|
18
|
+
|
|
19
|
+
frame.render_widget(tui.clear, popup_area)
|
|
20
|
+
|
|
21
|
+
sev = @event["severity"] || "info"
|
|
22
|
+
title = " #{@event['event_name']} · #{sev.upcase} "
|
|
23
|
+
|
|
24
|
+
frame.render_widget(
|
|
25
|
+
tui.paragraph(
|
|
26
|
+
text: build_lines,
|
|
27
|
+
block: tui.block(title: title, borders: :all),
|
|
28
|
+
scroll: [0, 0]
|
|
29
|
+
),
|
|
30
|
+
popup_area
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def build_lines
|
|
37
|
+
lines = []
|
|
38
|
+
lines << " timestamp: #{@event['timestamp']}"
|
|
39
|
+
lines << " trace_id: #{@event['trace_id']}"
|
|
40
|
+
lines << " span_id: #{@event['span_id']}"
|
|
41
|
+
lines << ""
|
|
42
|
+
lines << " payload:"
|
|
43
|
+
JSON.pretty_generate(@event["payload"] || {}).each_line do |l|
|
|
44
|
+
lines << " #{l.chomp}"
|
|
45
|
+
end
|
|
46
|
+
lines << ""
|
|
47
|
+
lines << " [c] copy JSON [b] back"
|
|
48
|
+
lines
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def centered_rect(area, percent_x:, percent_y:)
|
|
52
|
+
w = (area.width * percent_x / 100).to_i
|
|
53
|
+
h = (area.height * percent_y / 100).to_i
|
|
54
|
+
x = area.x + ((area.width - w) / 2)
|
|
55
|
+
y = area.y + ((area.height - h) / 2)
|
|
56
|
+
RatatuiRuby::Rect.new(x: x, y: y, width: w, height: h)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|