From 7c2a9abed442da0fd72520b6a7727d8342f0a113 Mon Sep 17 00:00:00 2001 From: eason <85663565+mango766@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:01:49 +0800 Subject: [PATCH] fix: prevent NaN in health indicator calculations (#19378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #19377 - **Redis health**: The hit rate calculation divides by zero when both `keyspace_hits` and `keyspace_misses` are `"0"` (common on fresh instances). The string `"0"` is truthy so the guard `statsData.keyspace_hits ? ...` doesn't catch this case, resulting in `0/0 = NaN`. Fixed by computing the total first and checking it's a valid non-zero number. - **Database health**: The cache hit ratio query returns `null` when `pg_statio_user_tables` is empty (no user tables). `parseFloat(null)` → `NaN`. Fixed by adding a null check. ## Test plan - [ ] Verify health indicators display correctly on a fresh instance with no Redis keyspace activity - [ ] Verify health indicators display correctly on a database with no user tables - [ ] Existing tests in `redis.health.spec.ts` still pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: easonysliu Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Charles Bochet --- .../admin-panel/indicators/database.health.ts | 5 +++- .../admin-panel/indicators/redis.health.ts | 23 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/indicators/database.health.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/indicators/database.health.ts index c79982bcce..f2273f84b8 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/indicators/database.health.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/indicators/database.health.ts @@ -6,6 +6,7 @@ import { import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; +import { isDefined } from 'twenty-shared/utils'; import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/admin-panel/constants/health-error-messages.constants'; import { withHealthCheckTimeout } from 'src/engine/core-modules/admin-panel/utils/health-check-timeout.util'; @@ -89,7 +90,9 @@ export class DatabaseHealthIndicator { }, databaseSize: databaseSize.size, performance: { - cacheHitRatio: Math.round(parseFloat(cacheHitRatio.ratio)) + '%', + cacheHitRatio: isDefined(cacheHitRatio.ratio) + ? Math.round(parseFloat(cacheHitRatio.ratio)) + '%' + : 'N/A', deadlocks: parseInt(deadlocks.deadlocks), slowQueries: parseInt(slowQueries.count), }, diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/indicators/redis.health.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/indicators/redis.health.ts index 84034e0d83..1e4fed56e1 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/indicators/redis.health.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/indicators/redis.health.ts @@ -4,6 +4,8 @@ import { HealthIndicatorService, } from '@nestjs/terminus'; +import { isDefined } from 'twenty-shared/utils'; + import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/admin-panel/constants/health-error-messages.constants'; import { withHealthCheckTimeout } from 'src/engine/core-modules/admin-panel/utils/health-check-timeout.util'; import { HealthStateManager } from 'src/engine/core-modules/admin-panel/utils/health-state-manager.util'; @@ -70,14 +72,19 @@ export class RedisHealthIndicator { }, performance: { opsPerSecond: parseInt(statsData.instantaneous_ops_per_sec), - hitRate: statsData.keyspace_hits - ? Math.round( - (parseInt(statsData.keyspace_hits) / - (parseInt(statsData.keyspace_hits) + - parseInt(statsData.keyspace_misses))) * - 100, - ) + '%' - : '0%', + hitRate: + isDefined(statsData.keyspace_hits) && + isDefined(statsData.keyspace_misses) + ? (() => { + const hits = parseInt(statsData.keyspace_hits); + const misses = parseInt(statsData.keyspace_misses); + const total = hits + misses; + + return total > 0 + ? Math.round((hits / total) * 100) + '%' + : '0%'; + })() + : '0%', evictedKeys: parseInt(statsData.evicted_keys), expiredKeys: parseInt(statsData.expired_keys), },