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,827 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Fab from "./components/Fab.svelte"
|
|
3
|
+
import FilterBar from "./components/FilterBar.svelte"
|
|
4
|
+
import FullscreenPanel from "./components/FullscreenPanel.svelte"
|
|
5
|
+
import InteractionsTimeline, { type TimelineTimeRange } from "./components/InteractionsTimeline.svelte"
|
|
6
|
+
import RecentHistogram from "./components/RecentHistogram.svelte"
|
|
7
|
+
import { fetchInteractions, fetchRecent, fetchTraceEvents } from "./lib/api"
|
|
8
|
+
import { eventKey } from "./lib/eventIdentity"
|
|
9
|
+
import { formatInteractionStarted, summarizeTraceIds } from "./lib/format"
|
|
10
|
+
import { filterEventList, type ListSeverityFilter } from "./lib/listFilter"
|
|
11
|
+
import { buildRecentVolumeBuckets, eventTimestampMs, type HistogramTimeRange } from "./lib/recentVolume"
|
|
12
|
+
import type { OverlayRoute, SourceFilter } from "./lib/router"
|
|
13
|
+
import type { CircleOrigin } from "./lib/transitions"
|
|
14
|
+
import { originFallbackFabCorner, originFromFabButton } from "./lib/viewportOrigin"
|
|
15
|
+
|
|
16
|
+
const SPLIT_MIN_PX = 900
|
|
17
|
+
|
|
18
|
+
let panelOpen = $state(false)
|
|
19
|
+
let panelCircleOrigin = $state<CircleOrigin | null>(null)
|
|
20
|
+
let source = $state<SourceFilter>("web")
|
|
21
|
+
let route = $state<OverlayRoute>({ screen: "interactions" })
|
|
22
|
+
let interactions = $state<Record<string, unknown>[]>([])
|
|
23
|
+
let events = $state<Record<string, unknown>[]>([])
|
|
24
|
+
let recentEvents = $state<Record<string, unknown>[]>([])
|
|
25
|
+
let loadError = $state<string | null>(null)
|
|
26
|
+
/** Selected interaction row key when using wide split layout. */
|
|
27
|
+
let splitSelectedKey = $state<string | null>(null)
|
|
28
|
+
let layoutWide = $state(false)
|
|
29
|
+
|
|
30
|
+
/** Global text search and severity filter across all lists. */
|
|
31
|
+
let globalSearch = $state("")
|
|
32
|
+
let globalSeverity = $state<ListSeverityFilter>("all")
|
|
33
|
+
|
|
34
|
+
let rowExpanded = $state<Record<string, boolean>>({})
|
|
35
|
+
/** Index in the current filtered trace list; neighbors ±2 highlighted after returning from detail. */
|
|
36
|
+
let contextAnchorIndex = $state<number | null>(null)
|
|
37
|
+
|
|
38
|
+
/** Problems-tab histogram: filter errors to [startMs, endMs] (UTC, inclusive). */
|
|
39
|
+
let histogramTimeRange = $state<HistogramTimeRange | null>(null)
|
|
40
|
+
|
|
41
|
+
/** Interactions-tab timeline: filter interactions to [startMs, endMs] (UTC, inclusive). */
|
|
42
|
+
let interactionsTimeRange = $state<TimelineTimeRange | null>(null)
|
|
43
|
+
|
|
44
|
+
let prevRecentKeys = $state<Set<string>>(new Set())
|
|
45
|
+
let firstRecentPoll = $state(true)
|
|
46
|
+
let pulseKind = $state<"none" | "error" | "warn">("none")
|
|
47
|
+
let pulseTimer: ReturnType<typeof setTimeout> | null = null
|
|
48
|
+
|
|
49
|
+
const POLL_MS = 2000
|
|
50
|
+
const PULSE_MS = 3000
|
|
51
|
+
|
|
52
|
+
let problemEvents = $derived.by(() =>
|
|
53
|
+
recentEvents.filter((e) => e.severity === "error" || e.severity === "fatal")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
let filteredInteractions = $derived.by(() => {
|
|
57
|
+
let rows = interactions
|
|
58
|
+
|
|
59
|
+
if (interactionsTimeRange) {
|
|
60
|
+
const { startMs, endMs } = interactionsTimeRange
|
|
61
|
+
rows = rows.filter((i) => {
|
|
62
|
+
const t = new Date(String(i.started_at || "")).getTime()
|
|
63
|
+
if (isNaN(t)) return false
|
|
64
|
+
return t >= startMs && t <= endMs
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (globalSeverity !== "all") {
|
|
69
|
+
rows = rows.filter((i) => {
|
|
70
|
+
const s = Number(i.status) || 200
|
|
71
|
+
const hasErr = !!i.has_error
|
|
72
|
+
if (globalSeverity === "error") return s >= 500 || hasErr
|
|
73
|
+
if (globalSeverity === "warn") return s >= 400 && s < 500
|
|
74
|
+
if (globalSeverity === "rest") return s < 400 && !hasErr
|
|
75
|
+
return true
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const q = globalSearch.trim().toLowerCase()
|
|
80
|
+
if (q) {
|
|
81
|
+
rows = rows.filter((i) => {
|
|
82
|
+
const path = String(i.path || "").toLowerCase()
|
|
83
|
+
const method = String(i.method || "").toLowerCase()
|
|
84
|
+
const traceIds = ((i.trace_ids as string[]) || []).join(" ").toLowerCase()
|
|
85
|
+
return path.includes(q) || method.includes(q) || traceIds.includes(q)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return rows
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
let filteredProblemEvents = $derived.by(() => {
|
|
93
|
+
let rows = filterEventList(problemEvents, globalSeverity, globalSearch)
|
|
94
|
+
const r = histogramTimeRange
|
|
95
|
+
if (r) {
|
|
96
|
+
const buckets = buildRecentVolumeBuckets(recentEvents, 32)
|
|
97
|
+
const n = buckets.length
|
|
98
|
+
if (n === 0) {
|
|
99
|
+
rows = []
|
|
100
|
+
} else {
|
|
101
|
+
const tMin = buckets[0].t0
|
|
102
|
+
const tMax = buckets[n - 1].t1
|
|
103
|
+
const w = (tMax - tMin) / n
|
|
104
|
+
|
|
105
|
+
// If lo/hi are missing (e.g. old state), fallback to time-based filter
|
|
106
|
+
if (r.lo == null || r.hi == null) {
|
|
107
|
+
rows = rows.filter((ev) => {
|
|
108
|
+
const t = eventTimestampMs(ev)
|
|
109
|
+
if (t == null) return false
|
|
110
|
+
return t >= r.startMs && t <= r.endMs
|
|
111
|
+
})
|
|
112
|
+
} else {
|
|
113
|
+
const lo = Math.max(0, Math.min(r.lo, n - 1))
|
|
114
|
+
const hi = Math.max(0, Math.min(r.hi, n - 1))
|
|
115
|
+
const i0 = Math.min(lo, hi)
|
|
116
|
+
const i1 = Math.max(lo, hi)
|
|
117
|
+
rows = rows.filter((ev) => {
|
|
118
|
+
const t = eventTimestampMs(ev)
|
|
119
|
+
if (t == null) return false
|
|
120
|
+
const idx = Math.min(n - 1, Math.max(0, Math.floor((t - tMin) / w)))
|
|
121
|
+
return idx >= i0 && idx <= i1
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return rows
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
let filteredTraceEvents = $derived.by(() =>
|
|
130
|
+
filterEventList(events, globalSeverity, globalSearch)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
let activeTraceId = $derived.by(() => {
|
|
134
|
+
if (route.screen === "events") return route.traceId
|
|
135
|
+
return String((events[0] as Record<string, unknown> | undefined)?.trace_id ?? "")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const CONTEXT_RADIUS = 2
|
|
139
|
+
|
|
140
|
+
$effect(() => {
|
|
141
|
+
globalSearch
|
|
142
|
+
globalSeverity
|
|
143
|
+
contextAnchorIndex = null
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
$effect(() => {
|
|
147
|
+
const mq = window.matchMedia(`(min-width: ${SPLIT_MIN_PX}px)`)
|
|
148
|
+
const sync = (): void => {
|
|
149
|
+
layoutWide = mq.matches
|
|
150
|
+
}
|
|
151
|
+
sync()
|
|
152
|
+
mq.addEventListener("change", sync)
|
|
153
|
+
return () => mq.removeEventListener("change", sync)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
/** If viewport becomes narrow while split view had a selection, fall back to full-screen events list. */
|
|
157
|
+
$effect(() => {
|
|
158
|
+
if (layoutWide) return
|
|
159
|
+
if (route.screen !== "interactions" || !splitSelectedKey) return
|
|
160
|
+
const tid = events[0] && String((events[0] as Record<string, unknown>).trace_id ?? "")
|
|
161
|
+
if (tid) route = { screen: "events", traceId: tid }
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
function severityRank(s: "none" | "error" | "warn"): number {
|
|
165
|
+
if (s === "error") return 2
|
|
166
|
+
if (s === "warn") return 1
|
|
167
|
+
return 0
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function applyPulse(next: "error" | "warn"): void {
|
|
171
|
+
if (pulseTimer) clearTimeout(pulseTimer)
|
|
172
|
+
if (severityRank(next) >= severityRank(pulseKind)) pulseKind = next
|
|
173
|
+
pulseTimer = setTimeout(() => {
|
|
174
|
+
pulseKind = "none"
|
|
175
|
+
pulseTimer = null
|
|
176
|
+
}, PULSE_MS)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function processRecentForPulse(rows: Record<string, unknown>[]): void {
|
|
180
|
+
const nextKeys = new Set<string>()
|
|
181
|
+
rows.forEach((e, i) => nextKeys.add(eventKey(e, i)))
|
|
182
|
+
|
|
183
|
+
if (firstRecentPoll) {
|
|
184
|
+
firstRecentPoll = false
|
|
185
|
+
prevRecentKeys = nextKeys
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let sawNewError = false
|
|
190
|
+
let sawNewWarn = false
|
|
191
|
+
for (let i = 0; i < rows.length; i++) {
|
|
192
|
+
const e = rows[i]
|
|
193
|
+
const k = eventKey(e, i)
|
|
194
|
+
if (!prevRecentKeys.has(k)) {
|
|
195
|
+
const sev = e.severity
|
|
196
|
+
if (sev === "error" || sev === "fatal") sawNewError = true
|
|
197
|
+
else if (sev === "warn") sawNewWarn = true
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
prevRecentKeys = nextKeys
|
|
201
|
+
if (sawNewError) applyPulse("error")
|
|
202
|
+
else if (sawNewWarn) applyPulse("warn")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function countsFromRecent(rows: Record<string, unknown>[]): {
|
|
206
|
+
total: number
|
|
207
|
+
err: number
|
|
208
|
+
warn: number
|
|
209
|
+
} {
|
|
210
|
+
let err = 0
|
|
211
|
+
let warn = 0
|
|
212
|
+
for (const e of rows) {
|
|
213
|
+
const s = e.severity
|
|
214
|
+
if (s === "error" || s === "fatal") err++
|
|
215
|
+
else if (s === "warn") warn++
|
|
216
|
+
}
|
|
217
|
+
return { total: rows.length, err, warn }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function pollRecent(): Promise<void> {
|
|
221
|
+
try {
|
|
222
|
+
const rows = await fetchRecent()
|
|
223
|
+
recentEvents = rows
|
|
224
|
+
processRecentForPulse(rows)
|
|
225
|
+
loadError = null
|
|
226
|
+
} catch {
|
|
227
|
+
/* ignore transient poll failures */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function loadInteractionsList(): Promise<void> {
|
|
232
|
+
try {
|
|
233
|
+
interactions = await fetchInteractions(source)
|
|
234
|
+
loadError = null
|
|
235
|
+
} catch (e) {
|
|
236
|
+
loadError = String(e)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function goTabProblems(): void {
|
|
241
|
+
splitSelectedKey = null
|
|
242
|
+
contextAnchorIndex = null
|
|
243
|
+
route = { screen: "problems" }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function goTabInteractions(): void {
|
|
247
|
+
splitSelectedKey = null
|
|
248
|
+
contextAnchorIndex = null
|
|
249
|
+
histogramTimeRange = null
|
|
250
|
+
events = []
|
|
251
|
+
route = { screen: "interactions" }
|
|
252
|
+
void loadInteractionsList()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function onInteractionRowClick(row: Record<string, unknown>): Promise<void> {
|
|
256
|
+
const ids = row.trace_ids as string[] | undefined
|
|
257
|
+
const tid = ids?.[0]
|
|
258
|
+
if (!tid) return
|
|
259
|
+
const key = interactionRowKey(row)
|
|
260
|
+
try {
|
|
261
|
+
contextAnchorIndex = null
|
|
262
|
+
events = await fetchTraceEvents(tid)
|
|
263
|
+
if (layoutWide) {
|
|
264
|
+
splitSelectedKey = key
|
|
265
|
+
route = { screen: "interactions" }
|
|
266
|
+
} else {
|
|
267
|
+
splitSelectedKey = null
|
|
268
|
+
route = { screen: "events", traceId: tid }
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
loadError = String(e)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function openProblemDetail(ev: Record<string, unknown>): void {
|
|
276
|
+
const tid = String(ev.trace_id ?? "")
|
|
277
|
+
route = { screen: "detail", traceId: tid, event: ev, detailFrom: "problems" }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function selectEvent(ev: Record<string, unknown>, indexInFiltered: number): void {
|
|
281
|
+
const tid = String(ev.trace_id ?? "")
|
|
282
|
+
contextAnchorIndex = indexInFiltered
|
|
283
|
+
if (route.screen === "events") {
|
|
284
|
+
route = { screen: "detail", traceId: route.traceId, event: ev, detailFrom: "events" }
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
if (route.screen === "interactions" && layoutWide && splitSelectedKey) {
|
|
288
|
+
route = { screen: "detail", traceId: tid, event: ev, detailFrom: "events" }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function back(): void {
|
|
293
|
+
if (route.screen === "detail") {
|
|
294
|
+
if (route.detailFrom === "problems") {
|
|
295
|
+
route = { screen: "problems" }
|
|
296
|
+
} else if (layoutWide && splitSelectedKey) {
|
|
297
|
+
route = { screen: "interactions" }
|
|
298
|
+
} else {
|
|
299
|
+
route = { screen: "events", traceId: route.traceId }
|
|
300
|
+
}
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
if (route.screen === "events") {
|
|
304
|
+
splitSelectedKey = null
|
|
305
|
+
route = { screen: "interactions" }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function fabClick(e: MouseEvent): void {
|
|
310
|
+
const el = e.currentTarget
|
|
311
|
+
if (panelOpen) {
|
|
312
|
+
if (el instanceof HTMLButtonElement) panelCircleOrigin = originFromFabButton(el)
|
|
313
|
+
panelOpen = false
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
if (el instanceof HTMLButtonElement) {
|
|
317
|
+
panelCircleOrigin = originFromFabButton(el)
|
|
318
|
+
} else {
|
|
319
|
+
panelCircleOrigin = originFallbackFabCorner()
|
|
320
|
+
}
|
|
321
|
+
panelOpen = true
|
|
322
|
+
splitSelectedKey = null
|
|
323
|
+
const { err } = countsFromRecent(recentEvents)
|
|
324
|
+
if (err > 0) {
|
|
325
|
+
route = { screen: "problems" }
|
|
326
|
+
} else {
|
|
327
|
+
histogramTimeRange = null
|
|
328
|
+
route = { screen: "interactions" }
|
|
329
|
+
}
|
|
330
|
+
void loadInteractionsList()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function copyText(text: string): Promise<void> {
|
|
334
|
+
try {
|
|
335
|
+
await navigator.clipboard.writeText(text)
|
|
336
|
+
} catch {
|
|
337
|
+
/* ignore */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function copyDetailJson(): Promise<void> {
|
|
342
|
+
if (route.screen !== "detail") return
|
|
343
|
+
await copyText(JSON.stringify(route.event, null, 2))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function copyDetailTraceId(): Promise<void> {
|
|
347
|
+
if (route.screen !== "detail") return
|
|
348
|
+
const tid = String(route.event.trace_id ?? "")
|
|
349
|
+
if (tid) await copyText(tid)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function copyDetailRequestId(): Promise<void> {
|
|
353
|
+
if (route.screen !== "detail") return
|
|
354
|
+
const m = route.event.metadata as Record<string, unknown> | undefined
|
|
355
|
+
const rid = m && typeof m.request_id === "string" ? m.request_id : ""
|
|
356
|
+
if (rid) await copyText(rid)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function toggleRowExpand(key: string, e: MouseEvent): void {
|
|
360
|
+
e.stopPropagation()
|
|
361
|
+
rowExpanded = { ...rowExpanded, [key]: !rowExpanded[key] }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function payloadSummary(ev: Record<string, unknown>): string {
|
|
365
|
+
const p = ev.payload
|
|
366
|
+
if (p && typeof p === "object" && !Array.isArray(p)) {
|
|
367
|
+
const o = p as Record<string, unknown>
|
|
368
|
+
const msg = o.message
|
|
369
|
+
if (typeof msg === "string" && msg.length > 0) {
|
|
370
|
+
return msg.length > 140 ? `${msg.slice(0, 137)}…` : msg
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const s = JSON.stringify(p)
|
|
375
|
+
return s.length > 120 ? `${s.slice(0, 117)}…` : s
|
|
376
|
+
} catch {
|
|
377
|
+
return ""
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isContextNeighbor(indexInFiltered: number): boolean {
|
|
382
|
+
if (contextAnchorIndex === null) return false
|
|
383
|
+
if (!activeTraceId) return false
|
|
384
|
+
return Math.abs(indexInFiltered - contextAnchorIndex) <= CONTEXT_RADIUS
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let panelTitle = $derived.by(() => {
|
|
388
|
+
if (route.screen === "problems") return "e11y — problems"
|
|
389
|
+
if (route.screen === "interactions") return "e11y — interactions"
|
|
390
|
+
if (route.screen === "events") return `e11y — trace ${route.traceId}`
|
|
391
|
+
if (route.screen === "detail") return `e11y — ${String(route.event.event_name ?? "event")}`
|
|
392
|
+
return "e11y"
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
let badgeLabel = $derived.by(() => {
|
|
396
|
+
const { total, err, warn } = countsFromRecent(recentEvents)
|
|
397
|
+
if (total === 0) return "e11y"
|
|
398
|
+
const parts: string[] = [`e11y ${total}`]
|
|
399
|
+
if (warn) parts.push(`${warn}⚠`)
|
|
400
|
+
if (err) parts.push(`${err}✕`)
|
|
401
|
+
return parts.join(" · ")
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
let fabStateClass = $derived.by((): "" | "e11y-fab--state-warn" | "e11y-fab--state-err" => {
|
|
405
|
+
const { err, warn } = countsFromRecent(recentEvents)
|
|
406
|
+
if (err) return "e11y-fab--state-err"
|
|
407
|
+
if (warn) return "e11y-fab--state-warn"
|
|
408
|
+
return ""
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
let fabPulseClass = $derived.by((): "" | "e11y-fab--pulse-error" | "e11y-fab--pulse-warn" => {
|
|
412
|
+
if (pulseKind === "error") return "e11y-fab--pulse-error"
|
|
413
|
+
if (pulseKind === "warn") return "e11y-fab--pulse-warn"
|
|
414
|
+
return ""
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
let tabProblemsActive = $derived.by(
|
|
418
|
+
() => route.screen === "problems" || (route.screen === "detail" && route.detailFrom === "problems")
|
|
419
|
+
)
|
|
420
|
+
let tabInteractionsActive = $derived.by(
|
|
421
|
+
() =>
|
|
422
|
+
route.screen === "interactions" ||
|
|
423
|
+
route.screen === "events" ||
|
|
424
|
+
(route.screen === "detail" && route.detailFrom === "events")
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
let showTraceFilters = $derived.by(
|
|
428
|
+
() =>
|
|
429
|
+
route.screen === "events" ||
|
|
430
|
+
(route.screen === "interactions" && layoutWide && !!splitSelectedKey)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
let errCount = $derived.by(() => countsFromRecent(recentEvents).err)
|
|
434
|
+
|
|
435
|
+
function sevClass(sev: unknown): string {
|
|
436
|
+
const s = String(sev ?? "info")
|
|
437
|
+
if (s === "error" || s === "fatal") return "e11y-sev--error"
|
|
438
|
+
if (s === "warn") return "e11y-sev--warn"
|
|
439
|
+
return "e11y-sev--info"
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function interactionRowKey(row: Record<string, unknown>): string {
|
|
443
|
+
const ids = (row.trace_ids as string[] | undefined) ?? []
|
|
444
|
+
return `${row.started_at ?? ""}|${ids.join(",")}`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function sourcePillClass(src: unknown): string {
|
|
448
|
+
const s = String(src ?? "")
|
|
449
|
+
if (s === "web") return "e11y-pill e11y-pill--web"
|
|
450
|
+
if (s === "job") return "e11y-pill e11y-pill--job"
|
|
451
|
+
return "e11y-pill"
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
$effect(() => {
|
|
455
|
+
const id = setInterval(() => void pollRecent(), POLL_MS)
|
|
456
|
+
void pollRecent()
|
|
457
|
+
return () => clearInterval(id)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
$effect(() => {
|
|
461
|
+
if (!panelOpen) return
|
|
462
|
+
void loadInteractionsList()
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
$effect(() => {
|
|
466
|
+
source
|
|
467
|
+
if (panelOpen) void loadInteractionsList()
|
|
468
|
+
})
|
|
469
|
+
</script>
|
|
470
|
+
|
|
471
|
+
<div class="e11y-dt">
|
|
472
|
+
<Fab label={badgeLabel} onclick={fabClick} stateClass={fabStateClass} pulseClass={fabPulseClass} />
|
|
473
|
+
|
|
474
|
+
<FullscreenPanel
|
|
475
|
+
open={panelOpen}
|
|
476
|
+
onclose={() => (panelOpen = false)}
|
|
477
|
+
origin={panelCircleOrigin}
|
|
478
|
+
>
|
|
479
|
+
{#snippet headerTopLeft()}
|
|
480
|
+
{#if route.screen === "events" || route.screen === "detail"}
|
|
481
|
+
<button type="button" class="e11y-icon-btn" onclick={back} aria-label="Back" title="Go back" style="margin-right: -4px;">
|
|
482
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
|
483
|
+
</button>
|
|
484
|
+
{/if}
|
|
485
|
+
|
|
486
|
+
<div class="e11y-tab-row" role="tablist">
|
|
487
|
+
<button
|
|
488
|
+
type="button"
|
|
489
|
+
role="tab"
|
|
490
|
+
class="e11y-tab"
|
|
491
|
+
class:e11y-tab--active={tabProblemsActive}
|
|
492
|
+
aria-selected={tabProblemsActive}
|
|
493
|
+
onclick={goTabProblems}
|
|
494
|
+
>
|
|
495
|
+
Problems{#if errCount > 0} <span class="e11y-tab-badge">{errCount}</span>{/if}
|
|
496
|
+
</button>
|
|
497
|
+
<button
|
|
498
|
+
type="button"
|
|
499
|
+
role="tab"
|
|
500
|
+
class="e11y-tab"
|
|
501
|
+
class:e11y-tab--active={tabInteractionsActive}
|
|
502
|
+
aria-selected={tabInteractionsActive}
|
|
503
|
+
onclick={goTabInteractions}
|
|
504
|
+
>
|
|
505
|
+
Interactions
|
|
506
|
+
</button>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
{#if route.screen === "events" || route.screen === "detail"}
|
|
510
|
+
<span class="e11y-breadcrumb-sep">/</span>
|
|
511
|
+
<span class="e11y-panel-title e11y-panel-title--sub" title={activeTraceId || undefined}>
|
|
512
|
+
{#if route.screen === "detail"}
|
|
513
|
+
Event Details
|
|
514
|
+
{:else}
|
|
515
|
+
Trace: {activeTraceId ? activeTraceId.slice(0, 8) + "…" : "Unknown"}
|
|
516
|
+
{/if}
|
|
517
|
+
</span>
|
|
518
|
+
{/if}
|
|
519
|
+
{/snippet}
|
|
520
|
+
|
|
521
|
+
{#snippet headerTopRight()}
|
|
522
|
+
{#if route.screen === "detail"}
|
|
523
|
+
<button type="button" class="e11y-btn" onclick={() => void copyDetailJson()}>Copy JSON</button>
|
|
524
|
+
<button type="button" class="e11y-btn" onclick={() => void copyDetailTraceId()}>Copy trace_id</button>
|
|
525
|
+
<button type="button" class="e11y-btn" onclick={() => void copyDetailRequestId()}>Copy request_id</button>
|
|
526
|
+
{:else if tabInteractionsActive && route.screen === "interactions"}
|
|
527
|
+
<div class="e11y-chip-row e11y-chip-row--header">
|
|
528
|
+
{#each ["web", "job", "all"] as s (s)}
|
|
529
|
+
<button
|
|
530
|
+
type="button"
|
|
531
|
+
class="e11y-chip"
|
|
532
|
+
class:e11y-chip--active={source === s}
|
|
533
|
+
onclick={() => (source = s as SourceFilter)}
|
|
534
|
+
>
|
|
535
|
+
{s}
|
|
536
|
+
</button>
|
|
537
|
+
{/each}
|
|
538
|
+
</div>
|
|
539
|
+
{/if}
|
|
540
|
+
{/snippet}
|
|
541
|
+
|
|
542
|
+
{#snippet headerBottom()}
|
|
543
|
+
<FilterBar
|
|
544
|
+
bind:search={globalSearch}
|
|
545
|
+
bind:severity={globalSeverity}
|
|
546
|
+
placeholder={
|
|
547
|
+
route.screen === "interactions"
|
|
548
|
+
? "Search paths, methods, traces…"
|
|
549
|
+
: route.screen === "problems"
|
|
550
|
+
? "Search problems, traces…"
|
|
551
|
+
: "Filter name, trace, payload…"
|
|
552
|
+
}
|
|
553
|
+
/>
|
|
554
|
+
{/snippet}
|
|
555
|
+
|
|
556
|
+
{#snippet children()}
|
|
557
|
+
{#if loadError}
|
|
558
|
+
<p class="e11y-err-msg">{loadError}</p>
|
|
559
|
+
{/if}
|
|
560
|
+
|
|
561
|
+
{#if showTraceFilters}
|
|
562
|
+
{#if contextAnchorIndex !== null}
|
|
563
|
+
<p class="e11y-context-hint">
|
|
564
|
+
Context: ±{CONTEXT_RADIUS} rows around last opened event (change filter to clear).
|
|
565
|
+
</p>
|
|
566
|
+
{/if}
|
|
567
|
+
{/if}
|
|
568
|
+
|
|
569
|
+
{#if route.screen === "problems"}
|
|
570
|
+
<RecentHistogram bind:timeRange={histogramTimeRange} recent={recentEvents} />
|
|
571
|
+
<p class="e11y-problems-hint">Recent error / fatal events from the dev log (newest first).</p>
|
|
572
|
+
{#if problemEvents.length === 0}
|
|
573
|
+
<p class="e11y-muted e11y-empty">No errors in the recent buffer.</p>
|
|
574
|
+
<button type="button" class="e11y-btn" onclick={goTabInteractions}>Open interactions</button>
|
|
575
|
+
{:else if filteredProblemEvents.length === 0}
|
|
576
|
+
<p class="e11y-muted e11y-empty">
|
|
577
|
+
{#if histogramTimeRange && !globalSearch.trim()}
|
|
578
|
+
No errors in the selected time range. Widen the selection or clear it.
|
|
579
|
+
{:else if globalSearch.trim()}
|
|
580
|
+
No matches for this search (and current time range, if any).
|
|
581
|
+
{:else}
|
|
582
|
+
No matching errors.
|
|
583
|
+
{/if}
|
|
584
|
+
</p>
|
|
585
|
+
{:else}
|
|
586
|
+
{#each filteredProblemEvents as ev, i (eventKey(ev, i))}
|
|
587
|
+
{@const tr = String(ev.trace_id ?? "")}
|
|
588
|
+
{@const ek = eventKey(ev, i)}
|
|
589
|
+
{@const sum = payloadSummary(ev)}
|
|
590
|
+
<div
|
|
591
|
+
class="e11y-row e11y-row--problem"
|
|
592
|
+
role="button"
|
|
593
|
+
tabindex="0"
|
|
594
|
+
onclick={() => openProblemDetail(ev)}
|
|
595
|
+
onkeydown={(e) => e.key === "Enter" && openProblemDetail(ev)}
|
|
596
|
+
>
|
|
597
|
+
<button
|
|
598
|
+
type="button"
|
|
599
|
+
class="e11y-row-expand"
|
|
600
|
+
class:e11y-row-expand--open={rowExpanded[ek]}
|
|
601
|
+
aria-expanded={!!rowExpanded[ek]}
|
|
602
|
+
aria-label={rowExpanded[ek] ? "Collapse row" : "Expand row"}
|
|
603
|
+
onclick={(e) => toggleRowExpand(ek, e)}>▸</button
|
|
604
|
+
>
|
|
605
|
+
<span class="e11y-sev {sevClass(ev.severity)}">{String(ev.severity ?? "?")}</span>
|
|
606
|
+
<span class="e11y-row-title">{String(ev.event_name ?? "")}</span>
|
|
607
|
+
<span class="e11y-row-meta e11y-mono" title={tr || undefined}
|
|
608
|
+
>{tr.length > 14 ? `${tr.slice(0, 12)}…` : tr || "—"}</span
|
|
609
|
+
>
|
|
610
|
+
<span class="e11y-row-meta">{String(ev.timestamp ?? "")}</span>
|
|
611
|
+
{#if rowExpanded[ek]}
|
|
612
|
+
<div class="e11y-row-body">
|
|
613
|
+
{#if sum}<p class="e11y-row-sum">{sum}</p>{/if}
|
|
614
|
+
<pre class="e11y-row-pre">{JSON.stringify(ev.payload, null, 2)}</pre>
|
|
615
|
+
</div>
|
|
616
|
+
{/if}
|
|
617
|
+
</div>
|
|
618
|
+
{/each}
|
|
619
|
+
{/if}
|
|
620
|
+
{:else if route.screen === "interactions"}
|
|
621
|
+
<InteractionsTimeline bind:timeRange={interactionsTimeRange} {interactions} />
|
|
622
|
+
{#if layoutWide}
|
|
623
|
+
<div class="e11y-split">
|
|
624
|
+
<div class="e11y-split-primary">
|
|
625
|
+
{#if interactions.length === 0}
|
|
626
|
+
<p class="e11y-muted e11y-empty">No interactions recorded yet.</p>
|
|
627
|
+
{:else if filteredInteractions.length === 0}
|
|
628
|
+
<p class="e11y-muted e11y-empty">No interactions in the selected time range.</p>
|
|
629
|
+
{:else}
|
|
630
|
+
{#each filteredInteractions as row (interactionRowKey(row))}
|
|
631
|
+
{@const ids = (row.trace_ids as string[] | undefined) ?? []}
|
|
632
|
+
{@const tc = Number(row.traces_count ?? ids.length)}
|
|
633
|
+
{@const { absolute, relative } = formatInteractionStarted(String(row.started_at ?? ""))}
|
|
634
|
+
{@const { primary, extra, preview } = summarizeTraceIds(ids)}
|
|
635
|
+
{@const ikey = interactionRowKey(row)}
|
|
636
|
+
<div
|
|
637
|
+
class="e11y-ix"
|
|
638
|
+
class:e11y-ix--error={!!row.has_error}
|
|
639
|
+
class:e11y-ix--selected={splitSelectedKey === ikey}
|
|
640
|
+
role="button"
|
|
641
|
+
tabindex="0"
|
|
642
|
+
onclick={() => void onInteractionRowClick(row)}
|
|
643
|
+
onkeydown={(e) => e.key === "Enter" && void onInteractionRowClick(row)}
|
|
644
|
+
>
|
|
645
|
+
<div class="e11y-ix-main">
|
|
646
|
+
<div class="e11y-ix-time">
|
|
647
|
+
<span class="e11y-ix-time-abs">{absolute}</span>
|
|
648
|
+
{#if relative}
|
|
649
|
+
<span class="e11y-ix-time-rel">{relative}</span>
|
|
650
|
+
{/if}
|
|
651
|
+
</div>
|
|
652
|
+
<div class="e11y-ix-trace-line">
|
|
653
|
+
<code class="e11y-ix-trace-primary">{primary}</code>
|
|
654
|
+
{#if extra > 0}
|
|
655
|
+
<span class="e11y-muted">+{extra}</span>
|
|
656
|
+
{/if}
|
|
657
|
+
</div>
|
|
658
|
+
{#if preview && ids.length > 1}
|
|
659
|
+
<div class="e11y-ix-preview">{preview}</div>
|
|
660
|
+
{/if}
|
|
661
|
+
</div>
|
|
662
|
+
<div class="e11y-ix-aside">
|
|
663
|
+
<span class={sourcePillClass(row.source)}>{String(row.source ?? "?")}</span>
|
|
664
|
+
{#if row.has_error}
|
|
665
|
+
<span class="e11y-pill e11y-pill--err">err</span>
|
|
666
|
+
{/if}
|
|
667
|
+
<span class="e11y-ix-count">{tc}×</span>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
{/each}
|
|
671
|
+
{/if}
|
|
672
|
+
</div>
|
|
673
|
+
<div class="e11y-split-secondary">
|
|
674
|
+
{#if !splitSelectedKey}
|
|
675
|
+
<p class="e11y-split-placeholder">Select an interaction to see events.</p>
|
|
676
|
+
{:else if filteredTraceEvents.length === 0}
|
|
677
|
+
<p class="e11y-muted e11y-split-placeholder">No events match the current filter.</p>
|
|
678
|
+
{:else}
|
|
679
|
+
{#each filteredTraceEvents as ev, j (eventKey(ev, j))}
|
|
680
|
+
{@const ek = eventKey(ev, j)}
|
|
681
|
+
{@const sum = payloadSummary(ev)}
|
|
682
|
+
<div
|
|
683
|
+
class="e11y-row"
|
|
684
|
+
class:e11y-row--context={isContextNeighbor(j)}
|
|
685
|
+
role="button"
|
|
686
|
+
tabindex="0"
|
|
687
|
+
onclick={() => selectEvent(ev, j)}
|
|
688
|
+
onkeydown={(e) => e.key === "Enter" && selectEvent(ev, j)}
|
|
689
|
+
>
|
|
690
|
+
<button
|
|
691
|
+
type="button"
|
|
692
|
+
class="e11y-row-expand"
|
|
693
|
+
class:e11y-row-expand--open={rowExpanded[ek]}
|
|
694
|
+
aria-expanded={!!rowExpanded[ek]}
|
|
695
|
+
aria-label={rowExpanded[ek] ? "Collapse row" : "Expand row"}
|
|
696
|
+
onclick={(e) => toggleRowExpand(ek, e)}>▸</button
|
|
697
|
+
>
|
|
698
|
+
<span class="e11y-sev {sevClass(ev.severity)}">{String(ev.severity ?? "?")}</span>
|
|
699
|
+
<span class="e11y-row-title">{String(ev.event_name ?? "")}</span>
|
|
700
|
+
<span class="e11y-row-meta">{String(ev.timestamp ?? "")}</span>
|
|
701
|
+
{#if rowExpanded[ek]}
|
|
702
|
+
<div class="e11y-row-body">
|
|
703
|
+
{#if sum}<p class="e11y-row-sum">{sum}</p>{/if}
|
|
704
|
+
<pre class="e11y-row-pre">{JSON.stringify(ev.payload, null, 2)}</pre>
|
|
705
|
+
</div>
|
|
706
|
+
{/if}
|
|
707
|
+
</div>
|
|
708
|
+
{/each}
|
|
709
|
+
{/if}
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
{:else}
|
|
713
|
+
{#if interactions.length === 0}
|
|
714
|
+
<p class="e11y-muted e11y-empty">No interactions recorded yet.</p>
|
|
715
|
+
{:else if filteredInteractions.length === 0}
|
|
716
|
+
<p class="e11y-muted e11y-empty">No interactions in the selected time range.</p>
|
|
717
|
+
{:else}
|
|
718
|
+
{#each filteredInteractions as row (interactionRowKey(row))}
|
|
719
|
+
{@const ids = (row.trace_ids as string[] | undefined) ?? []}
|
|
720
|
+
{@const tc = Number(row.traces_count ?? ids.length)}
|
|
721
|
+
{@const { absolute, relative } = formatInteractionStarted(String(row.started_at ?? ""))}
|
|
722
|
+
{@const { primary, extra, preview } = summarizeTraceIds(ids)}
|
|
723
|
+
<div
|
|
724
|
+
class="e11y-ix"
|
|
725
|
+
class:e11y-ix--error={!!row.has_error}
|
|
726
|
+
role="button"
|
|
727
|
+
tabindex="0"
|
|
728
|
+
onclick={() => void onInteractionRowClick(row)}
|
|
729
|
+
onkeydown={(e) => e.key === "Enter" && void onInteractionRowClick(row)}
|
|
730
|
+
>
|
|
731
|
+
<div class="e11y-ix-main">
|
|
732
|
+
<div class="e11y-ix-time">
|
|
733
|
+
<span class="e11y-ix-time-abs">{absolute}</span>
|
|
734
|
+
{#if relative}
|
|
735
|
+
<span class="e11y-ix-time-rel">{relative}</span>
|
|
736
|
+
{/if}
|
|
737
|
+
</div>
|
|
738
|
+
<div class="e11y-ix-trace-line">
|
|
739
|
+
<code class="e11y-ix-trace-primary" title="First trace_id">{primary}</code>
|
|
740
|
+
{#if extra > 0}
|
|
741
|
+
<span class="e11y-muted">+{extra} parallel</span>
|
|
742
|
+
{/if}
|
|
743
|
+
</div>
|
|
744
|
+
{#if preview && ids.length > 1}
|
|
745
|
+
<div class="e11y-ix-preview" title="Trace ids in group">{preview}</div>
|
|
746
|
+
{/if}
|
|
747
|
+
<div class="e11y-ix-hint">Click → events for first trace</div>
|
|
748
|
+
</div>
|
|
749
|
+
<div class="e11y-ix-aside">
|
|
750
|
+
<span class={sourcePillClass(row.source)}>{String(row.source ?? "?")}</span>
|
|
751
|
+
{#if row.has_error}
|
|
752
|
+
<span class="e11y-pill e11y-pill--err">Has errors</span>
|
|
753
|
+
{:else}
|
|
754
|
+
<span class="e11y-pill e11y-pill--ok">Clean</span>
|
|
755
|
+
{/if}
|
|
756
|
+
<span class="e11y-ix-count" title="Traces in group">{tc} trace{tc === 1 ? "" : "s"}</span>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
{/each}
|
|
760
|
+
{/if}
|
|
761
|
+
{/if}
|
|
762
|
+
{:else if route.screen === "events"}
|
|
763
|
+
{#if filteredTraceEvents.length === 0}
|
|
764
|
+
<p class="e11y-muted e11y-empty">No events match the current filter.</p>
|
|
765
|
+
{:else}
|
|
766
|
+
{#each filteredTraceEvents as ev, i (eventKey(ev, i))}
|
|
767
|
+
{@const ek = eventKey(ev, i)}
|
|
768
|
+
{@const sum = payloadSummary(ev)}
|
|
769
|
+
<div
|
|
770
|
+
class="e11y-row"
|
|
771
|
+
class:e11y-row--context={isContextNeighbor(i)}
|
|
772
|
+
role="button"
|
|
773
|
+
tabindex="0"
|
|
774
|
+
onclick={() => selectEvent(ev, i)}
|
|
775
|
+
onkeydown={(e) => e.key === "Enter" && selectEvent(ev, i)}
|
|
776
|
+
>
|
|
777
|
+
<button
|
|
778
|
+
type="button"
|
|
779
|
+
class="e11y-row-expand"
|
|
780
|
+
class:e11y-row-expand--open={rowExpanded[ek]}
|
|
781
|
+
aria-expanded={!!rowExpanded[ek]}
|
|
782
|
+
aria-label={rowExpanded[ek] ? "Collapse row" : "Expand row"}
|
|
783
|
+
onclick={(e) => toggleRowExpand(ek, e)}>▸</button
|
|
784
|
+
>
|
|
785
|
+
<span class="e11y-sev {sevClass(ev.severity)}">{String(ev.severity ?? "?")}</span>
|
|
786
|
+
<span class="e11y-row-title">{String(ev.event_name ?? "")}</span>
|
|
787
|
+
<span class="e11y-row-meta">{String(ev.timestamp ?? "")}</span>
|
|
788
|
+
{#if rowExpanded[ek]}
|
|
789
|
+
<div class="e11y-row-body">
|
|
790
|
+
{#if sum}<p class="e11y-row-sum">{sum}</p>{/if}
|
|
791
|
+
<pre class="e11y-row-pre">{JSON.stringify(ev.payload, null, 2)}</pre>
|
|
792
|
+
</div>
|
|
793
|
+
{/if}
|
|
794
|
+
</div>
|
|
795
|
+
{/each}
|
|
796
|
+
{/if}
|
|
797
|
+
{:else if route.screen === "detail"}
|
|
798
|
+
{@const d = route.event}
|
|
799
|
+
{@const meta = d.metadata as Record<string, unknown> | undefined}
|
|
800
|
+
<div class="e11y-detail">
|
|
801
|
+
<dl class="e11y-detail-dl">
|
|
802
|
+
<dt>trace_id</dt>
|
|
803
|
+
<dd class="e11y-mono">{String(d.trace_id ?? "—")}</dd>
|
|
804
|
+
<dt>span_id</dt>
|
|
805
|
+
<dd class="e11y-mono">{String(d.span_id ?? "—")}</dd>
|
|
806
|
+
<dt>request_id</dt>
|
|
807
|
+
<dd class="e11y-mono">{String(meta?.request_id ?? "—")}</dd>
|
|
808
|
+
<dt>timestamp</dt>
|
|
809
|
+
<dd>{String(d.timestamp ?? "—")}</dd>
|
|
810
|
+
</dl>
|
|
811
|
+
<details class="e11y-details">
|
|
812
|
+
<summary>payload</summary>
|
|
813
|
+
<pre class="e11y-detail-pre">{JSON.stringify(d.payload, null, 2)}</pre>
|
|
814
|
+
</details>
|
|
815
|
+
<details class="e11y-details">
|
|
816
|
+
<summary>metadata</summary>
|
|
817
|
+
<pre class="e11y-detail-pre">{JSON.stringify(d.metadata ?? {}, null, 2)}</pre>
|
|
818
|
+
</details>
|
|
819
|
+
<details class="e11y-details">
|
|
820
|
+
<summary>full JSON</summary>
|
|
821
|
+
<pre class="e11y-detail-pre">{JSON.stringify(d, null, 2)}</pre>
|
|
822
|
+
</details>
|
|
823
|
+
</div>
|
|
824
|
+
{/if}
|
|
825
|
+
{/snippet}
|
|
826
|
+
</FullscreenPanel>
|
|
827
|
+
</div>
|