diff --git a/src/client/eppo-precomputed-client.spec.ts b/src/client/eppo-precomputed-client.spec.ts index 9c6d886..05e6922 100644 --- a/src/client/eppo-precomputed-client.spec.ts +++ b/src/client/eppo-precomputed-client.spec.ts @@ -22,7 +22,7 @@ describe('EppoPrecomputedClient E2E test', () => { name: 'Test', }, flags: { - 'new-user-onboarding': { + 'string-flag': { allocationKey: 'allocation-123', variationKey: 'variation-123', variationType: 'STRING', @@ -30,6 +30,38 @@ describe('EppoPrecomputedClient E2E test', () => { extraLogging: {}, doLog: true, }, + 'boolean-flag': { + allocationKey: 'allocation-124', + variationKey: 'variation-124', + variationType: 'BOOLEAN', + variationValue: true, + extraLogging: {}, + doLog: true, + }, + 'integer-flag': { + allocationKey: 'allocation-125', + variationKey: 'variation-125', + variationType: 'INTEGER', + variationValue: 42, + extraLogging: {}, + doLog: true, + }, + 'numeric-flag': { + allocationKey: 'allocation-126', + variationKey: 'variation-126', + variationType: 'NUMERIC', + variationValue: 3.14, + extraLogging: {}, + doLog: true, + }, + 'json-flag': { + allocationKey: 'allocation-127', + variationKey: 'variation-127', + variationType: 'JSON', + variationValue: '{"key": "value", "number": 123}', + extraLogging: {}, + doLog: true, + }, }, }; // TODO: readMockPrecomputedFlagsResponse(MOCK_PRECOMPUTED_FLAGS_RESPONSE_FILE); @@ -337,7 +369,7 @@ describe('EppoPrecomputedClient E2E test', () => { let precomputedFlagStore: IConfigurationStore; let requestParameters: PrecomputedFlagsRequestParameters; - const precomputedFlagKey = 'new-user-onboarding'; + const precomputedFlagKey = 'string-flag'; const red = 'red'; const maxRetryDelay = DEFAULT_POLL_INTERVAL_MS * POLL_JITTER_PCT; @@ -467,6 +499,50 @@ describe('EppoPrecomputedClient E2E test', () => { expect(variation).toBe('default'); }); + describe('Gets typed assignments', () => { + let client: EppoPrecomputedClient; + + beforeEach(async () => { + client = new EppoPrecomputedClient(storage, requestParameters); + await client.fetchPrecomputedFlags(); + }); + + it('returns string assignment', () => { + expect(client.getStringAssignment('string-flag', 'default')).toBe('red'); + expect(client.getStringAssignment('non-existent', 'default')).toBe('default'); + }); + + it('returns boolean assignment', () => { + expect(client.getBooleanAssignment('boolean-flag', false)).toBe(true); + expect(client.getBooleanAssignment('non-existent', false)).toBe(false); + }); + + it('returns integer assignment', () => { + expect(client.getIntegerAssignment('integer-flag', 0)).toBe(42); + expect(client.getIntegerAssignment('non-existent', 0)).toBe(0); + }); + + it('returns numeric assignment', () => { + expect(client.getNumericAssignment('numeric-flag', 0)).toBe(3.14); + expect(client.getNumericAssignment('non-existent', 0)).toBe(0); + }); + + it('returns JSON assignment', () => { + expect(client.getJSONAssignment('json-flag', {})).toEqual({ + key: 'value', + number: 123, + }); + expect(client.getJSONAssignment('non-existent', {})).toEqual({}); + }); + + it('returns default value when type mismatches', () => { + // Try to get a string value from a boolean flag + expect(client.getStringAssignment('boolean-flag', 'default')).toBe('default'); + // Try to get a boolean value from a string flag + expect(client.getBooleanAssignment('string-flag', false)).toBe(false); + }); + }); + it.each([ { pollAfterSuccessfulInitialization: false }, { pollAfterSuccessfulInitialization: true }, diff --git a/src/client/eppo-precomputed-client.ts b/src/client/eppo-precomputed-client.ts index 90a7b94..88a2d13 100644 --- a/src/client/eppo-precomputed-client.ts +++ b/src/client/eppo-precomputed-client.ts @@ -17,7 +17,7 @@ import { import { decodePrecomputedFlag } from '../decoding'; import { FlagEvaluationWithoutDetails } from '../evaluator'; import FetchHttpClient from '../http-client'; -import { FormatEnum, PrecomputedFlag } from '../interfaces'; +import { PrecomputedFlag, VariationType } from '../interfaces'; import { getMD5Hash } from '../obfuscation'; import initPoller, { IPoller } from '../poller'; import PrecomputedRequestor from '../precomputed-requestor'; @@ -130,16 +130,12 @@ export default class EppoPrecomputedClient { } } - /** - * Maps a subject to a string variation for a given experiment. - * - * @param flagKey feature flag identifier - * @param defaultValue default value to return if the subject is not part of the experiment sample - * The subject attributes are used for evaluating any targeting rules tied to the experiment. - * @returns a variation value if a flag was precomputed for the subject, otherwise the default value - * @public - */ - public getStringAssignment(flagKey: string, defaultValue: string): string { + private getPrecomputedAssignment( + flagKey: string, + defaultValue: T, + expectedType: VariationType, + valueTransformer: (value: unknown) => T = (v) => v as T, + ): T { validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank'); const preComputedFlag = this.getPrecomputedFlag(flagKey); @@ -149,6 +145,14 @@ export default class EppoPrecomputedClient { return defaultValue; } + // Check variation type + if (preComputedFlag.variationType !== expectedType) { + logger.error( + `[Eppo SDK] Type mismatch: expected ${expectedType} but flag ${flagKey} has type ${preComputedFlag.variationType}`, + ); + return defaultValue; + } + const result: FlagEvaluationWithoutDetails = { flagKey, format: this.precomputedFlagStore.getFormat() ?? '', @@ -170,7 +174,77 @@ export default class EppoPrecomputedClient { } catch (error) { logger.error(`[Eppo SDK] Error logging assignment event: ${error}`); } - return (result.variation?.value as string) ?? defaultValue; + + try { + return result.variation?.value !== undefined + ? valueTransformer(result.variation.value) + : defaultValue; + } catch (error) { + logger.error(`[Eppo SDK] Error transforming value: ${error}`); + return defaultValue; + } + } + + /** + * Maps a subject to a string variation for a given experiment. + * + * @param flagKey feature flag identifier + * @param defaultValue default value to return if the subject is not part of the experiment sample + * @returns a variation value if a flag was precomputed for the subject, otherwise the default value + * @public + */ + public getStringAssignment(flagKey: string, defaultValue: string): string { + return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.STRING); + } + + /** + * Maps a subject to a boolean variation for a given experiment. + * + * @param flagKey feature flag identifier + * @param defaultValue default value to return if the subject is not part of the experiment sample + * @returns a variation value if a flag was precomputed for the subject, otherwise the default value + * @public + */ + public getBooleanAssignment(flagKey: string, defaultValue: boolean): boolean { + return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.BOOLEAN); + } + + /** + * Maps a subject to an integer variation for a given experiment. + * + * @param flagKey feature flag identifier + * @param defaultValue default value to return if the subject is not part of the experiment sample + * @returns a variation value if a flag was precomputed for the subject, otherwise the default value + * @public + */ + public getIntegerAssignment(flagKey: string, defaultValue: number): number { + return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.INTEGER); + } + + /** + * Maps a subject to a numeric (floating point) variation for a given experiment. + * + * @param flagKey feature flag identifier + * @param defaultValue default value to return if the subject is not part of the experiment sample + * @returns a variation value if a flag was precomputed for the subject, otherwise the default value + * @public + */ + public getNumericAssignment(flagKey: string, defaultValue: number): number { + return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.NUMERIC); + } + + /** + * Maps a subject to a JSON object variation for a given experiment. + * + * @param flagKey feature flag identifier + * @param defaultValue default value to return if the subject is not part of the experiment sample + * @returns a parsed JSON object if a flag was precomputed for the subject, otherwise the default value + * @public + */ + public getJSONAssignment(flagKey: string, defaultValue: object): object { + return this.getPrecomputedAssignment(flagKey, defaultValue, VariationType.JSON, (value) => + typeof value === 'string' ? JSON.parse(value) : defaultValue, + ); } private getPrecomputedFlag(flagKey: string): PrecomputedFlag | null {