feat(alerts): first-run empty-state launcher + migrate alerts lists to Electric collections#192
Merged
Merged
Conversation
…o Electric collections Redesign the alerts overview zero-rules empty state into a "starter launcher": a purpose-built "quiet monitor" SVG (a steady signal held below a dashed amber threshold, reusing .infra-ref-line / .infra-pulse) replaces the generic bell-badge, and the five real starter templates surface as chart-*-tinted tiles that deep-link into a pre-filled create form. Adds a `template` search param to /alerts/create; deriveInitialRuleDraft pre-applies the matching preset and skips the first-touch overlay (unknown ids fall through to blank + overlay), reusing applyTemplate so the overlay, empty-state tiles, and create_alert_rule MCP tool resolve the same set. Also bundles the concurrent alerts-data migration to TanStack DB + Electric collections: useAlertDestinationsList and the alertDestinations/scrapeTargets collections, the electric-sync shape route updates, the settings + scrape-target sections wiring, and the 0011_electric_publication_wave1 drizzle migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
|
Your LLM provider API key was rejected. Rotate the key in your provider dashboard, then update the matching GitHub Actions secret. Update repo secret → · Model settings → · Setup docs → · Ask in Discord →
|
Ingest Rust Test + Benchmark ResultsCommit: Load Benchmark —
|
| Metric | main (median) | PR (median) | Delta |
|---|---|---|---|
| Requests/sec | 1941.39 | 1947.57 | +0.3% better |
| Rows/sec | 19413.94 | 19475.70 | +0.3% better |
| p50 latency | 31.72 ms | 31.80 ms | +0.2% worse |
| p95 latency | 37.96 ms | 37.83 ms | -0.3% better |
| p99 latency | 41.34 ms | 39.46 ms | -4.6% better |
| Export catch-up | 0.026 s | 0.026 s | -0.1% better |
| Max RSS | 100.61 MiB | 104.29 MiB | +3.7% worse |
| Failures | 0 | 0 | same |
Same code path on both sides (same LOAD_TEST_INGEST_MODE), so the delta column is meaningful. Numbers come from ubuntu-latest, which is noisy — treat single-digit-percent deltas as noise.
PR load benchmark JSON (per-iteration)
[
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 24,
"duration_seconds": 1.052165615,
"export_catchup_seconds": 0.026205813,
"request_rps": 1900.8414374005179,
"row_rps": 19008.41437400518,
"p50_ms": 32.542,
"p95_ms": 37.827,
"p99_ms": 40.121,
"max_rss_mb": 105.2265625,
"max_cpu_percent": 67.8,
"avg_cpu_percent": 50.333333333333336
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 25,
"duration_seconds": 1.02692088,
"export_catchup_seconds": 0.026295137,
"request_rps": 1947.5697095573712,
"row_rps": 19475.697095573712,
"p50_ms": 31.798,
"p95_ms": 38.423,
"p99_ms": 39.46,
"max_rss_mb": 104.28515625,
"max_cpu_percent": 67.8,
"avg_cpu_percent": 56.0
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 26,
"duration_seconds": 0.965432056,
"export_catchup_seconds": 0.026203633,
"request_rps": 2071.611344962426,
"row_rps": 20716.113449624256,
"p50_ms": 29.527,
"p95_ms": 37.278,
"p99_ms": 38.558,
"max_rss_mb": 98.3203125,
"max_cpu_percent": 71.4,
"avg_cpu_percent": 52.35
}
]main load benchmark JSON (per-iteration)
[
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 24,
"duration_seconds": 1.083577624,
"export_catchup_seconds": 0.026574747,
"request_rps": 1845.7376340211324,
"row_rps": 18457.376340211325,
"p50_ms": 33.036,
"p95_ms": 42.968,
"p99_ms": 45.334,
"max_rss_mb": 101.6796875,
"max_cpu_percent": 64.8,
"avg_cpu_percent": 48.53333333333334
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 26,
"duration_seconds": 1.030187432,
"export_catchup_seconds": 0.025617597,
"request_rps": 1941.3942918301882,
"row_rps": 19413.94291830188,
"p50_ms": 31.724,
"p95_ms": 37.956,
"p99_ms": 41.344,
"max_rss_mb": 100.60546875,
"max_cpu_percent": 69.6,
"avg_cpu_percent": 51.56666666666666
},
{
"ingest_mode": "tinybird",
"requests": 2000,
"successes": 2000,
"failures": 0,
"rows_sent": 20000,
"rows_exported": 20000,
"imports": 25,
"duration_seconds": 0.958913149,
"export_catchup_seconds": 0.026225924,
"request_rps": 2085.6946242584063,
"row_rps": 20856.94624258406,
"p50_ms": 29.285,
"p95_ms": 35.813,
"p99_ms": 38.655,
"max_rss_mb": 99.4765625,
"max_cpu_percent": 73.2,
"avg_cpu_percent": 44.900000000000006
}
]WAL-acked microbench (cargo bench --bench ingest_bench)
Compiling maple-ingest v0.1.0 (/home/runner/work/maple/maple/apps/ingest)
Finished `bench` profile [optimized] target(s) in 40.87s
Running benches/ingest_bench.rs (target/release/deps/ingest_bench-581d2100de893627)
Gnuplot not found, using plotters backend
test ingest_accept/logs_10_rows_wal_ack ... bench: 512991 ns/iter (+/- 29101)
test ingest_accept/traces_10_spans_wal_ack ... bench: 545127 ns/iter (+/- 38495)
cargo test
test telemetry::tests::metrics_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::metrics_summary_data_points_are_dropped ... ok
test telemetry::tests::migrate_legacy_shard_relocates_frames_into_lanes ... ok
test telemetry::tests::pipeline_can_start_for_clickhouse_only_without_tinybird_credentials ... ok
test telemetry::tests::clickhouse_export_drops_passworded_non_https_endpoint_without_sending ... ok
test telemetry::tests::pipeline_e2e_exports_gzip_ndjson_to_fake_tinybird ... ok
test telemetry::tests::pipeline_e2e_exports_traces_to_fake_tinybird ... ok
test telemetry::tests::sampling_keeps_errors_even_when_ratio_low ... ok
test telemetry::tests::scraper_contract::scraper_otlp_json_decodes_with_gateway_serde_and_encodes_to_rows ... ok
test telemetry::tests::signal_tag_round_trips_all_variants ... ok
test telemetry::tests::pipeline_e2e_exports_metrics_to_fake_tinybird ... ok
test telemetry::tests::telemetry_signal_as_str_is_canonical_lowercase ... ok
test telemetry::tests::timestamp_has_nano_precision ... ok
test telemetry::tests::timestamps_match_clickhouse_datetime64_nine_format ... ok
test telemetry::tests::trace_encoder_matches_tinybird_row_shape ... ok
test telemetry::tests::traces_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::wal_partial_drain_advances_cursor_without_truncating ... ok
test telemetry::tests::wal_round_trips_frame ... ok
test telemetry::tests::wal_truncates_after_full_drain_allowing_further_appends ... ok
test telemetry::tests::pipeline_exports_ready_org_to_clickhouse_without_tinybird_calls ... ok
test telemetry::tests::slow_clickhouse_lane_does_not_block_cosharded_tinybird_org ... ok
test telemetry::tests::clickhouse_breaker_sheds_after_threshold_failures ... ok
test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.78s
Running unittests src/bin/load_test.rs (target/debug/deps/load_test-661a0aa1eb3f6d6d)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/maple_ingest-c33bf80c577edb95)
running 38 tests
test autumn::tests::flat_hardcap_with_remaining_allows ... ok
test autumn::tests::allowed_only_no_balance_field ... ok
test autumn::tests::flat_hardcap_depleted_blocks ... ok
test autumn::tests::flat_overage_allows ... ok
test autumn::tests::flat_sub_one_gb_remaining_still_allows ... ok
test autumn::tests::flat_unlimited_allows ... ok
test autumn::tests::nested_balance_object_with_remaining_allows ... ok
test autumn::tests::nested_overage_allows ... ok
test autumn::tests::null_balance_no_subscription_blocks ... ok
test autumn::tests::unrecognized_shape_returns_none ... ok
test autumn::tests::nested_balance_object_depleted_blocks ... ok
test tests::api_error_kind_maps_status_to_stable_label ... ok
test tests::clickhouse_destination_is_terminal_in_dual_mode ... ok
test tests::api_error_from_pipeline_maps_variants_to_status ... ok
test tests::clickhouse_destination_uses_native_pipeline_even_in_forward_mode ... ok
test tests::clickhouse_target_resolver_decrypts_current_schema_password ... ok
test tests::clickhouse_target_resolver_rejects_password_over_http ... ok
test tests::clickhouse_target_resolver_requires_current_schema ... ok
test tests::cloudflare_ndjson_payload_parses_multiple_records ... ok
test tests::cloudflare_log_record_maps_body_severity_and_attributes ... ok
test tests::cloudflare_validation_payload_is_detected ... ok
test tests::cloudflare_timestamps_support_rfc3339_unix_and_unix_nano ... ok
test tests::decrypt_aes256_gcm_matches_node_crypto_fixture ... ok
test tests::enrichment_overwrites_tenant_fields ... ok
test tests::extract_ingest_key_returns_sentinel_literal_unchanged ... ok
test tests::hash_is_deterministic ... ok
test tests::rejection_span_status_is_error_only_for_5xx ... ok
test tests::resolve_ingest_key_keeps_stale_schema_on_managed_native_path ... ok
test tests::resolve_connector_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_ingest_key_returns_none_when_hash_missing ... ok
test tests::resolve_ingest_key_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_ingest_key_returns_self_managed_false_when_no_settings_row ... ok
test tests::resolve_ingest_key_returns_self_managed_true_when_active_settings_row ... ok
test tests::sentinel_token_matches_only_exact_literal ... ok
test tests::tinybird_destination_keeps_forward_mode_on_forward_path ... ok
test autumn::tests::fails_open_on_transport_error ... ok
test tests::resolve_ingest_key_serves_last_known_routing_when_refresh_fails ... ok
test tests::forward_mode_switches_ready_org_to_clickhouse_without_forwarding_again ... ok
test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s
Doc-tests maple_ingest
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
…policy useAlertDestinationsList() resolves off the live-synced collection, so its Result is only ever `initial` (loading) or `success` — never a failure. The `.onError` branch in the escalation policy section was therefore unreachable dead code, and its "Retry" button was wired to the hook's no-op refresh. Remove the dead error/retry branch; loading shows the skeleton and success renders the form. Addresses Devin review finding on escalation-policy-section.tsx. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Two changes landed together in the working tree (assembled from two concurrent sessions — reviewer note below).
1. Alerts empty-state redesign (primary)
Turns the alerts overview zero-rules empty state into a first-run starter launcher that fits Maple's operator-terminal identity instead of the generic
Emptybell-badge.--primary) threshold with a pulsing leading dot, reusing the existing.infra-ref-line(dashed draw-in) and.infra-pulseidioms. It reads as two true things at once: no incidents, and nothing is watching yet.ALERT_TEMPLATES(High error rate, Slow P95/P99, Low Apdex, Throughput drop) aschart-*-tinted tiles (hues mirrorBUILTIN_SIGNAL_OPTIONSon the create form), each a one-click deep link into a pre-filled create form, plus a "Start from scratch" ghost link.templatesearch param on/alerts/create;deriveInitialRuleDraftpre-applies the matching preset and skips the first-touch overlay. Unknown ids fall through to blank + overlay. ReusesapplyTemplate, so the overlay, the tiles, and thecreate_alert_ruleMCP tool resolve the same set.Files:
components/alerts/overview/alerts-empty-state.tsx(new),routes/alerts/create.tsx,components/alerts/alert-create-page-content.tsx(+ colocated test),components/alerts/overview/alerts-overview-tab.tsx.2. Alerts data → TanStack DB + Electric collections (bundled)
Migrates alerts destinations & scrape targets off
MapleApiAtomClient.queryonto Electric-synced collections:useAlertDestinationsList,alertDestinations/scrapeTargetscollections,use-scrape-target-checks, electric-sync shape route updates, settings + scrape-target section wiring, and the0011_electric_publication_wave1drizzle migration.Reviewer note
The empty-state work depends on the Electric-collections migration (my edited
alerts-overview-tab.tsx/alert-create-page-content.tsxcall the newuseAlertDestinationsList), so the two are shipped together and are green as a whole rather than in isolation.Verification
@maple/webtypecheck: clean.chart-*colors,?template=low_apdexlands on the Apdex-prefilled form (< 0.8threshold) with no overlay, and the blank path still opens the overlay.🤖 Generated with Claude Code