Skip to content

Commit c6d0b5d

Browse files
authored
fix: run error hook when provider returns reason error or error code (#926)
## This PR - runs error hook when provider returns reason error or error code ### Related Issues Fixes #925 ### Notes Based on a conversation in Slack: https://cloud-native.slack.com/archives/C06E4DE6S07/p1714581197391509 --------- Signed-off-by: Michael Beemer <[email protected]>
1 parent f0de667 commit c6d0b5d

File tree

6 files changed

+120
-22
lines changed

6 files changed

+120
-22
lines changed

jest.config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,13 @@ export default {
175175
displayName: 'react',
176176
testEnvironment: 'jsdom',
177177
preset: 'ts-jest',
178-
testMatch: ['<rootDir>/packages/react/test/**/*.spec.ts*'],
178+
testMatch: ['<rootDir>/packages/react/test/**/*.spec.{ts,tsx}'],
179179
moduleNameMapper: {
180180
'@openfeature/core': '<rootDir>/packages/shared/src',
181181
'@openfeature/web-sdk': '<rootDir>/packages/client/src',
182182
},
183183
transform: {
184-
'^.+\\.tsx$': [
184+
'^.+\\.(ts|tsx)$': [
185185
'ts-jest',
186186
{
187187
tsconfig: '<rootDir>/packages/react/test/tsconfig.json',

packages/client/src/client/open-feature-client.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
ResolutionDetails,
1616
SafeLogger,
1717
StandardResolutionReasons,
18-
statusMatchesEvent
18+
instantiateErrorByErrorCode,
19+
statusMatchesEvent,
1920
} from '@openfeature/core';
2021
import { FlagEvaluationOptions } from '../evaluation';
2122
import { ProviderEvents } from '../events';
@@ -208,7 +209,7 @@ export class OpenFeatureClient implements Client {
208209

209210
try {
210211
this.beforeHooks(allHooks, hookContext, options);
211-
212+
212213
// short circuit evaluation entirely if provider is in a bad state
213214
if (this.providerStatus === ProviderStatus.NOT_READY) {
214215
throw new ProviderNotReadyError('provider has not yet initialized');
@@ -225,6 +226,10 @@ export class OpenFeatureClient implements Client {
225226
flagKey,
226227
};
227228

229+
if (evaluationDetails.errorCode) {
230+
throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
231+
}
232+
228233
this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
229234

230235
return evaluationDetails;

packages/client/test/hooks.spec.ts

+23
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
GeneralError,
88
OpenFeature,
99
Hook,
10+
StandardResolutionReasons,
11+
ErrorCode,
1012
} from '../src';
1113

1214
const BOOLEAN_VALUE = true;
@@ -206,6 +208,27 @@ describe('Hooks', () => {
206208
],
207209
});
208210
});
211+
212+
it('"error" must run if resolution details contains an error code', () => {
213+
(MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockReturnValue({
214+
value: BOOLEAN_VALUE,
215+
errorCode: ErrorCode.FLAG_NOT_FOUND,
216+
});
217+
218+
const mockErrorHook = jest.fn();
219+
220+
const details = client.getBooleanDetails(FLAG_KEY, false, {
221+
hooks: [{ error: mockErrorHook }],
222+
});
223+
224+
expect(mockErrorHook).toHaveBeenCalled();
225+
expect(details).toEqual(
226+
expect.objectContaining({
227+
errorCode: ErrorCode.FLAG_NOT_FOUND,
228+
reason: StandardResolutionReasons.ERROR,
229+
}),
230+
);
231+
});
209232
});
210233
});
211234

packages/server/src/client/open-feature-client.ts

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ResolutionDetails,
1717
SafeLogger,
1818
StandardResolutionReasons,
19+
instantiateErrorByErrorCode,
1920
statusMatchesEvent,
2021
} from '@openfeature/core';
2122
import { FlagEvaluationOptions } from '../evaluation';
@@ -278,6 +279,10 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
278279
flagKey,
279280
};
280281

282+
if (evaluationDetails.errorCode) {
283+
throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
284+
}
285+
281286
await this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
282287

283288
return evaluationDetails;

packages/server/test/hooks.spec.ts

+38-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import { OpenFeature, Provider, ResolutionDetails, Client, FlagValueType, EvaluationContext, Hook } from '../src';
1+
import {
2+
OpenFeature,
3+
Provider,
4+
ResolutionDetails,
5+
Client,
6+
FlagValueType,
7+
EvaluationContext,
8+
Hook,
9+
StandardResolutionReasons,
10+
ErrorCode,
11+
} from '../src';
212

313
const BOOLEAN_VALUE = true;
414

515
const BOOLEAN_VARIANT = `${BOOLEAN_VALUE}`;
616
const REASON = 'mocked-value';
7-
const ERROR_REASON = 'error';
8-
const ERROR_CODE = 'MOCKED_ERROR';
917

