Skip to content

Commit 4a0dc9f

Browse files
authored
[worker] Remove duplicated user-facing fields from build errors (#3469)
## Summary - remove the duplicated `userFacingMessage` and `userFacingErrorCode` fields from the build error flow - keep current runtime behavior and unknown-error semantics - preserve the current worker protocol shape Stanley: I don't want to have separate `userFacingMessage` and `message` / `userFacingErrorCode` and `errorCode`. It looks like the reason we had them separate was to log "UNKNOWN_ERROR" to the user if we encounter an internal issue like "NPM_CORRUPTED_PACKAGE". This pull request inverts the logic — we're going to have a `trackingCode` so we keep logging "NPM_CORRUPTED_PACKAGE" to Sentry and the default `errorCode` is the one we log to the user. ## Changes - make `BuildError.format()` use canonical `message` and `errorCode` - make `build-tools` store internal-only classification in `trackingCode` instead of parallel public fields - make worker derive `internalErrorCode` from `trackingCode ?? errorCode` - delete the obsolete `userFacing...` fields from `BuildError` ## Testing - `packages/eas-build-job`: `npm exec -- corepack yarn test src/__tests__/errors.test.ts --runInBand` - `packages/build-tools`: `npm exec -- corepack yarn jest-unit src/buildErrors/__tests__/detectError.test.ts --runInBand` - `packages/worker`: `npm exec -- corepack yarn test:integration src/__integration__/stateSync.test.ts`
1 parent 146656c commit 4a0dc9f

9 files changed

Lines changed: 149 additions & 60 deletions

File tree

packages/build-tools/src/buildErrors/__tests__/detectError.test.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ describe(resolveBuildPhaseErrorAsync, () => {
2626
},
2727
'/fake/path'
2828
);
29-
expect(err.errorCode).toBe('NPM_CORRUPTED_PACKAGE');
30-
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
29+
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
30+
expect(err.message).toBe(
31+
'Unknown error. See logs of the Install dependencies build phase for more information.'
32+
);
33+
expect(err.trackingCode).toBe('NPM_CORRUPTED_PACKAGE');
3134
});
3235

3336
it('detects log for invalid bundler and reports it to user', async () => {
@@ -45,7 +48,10 @@ describe(resolveBuildPhaseErrorAsync, () => {
4548
'/fake/path'
4649
);
4750
expect(err.errorCode).toBe('EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR');
48-
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR');
51+
expect(err.message).toBe(
52+
'Your project requires a different version of the Ruby "bundler" program than the version installed in this EAS Build environment. You can specify which version of "bundler" to install by specifying the version under "build"→[buildProfileName]→"ios"→"bundler" in eas.json.'
53+
);
54+
expect(err.trackingCode).toBeUndefined();
4955
});
5056

5157
it('does not detect errors if they show up in different build phase', async () => {
@@ -63,7 +69,10 @@ describe(resolveBuildPhaseErrorAsync, () => {
6369
'/fake/path'
6470
);
6571
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
66-
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
72+
expect(err.message).toBe(
73+
'Unknown error. See logs of the Install dependencies build phase for more information.'
74+
);
75+
expect(err.trackingCode).toBeUndefined();
6776
});
6877

6978
it('detects npm cache error if cache is enabled', async () => {
@@ -82,8 +91,11 @@ describe(resolveBuildPhaseErrorAsync, () => {
8291
},
8392
'/fake/path'
8493
);
85-
expect(err.errorCode).toBe('NPM_CACHE_ERROR');
86-
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
94+
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
95+
expect(err.message).toBe(
96+
'Unknown error. See logs of the Install dependencies build phase for more information.'
97+
);
98+
expect(err.trackingCode).toBe('NPM_CACHE_ERROR');
8799
});
88100

89101
it('does not detect npm cache error if cache is disabled', async () => {
@@ -103,7 +115,10 @@ describe(resolveBuildPhaseErrorAsync, () => {
103115
'/fake/path'
104116
);
105117
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
106-
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
118+
expect(err.message).toBe(
119+
'Unknown error. See logs of the Install dependencies build phase for more information.'
120+
);
121+
expect(err.trackingCode).toBeUndefined();
107122
});
108123

109124
it('detects xcode line error', async () => {
@@ -126,7 +141,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
126141
'/path/to/'
127142
);
128143
expect(err.errorCode).toBe('XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR');
129-
expect(err.userFacingErrorCode).toBe('XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR');
144+
expect(err.trackingCode).toBeUndefined();
130145
});
131146

132147
it('detects minimum deployment target error correctly', async () => {
@@ -144,7 +159,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
144159
'/fake/path'
145160
);
146161
expect(err.errorCode).toBe('EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR');
147-
expect(err.userFacingErrorCode).toBe('EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR');
162+
expect(err.trackingCode).toBeUndefined();
148163
});
149164

150165
it('detects provisioning profile mismatch error correctly', async () => {
@@ -162,7 +177,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
162177
'/fake/path'
163178
);
164179
expect(err.errorCode).toBe('EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR');
165-
expect(err.userFacingErrorCode).toBe('EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR');
180+
expect(err.trackingCode).toBeUndefined();
166181
});
167182

