Skip to content

fix(keeper): wire health monitors to real outcomes instead of placeholder data - #369

Open
Morenikeoa wants to merge 1 commit into
dcccrypto:mainfrom
Morenikeoa:fix/health-monitors-wiring
Open

fix(keeper): wire health monitors to real outcomes instead of placeholder data#369
Morenikeoa wants to merge 1 commit into
dcccrypto:mainfrom
Morenikeoa:fix/health-monitors-wiring

Conversation

@Morenikeoa

@Morenikeoa Morenikeoa commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Problem

/health's monitors.{rpc,scan,oracle,db} sub-objects (src/index.ts, backed by @percolatorct/shared's ServiceMonitor) look like genuine connectivity probes — named exactly that way — but recordSuccess()/recordFailure() were never called anywhere in the codebase. Confirmed via repo-wide grep: the only recordSuccess/recordFailure calls in the whole repo belonged to an unrelated tracker in rpc-pool.ts.

lastSuccessTime is set once at construction and consecutiveFailures stays at 0 forever, so getStatus().healthy is permanently true regardless of actual RPC/DB/oracle connectivity, and timeSinceSuccessMs grows unbounded without ever being checked against a staleness threshold.

Production Impact

Anyone building dashboards or alert rules off health.monitors.rpc.healthy (etc.) is reading fabricated-looking "all good" telemetry that can never go false — even while the keeper's actual RPC/DB/oracle connectivity is genuinely broken in logs.

Fix