1018
// a mock provider with some jest spies
1119
const MOCK_PROVIDER: Provider = {
@@ -28,8 +36,8 @@ const MOCK_ERROR_PROVIDER: Provider = {
2836
},
2937
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
3038
return Promise.reject({
31-
reason: ERROR_REASON,
32-
errorCode: ERROR_CODE,
39+
reason: StandardResolutionReasons.ERROR,
40+
errorCode: ErrorCode.GENERAL,
3341
});
3442
}),
3543
} as unknown as Provider;
@@ -357,6 +365,27 @@ describe('Hooks', () => {
357365
],
358366
});
359367
});
368+
369+
it('"error" must run if resolution details contains an error code', async () => {
370+
(MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockResolvedValueOnce({
371+
value: BOOLEAN_VALUE,
372+
errorCode: ErrorCode.FLAG_NOT_FOUND,
373+
});
374+
375+
const mockErrorHook = jest.fn();
376+
377+
const details = await client.getBooleanDetails(FLAG_KEY, false, undefined, {
378+
hooks: [{ error: mockErrorHook }],
379+
});
380+
381+
expect(mockErrorHook).toHaveBeenCalled();
382+
expect(details).toEqual(
383+
expect.objectContaining({
384+
errorCode: ErrorCode.FLAG_NOT_FOUND,
385+
reason: StandardResolutionReasons.ERROR,
386+
}),
387+
);
388+
});
360389
});
361390
});
362391

@@ -636,8 +665,8 @@ describe('Hooks', () => {
636665
],
637666
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
638667
return Promise.reject({
639-
reason: ERROR_REASON,
640-
errorCode: ERROR_CODE,
668+
reason: StandardResolutionReasons.ERROR,
669+
errorCode: ErrorCode.INVALID_CONTEXT,
641670
});
642671
}),
643672
} as unknown as Provider;
@@ -717,8 +746,8 @@ describe('Hooks', () => {
717746
],
718747
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
719748
return Promise.reject({
720-
reason: ERROR_REASON,
721-
errorCode: ERROR_CODE,
749+
reason: StandardResolutionReasons.ERROR,
750+
errorCode: ErrorCode.PROVIDER_NOT_READY,
722751
});
723752
}),
724753
} as unknown as Provider;

packages/shared/src/errors/index.ts

+45-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,45 @@
1-
export * from './general-error';
2-
export * from './flag-not-found-error';
3-
export * from './parse-error';
4-
export * from './type-mismatch-error';
5-
export * from './targeting-key-missing-error';
6-
export * from './invalid-context-error';
7-
export * from './open-feature-error-abstract';
8-
export * from './provider-not-ready-error';
9-
export * from './provider-fatal-error';
1+
import { ErrorCode } from '../evaluation';
2+
3+
import { FlagNotFoundError } from './flag-not-found-error';
4+
import { GeneralError } from './general-error';
5+
import { InvalidContextError } from './invalid-context-error';
6+
import { OpenFeatureError } from './open-feature-error-abstract';
7+
import { ParseError } from './parse-error';
8+
import { ProviderFatalError } from './provider-fatal-error';
9+
import { ProviderNotReadyError } from './provider-not-ready-error';
10+
import { TargetingKeyMissingError } from './targeting-key-missing-error';
11+
import { TypeMismatchError } from './type-mismatch-error';
12+
13+
const instantiateErrorByErrorCode = (errorCode: ErrorCode, message?: string): OpenFeatureError => {
14+
switch (errorCode) {
15+
case ErrorCode.FLAG_NOT_FOUND:
16+
return new FlagNotFoundError(message);
17+
case ErrorCode.PARSE_ERROR:
18+
return new ParseError(message);
19+
case ErrorCode.TYPE_MISMATCH:
20+
return new TypeMismatchError(message);
21+
case ErrorCode.TARGETING_KEY_MISSING:
22+
return new TargetingKeyMissingError(message);
23+
case ErrorCode.INVALID_CONTEXT:
24+
return new InvalidContextError(message);
25+
case ErrorCode.PROVIDER_NOT_READY:
26+
return new ProviderNotReadyError(message);
27+
case ErrorCode.PROVIDER_FATAL:
28+
return new ProviderFatalError(message);
29+
default:
30+
return new GeneralError(message);
31+
}
32+
};
33+
34+
export {
35+
FlagNotFoundError,
36+
GeneralError,
37+
InvalidContextError,
38+
ParseError,
39+
ProviderFatalError,
40+
ProviderNotReadyError,
41+
TargetingKeyMissingError,
42+
TypeMismatchError,
43+
OpenFeatureError,
44+
instantiateErrorByErrorCode,
45+
};

0 commit comments

Comments
 (0)