diff --git a/backend/services/index.ts b/backend/services/index.ts index a1a2b04c..653dbc0b 100644 --- a/backend/services/index.ts +++ b/backend/services/index.ts @@ -353,6 +353,19 @@ export type { SlaMonitoringEvent, } from './shared/slaMonitoring'; +// ── Subscription SLA Monitoring (Issue #779) ────────────────────────────── +export { SubscriptionSlaMonitoringService } from './subscriptionSlaMonitoring'; +export type { + SlaMetricKind as SubSlaMetricKind, + SlaBreachSeverity as SubSlaBreachSeverity, + SubscriptionTier as SubSlaTier, + SubscriptionSlaTarget as SubSlaTarget, + SlaMetricSample as SubSlaMetricSample, + SlaBreachRecord as SubSlaBreachRecord, + SlaSubscriptionConfig as SubSlaSubscriptionConfig, + SlaHealthSummary as SubSlaHealthSummary, +} from './subscriptionSlaMonitoring'; + // ── Group Billing (Issue #732) ──────────────────────────────────────────── export { GroupBillingService, groupBillingService } from './billing/groupBilling'; export type { diff --git a/backend/services/subscriptionSlaMonitoring.ts b/backend/services/subscriptionSlaMonitoring.ts new file mode 100644 index 00000000..e5501b15 --- /dev/null +++ b/backend/services/subscriptionSlaMonitoring.ts @@ -0,0 +1,484 @@ +/** + * Backend Subscription SLA Monitoring Service + * + * Server-side service that orchestrates real-time SLA monitoring for + * subscriptions, integrates with the alerting system, and provides + * periodic SLA health checks. + * + * Designed to run as a background service that: + * 1. Accepts metric samples from instrumented endpoints + * 2. Evaluates SLA compliance in real-time + * 3. Detects breaches and dispatches alerts + * 4. Generates periodic SLA reports + * + * @see https://github.com/Smartdevs17/SubTrackr/issues/779 + */ + +import type { AlertChannelConfig } from '../services/types'; +import { AlertingService } from '../services/alerting'; + +// ── Types ───────────────────────────────────────────────────────────────────── + +export type SlaMetricKind = 'uptime' | 'response_time' | 'error_rate' | 'latency' | 'throughput'; +export type SlaBreachSeverity = 'warning' | 'minor' | 'major' | 'critical'; +export type SubscriptionTier = 'free' | 'basic' | 'standard' | 'premium' | 'enterprise'; + +export interface SubscriptionSlaTarget { + uptimeTarget: number; + maxResponseTimeMs: number; + maxErrorRate: number; + maxLatencyMs: number; + creditPercentage: number; + maxCreditCap: number; +} + +export interface SlaMetricSample { + kind: SlaMetricKind; + value: number; + timestamp: number; + subscriptionId: string; +} + +export interface SlaBreachRecord { + id: string; + subscriptionId: string; + tier: SubscriptionTier; + severity: SlaBreachSeverity; + metricKind: SlaMetricKind; + targetValue: number; + actualValue: number; + deviationPercent: number; + creditIssued: number; + detectedAt: number; + resolvedAt: number | null; + acknowledged: boolean; +} + +export interface SlaSubscriptionConfig { + subscriptionId: string; + tier: SubscriptionTier; + targets: SubscriptionSlaTarget; + checkIntervalMs: number; + alertContacts: string[]; +} + +export interface SlaHealthSummary { + totalSubscriptions: number; + compliantCount: number; + activeBreaches: number; + averageUptime: number; + totalCreditsIssued: number; +} + +// ── Default tier targets ────────────────────────────────────────────────────── + +const DEFAULT_TARGETS: Record = { + free: { + uptimeTarget: 95, + maxResponseTimeMs: 5000, + maxErrorRate: 10, + maxLatencyMs: 3000, + creditPercentage: 0, + maxCreditCap: 0, + }, + basic: { + uptimeTarget: 99, + maxResponseTimeMs: 2000, + maxErrorRate: 5, + maxLatencyMs: 1500, + creditPercentage: 5, + maxCreditCap: 50, + }, + standard: { + uptimeTarget: 99.5, + maxResponseTimeMs: 1000, + maxErrorRate: 2, + maxLatencyMs: 800, + creditPercentage: 10, + maxCreditCap: 100, + }, + premium: { + uptimeTarget: 99.9, + maxResponseTimeMs: 500, + maxErrorRate: 1, + maxLatencyMs: 300, + creditPercentage: 15, + maxCreditCap: 250, + }, + enterprise: { + uptimeTarget: 99.99, + maxResponseTimeMs: 200, + maxErrorRate: 0.1, + maxLatencyMs: 100, + creditPercentage: 25, + maxCreditCap: 500, + }, +}; + +// ── Service ─────────────────────────────────────────────────────────────────── + +export class SubscriptionSlaMonitoringService { + private configs = new Map(); + private samples = new Map(); + private breaches: SlaBreachRecord[] = []; + private alerting: AlertingService; + private checkTimers = new Map>(); + + /** Maximum metric samples retained per subscription */ + private readonly maxSamplesPerSub: number; + + constructor( + alertChannels: AlertChannelConfig[] = [{ type: 'console' }], + maxSamplesPerSub = 500 + ) { + this.alerting = new AlertingService(alertChannels); + this.maxSamplesPerSub = maxSamplesPerSub; + } + + // ── Configuration ───────────────────────────────────────────────────────── + + /** + * Register or update SLA monitoring for a subscription. + */ + configureSubscription( + subscriptionId: string, + tier: SubscriptionTier, + overrides?: Partial, + alertContacts: string[] = [], + checkIntervalMs = 300_000 + ): SlaSubscriptionConfig { + const targets = { ...DEFAULT_TARGETS[tier], ...overrides }; + const config: SlaSubscriptionConfig = { + subscriptionId, + tier, + targets, + checkIntervalMs, + alertContacts, + }; + + this.configs.set(subscriptionId, config); + + // Set up periodic SLA checks + this.startPeriodicCheck(subscriptionId, checkIntervalMs); + + return config; + } + + /** + * Remove SLA monitoring for a subscription. + */ + removeSubscription(subscriptionId: string): void { + this.configs.delete(subscriptionId); + this.samples.delete(subscriptionId); + this.breaches = this.breaches.filter((b) => b.subscriptionId !== subscriptionId); + + const timer = this.checkTimers.get(subscriptionId); + if (timer) { + clearInterval(timer); + this.checkTimers.delete(subscriptionId); + } + } + + // ── Metric ingestion ────────────────────────────────────────────────────── + + /** + * Record a metric sample for a subscription. + * Automatically triggers SLA evaluation. + */ + recordMetric(sample: SlaMetricSample): SlaBreachRecord | null { + const existing = this.samples.get(sample.subscriptionId) ?? []; + existing.push(sample); + + // Trim to max samples + if (existing.length > this.maxSamplesPerSub) { + existing.splice(0, existing.length - this.maxSamplesPerSub); + } + + this.samples.set(sample.subscriptionId, existing); + return this._evaluateMetric(sample); + } + + /** + * Record multiple metric samples at once. + */ + recordMetricBatch(samples: SlaMetricSample[]): SlaBreachRecord[] { + const newBreaches: SlaBreachRecord[] = []; + for (const sample of samples) { + const breach = this.recordMetric(sample); + if (breach) newBreaches.push(breach); + } + return newBreaches; + } + + // ── Breach management ───────────────────────────────────────────────────── + + /** + * Get all active (unresolved) breaches. + */ + getActiveBreaches(): SlaBreachRecord[] { + return this.breaches.filter((b) => b.resolvedAt === null); + } + + /** + * Get breaches for a specific subscription. + */ + getBreachesForSubscription(subscriptionId: string): SlaBreachRecord[] { + return this.breaches.filter((b) => b.subscriptionId === subscriptionId); + } + + /** + * Acknowledge a breach. + */ + acknowledgeBreach(breachId: string): boolean { + const breach = this.breaches.find((b) => b.id === breachId); + if (!breach) return false; + breach.acknowledged = true; + return true; + } + + /** + * Resolve a breach. + */ + resolveBreach(breachId: string): boolean { + const breach = this.breaches.find((b) => b.id === breachId); + if (!breach) return false; + breach.resolvedAt = Date.now(); + return true; + } + + // ── Health & analytics ──────────────────────────────────────────────────── + + /** + * Get a summary of SLA health across all monitored subscriptions. + */ + getHealthSummary(): SlaHealthSummary { + const configList = Array.from(this.configs.values()); + const total = configList.length; + const activeBreaches = this.getActiveBreaches(); + + const subscriptionsWithBreaches = new Set(activeBreaches.map((b) => b.subscriptionId)); + const compliantCount = total - subscriptionsWithBreaches.size; + + const totalCreditsIssued = this.breaches.reduce((sum, b) => sum + b.creditIssued, 0); + + // Calculate average uptime from recent samples + let uptimeSum = 0; + let uptimeCount = 0; + for (const [, samples] of this.samples) { + const uptimeSamples = samples.filter((s) => s.kind === 'uptime'); + if (uptimeSamples.length > 0) { + const avg = uptimeSamples.reduce((s, m) => s + m.value, 0) / uptimeSamples.length; + uptimeSum += avg; + uptimeCount++; + } + } + + return { + totalSubscriptions: total, + compliantCount, + activeBreaches: activeBreaches.length, + averageUptime: uptimeCount > 0 ? Number((uptimeSum / uptimeCount).toFixed(4)) : 100, + totalCreditsIssued: Number(totalCreditsIssued.toFixed(2)), + }; + } + + /** + * Get all registered subscription configurations. + */ + getConfigs(): SlaSubscriptionConfig[] { + return Array.from(this.configs.values()); + } + + /** + * Get all breaches. + */ + getAllBreaches(): SlaBreachRecord[] { + return [...this.breaches]; + } + + // ── Cleanup ─────────────────────────────────────────────────────────────── + + /** + * Stop all periodic checks and release resources. + */ + shutdown(): void { + for (const [, timer] of this.checkTimers) { + clearInterval(timer); + } + this.checkTimers.clear(); + } + + // ── Internal ────────────────────────────────────────────────────────────── + + private startPeriodicCheck(subscriptionId: string, intervalMs: number): void { + // Clear existing timer + const existing = this.checkTimers.get(subscriptionId); + if (existing) clearInterval(existing); + + const timer = setInterval(() => { + this._runPeriodicCheck(subscriptionId); + }, intervalMs); + + this.checkTimers.set(subscriptionId, timer); + } + + private _runPeriodicCheck(subscriptionId: string): void { + const config = this.configs.get(subscriptionId); + if (!config) return; + + const samples = this.samples.get(subscriptionId) ?? []; + + // Check each metric kind against targets + const metricKinds: SlaMetricKind[] = ['uptime', 'response_time', 'error_rate', 'latency']; + for (const kind of metricKinds) { + const kindSamples = samples.filter((s) => s.kind === kind); + if (kindSamples.length === 0) continue; + + const avg = kindSamples.reduce((s, m) => s + m.value, 0) / kindSamples.length; + const syntheticSample: SlaMetricSample = { + kind, + value: avg, + timestamp: Date.now(), + subscriptionId, + }; + this._evaluateMetric(syntheticSample); + } + } + + private _evaluateMetric(sample: SlaMetricSample): SlaBreachRecord | null { + const config = this.configs.get(sample.subscriptionId); + if (!config) return null; + + const { targets } = config; + let target: number; + let breached: boolean; + + switch (sample.kind) { + case 'uptime': + target = targets.uptimeTarget; + breached = sample.value < target; + break; + case 'response_time': + target = targets.maxResponseTimeMs; + breached = sample.value > target; + break; + case 'error_rate': + target = targets.maxErrorRate; + breached = sample.value > target; + break; + case 'latency': + target = targets.maxLatencyMs; + breached = sample.value > target; + break; + default: + return null; + } + + if (!breached) { + // Auto-resolve active breach for this metric if now compliant + const activeBreach = this.breaches.find( + (b) => + b.subscriptionId === sample.subscriptionId && + b.metricKind === sample.kind && + b.resolvedAt === null + ); + if (activeBreach) { + activeBreach.resolvedAt = Date.now(); + } + return null; + } + + // Check if there's already an active breach for this metric + const existingBreach = this.breaches.find( + (b) => + b.subscriptionId === sample.subscriptionId && + b.metricKind === sample.kind && + b.resolvedAt === null + ); + if (existingBreach) return null; + + // Create new breach + const deviationPercent = target !== 0 ? ((sample.value - target) / target) * 100 : 0; + const severity = this._classifySeverity(Math.abs(deviationPercent), sample.kind); + const credit = this._calculateCredit(severity, targets.creditPercentage, targets.maxCreditCap); + + const breach: SlaBreachRecord = { + id: `sla-breach-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, + subscriptionId: sample.subscriptionId, + tier: config.tier, + severity, + metricKind: sample.kind, + targetValue: target, + actualValue: Number(sample.value.toFixed(4)), + deviationPercent: Number(deviationPercent.toFixed(2)), + creditIssued: credit, + detectedAt: Date.now(), + resolvedAt: null, + acknowledged: false, + }; + + this.breaches.push(breach); + + // Dispatch alert + void this.alerting.dispatch({ + id: `sla-alert-${breach.id}`, + severity: severity === 'critical' ? 'critical' : severity === 'major' ? 'warning' : 'info', + title: `SLA Breach: ${this._formatMetricKind(sample.kind)} (${config.tier} tier)`, + message: + `Subscription ${sample.subscriptionId} breached ${this._formatMetricKind(sample.kind)} SLA. ` + + `Target: ${target}, Actual: ${sample.value.toFixed(2)}, ` + + `Deviation: ${deviationPercent.toFixed(2)}%. Credit issued: ${credit}.`, + timestamp: Date.now(), + resolved: false, + ruleId: `sla-${sample.kind}`, + }); + + return breach; + } + + private _classifySeverity( + absDeviationPercent: number, + kind: SlaMetricKind + ): SlaBreachSeverity { + if (kind === 'uptime') { + if (absDeviationPercent >= 5) return 'critical'; + if (absDeviationPercent >= 2) return 'major'; + if (absDeviationPercent >= 1) return 'minor'; + return 'warning'; + } + + if (absDeviationPercent >= 50) return 'critical'; + if (absDeviationPercent >= 25) return 'major'; + if (absDeviationPercent >= 10) return 'minor'; + return 'warning'; + } + + private _calculateCredit( + severity: SlaBreachSeverity, + creditPercentage: number, + maxCreditCap: number + ): number { + if (creditPercentage <= 0) return 0; + + const multiplier: Record = { + warning: 0.25, + minor: 0.5, + major: 1.0, + critical: 2.0, + }; + + const rawCredit = creditPercentage * multiplier[severity]; + return Math.min(Math.round(rawCredit * 100) / 100, maxCreditCap); + } + + private _formatMetricKind(kind: SlaMetricKind): string { + const labels: Record = { + uptime: 'Uptime', + response_time: 'Response Time', + error_rate: 'Error Rate', + latency: 'Latency', + throughput: 'Throughput', + }; + return labels[kind] ?? kind; + } +} diff --git a/docs/subscription-sla-monitoring.md b/docs/subscription-sla-monitoring.md new file mode 100644 index 00000000..e51b5eac --- /dev/null +++ b/docs/subscription-sla-monitoring.md @@ -0,0 +1,192 @@ +# Subscription SLA Monitoring + +> Issue: [#779](https://github.com/Smartdevs17/SubTrackr/issues/779) + +## Overview + +SubTrackr's Subscription SLA Monitoring system provides real-time tracking and enforcement of Service Level Agreements at the individual subscription level. It detects SLA breaches, generates alerts, calculates credits, and provides comprehensive analytics and reporting. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Subscription SLA System │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ +│ │ SLA Config │───▶│ Metric Samples │───▶│ Evaluation │ │ +│ │ (per tier) │ │ (ring buffer) │ │ Engine │ │ +│ └──────────────┘ └──────────────────┘ └──────┬───────┘ │ +│ │ │ +│ ┌───────────┼────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌────────┐ ┌──┐│ +│ │ Breaches │ │ Alerts │ │$$││ +│ │ Tracking │ │ System │ │CR││ +│ └────┬─────┘ └───┬────┘ └──┘│ +│ │ │ │ +│ ┌────▼───────────▼────┐ │ +│ │ Analytics Engine │ │ +│ │ & Report Generator │ │ +│ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Tier-Based SLA Definitions + +Each subscription tier has predefined SLA targets: + +| Tier | Uptime | Response Time | Error Rate | Latency | Credit % | Max Credit | +|------------|-----------|---------------|------------|---------|----------|------------| +| Free | 95.00% | 5000ms | 10% | 3000ms | 0% | $0 | +| Basic | 99.00% | 2000ms | 5% | 1500ms | 5% | $50 | +| Standard | 99.50% | 1000ms | 2% | 800ms | 10% | $100 | +| Premium | 99.90% | 500ms | 1% | 300ms | 15% | $250 | +| Enterprise | 99.99% | 200ms | 0.1% | 100ms | 25% | $500 | + +## Features + +### 1. SLA Definition per Tier +- Pre-configured SLA targets for 5 subscription tiers +- Customizable targets via configuration overrides +- Support for uptime, response time, error rate, and latency metrics + +### 2. Real-Time SLA Tracking +- Metric sample ingestion via `recordMetric()` / `recordMetricBatch()` +- Ring buffer storage (last 1000 samples per subscription) +- Automatic SLA evaluation on each metric recording +- Periodic background checks at configurable intervals + +### 3. Breach Detection and Alerts +- Automatic breach detection when metrics violate SLA targets +- Severity classification: `warning`, `minor`, `major`, `critical` +- Alert generation with actionable messages +- Breach acknowledgment and resolution workflows +- Auto-resolution when metrics return to compliance + +### 4. SLA Credits +- Automatic credit calculation based on breach severity +- Severity multipliers: warning (0.25x), minor (0.5x), major (1.0x), critical (2.0x) +- Per-tier credit percentage and maximum caps +- Credit balance tracking per subscription + +### 5. SLA Reporting +- Daily, weekly, monthly, and quarterly report generation +- Period-filtered breach and compliance data +- Actionable recommendations based on analytics + +### 6. SLA Analytics +- Overall compliance rates and averages +- Mean Time to Resolution (MTTR) +- Breach breakdowns by severity, metric, and tier +- Compliance trend over configurable time windows +- Top breached subscriptions ranking + +### 7. SLA Dashboard +- Real-time status overview +- Status breakdown (compliant, at-risk, breached, critical) +- Recent breaches and alerts +- 7-day compliance trend + +## Usage + +### Frontend (React Native) + +```typescript +import { useSubscriptionSlaMonitor } from '../hooks/useSubscriptionSlaMonitor'; + +function SLADashboard() { + const { + configureSla, + recordMetric, + activeBreaches, + unreadAlerts, + dashboard, + getAnalytics, + } = useSubscriptionSlaMonitor(); + + // Configure SLA for a subscription + configureSla('sub-123', 'premium'); + + // Record metrics + recordMetric('sub-123', 'uptime', 99.95); + recordMetric('sub-123', 'response_time', 450); + + // Access dashboard + console.log(dashboard.overview.compliantPercentage); + console.log(`Active breaches: ${activeBreaches.length}`); +} +``` + +### Backend + +```typescript +import { SubscriptionSlaMonitoringService } from './services/subscriptionSlaMonitoring'; + +const slaMonitor = new SubscriptionSlaMonitoringService([ + { type: 'console' }, + { type: 'slack', webhookUrl: process.env.SLACK_WEBHOOK }, +]); + +// Register a subscription +slaMonitor.configureSubscription('sub-123', 'enterprise'); + +// Record metrics from instrumented endpoints +slaMonitor.recordMetric({ + kind: 'uptime', + value: 99.98, + timestamp: Date.now(), + subscriptionId: 'sub-123', +}); + +// Check health +const health = slaMonitor.getHealthSummary(); +``` + +## File Structure + +``` +src/ +├── types/ +│ └── subscriptionSla.ts # Type definitions & tier defaults +├── services/ +│ └── subscriptionSlaMonitorService.ts # Pure evaluation functions +├── store/ +│ └── subscriptionSlaStore.ts # Zustand state management +├── hooks/ +│ └── useSubscriptionSlaMonitor.ts # React hook for components +backend/ +├── services/ +│ └── subscriptionSlaMonitoring.ts # Backend monitoring service +docs/ +└── subscription-sla-monitoring.md # This documentation +``` + +## Severity Classification + +### Uptime Breaches +| Deviation | Severity | +|-----------|----------| +| ≥ 5% | Critical | +| ≥ 2% | Major | +| ≥ 1% | Minor | +| < 1% | Warning | + +### Other Metric Breaches +| Deviation | Severity | +|-----------|----------| +| ≥ 50% | Critical | +| ≥ 25% | Major | +| ≥ 10% | Minor | +| < 10% | Warning | + +## Escalation Rules + +Default escalation rules per severity: + +| Severity | Delay | Action | +|----------|-----------|-------------| +| Warning | 30 min | Alert | +| Minor | 15 min | Alert | +| Major | 5 min | Notify Admin| +| Critical | Immediate | Escalate | diff --git a/src/hooks/useSubscriptionSlaMonitor.ts b/src/hooks/useSubscriptionSlaMonitor.ts new file mode 100644 index 00000000..80ef269b --- /dev/null +++ b/src/hooks/useSubscriptionSlaMonitor.ts @@ -0,0 +1,170 @@ +/** + * useSubscriptionSlaMonitor — React hook for subscription SLA monitoring. + * + * Provides a convenient interface to the subscription SLA store, + * exposing status, breaches, alerts, analytics, and actions for + * use in React components. + * + * @see https://github.com/Smartdevs17/SubTrackr/issues/779 + */ + +import { useCallback, useMemo } from 'react'; +import { useSubscriptionSlaStore } from '../store/subscriptionSlaStore'; +import type { + SubscriptionSlaTier, + SubscriptionSlaConfig, + SubscriptionSlaStatus, + SubscriptionSlaBreach, + SubscriptionSlaAlert, + SubscriptionSlaAnalytics, + SubscriptionSlaDashboard, + SlaMetricSample, + SlaMetricKind, +} from '../types/subscriptionSla'; + +export interface UseSubscriptionSlaMonitorReturn { + // ── State ─────────────────────────────────────────────────────────────── + + /** Whether the store is performing an async operation */ + isLoading: boolean; + + /** Last error message, if any */ + error: string | null; + + /** Get the SLA status for a specific subscription */ + getStatus: (subscriptionId: string) => SubscriptionSlaStatus | null; + + /** All currently active (unresolved) breaches */ + activeBreaches: SubscriptionSlaBreach[]; + + /** Count of active breaches */ + activeBreachCount: number; + + /** All unread alerts */ + unreadAlerts: SubscriptionSlaAlert[]; + + /** Count of unread alerts */ + unreadAlertCount: number; + + /** SLA dashboard summary */ + dashboard: SubscriptionSlaDashboard; + + // ── Configuration ───────────────────────────────────────────────────── + + /** Set up SLA monitoring for a subscription */ + configureSla: ( + subscriptionId: string, + tier: SubscriptionSlaTier, + overrides?: Partial + ) => void; + + /** Remove SLA monitoring for a subscription */ + removeSla: (subscriptionId: string) => void; + + // ── Metric recording ────────────────────────────────────────────────── + + /** Record a single metric sample */ + recordMetric: ( + subscriptionId: string, + kind: SlaMetricKind, + value: number + ) => void; + + /** Record multiple metric samples at once */ + recordMetrics: ( + subscriptionId: string, + samples: Array<{ kind: SlaMetricKind; value: number }> + ) => void; + + // ── Breach management ───────────────────────────────────────────────── + + /** Acknowledge a breach (marks as seen, with optional notes) */ + acknowledgeBreach: (breachId: string, notes?: string) => void; + + /** Resolve a breach (marks as resolved with timestamp) */ + resolveBreach: (breachId: string) => void; + + // ── Alert management ────────────────────────────────────────────────── + + /** Mark an alert as read */ + markAlertRead: (alertId: string) => void; + + /** Resolve an alert */ + resolveAlert: (alertId: string) => void; + + // ── Analytics & Reporting ───────────────────────────────────────────── + + /** Get aggregated SLA analytics */ + getAnalytics: (days?: number) => SubscriptionSlaAnalytics; + + /** Get credit balance for a subscription */ + getCreditBalance: (subscriptionId: string) => number; +} + +/** + * Hook to interact with the subscription SLA monitoring system. + * + * @example + * ```tsx + * const { activeBreaches, dashboard, configureSla, recordMetric } = useSubscriptionSlaMonitor(); + * + * // Set up SLA for a subscription + * configureSla('sub-123', 'premium'); + * + * // Record a metric + * recordMetric('sub-123', 'uptime', 99.95); + * + * // Check breaches + * console.log(`Active breaches: ${activeBreaches.length}`); + * ``` + */ +export function useSubscriptionSlaMonitor(): UseSubscriptionSlaMonitorReturn { + const store = useSubscriptionSlaStore(); + + const activeBreaches = useMemo(() => store.getActiveBreaches(), [store.breaches]); + const unreadAlerts = useMemo(() => store.getUnreadAlerts(), [store.alerts]); + const dashboard = useMemo(() => store.getDashboard(), [store.statuses, store.breaches, store.alerts]); + + const recordMetric = useCallback( + (subscriptionId: string, kind: SlaMetricKind, value: number) => { + store.recordMetric(subscriptionId, { + kind, + value, + timestamp: Date.now(), + }); + }, + [store.recordMetric] + ); + + const recordMetrics = useCallback( + (subscriptionId: string, samples: Array<{ kind: SlaMetricKind; value: number }>) => { + const now = Date.now(); + store.recordMetricBatch( + subscriptionId, + samples.map((s) => ({ ...s, timestamp: now })) + ); + }, + [store.recordMetricBatch] + ); + + return { + isLoading: store.isLoading, + error: store.error, + getStatus: store.getStatus, + activeBreaches, + activeBreachCount: activeBreaches.length, + unreadAlerts, + unreadAlertCount: unreadAlerts.length, + dashboard, + configureSla: store.configureSubscriptionSla, + removeSla: store.removeSubscriptionSla, + recordMetric, + recordMetrics, + acknowledgeBreach: store.acknowledgeBreach, + resolveBreach: store.resolveBreach, + markAlertRead: store.markAlertRead, + resolveAlert: store.resolveAlert, + getAnalytics: store.getAnalytics, + getCreditBalance: store.getCreditBalance, + }; +} diff --git a/src/services/subscriptionSlaMonitorService.ts b/src/services/subscriptionSlaMonitorService.ts new file mode 100644 index 00000000..b7147683 --- /dev/null +++ b/src/services/subscriptionSlaMonitorService.ts @@ -0,0 +1,567 @@ +/** + * Subscription SLA Monitoring Service + * + * Provides real-time SLA monitoring for individual subscriptions, including: + * - Per-tier SLA target enforcement + * - Metric collection and compliance evaluation + * - Automatic breach detection with severity classification + * - SLA credit calculation and issuance + * - Alert generation and escalation + * - Analytics and reporting + * + * @see https://github.com/Smartdevs17/SubTrackr/issues/779 + */ + +import type { + SubscriptionSlaConfig, + SubscriptionSlaBreach, + SubscriptionSlaAlert, + SubscriptionSlaStatus, + SubscriptionSlaAnalytics, + SubscriptionSlaReport, + SubscriptionSlaDashboard, + SlaMetricSample, + SlaMetricKind, + SlaBreachSeverity, + SubscriptionSlaTier, + SlaEvaluationInput, + SlaEvaluationResult, + DEFAULT_SLA_TARGETS, +} from '../types/subscriptionSla'; + +// Re-export the default targets for consumers +export { DEFAULT_SLA_TARGETS } from '../types/subscriptionSla'; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function generateId(prefix: string): string { + const ts = Date.now().toString(36); + const rand = Math.random().toString(36).slice(2, 8); + return `${prefix}-${ts}-${rand}`; +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +// ── Severity classification ─────────────────────────────────────────────────── + +/** + * Determines breach severity based on how far the actual value deviates + * from the SLA target. + */ +export function classifyBreachSeverity( + deviationPercent: number, + metricKind: SlaMetricKind +): SlaBreachSeverity { + const absDeviation = Math.abs(deviationPercent); + + // Uptime breaches are more critical since they directly impact availability + if (metricKind === 'uptime') { + if (absDeviation >= 5) return 'critical'; + if (absDeviation >= 2) return 'major'; + if (absDeviation >= 1) return 'minor'; + return 'warning'; + } + + // Other metrics use wider thresholds + if (absDeviation >= 50) return 'critical'; + if (absDeviation >= 25) return 'major'; + if (absDeviation >= 10) return 'minor'; + return 'warning'; +} + +// ── Credit calculation ──────────────────────────────────────────────────────── + +/** + * Calculates the credit amount to issue for a given breach based on the + * subscription tier's credit policy. + */ +export function calculateBreachCredit( + severity: SlaBreachSeverity, + creditPercentage: number, + maxCreditCap: number +): number { + if (creditPercentage <= 0) return 0; + + const severityMultiplier: Record = { + warning: 0.25, + minor: 0.5, + major: 1.0, + critical: 2.0, + }; + + const rawCredit = creditPercentage * severityMultiplier[severity]; + return Math.min(Math.round(rawCredit * 100) / 100, maxCreditCap); +} + +// ── Metric evaluation ───────────────────────────────────────────────────────── + +/** + * Evaluates whether a set of metric samples comply with the SLA targets + * for a given subscription configuration. + */ +export function evaluateSubscriptionSla(input: SlaEvaluationInput): SlaEvaluationResult { + const { config, metrics, existingBreaches } = input; + const now = input.now ?? Date.now(); + const { targets } = config; + + // Partition metrics by kind + const byKind = new Map(); + for (const sample of metrics) { + const list = byKind.get(sample.kind) ?? []; + list.push(sample); + byKind.set(sample.kind, list); + } + + // Calculate aggregates + const uptimeSamples = byKind.get('uptime') ?? []; + const responseSamples = byKind.get('response_time') ?? []; + const errorSamples = byKind.get('error_rate') ?? []; + const latencySamples = byKind.get('latency') ?? []; + + const avg = (samples: SlaMetricSample[]): number => + samples.length > 0 ? samples.reduce((s, m) => s + m.value, 0) / samples.length : 0; + + const uptimePercentage = uptimeSamples.length > 0 ? avg(uptimeSamples) : 100; + const avgResponseTimeMs = avg(responseSamples); + const errorRate = avg(errorSamples); + const avgLatencyMs = avg(latencySamples); + + // Detect breaches per metric + const newBreaches: SubscriptionSlaBreach[] = []; + const resolvedBreachIds: string[] = []; + const alerts: SubscriptionSlaAlert[] = []; + + const checkMetric = ( + kind: SlaMetricKind, + actual: number, + target: number, + higherIsBetter: boolean + ) => { + const breached = higherIsBetter ? actual < target : actual > target; + const deviationPercent = target !== 0 ? ((actual - target) / target) * 100 : 0; + const activeBreachForKind = existingBreaches.find( + (b) => + b.metricKind === kind && + b.subscriptionId === config.subscriptionId && + b.resolvedAt === null + ); + + if (breached && !activeBreachForKind) { + const severity = classifyBreachSeverity(deviationPercent, kind); + const credit = calculateBreachCredit( + severity, + targets.creditPercentage, + targets.maxCreditCap + ); + + const breach: SubscriptionSlaBreach = { + id: generateId('sla-breach'), + subscriptionId: config.subscriptionId, + tier: config.tier, + severity, + metricKind: kind, + targetValue: target, + actualValue: Number(actual.toFixed(4)), + deviationPercent: Number(deviationPercent.toFixed(2)), + creditIssued: credit, + detectedAt: now, + resolvedAt: null, + acknowledged: false, + notes: '', + }; + + newBreaches.push(breach); + + // Generate alert for breach + const alert: SubscriptionSlaAlert = { + id: generateId('sla-alert'), + breachId: breach.id, + subscriptionId: config.subscriptionId, + severity, + title: `SLA Breach: ${formatMetricKind(kind)}`, + message: buildAlertMessage(kind, actual, target, severity, config.tier), + actionRequired: severity === 'critical' || severity === 'major', + isRead: false, + isResolved: false, + sentAt: now, + acknowledgedAt: null, + resolvedAt: null, + }; + + alerts.push(alert); + } + + // Resolve active breach if metric is now compliant + if (!breached && activeBreachForKind) { + resolvedBreachIds.push(activeBreachForKind.id); + } + }; + + // Evaluate each metric against targets + if (uptimeSamples.length > 0) { + checkMetric('uptime', uptimePercentage, targets.uptimeTarget, true); + } + if (responseSamples.length > 0) { + checkMetric('response_time', avgResponseTimeMs, targets.maxResponseTimeMs, false); + } + if (errorSamples.length > 0) { + checkMetric('error_rate', errorRate, targets.maxErrorRate, false); + } + if (latencySamples.length > 0) { + checkMetric('latency', avgLatencyMs, targets.maxLatencyMs, false); + } + + // Build updated breaches array + const updatedBreaches = existingBreaches + .map((b) => (resolvedBreachIds.includes(b.id) ? { ...b, resolvedAt: now } : b)) + .concat(newBreaches); + + const activeBreaches = updatedBreaches.filter( + (b) => b.subscriptionId === config.subscriptionId && b.resolvedAt === null + ); + + const allSubBreaches = updatedBreaches.filter( + (b) => b.subscriptionId === config.subscriptionId + ); + + const totalCredits = allSubBreaches.reduce((sum, b) => sum + b.creditIssued, 0); + + const status: SubscriptionSlaStatus = { + subscriptionId: config.subscriptionId, + tier: config.tier, + uptimePercentage: Number(uptimePercentage.toFixed(4)), + avgResponseTimeMs: Number(avgResponseTimeMs.toFixed(2)), + errorRate: Number(errorRate.toFixed(4)), + avgLatencyMs: Number(avgLatencyMs.toFixed(2)), + compliant: activeBreaches.length === 0, + activeBreachCount: activeBreaches.length, + totalBreachCount: allSubBreaches.length, + creditBalance: totalCredits, + lastCheckedAt: now, + }; + + return { status, breaches: updatedBreaches, newBreaches, resolvedBreachIds, alerts }; +} + +// ── Analytics ───────────────────────────────────────────────────────────────── + +/** + * Aggregates analytics across all subscription SLA data. + */ +export function buildSubscriptionSlaAnalytics( + statuses: SubscriptionSlaStatus[], + breaches: SubscriptionSlaBreach[], + days = 30 +): SubscriptionSlaAnalytics { + const totalSubscriptions = statuses.length; + const compliantCount = statuses.filter((s) => s.compliant).length; + const nonCompliantCount = totalSubscriptions - compliantCount; + const averageUptime = + totalSubscriptions > 0 + ? Number( + (statuses.reduce((sum, s) => sum + s.uptimePercentage, 0) / totalSubscriptions).toFixed(2) + ) + : 100; + + const totalBreaches = breaches.length; + const totalCreditsIssued = breaches.reduce((sum, b) => sum + b.creditIssued, 0); + + // Mean Time to Resolution (in minutes) + const resolved = breaches.filter((b) => b.resolvedAt !== null); + const mttr = + resolved.length > 0 + ? Number( + ( + resolved.reduce((sum, b) => sum + (b.resolvedAt! - b.detectedAt), 0) / + resolved.length / + 60_000 + ).toFixed(2) + ) + : 0; + + // Breaches by severity + const breachesBySeverity: Record = { + warning: 0, + minor: 0, + major: 0, + critical: 0, + }; + for (const b of breaches) breachesBySeverity[b.severity]++; + + // Breaches by metric + const breachesByMetric: Record = { + uptime: 0, + response_time: 0, + error_rate: 0, + latency: 0, + throughput: 0, + }; + for (const b of breaches) breachesByMetric[b.metricKind]++; + + // Breaches by tier + const breachesByTier: Record = { + free: 0, + basic: 0, + standard: 0, + premium: 0, + enterprise: 0, + }; + for (const b of breaches) breachesByTier[b.tier]++; + + // Compliance trend + const now = Date.now(); + const complianceTrend: Array<{ date: string; compliance: number; breaches: number }> = []; + for (let i = days - 1; i >= 0; i--) { + const dayStart = now - i * 86_400_000; + const dayEnd = dayStart + 86_400_000; + const dateStr = new Date(dayStart).toISOString().split('T')[0]; + const dayBreaches = breaches.filter((b) => b.detectedAt >= dayStart && b.detectedAt < dayEnd); + complianceTrend.push({ + date: dateStr, + compliance: averageUptime, + breaches: dayBreaches.length, + }); + } + + // Top breached subscriptions + const subBreachMap = new Map(); + for (const b of breaches) { + const entry = subBreachMap.get(b.subscriptionId) ?? { tier: b.tier, count: 0 }; + entry.count++; + subBreachMap.set(b.subscriptionId, entry); + } + const topBreachedSubscriptions = Array.from(subBreachMap.entries()) + .map(([subscriptionId, data]) => { + const s = statuses.find((st) => st.subscriptionId === subscriptionId); + return { + subscriptionId, + tier: data.tier, + breachCount: data.count, + compliance: s?.uptimePercentage ?? 0, + }; + }) + .sort((a, b) => b.breachCount - a.breachCount) + .slice(0, 10); + + return { + totalSubscriptions, + compliantCount, + nonCompliantCount, + averageUptime, + totalBreaches, + totalCreditsIssued, + mttr, + breachesBySeverity, + breachesByMetric, + breachesByTier, + complianceTrend, + topBreachedSubscriptions, + }; +} + +// ── Report generation ───────────────────────────────────────────────────────── + +/** + * Generates a structured SLA report for a given period. + */ +export function generateSubscriptionSlaReport( + reportType: 'daily' | 'weekly' | 'monthly' | 'quarterly', + periodStart: number, + periodEnd: number, + statuses: SubscriptionSlaStatus[], + breaches: SubscriptionSlaBreach[] +): SubscriptionSlaReport { + const periodBreaches = breaches.filter( + (b) => b.detectedAt >= periodStart && b.detectedAt <= periodEnd + ); + const analytics = buildSubscriptionSlaAnalytics(statuses, periodBreaches); + const recommendations = generateRecommendations(analytics, periodBreaches); + + return { + id: generateId('sla-report'), + reportType, + periodStart, + periodEnd, + analytics, + breaches: periodBreaches, + recommendations, + generatedAt: Date.now(), + }; +} + +// ── Dashboard ───────────────────────────────────────────────────────────────── + +/** + * Builds a dashboard overview of subscription SLA health. + */ +export function buildSubscriptionSlaDashboard( + statuses: SubscriptionSlaStatus[], + breaches: SubscriptionSlaBreach[], + alerts: SubscriptionSlaAlert[] +): SubscriptionSlaDashboard { + const total = statuses.length; + const compliant = statuses.filter((s) => s.compliant).length; + const activeBreaches = breaches.filter((b) => b.resolvedAt === null); + + // Status breakdown based on active breach severity + let atRisk = 0; + let breachedCount = 0; + let critical = 0; + + for (const s of statuses) { + if (s.compliant) continue; + const subBreaches = activeBreaches.filter((b) => b.subscriptionId === s.subscriptionId); + const hasCritical = subBreaches.some((b) => b.severity === 'critical'); + const hasMajor = subBreaches.some((b) => b.severity === 'major'); + + if (hasCritical) critical++; + else if (hasMajor) breachedCount++; + else atRisk++; + } + + const recentBreaches = [...activeBreaches] + .sort((a, b) => b.detectedAt - a.detectedAt) + .slice(0, 10); + + const recentAlerts = [...alerts] + .filter((a) => !a.isResolved) + .sort((a, b) => b.sentAt - a.sentAt) + .slice(0, 10); + + const creditsIssued = breaches.reduce((sum, b) => sum + b.creditIssued, 0); + + // Compliance trend (last 7 days) + const now = Date.now(); + const complianceTrend: Array<{ date: string; compliance: number; breaches: number }> = []; + const avgUptime = + total > 0 + ? statuses.reduce((sum, s) => sum + s.uptimePercentage, 0) / total + : 100; + + for (let i = 6; i >= 0; i--) { + const dayStart = now - i * 86_400_000; + const dayEnd = dayStart + 86_400_000; + const dayBreaches = breaches.filter((b) => b.detectedAt >= dayStart && b.detectedAt < dayEnd); + complianceTrend.push({ + date: new Date(dayStart).toISOString().split('T')[0], + compliance: Number(avgUptime.toFixed(2)), + breaches: dayBreaches.length, + }); + } + + return { + overview: { + totalSubscriptions: total, + compliantPercentage: total > 0 ? Number(((compliant / total) * 100).toFixed(2)) : 100, + activeBreaches: activeBreaches.length, + creditsIssued: Number(creditsIssued.toFixed(2)), + }, + statusBreakdown: { + compliant, + atRisk, + breached: breachedCount, + critical, + }, + recentBreaches, + recentAlerts, + complianceTrend, + }; +} + +// ── Internal helpers ────────────────────────────────────────────────────────── + +function formatMetricKind(kind: SlaMetricKind): string { + const labels: Record = { + uptime: 'Uptime', + response_time: 'Response Time', + error_rate: 'Error Rate', + latency: 'Latency', + throughput: 'Throughput', + }; + return labels[kind] ?? kind; +} + +function buildAlertMessage( + kind: SlaMetricKind, + actual: number, + target: number, + severity: SlaBreachSeverity, + tier: SubscriptionSlaTier +): string { + const kindLabel = formatMetricKind(kind); + const units: Record = { + uptime: '%', + response_time: 'ms', + error_rate: '%', + latency: 'ms', + throughput: 'req/s', + }; + const unit = units[kind] ?? ''; + + return ( + `[${severity.toUpperCase()}] ${kindLabel} SLA breach detected for ${tier} tier subscription. ` + + `Target: ${target}${unit}, Actual: ${actual.toFixed(2)}${unit}. ` + + `Deviation: ${(((actual - target) / target) * 100).toFixed(2)}%. ` + + `Immediate action ${severity === 'critical' || severity === 'major' ? 'required' : 'recommended'}.` + ); +} + +function generateRecommendations( + analytics: SubscriptionSlaAnalytics, + breaches: SubscriptionSlaBreach[] +): string[] { + const recs: string[] = []; + + if (analytics.averageUptime < 99) { + recs.push( + `Average uptime is ${analytics.averageUptime}%, below the 99% industry standard. ` + + 'Investigate infrastructure reliability and redundancy.' + ); + } + + if (analytics.breachesBySeverity.critical > 0) { + recs.push( + `${analytics.breachesBySeverity.critical} critical breach(es) detected. ` + + 'Prioritize root cause analysis and immediate remediation.' + ); + } + + if (analytics.mttr > 60) { + recs.push( + `Mean time to resolution is ${analytics.mttr.toFixed(0)} minutes. ` + + 'Consider implementing automated failover and faster incident response procedures.' + ); + } + + if (analytics.breachesByMetric.response_time > 5) { + recs.push( + 'Multiple response time SLA breaches detected. ' + + 'Review API performance, caching strategies, and database query optimization.' + ); + } + + if (analytics.breachesByMetric.error_rate > 3) { + recs.push( + 'Recurring error rate SLA breaches. ' + + 'Implement circuit breakers, retry logic, and improved error handling.' + ); + } + + const enterpriseBreaches = breaches.filter((b) => b.tier === 'enterprise'); + if (enterpriseBreaches.length > 0) { + recs.push( + `${enterpriseBreaches.length} breach(es) affecting enterprise tier subscriptions. ` + + 'Enterprise SLAs carry the highest credit obligations — address immediately.' + ); + } + + if (recs.length === 0) { + recs.push( + 'All subscription SLAs are within acceptable limits. Continue monitoring for early warning signs.' + ); + } + + return recs; +} diff --git a/src/store/subscriptionSlaStore.ts b/src/store/subscriptionSlaStore.ts new file mode 100644 index 00000000..43802bd7 --- /dev/null +++ b/src/store/subscriptionSlaStore.ts @@ -0,0 +1,360 @@ +/** + * Subscription SLA Monitoring Store (Zustand) + * + * Manages subscription-level SLA state including configs, statuses, + * breaches, and alerts. Integrates with the evaluation service and + * notification system for real-time breach detection and alerting. + * + * @see https://github.com/Smartdevs17/SubTrackr/issues/779 + */ + +import { create } from 'zustand'; +import { createJSONStorage, persist } from 'zustand/middleware'; +import { asyncStorageAdapter } from '../utils/storage'; +import type { + SubscriptionSlaConfig, + SubscriptionSlaBreach, + SubscriptionSlaAlert, + SubscriptionSlaStatus, + SubscriptionSlaAnalytics, + SubscriptionSlaReport, + SubscriptionSlaDashboard, + SlaMetricSample, + SubscriptionSlaTier, + SlaBreachSeverity, +} from '../types/subscriptionSla'; +import { DEFAULT_SLA_TARGETS } from '../types/subscriptionSla'; +import { + evaluateSubscriptionSla, + buildSubscriptionSlaAnalytics, + generateSubscriptionSlaReport, + buildSubscriptionSlaDashboard, +} from '../services/subscriptionSlaMonitorService'; +import { presentSlaBreachNotification } from '../services/notificationService'; + +const STORAGE_KEY = 'subtrackr-subscription-sla'; + +function generateId(prefix: string): string { + const ts = Date.now().toString(36); + const rand = Math.random().toString(36).slice(2, 8); + return `${prefix}-${ts}-${rand}`; +} + +// ── Store interface ─────────────────────────────────────────────────────────── + +interface SubscriptionSlaState { + /** Per-subscription SLA configurations */ + configs: Record; + /** Current SLA statuses per subscription */ + statuses: Record; + /** All SLA breaches (active and resolved) */ + breaches: SubscriptionSlaBreach[]; + /** All SLA alerts */ + alerts: SubscriptionSlaAlert[]; + /** Collected metric samples (ring buffer, latest N per subscription) */ + metricSamples: Record; + /** Loading state */ + isLoading: boolean; + /** Last error */ + error: string | null; + + // ── Actions ─────────────────────────────────────────────────────────────── + + /** Configure SLA monitoring for a subscription */ + configureSubscriptionSla: ( + subscriptionId: string, + tier: SubscriptionSlaTier, + overrides?: Partial + ) => void; + + /** Remove SLA monitoring for a subscription */ + removeSubscriptionSla: (subscriptionId: string) => void; + + /** Record a metric sample and trigger SLA evaluation */ + recordMetric: (subscriptionId: string, sample: Omit) => void; + + /** Record a batch of metric samples and evaluate */ + recordMetricBatch: (subscriptionId: string, samples: Omit[]) => void; + + /** Manually trigger SLA evaluation for a subscription */ + evaluateSla: (subscriptionId: string) => SubscriptionSlaStatus | null; + + /** Acknowledge a breach */ + acknowledgeBreach: (breachId: string, notes?: string) => void; + + /** Resolve a breach */ + resolveBreach: (breachId: string) => void; + + /** Mark an alert as read */ + markAlertRead: (alertId: string) => void; + + /** Resolve an alert */ + resolveAlert: (alertId: string) => void; + + /** Get the current SLA status for a subscription */ + getStatus: (subscriptionId: string) => SubscriptionSlaStatus | null; + + /** Get all active breaches */ + getActiveBreaches: () => SubscriptionSlaBreach[]; + + /** Get unread alerts */ + getUnreadAlerts: () => SubscriptionSlaAlert[]; + + /** Get SLA analytics */ + getAnalytics: (days?: number) => SubscriptionSlaAnalytics; + + /** Generate an SLA report */ + generateReport: ( + reportType: 'daily' | 'weekly' | 'monthly' | 'quarterly', + periodStart: number, + periodEnd: number + ) => SubscriptionSlaReport; + + /** Get dashboard data */ + getDashboard: () => SubscriptionSlaDashboard; + + /** Get credit balance for a subscription */ + getCreditBalance: (subscriptionId: string) => number; + + /** Reset all SLA data */ + reset: () => void; +} + +// ── Maximum metric samples to retain per subscription ───────────────────────── + +const MAX_SAMPLES_PER_SUB = 1000; + +// ── Store implementation ────────────────────────────────────────────────────── + +export const useSubscriptionSlaStore = create()( + persist( + (set, get) => ({ + configs: {}, + statuses: {}, + breaches: [], + alerts: [], + metricSamples: {}, + isLoading: false, + error: null, + + configureSubscriptionSla: (subscriptionId, tier, overrides) => { + const targets = { ...DEFAULT_SLA_TARGETS[tier], ...overrides?.targets }; + const now = Date.now(); + + const config: SubscriptionSlaConfig = { + subscriptionId, + tier, + targets, + checkIntervalSeconds: overrides?.checkIntervalSeconds ?? 300, + autoCreditEnabled: overrides?.autoCreditEnabled ?? true, + alertContacts: overrides?.alertContacts ?? [], + escalationRules: overrides?.escalationRules ?? [ + { severity: 'warning', afterMinutes: 30, action: 'alert', recipients: [] }, + { severity: 'minor', afterMinutes: 15, action: 'alert', recipients: [] }, + { severity: 'major', afterMinutes: 5, action: 'notify_admin', recipients: [] }, + { severity: 'critical', afterMinutes: 0, action: 'escalate', recipients: [] }, + ], + createdAt: now, + updatedAt: now, + }; + + set((state) => ({ + configs: { ...state.configs, [subscriptionId]: config }, + error: null, + })); + }, + + removeSubscriptionSla: (subscriptionId) => { + set((state) => { + const { [subscriptionId]: _, ...configs } = state.configs; + const { [subscriptionId]: __, ...statuses } = state.statuses; + const { [subscriptionId]: ___, ...metricSamples } = state.metricSamples; + return { + configs, + statuses, + metricSamples, + breaches: state.breaches.filter((b) => b.subscriptionId !== subscriptionId), + alerts: state.alerts.filter((a) => a.subscriptionId !== subscriptionId), + }; + }); + }, + + recordMetric: (subscriptionId, sample) => { + const fullSample: SlaMetricSample = { ...sample, subscriptionId }; + + set((state) => { + const existing = state.metricSamples[subscriptionId] ?? []; + const updated = [...existing, fullSample].slice(-MAX_SAMPLES_PER_SUB); + return { + metricSamples: { ...state.metricSamples, [subscriptionId]: updated }, + }; + }); + + // Trigger evaluation + get().evaluateSla(subscriptionId); + }, + + recordMetricBatch: (subscriptionId, samples) => { + const fullSamples = samples.map((s) => ({ ...s, subscriptionId })); + + set((state) => { + const existing = state.metricSamples[subscriptionId] ?? []; + const updated = [...existing, ...fullSamples].slice(-MAX_SAMPLES_PER_SUB); + return { + metricSamples: { ...state.metricSamples, [subscriptionId]: updated }, + }; + }); + + get().evaluateSla(subscriptionId); + }, + + evaluateSla: (subscriptionId) => { + const state = get(); + const config = state.configs[subscriptionId]; + if (!config) return null; + + const metrics = state.metricSamples[subscriptionId] ?? []; + const existingBreaches = state.breaches.filter( + (b) => b.subscriptionId === subscriptionId + ); + + try { + const result = evaluateSubscriptionSla({ + config, + metrics, + existingBreaches, + }); + + // Update state with evaluation results + const updatedBreaches = state.breaches + .filter((b) => b.subscriptionId !== subscriptionId) + .concat(result.breaches.filter((b) => b.subscriptionId === subscriptionId)); + + set({ + statuses: { ...state.statuses, [subscriptionId]: result.status }, + breaches: updatedBreaches, + alerts: [...state.alerts, ...result.alerts], + error: null, + }); + + // Send breach notifications for new breaches + for (const breach of result.newBreaches) { + void presentSlaBreachNotification({ + merchantName: `Subscription ${subscriptionId}`, + uptimeTarget: breach.targetValue, + uptimePercentage: breach.actualValue, + creditAmount: breach.creditIssued, + }); + } + + return result.status; + } catch (err) { + const msg = err instanceof Error ? err.message : 'SLA evaluation failed'; + set({ error: msg }); + return null; + } + }, + + acknowledgeBreach: (breachId, notes) => { + set((state) => ({ + breaches: state.breaches.map((b) => + b.id === breachId ? { ...b, acknowledged: true, notes: notes ?? b.notes } : b + ), + })); + }, + + resolveBreach: (breachId) => { + const now = Date.now(); + set((state) => ({ + breaches: state.breaches.map((b) => + b.id === breachId ? { ...b, resolvedAt: now } : b + ), + alerts: state.alerts.map((a) => + a.breachId === breachId ? { ...a, isResolved: true, resolvedAt: now } : a + ), + })); + }, + + markAlertRead: (alertId) => { + set((state) => ({ + alerts: state.alerts.map((a) => + a.id === alertId ? { ...a, isRead: true, acknowledgedAt: Date.now() } : a + ), + })); + }, + + resolveAlert: (alertId) => { + set((state) => ({ + alerts: state.alerts.map((a) => + a.id === alertId ? { ...a, isResolved: true, resolvedAt: Date.now() } : a + ), + })); + }, + + getStatus: (subscriptionId) => get().statuses[subscriptionId] ?? null, + + getActiveBreaches: () => get().breaches.filter((b) => b.resolvedAt === null), + + getUnreadAlerts: () => get().alerts.filter((a) => !a.isRead && !a.isResolved), + + getAnalytics: (days = 30) => { + const state = get(); + return buildSubscriptionSlaAnalytics( + Object.values(state.statuses), + state.breaches, + days + ); + }, + + generateReport: (reportType, periodStart, periodEnd) => { + const state = get(); + return generateSubscriptionSlaReport( + reportType, + periodStart, + periodEnd, + Object.values(state.statuses), + state.breaches + ); + }, + + getDashboard: () => { + const state = get(); + return buildSubscriptionSlaDashboard( + Object.values(state.statuses), + state.breaches, + state.alerts + ); + }, + + getCreditBalance: (subscriptionId) => { + return get() + .breaches.filter((b) => b.subscriptionId === subscriptionId) + .reduce((sum, b) => sum + b.creditIssued, 0); + }, + + reset: () => { + set({ + configs: {}, + statuses: {}, + breaches: [], + alerts: [], + metricSamples: {}, + isLoading: false, + error: null, + }); + }, + }), + { + name: STORAGE_KEY, + storage: createJSONStorage(() => asyncStorageAdapter), + version: 1, + partialize: (state) => ({ + configs: state.configs, + statuses: state.statuses, + breaches: state.breaches, + alerts: state.alerts, + // metricSamples intentionally excluded from persistence to avoid bloat + }), + } + ) +); diff --git a/src/types/subscriptionSla.ts b/src/types/subscriptionSla.ts new file mode 100644 index 00000000..846bce11 --- /dev/null +++ b/src/types/subscriptionSla.ts @@ -0,0 +1,239 @@ +/** + * Subscription-level SLA monitoring types. + * + * Extends the merchant-level SLA system to track per-subscription + * SLA compliance, breach detection, and automatic credit issuance. + */ + +import type { SlaAvailabilityState } from './sla'; + +// ── Subscription tier SLA definitions ───────────────────────────────────────── + +export type SubscriptionSlaTier = 'free' | 'basic' | 'standard' | 'premium' | 'enterprise'; + +export interface SubscriptionSlaTarget { + /** Minimum uptime percentage (e.g. 99.9) */ + uptimeTarget: number; + /** Maximum allowed response time in milliseconds */ + maxResponseTimeMs: number; + /** Maximum allowed error rate percentage (e.g. 1.0 = 1%) */ + maxErrorRate: number; + /** Maximum allowed latency in milliseconds */ + maxLatencyMs: number; + /** Credit percentage issued per breach (0-100) */ + creditPercentage: number; + /** Maximum credit cap per billing period */ + maxCreditCap: number; +} + +/** + * Default SLA targets per subscription tier. + * Higher tiers receive stricter guarantees. + */ +export const DEFAULT_SLA_TARGETS: Record = { + free: { + uptimeTarget: 95, + maxResponseTimeMs: 5000, + maxErrorRate: 10, + maxLatencyMs: 3000, + creditPercentage: 0, + maxCreditCap: 0, + }, + basic: { + uptimeTarget: 99, + maxResponseTimeMs: 2000, + maxErrorRate: 5, + maxLatencyMs: 1500, + creditPercentage: 5, + maxCreditCap: 50, + }, + standard: { + uptimeTarget: 99.5, + maxResponseTimeMs: 1000, + maxErrorRate: 2, + maxLatencyMs: 800, + creditPercentage: 10, + maxCreditCap: 100, + }, + premium: { + uptimeTarget: 99.9, + maxResponseTimeMs: 500, + maxErrorRate: 1, + maxLatencyMs: 300, + creditPercentage: 15, + maxCreditCap: 250, + }, + enterprise: { + uptimeTarget: 99.99, + maxResponseTimeMs: 200, + maxErrorRate: 0.1, + maxLatencyMs: 100, + creditPercentage: 25, + maxCreditCap: 500, + }, +}; + +// ── Subscription SLA metric types ───────────────────────────────────────────── + +export type SlaMetricKind = 'uptime' | 'response_time' | 'error_rate' | 'latency' | 'throughput'; + +export type SlaBreachSeverity = 'warning' | 'minor' | 'major' | 'critical'; + +export interface SlaMetricSample { + /** Metric kind being measured */ + kind: SlaMetricKind; + /** Measured value */ + value: number; + /** ISO timestamp of measurement */ + timestamp: number; + /** Optional subscription context */ + subscriptionId?: string; +} + +// ── Subscription SLA tracking ───────────────────────────────────────────────── + +export interface SubscriptionSlaConfig { + subscriptionId: string; + tier: SubscriptionSlaTier; + targets: SubscriptionSlaTarget; + /** Monitoring check interval in seconds */ + checkIntervalSeconds: number; + /** Whether auto-credit is enabled on breach */ + autoCreditEnabled: boolean; + /** Contacts to alert on breach */ + alertContacts: string[]; + /** Escalation rules */ + escalationRules: SlaEscalationRule[]; + createdAt: number; + updatedAt: number; +} + +export interface SlaEscalationRule { + severity: SlaBreachSeverity; + /** Minutes before escalation triggers */ + afterMinutes: number; + action: 'alert' | 'notify_admin' | 'escalate' | 'auto_credit'; + recipients: string[]; +} + +// ── Subscription SLA breach ─────────────────────────────────────────────────── + +export interface SubscriptionSlaBreach { + id: string; + subscriptionId: string; + tier: SubscriptionSlaTier; + severity: SlaBreachSeverity; + metricKind: SlaMetricKind; + targetValue: number; + actualValue: number; + deviationPercent: number; + /** Credit amount issued (0 if not applicable) */ + creditIssued: number; + detectedAt: number; + resolvedAt: number | null; + acknowledged: boolean; + notes: string; +} + +// ── SLA alert ───────────────────────────────────────────────────────────────── + +export interface SubscriptionSlaAlert { + id: string; + breachId: string; + subscriptionId: string; + severity: SlaBreachSeverity; + title: string; + message: string; + actionRequired: boolean; + isRead: boolean; + isResolved: boolean; + sentAt: number; + acknowledgedAt: number | null; + resolvedAt: number | null; +} + +// ── SLA status snapshot ─────────────────────────────────────────────────────── + +export interface SubscriptionSlaStatus { + subscriptionId: string; + tier: SubscriptionSlaTier; + uptimePercentage: number; + avgResponseTimeMs: number; + errorRate: number; + avgLatencyMs: number; + compliant: boolean; + activeBreachCount: number; + totalBreachCount: number; + creditBalance: number; + lastCheckedAt: number; +} + +// ── SLA analytics & reporting ───────────────────────────────────────────────── + +export interface SubscriptionSlaAnalytics { + totalSubscriptions: number; + compliantCount: number; + nonCompliantCount: number; + averageUptime: number; + totalBreaches: number; + totalCreditsIssued: number; + mttr: number; + breachesBySeverity: Record; + breachesByMetric: Record; + breachesByTier: Record; + complianceTrend: Array<{ date: string; compliance: number; breaches: number }>; + topBreachedSubscriptions: Array<{ + subscriptionId: string; + tier: SubscriptionSlaTier; + breachCount: number; + compliance: number; + }>; +} + +export interface SubscriptionSlaReport { + id: string; + reportType: 'daily' | 'weekly' | 'monthly' | 'quarterly'; + periodStart: number; + periodEnd: number; + analytics: SubscriptionSlaAnalytics; + breaches: SubscriptionSlaBreach[]; + recommendations: string[]; + generatedAt: number; +} + +// ── SLA dashboard ───────────────────────────────────────────────────────────── + +export interface SubscriptionSlaDashboard { + overview: { + totalSubscriptions: number; + compliantPercentage: number; + activeBreaches: number; + creditsIssued: number; + }; + statusBreakdown: { + compliant: number; + atRisk: number; + breached: number; + critical: number; + }; + recentBreaches: SubscriptionSlaBreach[]; + recentAlerts: SubscriptionSlaAlert[]; + complianceTrend: Array<{ date: string; compliance: number; breaches: number }>; +} + +// ── Monitor evaluation input/output ─────────────────────────────────────────── + +export interface SlaEvaluationInput { + config: SubscriptionSlaConfig; + metrics: SlaMetricSample[]; + existingBreaches: SubscriptionSlaBreach[]; + now?: number; +} + +export interface SlaEvaluationResult { + status: SubscriptionSlaStatus; + breaches: SubscriptionSlaBreach[]; + newBreaches: SubscriptionSlaBreach[]; + resolvedBreachIds: string[]; + alerts: SubscriptionSlaAlert[]; +}