Skip to content

Commit ef4b888

Browse files
authored
feat: precomputed assignment methods for supported types (#135)
* Add methods for boolean, integer, numeric, and json types * Add tests for typed assignments * Remove eslint warnings
1 parent 19b0e1e commit ef4b888

File tree

2 files changed

+164
-14
lines changed

2 files changed

+164
-14
lines changed

src/client/eppo-precomputed-client.spec.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,46 @@ describe('EppoPrecomputedClient E2E test', () => {
2222
name: 'Test',
2323
},
2424
flags: {
25-
'new-user-onboarding': {
25+
'string-flag': {
2626
allocationKey: 'allocation-123',
2727
variationKey: 'variation-123',
2828
variationType: 'STRING',
2929
variationValue: 'red',
3030
extraLogging: {},
3131
doLog: true,
3232
},
33+
'boolean-flag': {
34+
allocationKey: 'allocation-124',
35+
variationKey: 'variation-124',
36+
variationType: 'BOOLEAN',
37+
variationValue: true,
38+
extraLogging: {},
39+
doLog: true,
40+
},
41+
'integer-flag': {
42+
allocationKey: 'allocation-125',
43+
variationKey: 'variation-125',
44+
variationType: 'INTEGER',
45+
variationValue: 42,
46+
extraLogging: {},
47+
doLog: true,
48+
},
49+
'numeric-flag': {
50+
allocationKey: 'allocation-126',
51+
variationKey: 'variation-126',
52+
variationType: 'NUMERIC',
53+
variationValue: 3.14,
54+
extraLogging: {},
55+
doLog: true,
56+
},
57+
'json-flag': {
58+
allocationKey: 'allocation-127',
59+
variationKey: 'variation-127',
60+
variationType: 'JSON',
61+
variationValue: '{"key": "value", "number": 123}',
62+
extraLogging: {},
63+
doLog: true,
64+
},
3365
},
3466
}; // TODO: readMockPrecomputedFlagsResponse(MOCK_PRECOMPUTED_FLAGS_RESPONSE_FILE);
3567

@@ -337,7 +369,7 @@ describe('EppoPrecomputedClient E2E test', () => {
337369
let precomputedFlagStore: IConfigurationStore<PrecomputedFlag>;
338370
let requestParameters: PrecomputedFlagsRequestParameters;
339371

340-
const precomputedFlagKey = 'new-user-onboarding';
372+
const precomputedFlagKey = 'string-flag';
341373
const red = 'red';
342374

343375
const maxRetryDelay = DEFAULT_POLL_INTERVAL_MS * POLL_JITTER_PCT;
@@ -467,6 +499,50 @@ describe('EppoPrecomputedClient E2E test', () => {
467499
expect(variation).toBe('default');
468500
});
469501

502+
describe('Gets typed assignments', () => {
503+
let client: EppoPrecomputedClient;
504+
505+
beforeEach(async () => {
506+
client = new EppoPrecomputedClient(storage, requestParameters);
507+
await client.fetchPrecomputedFlags();
508+
});
509+
510+
it('returns string assignment', () => {
511+
expect(client.getStringAssignment('string-flag', 'default')).toBe('red');
512+
expect(client.getStringAssignment('non-existent', 'default')).toBe('default');
513+
});
514+
515+
it('returns boolean assignment', () => {
516+
expect(client.getBooleanAssignment('boolean-flag', false)).toBe(true);
517+
expect(client.getBooleanAssignment('non-existent', false)).toBe(false);
518+
});
519+
520+
it('returns integer assignment', () => {
521+
expect(client.getIntegerAssignment('integer-flag', 0)).toBe(42);
522+
expect(client.getIntegerAssignment('non-existent', 0)).toBe(0);
523+
});
524+
525+
it('returns numeric assignment', () => {
526+
expect(client.getNumericAssignment('numeric-flag', 0)).toBe(3.14);
527+
expect(client.getNumericAssignment('non-existent', 0)).toBe(0);
528+
});
529+
530+
it('returns JSON assignment', () => {
531+
expect(client.getJSONAssignment('json-flag', {})).toEqual({
532+
key: 'value',
533+
number: 123,
534+
});
535+
expect(client.getJSONAssignment('non-existent', {})).toEqual({});
536+
});
537+
538+
it('returns default value when type mismatches', () => {
539+
// Try to get a string value from a boolean flag
540+
expect(client.getStringAssignment('boolean-flag', 'default')).toBe('default');
541+
// Try to get a boolean value from a string flag
542+
expect(client.getBooleanAssignment('string-flag', false)).toBe(false);
543+
});
544+
});
545+
470546
it.each([
471547
{ pollAfterSuccessfulInitialization: false },
472548
{ pollAfterSuccessfulInitialization: true },

src/client/eppo-precomputed-client.ts

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { decodePrecomputedFlag } from '../decoding';
1818
import { FlagEvaluationWithoutDetails } from '../evaluator';
1919
import FetchHttpClient from '../http-client';
20-
import { FormatEnum, PrecomputedFlag } from '../interfaces';
20+
import { PrecomputedFlag, VariationType } from '../interfaces';
2121
import { getMD5Hash } from '../obfuscation';
2222
import initPoller, { IPoller } from '../poller';
2323
import PrecomputedRequestor from '../precomputed-requestor';
@@ -130,16 +130,12 @@ export default class EppoPrecomputedClient {
130130
}
131131
}
132132

133-
/**
134-
* Maps a subject to a string variation for a given experiment.
135-
*
136-
* @param flagKey feature flag identifier
137-
* @param defaultValue default value to return if the subject is not part of the experiment sample
138-
* The subject attributes are used for evaluating any targeting rules tied to the experiment.
139-
* @returns a variation value if a flag was precomputed for the subject, otherwise the default value
140-
* @public
141-
*/
142-
public getStringAssignment(flagKey: string, defaultValue: string): string {
133+
private getPrecomputedAssignment<T>(
134+
flagKey: string,
135+
defaultValue: T,
136+
expectedType: VariationType,
137+
valueTransformer: (value: unknown) => T = (v) => v as T,
138+
): T {
143139
validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
144140

145141
const preComputedFlag = this.getPrecomputedFlag(flagKey);
@@ -149,6 +145,14 @@ export default class EppoPrecomputedClient {
149145
return defaultValue;
150146
}
151147

148+
// Check variation type
149+
if (preComputedFlag.variationType !== expectedType) {
150+
logger.error(
151+
`[Eppo SDK] Type mismatch: expected ${expectedType} but flag ${flagKey} has type ${preComputedFlag.variationType}`,
152+
);
153+
return defaultValue;
154+
}
155+
152156
const result: FlagEvaluationWithoutDetails = {
153157
flagKey,
154158
format: this.precomputedFlagStore.getFormat() ?? '',
@@ -170,7 +174,77 @@ export default class EppoPrecomputedClient {
170174
} catch (error) {
171175
logger.error(`[Eppo SDK] Error logging assignment event: ${error}`);
172176
}
173-
return (result.variation?.value as string) ?? defaultValue;
177+
178+
try {
179+
return result.variation?.value !== undefined
180+
? valueTransformer(result.variation.value)
181+
: defaultValue;
182+
} catch (error) {
183+
logger.error(`[Eppo SDK] Error transforming value: ${error}`);
184+
return defaultValue;
185+
}
186+
}
187+
188+
/**
189+
* Maps a subject to a string variation for a given experiment.
190+
*
191+
* @param flagKey feature flag identifier
192+
* @param defaultValue default value to return if the subject is not part of the experiment sample
193+
* @returns a variation value if a flag was precomputed for the subject, otherwise the default value
194+
* @public
195+
*/
196+
public getStringAssignment(flagKey: string, defaultValue: string): string {
197+
return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.STRING);
198+
}
199+
200+
/**
201+
* Maps a subject to a boolean variation for a given experiment.
202+
*
203+
* @param flagKey feature flag identifier
204+
* @param defaultValue default value to return if the subject is not part of the experiment sample
205+
* @returns a variation value if a flag was precomputed for the subject, otherwise the default value
206+
* @public
207+
*/
208+
public getBooleanAssignment(flagKey: string, defaultValue: boolean): boolean {
209+
return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.BOOLEAN);
210+
}
211+
212+
/**
213+
* Maps a subject to an integer variation for a given experiment.
214+
*
215+
* @param flagKey feature flag identifier
216+
* @param defaultValue default value to return if the subject is not part of the experiment sample
217+
* @returns a variation value if a flag was precomputed for the subject, otherwise the default value
218+
* @public
219+
*/
220+
public getIntegerAssignment(flagKey: string, defaultValue: number): number {
221+
return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.INTEGER);
222+
}
223+
224+
/**
225+
* Maps a subject to a numeric (floating point) variation for a given experiment.
226+
*
227+
* @param flagKey feature flag identifier
228+
* @param defaultValue default value to return if the subject is not part of the experiment sample
229+
* @returns a variation value if a flag was precomputed for the subject, otherwise the default value
230+
* @public
231+
*/
232+
public getNumericAssignment(flagKey: string, defaultValue: number): number {
233+
return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.NUMERIC);
234+
}
235+
236+
/**
237+
* Maps a subject to a JSON object variation for a given experiment.
238+
*
239+
* @param flagKey feature flag identifier
240+
* @param defaultValue default value to return if the subject is not part of the experiment sample
241+
* @returns a parsed JSON object if a flag was precomputed for the subject, otherwise the default value
242+
* @public
243+
*/
244+
public getJSONAssignment(flagKey: string, defaultValue: object): object {
245+
return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.JSON, (value) =>
246+
typeof value === 'string' ? JSON.parse(value) : defaultValue,
247+
);
174248
}
175249

176250
private getPrecomputedFlag(flagKey: string): PrecomputedFlag | null {

0 commit comments

Comments
 (0)