diff --git a/.changeset/uplot-posix-locale.md b/.changeset/uplot-posix-locale.md new file mode 100644 index 0000000000..2b502a2829 --- /dev/null +++ b/.changeset/uplot-posix-locale.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/app': patch +--- + +fix: The app no longer crashes on load in browsers that report a POSIX-style language such as `en-US@posix` diff --git a/.yarn/patches/uplot-npm-1.6.32-350d2da00d.patch b/.yarn/patches/uplot-npm-1.6.32-350d2da00d.patch new file mode 100644 index 0000000000..80a885b116 --- /dev/null +++ b/.yarn/patches/uplot-npm-1.6.32-350d2da00d.patch @@ -0,0 +1,40 @@ +diff --git a/dist/uPlot.cjs.js b/dist/uPlot.cjs.js +index 98b7582dff68d4f4d9e0068cc77bec9d33ea4904..d137fbcb9f24358d7e59350907181106fc5b231d 100644 +--- a/dist/uPlot.cjs.js ++++ b/dist/uPlot.cjs.js +@@ -441,7 +441,14 @@ function _rangeNum(_min, _max, cfg) { + } + + // alternative: https://stackoverflow.com/a/2254896 +-const numFormatter = new Intl.NumberFormat(domEnv ? nav.language : 'en-US'); ++// Patched: Intl rejects POSIX-style tags such as 'en-US@posix', which some ++// browsers report as navigator.language. Fall back instead of throwing at import. ++let numFormatter; ++try { ++ numFormatter = new Intl.NumberFormat(domEnv ? nav.language : 'en-US'); ++} catch (e) { ++ numFormatter = new Intl.NumberFormat('en-US'); ++} + const fmtNum = val => numFormatter.format(val); + + const M = Math; +diff --git a/dist/uPlot.esm.js b/dist/uPlot.esm.js +index 87473c9cfb3366f57883cb803114a87c64d4b6b8..740bd3626a1f8bd6bbe352878e55e898e759784c 100644 +--- a/dist/uPlot.esm.js ++++ b/dist/uPlot.esm.js +@@ -439,7 +439,14 @@ function _rangeNum(_min, _max, cfg) { + } + + // alternative: https://stackoverflow.com/a/2254896 +-const numFormatter = new Intl.NumberFormat(domEnv ? nav.language : 'en-US'); ++// Patched: Intl rejects POSIX-style tags such as 'en-US@posix', which some ++// browsers report as navigator.language. Fall back instead of throwing at import. ++let numFormatter; ++try { ++ numFormatter = new Intl.NumberFormat(domEnv ? nav.language : 'en-US'); ++} catch (e) { ++ numFormatter = new Intl.NumberFormat('en-US'); ++} + const fmtNum = val => numFormatter.format(val); + + const M = Math; diff --git a/packages/app/package.json b/packages/app/package.json index 829742797e..101e226cce 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -100,7 +100,7 @@ "store2": "^2.14.3", "strip-ansi": "^6.0.1", "timestamp-nano": "^1.0.1", - "uplot": "^1.6.31", + "uplot": "patch:uplot@npm%3A1.6.32#~/.yarn/patches/uplot-npm-1.6.32-350d2da00d.patch", "uplot-react": "^1.2.2", "zod": "3.25" }, diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts index 78524bd2f9..c5bca85edd 100644 --- a/packages/app/playwright.config.ts +++ b/packages/app/playwright.config.ts @@ -75,6 +75,9 @@ export default defineConfig({ screenshot: 'only-on-failure', /* Record video on failure */ video: 'retain-on-failure', + // Otherwise Chromium takes navigator.language from the host locale, so + // number and date formatting would vary between machines. + locale: 'en-US', }, /* Global test timeout - CI needs more time than local */ diff --git a/packages/app/src/components/DBHeatmapChart/__tests__/uplotLocale.test.ts b/packages/app/src/components/DBHeatmapChart/__tests__/uplotLocale.test.ts new file mode 100644 index 0000000000..d18b2f3ac9 --- /dev/null +++ b/packages/app/src/components/DBHeatmapChart/__tests__/uplotLocale.test.ts @@ -0,0 +1,23 @@ +// uPlot builds an Intl.NumberFormat from navigator.language at import time. +// Chromium on LANG=C.UTF-8 hosts reports 'en-US@posix', which Intl rejects; +// our yarn patch makes uPlot fall back to 'en-US' instead of throwing. +describe('uplot locale patch', () => { + let languageSpy: jest.SpyInstance; + + afterEach(() => { + languageSpy.mockRestore(); + }); + + it('imports and formats numbers when navigator.language is not a valid BCP 47 tag', async () => { + languageSpy = jest + .spyOn(window.navigator, 'language', 'get') + .mockReturnValue('en-US@posix'); + + expect(() => new Intl.NumberFormat('en-US@posix')).toThrow(RangeError); + + await jest.isolateModulesAsync(async () => { + const { default: uPlot } = await import('uplot'); + expect(uPlot.fmtNum(1234.5)).toBe('1,234.5'); + }); + }); +}); diff --git a/packages/app/tests/e2e/global-setup-fullstack.ts b/packages/app/tests/e2e/global-setup-fullstack.ts index 9c196b6900..a0c92624e7 100644 --- a/packages/app/tests/e2e/global-setup-fullstack.ts +++ b/packages/app/tests/e2e/global-setup-fullstack.ts @@ -145,7 +145,11 @@ async function globalSetup(_config: FullConfig) { console.log('Creating test user and logging in'); const browser = await chromium.launch(); - const context = await browser.newContext({ baseURL: APP_URL }); + // Global setup doesn't inherit `use` from playwright.config.ts. + const context = await browser.newContext({ + baseURL: APP_URL, + locale: 'en-US', + }); const page = await context.newPage(); try { diff --git a/yarn.lock b/yarn.lock index b7d3b4f74f..9cb5ded064 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4977,7 +4977,7 @@ __metadata: timestamp-nano: "npm:^1.0.1" ts-jest: "npm:^29.4.11" typescript: "npm:^6.0.3" - uplot: "npm:^1.6.31" + uplot: "patch:uplot@npm%3A1.6.32#~/.yarn/patches/uplot-npm-1.6.32-350d2da00d.patch" uplot-react: "npm:^1.2.2" zod: "npm:3.25" languageName: unknown @@ -30604,13 +30604,20 @@ __metadata: languageName: node linkType: hard -"uplot@npm:^1.6.31": +"uplot@npm:1.6.32": version: 1.6.32 resolution: "uplot@npm:1.6.32" checksum: 10c0/4d9cdd5f53371656cfc178ea4ae0d8c9bdbe4c98c99df847926d7e5f5a5bcb8043561219da2979fee7616aabe3619d80fee093befdfe39aae65e6621284c39b1 languageName: node linkType: hard +"uplot@patch:uplot@npm%3A1.6.32#~/.yarn/patches/uplot-npm-1.6.32-350d2da00d.patch": + version: 1.6.32 + resolution: "uplot@patch:uplot@npm%3A1.6.32#~/.yarn/patches/uplot-npm-1.6.32-350d2da00d.patch::version=1.6.32&hash=d25b41" + checksum: 10c0/8eac8ee3e294a2834f6295eea230bd6dba6c7031a0d67f79f02eacec15ceaada5e82accd9ef1b6462326ec35e9475779f072b9be7625550a3e2f1d8fc70d2470 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1"