fix: guard formatNumber against non-numeric values (#1721)

## Summary

Fixes #1510

**Root cause:** When a number format (e.g., Percent) is applied to table columns that contain non-numeric values, `formatNumber` doesn't catch non-empty strings. `numbro()` then produces NaN.

The existing check `!value && value !== 0` catches `null`, `undefined`, and `NaN`, but NOT non-empty strings (which are truthy and pass through to `numbro()`).

## Changes

- `packages/app/src/utils.ts`: Added `typeof value !== 'number' || isNaN(value)` guard that returns the raw value as a string instead of formatting it.

## Risk Assessment

**Low** - Only adds a guard for an edge case. Numeric values are formatted exactly as before.

Co-authored-by: Karl Power <85935352+karl-power@users.noreply.github.com>
This commit is contained in:
The Mavik 2026-02-26 05:43:53 -05:00 committed by GitHub
parent 4cb175d477
commit 3797e657d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 0 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
fix: guard formatNumber against non-numeric values

View file

@ -264,6 +264,23 @@ describe('formatNumber', () => {
expect(formatNumber(1234567, format)).toBe('1m');
});
});
describe('NaN handling', () => {
it('returns "N/A" for NaN without options', () => {
expect(formatNumber(NaN)).toBe('N/A');
expect(formatNumber(NaN, { output: 'number', mantissa: 2 })).toBe('N/A');
});
it('returns a string unchanged if a number cannot be parsed from it', () => {
// @ts-expect-error not passing a number
expect(formatNumber('not a number')).toBe('not a number');
expect(
// @ts-expect-error not passing a number
formatNumber('not a number', { output: 'number', mantissa: 2 }),
).toBe('not a number');
});
});
});
describe('useLocalStorage', () => {

View file

@ -760,6 +760,12 @@ export const formatNumber = (
return 'N/A';
}
// Guard against NaN only - ClickHouse can return numbers as strings, which
// we should still format. Only truly non-numeric values (NaN) get passed through.
if (isNaN(value as number)) {
return String(value);
}
if (!options) {
return value.toString();
}