-
Notifications
You must be signed in to change notification settings - Fork 1
fix: non-bandit flags return string assignment #238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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,10 +17,8 @@ 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; | ||
|
||
|
@@ -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,16 @@ 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; | ||
await precomputedFlagStore.setEntries(parsed.flags); | ||
await precomputedBanditStore.setEntries(parsed.bandits); | ||
} | ||
}); | ||
|
||
|
@@ -102,6 +103,11 @@ 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' }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also consider a situation where a flag IS a bandit flag, but they got assigned a non-bandit variation. Also we should be using mocks to make sure the proper event loggers are called or not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For the precomputed response, wouldn't that be the same situation? The flag would be in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Updated! And I also added the happy path case of a bandit flag with a bandit variation |
||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -324,39 +324,38 @@ export default class EppoPrecomputedClient { | |
flagKey: string, | ||
defaultValue: string, | ||
): Omit<IAssignmentDetails<string>, '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 }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning this if the precomputed flag is not found, otherwise returning the precomputed flag assignment if the bandit could not be evaluated. Makes sense |
||
} | ||
|
||
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 }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we ax this line all together, just keeping that final return with optional chaining and null coalesce: return { variation: assignedVariation, action: banditEvent?.action ?? null } There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All good I misread it ✅ |
||
} | ||
|
||
return { variation: assignedVariation, action: banditEvent.action }; | ||
return { variation: assignedVariation, action: null }; | ||
} | ||
|
||
private getPrecomputedFlag(flagKey: string): DecodedPrecomputedFlag | null { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙌