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 <beeme1mr@users.noreply.github.com>
This commit is contained in:
parent
f0de66770b
commit
c6d0b5da9c
|
|
@ -175,13 +175,13 @@ export default {
|
||||||
displayName: 'react',
|
displayName: 'react',
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testMatch: ['<rootDir>/packages/react/test/**/*.spec.ts*'],
|
testMatch: ['<rootDir>/packages/react/test/**/*.spec.{ts,tsx}'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@openfeature/core': '<rootDir>/packages/shared/src',
|
'@openfeature/core': '<rootDir>/packages/shared/src',
|
||||||
'@openfeature/web-sdk': '<rootDir>/packages/client/src',
|
'@openfeature/web-sdk': '<rootDir>/packages/client/src',
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.tsx$': [
|
'^.+\\.(ts|tsx)$': [
|
||||||
'ts-jest',
|
'ts-jest',
|
||||||
{
|
{
|
||||||
tsconfig: '<rootDir>/packages/react/test/tsconfig.json',
|
tsconfig: '<rootDir>/packages/react/test/tsconfig.json',
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import {
|
||||||
ResolutionDetails,
|
ResolutionDetails,
|
||||||
SafeLogger,
|
SafeLogger,
|
||||||
StandardResolutionReasons,
|
StandardResolutionReasons,
|
||||||
statusMatchesEvent
|
instantiateErrorByErrorCode,
|
||||||
|
statusMatchesEvent,
|
||||||
} from '@openfeature/core';
|
} from '@openfeature/core';
|
||||||
import { FlagEvaluationOptions } from '../evaluation';
|
import { FlagEvaluationOptions } from '../evaluation';
|
||||||
import { ProviderEvents } from '../events';
|
import { ProviderEvents } from '../events';
|
||||||
|
|
@ -208,7 +209,7 @@ export class OpenFeatureClient implements Client {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.beforeHooks(allHooks, hookContext, options);
|
this.beforeHooks(allHooks, hookContext, options);
|
||||||
|
|
||||||
// short circuit evaluation entirely if provider is in a bad state
|
// short circuit evaluation entirely if provider is in a bad state
|
||||||
if (this.providerStatus === ProviderStatus.NOT_READY) {
|
if (this.providerStatus === ProviderStatus.NOT_READY) {
|
||||||
throw new ProviderNotReadyError('provider has not yet initialized');
|
throw new ProviderNotReadyError('provider has not yet initialized');
|
||||||
|
|
@ -225,6 +226,10 @@ export class OpenFeatureClient implements Client {
|
||||||
flagKey,
|
flagKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (evaluationDetails.errorCode) {
|
||||||
|
throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
|
this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
|
||||||
|
|
||||||
return evaluationDetails;
|
return evaluationDetails;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import {
|
||||||
GeneralError,
|
GeneralError,
|
||||||
OpenFeature,
|
OpenFeature,
|
||||||
Hook,
|
Hook,
|
||||||
|
StandardResolutionReasons,
|
||||||
|
ErrorCode,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
const BOOLEAN_VALUE = true;
|
const BOOLEAN_VALUE = true;
|
||||||
|
|
@ -206,6 +208,27 @@ describe('Hooks', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('"error" must run if resolution details contains an error code', () => {
|
||||||
|
(MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockReturnValue({
|
||||||
|
value: BOOLEAN_VALUE,
|
||||||
|
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockErrorHook = jest.fn();
|
||||||
|
|
||||||
|
const details = client.getBooleanDetails(FLAG_KEY, false, {
|
||||||
|
hooks: [{ error: mockErrorHook }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockErrorHook).toHaveBeenCalled();
|
||||||
|
expect(details).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||||
|
reason: StandardResolutionReasons.ERROR,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
ResolutionDetails,
|
ResolutionDetails,
|
||||||
SafeLogger,
|
SafeLogger,
|
||||||
StandardResolutionReasons,
|
StandardResolutionReasons,
|
||||||
|
instantiateErrorByErrorCode,
|
||||||
statusMatchesEvent,
|
statusMatchesEvent,
|
||||||
} from '@openfeature/core';
|
} from '@openfeature/core';
|
||||||
import { FlagEvaluationOptions } from '../evaluation';
|
import { FlagEvaluationOptions } from '../evaluation';
|
||||||
|
|
@ -278,6 +279,10 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
|
||||||
flagKey,
|
flagKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (evaluationDetails.errorCode) {
|
||||||
|
throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
await this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
|
await this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);
|
||||||
|
|
||||||
return evaluationDetails;
|
return evaluationDetails;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
import { OpenFeature, Provider, ResolutionDetails, Client, FlagValueType, EvaluationContext, Hook } from '../src';
|
import {
|
||||||
|
OpenFeature,
|
||||||
|
Provider,
|
||||||
|
ResolutionDetails,
|
||||||
|
Client,
|
||||||
|
FlagValueType,
|
||||||
|
EvaluationContext,
|
||||||
|
Hook,
|
||||||
|
StandardResolutionReasons,
|
||||||
|
ErrorCode,
|
||||||
|
} from '../src';
|
||||||
|
|
||||||
const BOOLEAN_VALUE = true;
|
const BOOLEAN_VALUE = true;
|
||||||
|
|
||||||
const BOOLEAN_VARIANT = `${BOOLEAN_VALUE}`;
|
const BOOLEAN_VARIANT = `${BOOLEAN_VALUE}`;
|
||||||
const REASON = 'mocked-value';
|
const REASON = 'mocked-value';
|
||||||
const ERROR_REASON = 'error';
|
|
||||||
const ERROR_CODE = 'MOCKED_ERROR';
|
|
||||||
|
|
||||||
// a mock provider with some jest spies
|
// a mock provider with some jest spies
|
||||||
const MOCK_PROVIDER: Provider = {
|
const MOCK_PROVIDER: Provider = {
|
||||||
|
|
@ -28,8 +36,8 @@ const MOCK_ERROR_PROVIDER: Provider = {
|
||||||
},
|
},
|
||||||
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
reason: ERROR_REASON,
|
reason: StandardResolutionReasons.ERROR,
|
||||||
errorCode: ERROR_CODE,
|
errorCode: ErrorCode.GENERAL,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
} as unknown as Provider;
|
} as unknown as Provider;
|
||||||
|
|
@ -357,6 +365,27 @@ describe('Hooks', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('"error" must run if resolution details contains an error code', async () => {
|
||||||
|
(MOCK_ERROR_PROVIDER.resolveBooleanEvaluation as jest.Mock).mockResolvedValueOnce({
|
||||||
|
value: BOOLEAN_VALUE,
|
||||||
|
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockErrorHook = jest.fn();
|
||||||
|
|
||||||
|
const details = await client.getBooleanDetails(FLAG_KEY, false, undefined, {
|
||||||
|
hooks: [{ error: mockErrorHook }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockErrorHook).toHaveBeenCalled();
|
||||||
|
expect(details).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||||
|
reason: StandardResolutionReasons.ERROR,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -636,8 +665,8 @@ describe('Hooks', () => {
|
||||||
],
|
],
|
||||||
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
reason: ERROR_REASON,
|
reason: StandardResolutionReasons.ERROR,
|
||||||
errorCode: ERROR_CODE,
|
errorCode: ErrorCode.INVALID_CONTEXT,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
} as unknown as Provider;
|
} as unknown as Provider;
|
||||||
|
|
@ -717,8 +746,8 @@ describe('Hooks', () => {
|
||||||
],
|
],
|
||||||
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
reason: ERROR_REASON,
|
reason: StandardResolutionReasons.ERROR,
|
||||||
errorCode: ERROR_CODE,
|
errorCode: ErrorCode.PROVIDER_NOT_READY,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
} as unknown as Provider;
|
} as unknown as Provider;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,45 @@
|
||||||
export * from './general-error';
|
import { ErrorCode } from '../evaluation';
|
||||||
export * from './flag-not-found-error';
|
|
||||||
export * from './parse-error';
|
import { FlagNotFoundError } from './flag-not-found-error';
|
||||||
export * from './type-mismatch-error';
|
import { GeneralError } from './general-error';
|
||||||
export * from './targeting-key-missing-error';
|
import { InvalidContextError } from './invalid-context-error';
|
||||||
export * from './invalid-context-error';
|
import { OpenFeatureError } from './open-feature-error-abstract';
|
||||||
export * from './open-feature-error-abstract';
|
import { ParseError } from './parse-error';
|
||||||
export * from './provider-not-ready-error';
|
import { ProviderFatalError } from './provider-fatal-error';
|
||||||
export * from './provider-fatal-error';
|
import { ProviderNotReadyError } from './provider-not-ready-error';
|
||||||
|
import { TargetingKeyMissingError } from './targeting-key-missing-error';
|
||||||
|
import { TypeMismatchError } from './type-mismatch-error';
|
||||||
|
|
||||||
|
const instantiateErrorByErrorCode = (errorCode: ErrorCode, message?: string): OpenFeatureError => {
|
||||||
|
switch (errorCode) {
|
||||||
|
case ErrorCode.FLAG_NOT_FOUND:
|
||||||
|
return new FlagNotFoundError(message);
|
||||||
|
case ErrorCode.PARSE_ERROR:
|
||||||
|
return new ParseError(message);
|
||||||
|
case ErrorCode.TYPE_MISMATCH:
|
||||||
|
return new TypeMismatchError(message);
|
||||||
|
case ErrorCode.TARGETING_KEY_MISSING:
|
||||||
|
return new TargetingKeyMissingError(message);
|
||||||
|
case ErrorCode.INVALID_CONTEXT:
|
||||||
|
return new InvalidContextError(message);
|
||||||
|
case ErrorCode.PROVIDER_NOT_READY:
|
||||||
|
return new ProviderNotReadyError(message);
|
||||||
|
case ErrorCode.PROVIDER_FATAL:
|
||||||
|
return new ProviderFatalError(message);
|
||||||
|
default:
|
||||||
|
return new GeneralError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
FlagNotFoundError,
|
||||||
|
GeneralError,
|
||||||
|
InvalidContextError,
|
||||||
|
ParseError,
|
||||||
|
ProviderFatalError,
|
||||||
|
ProviderNotReadyError,
|
||||||
|
TargetingKeyMissingError,
|
||||||
|
TypeMismatchError,
|
||||||
|
OpenFeatureError,
|
||||||
|
instantiateErrorByErrorCode,
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue