Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
516 changes: 414 additions & 102 deletions packages/core/src/flags/FlagsClient.ts

Large diffs are not rendered by default.

641 changes: 624 additions & 17 deletions packages/core/src/flags/__tests__/FlagsClient.test.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import type {
import { configurationFromString } from '../../wire';

// A complete protobuf response from the flagging-core 3.0.0 wire contract.
// It contains one static boolean flag with the key `browser-flag`.
// It contains one static boolean flag with the key `dynamic-flag`.
export const RULES_RESPONSE =
'EgRwcm9kGigKDGJyb3dzZXItZmxhZxIYEAQaAigBIhAKCmFsbG9jYXRpb24iAiADKgJvbg==';
'EgRwcm9kGigKDGR5bmFtaWMtZmxhZxIYEAQaAigBIhAKCmFsbG9jYXRpb24iAiADKgJvbg==';

export const buildRulesWire = (response: string = RULES_RESPONSE): string =>
JSON.stringify({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('rules configuration', () => {
const result = flaggingCoreRulesEngine.evaluate({
configuration: buildRulesConfiguration(),
type: 'boolean',
flagKey: 'browser-flag',
flagKey: 'dynamic-flag',
defaultValue: false,
context: { targetingKey: 'user-1' },
logger: getNoopRulesLogger()
Expand All @@ -92,13 +92,13 @@ describe('rules configuration', () => {

it('preserves a deterministic flag-scoped PARSE_ERROR', () => {
const configuration = buildRulesConfiguration();
configuration.flags['browser-flag'].minimumFeatureLevel = 1;
configuration.flags['dynamic-flag'].minimumFeatureLevel = 1;

expect(
flaggingCoreRulesEngine.evaluate({
configuration,
type: 'boolean',
flagKey: 'browser-flag',
flagKey: 'dynamic-flag',
defaultValue: false,
context: { targetingKey: 'user-1' },
logger: getNoopRulesLogger()
Expand Down Expand Up @@ -144,7 +144,7 @@ describe('rules configuration', () => {
fake.evaluate({
configuration: buildRulesConfiguration(),
type: 'boolean',
flagKey: 'browser-flag',
flagKey: 'dynamic-flag',
defaultValue: false,
context: { targetingKey: 'user-1' },
logger: getNoopRulesLogger()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { configurationFromString, configurationToString } from '../wire';
import { buildRulesWire, RULES_RESPONSE } from './__utils__/rulesTestUtils';

const RULES_RESPONSE_WITH_UNKNOWN_FIELD =
'EgRwcm9kGigKDGJyb3dzZXItZmxhZxIYEAQaAigBIhAKCmFsbG9jYXRpb24iAiADKgJvbqAGBw==';
'EgRwcm9kGigKDGR5bmFtaWMtZmxhZxIYEAQaAigBIhAKCmFsbG9jYXRpb24iAiADKgJvbqAGBw==';

const buildResponse = () => ({
data: {
Expand Down Expand Up @@ -174,6 +174,6 @@ describe('configurationToString', () => {
);

expect(parsed.precomputed).toBeDefined();
expect(parsed.rules?.response.flags['browser-flag']).toBeDefined();
expect(parsed.rules?.response.flags['dynamic-flag']).toBeDefined();
});
});
4 changes: 2 additions & 2 deletions packages/core/src/flags/configuration/precomputed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const toFlagCacheEntry = (
allocationKey,
variationKey,
variationType,
variationValue: stringifyValue(variationValue),
variationValue: stringifyFlagValue(variationValue),
reason,
doLog,
// The native bridge still requires this deprecated field. Portable
Expand Down Expand Up @@ -199,7 +199,7 @@ const valueMatchesVariationType = (
* Objects/arrays are JSON-encoded; everything else uses `String(...)`, which yields
* lowercase `"true"/"false"` for booleans.
*/
const stringifyValue = (value: unknown): string => {
export const stringifyFlagValue = (value: unknown): string => {
if (value === null) {
return 'null';
}
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/flags/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SdkVerbosity } from '../config/types/SdkVerbosity';

import type { EvaluationContext, PrimitiveValue } from './types';

export interface FlagCacheEntry {
export interface TrackableAssignment {
key: string;
value: unknown;
allocationKey: string;
Expand All @@ -21,6 +21,8 @@ export interface FlagCacheEntry {
extraLogging: Record<string, unknown>;
}

export type FlagCacheEntry = TrackableAssignment;

export const processEvaluationContext = (
context: EvaluationContext
): EvaluationContext => {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/flags/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export interface EvaluationContext {
type FlagErrorCode =
| 'PROVIDER_NOT_READY'
| 'FLAG_NOT_FOUND'
| 'TARGETING_KEY_MISSING'
| 'PARSE_ERROR'
| 'TYPE_MISMATCH'
| 'INVALID_CONTEXT'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ErrorCode,
GeneralError,
InvalidContextError,
ParseError,
ProviderEvents,
ProviderNotReadyError
} from '@openfeature/web-sdk';
Expand All @@ -17,6 +18,7 @@ import { DatadogOfflineOpenFeatureProvider } from '../offlineProvider';
const READY = { status: 'ready' as const };
const mismatch = { status: 'error' as const, errorCode: 'INVALID_CONTEXT' };
const notReady = { status: 'error' as const, errorCode: 'PROVIDER_NOT_READY' };
const parseError = { status: 'error' as const, errorCode: 'PARSE_ERROR' };
const generalError = { status: 'error' as const, errorCode: 'GENERAL' };

const mockFlagsClient = {
Expand Down Expand Up @@ -106,6 +108,15 @@ describe('DatadogOfflineOpenFeatureProvider', () => {
);
});

it('rejects initialize with a parse error for an unusable configuration', async () => {
const provider = new DatadogOfflineOpenFeatureProvider();
mockFlagsClient.setEvaluationContextWithoutFetching.mockReturnValueOnce(
parseError
);

await expect(provider.initialize({})).rejects.toThrow(ParseError);
});

it('reconciles a matching context change without fetching or signalling a change', () => {
const provider = new DatadogOfflineOpenFeatureProvider();
const emitSpy = jest.spyOn(provider.events, 'emit');
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native-openfeature/src/offlineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ErrorCode,
GeneralError,
InvalidContextError,
ParseError,
ProviderEvents,
ProviderNotReadyError
} from '@openfeature/web-sdk';
Expand Down Expand Up @@ -40,6 +41,7 @@ type ProviderErrorEvent = { message: string; errorCode: ErrorCode };
const OF_ERROR_CODE: Record<ConfigurationErrorCode, ErrorCode> = {
INVALID_CONTEXT: ErrorCode.INVALID_CONTEXT,
PROVIDER_NOT_READY: ErrorCode.PROVIDER_NOT_READY,
PARSE_ERROR: ErrorCode.PARSE_ERROR,
GENERAL: ErrorCode.GENERAL
};

Expand Down Expand Up @@ -177,6 +179,10 @@ export class DatadogOfflineOpenFeatureProvider extends DatadogCoreOpenFeaturePro
return new ProviderNotReadyError(
'The Datadog offline provider has no configuration loaded. Provide one via setConfiguration.'
);
case 'PARSE_ERROR':
return new ParseError(
'The Datadog offline provider cannot parse the loaded configuration.'
);
default:
return new GeneralError(
'The Datadog offline provider cannot serve the loaded configuration.'
Expand Down