From 5e5ea308b91cc781ec84a879af22752c8f98621b Mon Sep 17 00:00:00 2001 From: Greg Huels Date: Mon, 30 Sep 2024 14:20:09 -0500 Subject: [PATCH] CR changes --- .../eppo-client-experiment-container.spec.ts | 20 ++++----- src/client/eppo-client.ts | 43 +++++++++++++------ src/client/test-utils.ts | 2 +- src/index.ts | 2 + 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/client/eppo-client-experiment-container.spec.ts b/src/client/eppo-client-experiment-container.spec.ts index 71647ce..2f794f7 100644 --- a/src/client/eppo-client-experiment-container.spec.ts +++ b/src/client/eppo-client-experiment-container.spec.ts @@ -3,12 +3,12 @@ import * as applicationLogger from '../application-logger'; import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; import { Flag, ObfuscatedFlag } from '../interfaces'; -import EppoClient, { IFlagExperiment } from './eppo-client'; +import EppoClient, { IContainerExperiment } from './eppo-client'; import { initConfiguration } from './test-utils'; type Container = { name: string }; -describe('getExperimentContainer', () => { +describe('getExperimentContainerEntry', () => { global.fetch = jest.fn(() => { const ufc = readMockUFCResponse(MOCK_UFC_RESPONSE_FILE); return Promise.resolve({ @@ -24,7 +24,7 @@ describe('getExperimentContainer', () => { const treatment3Container: Container = { name: 'Treatment Variation 3 Container' }; let client: EppoClient; - let flagExperiment: IFlagExperiment; + let flagExperiment: IContainerExperiment; let getStringAssignmentSpy: jest.SpyInstance; let loggerWarnSpy: jest.SpyInstance; @@ -35,8 +35,8 @@ describe('getExperimentContainer', () => { client.setIsGracefulFailureMode(true); flagExperiment = { flagKey: 'my-key', - controlVariation: controlContainer, - treatmentVariations: [treatment1Container, treatment2Container, treatment3Container], + controlVariationEntry: controlContainer, + treatmentVariationEntries: [treatment1Container, treatment2Container, treatment3Container], }; getStringAssignmentSpy = jest.spyOn(client, 'getStringAssignment'); loggerWarnSpy = jest.spyOn(applicationLogger.logger, 'warn'); @@ -49,19 +49,19 @@ describe('getExperimentContainer', () => { it('should return the right container when a treatment variation is assigned', async () => { jest.spyOn(client, 'getStringAssignment').mockReturnValue('treatment-2'); - expect(client.getExperimentContainer(flagExperiment, 'subject-key', {})).toEqual( + expect(client.getExperimentContainerEntry(flagExperiment, 'subject-key', {})).toEqual( treatment2Container, ); jest.spyOn(client, 'getStringAssignment').mockReturnValue('treatment-3'); - expect(client.getExperimentContainer(flagExperiment, 'subject-key', {})).toEqual( + expect(client.getExperimentContainerEntry(flagExperiment, 'subject-key', {})).toEqual( treatment3Container, ); }); it('should return the right container when control is assigned', async () => { jest.spyOn(client, 'getStringAssignment').mockReturnValue('control'); - expect(client.getExperimentContainer(flagExperiment, 'subject-key', {})).toEqual( + expect(client.getExperimentContainerEntry(flagExperiment, 'subject-key', {})).toEqual( controlContainer, ); expect(loggerWarnSpy).not.toHaveBeenCalled(); @@ -69,7 +69,7 @@ describe('getExperimentContainer', () => { it('should default to the control container if an unknown variation is assigned', async () => { jest.spyOn(client, 'getStringAssignment').mockReturnValue('adsfsadfsadf'); - expect(client.getExperimentContainer(flagExperiment, 'subject-key', {})).toEqual( + expect(client.getExperimentContainerEntry(flagExperiment, 'subject-key', {})).toEqual( controlContainer, ); expect(loggerWarnSpy).toHaveBeenCalled(); @@ -77,7 +77,7 @@ describe('getExperimentContainer', () => { it('should default to the control container if an out-of-bounds treatment variation is assigned', async () => { jest.spyOn(client, 'getStringAssignment').mockReturnValue('treatment-9'); - expect(client.getExperimentContainer(flagExperiment, 'subject-key', {})).toEqual( + expect(client.getExperimentContainerEntry(flagExperiment, 'subject-key', {})).toEqual( controlContainer, ); expect(loggerWarnSpy).toHaveBeenCalled(); diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index a6ff332..623c3a1 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -68,10 +68,10 @@ export type FlagConfigurationRequestParameters = { skipInitialPoll?: boolean; }; -export interface IFlagExperiment { +export interface IContainerExperiment { flagKey: string; - controlVariation: T; - treatmentVariations: Array; + controlVariationEntry: T; + treatmentVariationEntries: Array; } export default class EppoClient { @@ -531,32 +531,49 @@ export default class EppoClient { } /** - * For use with 3rd party CMS tooling, such as the Contentful Eppo plugin + * For use with 3rd party CMS tooling, such as the Contentful Eppo plugin. + * + * CMS plugins that integrate with Eppo will follow a common format for + * creating a feature flag. The flag created by the CMS plugin will have + * variations with values 'control', 'treatment-1', 'treatment-2', etc. + * This function allows users to easily return the CMS container entry + * for the assigned variation. + * + * @param flagExperiment the flag key, control container entry and treatment container entries. + * @param subjectKey an identifier of the experiment subject, for example a user ID. + * @param subjectAttributes optional attributes associated with the subject, for example name and email. + * @returns The container entry associated with the experiment. */ - public getExperimentContainer( - flagExperiment: IFlagExperiment, + public getExperimentContainerEntry( + flagExperiment: IContainerExperiment, subjectKey: string, subjectAttributes: Attributes, ): T { - const { flagKey, controlVariation, treatmentVariations } = flagExperiment; + const { flagKey, controlVariationEntry, treatmentVariationEntries } = flagExperiment; const assignment = this.getStringAssignment(flagKey, subjectKey, subjectAttributes, 'control'); if (assignment === 'control') { - return controlVariation; + return controlVariationEntry; } if (!assignment.startsWith('treatment-')) { logger.warn( `Variation ${assignment} cannot be mapped to a container. Defaulting to control variation.`, ); - return controlVariation; + return controlVariationEntry; + } + const treatmentVariationIndex = Number.parseInt(assignment.split('-')[1]) - 1; + if (isNaN(treatmentVariationIndex)) { + logger.warn( + `Variation ${assignment} cannot be mapped to a container. Defaulting to control variation.`, + ); + return controlVariationEntry; } - const treatmentVariationIndex = Number.parseInt(assignment.split('-')[1]); - if (treatmentVariationIndex > treatmentVariations.length) { + if (treatmentVariationIndex >= treatmentVariationEntries.length) { logger.warn( `Selected treatment variation (${treatmentVariationIndex}) index is out of bounds. Defaulting to control variation.`, ); - return controlVariation; + return controlVariationEntry; } - return treatmentVariations[treatmentVariationIndex - 1]; + return treatmentVariationEntries[treatmentVariationIndex]; } private evaluateBanditAction( diff --git a/src/client/test-utils.ts b/src/client/test-utils.ts index 13a52e3..546ae36 100644 --- a/src/client/test-utils.ts +++ b/src/client/test-utils.ts @@ -12,7 +12,7 @@ export async function initConfiguration( queryParams: { apiKey: 'dummy', sdkName: 'js-client-sdk-common', - sdkVersion: '1.0.0', + sdkVersion: '3.0.0', }, }); const httpClient = new FetchHttpClient(apiEndpoints, 1000); diff --git a/src/index.ts b/src/index.ts index eb79512..a4b0eec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { import EppoClient, { FlagConfigurationRequestParameters, IAssignmentDetails, + IContainerExperiment, } from './client/eppo-client'; import FlagConfigRequestor from './configuration-requestor'; import { @@ -48,6 +49,7 @@ export { IAssignmentEvent, IBanditLogger, IBanditEvent, + IContainerExperiment, EppoClient, constants, ApiEndpoints,