Skip to content

Commit 1aba8d3

Browse files
Merge pull request #6 from splitio/fme-9857
[FME-9857] Add evaluation with details
2 parents d1fa066 + 647beef commit 1aba8d3

File tree

5 files changed

+80
-92
lines changed

5 files changed

+80
-92
lines changed

src/__tests__/integration/e2e.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ describe('OpenFeature Split Provider - E2E Integration Tests', () => {
103103
expect(details.flagKey).toBe('my_feature');
104104
expect(details.reason).toBe('TARGETING_MATCH');
105105
expect(details.variant).toBe('on');
106+
expect(details.flagMetadata.config).toBe('{"desc": "this is a test"}')
106107
});
107108
});
108109

@@ -162,6 +163,7 @@ describe('OpenFeature Split Provider - E2E Integration Tests', () => {
162163
expect(details.flagKey).toBe('my_feature');
163164
expect(details.variant).toBe('on');
164165
expect(details.reason).toBe('TARGETING_MATCH');
166+
expect(details.flagMetadata.config).toBe('{"desc": "this is a test"}')
165167
});
166168
});
167169
});

src/__tests__/integration/mock.test.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ describe('OpenFeature Split Provider - Mock Integration Tests', () => {
2323
__getStatus: () => ({isReady: true}),
2424

2525
// Mock treatment evaluation methods
26-
getTreatment: jest.fn((splitName) => {
27-
if (splitName === 'my_feature') return 'on';
28-
if (splitName === 'some_other_feature') return 'off';
29-
if (splitName === 'int_feature') return '32';
30-
if (splitName === 'obj_feature') return '{"key": "value"}';
31-
return 'control';
26+
getTreatmentWithConfig: jest.fn((splitName) => {
27+
if (splitName === 'my_feature') return { treatment: 'on', config: '{"desc": "this is a test"}' };
28+
if (splitName === 'some_other_feature') return { treatment: 'off' };
29+
if (splitName === 'int_feature') return { treatment: '32' };
30+
if (splitName === 'obj_feature') return { treatment: '{"key": "value"}' };
31+
return { treatment: 'control' };
3232
}),
3333

3434
// Mock for cleanup
@@ -53,7 +53,7 @@ describe('OpenFeature Split Provider - Mock Integration Tests', () => {
5353
test('boolean evaluation should work', async () => {
5454
const result = await client.getBooleanValue('my_feature', false);
5555
expect(result).toBe(true);
56-
expect(mockSplitClient.getTreatment).toHaveBeenCalledWith('my_feature', {});
56+
expect(mockSplitClient.getTreatmentWithConfig).toHaveBeenCalledWith('my_feature', {});
5757
});
5858

5959
test('boolean evaluation should handle off value', async () => {
@@ -81,6 +81,7 @@ describe('OpenFeature Split Provider - Mock Integration Tests', () => {
8181
expect(details.value).toBe(true);
8282
expect(details.variant).toBe('on');
8383
expect(details.flagKey).toBe('my_feature');
84+
expect(details.flagMetadata.config).toBe('{"desc": "this is a test"}')
8485
expect(details.reason).toBe('TARGETING_MATCH');
8586
});
8687

src/__tests__/integration/working.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ describe('OpenFeature Split Provider - Working Integration Test', () => {
4848
expect(result).toBe(true);
4949
});
5050

51+
test('boolean treatment details evaluations', async () => {
52+
53+
// Test the boolean value evaluation
54+
const result = await client.getBooleanDetails('my_feature', false);
55+
expect(result.value).toBe(true);
56+
expect(result.flagMetadata.config).toBe('{"desc": "this is a test"}')
57+
});
58+
5159
// Add a test for string treatment
5260
test('string treatment evaluations', async () => {
5361
const result = await client.getStringValue('some_other_feature', 'default');

src/__tests__/provider-unit.test.js

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ describe('OpenFeatureSplitProvider Unit Tests', () => {
2424
Event: { SDK_READY: 'SDK_READY' },
2525

2626
// Mock the treatments
27-
getTreatment: jest.fn((flagKey, _attributes) => {
27+
getTreatmentWithConfig: jest.fn((flagKey, _attributes) => {
2828
// Return specific values for our test cases
29-
if (flagKey === 'boolean-flag') return 'on';
30-
if (flagKey === 'boolean-flag-off') return 'off';
31-
if (flagKey === 'string-flag') return 'a-string-treatment';
32-
if (flagKey === 'number-flag') return '42';
33-
if (flagKey === 'object-flag') return '{"key":"value","nested":{"inner":"data"}}';
34-
if (flagKey === 'non-existent') return 'control';
35-
return 'control';
29+
if (flagKey === 'boolean-flag') return { treatment: 'on', config: '{"desc": "this is a test"}' };
30+
if (flagKey === 'boolean-flag-off') return { treatment: 'off', config: {} };
31+
if (flagKey === 'string-flag') return { treatment: 'a-string-treatment', config: {} };
32+
if (flagKey === 'number-flag') return { treatment: '42', config: {} };
33+
if (flagKey === 'object-flag') return { treatment: '{"key":"value","nested":{"inner":"data"}}', config: {} };
34+
if (flagKey === 'non-existent') return { treatment: 'control', config: {} };
35+
return { treatment: 'control', config: {} };
3636
}),
3737

3838
// Clean up
@@ -55,7 +55,8 @@ describe('OpenFeatureSplitProvider Unit Tests', () => {
5555

5656
expect(result.value).toBe(true);
5757
expect(result.variant).toBe('on');
58-
expect(mockSplitClient.getTreatment).toHaveBeenCalledWith(
58+
expect(result.flagMetadata.config).toBe('{"desc": "this is a test"}')
59+
expect(mockSplitClient.getTreatmentWithConfig).toHaveBeenCalledWith(
5960
'boolean-flag',
6061
{}
6162
);
@@ -131,15 +132,4 @@ describe('OpenFeatureSplitProvider Unit Tests', () => {
131132
}).toThrow(/targeting key/i);
132133
});
133134

134-
test('should include metadata in evaluation result', () => {
135-
const result = provider.resolveStringEvaluation(
136-
'string-flag',
137-
'default', // default value
138-
{ targetingKey: 'user-key' },
139-
console // logger
140-
);
141-
142-
expect(result.flagKey).toBe('string-flag');
143-
expect(result.reason).toBe('TARGETING_MATCH');
144-
});
145135
});

src/lib/js-split-provider.ts

Lines changed: 52 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
StandardResolutionReasons,
1212
Logger,
1313
ProviderEvents,
14-
OpenFeature,
1514
OpenFeatureEventEmitter,
1615
} from "@openfeature/web-sdk";
1716
import type SplitIO from "@splitsoftware/splitio/types/splitio";
@@ -22,6 +21,7 @@ type Consumer = {
2221
};
2322

2423
const CONTROL_VALUE_ERROR_MESSAGE = "Received the 'control' value from Split.";
24+
const CONTROL_TREATMENT = 'control';
2525

2626
export class OpenFeatureSplitProvider implements Provider {
2727
metadata = {
@@ -41,130 +41,117 @@ export class OpenFeatureSplitProvider implements Provider {
4141
this.events.emit(ProviderEvents.Ready)
4242
};
4343

44-
// If client is ready, resolve immediately
45-
if (this.isClientReady()) {
44+
const onSdkTimedOut = () => {
45+
console.log(`${this.metadata.name} provider couldn't initialize`);
46+
this.events.emit(ProviderEvents.Error);
47+
};
48+
49+
const clientStatus = (this.client as any).__getStatus();
50+
if (clientStatus.isReady) {
4651
onSdkReady();
47-
} else {
48-
this.client.on(this.client.Event.SDK_READY, onSdkReady);
52+
return;
53+
}
54+
if (clientStatus.hasTimedout || clientStatus.isTimedOut) {
55+
onSdkTimedOut();
56+
return;
4957
}
50-
}
51-
52-
// Safe method to check if client is ready
53-
private isClientReady(): boolean {
54-
return (this.client as any).__getStatus().isReady;
58+
this.client.on(this.client.Event.SDK_READY, onSdkReady);
59+
this.client.on(this.client.Event.SDK_READY_TIMED_OUT, onSdkTimedOut);
5560
}
5661

5762
resolveBooleanEvaluation(
5863
flagKey: string,
59-
defaultValue: boolean,
64+
_: boolean,
6065
context: EvaluationContext,
6166
_logger: Logger
6267
): ResolutionDetails<boolean> {
6368
const details = this.evaluateTreatment(
6469
flagKey,
6570
this.transformContext(context),
66-
defaultValue.toString()
6771
);
6872

69-
let value: boolean;
70-
switch (details.value as unknown) {
71-
case "on":
72-
case "true":
73-
case true:
74-
value = true;
75-
break;
76-
case "off":
77-
case "false":
78-
case false:
79-
value = false;
80-
break;
81-
case "control":
82-
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
83-
default:
84-
throw new ParseError(`Invalid boolean value for ${details.value}`);
73+
const treatment = details.value.toLowerCase();
74+
75+
if ( treatment === 'on' || treatment === 'true' ) {
76+
return { ...details, value: true };
8577
}
86-
return { ...details, value };
78+
79+
if ( treatment === 'off' || treatment === 'false' ) {
80+
return { ...details, value: false };
81+
}
82+
83+
throw new ParseError(`Invalid boolean value for ${treatment}`);
8784
}
8885

8986
resolveStringEvaluation(
9087
flagKey: string,
91-
defaultValue: string,
88+
_: string,
9289
context: EvaluationContext,
9390
_logger: Logger
9491
): ResolutionDetails<string> {
9592
const details = this.evaluateTreatment(
9693
flagKey,
9794
this.transformContext(context),
98-
defaultValue
9995
);
100-
if (details.value === "control") {
101-
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
102-
}
10396
return details;
10497
}
10598

10699
resolveNumberEvaluation(
107100
flagKey: string,
108-
defaultValue: number,
101+
_: number,
109102
context: EvaluationContext,
110103
_logger: Logger
111104
): ResolutionDetails<number> {
112105
const details = this.evaluateTreatment(
113106
flagKey,
114107
this.transformContext(context),
115-
defaultValue.toString()
116108
);
117109
return { ...details, value: this.parseValidNumber(details.value) };
118110
}
119111

120112
resolveObjectEvaluation<U extends JsonValue>(
121113
flagKey: string,
122-
defaultValue: U,
114+
_: U,
123115
context: EvaluationContext,
124116
_logger: Logger
125117
): ResolutionDetails<U> {
126118
const details = this.evaluateTreatment(
127119
flagKey,
128-
this.transformContext(context),
129-
JSON.stringify(defaultValue)
120+
this.transformContext(context)
130121
);
131122
return { ...details, value: this.parseValidJsonObject(details.value) };
132123
}
133124

134125
private evaluateTreatment(
135126
flagKey: string,
136-
consumer: Consumer,
137-
defaultValue: string
127+
consumer: Consumer
138128
): ResolutionDetails<string> {
139129
if (!consumer.key) {
140130
throw new TargetingKeyMissingError(
141-
"The Split provider requires a targeting key."
131+
'The Split provider requires a targeting key.'
142132
);
143-
} else {
144-
// The SDK should be ready by now, but if not, return default value
145-
// Use our isClientReady helper to safely check
146-
if (!this.isClientReady()) {
147-
return {
148-
value: defaultValue,
149-
variant: defaultValue,
150-
reason: StandardResolutionReasons.DEFAULT
151-
};
152-
}
153-
const value = this.client.getTreatment(
154-
flagKey,
155-
consumer.attributes
133+
}
134+
if (flagKey == null || flagKey === '') {
135+
throw new FlagNotFoundError(
136+
'flagKey must be a non-empty string'
156137
);
157-
// Create resolution details and add flagKey as additional property for tests
158-
const details: ResolutionDetails<string> = {
159-
value: value,
160-
variant: value,
161-
reason: StandardResolutionReasons.TARGETING_MATCH,
162-
};
163-
164-
// Add flagKey for OpenFeature v1 compatibility, using assertion to avoid TypeScript errors
165-
(details as any).flagKey = flagKey;
166-
return details;
167138
}
139+
const { treatment: value, config }: SplitIO.TreatmentWithConfig = this.client.getTreatmentWithConfig(
140+
flagKey,
141+
consumer.attributes
142+
);
143+
144+
if (value === CONTROL_TREATMENT) {
145+
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
146+
}
147+
const flagMetadata = { config: config ? config : '' };
148+
const details: ResolutionDetails<string> = {
149+
value: value,
150+
variant: value,
151+
flagMetadata: flagMetadata,
152+
reason: StandardResolutionReasons.TARGETING_MATCH,
153+
};
154+
return details;
168155
}
169156

170157
//Transform the context into an object useful for the Split API, an key string with arbitrary Split "Attributes".

0 commit comments

Comments
 (0)