Skip to content

Monitor history: per-check logging, daily aggregation, and detail UI (issue #19)#68

Merged
rathorevaibhav merged 14 commits into
mainfrom
feat/issue-19-monitor-history-plan
Jun 24, 2026
Merged

Monitor history: per-check logging, daily aggregation, and detail UI (issue #19)#68
rathorevaibhav merged 14 commits into
mainfrom
feat/issue-19-monitor-history-plan

Conversation

@rathorevaibhav

@rathorevaibhav rathorevaibhav commented Feb 14, 2026

Copy link
Copy Markdown
Member

Summary

Implements monitor history (issue #19): every uptime/domain/certificate check is logged, rolled up into daily metrics, and surfaced on a new monitor detail page with GitHub-style calendar heatmaps, per-day tooltips, range filters, and totals.

The whole feature is gated behind a MONITOR_HISTORY_ENABLED flag (default off), so it can be merged and rolled out independently.

Note: this branch started as just the implementation plan (plans/issue-19-monitor-history-implementation-plan.md) and grew into the full phase 0–5 implementation. Title/description updated to match.

What's included

Data layer (Phase 1)

  • monitor_check_logs (raw per-check rows) + monitor_daily_check_metrics (pre-aggregated) migrations, models, relations, and query scopes.

Ingestion (Phase 2)

  • Uptime: overrides Monitor::uptimeRequestSucceeded/Failed to log one row per attempt.
  • Certificate: CertificateCheckSucceeded/Failed listeners.
  • Domain: DomainService::verifyDomainExpiration() returns a normalized DTO and logs every run.
  • MonitorCheckLogService writes via firstOrCreate on an idempotency key.

Aggregation + API (Phase 3)

  • MonitorDailyCheckMetricsAggregator + monitor:aggregate-check-metrics command (incremental, scheduled hourly).
  • MonitorsController@show exposes daily heatmap points, all-time + selected-range totals, and recent checks.

Detail UI (Phase 4)

  • New Monitors/Show.jsx, reusable MonitorHistoryHeatmap, summary cards, range filters (7d/30d/all/custom), recent-checks table.

Ops (Phase 5)

  • monitor:backfill-check-history and monitor:prune-check-history commands, scheduler entries, and docs/monitor-history.md.

Related


✅ Fixed (commit 8b17248)

Reviewed the implementation against the plan's acceptance criteria / Definition of Done and fixed:

Blockers

  • Migrations failed on MySQL. monitor_id now created as unsigned INT to match monitors.id (increments()); foreignId() produced BIGINT and aborted migrate with SQLSTATE 3780. Verified up and down on MySQL.
  • Heatmaps rendered empty for non-UTC users. Daily metrics are now read back under the same server-side timezone they're aggregated with (config('monitor-history.timezone'), single source of truth for both the command and the controller). The detail page no longer trusts the browser timezone.

Correctness

  • Idempotency key reduced to monitor_id + check_type + status + checked_at(second) (per the plan), so a quick command retry of the same check de-dupes instead of the old message/metadata-sensitive key that never collapsed retries.
  • Recent checks now respect the selected date range (was always the global newest 50).
  • Disabled / not-applicable check typesshow() advertises each check type with an enabled flag; the UI renders an explicit "not enabled" panel instead of omitting domain or showing a permanently empty certificate heatmap.
  • Domain "expires today" message is reachable again (diffInDays float cast to int).
  • Heatmap legend now matches the actual cell color scale (adds warning/unknown).
  • Custom-range inputs stay in sync with the range the server actually applied.

Tests (Definition of Done #4)

  • Suite now runs against a dedicated monitor_test MySQL database (with a guard test) so RefreshDatabase exercises the real engine and the FK regression is caught.
  • Added feature/unit coverage: log persistence + idempotency, aggregation math, show payload (auth, timezone, check_types, recent-range), and domain outcome mapping. 32 passed.

🔭 Deferred (follow-ups, not blocking)

  • response_time_ms for uptime stays null — Spatie's Guzzle flow doesn't expose transfer timing to the override; populating it needs Guzzle on_stats middleware. avg/p95 remain null-safe meanwhile.
  • Recent-checks pagination — now range-aware, but still capped at the newest N (no paging controls yet).
  • WHOIS failure detail — domain lookup failures still log a generic reason; threading the underlying error through is a follow-up.
  • Frontend test tooling — no JS test runner configured; the React components were verified via the production build only.
  • Minor: month range preset; aggregator chunking for very large windows; "history starts on " messaging.

🤖 Generated with Claude Code

@rathorevaibhav rathorevaibhav changed the title Add implementation plan for monitor history (issue #19) Monitor history: per-check logging, daily aggregation, and detail UI (issue #19) Jun 24, 2026
rathorevaibhav and others added 5 commits June 24, 2026 21:46
…te mismatch

Blockers:
- Migrations now create monitor_id as unsigned INT to match monitors.id
  (increments). foreignId() produced BIGINT and aborted `migrate` on MySQL
  with error 3780. Verified up/down on MySQL.
- Daily metrics are now read back under the same server-side timezone they
  are aggregated with. The detail page previously queried metrics using the
  browser timezone while the scheduler only ever wrote UTC rows, so heatmaps
  rendered empty for every non-UTC user. Added config('monitor-history.timezone')
  as the single source of truth for both the aggregate command and the controller.

Correctness:
- Idempotency key reduced to monitor_id + check_type + status + checked_at(second),
  per the plan, so a quick command retry of the same check de-dupes instead of the
  previous message/metadata-sensitive key that never collapsed retries.
- Recent checks now respect the selected date range (was always the global newest 50).
- show() payload now advertises each check type with an `enabled` flag; the UI
  renders an explicit "not enabled" panel instead of omitting domain or showing a
  permanently empty certificate heatmap.
- Domain "expires today" message is reachable again (diffInDays float cast to int).
- Heatmap legend now reflects the real cell color scale (adds warning/unknown).
- Custom-range inputs stay in sync with the range the server actually applied.

Tests:
- Configure the suite against a dedicated monitor_test MySQL database (with a guard
  test) so RefreshDatabase exercises the real engine and the FK regression is caught.
- Add feature/unit coverage for log persistence + idempotency, aggregation math,
  the show payload (auth, timezone, check_types, recent-range), and domain outcome
  mapping. Full suite: 32 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seeds 4 demo monitors with distinct health profiles (healthy, flaky,
expiring-domain, recovered-outage) and fabricates multi-day uptime/domain/
certificate check logs, then aggregates them into daily metrics so the
monitor detail page heatmaps, tooltips, and totals have realistic data to
render locally.

- Run: php artisan db:seed --class=MonitorHistorySeeder
- Window length via config('monitor-history.seed_days') / MONITOR_HISTORY_SEED_DAYS (default 90)
- Re-runnable (updateOrCreate by URL, clears prior seeded history)
- Guarded against running in production

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itch

MONITOR_HISTORY_ENABLED was read-path only, so "default off" still ran
per-check ingestion and the hourly aggregate / daily prune jobs. Plus
follow-up correctness and clarity fixes from the code review.

Feature flag (blocker):
- Add MonitorCheckLogService::logCheckIfEnabled(); all automatic per-check
  ingestion (uptime hooks, certificate listeners, domain verification) now
  no-ops when the flag is off. logCheck() stays unconditional for operator
  paths (backfill, seeder) so history can still be pre-staged.
- Gate monitor:aggregate-check-metrics and monitor:prune-check-history behind
  ->when(config('monitor-history.enabled')) and add withoutOverlapping().
  Core uptime/certificate/domain checks remain ungated.

Correctness / clarity:
- Extract DomainService::resolveExpirationNotifications() and cover the
  expiry-warning thresholds with tests (locks the whole-day int behaviour).
- Reword monitor:backfill-check-history so it no longer implies multi-day
  history: it writes one current-state snapshot per enabled check type and
  --days only sizes the aggregation window. Locked with a test.
- Heatmap legend now reflects every colour getCellClasses() emits (green/red
  gradients + yellow), not a single swatch per status.

Docs / hygiene:
- Document the flag semantics, a revised rollout checklist, and the dedicated
  monitor_test MySQL database the suite requires.
- Untrack the accidentally committed .phpunit.cache artifact and gitignore it.

Tests: 48 passed (was 34). Pint clean. Frontend build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rathorevaibhav rathorevaibhav merged commit e3891c0 into main Jun 24, 2026
1 check passed
@rathorevaibhav rathorevaibhav deleted the feat/issue-19-monitor-history-plan branch June 24, 2026 17:54
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.

Monitor history

1 participant