From 6568e16e81f5f244cd4938a19a5ba9a44cf6152c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Wed, 17 Jun 2026 14:27:37 +0200 Subject: [PATCH 1/2] Add the cli package name as a constant into the codebase This will allow us to see the package name from the cli source code and suggest updating it via npm. --- profiler-cli/src/constants.ts | 6 ++++++ scripts/build-profiler-cli.mjs | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/profiler-cli/src/constants.ts b/profiler-cli/src/constants.ts index 3050cbaaac..d8be3144d0 100644 --- a/profiler-cli/src/constants.ts +++ b/profiler-cli/src/constants.ts @@ -8,6 +8,7 @@ // These globals are defined via esbuild's define option. declare const __BUILD_HASH__: string; +declare const __PACKAGE_NAME__: string; declare const __VERSION__: string; /** @@ -16,6 +17,11 @@ declare const __VERSION__: string; */ export const BUILD_HASH = __BUILD_HASH__; +/** + * Package name from profiler-cli/package.json, injected at build time. + */ +export const PACKAGE_NAME = __PACKAGE_NAME__; + /** * Package version from profiler-cli/package.json, injected at build time. */ diff --git a/scripts/build-profiler-cli.mjs b/scripts/build-profiler-cli.mjs index 9e90b69c28..a8f8775c44 100644 --- a/scripts/build-profiler-cli.mjs +++ b/scripts/build-profiler-cli.mjs @@ -5,7 +5,7 @@ import esbuild from 'esbuild'; import { chmodSync, readFileSync } from 'fs'; import { nodeBaseConfig } from './lib/esbuild-configs.mjs'; -const { version } = JSON.parse( +const { name, version } = JSON.parse( readFileSync(new URL('../profiler-cli/package.json', import.meta.url), 'utf8') ); @@ -22,6 +22,7 @@ const profilerCliConfig = { }, define: { __BUILD_HASH__: JSON.stringify(BUILD_HASH), + __PACKAGE_NAME__: JSON.stringify(name), __VERSION__: JSON.stringify(version), // SOURCE_MAP_WORKER_PATH is injected by the browser build. The CLI doesn't // use source map workers but the shared code references this constant. From 53a30710ffc076cc137b282511b7b4c858952022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Wed, 17 Jun 2026 14:05:12 +0200 Subject: [PATCH 2/2] Show more user friendly errors for unsupported profile version in both the frontend and the cli I initially wanted to add extra information to the profile version errors in the cli, because currently we don't give any hint about how to update it. But while doing that, I realized that we could also improve the error handling of the frontend a bit more. The old frontend error was a non-localized text. This commit creates a new localized text for this type of error and serializes it in a more friendly way. Probably it's a bit of an overkill for this error as it should ideally be not seen by the users, but there were existing errors with the same way, so I wanted to be consistent. Also, the cli now shows a tip about how to update the cli. --- locales/en-US/app.ftl | 6 ++++ profiler-cli/src/daemon.ts | 32 +++++++++++++++---- src/components/app/AppViewRouter.tsx | 3 ++ src/profile-logic/errors.ts | 27 ++++++++++++++++ src/profile-logic/gecko-profile-versioning.ts | 9 +++--- src/profile-logic/process-profile.ts | 6 ++++ .../processed-profile-versioning.ts | 9 +++--- 7 files changed, 78 insertions(+), 14 deletions(-) diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 5625f479b5..86409569e7 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -54,6 +54,12 @@ AppViewRouter--error-from-localhost-url-safari = this page in { -firefox-brand-name } or Chrome instead. .title = Safari cannot import local profiles +# This error message is displayed when the profile is in a newer format version +# than this build of the Profiler is able to read. +AppViewRouter--error-profile-version = + This profile uses a format that is not supported by this version of { -profiler-brand-name }. + Try refreshing the page to check if there is an update available for { -profiler-brand-name }. + AppViewRouter--route-not-found--home = .specialMessage = The URL you tried to reach was not recognized. diff --git a/profiler-cli/src/daemon.ts b/profiler-cli/src/daemon.ts index 992b827f62..44e2e3e9ec 100644 --- a/profiler-cli/src/daemon.ts +++ b/profiler-cli/src/daemon.ts @@ -11,6 +11,7 @@ import * as net from 'net'; import * as fs from 'fs'; import { ProfileQuerier } from '../../src/profile-query'; import type { LoadPhase } from '../../src/profile-query/loader'; +import { ProfileVersionError } from 'firefox-profiler/profile-logic/errors'; import type { ClientCommand, ClientMessage, @@ -28,7 +29,27 @@ import { ensureSessionDir, } from './session'; import { assertExhaustiveCheck } from 'firefox-profiler/utils/types'; -import { BUILD_HASH } from './constants'; +import { BUILD_HASH, PACKAGE_NAME } from './constants'; + +/** + * Build a user-facing message for a profile load failure. When the profile is + * too new for this build, append instructions on how to update the CLI. + */ +function formatProfileLoadError(error: unknown): string { + if ( + error instanceof ProfileVersionError || + (error instanceof Error && error.name === 'ProfileVersionError') + ) { + const versionError = error as ProfileVersionError; + return ( + `This profile is version ${versionError.profileVersion}, but this profiler-cli only ` + + `supports up to version ${versionError.supportedVersion} of the ${versionError.formatName} profile format.\n` + + `Update to the latest version with:\n` + + ` npm install -g ${PACKAGE_NAME}@latest` + ); + } + return error instanceof Error ? error.message : String(error); +} export class Daemon { private querier: ProfileQuerier | null = null; @@ -41,7 +62,7 @@ export class Daemon { private profilePath: string; private symbolServerUrl?: string; private loadPhase: LoadPhase = 'fetching'; - private profileLoadError: Error | null = null; + private profileLoadError: string | null = null; constructor( sessionDir: string, @@ -149,8 +170,7 @@ export class Daemon { console.log('Profile loaded successfully'); } catch (error) { console.error(`Failed to load profile: ${error}`); - this.profileLoadError = - error instanceof Error ? error : new Error(String(error)); + this.profileLoadError = formatProfileLoadError(error); } } @@ -210,7 +230,7 @@ export class Daemon { if (this.profileLoadError) { return { type: 'error', - error: `Profile load failed: ${this.profileLoadError.message}`, + error: `Profile load failed: ${this.profileLoadError}`, }; } switch (this.loadPhase) { @@ -245,7 +265,7 @@ export class Daemon { if (this.profileLoadError) { return { type: 'error', - error: `Profile load failed: ${this.profileLoadError.message}`, + error: `Profile load failed: ${this.profileLoadError}`, }; } if (this.loadPhase !== 'ready' || !this.querier) { diff --git a/src/components/app/AppViewRouter.tsx b/src/components/app/AppViewRouter.tsx index fc04a205ff..8c025eab8b 100644 --- a/src/components/app/AppViewRouter.tsx +++ b/src/components/app/AppViewRouter.tsx @@ -87,6 +87,9 @@ class AppViewRouterImpl extends PureComponent { if (view.error) { if (view.error.name === 'SafariLocalhostHTTPLoadError') { message = 'AppViewRouter--error-from-localhost-url-safari'; + } else if (view.error.name === 'ProfileVersionError') { + message = 'AppViewRouter--error-profile-version'; + additionalMessage =

