diff --git a/.changeset/large-seas-clean.md b/.changeset/large-seas-clean.md new file mode 100644 index 00000000..49f8b062 --- /dev/null +++ b/.changeset/large-seas-clean.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +fix: guard formatNumber against non-numeric values diff --git a/packages/app/src/__tests__/utils.test.ts b/packages/app/src/__tests__/utils.test.ts index b729bb23..e94cbf68 100644 --- a/packages/app/src/__tests__/utils.test.ts +++ b/packages/app/src/__tests__/utils.test.ts @@ -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', () => { diff --git a/packages/app/src/utils.ts b/packages/app/src/utils.ts index 8ca0240d..02c75ca4 100644 --- a/packages/app/src/utils.ts +++ b/packages/app/src/utils.ts @@ -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(); }