Skip to content

Commit 4bd8b75

Browse files
authored
Show more user friendly errors for unsupported profile version in both the frontend and the cli (#6107)
Fixes #6105 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. Frontend: Before: <img width="693" height="417" alt="Screenshot 2026-06-17 at 15 05 53" src="https://github.com/user-attachments/assets/2cbb8876-4011-4c3e-9fa6-765319f5f4bd" /> After: <img width="758" height="488" alt="Screenshot 2026-06-17 at 15 22 31" src="https://github.com/user-attachments/assets/96bb0c5a-a6ac-47dd-9f4e-5a3b87274639" /> CLI: Before: <img width="1227" height="120" alt="Screenshot 2026-06-17 at 15 23 58" src="https://github.com/user-attachments/assets/e0d52008-6d17-4943-9e2e-cff9a8806ab7" /> After: <img width="1171" height="127" alt="Screenshot 2026-06-17 at 15 03 34" src="https://github.com/user-attachments/assets/3d9031ed-b1ec-40d5-8c13-c77fe7c82465" />
2 parents 0a21ce1 + 53a3071 commit 4bd8b75

9 files changed

Lines changed: 86 additions & 15 deletions

File tree

locales/en-US/app.ftl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ AppViewRouter--error-from-localhost-url-safari =
5454
this page in { -firefox-brand-name } or Chrome instead.
5555
.title = Safari cannot import local profiles
5656
57+
# This error message is displayed when the profile is in a newer format version
58+
# than this build of the Profiler is able to read.
59+
AppViewRouter--error-profile-version =
60+
This profile uses a format that is not supported by this version of { -profiler-brand-name }.
61+
Try refreshing the page to check if there is an update available for { -profiler-brand-name }.
62+
5763
AppViewRouter--route-not-found--home =
5864
.specialMessage = The URL you tried to reach was not recognized.
5965

profiler-cli/src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
// These globals are defined via esbuild's define option.
1010
declare const __BUILD_HASH__: string;
11+
declare const __PACKAGE_NAME__: string;
1112
declare const __VERSION__: string;
1213

1314
/**
@@ -16,6 +17,11 @@ declare const __VERSION__: string;
1617
*/
1718
export const BUILD_HASH = __BUILD_HASH__;
1819

20+
/**
21+
* Package name from profiler-cli/package.json, injected at build time.
22+
*/
23+
export const PACKAGE_NAME = __PACKAGE_NAME__;
24+
1925
/**
2026
* Package version from profiler-cli/package.json, injected at build time.
2127
*/

profiler-cli/src/daemon.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as net from 'net';
1111
import * as fs from 'fs';
1212
import { ProfileQuerier } from '../../src/profile-query';
1313
import type { LoadPhase } from '../../src/profile-query/loader';
14+
import { ProfileVersionError } from 'firefox-profiler/profile-logic/errors';
1415
import type {
1516
ClientCommand,
1617
ClientMessage,
@@ -28,7 +29,27 @@ import {
2829
ensureSessionDir,
2930
} from './session';
3031
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
31-
import { BUILD_HASH } from './constants';
32+
import { BUILD_HASH, PACKAGE_NAME } from './constants';
33+
34+
/**
35+
* Build a user-facing message for a profile load failure. When the profile is
36+
* too new for this build, append instructions on how to update the CLI.
37+
*/
38+
function formatProfileLoadError(error: unknown): string {
39+
if (
40+
error instanceof ProfileVersionError ||
41+
(error instanceof Error && error.name === 'ProfileVersionError')
42+
) {
43+
const versionError = error as ProfileVersionError;
44+
return (
45+
`This profile is version ${versionError.profileVersion}, but this profiler-cli only ` +
46+
`supports up to version ${versionError.supportedVersion} of the ${versionError.formatName} profile format.\n` +
47+
`Update to the latest version with:\n` +
48+
` npm install -g ${PACKAGE_NAME}@latest`
49+
);
50+
}
51+
return error instanceof Error ? error.message : String(error);
52+
}
3253

3354
export class Daemon {
3455
private querier: ProfileQuerier | null = null;
@@ -41,7 +62,7 @@ export class Daemon {
4162
private profilePath: string;
4263
private symbolServerUrl?: string;
4364
private loadPhase: LoadPhase = 'fetching';
44-
private profileLoadError: Error | null = null;
65+
private profileLoadError: string | null = null;
4566

4667
constructor(
4768
sessionDir: string,
@@ -149,8 +170,7 @@ export class Daemon {
149170
console.log('Profile loaded successfully');
150171
} catch (error) {
151172
console.error(`Failed to load profile: ${error}`);
152-
this.profileLoadError =
153-
error instanceof Error ? error : new Error(String(error));
173+
this.profileLoadError = formatProfileLoadError(error);
154174
}
155175
}
156176

@@ -210,7 +230,7 @@ export class Daemon {
210230
if (this.profileLoadError) {
211231
return {
212232
type: 'error',
213-
error: `Profile load failed: ${this.profileLoadError.message}`,
233+
error: `Profile load failed: ${this.profileLoadError}`,
214234
};
215235
}
216236
switch (this.loadPhase) {
@@ -245,7 +265,7 @@ export class Daemon {
245265
if (this.profileLoadError) {
246266
return {
247267
type: 'error',
248-
error: `Profile load failed: ${this.profileLoadError.message}`,
268+
error: `Profile load failed: ${this.profileLoadError}`,
249269
};
250270
}
251271
if (this.loadPhase !== 'ready' || !this.querier) {

scripts/build-profiler-cli.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import esbuild from 'esbuild';
55
import { chmodSync, readFileSync } from 'fs';
66
import { nodeBaseConfig } from './lib/esbuild-configs.mjs';
77

8-
const { version } = JSON.parse(
8+
const { name, version } = JSON.parse(
99
readFileSync(new URL('../profiler-cli/package.json', import.meta.url), 'utf8')
1010
);
1111

@@ -22,6 +22,7 @@ const profilerCliConfig = {
2222
},
2323
define: {
2424
__BUILD_HASH__: JSON.stringify(BUILD_HASH),
25+
__PACKAGE_NAME__: JSON.stringify(name),
2526
__VERSION__: JSON.stringify(version),
2627
// SOURCE_MAP_WORKER_PATH is injected by the browser build. The CLI doesn't
2728
// use source map workers but the shared code references this constant.

src/components/app/AppViewRouter.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ class AppViewRouterImpl extends PureComponent<AppViewRouterProps> {
8787
if (view.error) {
8888
if (view.error.name === 'SafariLocalhostHTTPLoadError') {
8989
message = 'AppViewRouter--error-from-localhost-url-safari';
90+
} else if (view.error.name === 'ProfileVersionError') {
91+
message = 'AppViewRouter--error-profile-version';
92+
additionalMessage = <p>{view.error.toString()}</p>;
9093
} else {
9194
console.error(view.error);
9295
additionalMessage = (

src/profile-logic/errors.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,30 @@ export class SymbolsNotFoundError extends Error {
2121
this.errors = errors;
2222
}
2323
}
24+
25+
// Thrown when a profile's format version is newer than the most recent version
26+
// understood by this build. The message is deliberately neutral and only states
27+
// the facts. Consumers (the web app, the CLI) detect this by name and append
28+
// their own advice on how to update, since that advice is frontend-specific.
29+
export class ProfileVersionError extends Error {
30+
formatName: string;
31+
profileVersion: number;
32+
supportedVersion: number;
33+
34+
constructor(
35+
formatName: string,
36+
profileVersion: number,
37+
supportedVersion: number
38+
) {
39+
super(
40+
`Unable to parse a ${formatName} profile of version ${profileVersion}. ` +
41+
`The most recent version understood by this build is version ${supportedVersion}.`
42+
);
43+
// Workaround for a babel issue when extending Errors
44+
(this as any).__proto__ = ProfileVersionError.prototype;
45+
this.name = 'ProfileVersionError';
46+
this.formatName = formatName;
47+
this.profileVersion = profileVersion;
48+
this.supportedVersion = supportedVersion;
49+
}
50+
}

src/profile-logic/gecko-profile-versioning.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import { StringTable } from '../utils/string-table';
1616
import { GECKO_PROFILE_VERSION } from '../app-logic/constants';
17+
import { ProfileVersionError } from './errors';
1718

1819
// Gecko profiles before version 1 did not have a profile.meta.version field.
1920
// Treat those as version zero.
@@ -45,10 +46,10 @@ export function upgradeGeckoProfileToCurrentVersion(json: unknown) {
4546
}
4647

4748
if (profileVersion > GECKO_PROFILE_VERSION) {
48-
throw new Error(
49-
`Unable to parse a Gecko profile of version ${profileVersion}, most likely profiler.firefox.com needs to be refreshed. ` +
50-
`The most recent version understood by this version of profiler.firefox.com is version ${GECKO_PROFILE_VERSION}.\n` +
51-
'You can try refreshing this page in case profiler.firefox.com has updated in the meantime.'
49+
throw new ProfileVersionError(
50+
'Gecko',
51+
profileVersion,
52+
GECKO_PROFILE_VERSION
5253
);
5354
}
5455

src/profile-logic/process-profile.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { verifyMagic, SIMPLEPERF as SIMPLEPERF_MAGIC } from '../utils/magic';
2323
import { attemptToUpgradeProcessedProfileThroughMutation } from './processed-profile-versioning';
2424
import type { ProfileUpgradeInfo } from './processed-profile-versioning';
2525
import { upgradeGeckoProfileToCurrentVersion } from './gecko-profile-versioning';
26+
import { ProfileVersionError } from './errors';
2627
import {
2728
isPerfScriptFormat,
2829
convertPerfScriptProfile,
@@ -2313,6 +2314,11 @@ export async function unserializeProfileOfArbitraryFormat(
23132314
return processGeckoOrDevToolsProfile(json);
23142315
} catch (e) {
23152316
console.error('UnserializationError:', e);
2317+
// A version mismatch is already a clear, user-facing error. Re-throw it
2318+
// as-is so each frontend can detect it and add its own update advice.
2319+
if (e instanceof ProfileVersionError) {
2320+
throw e;
2321+
}
23162322
throw new Error(`Unserializing the profile failed: ${e}`);
23172323
}
23182324
}

src/profile-logic/processed-profile-versioning.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ResourceType } from 'firefox-profiler/types';
1818
import { StringTable } from '../utils/string-table';
1919
import { timeCode } from '../utils/time-code';
2020
import { PROCESSED_PROFILE_VERSION } from '../app-logic/constants';
21+
import { ProfileVersionError } from './errors';
2122
import type { Profile } from 'firefox-profiler/types';
2223

2324
export type ProfileUpgradeInfo = {
@@ -85,10 +86,10 @@ export function attemptToUpgradeProcessedProfileThroughMutation(
8586
}
8687

8788
if (profileVersion > PROCESSED_PROFILE_VERSION) {
88-
throw new Error(
89-
`Unable to parse a processed profile of version ${profileVersion}, most likely profiler.firefox.com needs to be refreshed. ` +
90-
`The most recent version understood by this version of profiler.firefox.com is version ${PROCESSED_PROFILE_VERSION}.\n` +
91-
'You can try refreshing this page in case profiler.firefox.com has updated in the meantime.'
89+
throw new ProfileVersionError(
90+
'processed',
91+
profileVersion,
92+
PROCESSED_PROFILE_VERSION
9293
);
9394
}
9495

0 commit comments

Comments
 (0)