{view.error.toString()}

; } else { console.error(view.error); additionalMessage = ( diff --git a/src/profile-logic/errors.ts b/src/profile-logic/errors.ts index 863abc0a2f..30d8324079 100644 --- a/src/profile-logic/errors.ts +++ b/src/profile-logic/errors.ts @@ -21,3 +21,30 @@ export class SymbolsNotFoundError extends Error { this.errors = errors; } } + +// Thrown when a profile's format version is newer than the most recent version +// understood by this build. The message is deliberately neutral and only states +// the facts. Consumers (the web app, the CLI) detect this by name and append +// their own advice on how to update, since that advice is frontend-specific. +export class ProfileVersionError extends Error { + formatName: string; + profileVersion: number; + supportedVersion: number; + + constructor( + formatName: string, + profileVersion: number, + supportedVersion: number + ) { + super( + `Unable to parse a ${formatName} profile of version ${profileVersion}. ` + + `The most recent version understood by this build is version ${supportedVersion}.` + ); + // Workaround for a babel issue when extending Errors + (this as any).__proto__ = ProfileVersionError.prototype; + this.name = 'ProfileVersionError'; + this.formatName = formatName; + this.profileVersion = profileVersion; + this.supportedVersion = supportedVersion; + } +} diff --git a/src/profile-logic/gecko-profile-versioning.ts b/src/profile-logic/gecko-profile-versioning.ts index b15d93c3ba..38923e9c45 100644 --- a/src/profile-logic/gecko-profile-versioning.ts +++ b/src/profile-logic/gecko-profile-versioning.ts @@ -14,6 +14,7 @@ import { StringTable } from '../utils/string-table'; import { GECKO_PROFILE_VERSION } from '../app-logic/constants'; +import { ProfileVersionError } from './errors'; // Gecko profiles before version 1 did not have a profile.meta.version field. // Treat those as version zero. @@ -45,10 +46,10 @@ export function upgradeGeckoProfileToCurrentVersion(json: unknown) { } if (profileVersion > GECKO_PROFILE_VERSION) { - throw new Error( - `Unable to parse a Gecko profile of version ${profileVersion}, most likely profiler.firefox.com needs to be refreshed. ` + - `The most recent version understood by this version of profiler.firefox.com is version ${GECKO_PROFILE_VERSION}.\n` + - 'You can try refreshing this page in case profiler.firefox.com has updated in the meantime.' + throw new ProfileVersionError( + 'Gecko', + profileVersion, + GECKO_PROFILE_VERSION ); } diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index d1f96c4c90..eaf192e0e7 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -23,6 +23,7 @@ import { verifyMagic, SIMPLEPERF as SIMPLEPERF_MAGIC } from '../utils/magic'; import { attemptToUpgradeProcessedProfileThroughMutation } from './processed-profile-versioning'; import type { ProfileUpgradeInfo } from './processed-profile-versioning'; import { upgradeGeckoProfileToCurrentVersion } from './gecko-profile-versioning'; +import { ProfileVersionError } from './errors'; import { isPerfScriptFormat, convertPerfScriptProfile, @@ -2313,6 +2314,11 @@ export async function unserializeProfileOfArbitraryFormat( return processGeckoOrDevToolsProfile(json); } catch (e) { console.error('UnserializationError:', e); + // A version mismatch is already a clear, user-facing error. Re-throw it + // as-is so each frontend can detect it and add its own update advice. + if (e instanceof ProfileVersionError) { + throw e; + } throw new Error(`Unserializing the profile failed: ${e}`); } } diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index 16ab69e409..62dd36070d 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -18,6 +18,7 @@ import { ResourceType } from 'firefox-profiler/types'; import { StringTable } from '../utils/string-table'; import { timeCode } from '../utils/time-code'; import { PROCESSED_PROFILE_VERSION } from '../app-logic/constants'; +import { ProfileVersionError } from './errors'; import type { Profile } from 'firefox-profiler/types'; export type ProfileUpgradeInfo = { @@ -85,10 +86,10 @@ export function attemptToUpgradeProcessedProfileThroughMutation( } if (profileVersion > PROCESSED_PROFILE_VERSION) { - throw new Error( - `Unable to parse a processed profile of version ${profileVersion}, most likely profiler.firefox.com needs to be refreshed. ` + - `The most recent version understood by this version of profiler.firefox.com is version ${PROCESSED_PROFILE_VERSION}.\n` + - 'You can try refreshing this page in case profiler.firefox.com has updated in the meantime.' + throw new ProfileVersionError( + 'processed', + profileVersion, + PROCESSED_PROFILE_VERSION ); }