Skip to content

Commit

Permalink
fix: run error hook when provider returns reason error or error code (#…
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
beeme1mr authored May 8, 2024
1 parent f0de667 commit c6d0b5d
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 22 deletions.
4 changes: 2 additions & 2 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,13 @@ export default {
displayName: 'react',
testEnvironment: 'jsdom',
preset: 'ts-jest',
testMatch: ['<rootDir>/packages/react/test/**/*.spec.ts*'],
testMatch: ['<rootDir>/packages/react/test/**/*.spec.{ts,tsx}'],
moduleNameMapper: {
'@openfeature/core': '<rootDir>/packages/shared/src',
'@openfeature/web-sdk': '<rootDir>/packages/client/src',
},
transform: {
'^.+\\.tsx$': [
'^.+\\.(ts|tsx)$': [
'ts-jest',
{
tsconfig: '<rootDir>/packages/react/test/tsconfig.json',
Expand Down
9 changes: 7 additions & 2 deletions packages/client/src/client/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
ResolutionDetails,
SafeLogger,
StandardResolutionReasons,
statusMatchesEvent
instantiateErrorByErrorCode,
statusMatchesEvent,
} from '@openfeature/core';
import { FlagEvaluationOptions } from '../evaluation';
import { ProviderEvents } from '../events';
Expand Down Expand Up @@ -208,7 +209,7 @@ export class OpenFeatureClient implements Client {

try {
this.beforeHooks(allHooks, hookContext, options);

// short circuit evaluation entirely if provider is in a bad state
if (this.providerStatus === ProviderStatus.NOT_READY) {
throw new ProviderNotReadyError('provider has not yet initialized');
Expand All @@ -225,6 +226,10 @@ export class OpenFeatureClient implements Client {
flagKey,
};

if (evaluationDetails.errorCode) {
throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
}

this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);

return evaluationDetails;
Expand Down
23 changes: 23 additions & 0 deletions packages/client/test/hooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
GeneralError,
OpenFeature,
Hook,
StandardResolutionReasons,
ErrorCode,
} from '../src';

const BOOLEAN_VALUE = true;
Expand Down Expand Up @@ -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,
}),
);
});
});
});

Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/client/open-feature-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ResolutionDetails,
SafeLogger,
StandardResolutionReasons,
instantiateErrorByErrorCode,
statusMatchesEvent,
} from '@openfeature/core';
import { FlagEvaluationOptions } from '../evaluation';
Expand Down Expand Up @@ -278,6 +279,10 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
flagKey,
};

if (evaluationDetails.errorCode) {
throw instantiateErrorByErrorCode(evaluationDetails.errorCode);
}

await this.afterHooks(allHooksReversed, hookContext, evaluationDetails, options);

return evaluationDetails;
Expand Down
47 changes: 38 additions & 9 deletions packages/server/test/hooks.spec.ts
Original file line number Diff line number Diff line change
@@ -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_VARIANT = `${BOOLEAN_VALUE}`;
const REASON = 'mocked-value';
const ERROR_REASON = 'error';
const ERROR_CODE = 'MOCKED_ERROR';

// a mock provider with some jest spies
const MOCK_PROVIDER: Provider = {
Expand All @@ -28,8 +36,8 @@ const MOCK_ERROR_PROVIDER: Provider = {
},
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: ERROR_REASON,
errorCode: ERROR_CODE,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.GENERAL,
});
}),
} as unknown as Provider;
Expand Down Expand Up @@ -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,
}),
);
});
});
});

Expand Down Expand Up @@ -636,8 +665,8 @@ describe('Hooks', () => {
],
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: ERROR_REASON,
errorCode: ERROR_CODE,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.INVALID_CONTEXT,
});
}),
} as unknown as Provider;
Expand Down Expand Up @@ -717,8 +746,8 @@ describe('Hooks', () => {
],
resolveBooleanEvaluation: jest.fn((): Promise<ResolutionDetails<boolean>> => {
return Promise.reject({
reason: ERROR_REASON,
errorCode: ERROR_CODE,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.PROVIDER_NOT_READY,
});
}),
} as unknown as Provider;
Expand Down
54 changes: 45 additions & 9 deletions packages/shared/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,45 @@
export * from './general-error';
export * from './flag-not-found-error';
export * from './parse-error';
export * from './type-mismatch-error';
export * from './targeting-key-missing-error';
export * from './invalid-context-error';
export * from './open-feature-error-abstract';
export * from './provider-not-ready-error';
export * from './provider-fatal-error';
import { ErrorCode } from '../evaluation';

import { FlagNotFoundError } from './flag-not-found-error';
import { GeneralError } from './general-error';
import { InvalidContextError } from './invalid-context-error';
import { OpenFeatureError } from './open-feature-error-abstract';
import { ParseError } from './parse-error';
import { ProviderFatalError } 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,
};

0 comments on commit c6d0b5d

Please sign in to comment.