Conversation
Introduced new APIs for throughput forecasting, including endpoints for retrieving forecasts, accessing settings, and updating settings. Added corresponding types and a comprehensive test suite for the `ThroughputForecastingService`. Enhanced `WebhookForm` event options to support throughput limit alerts.
Split throughput forecasting page into reusable components (`ForecastCard`, `ThroughputChart`, `SettingsPanel`, etc.) and utility functions to improve code organization and maintainability. Updated imports to use shared types from `@betterdb/shared`.
…hputForecasting page
…hputForecasting page
apps/api/src/throughput-forecasting/throughput-forecasting.service.ts
Outdated
Show resolved
Hide resolved
apps/api/src/throughput-forecasting/throughput-forecasting.controller.ts
Outdated
Show resolved
Hide resolved
apps/web/src/components/pages/throughput-forecasting/ForecastCard.tsx
Outdated
Show resolved
Hide resolved
This comment was marked as outdated.
This comment was marked as outdated.
Replaced custom polling hooks with `react-query` for data fetching, caching, and synchronization in throughput forecasting workflows. Adjusted API calls, updated components to use `react-query` hooks, and removed redundant `usePolling` implementation. Enhanced error handling and refetch mechanisms for improved user experience. Extract data-fetching logic from page components into dedicated hooks using @tanstack/react-query for caching, deduplication, and polling. Add Vitest test infrastructure and unit tests for all new hooks.
apps/api/src/throughput-forecasting/throughput-forecasting.controller.ts
Outdated
Show resolved
Hide resolved
apps/api/src/throughput-forecasting/throughput-forecasting.controller.ts
Outdated
Show resolved
Hide resolved
Generalize throughput forecasting into a MetricForecastingService parameterized by MetricKind. Add metric extractors, ceiling resolvers (with auto-detect for memory maxmemory), unified storage table, REST endpoints, Prometheus export, and a tabbed frontend page at /forecasting. Add tests for checkAlerts dispatch, ceiling-exceeded paths, falling/ stable trends for non-ops metrics, zero-slope behavior, connection isolation, data sufficiency boundaries, formatter edge cases, and validation pipe edge cases. Add `docker-compose.test.yml` for isolated test environments using dedicated containers and ports. Refactor metric forecasting to use actual values for fast spike detection. Enhance tier validation with `DEV_LICENSE_TIER` override for development. Adjust global test setup/teardown scripts for test containers. Aligns setting names with the generic metric forecasting feature, replacing the old throughput-specific naming. DB column names are unchanged to avoid requiring a migration. Improve logging in `checkAlerts` for uninitialized services. Add safe fallback values for hysteresis recovery during webhook dispatch. Refactor to use `WebhookEventType` for consistency. Update tests to validate dispatch behavior with recovery values. --------- Co-authored-by: Kristiyan Ivanov <k.ivanow@gmail.com>
apps/api/src/metric-forecasting/metric-forecasting.controller.ts
Outdated
Show resolved
Hide resolved
apps/web/src/components/pages/metric-forecasting/MetricSettingsPanel.tsx
Outdated
Show resolved
Hide resolved
This comment was marked as outdated.
This comment was marked as outdated.
The storage-port interface re-exports it as 'export type', which cannot be used as a runtime value. Import directly from shared.
apps/api/src/metric-forecasting/metric-forecasting.controller.ts
Outdated
Show resolved
Hide resolved
This comment was marked as outdated.
This comment was marked as outdated.
|
Thanks for the review. Here's the status of each item: HIGH — checkAlerts dispatches unconditionally: Intentional for hysteresis recovery. The dispatcher's HIGH — HIGH — No auth guards: Verified — MEDIUM — No explicit ValidationPipe on @Body: Global MEDIUM — LOW — LOW — Negative trend line: Fixed — clamped to Minor — Column naming: Intentional to avoid DB migration. Documented in code comments. |
apps/api/src/metric-forecasting/dto/update-metric-forecast-settings.dto.ts
Show resolved
Hide resolved
apps/api/src/metric-forecasting/metric-forecasting.controller.ts
Outdated
Show resolved
Hide resolved
Review: Throughput Forecasting UI and APIGood overall structure — the linear regression is numerically sound (x-normalisation avoids epoch cancellation), the storage adapters all use UPSERT correctly, and the test coverage is solid. Three issues need attention before merge; one is low-severity but tracked. High
After the Fix: add MediumTOCTOU race in The read-then-conditional-write is not atomic. If a concurrent Hard-coded
No cross-field validation: A 24 h alert threshold with a 1 h rolling window will never trigger because the regression can't project that far ahead. The combination is silently accepted and produces a perpetually-inactive alert. See inline comment. Low
The registry service already has |
- Prevent concurrent checkAlerts runs with a boolean guard - Export ENV_DEFAULT_ID from connection-registry and use it in the metric forecasting controller
apps/api/src/metric-forecasting/dto/update-metric-forecast-settings.dto.ts
Show resolved
Hide resolved
Review: Throughput Forecasting UI and APIOverall the feature is well-structured — the linear regression is numerically sound (normalised timestamps to avoid floating-point cancellation), the hysteresis path for webhook alerts goes through the existing Four issues need attention before merge. High — Snapshot
|
…m/BetterDB-inc/monitor into feature/61-throughput-forecasting
| @IsNumber() | ||
| @Min(60_000) | ||
| @Max(86_400_000) | ||
| alertThresholdMs?: number; |
There was a problem hiding this comment.
Medium — Silent misconfiguration: alertThresholdMs can exceed rollingWindowMs
Both fields are validated independently, but there is no cross-field constraint. If a user sets rollingWindowMs = 3_600_000 (1 h) and alertThresholdMs = 86_400_000 (24 h), the forecast can project at most ~1 h of growth, so timeToLimitMs will essentially never drop below a 24 h threshold. The alert silently never fires.
Add a custom validator (or a simple @ValidateIf-based check) that enforces alertThresholdMs <= rollingWindowMs:
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';
function IsLessThanOrEqual(property: string, opts?: ValidationOptions) {
return (object: object, propertyName: string) => {
registerDecorator({
name: 'isLessThanOrEqual',
target: (object as any).constructor,
propertyName,
constraints: [property],
options: opts,
validator: {
validate(value: any, args: ValidationArguments) {
const other = (args.object as any)[args.constraints[0]];
return typeof value === 'number' && typeof other === 'number' ? value <= other : true;
},
defaultMessage: (args) => `${args.property} must be ≤ ${args.constraints[0]}`,
},
});
};
}Then annotate alertThresholdMs with @IsLessThanOrEqual('rollingWindowMs'). Because both fields are optional in a PATCH, you also need to validate at the service layer (reading existing settings to resolve the final values) if only one of the two is updated.
| const growthPercent = | ||
| predictedStart !== 0 | ||
| ? ((predictedEnd - predictedStart) / Math.abs(predictedStart)) * 100 | ||
| : predictedEnd !== 0 | ||
| ? 100 // growing from zero — treat as significant rise | ||
| : 0; |
There was a problem hiding this comment.
Medium — Misleading trendDirection when metric falls from near-zero
When predictedStart === 0 (regression line starts at zero) and predictedEnd < 0 (negative slope — metric falling away from zero), the ternary returns 100, making trendDirection = 'rising'. The slope <= 0 guard on line 187 correctly prevents a negative timeToLimitMs, so no incorrect alert fires, but the returned MetricForecast object has the contradictory state:
trendDirection: 'rising'
timeToLimitHuman: 'Not projected to reach ceiling'
Any UI or downstream consumer that displays trendDirection directly will show an upward trend indicator for a metric that is actually falling.
Fix the ternary so a negative predictedEnd with zero predictedStart is treated as falling rather than rising:
const growthPercent =
predictedStart !== 0
? ((predictedEnd - predictedStart) / Math.abs(predictedStart)) * 100
: predictedEnd > 0
? 100 // growing from zero — treat as significant rise
: predictedEnd < 0
? -100 // falling from zero — treat as significant fall
: 0;| for (const settings of activeSettings) { | ||
| try { | ||
| const forecast = await this.getForecast(settings.connectionId, settings.metricKind); | ||
| if (!forecast.enabled || forecast.insufficientData) continue; |
There was a problem hiding this comment.
Low — Unnecessary work for stable/falling metrics on every alert tick
When timeToLimitMs === null (metric is stable or falling), checkAlerts still calls dispatchMetricForecastLimit with Number.MAX_SAFE_INTEGER. shouldFireAlert will return false immediately because MAX_SAFE_INTEGER <= threshold is never true, but the call still incurs a getWebhooksByEvent DB query and an LRU cache lookup for every stable metric on every 60-second tick.
Consider adding an early-exit after the debug log:
if (forecast.timeToLimitMs === null) continue; // stable/falling — no alert neededThis makes the intent explicit and avoids the wasted DB query per stable metric per minute.
Review SummaryOverall the implementation is solid: linear regression is properly normalized (avoiding the catastrophic cancellation that was fixed in an earlier commit), the hysteresis logic in Four issues worth addressing before merge: Medium — Missing cross-field validation:
|
Summary
Test plan
pnpm test🤖 Generated with Claude Code