fix: prevent NaN in health indicator calculations (#19378)

## 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 <easonysliu@tencent.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
eason
2026-04-07 18:01:49 +08:00
committed by GitHub
parent c0c0cbb896
commit 7c2a9abed4
2 changed files with 19 additions and 9 deletions
@@ -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),
},
@@ -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),
},