Skip to content

feat: Sentry Crons (monitor check-in ingestion) - #164

Open
AbianS wants to merge 9 commits into
mainfrom
feat/crons-monitors
Open

AbianS wants to merge 9 commits into
mainfrom
feat/crons-monitors

Conversation

@AbianS

@AbianS AbianS commented Jun 29, 2026 •

Copy link
Copy Markdown
Member

Overview

Implements Sentry Crons (cron/scheduled-job monitoring) end-to-end — item type #4 of #143. Ground truth for the protocol is relay-monitors/src/lib.rs (CheckIn, process_check_in); behaviour mirrors Relay so a Sentry SDK can't tell the difference.

Note: #143 lists 9 item types. Log (#7) was already shipped in v0.8.0. This PR delivers Crons (#4). The rest remain open.

How it works (SDK-driven, like Sentry)

Monitors are not created in the dashboard — they're auto-upserted from SDK check-ins. A check-in carrying monitor_config provisions the monitor's schedule; in_progress then ok/error (sharing a check_in_id) record the run. missed/timeout are computed server-side (Relay rejects an inbound missed).

Layers

  • server — EnvelopeItemKind::CheckIn + CheckInProcessor (upsert monitor by (project, slug), schedule config via COALESCE, in_progress→ok lifecycle upsert, derived status, next_expected_at from crontab/interval + timezone). monitors + check_ins tables (dual Postgres/SQLite). MonitorWorker background loop marks missed (overdue past checkin_margin) and timeout (in-progress past max_runtime). Read API: GET /api/projects/{id}/monitors and /monitors/{slug}/checkins. OpenAPI regenerated.
  • client (@rustrak/client) — client.monitors.list() / listCheckIns() with Zod schemas.
  • mcp (@rustrak/mcp) — list_monitors + list_monitor_check_ins tools.
  • webview-ui — new Crons tab: monitor list (status, schedule, last/next check-in) with lazily-loaded check-in history.
  • test-sentry — pnpm demo:crons exercises the real @sentry/node SDK (withMonitor / captureCheckIn).

New deps (server)

cron (crontab parsing) + chrono-tz (timezone-aware next-occurrence).

Testing

  • Server: TDD throughout. Green on both SQLite (default) and real Postgres (cargo test --features postgres, testcontainers). Client 357 tests, MCP 78 tests, webview-ui tsc/biome/next build clean.
  • Verified live against the Postgres dev server with the real SDK: 4 monitors auto-created, timezone math correct (0 0 * * * America/New_York → 04:00 UTC), error/lifecycle/idempotent upsert all correct.
  • A dialect bug (minute columns must be BIGINT on Postgres, not INTEGER) was caught during live testing and fixed; test placeholders switched to $N so the suite runs under both engines.

Config

MONITOR_TICK_INTERVAL_SECS (default 60) — how often the missed/timeout worker scans.

Not included (open question)

Write operations (delete / mute-disable / manual create-edit) — Sentry offers these via UI/API, but the SDK-upsert read-only flow is the canonical path. Can follow up if wanted.

Summary by CodeRabbit

  • New Features

    • Added project monitor/cron pages, API endpoints, and client support for viewing monitors and recent check-ins.
    • Added background handling for missed and timed-out check-ins, plus schedule-based next check-in estimates.
    • Added a new demo flow for monitor check-ins.
  • Bug Fixes

    • Improved check-in handling for repeated updates and terminal statuses.
    • Added support for the new check-in envelope type across ingest and parsing paths.
  • Tests

    • Added coverage for monitor APIs, scheduling logic, worker behavior, and client integrations.

