Skip to content

Commit 9dfebeb

Browse files
authored
fix: non-bandit flags return string assignment (#238)
* fix: non-bandit flags return string assignment * CR changes
1 parent 57f4d72 commit 9dfebeb

File tree

3 files changed

+56
-40
lines changed

3 files changed

+56
-40
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "4.13.1",
3+
"version": "4.13.2",
44
"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)",
55
"main": "dist/index.js",
66
"files": [

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

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
2+
MOCK_PRECOMPUTED_WIRE_FILE,
23
readMockConfigurationWireResponse,
3-
MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE,
44
} from '../../test/testHelpers';
55
import ApiEndpoints from '../api-endpoints';
66
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
77
import FetchHttpClient from '../http-client';
8-
import { PrecomputedFlag, IObfuscatedPrecomputedBandit } from '../interfaces';
8+
import { IObfuscatedPrecomputedBandit, PrecomputedFlag } from '../interfaces';
99
import PrecomputedFlagRequestor from '../precomputed-requestor';
1010

1111
import EppoPrecomputedClient from './eppo-precomputed-client';
@@ -17,12 +17,10 @@ describe('EppoPrecomputedClient Bandits E2E test', () => {
1717
const mockLogAssignment = jest.fn();
1818
const mockLogBanditAction = jest.fn();
1919

20-
const precomputedConfigurationWire = readMockConfigurationWireResponse(
21-
MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE,
22-
);
23-
const parsedPrecomputedResponse = JSON.parse(precomputedConfigurationWire).precomputed.response;
20+
const obfuscatedConfigurationWire = readMockConfigurationWireResponse(MOCK_PRECOMPUTED_WIRE_FILE);
21+
const obfuscatedResponse = JSON.parse(obfuscatedConfigurationWire).precomputed.response;
2422

25-
const testModes = ['offline', 'online'] as const;
23+
const testModes = ['offline'];
2624

2725
testModes.forEach((mode) => {
2826
describe(`${mode} mode`, () => {
@@ -33,7 +31,7 @@ describe('EppoPrecomputedClient Bandits E2E test', () => {
3331
return Promise.resolve({
3432
ok: true,
3533
status: 200,
36-
json: () => Promise.resolve(parsedPrecomputedResponse),
34+
json: () => Promise.resolve(JSON.parse(obfuscatedResponse)),
3735
});
3836
}) as jest.Mock;
3937

@@ -62,13 +60,17 @@ describe('EppoPrecomputedClient Bandits E2E test', () => {
6260
categoricalAttributes: { loyalty_tier: 'bronze' },
6361
},
6462
},
63+
'not-a-bandit-flag': {},
6564
},
6665
);
6766
await configurationRequestor.fetchAndStorePrecomputedFlags();
6867
} else if (mode === 'offline') {
68+
const parsed = JSON.parse(obfuscatedResponse);
6969
// Offline mode: directly populate stores with precomputed response
70-
await precomputedFlagStore.setEntries(parsedPrecomputedResponse.flags);
71-
await precomputedBanditStore.setEntries(parsedPrecomputedResponse.bandits);
70+
precomputedFlagStore.salt = parsed.salt;
71+
precomputedBanditStore.salt = parsed.salt;
72+
await precomputedFlagStore.setEntries(parsed.flags);
73+
await precomputedBanditStore.setEntries(parsed.bandits);
7274
}
7375
});
7476

@@ -102,6 +104,21 @@ describe('EppoPrecomputedClient Bandits E2E test', () => {
102104
const precomputedConfiguration = client.getBanditAction('banner_bandit_flag', 'nike');
103105
expect(precomputedConfiguration).toEqual({ action: null, variation: 'nike' });
104106
});
107+
108+
it('should return the assigned variation if a flag is not a bandit', () => {
109+
const precomputedConfiguration = client.getBanditAction('not-a-bandit-flag', 'default');
110+
expect(precomputedConfiguration).toEqual({ action: null, variation: 'control' });
111+
expect(mockLogBanditAction).not.toHaveBeenCalled();
112+
});
113+
114+
it('should return the bandit variation and action if a flag is a bandit', () => {
115+
const precomputedConfiguration = client.getBanditAction('string-flag', 'default');
116+
expect(precomputedConfiguration).toEqual({
117+
action: 'show_red_button',
118+
variation: 'red',
119+
});
120+
expect(mockLogBanditAction).toHaveBeenCalled();
121+
});
105122
});
106123
});
107124
});

