{{ message }}
feat(alerting): custom alert rule builder (plan 32) - #2257
Merged
Conversation
duyet
enabled auto-merge (squash)
July 3, 2026 17:29
Member
Author
This was referenced Jul 4, 2026
Lets users build a numeric-threshold alert rule from whitelisted building blocks (a known metric -> a vetted read-only SQL template, an operator, warning/critical thresholds) instead of editing TypeScript. Compiles to an AlertRuleDef, persists in D1 (custom_alert_rules), and the sweep re-syncs enabled custom rules every tick alongside the built-ins. - rule-builder-schema.ts: fixed METRIC_CATALOG (10 vetted read-only queries), zod validation, pure compileCustomRule, assertReadOnlySql deny-list (defense-in-depth; no SQL is ever built from free text) - custom-rules-store.ts + 0014 migration: owner-scoped D1 CRUD, fails open on D1/query failure so built-ins keep running - custom-rules-auth.ts: owner resolution that stays auth-gated when Clerk is configured but falls back to a fixed single-tenant owner when it is not, so this works self-hosted without Clerk (core feature, not cloud-only) - custom-rules.ts / $id.ts / test.ts API routes: GET/POST/DELETE + a read-only "test" endpoint for the builder's live preview - rule-builder.tsx + Custom Rules tab in health settings: no free-form SQL field anywhere - rule-registry.ts: optional AlertRuleDef.classify so "<"/"<=" ops work without changing classifyValue's default higher-is-worse semantics Co-Authored-By: duyetbot <bot@duyet.net>
duyet
force-pushed
the
advisor/32-custom-alert-rule-builder
branch
from
July 4, 2026 06:04
10aaac1 to
1aac6e3
Compare
Contributor
☁️ Cloudflare Preview Deployment
|
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
Implements plan 32: a no-code builder for numeric-threshold alert rules — pick
a whitelisted metric (a fixed catalog key mapped server-side to one vetted
read-only SQL template), an operator, and warning/critical thresholds.
No free-form SQL field exists anywhere in the API or UI.
lib/health/rule-builder-schema.ts—METRIC_CATALOG(10 curated read-onlyqueries: active/failed mutations, max parts per partition, readonly
replicas, replication lag/queue, disk usage %, running/long-running
queries, stuck merges), zod validation, pure
compileCustomRule, andassertReadOnlySql— a deny-list guard checked at compile time, againbefore persisting, and again before registering into the sweep. Plan 33
(
assertReadOnlyAction) isn't merged yet, so this ports the same intentlocally rather than depending on it.
lib/health/custom-rules-store.ts+ migration0014_custom_alert_rules.sql— owner-scoped D1 CRUD (mirrors
events/subscription-store.ts), fails openon any D1/query failure (
loadCustomRulesIntoRegistry) so built-ins keeprunning untouched.
lib/health/custom-rules-auth.ts— owner resolution that stays auth-gatedwhen Clerk is configured (signed-out request → 401, preserves cloud
per-user isolation) but falls back to a fixed single-tenant owner id when
Clerk is not configured. Custom rules are a core monitoring feature per
root CLAUDE.md ("self-hosted stays whole"), so this had to work without
Clerk — unlike webhook subscriptions (plan 44), which are Clerk-only.
routes/api/v1/health/custom-rules.ts(GET list / POST create, +?catalog=1for the builder dropdown),custom-rules/$id.ts(DELETE),custom-rules/test.ts(read-only "test" preview against a host).components/health/rule-builder.tsx— new "Custom Rules" tab in HealthSettings: metric/op/threshold form, live SQL-free preview, save/list/
delete/test. Shows an explicit "not available" message (like the sibling
webhook panel) when no D1 binding is configured, instead of a form that
would 501 on save.
lib/alerting/rule-registry.ts— added an optionalclassifyfield toAlertRuleDefso</<=operators ("lower is worse", e.g. a cache-hitratio) work without changing
classifyValue's default higher-is-worsesemantics that every built-in rule relies on. The sweep's unregister loop
keys off
id.startsWith('custom:'), nottype === 'custom'— so it nevertouches the pre-existing built-in
fatal-log-entriesrule, which alreadyused
type: 'custom'for taxonomy purposes.lib/health/server-sweep.ts— re-syncs custom rules every sweep tick:unregisters stale
custom:*ids first, then loads whatever is currentlyenabled in D1 (cron is a single global process, not scoped to a signed-in
visitor — see open question 3 below).
Coordination note (plan 31 — compound alert rules, parallel work)
Plan 31 (compound alert rules) is being implemented in parallel on
advisor/31-compound-alert-rules, but that branch has zero commits as ofthis PR (still even with
origin/main), so there is no code overlap today.Both plans touch the concept of a "rule" but at different layers: this PR
extends
rule-registry.ts/AlertRuleDefwith one small additive optionalfield (
classify) and never restructures the rule shape. If plan 31introduces a
type: 'compound'variant or a different composition mechanism,it should compose cleanly with this — a compound rule's sub-conditions could
themselves reference custom rules by id. Flagging for the reviewer to
reconcile if 31 lands with conflicting assumptions about the rule model.
Answers to the plan's open questions
custom:*ids, then reload enabled rows from D1 every tick.AlertRuleDef.classifyfield (see above) rather than inverting SQL ornegating thresholds.
custom rules across every owner (
listAllEnabledCustomRules, no ownerfilter). The cron sweep is a single global process, mirroring how
HEALTH_ALERT_WEBHOOK_URLis already a single env-wide destination today.True per-owner alert routing in a multi-tenant cloud deployment is a
documented follow-up, not attempted here.
deliberate code change in
METRIC_CATALOG.plan-enforcement.tsdoes not list customrules as a paid capability.
Test plan
cd apps/dashboard && bun run type-check— 0 errorscd apps/dashboard && bun run build— vite build + tsc --noEmit, exit 0cd apps/dashboard && bun test src/lib/health/rule-builder-schema.test.ts --isolate— 18 pass, 0 fail (off-catalog metric rejected, non-numeric/non-finite threshold rejected, compiled SQL === catalog template exactly, deny-list passes/fails as expected,</<=classification direction)cd apps/dashboard && bun test src/lib/health/ --isolate— 192 pass, 0 fail (includes the newcustom-rules-store.sql.test.ts, which proves the DELETE SQL is ownership-guarded by running it against realbun:sqlite, mirroringsubscription-store.sql.test.ts's pattern for the same IDOR class)bun run lint(root —apps/dashboardhas no standalonelintscript; the plan's literalcd apps/dashboard && bun run lintfails with "Script not found", a pre-existing plan/package.json mismatch, not something introduced here) — clean, no fixes neededhttps://claude.ai/code/session_01XpfaNdJZh4Rtr6cQm4DYqa