From 70dd220deff46e4bffebebecdc80339e7e850895 Mon Sep 17 00:00:00 2001 From: The Mavik <179817126+themavik@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:18:35 +0530 Subject: [PATCH 1/5] fix: guard formatNumber against non-numeric values When table columns contain non-numeric values (strings, etc.) and a number format like Percent is applied, the formatting produces NaN because `numbro()` can't parse strings. The existing falsy check (`!value && value !== 0`) doesn't catch non-empty strings since they're truthy. Adds a `typeof` and `isNaN` guard to return the raw value as-is when it's not actually numeric. Fixes #1510 --- packages/app/src/utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/app/src/utils.ts b/packages/app/src/utils.ts index 8ca0240d4a..fc2cd4d13f 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 non-numeric values at runtime (e.g., string column values) + // that slip past the TypeScript type system + if (typeof value !== 'number' || isNaN(value)) { + return String(value); + } + if (!options) { return value.toString(); } From 3e795adce5382f5b4d10060318b4b012a04783f8 Mon Sep 17 00:00:00 2001 From: zoom Date: Fri, 20 Feb 2026 12:54:33 -0500 Subject: [PATCH 2/5] fix: narrow formatNumber guard to isNaN only per review - Remove typeof check; ClickHouse returns numbers as strings - Only guard for isNaN to catch truly non-numeric values Co-authored-by: Cursor --- packages/app/src/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/utils.ts b/packages/app/src/utils.ts index fc2cd4d13f..02c75ca444 100644 --- a/packages/app/src/utils.ts +++ b/packages/app/src/utils.ts @@ -760,9 +760,9 @@ export const formatNumber = ( return 'N/A'; } - // Guard against non-numeric values at runtime (e.g., string column values) - // that slip past the TypeScript type system - if (typeof value !== 'number' || isNaN(value)) { + // 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); } From c76af9f1284b0b24c1289abcba899bf628442df4 Mon Sep 17 00:00:00 2001 From: The Mavik <179817126+themavik@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:23:26 -0500 Subject: [PATCH 3/5] test: add isNaN guard test for formatNumber --- packages/app/src/__tests__/utils.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/app/src/__tests__/utils.test.ts b/packages/app/src/__tests__/utils.test.ts index b729bb23c8..5ed871c166 100644 --- a/packages/app/src/__tests__/utils.test.ts +++ b/packages/app/src/__tests__/utils.test.ts @@ -264,6 +264,16 @@ describe('formatNumber', () => { expect(formatNumber(1234567, format)).toBe('1m'); }); }); + + describe('NaN handling', () => { + it('returns string representation for NaN without options', () => { + expect(formatNumber(NaN)).toBe('NaN'); + }); + + it('returns string representation for NaN with options', () => { + expect(formatNumber(NaN, { output: 'number', mantissa: 2 })).toBe('NaN'); + }); + }); }); describe('useLocalStorage', () => { From db6440d0b50f0fa9cb88e3368850ee873ce6f57c Mon Sep 17 00:00:00 2001 From: Karl Power Date: Thu, 26 Feb 2026 11:29:03 +0100 Subject: [PATCH 4/5] fix tests --- packages/app/src/__tests__/utils.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/app/src/__tests__/utils.test.ts b/packages/app/src/__tests__/utils.test.ts index 5ed871c166..e94cbf68bc 100644 --- a/packages/app/src/__tests__/utils.test.ts +++ b/packages/app/src/__tests__/utils.test.ts @@ -266,12 +266,19 @@ describe('formatNumber', () => { }); describe('NaN handling', () => { - it('returns string representation for NaN without options', () => { - expect(formatNumber(NaN)).toBe('NaN'); + 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 string representation for NaN with options', () => { - expect(formatNumber(NaN, { output: 'number', mantissa: 2 })).toBe('NaN'); + 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'); }); }); }); From 5a572cab4c22eede598d90879639413ebaa70057 Mon Sep 17 00:00:00 2001 From: Karl Power Date: Thu, 26 Feb 2026 11:31:10 +0100 Subject: [PATCH 5/5] add changeset --- .changeset/large-seas-clean.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/large-seas-clean.md diff --git a/.changeset/large-seas-clean.md b/.changeset/large-seas-clean.md new file mode 100644 index 0000000000..49f8b06289 --- /dev/null +++ b/.changeset/large-seas-clean.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +fix: guard formatNumber against non-numeric values