src/client/eppo-precomputed-client.ts

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -324,39 +324,38 @@ export default class EppoPrecomputedClient {
324324
flagKey: string,
325325
defaultValue: string,
326326
): Omit<IAssignmentDetails<string>, 'evaluationDetails'> {
327-
const banditEvaluation = this.getPrecomputedBandit(flagKey);
328-
329-
if (banditEvaluation == null) {
330-
logger.warn(`${loggerPrefix} No assigned variation. Bandit not found: ${flagKey}`);
327+
const precomputedFlag = this.getPrecomputedFlag(flagKey);
328+
if (!precomputedFlag) {
329+
logger.warn(`${loggerPrefix} No assigned variation. Flag not found: ${flagKey}`);
331330
return { variation: defaultValue, action: null };
332331
}
333-
332+
const banditEvaluation = this.getPrecomputedBandit(flagKey);
334333
const assignedVariation = this.getStringAssignment(flagKey, defaultValue);
335-
336-
const banditEvent: IBanditEvent = {
337-
timestamp: new Date().toISOString(),
338-
featureFlag: flagKey,
339-
bandit: banditEvaluation.banditKey,
340-
subject: this.subject.subjectKey ?? '',
341-
action: banditEvaluation.action,
342-
actionProbability: banditEvaluation.actionProbability,
343-
optimalityGap: banditEvaluation.optimalityGap,
344-
modelVersion: banditEvaluation.modelVersion,
345-
subjectNumericAttributes: banditEvaluation.actionNumericAttributes,
346-
subjectCategoricalAttributes: banditEvaluation.actionCategoricalAttributes,
347-
actionNumericAttributes: banditEvaluation.actionNumericAttributes,
348-
actionCategoricalAttributes: banditEvaluation.actionCategoricalAttributes,
349-
metaData: this.buildLoggerMetadata(),
350-
evaluationDetails: null,
351-
};
352-
353-
try {
354-
this.logBanditAction(banditEvent);
355-
} catch (error) {
356-
logger.error(`${loggerPrefix} Error logging bandit action: ${error}`);
334+
if (banditEvaluation) {
335+
const banditEvent: IBanditEvent = {
336+
timestamp: new Date().toISOString(),
337+
featureFlag: flagKey,
338+
bandit: banditEvaluation.banditKey,
339+
subject: this.subject.subjectKey ?? '',
340+
action: banditEvaluation.action,
341+
actionProbability: banditEvaluation.actionProbability,
342+
optimalityGap: banditEvaluation.optimalityGap,
343+
modelVersion: banditEvaluation.modelVersion,
344+
subjectNumericAttributes: banditEvaluation.actionNumericAttributes,
345+
subjectCategoricalAttributes: banditEvaluation.actionCategoricalAttributes,
346+
actionNumericAttributes: banditEvaluation.actionNumericAttributes,
347+
actionCategoricalAttributes: banditEvaluation.actionCategoricalAttributes,
348+
metaData: this.buildLoggerMetadata(),
349+
evaluationDetails: null,
350+
};
351+
try {
352+
this.logBanditAction(banditEvent);
353+
} catch (error) {
354+
logger.error(`${loggerPrefix} Error logging bandit action: ${error}`);
355+
}
356+
return { variation: assignedVariation, action: banditEvent.action };
357357
}
358-
359-
return { variation: assignedVariation, action: banditEvent.action };
358+
return { variation: assignedVariation, action: null };
360359
}
361360

362361
private getPrecomputedFlag(flagKey: string): DecodedPrecomputedFlag | null {

0 commit comments

Comments
 (0)