168183
it('detects generic "Run Fastlane" error for resign correctly', async () => {
@@ -178,7 +193,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
178193
'/fake/path'
179194
);
180195
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_RESIGN_ERROR');
181-
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_RESIGN_ERROR');
196+
expect(err.trackingCode).toBeUndefined();
182197
});
183198

184199
it('detects build error in "Run Fastlane" phase correctly', async () => {
@@ -194,7 +209,7 @@ describe(resolveBuildPhaseErrorAsync, () => {
194209
'/fake/path'
195210
);
196211
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_ERROR');
197-
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_ERROR');
212+
expect(err.trackingCode).toBeUndefined();
198213
});
199214

200215
it('detects provisioning profile mismatch error correctly', async () => {
@@ -240,8 +255,8 @@ note`;
240255
'/path/to'
241256
);
242257
expect(err.errorCode).toBe('XCODE_BUILD_ERROR');
243-
expect(err.userFacingErrorCode).toBe('XCODE_BUILD_ERROR');
244-
expect(err.userFacingMessage)
258+
expect(err.trackingCode).toBeUndefined();
259+
expect(err.message)
245260
.toBe(`The "Run fastlane" step failed because of an error in the Xcode build process. We automatically detected following errors in your Xcode build logs:
246261
- The signature of “ProgrammaticAccessLibrary.xcframework” cannot be verified.
247262
- Some other error
@@ -263,9 +278,9 @@ Refer to "Xcode Logs" below for additional, more detailed logs.`);
263278
'/fake/path'
264279
);
265280

266-
expect(err.errorCode).toBe('MAVEN_CACHE_ERROR');
267-
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
268-
expect(err.userFacingMessage).toBe(
281+
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
282+
expect(err.trackingCode).toBe('MAVEN_CACHE_ERROR');
283+
expect(err.message).toBe(
269284
`Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.`
270285
);
271286
});
@@ -285,8 +300,8 @@ Refer to "Xcode Logs" below for additional, more detailed logs.`);
285300
);
286301

287302
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
288-
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
289-
expect(err.userFacingMessage).toBe(
303+
expect(err.trackingCode).toBeUndefined();
304+
expect(err.message).toBe(
290305
`Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.`
291306
);
292307
});

packages/build-tools/src/buildErrors/detectError.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,14 @@ export async function resolveBuildPhaseErrorAsync(
8080
new errors.UnknownError(errorContext.phase));
8181
const buildError = resolveError(buildErrorHandlers, logLines, errorContext, xcodeBuildLogs);
8282

83-
const isUnknownUserError =
84-
!userFacingError ||
85-
(
86-
[
87-
errors.ErrorCode.UNKNOWN_ERROR,
88-
errors.ErrorCode.UNKNOWN_GRADLE_ERROR,
89-
errors.ErrorCode.UNKNOWN_FASTLANE_ERROR,
90-
] as string[]
91-
).includes(userFacingError.errorCode);
92-
const message =
93-
(isUnknownUserError ? buildError?.message : userFacingError.message) ?? userFacingError.message;
94-
const errorCode =
95-
(isUnknownUserError ? buildError?.errorCode : userFacingError.errorCode) ??
96-
userFacingError.errorCode;
83+
const trackingCode =
84+
buildError && buildError.errorCode !== userFacingError.errorCode
85+
? buildError.errorCode
86+
: undefined;
9787

