Skip to content

Commit

Permalink
feat!: errorCode as enum, reason as string (open-feature#244)
Browse files Browse the repository at this point in the history
- makes errorCode an enum
- makes reason a string
- adds errorMessage to resolution/evaluation details
  • Loading branch information
toddbaert authored Sep 29, 2022
1 parent 1f6eb21 commit ce7c4ad
Show file tree
Hide file tree
Showing 19 changed files with 196 additions and 82 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![codecov](https://codecov.io/gh/open-feature/js-sdk/branch/main/graph/badge.svg?token=3DC5XOEHMY)](https://codecov.io/gh/open-feature/js-sdk)
[![npm version](https://badge.fury.io/js/@openfeature%2Fjs-sdk.svg)](https://badge.fury.io/js/@openfeature%2Fjs-sdk)
[![Known Vulnerabilities](https://snyk.io/test/github/open-feature/js-sdk/badge.svg)](https://snyk.io/test/github/open-feature/js-sdk)
[![Specification](https://img.shields.io/static/v1?label=Specification&message=v0.4.0&color=yellow)](https://github.com/open-feature/spec/tree/v0.4.0)
[![Specification](https://img.shields.io/static/v1?label=Specification&message=v0.5.0&color=yellow)](https://github.com/open-feature/spec/tree/v0.5.0)

This is the JavaScript implementation of [OpenFeature](https://openfeature.dev), a vendor-agnostic abstraction library for evaluating feature flags.

Expand Down
14 changes: 9 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ERROR_REASON, GENERAL_ERROR } from './constants';
import { OpenFeature } from './open-feature';
import { OpenFeatureError } from './errors';
import { SafeLogger } from './logger';
import { OpenFeature } from './open-feature';
import {
Client,
ClientMetadata,
ErrorCode,
EvaluationContext,
EvaluationDetails,
FlagEvaluationOptions,
Expand All @@ -15,6 +16,7 @@ import {
Logger,
Provider,
ResolutionDetails,
StandardResolutionReasons,
} from './types';

type OpenFeatureClientOptions = {
Expand Down Expand Up @@ -47,7 +49,7 @@ export class OpenFeatureClient implements Client {
this._clientLogger = new SafeLogger(logger);
return this;
}

setContext(context: EvaluationContext): OpenFeatureClient {
this._context = context;
return this;
Expand Down Expand Up @@ -223,14 +225,16 @@ export class OpenFeatureClient implements Client {

return evaluationDetails;
} catch (err: unknown) {
const errorCode = (!!err && (err as { code: string }).code) || GENERAL_ERROR;
const errorMessage: string = (err as Error)?.message;
const errorCode: ErrorCode = (err as OpenFeatureError)?.code || ErrorCode.GENERAL;

await this.errorHooks(allHooksReversed, hookContext, err, options);

return {
errorCode,
errorMessage,
value: defaultValue,
reason: ERROR_REASON,
reason: StandardResolutionReasons.ERROR,
flagKey,
};
} finally {
Expand Down
5 changes: 0 additions & 5 deletions src/constants.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/errors/codes.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/errors/flag-not-found-error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OpenFeatureError } from './error-abstract';
import { ErrorCode } from './codes';
import { ErrorCode } from '../types';
import { OpenFeatureError } from './open-feature-error-abstract';

export class FlagNotFoundError extends OpenFeatureError {
code: ErrorCode;
Expand Down
12 changes: 12 additions & 0 deletions src/errors/general-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ErrorCode } from '../types';
import { OpenFeatureError } from './open-feature-error-abstract';

export class GeneralError extends OpenFeatureError {
code: ErrorCode;
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, GeneralError.prototype);
this.name = 'GeneralError';
this.code = ErrorCode.GENERAL;
}
}
6 changes: 4 additions & 2 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from './codes';
export * from './general-error';
export * from './flag-not-found-error';
export * from './parse-error';
export * from './type-mismatch-error';
export * from './error-abstract';
export * from './targeting-key-missing-error';
export * from './invalid-context-error';
export * from './open-feature-error-abstract';
12 changes: 12 additions & 0 deletions src/errors/invalid-context-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ErrorCode } from '../types';
import { OpenFeatureError } from './open-feature-error-abstract';

export class InvalidContextError extends OpenFeatureError {
code: ErrorCode;
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, InvalidContextError.prototype);
this.name = 'InvalidContextError';
this.code = ErrorCode.INVALID_CONTEXT;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ErrorCode } from './codes';
import { ErrorCode } from '../types';

export abstract class OpenFeatureError extends Error {
abstract code: ErrorCode;
Expand Down
4 changes: 2 additions & 2 deletions src/errors/parse-error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OpenFeatureError } from './error-abstract';
import { ErrorCode } from './codes';
import { ErrorCode } from '../types';
import { OpenFeatureError } from './open-feature-error-abstract';

export class ParseError extends OpenFeatureError {
code: ErrorCode;
Expand Down
12 changes: 12 additions & 0 deletions src/errors/targeting-key-missing-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ErrorCode } from '../types';
import { OpenFeatureError } from './open-feature-error-abstract';

export class TargetingKeyMissingError extends OpenFeatureError {
code: ErrorCode;
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, TargetingKeyMissingError.prototype);
this.name = 'TargetingKeyMissingError';
this.code = ErrorCode.TARGETING_KEY_MISSING;
}
}
4 changes: 2 additions & 2 deletions src/errors/type-mismatch-error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OpenFeatureError } from './error-abstract';
import { ErrorCode } from './codes';
import { ErrorCode } from '../types';
import { OpenFeatureError } from './open-feature-error-abstract';

export class TypeMismatchError extends OpenFeatureError {
code: ErrorCode;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './open-feature';
export * from './types';
export * from './errors/index';
export * from './errors';
84 changes: 57 additions & 27 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,55 +272,85 @@ export interface Provider {
): Promise<ResolutionDetails<T>>;
}

export enum StandardResolutionReasons {
export const StandardResolutionReasons = {
/**
* Indicates that the feature flag is targeting
* 100% of the targeting audience,
* e.g. 100% rollout percentage
* The resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting.
*/
TARGETING_MATCH = 'TARGETING_MATCH',
TARGETING_MATCH: 'TARGETING_MATCH',

/**
* Indicates that the feature flag is targeting
* a subset of the targeting audience,
* e.g. less than 100% rollout percentage
* The resolved value was the result of pseudorandom assignment.
*/
SPLIT = 'SPLIT',
SPLIT: 'SPLIT',

/**
* Indicates that the feature flag is disabled
* The resolved value was the result of the flag being disabled in the management system.
*/
DISABLED = 'DISABLED',
DISABLED: 'DISABLED',

/**
* Indicates that the feature flag evaluated to the
* default value as passed in getBooleanValue/getBooleanValueDetails and
* similar functions in the Client
* The resolved value was configured statically, or otherwise fell back to a pre-configured value.
*/
DEFAULT = 'DEFAULT',
DEFAULT: 'DEFAULT',

/**
* The reason for the resolved value could not be determined.
*/
UNKNOWN: 'UNKNOWN',

/**
* Indicates that the feature flag evaluated to a
* static value, for example, the default value for the flag
* The resolved value was the result of an error.
*
* Note: Typically means that no dynamic evaluation has been
* executed for the feature flag
* Note: The `errorCode` and `errorMessage` fields may contain additional details of this error.
*/
STATIC = 'STATIC',
ERROR: 'ERROR',
} as const;

export enum ErrorCode {
/**
* Indicates an unknown issue occurred during evaluation
* The value was resolved before the provider was ready.
*/
UNKNOWN = 'UNKNOWN',
PROVIDER_NOT_READY = 'PROVIDER_NOT_READY',

/**
* Indicates that an error occurred during evaluation
*
* Note: The `errorCode`-field contains the details of this error
* The flag could not be found.
*/
FLAG_NOT_FOUND = 'FLAG_NOT_FOUND',

/**
* An error was encountered parsing data, such as a flag configuration.
*/
ERROR = 'ERROR',
PARSE_ERROR = 'PARSE_ERROR',

/**
* The type of the flag value does not match the expected type.
*/
TYPE_MISMATCH = 'TYPE_MISMATCH',

/**
* The provider requires a targeting key and one was not provided in the evaluation context.
*/
TARGETING_KEY_MISSING = 'TARGETING_KEY_MISSING',

/**
* The evaluation context does not meet provider requirements.
*/
INVALID_CONTEXT = 'INVALID_CONTEXT',

/**
* An error with an unspecified code.
*/
GENERAL = 'GENERAL',
}

export type ResolutionReason = keyof typeof StandardResolutionReasons | (string & Record<never, never>);

export type ResolutionDetails<U> = {
value: U;
variant?: string;
reason?: ResolutionReason;
errorCode?: string;
errorCode?: ErrorCode;
errorMessage?: string;
};

export type EvaluationDetails<T extends FlagValue> = {
Expand Down
64 changes: 48 additions & 16 deletions test/client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OpenFeatureClient } from '../src/client.js';
import { ERROR_REASON, GENERAL_ERROR } from '../src/constants.js';
import { OpenFeature } from '../src/open-feature.js';
import { Client, EvaluationContext, EvaluationDetails, JsonArray, JsonObject, JsonValue, Provider, ResolutionDetails } from '../src/types.js';
import { OpenFeatureClient } from '../src/client';
import { ErrorCode, FlagNotFoundError } from '../src';
import { OpenFeature } from '../src/open-feature';
import { Client, EvaluationContext, EvaluationDetails, JsonArray, JsonObject, JsonValue, Provider, ResolutionDetails, StandardResolutionReasons } from '../src/types';

const BOOLEAN_VALUE = true;
const STRING_VALUE = 'val';
Expand Down Expand Up @@ -321,42 +321,74 @@ describe('OpenFeatureClient', () => {
});

describe('Abnormal execution', () => {
let details: EvaluationDetails<number>;
const NON_OPEN_FEATURE_ERROR_MESSAGE = 'A null dereference or something, I dunno.';
const OPEN_FEATURE_ERROR_MESSAGE = 'This ain\'t the flag you\'re looking for.';
let nonOpenFeatureErrorDetails: EvaluationDetails<number>;
let openFeatureErrorDetails : EvaluationDetails<string>;
let client: Client;
const errorProvider = {
name: 'error-mock',

resolveNumberEvaluation: jest.fn((): Promise<ResolutionDetails<number>> => {
throw new Error('Fake error!');
throw new Error(NON_OPEN_FEATURE_ERROR_MESSAGE); // throw a non-open-feature error
}),
resolveStringEvaluation: jest.fn((): Promise<ResolutionDetails<string>> => {
throw new FlagNotFoundError(OPEN_FEATURE_ERROR_MESSAGE); // throw an open-feature error
}),
} as unknown as Provider;
const defaultValue = 123;
const defaultNumberValue = 123;
const defaultStringValue = 'hey!';

beforeAll(async () => {
OpenFeature.setProvider(errorProvider);
client = OpenFeature.getClient();
details = await client.getNumberDetails('some-flag', defaultValue);
nonOpenFeatureErrorDetails = await client.getNumberDetails('some-flag', defaultNumberValue);
openFeatureErrorDetails = await client.getStringDetails('some-flag', defaultStringValue);
});

describe('Requirement 1.4.7', () => {
it('error code hould contain error', () => {
expect(details.errorCode).toBeTruthy();
expect(details.errorCode).toEqual(GENERAL_ERROR);
describe('OpenFeatureError', () => {
it('should contain error code', () => {
expect(openFeatureErrorDetails.errorCode).toBeTruthy();
expect(openFeatureErrorDetails.errorCode).toEqual(ErrorCode.FLAG_NOT_FOUND); // should get code from thrown OpenFeatureError
});
});

describe('Non-OpenFeatureError', () => {
it('should contain error code', () => {
expect(nonOpenFeatureErrorDetails.errorCode).toBeTruthy();
expect(nonOpenFeatureErrorDetails.errorCode).toEqual(ErrorCode.GENERAL); // should fall back to GENERAL
});
});
});

describe('Requirement 1.4.8', () => {
it('should contain "error" reason', () => {
expect(details.reason).toEqual(ERROR_REASON);
it('should contain error reason', () => {
expect(nonOpenFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR);
expect(openFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR);
});
});

describe('Requirement 1.4.9', () => {
it('must not throw, must return default', async () => {
details = await client.getNumberDetails('some-flag', defaultValue);
nonOpenFeatureErrorDetails = await client.getNumberDetails('some-flag', defaultNumberValue);

expect(nonOpenFeatureErrorDetails).toBeTruthy();
expect(nonOpenFeatureErrorDetails.value).toEqual(defaultNumberValue);
});
});

expect(details).toBeTruthy();
expect(details.value).toEqual(defaultValue);
describe('Requirement 1.4.12', () => {
describe('OpenFeatureError', () => {
it('should contain "error" message', () => {
expect(openFeatureErrorDetails.errorMessage).toEqual(OPEN_FEATURE_ERROR_MESSAGE);
});
});

describe('Non-OpenFeatureError', () => {
it('should contain "error" message', () => {
expect(nonOpenFeatureErrorDetails.errorMessage).toEqual(NON_OPEN_FEATURE_ERROR_MESSAGE);
});
});
});
});
Expand Down
Loading

0 comments on commit ce7c4ad

Please sign in to comment.