AbianS added 9 commits June 29, 2026 09:30
Dual Postgres/SQLite migrations for the Crons feature (issue #143):
- monitors: scheduled jobs, upserted per (project_id, slug), holding
  schedule config, derived status, last_check_in and next_expected_at.
- check_ins: one row per reported execution, linked to its monitor.
Add the check-in ingestion pipeline (issue #143), mirroring Relay's
relay-monitors CheckIn schema and process_check_in normalization:

- models::check_in: CheckInPayload + normalize() (slug<=50, env<=64,
  server-only 'missed' coerced to 'unknown'), schedule/config models,
  and Monitor/CheckIn response shapes.
- EnvelopeItemKind::CheckIn + parser 'check_in' arm + Route::CheckIn.
- CheckInProcessor: upsert monitor by (project,slug), COALESCE schedule
  config, in_progress->ok lifecycle upsert, derived monitor status, and
  next_expected_at from the schedule.
- services::monitor: schedule math (crontab via cron + interval +
  chrono-tz) and process_overdue() (missed + max_runtime timeout).

Deps: cron, chrono-tz.
- workers::monitor_worker: background loop running process_overdue every
  MONITOR_TICK_INTERVAL_SECS (default 60); wired in main.rs.
- GET /api/projects/{id}/monitors and
  GET /api/projects/{id}/monitors/{slug}/checkins (offset paginated),
  behind ViewProject access control.
- config: MONITOR_TICK_INTERVAL_SECS.
- Regenerated openapi.json with the monitor paths and schemas.
checkin_margin and max_runtime are read as Option<i64> but were declared
INTEGER (INT4) in the Postgres migration, which panics on decode under real
Postgres (INT8 vs INT4) while passing on SQLite's loose typing. Declare them
BIGINT to match.

Also switch the monitor test verification queries from '?' to '$N'
placeholders so the suite runs green under both SQLite and Postgres
(cargo test --features postgres), catching this class of dialect bug.
client.monitors.list(projectId) and listCheckIns(projectId, slug, options),
with Zod schemas (Monitor, CheckIn) and types. MSW handlers + integration
tests for both endpoints.
list_monitors and list_monitor_check_ins MCP tools wrapping the client,
registered in the server factory. Tool tests + updated integration tool count.
New 'Crons' entry in the project sidebar and a /crons page listing monitors
(status badge, schedule, last/next check-in) with lazily-loaded check-in
history per monitor. Server actions wrap the client's monitors resource.
demo/src/crons.ts (pnpm demo:crons) exercises Sentry.withMonitor and
captureCheckIn against a local server, the true wire-format compatibility
test for monitor check-ins.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements full Sentry Crons (monitors/check-ins) support: SQLite and Postgres migrations create monitors and check_ins tables; a new CheckInProcessor ingests check_in envelope items; MonitorService handles schedule math and overdue/timeout detection via a background MonitorWorker; REST endpoints and OpenAPI spec are added; a TypeScript MonitorsResource client, MCP tools, and a Next.js Crons UI page are wired end-to-end.

Changes

Sentry Crons — Monitors and Check-ins

Layer / File(s) Summary
DB migrations and data models
apps/server/migrations/sqlite/20260629000000_create_monitors.up.sql, apps/server/migrations/sqlite/20260629000000_create_monitors.down.sql, apps/server/migrations/postgres/20260629000000_create_monitors.up.sql, apps/server/migrations/postgres/20260629000000_create_monitors.down.sql, apps/server/src/models/check_in.rs, apps/server/src/models/mod.rs
Creates monitors (with scheduling, status, and timing fields) and check_ins (lifecycle, duration, metadata) tables for both SQLite and Postgres with supporting indexes. Defines CheckInStatus, Schedule, MonitorConfig, CheckInPayload (with parse/normalize), and response DTOs MonitorResponse/CheckInResponse.
Envelope ingestion and CheckInProcessor
apps/server/src/ingest/envelope.rs, apps/server/src/digest/processors/check_in.rs, apps/server/src/digest/processors/mod.rs, apps/server/src/routes/ingest.rs
Adds EnvelopeItemKind::CheckIn(Vec<u8>) variant and its parser. Implements CheckInProcessor which upserts monitors by (project_id, slug), computes next_expected_at, and inserts or updates check_ins rows within a transaction. Registers the processor in the routing layer and dispatches from ingest_envelope.
MonitorService: schedule math and lifecycle processing
apps/server/src/services/monitor.rs, apps/server/src/services/mod.rs, apps/server/src/workers/monitor_worker.rs, apps/server/src/workers/mod.rs, apps/server/src/config.rs, apps/server/Cargo.toml
Implements next_expected_after for interval and crontab schedules (using cron/chrono-tz). Adds MonitorService::process_overdue (missed transitions), process_timeouts (timeout transitions), list_monitors, and list_check_ins. Adds MonitorWorker async background loop ticking via monitor_tick_interval_secs config.
REST endpoints and OpenAPI
apps/server/src/routes/monitors.rs, apps/server/src/routes/mod.rs, apps/server/src/pagination/mod.rs, apps/server/src/openapi.rs, apps/server/openapi.json, apps/server/src/main.rs
Adds list_monitors and list_check_ins Actix handlers, MonitorsListResponse, ListCheckInsQuery pagination struct. Registers routes and MonitorWorker spawn in main.rs. Extends OpenAPI spec and generated openapi.json with new paths and schemas.
TypeScript client SDK
packages/client/src/schemas/monitor.ts, packages/client/src/types/monitor.ts, packages/client/src/types/common.ts, packages/client/src/resources/monitors.ts, packages/client/src/client.ts, packages/client/src/...
Adds Zod schemas (monitorSchema, checkInSchema, monitorsListResponseSchema), inferred types Monitor/CheckIn, ListCheckInsOptions, and MonitorsResource with list/listCheckIns methods. Wires the resource onto RustrakClient and re-exports all types.
MCP monitor tools
packages/mcp/src/tools/monitors.ts, packages/mcp/src/server.ts
Registers list_monitors and list_monitor_check_ins MCP tools with Zod-validated inputs (project_id, slug, optional pagination) forwarding to client.monitors.* and returning pretty-printed JSON.
Web UI Crons page and sidebar
apps/webview-ui/src/actions/monitors.ts, apps/webview-ui/src/app/(main)/projects/[id]/crons/page.tsx, apps/webview-ui/src/app/(main)/projects/[id]/crons/monitors-list.tsx, apps/webview-ui/src/app/(main)/projects/[id]/project-sidebar.tsx
Adds server actions listMonitors/listCheckIns, a Next.js CronsPage with empty-state and populated MonitorsList, inline check-in expansion with lazy fetch and caching, and a Crons sidebar nav entry.
Tests, demo, and config propagation
apps/server/tests/unit/*, apps/server/tests/integration/monitors_api_test.rs, apps/server/tests/integration/mod.rs, packages/client/tests/..., packages/mcp/tests/..., packages/test-sentry/demo/src/crons.ts, apps/server/tests/integration/* (config updates)
Unit tests for CheckInProcessor, schedule math (next_expected_after), and MonitorService::process_overdue/timeout. Integration tests for monitor and check-in API endpoints. Client/MCP mock handlers and test suites. Crons demo script. monitor_tick_interval_secs: 60 propagated to all test config helpers.
Agent session notes
_bmad/_memory/agent-rusty/BOND.md, _bmad/_memory/agent-rusty/sessions/2026-06-29.md
Updates BOND.md with CheckIn and Log implementation verification and relay-repo SHA. Adds session note analyzing EnvelopeItemKind coverage gaps against issue #143.

Sequence Diagram(s)

sequenceDiagram
  participant SDK as Sentry SDK
  participant Server as Ingest Server
  participant CheckInProcessor
  participant DB as Database
  participant MonitorWorker

  SDK->>Server: POST /api/{project}/envelope (check_in item)
  Server->>CheckInProcessor: process(payload, ctx)
  CheckInProcessor->>DB: UPSERT monitors(project_id, slug)
  CheckInProcessor->>CheckInProcessor: next_expected_after(schedule, now)
  CheckInProcessor->>DB: INSERT or UPDATE check_ins row
  CheckInProcessor->>DB: UPDATE monitors status/next_expected_at

  loop every monitor_tick_interval_secs
    MonitorWorker->>MonitorWorker: Utc::now()
    MonitorWorker->>DB: SELECT overdue monitors
    MonitorWorker->>DB: UPDATE status=missed, INSERT missed check_in
    MonitorWorker->>DB: SELECT in_progress exceeding max_runtime
    MonitorWorker->>DB: UPDATE status=timeout
  end

  SDK->>Server: GET /api/projects/{id}/monitors
  Server->>DB: SELECT monitors ORDER BY last_check_in_at
  Server-->>SDK: MonitorsListResponse

  SDK->>Server: GET /api/projects/{id}/monitors/{slug}/checkins
  Server->>DB: SELECT check_ins LIMIT/OFFSET + COUNT
  Server-->>SDK: OffsetPaginatedResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • rustrak/rustrak#132: Both PRs extend EnvelopeItemKind and the digest/processors routing table with new item type mappings, touching the same dispatch infrastructure.
  • rustrak/rustrak#157: Adds the Log envelope item and processor on the same code path (ingest/envelope.rs, processors/mod.rs) that this PR extends with CheckIn.
  • rustrak/rustrak#74: This PR extends the MCP server (packages/mcp/src/server.ts) introduced in #74 by registering the new registerMonitorTools alongside existing tool groups.

Poem

🐇 Hippity-hop, the crons are alive!
Monitors watch, so your jobs can survive.
A tick of the worker, a check-in arrives,
"Missed!" shouts the rabbit, then next_expected drives.
From DB to UI, the schedule survives —
no cron goes unnoticed while Rustrak thrives! 🕐

🚥 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 matches the main feature: Sentry Crons monitor check-in ingestion.
Docstring Coverage ✅ Passed Docstring coverage is 96.43% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/crons-monitors

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 oasdiff (1.20.0)
apps/server/openapi.json

Error: failed to load base spec from "/tmp/coderabbit-oasdiff-base.gyoX6l": map key "SortOrder" not found


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.

@AbianS AbianS changed the title feat: Sentry Crons (monitor check-in ingestion) — closes #143 (Crons) feat: Sentry Crons (monitor check-in ingestion) Jun 29, 2026
@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements Sentry Crons (monitor check-in ingestion) end-to-end: envelope item parsing, a CheckInProcessor that upserts monitors and records check-ins in a transaction, a background MonitorWorker that detects missed and timed-out runs, two read API endpoints, a TypeScript client, MCP tools, and a Crons tab in the webview UI. The implementation closely mirrors Relay's protocol and is well-tested across both SQLite and Postgres.

  • Server: monitors + check_ins tables with dual-dialect migrations; CheckInProcessor handles lifecycle upsert (in_progress → ok/error); MonitorService contains schedule math (cron + chrono-tz) and the missed/timeout detection worker.
  • Client/MCP/UI: @rustrak/client adds monitors.list() / listCheckIns() with Zod schemas; MCP exposes two tools; webview adds a Crons tab with lazy-loaded check-in history.
  • One state-machine gap: the missed-detection candidates query excludes 'missed' and 'disabled' but not 'timeout', and process_timeouts does not advance next_expected_at, so a timed-out monitor whose next expected window subsequently passes will be re-classified as missed, overwriting the timeout state.

Confidence Score: 3/5

Mostly solid implementation, but the missed-detection worker can overwrite a timeout monitor's status on subsequent ticks, producing incorrect derived state in the database.

The background worker's process_overdue does not exclude 'timeout' from its candidates query, and process_timeouts never advances next_expected_at. Once a monitor's run is marked timed-out, the very next worker tick where next_expected_at + margin is in the past will flip the monitor from timeout to missed and insert a synthetic missed check-in row — corrupting the record of what actually happened. This is a present defect on the checked-in path, not a speculative concern.

apps/server/src/services/monitor.rs — the process_overdue candidates query and process_timeouts need to be aligned so that a timed-out monitor cannot be re-classified as missed.

Important Files Changed

Filename Overview
apps/server/src/services/monitor.rs Core schedule math and missed/timeout detection. A state-machine gap allows monitors in timeout state to be subsequently flipped to missed because 'timeout' is not excluded from the overdue-candidates query.
apps/server/src/digest/processors/check_in.rs Check-in processor: upserts monitor, lifecycle UPDATE-or-INSERT, and monitor derived-state update. Logic is correct and well-structured.
apps/server/src/workers/monitor_worker.rs Thin background loop delegating to MonitorService::process_overdue. Error handling and logging are adequate.
apps/server/src/models/check_in.rs CheckInPayload model, normalization (missed→unknown coercion, slug/env truncation), and response structs. Correct and mirrors Relay behavior.
apps/server/src/routes/monitors.rs Two read-only endpoints (list monitors, list check-ins). Auth check applied correctly via access::require on both handlers.
apps/server/migrations/postgres/20260629000000_create_monitors.up.sql Postgres migration: monitors and check_ins tables with correct FK cascade, UNIQUE constraint on (project_id, slug), and appropriate indexes.
apps/webview-ui/src/app/(main)/projects/[id]/crons/monitors-list.tsx Crons tab UI: lazy-loads check-in history on row expand. Fetch errors are silently dropped leaving the expanded panel showing "No check-ins recorded yet." instead of an error message.
apps/server/tests/unit/monitor_worker_test.rs Good coverage of missed and timeout paths, but no test verifying that a timed-out monitor is not subsequently re-classified as missed on the next tick once next_expected_at has passed.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant SDK as Sentry SDK
    participant Ingest as Ingest Route
    participant Proc as CheckInProcessor
    participant DB as Database
    participant Worker as MonitorWorker

    SDK->>Ingest: "POST /api/{dsn}/envelope (check_in item)"
    Ingest->>Proc: process(payload, ctx)
    Proc->>DB: UPSERT monitors (project_id, slug) COALESCE schedule config
    Proc->>DB: SELECT monitor id + schedule
    Proc->>Proc: next_expected_after(schedule, ingested_at)
    alt closing check-in (ok/error) with check_in_id
        Proc->>DB: UPDATE check_ins SET status WHERE check_in_id
    else new check-in
        Proc->>DB: INSERT check_ins
    end
    Proc->>DB: UPDATE monitors SET status/last_check_in/next_expected_at
    DB-->>Proc: ok
    Proc-->>Ingest: ok

    loop every MONITOR_TICK_INTERVAL_SECS
        Worker->>Worker: process_timeouts(now)
        Worker->>DB: SELECT in_progress check_ins with max_runtime
        DB-->>Worker: open rows
        Worker->>DB: UPDATE check_ins SET timeout / UPDATE monitors SET timeout
        Worker->>Worker: process_overdue(now)
        Worker->>DB: SELECT monitors WHERE next_expected_at NOT NULL AND status NOT IN (missed, disabled)
        DB-->>Worker: candidates
        Worker->>DB: UPDATE monitors SET missed / INSERT synthetic missed check_in
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant SDK as Sentry SDK
    participant Ingest as Ingest Route
    participant Proc as CheckInProcessor
    participant DB as Database
    participant Worker as MonitorWorker

    SDK->>Ingest: "POST /api/{dsn}/envelope (check_in item)"
    Ingest->>Proc: process(payload, ctx)
    Proc->>DB: UPSERT monitors (project_id, slug) COALESCE schedule config
    Proc->>DB: SELECT monitor id + schedule
    Proc->>Proc: next_expected_after(schedule, ingested_at)
    alt closing check-in (ok/error) with check_in_id
        Proc->>DB: UPDATE check_ins SET status WHERE check_in_id
    else new check-in
        Proc->>DB: INSERT check_ins
    end
    Proc->>DB: UPDATE monitors SET status/last_check_in/next_expected_at
    DB-->>Proc: ok
    Proc-->>Ingest: ok

    loop every MONITOR_TICK_INTERVAL_SECS
        Worker->>Worker: process_timeouts(now)
        Worker->>DB: SELECT in_progress check_ins with max_runtime
        DB-->>Worker: open rows
        Worker->>DB: UPDATE check_ins SET timeout / UPDATE monitors SET timeout
        Worker->>Worker: process_overdue(now)
        Worker->>DB: SELECT monitors WHERE next_expected_at NOT NULL AND status NOT IN (missed, disabled)
        DB-->>Worker: candidates
        Worker->>DB: UPDATE monitors SET missed / INSERT synthetic missed check_in
    end
Loading

Reviews (1): Last reviewed commit: "docs(memory): record Crons feature + #14..." | Re-trigger Greptile

Comment on lines +120 to +130
let candidates = sqlx::query(
r#"
SELECT id, schedule_type, schedule_value, schedule_unit, timezone,
checkin_margin, next_expected_at
FROM monitors
WHERE next_expected_at IS NOT NULL
AND status NOT IN ('missed', 'disabled')
"#,
)
.fetch_all(pool)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 timeout status not excluded from missed-detection candidates

The candidates query filters status NOT IN ('missed', 'disabled') but does not exclude 'timeout'. Because process_timeouts marks a monitor as timeout without advancing next_expected_at, the very next worker tick (once next_expected_at + margin < now) will re-enter the monitor into the missed loop and overwrite its status to missed. A concrete failure path: a job's check-in times out, its next_expected_at is still in the past (since no new check-in arrived), the worker next tick enters the monitor as a candidate, and it is flipped from timeout to missed. Users see the wrong terminal state. The fix is to add 'timeout' to the exclusion list, or to advance next_expected_at in process_timeouts.

Comment on lines +66 to +84
const toggle = useCallback(
async (slug: string) => {
if (expanded === slug) {
setExpanded(null);
return;
}
setExpanded(slug);
if (!checkInsBySlug[slug]) {
setLoadingSlug(slug);
try {
const page = await listCheckIns(projectId, slug, { per_page: 10 });
setCheckInsBySlug((prev) => ({ ...prev, [slug]: page.items }));
} finally {
setLoadingSlug(null);
}
}
},
[expanded, checkInsBySlug, projectId],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Fetch errors are silently swallowed on check-in load

The toggle handler uses try-finally without a catch block. If listCheckIns throws (network error, server 5xx, auth expiry), setLoadingSlug(null) fires via finally, but checkInsBySlug[slug] remains undefined. The expanded panel then renders "No check-ins recorded yet." — indistinguishable from a monitor that genuinely has no history — giving users no indication that the load failed. Consider adding an error state (e.g. a errorBySlug map) and rendering an error message instead.

@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: 16

🧹 Nitpick comments (6)
_bmad/_memory/agent-rusty/sessions/2026-06-29.md (1)

26-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rephrase to avoid markdownlint false positive.

Line 26 starts with #143 which markdownlint interprets as an ATX heading lacking a space. Rephrase to start with text, e.g., "Issue #143 only covers performance items."

🤖 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 `@_bmad/_memory/agent-rusty/sessions/2026-06-29.md` at line 26, The line in the
session note starts with “#143”, which markdownlint reads as an unintended
heading; update the text in the affected markdown entry to begin with normal
prose instead, such as referencing the issue within a sentence. Keep the meaning
the same while rephrasing the sentence in the session file so it no longer
starts with a hash token and avoids the ATX heading false positive.
_bmad/_memory/agent-rusty/BOND.md (1)

38-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: EnvelopeItemKind::Log line reference may be stale.

The comment cites envelope.rs:60, but the enum variant appears around line 52 in the current file (per apps/server/src/ingest/envelope.rs). If line numbers are meant to be precise references, consider updating or using a symbol search instead of hardcoded lines.

🤖 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 `@_bmad/_memory/agent-rusty/BOND.md` around lines 38 - 40, The line reference
for EnvelopeItemKind::Log in BOND.md appears stale, so update the note to use
the current location or replace the hardcoded line number with a symbol-based
reference. Adjust the documentation entry that mentions envelope.rs and
digest/processors/logs.rs so it points to the correct EnvelopeItemKind::Log
definition and remains accurate if the file shifts again.
apps/server/tests/integration/monitors_api_test.rs (2)

112-116: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the schedule payload too.

This seeds monitor_config.schedule, but the test only checks slug and status. A regression in schedule persistence/serialization would still pass here while breaking the Crons UI.

🤖 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 `@apps/server/tests/integration/monitors_api_test.rs` around lines 112 - 116,
The integration test for monitors only verifies slug and status, so it can miss
regressions in schedule serialization. Update the assertions in the monitors API
test to also validate the schedule value returned in the response for the seeded
monitor_config.schedule, using the existing body["monitors"] item checks so the
test covers the full payload.

167-170: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Cover the pagination contract in this assertion block.

Right now this would still pass if has_more/next_cursor disappeared or changed shape, even though the new client/MCP layers depend on list metadata being stable.

🤖 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 `@apps/server/tests/integration/monitors_api_test.rs` around lines 167 - 170,
The current assertions in the monitors API integration test only validate
total_count and items, so they would miss regressions in the pagination metadata
contract. Update the assertion block in the test that reads the JSON response to
also verify the presence and expected shape/value of has_more and next_cursor,
using the same response body and items array checks already in place. Keep the
assertions tied to the list response contract so future changes in the API are
caught by this test.
packages/client/src/schemas/monitor.ts (1)

10-24: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Tighten the monitor/check-in schema domain constraints.

These schemas currently accept values the API should never emit, like arbitrary status strings or negative runtimes/durations. That weakens the Zod boundary and lets server/client contract drift pass validation unnoticed. Please model the known enum fields explicitly and make the runtime fields non-negative. As per coding guidelines, "Use Zod schemas as the single source of truth for both runtime validation and TypeScript type definitions."

Also applies to: 36-44

🤖 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 `@packages/client/src/schemas/monitor.ts` around lines 10 - 24, The
monitorSchema currently allows overly broad values for API-controlled fields, so
tighten the Zod contract by replacing free-form strings with explicit enums for
known status/check-in/schedule fields and constraining runtime/duration integers
to non-negative values. Update the relevant schema definitions in monitorSchema
(and the related schema block referenced by the same comment) so the inferred
TypeScript types stay aligned with the runtime validation and cannot accept
invalid server/client contract values.

Source: Coding guidelines

packages/mcp/src/tools/monitors.ts (1)

15-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap these MCP tool inputs in z.object(...). inputSchema is still relying on raw-shape normalization, but the Standard Schema form here should be explicit so these tools stay on the supported path and avoid future breakage. Also applies to the second tool registration.

🤖 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 `@packages/mcp/src/tools/monitors.ts` around lines 15 - 17, Wrap the MCP tool
input definitions in an explicit z.object(...) schema instead of passing a raw
shape to inputSchema. Update the tool registration in monitors.ts where
project_id is defined, and apply the same change to the second tool registration
so both use the supported Standard Schema form and avoid relying on shape
normalization.

Source: Coding guidelines

🤖 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 `@_bmad/_memory/agent-rusty/sessions/2026-06-29.md`:
- Around line 8-10: The `EnvelopeItemKind` inventory in the session note is
missing `CheckIn`, so update the list to match the current enum state reflected
by `EnvelopeItemKind` and the related digest processor modules. Edit the session
entry that mentions Event, Transaction, Session, Sessions, Log, and Other to
also include CheckIn, keeping the BOND/status note consistent with the
implemented item.

In `@apps/server/migrations/postgres/20260629000000_create_monitors.up.sql`:
- Around line 41-56: The check-in schema currently allows duplicate lifecycle
rows because `idx_check_ins_lifecycle` is only a non-unique index on
`(monitor_id, check_in_id)`, which can make terminal upserts hit multiple rows.
Update the `check_ins` table definition in the migration to enforce a unique
lifecycle key for the open/closing check-in pair, using the existing
`monitor_id` and `check_in_id` columns. If needed, adjust the lifecycle index
name or add a unique constraint/index so each check-in lifecycle can only exist
once and terminal updates remain deterministic.

In `@apps/server/migrations/sqlite/20260629000000_create_monitors.up.sql`:
- Around line 31-49: The lifecycle index on check_ins is currently non-unique,
so duplicate rows can still be inserted for the same monitor_id and check_in_id.
Update the sqlite migration that creates check_ins to make
idx_check_ins_lifecycle a UNIQUE index, using the existing check_ins table
definition and the lifecycle columns monitor_id and check_in_id so each run
identity can only appear once.

In `@apps/server/src/config.rs`:
- Around line 107-110: The MONITOR_TICK_INTERVAL_SECS parsing in Config
currently accepts 0 and can later trigger a tokio::time::interval panic. Update
the monitor_tick_interval_secs initialization in config.rs to reject zero by
validating the parsed value and returning a config load error, or by clamping it
to at least 1 before storing it in the Config struct.

In `@apps/server/src/digest/processors/check_in.rs`:
- Around line 109-129: The check-in update in `check_in` is too broad because it
matches any row with the same `check_in_id`, which can overwrite already
finalized runs. Tighten the `UPDATE` in
`apps/server/src/digest/processors/check_in.rs` so it only targets the open
`in_progress` row for the given `monitor_id` and `check_in_id`, and then adjust
the `rows_affected()` handling to distinguish “already finalized” from “missing”
instead of falling through to an insert. Use the existing `check_in_id`,
`monitor_id`, and `check_in.status` logic in `check_in` to keep retried or
out-of-order terminal events from rewriting completed rows.

In `@apps/server/src/main.rs`:
- Around line 82-85: The MonitorWorker loop currently only runs overdue/missed
detection via MonitorService::process_overdue, so timeout handling is missing.
Update MonitorWorker::run to also invoke MonitorService::process_timeouts on
each tick (using the same db_pool/config context as the overdue path), so
in-progress check-ins that exceed max_runtime are cleared. Keep the change
localized to the monitor worker path in main.rs/MonitorWorker so both missed and
timeout processing happen together.

In `@apps/server/src/models/check_in.rs`:
- Around line 131-151: Reject negative timing values during check-in
normalization by updating CheckIn::normalize() to validate
monitor_config.checkin_margin and monitor_config.max_runtime before persistence.
If either value is negative, return an AppError::Validation instead of allowing
the record to be saved unchanged. Use the existing normalize() flow in
check_in.rs so CheckInProcessor::process() cannot persist invalid timing values,
and keep the validation close to the current monitor_slug and environment
checks.

In `@apps/server/src/routes/ingest.rs`:
- Around line 116-123: The inline check-in handling in the check-in branch of
the ingest route is swallowing write failures by logging and continuing, which
allows a successful ACK even when the store/update failed. Update the logic
around processors.check_ins.process in ingest to propagate the error as a Result
instead of returning 200 OK on failure, following the existing idiomatic Rust
error-handling style used in apps/server/src/**/*.rs. Make sure the
EnvelopeItemKind::CheckIn path returns an error from the route when processing
fails so the caller can retry and the monitor/run-state updates are not silently
lost.

In `@apps/server/src/services/monitor.rs`:
- Around line 120-195: The monitor transition logic in the worker can overwrite
a real check-in because `process_missed_monitors` reads candidates first and
then performs unconditional writes later. Update the `UPDATE monitors`
statements in the missed/timeout transition paths to include the evaluated state
guards (such as `status`, `next_expected_at`, or the relevant check-in
identifier), or lock the row before calling `next_expected_after`, so only
still-stale rows transition. Make the synthetic `check_ins` insert run only when
the guarded update actually affects a row, and keep the transaction flow in
`process_missed_monitors` and the timeout path consistent.

In `@apps/server/tests/unit/check_in_test.rs`:
- Around line 406-413: The newest-first check in MonitorService::list_check_ins
is only verifying that both statuses exist, so it can pass even if the results
are not actually sorted by timestamp descending. Update the test in
check_in_test to assert the exact order of the returned check_ins using their
timestamps or another stable ordering field, alongside the existing status
checks, so the test explicitly proves newest-first behavior.

In `@apps/webview-ui/src/app/`(main)/projects/[id]/crons/monitors-list.tsx:
- Around line 64-80: Track loading state per monitor in monitors-list.tsx
instead of using a single loadingSlug that gets overwritten by overlapping
requests. Update the toggle callback and the row rendering logic to key loading
by slug (for example via a per-slug map or Set) so one monitor’s request
finishing does not clear another monitor’s spinner. Use the existing toggle,
loadingSlug, setLoadingSlug, and listCheckIns logic as the main places to
update, and apply the same change to the duplicated rendering path referenced in
the later section.
- Around line 107-123: The expand/collapse behavior on TableRow in
monitors-list.tsx is mouse-only, so make that row keyboard-accessible. Update
the row interaction in the TableRow/toggle(monitor.slug) block so it can be
focused and activated with keyboard controls (for example Enter/Space), and keep
aria-expanded in sync with isOpen. Use the existing toggle helper and isOpen
state to wire the accessible behavior without changing the panel logic.

In `@packages/client/src/resources/monitors.ts`:
- Around line 40-45: The query param handling in monitors.ts is incorrectly
gated on truthiness, so numeric values like page=0 or per_page=0 are skipped
instead of being serialized or explicitly rejected. Update the parameter checks
in the monitor request builder to test for options.page and options.per_page
being !== undefined, and keep any 1-based/positive validation separate from
serialization so the behavior is explicit and consistent.

In `@packages/client/tests/integration/monitors.test.ts`:
- Around line 23-25: The test for an unknown project currently uses a generic
rejection assertion, which does not verify the intended error mapping. Update
the `client.monitors.list(999)` expectation in `monitors.test.ts` to assert the
specific `NotFoundError` class explicitly, using the same test case name and
`rejects` chain so the check confirms the correct error type rather than any
thrown failure.

In `@packages/client/tests/mocks/handlers.ts`:
- Around line 1226-1248: The mock fixture for the monitors check-ins list
endpoint is using offset pagination fields, which conflicts with the repo’s
cursor-based list contract. Update the response shape in the relevant handlers
fixture to use the same cursor pagination fields as other list endpoints,
specifically `next_cursor` and `has_more`, and remove the
`page`/`per_page`/`total_pages` assumptions so tests and the API mock stay
aligned.

In `@packages/test-sentry/demo/src/crons.ts`:
- Around line 110-113: The error path in main().catch currently exits
immediately after logging, which can drop pending Sentry events. Update the
catch handler in crons.ts so it awaits Sentry.flush(2000) before calling
process.exit(1), matching the flush behavior used on the success path and
preserving any final batched check-ins.

---

Nitpick comments:
In `@_bmad/_memory/agent-rusty/BOND.md`:
- Around line 38-40: The line reference for EnvelopeItemKind::Log in BOND.md
appears stale, so update the note to use the current location or replace the
hardcoded line number with a symbol-based reference. Adjust the documentation
entry that mentions envelope.rs and digest/processors/logs.rs so it points to
the correct EnvelopeItemKind::Log definition and remains accurate if the file
shifts again.

In `@_bmad/_memory/agent-rusty/sessions/2026-06-29.md`:
- Line 26: The line in the session note starts with “#143”, which markdownlint
reads as an unintended heading; update the text in the affected markdown entry
to begin with normal prose instead, such as referencing the issue within a
sentence. Keep the meaning the same while rephrasing the sentence in the session
file so it no longer starts with a hash token and avoids the ATX heading false
positive.

In `@apps/server/tests/integration/monitors_api_test.rs`:
- Around line 112-116: The integration test for monitors only verifies slug and
status, so it can miss regressions in schedule serialization. Update the
assertions in the monitors API test to also validate the schedule value returned
in the response for the seeded monitor_config.schedule, using the existing
body["monitors"] item checks so the test covers the full payload.
- Around line 167-170: The current assertions in the monitors API integration
test only validate total_count and items, so they would miss regressions in the
pagination metadata contract. Update the assertion block in the test that reads
the JSON response to also verify the presence and expected shape/value of
has_more and next_cursor, using the same response body and items array checks
already in place. Keep the assertions tied to the list response contract so
future changes in the API are caught by this test.

In `@packages/client/src/schemas/monitor.ts`:
- Around line 10-24: The monitorSchema currently allows overly broad values for
API-controlled fields, so tighten the Zod contract by replacing free-form
strings with explicit enums for known status/check-in/schedule fields and
constraining runtime/duration integers to non-negative values. Update the
relevant schema definitions in monitorSchema (and the related schema block
referenced by the same comment) so the inferred TypeScript types stay aligned
with the runtime validation and cannot accept invalid server/client contract
values.

In `@packages/mcp/src/tools/monitors.ts`:
- Around line 15-17: Wrap the MCP tool input definitions in an explicit
z.object(...) schema instead of passing a raw shape to inputSchema. Update the
tool registration in monitors.ts where project_id is defined, and apply the same
change to the second tool registration so both use the supported Standard Schema
form and avoid relying on shape normalization.
🪄 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 Plus

Run ID: 2c3f4b92-c5a3-428f-b1ab-aa748d3369c7

📥 Commits

Reviewing files that changed from the base of the PR and between 109e2c3 and 1beca63.

⛔ Files ignored due to path filters (1)
  • apps/server/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (68)
  • _bmad/_memory/agent-rusty/BOND.md
  • _bmad/_memory/agent-rusty/sessions/2026-06-29.md
  • apps/server/Cargo.toml
  • apps/server/migrations/postgres/20260629000000_create_monitors.down.sql
  • apps/server/migrations/postgres/20260629000000_create_monitors.up.sql
  • apps/server/migrations/sqlite/20260629000000_create_monitors.down.sql
  • apps/server/migrations/sqlite/20260629000000_create_monitors.up.sql
  • apps/server/openapi.json
  • apps/server/src/config.rs
  • apps/server/src/digest/processors/check_in.rs
  • apps/server/src/digest/processors/mod.rs
  • apps/server/src/ingest/envelope.rs
  • apps/server/src/main.rs
  • apps/server/src/models/check_in.rs
  • apps/server/src/models/mod.rs
  • apps/server/src/openapi.rs
  • apps/server/src/pagination/mod.rs
  • apps/server/src/routes/ingest.rs
  • apps/server/src/routes/mod.rs
  • apps/server/src/routes/monitors.rs
  • apps/server/src/routes/projects.rs
  • apps/server/src/services/mod.rs
  • apps/server/src/services/monitor.rs
  • apps/server/src/workers/mod.rs
  • apps/server/src/workers/monitor_worker.rs
  • apps/server/tests/e2e/sentry_sdk_test.rs
  • apps/server/tests/integration/alerts_api_test.rs
  • apps/server/tests/integration/auth_test.rs
  • apps/server/tests/integration/envelope_v2_test.rs
  • apps/server/tests/integration/events_api_test.rs
  • apps/server/tests/integration/ingest_test.rs
  • apps/server/tests/integration/issues_api_test.rs
  • apps/server/tests/integration/logs_api_test.rs
  • apps/server/tests/integration/mod.rs
  • apps/server/tests/integration/monitors_api_test.rs
  • apps/server/tests/integration/projects_api_test.rs
  • apps/server/tests/integration/rate_limit_test.rs
  • apps/server/tests/integration/sourcemaps_api_test.rs
  • apps/server/tests/integration/storage_api_test.rs
  • apps/server/tests/integration/team_rbac_test.rs
  • apps/server/tests/integration/tokens_api_test.rs
  • apps/server/tests/integration/transactions_api_test.rs
  • apps/server/tests/unit/check_in_test.rs
  • apps/server/tests/unit/envelope_parser_test.rs
  • apps/server/tests/unit/mod.rs
  • apps/server/tests/unit/monitor_schedule_test.rs
  • apps/server/tests/unit/monitor_worker_test.rs
  • apps/webview-ui/src/actions/monitors.ts
  • apps/webview-ui/src/app/(main)/projects/[id]/crons/monitors-list.tsx
  • apps/webview-ui/src/app/(main)/projects/[id]/crons/page.tsx
  • apps/webview-ui/src/app/(main)/projects/[id]/project-sidebar.tsx
  • packages/client/src/client.ts
  • packages/client/src/index.ts
  • packages/client/src/resources/index.ts
  • packages/client/src/resources/monitors.ts
  • packages/client/src/schemas/index.ts
  • packages/client/src/schemas/monitor.ts
  • packages/client/src/types/common.ts
  • packages/client/src/types/index.ts
  • packages/client/src/types/monitor.ts
  • packages/client/tests/integration/monitors.test.ts
  • packages/client/tests/mocks/handlers.ts
  • packages/mcp/src/server.ts
  • packages/mcp/src/tools/monitors.ts
  • packages/mcp/tests/integration/server.test.ts
  • packages/mcp/tests/tools/monitors.test.ts
  • packages/test-sentry/demo/src/crons.ts
  • packages/test-sentry/package.json

Comment on lines +8 to +10
- Rustrak `EnvelopeItemKind` (envelope.rs:52): Event, Transaction, Session, Sessions, **Log**, Other.
- My BOND was stale: **Log is now implemented** (v0.8.0, `digest/processors/logs.rs`). Issue #143
item #7 is DONE.

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 | 🟡 Minor | ⚡ Quick win

Missing CheckIn in the EnvelopeItemKind variant list.

Line 8 lists: Event, Transaction, Session, Sessions, Log, Other. But CheckIn was just implemented and is present in the enum (per apps/server/src/ingest/envelope.rs and apps/server/src/digest/processors/mod.rs). The session note should include CheckIn in this inventory to accurately reflect the current state and be consistent with the BOND update.

🤖 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 `@_bmad/_memory/agent-rusty/sessions/2026-06-29.md` around lines 8 - 10, The
`EnvelopeItemKind` inventory in the session note is missing `CheckIn`, so update
the list to match the current enum state reflected by `EnvelopeItemKind` and the
related digest processor modules. Edit the session entry that mentions Event,
Transaction, Session, Sessions, Log, and Other to also include CheckIn, keeping
the BOND/status note consistent with the implemented item.

Comment on lines +41 to +56
-- SDK-provided id. Shared between the in_progress and the closing check-in,
-- so the closing status/duration upserts onto the open row. NULL when absent.
check_in_id UUID,

status VARCHAR(16) NOT NULL,
duration DOUBLE PRECISION, -- seconds
environment VARCHAR(64),
trace_id VARCHAR(64),

timestamp TIMESTAMPTZ NOT NULL,
ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_check_ins_monitor ON check_ins(monitor_id, timestamp DESC);
CREATE INDEX idx_check_ins_project_ingested ON check_ins(project_id, ingested_at DESC);
CREATE INDEX idx_check_ins_lifecycle ON check_ins(monitor_id, check_in_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Enforce one row per check-in lifecycle.

The comment here says the terminal check-in upserts onto the open row, but the schema only adds a non-unique index on (monitor_id, check_in_id). That lets duplicate lifecycle rows exist and makes terminal updates non-deterministic.

Suggested migration change
-CREATE INDEX idx_check_ins_lifecycle        ON check_ins(monitor_id, check_in_id);
+CREATE UNIQUE INDEX idx_check_ins_lifecycle ON check_ins(monitor_id, check_in_id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- SDK-provided id. Shared between the in_progress and the closing check-in,
-- so the closing status/duration upserts onto the open row. NULL when absent.
check_in_id UUID,
status VARCHAR(16) NOT NULL,
duration DOUBLE PRECISION, -- seconds
environment VARCHAR(64),
trace_id VARCHAR(64),
timestamp TIMESTAMPTZ NOT NULL,
ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_check_ins_monitor ON check_ins(monitor_id, timestamp DESC);
CREATE INDEX idx_check_ins_project_ingested ON check_ins(project_id, ingested_at DESC);
CREATE INDEX idx_check_ins_lifecycle ON check_ins(monitor_id, check_in_id);
-- SDK-provided id. Shared between the in_progress and the closing check-in,
-- so the closing status/duration upserts onto the open row. NULL when absent.
check_in_id UUID,
status VARCHAR(16) NOT NULL,
duration DOUBLE PRECISION, -- seconds
environment VARCHAR(64),
trace_id VARCHAR(64),
timestamp TIMESTAMPTZ NOT NULL,
ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_check_ins_monitor ON check_ins(monitor_id, timestamp DESC);
CREATE INDEX idx_check_ins_project_ingested ON check_ins(project_id, ingested_at DESC);
CREATE UNIQUE INDEX idx_check_ins_lifecycle ON check_ins(monitor_id, check_in_id);
🤖 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 `@apps/server/migrations/postgres/20260629000000_create_monitors.up.sql` around
lines 41 - 56, The check-in schema currently allows duplicate lifecycle rows
because `idx_check_ins_lifecycle` is only a non-unique index on `(monitor_id,
check_in_id)`, which can make terminal upserts hit multiple rows. Update the
`check_ins` table definition in the migration to enforce a unique lifecycle key
for the open/closing check-in pair, using the existing `monitor_id` and
`check_in_id` columns. If needed, adjust the lifecycle index name or add a
unique constraint/index so each check-in lifecycle can only exist once and
terminal updates remain deterministic.

Comment on lines +31 to +49
CREATE TABLE check_ins (
id TEXT PRIMARY KEY,
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,

check_in_id TEXT,

status VARCHAR(16) NOT NULL,
duration REAL,
environment VARCHAR(64),
trace_id VARCHAR(64),

timestamp TEXT NOT NULL,
ingested_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_check_ins_monitor ON check_ins(monitor_id, timestamp DESC);
CREATE INDEX idx_check_ins_project_ingested ON check_ins(project_id, ingested_at DESC);
CREATE INDEX idx_check_ins_lifecycle ON check_ins(monitor_id, check_in_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make the lifecycle index unique.

This schema treats check_in_id as the run identity, but idx_check_ins_lifecycle is only a plain index. That still allows duplicate rows for the same (monitor_id, check_in_id), so a later ok/error close can become ambiguous and check-in history can overcount executions.

Suggested migration change
-CREATE INDEX idx_check_ins_lifecycle        ON check_ins(monitor_id, check_in_id);
+CREATE UNIQUE INDEX idx_check_ins_lifecycle ON check_ins(monitor_id, check_in_id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE check_ins (
id TEXT PRIMARY KEY,
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
check_in_id TEXT,
status VARCHAR(16) NOT NULL,
duration REAL,
environment VARCHAR(64),
trace_id VARCHAR(64),
timestamp TEXT NOT NULL,
ingested_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_check_ins_monitor ON check_ins(monitor_id, timestamp DESC);
CREATE INDEX idx_check_ins_project_ingested ON check_ins(project_id, ingested_at DESC);
CREATE INDEX idx_check_ins_lifecycle ON check_ins(monitor_id, check_in_id);
CREATE TABLE check_ins (
id TEXT PRIMARY KEY,
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
check_in_id TEXT,
status VARCHAR(16) NOT NULL,
duration REAL,
environment VARCHAR(64),
trace_id VARCHAR(64),
timestamp TEXT NOT NULL,
ingested_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_check_ins_monitor ON check_ins(monitor_id, timestamp DESC);
CREATE INDEX idx_check_ins_project_ingested ON check_ins(project_id, ingested_at DESC);
CREATE UNIQUE INDEX idx_check_ins_lifecycle ON check_ins(monitor_id, check_in_id);
🤖 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 `@apps/server/migrations/sqlite/20260629000000_create_monitors.up.sql` around
lines 31 - 49, The lifecycle index on check_ins is currently non-unique, so
duplicate rows can still be inserted for the same monitor_id and check_in_id.
Update the sqlite migration that creates check_ins to make
idx_check_ins_lifecycle a UNIQUE index, using the existing check_ins table
definition and the lifecycle columns monitor_id and check_in_id so each run
identity can only appear once.

Comment thread apps/server/src/config.rs
Comment on lines +107 to +110
monitor_tick_interval_secs: env::var("MONITOR_TICK_INTERVAL_SECS")
.unwrap_or_else(|_| "60".to_string())
.parse()
.unwrap_or(60),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the config and worker definitions, then inspect the relevant slices.
sed -n '1,220p' apps/server/src/config.rs
printf '\n--- monitor worker ---\n'
sed -n '1,160p' apps/server/src/workers/monitor_worker.rs

Repository: rustrak/rustrak

Length of output: 10160


🌐 Web query:

tokio time interval zero duration panic docs

💡 Result:

The Tokio interval function will panic if the provided period is a zero duration [1][2]. This is an intentional design choice enforced by an assertion within the interval function, which requires the period to be strictly greater than a zero-duration Duration [2]. The official documentation explicitly notes this behavior under the Panics section [1][3], and the source code uses assert!(period > Duration::new(0, 0), "period must be non-zero."); to trigger this panic if the condition is not met [2].

Citations:


Reject MONITOR_TICK_INTERVAL_SECS=0 in apps/server/src/config.rs:107-110. tokio::time::interval(Duration::from_secs(0)) panics, and this path currently accepts 0 and passes it through unchanged. Fail config loading or clamp the value to >= 1.

🤖 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 `@apps/server/src/config.rs` around lines 107 - 110, The
MONITOR_TICK_INTERVAL_SECS parsing in Config currently accepts 0 and can later
trigger a tokio::time::interval panic. Update the monitor_tick_interval_secs
initialization in config.rs to reject zero by validating the parsed value and
returning a config load error, or by clamping it to at least 1 before storing it
in the Config struct.

Comment on lines +109 to +129
let updated = if let Some(cid) = check_in_id {
sqlx::query(
r#"
UPDATE check_ins SET
status = $1,
duration = COALESCE($2, duration),
environment = COALESCE($3, environment),
timestamp = $4
WHERE monitor_id = $5 AND check_in_id = $6
"#,
)
.bind(check_in.status.as_str())
.bind(check_in.duration)
.bind(check_in.environment.as_deref())
.bind(ctx.ingested_at)
.bind(monitor_id)
.bind(cid)
.execute(&mut *tx)
.await?
.rows_affected()
> 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Only close the open check-in row.

This UPDATE matches any row with the same check_in_id, not just the active in_progress one. A retried or out-of-order terminal check-in can therefore rewrite a completed run from ok to error (or the reverse) because rows_affected() > 0 suppresses the insert path.

Suggested direction
                 UPDATE check_ins SET
                     status      = $1,
                     duration    = COALESCE($2, duration),
                     environment = COALESCE($3, environment),
                     timestamp   = $4
-                WHERE monitor_id = $5 AND check_in_id = $6
+                WHERE monitor_id = $5
+                  AND check_in_id = $6
+                  AND status = 'in_progress'

After that, if no row was updated, treat an existing non-in_progress row with the same check_in_id as already finalized instead of inserting a duplicate.

🤖 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 `@apps/server/src/digest/processors/check_in.rs` around lines 109 - 129, The
check-in update in `check_in` is too broad because it matches any row with the
same `check_in_id`, which can overwrite already finalized runs. Tighten the
`UPDATE` in `apps/server/src/digest/processors/check_in.rs` so it only targets
the open `in_progress` row for the given `monitor_id` and `check_in_id`, and
then adjust the `rows_affected()` handling to distinguish “already finalized”
from “missing” instead of falling through to an insert. Use the existing
`check_in_id`, `monitor_id`, and `check_in.status` logic in `check_in` to keep
retried or out-of-order terminal events from rewriting completed rows.

Comment on lines +107 to +123
<TableRow
onClick={() => toggle(monitor.slug)}
aria-expanded={isOpen}
className={cn(
'cursor-pointer',
isOpen && 'bg-muted/40 hover:bg-muted/40',
)}
>
<TableCell className="py-2 text-muted-foreground">
<ChevronRight
className={cn(
'size-4 transition-transform',
isOpen && 'rotate-90',
)}
aria-hidden="true"
/>
</TableCell>

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

Make the row toggle keyboard-accessible.

Line 108 puts the expand/collapse action on TableRow, which leaves the recent check-ins panel unreachable from the keyboard. That blocks a core task for non-pointer users.

Proposed fix
-                <TableRow
-                  onClick={() => toggle(monitor.slug)}
-                  aria-expanded={isOpen}
+                <TableRow
                   className={cn(
                     'cursor-pointer',
                     isOpen && 'bg-muted/40 hover:bg-muted/40',
                   )}
                 >
                   <TableCell className="py-2 text-muted-foreground">
-                    <ChevronRight
-                      className={cn(
-                        'size-4 transition-transform',
-                        isOpen && 'rotate-90',
-                      )}
-                      aria-hidden="true"
-                    />
+                    <button
+                      type="button"
+                      onClick={() => toggle(monitor.slug)}
+                      aria-expanded={isOpen}
+                      aria-controls={`monitor-${monitor.id}-checkins`}
+                      className="inline-flex items-center rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+                    >
+                      <ChevronRight
+                        className={cn(
+                          'size-4 transition-transform',
+                          isOpen && 'rotate-90',
+                        )}
+                        aria-hidden="true"
+                      />
+                      <span className="sr-only">
+                        {isOpen ? 'Collapse' : 'Expand'} recent check-ins for {monitor.slug}
+                      </span>
+                    </button>
                   </TableCell>
@@
-                  <TableRow className="hover:bg-transparent">
+                  <TableRow
+                    id={`monitor-${monitor.id}-checkins`}
+                    className="hover:bg-transparent"
+                  >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TableRow
onClick={() => toggle(monitor.slug)}
aria-expanded={isOpen}
className={cn(
'cursor-pointer',
isOpen && 'bg-muted/40 hover:bg-muted/40',
)}
>
<TableCell className="py-2 text-muted-foreground">
<ChevronRight
className={cn(
'size-4 transition-transform',
isOpen && 'rotate-90',
)}
aria-hidden="true"
/>
</TableCell>
<TableRow
className={cn(
'cursor-pointer',
isOpen && 'bg-muted/40 hover:bg-muted/40',
)}
>
<TableCell className="py-2 text-muted-foreground">
<button
type="button"
onClick={() => toggle(monitor.slug)}
aria-expanded={isOpen}
aria-controls={`monitor-${monitor.id}-checkins`}
className="inline-flex items-center rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<ChevronRight
className={cn(
'size-4 transition-transform',
isOpen && 'rotate-90',
)}
aria-hidden="true"
/>
<span className="sr-only">
{isOpen ? 'Collapse' : 'Expand'} recent check-ins for {monitor.slug}
</span>
</button>
</TableCell>
🤖 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 `@apps/webview-ui/src/app/`(main)/projects/[id]/crons/monitors-list.tsx around
lines 107 - 123, The expand/collapse behavior on TableRow in monitors-list.tsx
is mouse-only, so make that row keyboard-accessible. Update the row interaction
in the TableRow/toggle(monitor.slug) block so it can be focused and activated
with keyboard controls (for example Enter/Space), and keep aria-expanded in sync
with isOpen. Use the existing toggle helper and isOpen state to wire the
accessible behavior without changing the panel logic.

Comment on lines +40 to +45
if (options?.page) {
searchParams.page = String(options.page);
}
if (options?.per_page) {
searchParams.per_page = String(options.per_page);
}

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 | 🟡 Minor | ⚡ Quick win

Don't gate numeric query params on truthiness.

page=0 or per_page=0 gets dropped here instead of being forwarded or rejected, which silently changes the request semantics. Check for !== undefined before serializing, and validate positivity separately if the API requires 1-based paging.

Suggested fix
-    if (options?.page) {
+    if (options?.page !== undefined) {
       searchParams.page = String(options.page);
     }
-    if (options?.per_page) {
+    if (options?.per_page !== undefined) {
       searchParams.per_page = String(options.per_page);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (options?.page) {
searchParams.page = String(options.page);
}
if (options?.per_page) {
searchParams.per_page = String(options.per_page);
}
if (options?.page !== undefined) {
searchParams.page = String(options.page);
}
if (options?.per_page !== undefined) {
searchParams.per_page = String(options.per_page);
}
🤖 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 `@packages/client/src/resources/monitors.ts` around lines 40 - 45, The query
param handling in monitors.ts is incorrectly gated on truthiness, so numeric
values like page=0 or per_page=0 are skipped instead of being serialized or
explicitly rejected. Update the parameter checks in the monitor request builder
to test for options.page and options.per_page being !== undefined, and keep any
1-based/positive validation separate from serialization so the behavior is
explicit and consistent.

Comment on lines +23 to +25
it('throws NotFoundError for an unknown project', async () => {
await expect(client.monitors.list(999)).rejects.toThrow();
});

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 | 🟡 Minor | ⚡ Quick win

Assert the specific error type here.

rejects.toThrow() will pass for any failure path, including transport or schema errors. If this is meant to verify NotFoundError mapping, assert that class explicitly.

🤖 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 `@packages/client/tests/integration/monitors.test.ts` around lines 23 - 25, The
test for an unknown project currently uses a generic rejection assertion, which
does not verify the intended error mapping. Update the
`client.monitors.list(999)` expectation in `monitors.test.ts` to assert the
specific `NotFoundError` class explicitly, using the same test case name and
`rejects` chain so the check confirms the correct error type rather than any
thrown failure.

Comment on lines +1226 to +1248
return HttpResponse.json({
items: [
{
id: 'c1c2c3c4-e89b-12d3-a456-426614174000',
status: 'ok',
duration: 12.5,
environment: 'production',
trace_id: null,
timestamp: '2026-06-18T00:00:01.000Z',
},
{
id: 'd1d2d3d4-e89b-12d3-a456-426614174000',
status: 'error',
duration: null,
environment: null,
trace_id: null,
timestamp: '2026-06-17T00:00:01.000Z',
},
],
total_count: 2,
page: 1,
per_page: 20,
total_pages: 1,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

This fixture locks the new endpoint into offset pagination.

The mock response uses page/per_page/total_pages, which bakes the new monitors check-ins API into an offset-based contract. If this endpoint is meant to follow the repo standard, it should expose cursor pagination (next_cursor, has_more) instead, and the fixture/tests should match that before the contract spreads further.

As per coding guidelines, packages/client/src/**/*.ts must “Implement cursor-based pagination in all list endpoints using next_cursor and has_more fields.”

🤖 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 `@packages/client/tests/mocks/handlers.ts` around lines 1226 - 1248, The mock
fixture for the monitors check-ins list endpoint is using offset pagination
fields, which conflicts with the repo’s cursor-based list contract. Update the
response shape in the relevant handlers fixture to use the same cursor
pagination fields as other list endpoints, specifically `next_cursor` and
`has_more`, and remove the `page`/`per_page`/`total_pages` assumptions so tests
and the API mock stay aligned.

Source: Coding guidelines

Comment on lines +110 to +113
main().catch((err) => {
console.error(err);
process.exit(1);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For @sentry/node 10.59.0, what is the recommended way to flush pending events or check-ins before terminating a Node.js process after an uncaught error?

💡 Result:

In @sentry/node 10.59.0, flushing pending events before terminating a Node.js process after an uncaught error is handled by calling Sentry.flush(timeout) [1][2]. For reliable flushing when your application encounters a fatal error, you should invoke Sentry.flush(timeout) manually before exiting the process [3]. The flush method returns a Promise that resolves when all pending events have been sent or the specified timeout (in milliseconds) has elapsed [1]. Important Considerations: 1. Manual Flushing: When handling uncaught exceptions, you are responsible for calling Sentry.flush before your application calls process.exit [3]. Without this, because process.exit terminates the process synchronously, any ongoing network requests used to send events will be aborted, causing data loss [3]. 2. Default Integration: The OnUncaughtException integration, which is enabled by default, captures these errors and handles the process exit automatically [4]. If you wish to perform custom cleanup, you can configure this integration (using the onFatalError callback) or disable the default behavior and implement your own handler to ensure Sentry.flush is called appropriately [4][5]. 3. Flush vs. Close: Use Sentry.flush(timeout) if you intend to continue using the client momentarily. Use Sentry.close(timeout) if you are permanently shutting down the application, as it flushes all pending events and disables the SDK [1]. 4. Node.js Behavior: Node.js documentation advises that after an uncaughtException, the process is in an undefined state and should be terminated [6][7]. Performing asynchronous operations like flushing Sentry events after an uncaught error carries inherent risks, but Sentry.flush is the intended mechanism to attempt this reliably before termination [3]. For a timeout, it is recommended to provide a reasonable duration (e.g., 2000 ms) to balance between waiting for network requests and ensuring the process eventually terminates [8]. Note that in recent versions of @sentry/node, internal improvements have been made to ensure flushing is more robust [8][9], but manually awaiting the flush remains the standard practice for guaranteed delivery before exit.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file first, then inspect the nearby lines.
ast-grep outline packages/test-sentry/demo/src/crons.ts --view expanded || true
wc -l packages/test-sentry/demo/src/crons.ts
sed -n '1,180p' packages/test-sentry/demo/src/crons.ts

# Find any existing flush/close handling in this demo package.
rg -n "Sentry\.(flush|close)|flush\(" packages/test-sentry/demo -S

Repository: rustrak/rustrak

Length of output: 4415


Flush before exiting on the error path. The success path already awaits Sentry.flush(2000), but main().catch(...) exits immediately, so the last batched check-ins can be dropped if main() rejects. Await a flush here before process.exit(1).

🤖 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 `@packages/test-sentry/demo/src/crons.ts` around lines 110 - 113, The error
path in main().catch currently exits immediately after logging, which can drop
pending Sentry events. Update the catch handler in crons.ts so it awaits
Sentry.flush(2000) before calling process.exit(1), matching the flush behavior
used on the success path and preserving any final batched check-ins.

@AbianS AbianS moved this to Exploring in Rustrak Roadmap Jul 23, 2026

This branch has not been deployed

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

Labels

None yet

Projects

Status: Exploring

Development

Successfully merging this pull request may close these issues.

1 participant