98-
return new errors.BuildError(message, {
99-
errorCode,
100-
userFacingErrorCode: userFacingError.errorCode,
101-
userFacingMessage: userFacingError.message,
88+
return new errors.BuildError(userFacingError.message, {
89+
errorCode: userFacingError.errorCode,
90+
trackingCode,
10291
docsUrl: userFacingError.docsUrl,
10392
innerError: error,
10493
buildPhase: phase,

packages/build-tools/src/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ export class BuildContext<TJob extends Job = Job> {
299299
// leaving message empty, website will display err.stack which already includes err.message
300300
this.logger.error({ err }, '');
301301
} else {
302-
this.logger.error(`Error: ${buildError.userFacingMessage}`);
302+
this.logger.error(`Error: ${buildError.message}`);
303303
}
304304
return buildError;
305305
}

packages/eas-build-job/src/__tests__/errors.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
1-
import { UserFacingError } from '../errors';
1+
import { BuildError, UserFacingError } from '../errors';
2+
import { BuildPhase } from '../logs';
3+
4+
describe(BuildError, () => {
5+
it('formats using canonical message and errorCode', () => {
6+
const error = new BuildError('canonical message', {
7+
errorCode: 'ERR_CODE',
8+
docsUrl: 'https://docs.example.dev',
9+
buildPhase: BuildPhase.PREBUILD,
10+
});
11+
12+
expect(error.format()).toEqual({
13+
errorCode: 'ERR_CODE',
14+
message: 'canonical message',
15+
docsUrl: 'https://docs.example.dev',
16+
buildPhase: BuildPhase.PREBUILD,
17+
});
18+
});
19+
});
220

321
describe(UserFacingError, () => {
422
it('supports docsUrl in options', () => {

packages/eas-build-job/src/errors.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,33 @@ export interface ExternalBuildError {
1919

2020
interface BuildErrorDetails {
2121
errorCode: string;
22-
userFacingMessage: string;
23-
userFacingErrorCode: string;
22+
trackingCode?: string;
2423
docsUrl?: string;
2524
innerError?: Error;
2625
buildPhase?: BuildPhase;
2726
}
2827

2928
export class BuildError extends Error {
3029
public errorCode: string;
31-
public userFacingMessage: string;
32-
public userFacingErrorCode: string;
30+
// Internal-only classification used for Sentry, analytics, and worker internalErrorCode.
31+
// The public error saved on builds and job runs is always `errorCode`.
32+
public trackingCode?: string;
3333
public docsUrl?: string;
3434
public innerError?: Error;
3535
public buildPhase?: BuildPhase;
3636

3737
constructor(message: string, details: BuildErrorDetails) {
3838
super(message);
3939
this.errorCode = details.errorCode;
40-
this.userFacingErrorCode = details.userFacingErrorCode;
41-
this.userFacingMessage = details.userFacingMessage;
40+
this.trackingCode = details.trackingCode;
4241
this.docsUrl = details.docsUrl;
4342
this.innerError = details.innerError;
4443
this.buildPhase = details.buildPhase;
4544
}
4645
public format(): ExternalBuildError {
4746
return {
48-
errorCode: this.userFacingErrorCode,
49-
message: this.userFacingMessage,
47+
errorCode: this.errorCode,
48+
message: this.message,
5049
docsUrl: this.docsUrl,
5150
buildPhase: this.buildPhase,
5251
};
@@ -83,8 +82,6 @@ export class UnknownBuildError extends BuildError {
8382
const message = 'Unknown error. See logs for more information.';
8483
super(message, {
8584
errorCode,
86-
userFacingMessage: message,
87-
userFacingErrorCode: errorCode,
8885
});
8986
}
9087
}
@@ -95,8 +92,6 @@ export class UnknownCustomBuildError extends BuildError {
9592
const message = 'Unknown custom build error. See logs for more information.';
9693
super(message, {
9794
errorCode,
98-
userFacingMessage: message,
99-
userFacingErrorCode: errorCode,
10095
});
10196
}
10297
}

packages/worker/src/__integration__/stateSync.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,5 +417,76 @@ describe('State sync mechanism', () => {
417417
ws.close();
418418
clearTimeout(messageTimeout);
419419
});
420+
421+
it('should expose tracking code as internal error code from generic job', async () => {
422+
jest.mocked(runGenericJobAsync).mockResolvedValueOnce({
423+
runResult: result(
424+
new errors.BuildError('ASC app was not found for this account.', {
425+
errorCode: genericJobRunErrorCode,
426+
trackingCode: 'GENERIC_TRACKING_ERROR',
427+
})
428+
),
429+
buildWorkflow: {} as any,
430+
});
431+
const dispatchWS = new WebSocket(`ws://localhost:${port}?expo_vm_name=${hostname()}`);
432+
const dispatchHelper = new WsHelper(dispatchWS);
433+
await dispatchHelper.onOpen();
434+
dispatchWS.send(
435+
JSON.stringify({
436+
type: 'dispatch',
437+
buildId,
438+
job: createTestGenericJob(),
439+
initiatingUserId: '14367e1b-26fc-4c00-aedb-0629d78f8286',
440+
metadata: {
441+
trackingContext: {},
442+
},
443+
})
444+
);
445+
dispatchWS.close();
446+
await dispatchHelper.onClose();
447+
448+
const ws = new WebSocket(`ws://localhost:${port}?expo_vm_name=${hostname()}`);
449+
const helper = new WsHelper(ws);
450+
451+
let stateResponsePromiseResolve: () => void;
452+
const stateResponsePromise = new Promise<void>(res => {
453+
stateResponsePromiseResolve = res;
454+
});
455+
const onMessage = jest.fn((message: any) => {
456+
clearTimeout(messageTimeout);
457+
try {
458+
expect(message).toBeTruthy();
459+
expect(message.type).toBe('state-response');
460+
expect(message.status).toBe('error');
461+
expect(message.externalBuildError?.errorCode).toBe(genericJobRunErrorCode);
462+
expect(message.externalBuildError?.message).toContain(
463+
'ASC app was not found for this account.'
464+
);
465+
expect(message.internalErrorCode).toBe('GENERIC_TRACKING_ERROR');
466+
} catch (err) {
467+
throw err;
468+
} finally {
469+
stateResponsePromiseResolve();
470+
}
471+
});
472+
const openPromise = helper.onOpen();
473+
helper.onMessage(onMessage);
474+
475+
await openPromise;
476+
messageTimeout = setTimeout(() => {
477+
unreachableCode('state-response timeout');
478+
}, 3000);
479+
480+
ws.send(JSON.stringify({ type: 'state-query', buildId }));
481+
482+
await stateResponsePromise;
483+
484+
expect(helper.onErrorCb).not.toHaveBeenCalled();
485+
expect(helper.onOpenCb).toHaveBeenCalled();
486+
expect(helper.onMessageCb).toHaveBeenCalled();
487+
expect(helper.onCloseCb).not.toHaveBeenCalled();
488+
ws.close();
489+
clearTimeout(messageTimeout);
490+
});
420491
});
421492
});

packages/worker/src/build.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,16 @@ function logBuildError(logger: bunyan, analytics: Analytics, err: Error): void {
104104
l.info({ marker: LogMarker.START_PHASE }, `Start phase: ${BuildPhase.FAIL_BUILD}`);
105105

106106
if (err instanceof errors.BuildError) {
107+
const internalErrorCode = err.trackingCode ?? err.errorCode;
107108
analytics.logEvent(Event.WORKER_BUILD_FAIL, {
108109
reason: err?.message,
109-
error_code: err.errorCode,
110+
error_code: internalErrorCode,
110111
build_phase: err.buildPhase,
111112
});
112113
if (err.errorCode !== errors.ErrorCode.UNKNOWN_ERROR) {
113-
l.error(`Build failed: ${err.userFacingMessage}`);
114+
l.error(`Build failed: ${err.message}`);
114115
} else {
115-
l.error({ err: err.innerError ?? err.userFacingMessage }, `Build failed\n`);
116+
l.error({ err: err.innerError ?? err }, `Build failed\n`);
116117
}
117118
} else {
118119
// This can only happen when error is thrown outside of a build phase.

0 commit comments

Comments
 (0)