Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
80 changes: 78 additions & 2 deletions src/client/eppo-precomputed-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,46 @@ describe('EppoPrecomputedClient E2E test', () => {
name: 'Test',
},
flags: {
'new-user-onboarding': {
'string-flag': {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice - thanks for putting this together. I will adjust the API to match ✔️

allocationKey: 'allocation-123',
variationKey: 'variation-123',
variationType: 'STRING',
variationValue: 'red',
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);

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

const precomputedFlagKey = 'new-user-onboarding';
const precomputedFlagKey = 'string-flag';
const red = 'red';

const maxRetryDelay = DEFAULT_POLL_INTERVAL_MS * POLL_JITTER_PCT;
Expand Down Expand Up @@ -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 },
Expand Down
100 changes: 88 additions & 12 deletions src/client/eppo-precomputed-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { decodePrecomputedFlag } from '../decoding';
import { FlagEvaluationWithoutDetails } from '../evaluator';
import FetchHttpClient from '../http-client';
import { PrecomputedFlag } from '../interfaces';
import { PrecomputedFlag, VariationType } from '../interfaces';
import { getMD5Hash } from '../obfuscation';
import initPoller, { IPoller } from '../poller';
import PrecomputedRequestor from '../precomputed-requestor';
Expand Down Expand Up @@ -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<T>(
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);
Expand All @@ -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,
subjectKey: this.precomputedFlagsRequestParameters?.precompute.subjectKey ?? '',
Expand All @@ -169,7 +173,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 {
Expand Down Expand Up @@ -225,6 +299,7 @@ export default class EppoPrecomputedClient {
eventsToFlush.forEach((event) => {
try {
logFunction(event);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
logger.error(`[Eppo SDK] Error flushing event to logger: ${error.message}`);
}
Expand Down Expand Up @@ -272,6 +347,7 @@ export default class EppoPrecomputedClient {
allocationKey: allocationKey ?? '__eppo_no_allocation',
variationKey: variation?.key ?? '__eppo_no_variation',
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
logger.error(`[Eppo SDK] Error logging assignment event: ${error.message}`);
}
Expand Down
Loading