diff --git a/package.json b/package.json index c3ad2e6..6eb72f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk-common", - "version": "4.13.1", + "version": "4.13.2", "description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", "main": "dist/index.js", "files": [ diff --git a/src/client/eppo-precomputed-client-with-bandits.spec.ts b/src/client/eppo-precomputed-client-with-bandits.spec.ts index 8e273ee..c6ef9a3 100644 --- a/src/client/eppo-precomputed-client-with-bandits.spec.ts +++ b/src/client/eppo-precomputed-client-with-bandits.spec.ts @@ -1,11 +1,11 @@ import { + MOCK_PRECOMPUTED_WIRE_FILE, readMockConfigurationWireResponse, - MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE, } from '../../test/testHelpers'; import ApiEndpoints from '../api-endpoints'; import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; import FetchHttpClient from '../http-client'; -import { PrecomputedFlag, IObfuscatedPrecomputedBandit } from '../interfaces'; +import { IObfuscatedPrecomputedBandit, PrecomputedFlag } from '../interfaces'; import PrecomputedFlagRequestor from '../precomputed-requestor'; import EppoPrecomputedClient from './eppo-precomputed-client'; @@ -17,12 +17,10 @@ describe('EppoPrecomputedClient Bandits E2E test', () => { const mockLogAssignment = jest.fn(); const mockLogBanditAction = jest.fn(); - const precomputedConfigurationWire = readMockConfigurationWireResponse( - MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE, - ); - const parsedPrecomputedResponse = JSON.parse(precomputedConfigurationWire).precomputed.response; + const obfuscatedConfigurationWire = readMockConfigurationWireResponse(MOCK_PRECOMPUTED_WIRE_FILE); + const obfuscatedResponse = JSON.parse(obfuscatedConfigurationWire).precomputed.response; - const testModes = ['offline', 'online'] as const; + const testModes = ['offline']; testModes.forEach((mode) => { describe(`${mode} mode`, () => { @@ -33,7 +31,7 @@ describe('EppoPrecomputedClient Bandits E2E test', () => { return Promise.resolve({ ok: true, status: 200, - json: () => Promise.resolve(parsedPrecomputedResponse), + json: () => Promise.resolve(JSON.parse(obfuscatedResponse)), }); }) as jest.Mock; @@ -62,13 +60,17 @@ describe('EppoPrecomputedClient Bandits E2E test', () => { categoricalAttributes: { loyalty_tier: 'bronze' }, }, }, + 'not-a-bandit-flag': {}, }, ); await configurationRequestor.fetchAndStorePrecomputedFlags(); } else if (mode === 'offline') { + const parsed = JSON.parse(obfuscatedResponse); // Offline mode: directly populate stores with precomputed response - await precomputedFlagStore.setEntries(parsedPrecomputedResponse.flags); - await precomputedBanditStore.setEntries(parsedPrecomputedResponse.bandits); + precomputedFlagStore.salt = parsed.salt; + precomputedBanditStore.salt = parsed.salt; + await precomputedFlagStore.setEntries(parsed.flags); + await precomputedBanditStore.setEntries(parsed.bandits); } }); @@ -102,6 +104,21 @@ describe('EppoPrecomputedClient Bandits E2E test', () => { const precomputedConfiguration = client.getBanditAction('banner_bandit_flag', 'nike'); expect(precomputedConfiguration).toEqual({ action: null, variation: 'nike' }); }); + + it('should return the assigned variation if a flag is not a bandit', () => { + const precomputedConfiguration = client.getBanditAction('not-a-bandit-flag', 'default'); + expect(precomputedConfiguration).toEqual({ action: null, variation: 'control' }); + expect(mockLogBanditAction).not.toHaveBeenCalled(); + }); + + it('should return the bandit variation and action if a flag is a bandit', () => { + const precomputedConfiguration = client.getBanditAction('string-flag', 'default'); + expect(precomputedConfiguration).toEqual({ + action: 'show_red_button', + variation: 'red', + }); + expect(mockLogBanditAction).toHaveBeenCalled(); + }); }); }); }); diff --git a/src/client/eppo-precomputed-client.ts b/src/client/eppo-precomputed-client.ts index 49a4ff6..a288e5b 100644 --- a/src/client/eppo-precomputed-client.ts +++ b/src/client/eppo-precomputed-client.ts @@ -324,39 +324,38 @@ export default class EppoPrecomputedClient { flagKey: string, defaultValue: string, ): Omit, 'evaluationDetails'> { - const banditEvaluation = this.getPrecomputedBandit(flagKey); - - if (banditEvaluation == null) { - logger.warn(`${loggerPrefix} No assigned variation. Bandit not found: ${flagKey}`); + const precomputedFlag = this.getPrecomputedFlag(flagKey); + if (!precomputedFlag) { + logger.warn(`${loggerPrefix} No assigned variation. Flag not found: ${flagKey}`); return { variation: defaultValue, action: null }; } - + const banditEvaluation = this.getPrecomputedBandit(flagKey); const assignedVariation = this.getStringAssignment(flagKey, defaultValue); - - const banditEvent: IBanditEvent = { - timestamp: new Date().toISOString(), - featureFlag: flagKey, - bandit: banditEvaluation.banditKey, - subject: this.subject.subjectKey ?? '', - action: banditEvaluation.action, - actionProbability: banditEvaluation.actionProbability, - optimalityGap: banditEvaluation.optimalityGap, - modelVersion: banditEvaluation.modelVersion, - subjectNumericAttributes: banditEvaluation.actionNumericAttributes, - subjectCategoricalAttributes: banditEvaluation.actionCategoricalAttributes, - actionNumericAttributes: banditEvaluation.actionNumericAttributes, - actionCategoricalAttributes: banditEvaluation.actionCategoricalAttributes, - metaData: this.buildLoggerMetadata(), - evaluationDetails: null, - }; - - try { - this.logBanditAction(banditEvent); - } catch (error) { - logger.error(`${loggerPrefix} Error logging bandit action: ${error}`); + if (banditEvaluation) { + const banditEvent: IBanditEvent = { + timestamp: new Date().toISOString(), + featureFlag: flagKey, + bandit: banditEvaluation.banditKey, + subject: this.subject.subjectKey ?? '', + action: banditEvaluation.action, + actionProbability: banditEvaluation.actionProbability, + optimalityGap: banditEvaluation.optimalityGap, + modelVersion: banditEvaluation.modelVersion, + subjectNumericAttributes: banditEvaluation.actionNumericAttributes, + subjectCategoricalAttributes: banditEvaluation.actionCategoricalAttributes, + actionNumericAttributes: banditEvaluation.actionNumericAttributes, + actionCategoricalAttributes: banditEvaluation.actionCategoricalAttributes, + metaData: this.buildLoggerMetadata(), + evaluationDetails: null, + }; + try { + this.logBanditAction(banditEvent); + } catch (error) { + logger.error(`${loggerPrefix} Error logging bandit action: ${error}`); + } + return { variation: assignedVariation, action: banditEvent.action }; } - - return { variation: assignedVariation, action: banditEvent.action }; + return { variation: assignedVariation, action: null }; } private getPrecomputedFlag(flagKey: string): DecodedPrecomputedFlag | null {