Moved monitors out of index.ts into a new src/lib/service-monitors.ts (so crank.ts and oracle.ts can wire it without creating a circular import on index.ts), and recorded real outcomes at each monitor's most natural existing call site:

  • rpc — the periodic SOL-balance getBalance() check (index.ts), a genuine recurring RPC round trip.
  • scan — the periodic discover() + crankAll() cycle (crank.ts) completing cleanly vs. throwing.
  • oracle — the DexScreener/Jupiter external price fetches' HTTP/network outcome (oracle.ts) — both fetch functions, both the !res.ok branch and the catch block.
  • db — the single Supabase market-metadata query (crank.ts's discoverMarkets), both the error branch and the catch block.

Proof of Fix

New tests:

  • tests/services/oracle.test.ts: records monitors.oracle success on a reachable fetch, failure on a network error, failure on a non-ok HTTP response (with the status code in the message).
  • tests/services/crank.test.ts: records monitors.db success on a clean Supabase query, failure on a Supabase-level error, failure when the Supabase call throws; records monitors.scan success after a clean periodic cycle.

Every existing test file that mocks @percolatorct/shared without spreading the real module needed createServiceMonitors added to its mock, since the new service-monitors.ts module calls it at import time — updated 8 affected test files (crank-error-code.poc.test.ts, crank-hyperp-detection.poc.test.ts, crank.b-fixes.test.ts, crank.processBatched.test.ts, program-id-allowlist.test.ts, oracle.b-fixes.test.ts, oracle-stale.test.ts, oracle-deviation.test.ts) plus oracle.test.ts/crank.test.ts directly.

Test Output

 Test Files  1 failed | 93 passed | 2 skipped (96)
      Tests  1 failed | 981 passed | 33 skipped (1015)

The one failure (tests/v17-risk-params.poc.test.ts) is pre-existing and unrelated — a stale assertion from commit 8ee810d (#345), out of scope here.

pnpm build — clean, zero errors.

Summary by CodeRabbit

  • New Features

    • Health checks now reflect real connectivity signals for RPC, database, scan, and price-feed services.
    • Monitor status is now updated automatically during normal service activity.
  • Bug Fixes

    • Improved reliability of service health reporting by capturing successful and failed external requests.
    • Fixed test stability around monitor initialization so affected services load consistently.

…lder data

/health's monitors.{rpc,scan,oracle,db} sub-objects look like genuine
connectivity probes, but recordSuccess()/recordFailure() were never called
anywhere in the codebase -- confirmed by repo-wide grep. lastSuccessTime is
set once at construction and consecutiveFailures stays 0 forever, so
getStatus().healthy is permanently true regardless of actual RPC/DB/oracle
connectivity. Anyone building dashboards or alert rules off
health.monitors.rpc.healthy etc. is reading fabricated-looking "all good"
telemetry that can never go false.

Moved `monitors` out of index.ts into a new src/lib/service-monitors.ts (so
crank.ts and oracle.ts can wire it without a circular import on index.ts) and
recorded real outcomes at the four monitors' most natural existing call
sites:
  - rpc: the periodic SOL-balance getBalance() check (index.ts)
  - scan: the periodic discover()+crankAll() cycle completing or throwing
    (crank.ts)
  - oracle: the DexScreener/Jupiter external price fetches' HTTP/network
    outcome (oracle.ts)
  - db: the single Supabase market-metadata query (crank.ts)

Each existing test file mocking @percolatorct/shared without spreading the
real module needed createServiceMonitors added to its mock (the new module
calls it at import time) -- updated 8 affected test files.

BUG-110 from a clean-room Phase 4 audit pass.
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a shared monitors module, re-exports it from the keeper entrypoint, and records RPC, database, scan, and oracle outcomes during keeper intervals, crank processing, and oracle price fetches. The test suites were updated to mock the new monitor factory and assert the new recordings.

Changes

Service Monitor Instrumentation

Layer / File(s) Summary
Shared monitor module and export
src/lib/service-monitors.ts, src/index.ts
Adds a shared monitors module and re-exports it from the keeper entrypoint.
Runtime monitor recording
src/index.ts, src/services/crank.ts, src/services/oracle.ts
Keeper balance checks, crank discovery/loop, and oracle fetches now record success and failure outcomes on the shared monitors.
Crank monitor tests and mocks
tests/services/crank*.test.ts, tests/services/program-id-allowlist.test.ts
Crank-related suites mock createServiceMonitors, and crank.test.ts asserts db and scan recording.
Oracle monitor tests and mocks
tests/services/oracle*.test.ts
Oracle suites mock createServiceMonitors, and oracle.test.ts asserts monitors.oracle recording for DexScreener fetch paths.

Sequence Diagram(s)

sequenceDiagram
  participant KeeperInterval as Keeper SOL-balance interval
  participant CrankService as CrankService
  participant OracleService as OracleService
  participant Monitors as monitors

  KeeperInterval->>Monitors: rpc.recordSuccess() after getBalance
  KeeperInterval->>Monitors: rpc.recordFailure(error.message) on catch
  CrankService->>Monitors: db.recordSuccess() after Supabase query
  CrankService->>Monitors: db.recordFailure(error.message) on Supabase error or throw
  CrankService->>Monitors: scan.recordSuccess() after a completed cycle
  CrankService->>Monitors: scan.recordFailure(error.message) on cycle throw
  OracleService->>Monitors: oracle.recordSuccess() after DexScreener/Jupiter HTTP 200
  OracleService->>Monitors: oracle.recordFailure(status or error.message) on HTTP error or catch
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I hopped through logs with a twitchy nose,
and watched each monitor bloom and glows.
RPC, DB, scan, and oracle too—
all got little carrots of truth to chew.
🐇 The keeper hums; my whiskers say “woo!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: wiring keeper health monitors to real outcomes instead of placeholders.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/services/oracle-deviation.test.ts (1)

37-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the service-monitor stub into shared test scaffolding.

This makeMonitor/createServiceMonitors mock is now duplicated across the oracle and crank suites. Centralizing it in one test helper would reduce drift the next time the ServiceMonitor contract changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/services/oracle-deviation.test.ts` around lines 37 - 71, The
service-monitor mock setup is duplicated in the oracle test suite and should be
moved into shared test scaffolding. Extract the `makeMonitor` helper and the
`createServiceMonitors` mock from this `vi.mock('`@percolatorct/shared`', ...)`
block into a reusable test helper used by both oracle and crank tests. Keep the
helper aligned with the `ServiceMonitor` contract so updates only need to be
made in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/index.ts`:
- Around line 30-31: The `/health` payload still omits the new DB lane, so
`monitors.db` is exposed but never serialized. Update the health response
builder where `rpc`, `scan`, and `oracle` are collected to also include the DB
monitor from `monitors` (the same object re-exported in `src/index.ts`), and
ensure the `/health` JSON reflects the DB status alongside the other lanes.

In `@src/services/crank.ts`:
- Around line 2074-2079: The scan monitor success path in crank.ts is
incorrectly tied only to the absence of a thrown exception, so a cycle with
logged per-program failures can still call monitors.scan.recordSuccess(). Update
the discover()/crankAll() flow to return or propagate an explicit cycle result
that reflects whether any scan failed, then use that result in the Crank cycle
handling block before calling recordSuccess or recordFailure. Keep the existing
logger.error and monitors.scan methods, but make the success decision based on
the explicit outcome instead of the try/catch alone.

In `@src/services/oracle.ts`:
- Around line 188-195: The `monitors.oracle` updates in `fetchPrice()` and
`peekPrice()` are racing because both the DexScreener and Jupiter branches call
`recordSuccess()`/`recordFailure()` independently, so the last finisher
overwrites the health state. Refactor the oracle health reporting so it is
written once after both upstream requests settle, or route DexScreener and
Jupiter to separate monitor lanes; use the `monitors.oracle` calls in
`fetchPrice`, `peekPrice`, and any shared upstream helper logic to keep the
health status deterministic.

---

Nitpick comments:
In `@tests/services/oracle-deviation.test.ts`:
- Around line 37-71: The service-monitor mock setup is duplicated in the oracle
test suite and should be moved into shared test scaffolding. Extract the
`makeMonitor` helper and the `createServiceMonitors` mock from this
`vi.mock('`@percolatorct/shared`', ...)` block into a reusable test helper used by
both oracle and crank tests. Keep the helper aligned with the `ServiceMonitor`
contract so updates only need to be made in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8d8f9c5-6da8-404b-8a16-4cf4725ce543

📥 Commits

Reviewing files that changed from the base of the PR and between 8ee810d and 7b724da.

📒 Files selected for processing (14)
  • src/index.ts
  • src/lib/service-monitors.ts
  • src/services/crank.ts
  • src/services/oracle.ts
  • tests/services/crank-error-code.poc.test.ts
  • tests/services/crank-hyperp-detection.poc.test.ts
  • tests/services/crank.b-fixes.test.ts
  • tests/services/crank.processBatched.test.ts
  • tests/services/crank.test.ts
  • tests/services/oracle-deviation.test.ts
  • tests/services/oracle-stale.test.ts
  • tests/services/oracle.b-fixes.test.ts
  • tests/services/oracle.test.ts
  • tests/services/program-id-allowlist.test.ts

Comment thread src/index.ts
Comment on lines +30 to +31
// Monitoring — alerts to Discord on threshold breaches (see src/lib/service-monitors.ts)
export { monitors };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Expose the new db lane in /health.

This re-export makes monitors.db available, but Lines 658-662 still serialize only rpc, scan, and oracle. The DB monitor updates added in src/services/crank.ts never reach /health, so DB outages remain invisible despite this PR’s stated goal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 30 - 31, The `/health` payload still omits the new
DB lane, so `monitors.db` is exposed but never serialized. Update the health
response builder where `rpc`, `scan`, and `oracle` are collected to also include
the DB monitor from `monitors` (the same object re-exported in `src/index.ts`),
and ensure the `/health` JSON reflects the DB status alongside the other lanes.

Comment thread src/services/crank.ts
Comment on lines +2074 to +2079
// BUG-110: the cycle (discovery + crank pass) completed without
// throwing — record so /health's monitors.scan reflects real outcomes.
monitors.scan.recordSuccess().catch(() => {});
} catch (err) {
logger.error("Crank cycle failed", { error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined });
monitors.scan.recordFailure(err instanceof Error ? err.message : String(err)).catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Don’t treat “didn’t throw” as a successful scan cycle.

Lines 1122-1145 and 1157-1168 inside discover() log per-program scan failures and keep going, so this block can still hit recordSuccess() even when discovery actually failed across the cycle. That leaves /health.monitors.scan green during real scan outages. Base the scan monitor on an explicit cycle result from discover()/crankAll(), not just the absence of an exception.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/crank.ts` around lines 2074 - 2079, The scan monitor success
path in crank.ts is incorrectly tied only to the absence of a thrown exception,
so a cycle with logged per-program failures can still call
monitors.scan.recordSuccess(). Update the discover()/crankAll() flow to return
or propagate an explicit cycle result that reflects whether any scan failed,
then use that result in the Crank cycle handling block before calling
recordSuccess or recordFailure. Keep the existing logger.error and monitors.scan
methods, but make the success decision based on the explicit outcome instead of
the try/catch alone.

Comment thread src/services/oracle.ts
Comment on lines +188 to +195
// BUG-110: record real connectivity outcomes so /health's monitors.oracle
// reflects whether the external price feeds are actually reachable.
if (!res.ok) {
monitors.oracle.recordFailure(`DexScreener HTTP ${res.status}`).catch(() => {});
return null;
}
monitors.oracle.recordSuccess().catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Avoid racing two upstreams into one oracle monitor.

fetchPrice() / peekPrice() run DexScreener and Jupiter in parallel, but these branches all mutate the same monitors.oracle lane. If one source fails and the other succeeds, whichever finishes last wins, so /health.monitors.oracle becomes nondeterministic and can hide a degraded single-source state. Record the monitor once after both requests settle, or split the sources into separate lanes.

Also applies to: 228-228, 262-268, 284-284

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/oracle.ts` around lines 188 - 195, The `monitors.oracle` updates
in `fetchPrice()` and `peekPrice()` are racing because both the DexScreener and
Jupiter branches call `recordSuccess()`/`recordFailure()` independently, so the
last finisher overwrites the health state. Refactor the oracle health reporting
so it is written once after both upstream requests settle, or route DexScreener
and Jupiter to separate monitor lanes; use the `monitors.oracle` calls in
`fetchPrice`, `peekPrice`, and any shared upstream helper logic to keep the
health status deterministic.

@dcccrypto

Copy link
Copy Markdown
Owner

Independent verification — not an approval (QA/Security own that). Verdict: the branch is stale, not broken. One git merge main and it's fully green.

I'm writing this up carefully because my first reading was wrong, and posting it would have been an unfair accusation.

What I first saw

full keeper suite on #369 → 1 FAILED / 981 passed
  × tests/v17-risk-params.poc.test.ts
    "throws when maintenanceMarginBps decodes to >= 10_000 (>= 100% margin)"

same file on main (full file, no -t filter) → 7 passed

Read naively that's "this PR breaks a risk-parameter safety check" — a serious claim about a health-monitoring PR.

What's actually going on

src/lib/v17-risk.ts is byte-identical between main and this branch — the PR doesn't touch the parser at all. The difference is in the test, which this branch has an older copy of:

main-only commits: 5    #369-only commits: 1

main gained 6c7c65c test(risk): correct v17 boundary expectation, which corrected the expectation to match the parser's real semantics — the engine permits exactly 100% margin as a fast-path config, so the valid range is (0, 10000] and 10_000 must not throw. This branch predates that and still carries the superseded >= 10_000 expectation.

So the failure is a stale-branch artefact, not a regression.

Verified

merge main into #369              → clean, no conflicts
tests/v17-risk-params.poc.test.ts → 7 passed
full keeper suite                 → 982 passed, 0 failed

Just merge main in (or rebase) and CI will be green. Nothing in the PR's own changes needs touching.

On the change itself

Wiring the health monitors to real outcomes rather than placeholder data is squarely the right direction — I filed and fixed a sibling instance of exactly this in #392, where the v17 conservation invariant reported ok: true without ever running. A monitor that reports a value it never computed is worse than no monitor, because it converts "we don't know" into "we're fine".

One thing I checked and want to note approvingly: src/lib/service-monitors.ts is new, and there's no tests/lib/service-monitors.test.ts — but it is exercised, by ten existing suites (crank ×4, oracle ×4, program-id-allowlist, processBatched) that this PR updates alongside it. So the behaviour is covered at the call sites rather than in isolation, which is the layer that actually matters. I've flagged the opposite pattern (helper tested, call site not) on several PRs recently; this is the right way round.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants