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,354 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { scaleBand } from "d3-scale"
|
|
3
|
+
import { Axis, Bars, Chart, Svg } from "layerchart"
|
|
4
|
+
import { buildRecentVolumeBuckets, type HistogramTimeRange } from "../lib/recentVolume"
|
|
5
|
+
|
|
6
|
+
type HistoRow = {
|
|
7
|
+
idx: number
|
|
8
|
+
t0: number
|
|
9
|
+
t1: number
|
|
10
|
+
e0: number
|
|
11
|
+
e1: number
|
|
12
|
+
w0: number
|
|
13
|
+
w1: number
|
|
14
|
+
r0: number
|
|
15
|
+
r1: number
|
|
16
|
+
total: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
recent = [] as Record<string, unknown>[],
|
|
21
|
+
timeRange = $bindable(null as HistogramTimeRange | null),
|
|
22
|
+
} = $props()
|
|
23
|
+
|
|
24
|
+
const COL_ERR = "var(--e11y-histo-err)"
|
|
25
|
+
const COL_WARN = "var(--e11y-histo-warn)"
|
|
26
|
+
const COL_REST = "var(--e11y-histo-ok)"
|
|
27
|
+
/** HTML shade above chart, under brush overlay (SVG rect was hidden below overlay). */
|
|
28
|
+
const COL_SEL_SHADE = "var(--e11y-sel-bg)"
|
|
29
|
+
|
|
30
|
+
const BAR_RADIUS = 3
|
|
31
|
+
const TWEEN = { duration: 420, easing: (t: number) => 1 - (1 - t) * (1 - t) }
|
|
32
|
+
|
|
33
|
+
/** Host height scales with peak bucket total (√ for large spikes). */
|
|
34
|
+
const HISTO_H_MIN = 120
|
|
35
|
+
const HISTO_H_MAX = 260
|
|
36
|
+
const HISTO_H_BASE = 96
|
|
37
|
+
const HISTO_H_PER_SQRT = 16
|
|
38
|
+
|
|
39
|
+
/** Must match Chart `padding` (Svg inner `<g>` is translated by this). */
|
|
40
|
+
const CHART_PAD = { top: 6, right: 6, bottom: 26, left: 4 } as const
|
|
41
|
+
|
|
42
|
+
let buckets = $derived(buildRecentVolumeBuckets(recent, 32))
|
|
43
|
+
|
|
44
|
+
let chartRows = $derived.by((): HistoRow[] => {
|
|
45
|
+
return buckets.map((b, idx) => {
|
|
46
|
+
const err = b.counts.err
|
|
47
|
+
const warn = b.counts.warn
|
|
48
|
+
const rest = b.counts.rest
|
|
49
|
+
const total = err + warn + rest
|
|
50
|
+
return {
|
|
51
|
+
idx,
|
|
52
|
+
t0: b.t0,
|
|
53
|
+
t1: b.t1,
|
|
54
|
+
e0: 0,
|
|
55
|
+
e1: err,
|
|
56
|
+
w0: err,
|
|
57
|
+
w1: err + warn,
|
|
58
|
+
r0: err + warn,
|
|
59
|
+
r1: total,
|
|
60
|
+
total,
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
let maxY = $derived(Math.max(1, ...chartRows.map((r) => r.total)))
|
|
66
|
+
|
|
67
|
+
let chartHostHeightPx = $derived(
|
|
68
|
+
Math.round(
|
|
69
|
+
Math.min(HISTO_H_MAX, Math.max(HISTO_H_MIN, HISTO_H_BASE + HISTO_H_PER_SQRT * Math.sqrt(maxY)))
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
let chartWidth = $state(0)
|
|
74
|
+
|
|
75
|
+
let xBand = $derived(
|
|
76
|
+
scaleBand<number>()
|
|
77
|
+
.domain(chartRows.map((r) => r.idx))
|
|
78
|
+
.range([0, Math.max(0, chartWidth - CHART_PAD.left - CHART_PAD.right)])
|
|
79
|
+
.paddingInner(0.18)
|
|
80
|
+
.paddingOuter(0.06)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
let tickIdxs = $derived.by(() => {
|
|
84
|
+
const n = chartRows.length
|
|
85
|
+
if (n === 0) return []
|
|
86
|
+
const want = [0, Math.floor(n / 4), Math.floor(n / 2), Math.floor((3 * n) / 4), n - 1]
|
|
87
|
+
return [...new Set(want.filter((i) => i >= 0 && i < n))]
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
let chartHost: HTMLDivElement | null = $state(null)
|
|
91
|
+
let brushOverlay: HTMLDivElement | null = $state(null)
|
|
92
|
+
let dragA = $state<number | null>(null)
|
|
93
|
+
let dragB = $state<number | null>(null)
|
|
94
|
+
let dragging = $state(false)
|
|
95
|
+
|
|
96
|
+
function formatTick(ms: number): string {
|
|
97
|
+
return new Date(ms).toISOString().slice(11, 23) + "Z"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** X in LayerChart inner `<g>` space (same as band scale output), robust to SVG/CSS transforms. */
|
|
101
|
+
function clientToInnerPlotX(clientX: number, clientY: number): number | null {
|
|
102
|
+
const root = chartHost
|
|
103
|
+
if (!root) return null
|
|
104
|
+
const svg = root.querySelector<SVGSVGElement>("svg.layercake-layout-svg")
|
|
105
|
+
const g = svg?.querySelector<SVGGElement>(".layercake-layout-svg_g")
|
|
106
|
+
if (!svg || !g) return null
|
|
107
|
+
const m = g.getScreenCTM()
|
|
108
|
+
if (!m) return null
|
|
109
|
+
return new DOMPoint(clientX, clientY).matrixTransform(m.inverse()).x
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function indexFromClientX(clientX: number, clientY: number): number {
|
|
113
|
+
const n = chartRows.length
|
|
114
|
+
if (n === 0) return 0
|
|
115
|
+
const root = chartHost
|
|
116
|
+
if (!root) return 0
|
|
117
|
+
const xInner = clientToInnerPlotX(clientX, clientY)
|
|
118
|
+
const bw = xBand.bandwidth()
|
|
119
|
+
if (xInner == null || !Number.isFinite(xInner) || bw <= 0) {
|
|
120
|
+
const svg = root.querySelector<SVGSVGElement>("svg.layercake-layout-svg")
|
|
121
|
+
const rect = svg?.getBoundingClientRect() ?? root.getBoundingClientRect()
|
|
122
|
+
const fallback = clientX - rect.left - CHART_PAD.left
|
|
123
|
+
return indexFromInnerX(fallback, n)
|
|
124
|
+
}
|
|
125
|
+
return indexFromInnerX(xInner, n)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function indexFromInnerX(xInner: number, n: number): number {
|
|
129
|
+
const bw = xBand.bandwidth()
|
|
130
|
+
for (let i = 0; i < n; i++) {
|
|
131
|
+
const x0 = xBand(i)
|
|
132
|
+
if (x0 === undefined) continue
|
|
133
|
+
if (xInner >= x0 && xInner < x0 + bw) return i
|
|
134
|
+
}
|
|
135
|
+
const first = xBand(0)
|
|
136
|
+
const last = xBand(n - 1)
|
|
137
|
+
if (first !== undefined && xInner < first) return 0
|
|
138
|
+
if (last !== undefined && xInner >= last + bw) return n - 1
|
|
139
|
+
let nearest = 0
|
|
140
|
+
let best = Infinity
|
|
141
|
+
for (let i = 0; i < n; i++) {
|
|
142
|
+
const x0 = xBand(i)
|
|
143
|
+
if (x0 === undefined) continue
|
|
144
|
+
const mid = x0 + bw / 2
|
|
145
|
+
const d = Math.abs(xInner - mid)
|
|
146
|
+
if (d < best) {
|
|
147
|
+
best = d
|
|
148
|
+
nearest = i
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return nearest
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function commitRange(i0: number, i1: number): void {
|
|
155
|
+
const b = buckets
|
|
156
|
+
const lo = Math.min(i0, i1)
|
|
157
|
+
const hi = Math.max(i0, i1)
|
|
158
|
+
timeRange = { startMs: b[lo].t0, endMs: b[hi].t1, lo, hi }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function onPointerDown(e: PointerEvent): void {
|
|
162
|
+
if (e.button !== 0) return
|
|
163
|
+
if (chartRows.length === 0) return
|
|
164
|
+
const cap = brushOverlay ?? chartHost
|
|
165
|
+
if (!cap) return
|
|
166
|
+
try {
|
|
167
|
+
cap.setPointerCapture(e.pointerId)
|
|
168
|
+
} catch {
|
|
169
|
+
/* ignore */
|
|
170
|
+
}
|
|
171
|
+
dragging = true
|
|
172
|
+
const i = indexFromClientX(e.clientX, e.clientY)
|
|
173
|
+
dragA = i
|
|
174
|
+
dragB = i
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function onPointerMove(e: PointerEvent): void {
|
|
178
|
+
if (!dragging || dragA === null) return
|
|
179
|
+
dragB = indexFromClientX(e.clientX, e.clientY)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function onPointerUp(e: PointerEvent): void {
|
|
183
|
+
const cap = brushOverlay ?? chartHost
|
|
184
|
+
if (!cap) return
|
|
185
|
+
try {
|
|
186
|
+
cap.releasePointerCapture(e.pointerId)
|
|
187
|
+
} catch {
|
|
188
|
+
/* ignore */
|
|
189
|
+
}
|
|
190
|
+
if (dragging && dragA !== null && dragB !== null) {
|
|
191
|
+
commitRange(dragA, dragB)
|
|
192
|
+
}
|
|
193
|
+
dragging = false
|
|
194
|
+
dragA = null
|
|
195
|
+
dragB = null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function onDoubleClick(): void {
|
|
199
|
+
timeRange = null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function clearRange(): void {
|
|
203
|
+
timeRange = null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function selIndexRange(): { lo: number; hi: number } | null {
|
|
207
|
+
if (dragging && dragA !== null && dragB !== null) {
|
|
208
|
+
return { lo: Math.min(dragA, dragB), hi: Math.max(dragA, dragB) }
|
|
209
|
+
}
|
|
210
|
+
if (!timeRange || buckets.length === 0) return null
|
|
211
|
+
const n = buckets.length
|
|
212
|
+
if (timeRange.lo == null || timeRange.hi == null) return null
|
|
213
|
+
const rawLo = Math.max(0, Math.min(timeRange.lo, n - 1))
|
|
214
|
+
const rawHi = Math.max(0, Math.min(timeRange.hi, n - 1))
|
|
215
|
+
return { lo: Math.min(rawLo, rawHi), hi: Math.max(rawLo, rawHi) }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let spanLabel = $derived.by((): string | null => {
|
|
219
|
+
if (buckets.length === 0) return null
|
|
220
|
+
const first = buckets[0]
|
|
221
|
+
const last = buckets[buckets.length - 1]
|
|
222
|
+
return `${formatTick(first.t0)} → ${formatTick(last.t1)}`
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
let selectionLayout = $derived.by((): { left: number; top: number; width: number; height: number } | null => {
|
|
226
|
+
const sel = selIndexRange()
|
|
227
|
+
if (!sel) return null
|
|
228
|
+
const bw = xBand.bandwidth()
|
|
229
|
+
const x0 = xBand(sel.lo)
|
|
230
|
+
const x1 = (xBand(sel.hi) ?? 0) + bw
|
|
231
|
+
if (x0 === undefined) return null
|
|
232
|
+
const plotH = chartHostHeightPx - CHART_PAD.top - CHART_PAD.bottom
|
|
233
|
+
return {
|
|
234
|
+
left: CHART_PAD.left + x0,
|
|
235
|
+
top: CHART_PAD.top,
|
|
236
|
+
width: Math.max(0, x1 - x0),
|
|
237
|
+
height: Math.max(0, plotH),
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
{#if chartRows.length > 0}
|
|
243
|
+
<div class="e11y-histo-wrap">
|
|
244
|
+
<div
|
|
245
|
+
bind:this={chartHost}
|
|
246
|
+
bind:clientWidth={chartWidth}
|
|
247
|
+
class="e11y-histo-chart-host"
|
|
248
|
+
style:height="{chartHostHeightPx}px"
|
|
249
|
+
style:min-height="{chartHostHeightPx}px"
|
|
250
|
+
role="application"
|
|
251
|
+
aria-label="Log volume by time (LayerChart). Drag to filter. Double-click resets."
|
|
252
|
+
>
|
|
253
|
+
<Chart
|
|
254
|
+
data={chartRows}
|
|
255
|
+
x="idx"
|
|
256
|
+
xDomain={chartRows.map((r) => r.idx)}
|
|
257
|
+
xScale={xBand}
|
|
258
|
+
y="total"
|
|
259
|
+
yDomain={[0, maxY]}
|
|
260
|
+
yNice={false}
|
|
261
|
+
padding={{ top: CHART_PAD.top, right: CHART_PAD.right, bottom: CHART_PAD.bottom, left: CHART_PAD.left }}
|
|
262
|
+
brush={{ disabled: true }}
|
|
263
|
+
>
|
|
264
|
+
<Svg class="e11y-histo-svg" label="Recent log volume">
|
|
265
|
+
<!-- LayerChart rect helper only reads range from `y` when it is [low, high]; plain y/y1 leaves y1 ignored. -->
|
|
266
|
+
<Bars
|
|
267
|
+
data={chartRows}
|
|
268
|
+
y={(d: HistoRow) => [d.e0, d.e1]}
|
|
269
|
+
fill={COL_ERR}
|
|
270
|
+
stroke="none"
|
|
271
|
+
strokeWidth={0}
|
|
272
|
+
radius={0}
|
|
273
|
+
rounded="none"
|
|
274
|
+
tweened={TWEEN}
|
|
275
|
+
/>
|
|
276
|
+
<Bars
|
|
277
|
+
data={chartRows}
|
|
278
|
+
y={(d: HistoRow) => [d.w0, d.w1]}
|
|
279
|
+
fill={COL_WARN}
|
|
280
|
+
stroke="none"
|
|
281
|
+
strokeWidth={0}
|
|
282
|
+
radius={0}
|
|
283
|
+
rounded="none"
|
|
284
|
+
tweened={TWEEN}
|
|
285
|
+
/>
|
|
286
|
+
<Bars
|
|
287
|
+
data={chartRows}
|
|
288
|
+
y={(d: HistoRow) => [d.r0, d.r1]}
|
|
289
|
+
fill={COL_REST}
|
|
290
|
+
stroke="none"
|
|
291
|
+
strokeWidth={0}
|
|
292
|
+
radius={BAR_RADIUS}
|
|
293
|
+
rounded="top"
|
|
294
|
+
tweened={TWEEN}
|
|
295
|
+
/>
|
|
296
|
+
|
|
297
|
+
<Axis
|
|
298
|
+
placement="bottom"
|
|
299
|
+
rule={false}
|
|
300
|
+
grid={false}
|
|
301
|
+
ticks={tickIdxs.map((i) => chartRows[i]!.idx)}
|
|
302
|
+
format={(v) => {
|
|
303
|
+
const row = chartRows.find((r) => r.idx === v)
|
|
304
|
+
return row ? formatTick(row.t0) : ""
|
|
305
|
+
}}
|
|
306
|
+
tickLength={3}
|
|
307
|
+
tickLabelProps={{ class: "e11y-histo-axis-tick" }}
|
|
308
|
+
/>
|
|
309
|
+
</Svg>
|
|
310
|
+
</Chart>
|
|
311
|
+
{#if selectionLayout}
|
|
312
|
+
<div
|
|
313
|
+
class="e11y-histo-sel-shade"
|
|
314
|
+
style:left="{selectionLayout.left}px"
|
|
315
|
+
style:top="{selectionLayout.top}px"
|
|
316
|
+
style:width="{selectionLayout.width}px"
|
|
317
|
+
style:height="{selectionLayout.height}px"
|
|
318
|
+
aria-hidden="true"
|
|
319
|
+
></div>
|
|
320
|
+
{/if}
|
|
321
|
+
<div
|
|
322
|
+
bind:this={brushOverlay}
|
|
323
|
+
class="e11y-histo-brush-overlay"
|
|
324
|
+
aria-hidden="true"
|
|
325
|
+
onpointerdown={onPointerDown}
|
|
326
|
+
onpointermove={onPointerMove}
|
|
327
|
+
onpointerup={onPointerUp}
|
|
328
|
+
onpointercancel={onPointerUp}
|
|
329
|
+
ondblclick={onDoubleClick}
|
|
330
|
+
></div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<div class="e11y-histo-footer">
|
|
334
|
+
<div class="e11y-histo-legend">
|
|
335
|
+
<span><i class="e11y-histo-dot e11y-histo-dot--err"></i> error</span>
|
|
336
|
+
<span><i class="e11y-histo-dot e11y-histo-dot--warn"></i> warn</span>
|
|
337
|
+
<span><i class="e11y-histo-dot e11y-histo-dot--rest"></i> other</span>
|
|
338
|
+
</div>
|
|
339
|
+
<div class="e11y-histo-meta">
|
|
340
|
+
{#if spanLabel}
|
|
341
|
+
<span class="e11y-histo-span" title="Full sample span (UTC)">{spanLabel}</span>
|
|
342
|
+
{/if}
|
|
343
|
+
{#if timeRange}
|
|
344
|
+
<span class="e11y-histo-filter" title="Active time filter (UTC)">
|
|
345
|
+
Filter: {formatTick(timeRange.startMs)}–{formatTick(timeRange.endMs)}
|
|
346
|
+
</span>
|
|
347
|
+
<button type="button" class="e11y-histo-clear" onclick={clearRange}>Clear range</button>
|
|
348
|
+
{:else}
|
|
349
|
+
<span class="e11y-histo-hint">Drag to narrow · double-click to reset</span>
|
|
350
|
+
{/if}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
{/if}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SourceFilter } from "./router"
|
|
2
|
+
|
|
3
|
+
function useMocks(): boolean {
|
|
4
|
+
return import.meta.env.DEV
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function fetchRecent(limit = 200): Promise<Record<string, unknown>[]> {
|
|
8
|
+
const url = useMocks()
|
|
9
|
+
? "/mocks/v1/events/recent.json"
|
|
10
|
+
: `/_e11y/v1/events/recent?limit=${limit}`
|
|
11
|
+
const r = await fetch(url)
|
|
12
|
+
if (!r.ok) throw new Error(`recent: ${r.status}`)
|
|
13
|
+
return r.json() as Promise<Record<string, unknown>[]>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function fetchInteractions(source: SourceFilter): Promise<Record<string, unknown>[]> {
|
|
17
|
+
const url = useMocks()
|
|
18
|
+
? "/mocks/v1/interactions.json"
|
|
19
|
+
: `/_e11y/v1/interactions${source === "all" ? "" : `?source=${source}`}`
|
|
20
|
+
const r = await fetch(url)
|
|
21
|
+
if (!r.ok) throw new Error(`interactions: ${r.status}`)
|
|
22
|
+
const rows = (await r.json()) as Record<string, unknown>[]
|
|
23
|
+
if (useMocks()) {
|
|
24
|
+
if (source === "all") return rows
|
|
25
|
+
return rows.filter((i) => i.source === source)
|
|
26
|
+
}
|
|
27
|
+
return rows
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchTraceEvents(traceId: string): Promise<Record<string, unknown>[]> {
|
|
31
|
+
const url = useMocks()
|
|
32
|
+
? `/mocks/v1/traces/${encodeURIComponent(traceId)}/events.json`
|
|
33
|
+
: `/_e11y/v1/traces/${encodeURIComponent(traceId)}/events`
|
|
34
|
+
const r = await fetch(url)
|
|
35
|
+
if (!r.ok) throw new Error(`trace events: ${r.status}`)
|
|
36
|
+
return r.json() as Promise<Record<string, unknown>[]>
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Stable key for deduping events across polls (pulse / badges). */
|
|
2
|
+
export function eventKey(e: Record<string, unknown>, index: number): string {
|
|
3
|
+
const id = e.id
|
|
4
|
+
if (typeof id === "string" && id.length > 0) return id
|
|
5
|
+
|
|
6
|
+
return [
|
|
7
|
+
String(e.trace_id ?? ""),
|
|
8
|
+
String(e.timestamp ?? ""),
|
|
9
|
+
String(e.event_name ?? ""),
|
|
10
|
+
index,
|
|
11
|
+
].join("|")
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Format ISO timestamp for list rows + short relative hint. */
|
|
2
|
+
export function formatInteractionStarted(iso: string): { absolute: string; relative: string } {
|
|
3
|
+
const d = new Date(iso)
|
|
4
|
+
if (Number.isNaN(d.getTime())) {
|
|
5
|
+
return { absolute: iso || "—", relative: "" }
|
|
6
|
+
}
|
|
7
|
+
const absolute = d.toLocaleString(undefined, {
|
|
8
|
+
month: "short",
|
|
9
|
+
day: "numeric",
|
|
10
|
+
hour: "2-digit",
|
|
11
|
+
minute: "2-digit",
|
|
12
|
+
second: "2-digit",
|
|
13
|
+
})
|
|
14
|
+
const sec = Math.round((Date.now() - d.getTime()) / 1000)
|
|
15
|
+
let relative = ""
|
|
16
|
+
if (sec < 60) relative = `${sec}s ago`
|
|
17
|
+
else if (sec < 3600) relative = `${Math.floor(sec / 60)}m ago`
|
|
18
|
+
else if (sec < 86400) relative = `${Math.floor(sec / 3600)}h ago`
|
|
19
|
+
else relative = `${Math.floor(sec / 86400)}d ago`
|
|
20
|
+
return { absolute, relative }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function summarizeTraceIds(ids: string[] | undefined): {
|
|
24
|
+
primary: string
|
|
25
|
+
extra: number
|
|
26
|
+
preview: string
|
|
27
|
+
} {
|
|
28
|
+
const list = ids ?? []
|
|
29
|
+
if (list.length === 0) {
|
|
30
|
+
return { primary: "—", extra: 0, preview: "" }
|
|
31
|
+
}
|
|
32
|
+
const primary = list[0] ?? "—"
|
|
33
|
+
const extra = Math.max(0, list.length - 1)
|
|
34
|
+
const preview =
|
|
35
|
+
list.length <= 2 ? list.join(", ") : `${list[0]}, ${list[1]} +${list.length - 2}`
|
|
36
|
+
return { primary, extra, preview }
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type ListSeverityFilter = "all" | "error" | "warn" | "rest"
|
|
2
|
+
|
|
3
|
+
/** error filter matches error + fatal */
|
|
4
|
+
export function eventMatchesSeverity(ev: Record<string, unknown>, filter: ListSeverityFilter): boolean {
|
|
5
|
+
if (filter === "all") return true
|
|
6
|
+
const s = String(ev.severity ?? "")
|
|
7
|
+
if (filter === "error") return s === "error" || s === "fatal"
|
|
8
|
+
if (filter === "warn") return s === "warn"
|
|
9
|
+
/* rest: debug, info, success, … */
|
|
10
|
+
return s !== "error" && s !== "fatal" && s !== "warn"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function eventMatchesSearch(ev: Record<string, unknown>, query: string): boolean {
|
|
14
|
+
const q = query.trim().toLowerCase()
|
|
15
|
+
if (!q) return true
|
|
16
|
+
if (String(ev.event_name ?? "").toLowerCase().includes(q)) return true
|
|
17
|
+
if (String(ev.trace_id ?? "").toLowerCase().includes(q)) return true
|
|
18
|
+
const meta = ev.metadata
|
|
19
|
+
if (meta && typeof meta === "object") {
|
|
20
|
+
try {
|
|
21
|
+
if (JSON.stringify(meta).toLowerCase().includes(q)) return true
|
|
22
|
+
} catch {
|
|
23
|
+
/* ignore */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const payload = ev.payload
|
|
27
|
+
if (payload !== undefined && payload !== null) {
|
|
28
|
+
try {
|
|
29
|
+
if (JSON.stringify(payload).toLowerCase().includes(q)) return true
|
|
30
|
+
} catch {
|
|
31
|
+
/* ignore */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function filterEventList(
|
|
38
|
+
rows: Record<string, unknown>[],
|
|
39
|
+
severity: ListSeverityFilter,
|
|
40
|
+
search: string
|
|
41
|
+
): Record<string, unknown>[] {
|
|
42
|
+
return rows.filter((ev) => eventMatchesSeverity(ev, severity) && eventMatchesSearch(ev, search))
|
|
43
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Stacked severity counts per time slice (for recent-events volume bar). */
|
|
2
|
+
|
|
3
|
+
/** Histogram brush: bucket indices + bounds (indices stay aligned with columns when `recent` changes). */
|
|
4
|
+
export type HistogramTimeRange = {
|
|
5
|
+
startMs: number
|
|
6
|
+
endMs: number
|
|
7
|
+
lo: number
|
|
8
|
+
hi: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type VolumeSeverity = "err" | "warn" | "rest"
|
|
12
|
+
|
|
13
|
+
export interface VolumeBucket {
|
|
14
|
+
/** Bucket start (ms since epoch). */
|
|
15
|
+
t0: number
|
|
16
|
+
t1: number
|
|
17
|
+
counts: Record<VolumeSeverity, number>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function severityVolumeGroup(sev: unknown): VolumeSeverity {
|
|
21
|
+
const s = String(sev ?? "")
|
|
22
|
+
if (s === "error" || s === "fatal") return "err"
|
|
23
|
+
if (s === "warn") return "warn"
|
|
24
|
+
return "rest"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Parse event `timestamp` (ISO string) to epoch ms. */
|
|
28
|
+
export function eventTimestampMs(ev: Record<string, unknown>): number | null {
|
|
29
|
+
const raw = ev.timestamp
|
|
30
|
+
if (typeof raw !== "string") return null
|
|
31
|
+
const ms = Date.parse(raw)
|
|
32
|
+
return Number.isFinite(ms) ? ms : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Newest-first input (as from API); builds ~`bucketCount` buckets from oldest→newest span. */
|
|
36
|
+
export function buildRecentVolumeBuckets(
|
|
37
|
+
rows: Record<string, unknown>[],
|
|
38
|
+
bucketCount = 28
|
|
39
|
+
): VolumeBucket[] {
|
|
40
|
+
if (rows.length === 0) return []
|
|
41
|
+
|
|
42
|
+
const times: number[] = []
|
|
43
|
+
for (const ev of rows) {
|
|
44
|
+
const t = eventTimestampMs(ev)
|
|
45
|
+
if (t != null) times.push(t)
|
|
46
|
+
}
|
|
47
|
+
if (times.length === 0) return []
|
|
48
|
+
|
|
49
|
+
let tMin = Math.min(...times)
|
|
50
|
+
let tMax = Math.max(...times)
|
|
51
|
+
if (tMax <= tMin) {
|
|
52
|
+
tMin -= 1
|
|
53
|
+
tMax += 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const n = Math.max(1, Math.min(bucketCount, Math.ceil(rows.length / 2) || bucketCount))
|
|
57
|
+
const width = (tMax - tMin) / n
|
|
58
|
+
const buckets: VolumeBucket[] = []
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
buckets.push({
|
|
61
|
+
t0: tMin + i * width,
|
|
62
|
+
t1: tMin + (i + 1) * width,
|
|
63
|
+
counts: { err: 0, warn: 0, rest: 0 },
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const ev of rows) {
|
|
68
|
+
const t = eventTimestampMs(ev)
|
|
69
|
+
if (t == null) continue
|
|
70
|
+
const idx = Math.min(n - 1, Math.max(0, Math.floor((t - tMin) / width)))
|
|
71
|
+
const g = severityVolumeGroup(ev.severity)
|
|
72
|
+
buckets[idx].counts[g] += 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return buckets
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function bucketTotal(b: VolumeBucket): number {
|
|
79
|
+
return b.counts.err + b.counts.warn + b.counts.rest
|
|
80
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type SourceFilter = "web" | "job" | "all"
|
|
2
|
+
|
|
3
|
+
export type OverlayRoute =
|
|
4
|
+
| { screen: "problems" }
|
|
5
|
+
| { screen: "interactions" }
|
|
6
|
+
| { screen: "events"; traceId: string }
|
|
7
|
+
| {
|
|
8
|
+
screen: "detail"
|
|
9
|
+
traceId: string
|
|
10
|
+
event: Record<string, unknown>
|
|
11
|
+
detailFrom: "problems" | "events"
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cubicInOut } from "svelte/easing"
|
|
2
|
+
import type { TransitionConfig } from "svelte/transition"
|
|
3
|
+
|
|
4
|
+
export type CircleOrigin = { x: number; y: number; r: number }
|
|
5
|
+
|
|
6
|
+
/** Circular reveal from (x,y) — same idea as Magic UI theme toggler (clip-path expand). */
|
|
7
|
+
export function circleExpand(
|
|
8
|
+
_node: Element,
|
|
9
|
+
{ x, y, r, duration }: CircleOrigin & { duration: number }
|
|
10
|
+
): TransitionConfig {
|
|
11
|
+
return {
|
|
12
|
+
duration,
|
|
13
|
+
easing: cubicInOut,
|
|
14
|
+
css: (t) => {
|
|
15
|
+
const radius = Math.max(0, t * r)
|
|
16
|
+
return `clip-path: circle(${radius}px at ${x}px ${y}px);`
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Circular collapse back toward (x,y). */
|
|
22
|
+
export function circleCollapse(
|
|
23
|
+
_node: Element,
|
|
24
|
+
{ x, y, r, duration }: CircleOrigin & { duration: number }
|
|
25
|
+
): TransitionConfig {
|
|
26
|
+
return {
|
|
27
|
+
duration,
|
|
28
|
+
easing: cubicInOut,
|
|
29
|
+
css: (t) => {
|
|
30
|
+
const radius = Math.max(0, t * r)
|
|
31
|
+
return `clip-path: circle(${radius}px at ${x}px ${y}px);`
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CircleOrigin } from "./transitions"
|
|
2
|
+
|
|
3
|
+
/** Circle origin from FAB button (center + radius to farthest viewport corner). */
|
|
4
|
+
export function originFromFabButton(el: HTMLButtonElement): CircleOrigin {
|
|
5
|
+
const { top, left, width, height } = el.getBoundingClientRect()
|
|
6
|
+
const x = left + width / 2
|
|
7
|
+
const y = top + height / 2
|
|
8
|
+
const vw = typeof window !== "undefined" ? (window.visualViewport?.width ?? window.innerWidth) : 800
|
|
9
|
+
const vh = typeof window !== "undefined" ? (window.visualViewport?.height ?? window.innerHeight) : 600
|
|
10
|
+
const r = Math.hypot(Math.max(x, vw - x), Math.max(y, vh - y))
|
|
11
|
+
return { x, y, r }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Fallback when opening without a click target (e.g. programmatic). ~bottom-right FAB. */
|
|
15
|
+
export function originFallbackFabCorner(): CircleOrigin {
|
|
16
|
+
const vw = typeof window !== "undefined" ? (window.visualViewport?.width ?? window.innerWidth) : 800
|
|
17
|
+
const vh = typeof window !== "undefined" ? (window.visualViewport?.height ?? window.innerHeight) : 600
|
|
18
|
+
const margin = 16
|
|
19
|
+
const halfW = 70
|
|
20
|
+
const halfH = 22
|
|
21
|
+
const x = vw - margin - halfW
|
|
22
|
+
const y = vh - margin - halfH
|
|
23
|
+
const r = Math.hypot(Math.max(x, vw - x), Math.max(y, vh - y))
|
|
24
|
+
return { x, y, r }
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mount } from "svelte"
|
|
2
|
+
import App from "./App.svelte"
|
|
3
|
+
import "./overlay.css"
|
|
4
|
+
|
|
5
|
+
const ROOT_ID = "e11y-devtools-root"
|
|
6
|
+
|
|
7
|
+
function boot(): void {
|
|
8
|
+
if (typeof document === "undefined") return
|
|
9
|
+
if (document.getElementById(ROOT_ID)) return
|
|
10
|
+
|
|
11
|
+
const target = document.createElement("div")
|
|
12
|
+
target.id = ROOT_ID
|
|
13
|
+
document.body.appendChild(target)
|
|
14
|
+
|
|
15
|
+
mount(App, { target })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof document !== "undefined") {
|
|
19
|
+
if (document.readyState === "loading") {
|
|
20
|
+
document.addEventListener("DOMContentLoaded", () => boot())
|
|
21
|
+
} else {
|
|
22
|
+
boot()
|
|
23
|
+
}
|
|
24
|
+
}
|