-
Notifications
You must be signed in to change notification settings - Fork 0
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
feat: create offlinePrecomputedInit #117
Changes from 10 commits
ecf184d
80340ca
ce4bf21
3fdd774
2cbe3e5
acbafaa
e3f35a5
b0f73a7
723728b
90d725e
c8c6a55
9216ca7
8b3f8c4
409019a
c625655
82ca8ba
265bdd9
6f32a79
9b4e404
c2f0c78
ee3409f
3410428
3320634
f31efd8
3f5a2bb
5f24951
72c6ac3
5c3712e
d71e589
e094f8e
27b413b
70a0667
a2db546
a53962d
8612f58
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,5 +1,4 @@ | ||
import { AttributeType, Flag, IAssignmentLogger, IAsyncStore } from '@eppo/js-client-sdk-common'; | ||
import { EventDispatcherConfig } from '@eppo/js-client-sdk-common/src/events/default-event-dispatcher'; | ||
|
||
import { ServingStoreUpdateStrategy } from './isolatable-hybrid.store'; | ||
|
||
|
@@ -64,10 +63,10 @@ interface IBaseRequestConfig { | |
} | ||
|
||
/** | ||
* Configuration for Eppo precomputed client initialization | ||
* Configuration for precomputed flag assignments | ||
* @public | ||
*/ | ||
export interface IPrecomputedClientConfig extends IBaseRequestConfig { | ||
export interface IPrecompute { | ||
/** | ||
* Subject key to use for precomputed flag assignments. | ||
*/ | ||
|
@@ -79,6 +78,14 @@ export interface IPrecomputedClientConfig extends IBaseRequestConfig { | |
subjectAttributes?: Record<string, AttributeType>; | ||
} | ||
|
||
/** | ||
* Configuration for Eppo precomputed client initialization | ||
* @public | ||
*/ | ||
export interface IPrecomputedClientConfig extends IBaseRequestConfig { | ||
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. Breaking change to the interface of |
||
precompute: IPrecompute; | ||
} | ||
|
||
/** | ||
* Configuration for regular client initialization | ||
* @public | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,9 +18,11 @@ import * as td from 'testdouble'; | |
import { | ||
getTestAssignments, | ||
IAssignmentTestCase, | ||
MOCK_PRECOMPUTED_RESPONSE_FILE, | ||
MOCK_UFC_RESPONSE_FILE, | ||
OBFUSCATED_MOCK_UFC_RESPONSE_FILE, | ||
readAssignmentTestData, | ||
readMockPrecomputedResponse, | ||
readMockUfcResponse, | ||
validateTestAssignments, | ||
} from '../test/testHelpers'; | ||
|
@@ -36,6 +38,7 @@ import { | |
IAssignmentLogger, | ||
init, | ||
offlineInit, | ||
offlinePrecomputedInit, | ||
precomputedInit, | ||
} from './index'; | ||
|
||
|
@@ -1066,59 +1069,14 @@ describe('EppoPrecomputedJSClient E2E test', () => { | |
|
||
beforeAll(async () => { | ||
global.fetch = jest.fn(() => { | ||
const precomputedConfigurationWire = readMockPrecomputedResponse( | ||
MOCK_PRECOMPUTED_RESPONSE_FILE, | ||
); | ||
const precomputedResponse = JSON.parse(precomputedConfigurationWire).precomputed.response; | ||
return Promise.resolve({ | ||
ok: true, | ||
status: 200, | ||
json: () => | ||
Promise.resolve({ | ||
createdAt: '2024-11-18T14:23:39.456Z', | ||
format: 'PRECOMPUTED', | ||
environment: { | ||
name: 'Test', | ||
}, | ||
flags: { | ||
'string-flag': { | ||
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, | ||
}, | ||
'numeric-flag': { | ||
allocationKey: 'allocation-126', | ||
variationKey: 'variation-126', | ||
variationType: 'NUMERIC', | ||
variationValue: 3.14, | ||
extraLogging: {}, | ||
doLog: true, | ||
}, | ||
'integer-flag': { | ||
allocationKey: 'allocation-125', | ||
variationKey: 'variation-125', | ||
variationType: 'INTEGER', | ||
variationValue: 42, | ||
extraLogging: {}, | ||
doLog: true, | ||
}, | ||
'json-flag': { | ||
allocationKey: 'allocation-127', | ||
variationKey: 'variation-127', | ||
variationType: 'JSON', | ||
variationValue: '{"key": "value", "number": 123}', | ||
extraLogging: {}, | ||
doLog: true, | ||
}, | ||
}, | ||
}), | ||
json: () => Promise.resolve(precomputedResponse), | ||
}); | ||
}) as jest.Mock; | ||
|
||
|
@@ -1128,8 +1086,10 @@ describe('EppoPrecomputedJSClient E2E test', () => { | |
apiKey: 'dummy', | ||
baseUrl: 'http://127.0.0.1:4000', | ||
assignmentLogger: mockLogger, | ||
subjectKey: 'test-subject', | ||
subjectAttributes: { attr1: 'value1' }, | ||
precompute: { | ||
subjectKey: 'test-subject', | ||
subjectAttributes: { attr1: 'value1' }, | ||
}, | ||
}); | ||
}); | ||
|
||
|
@@ -1179,6 +1139,50 @@ describe('EppoPrecomputedJSClient E2E test', () => { | |
}); | ||
}); | ||
|
||
describe('offlinePrecomputedInit', () => { | ||
let mockLogger: IAssignmentLogger; | ||
let precomputedConfigurationWire: string; | ||
|
||
beforeAll(() => { | ||
precomputedConfigurationWire = readMockPrecomputedResponse(MOCK_PRECOMPUTED_RESPONSE_FILE); | ||
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. Currently, tests use the unobfuscated, unstringified In a follow up PR I will add support for parsing json strings and handling obfuscated fields |
||
}); | ||
|
||
beforeEach(() => { | ||
mockLogger = td.object<IAssignmentLogger>(); | ||
// Reset the static instance before each test | ||
jest.isolateModules(() => { | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
require('./index'); | ||
sameerank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
}); | ||
|
||
it('initializes with precomputed assignments', () => { | ||
const client = offlinePrecomputedInit({ | ||
precomputedConfigurationWire, | ||
assignmentLogger: mockLogger, | ||
}); | ||
|
||
expect(client.getStringAssignment('string-flag', 'default')).toBe('red'); | ||
expect(td.explain(mockLogger.logAssignment).callCount).toBe(1); | ||
expect(td.explain(mockLogger.logAssignment).calls[0]?.args[0]).toMatchObject({ | ||
subject: 'test-subject-key', | ||
featureFlag: 'string-flag', | ||
allocation: 'allocation-123', | ||
variation: 'variation-123', | ||
subjectAttributes: { | ||
device: 'iPhone', | ||
country: 'USA', | ||
}, | ||
}); | ||
}); | ||
|
||
it('initializes without an assignment logger', () => { | ||
const client = offlinePrecomputedInit({ precomputedConfigurationWire }); | ||
|
||
expect(client.getStringAssignment('string-flag', 'default')).toBe('red'); | ||
}); | ||
}); | ||
|
||
describe('EppoClient config', () => { | ||
it('should initialize event dispatcher with default values', async () => { | ||
global.fetch = jest.fn(() => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,8 +17,10 @@ | |
ObfuscatedFlag, | ||
BoundedEventQueue, | ||
validation, | ||
PrecomputedFlag, | ||
Event, | ||
} from '@eppo/js-client-sdk-common'; | ||
import { Environment, FormatEnum } from '@eppo/js-client-sdk-common/dist/interfaces'; | ||
|
||
import { assignmentCacheFactory } from './cache/assignment-cache-factory'; | ||
import HybridAssignmentCache from './cache/hybrid-assignment-cache'; | ||
|
@@ -32,7 +34,7 @@ | |
} from './configuration-factory'; | ||
import BrowserNetworkStatusListener from './events/browser-network-status-listener'; | ||
import LocalStorageBackedNamedEventQueue from './events/local-storage-backed-named-event-queue'; | ||
import { IClientConfig, IPrecomputedClientConfig } from './i-client-config'; | ||
import { IClientConfig, IPrecompute, IPrecomputedClientConfig } from './i-client-config'; | ||
Check warning on line 37 in src/index.ts
|
||
import { sdkName, sdkVersion } from './sdk-data'; | ||
|
||
/** | ||
|
@@ -465,7 +467,7 @@ | |
// both failed, make the "fatal" error the fetch one | ||
initializationError = initFromFetchError; | ||
} | ||
} catch (error: any) { | ||
Check warning on line 470 in src/index.ts
|
||
initializationError = error; | ||
} | ||
|
||
|
@@ -558,13 +560,12 @@ | |
config: IPrecomputedClientConfig, | ||
): Promise<EppoPrecomputedClient> { | ||
validation.validateNotBlank(config.apiKey, 'API key required'); | ||
validation.validateNotBlank(config.subjectKey, 'Subject key required'); | ||
validation.validateNotBlank(config.precompute.subjectKey, 'Subject key required'); | ||
|
||
const instance = EppoPrecomputedJSClient.instance; | ||
const { | ||
apiKey, | ||
subjectKey, | ||
subjectAttributes = {}, | ||
precompute: { subjectKey, subjectAttributes = {} }, | ||
baseUrl, | ||
requestTimeoutMs, | ||
numInitialRequestRetries, | ||
|
@@ -584,10 +585,7 @@ | |
sdkName, | ||
sdkVersion, | ||
baseUrl, | ||
precompute: { | ||
subjectKey, | ||
subjectAttributes, | ||
}, | ||
precompute: { subjectKey, subjectAttributes }, | ||
requestTimeoutMs, | ||
numInitialRequestRetries, | ||
numPollRequestRetries, | ||
|
@@ -605,6 +603,92 @@ | |
return EppoPrecomputedJSClient.instance; | ||
} | ||
|
||
/** | ||
* Initializes the Eppo precomputed client with configuration parameters. | ||
* | ||
* The purpose is for use-cases where the precomputed assignments are available from an external process | ||
* that can bootstrap the SDK. | ||
* | ||
* This method should be called once on application startup. | ||
* | ||
* @param config - client configuration | ||
* @returns a singleton precomputed client instance | ||
* @public | ||
*/ | ||
sameerank marked this conversation as resolved.
Show resolved
Hide resolved
sameerank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
export interface IPrecomputedClientConfigSync { | ||
precomputedConfigurationWire: string; | ||
assignmentLogger?: IAssignmentLogger; | ||
throwOnFailedInitialization?: boolean; | ||
} | ||
|
||
export interface IConfigurationWire { | ||
version: number; | ||
precomputed: { | ||
subjectKey: string; | ||
subjectAttributes: Record<string, AttributeType>; | ||
fetchedAt: string; | ||
response: { | ||
createdAt: string; | ||
format: FormatEnum; | ||
obfuscated: boolean; | ||
environment: Environment; | ||
flags: Record<string, PrecomputedFlag>; | ||
}; | ||
}; | ||
} | ||
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. Will replace with an exported interface from Eppo-exp/js-sdk-common#160 |
||
|
||
/** | ||
* Initializes the Eppo precomputed client with configuration parameters. | ||
* | ||
* The purpose is for use-cases where the precomputed assignments are available from an external process | ||
* that can bootstrap the SDK. | ||
* | ||
* This method should be called once on application startup. | ||
* | ||
* @param config - precomputed client configuration | ||
* @returns a singleton precomputed client instance | ||
* @public | ||
*/ | ||
export function offlinePrecomputedInit( | ||
config: IPrecomputedClientConfigSync, | ||
): EppoPrecomputedClient { | ||
const throwOnFailedInitialization = config.throwOnFailedInitialization ?? true; | ||
|
||
const configurationWire: IConfigurationWire = JSON.parse(config.precomputedConfigurationWire); | ||
const { subjectKey, subjectAttributes, response } = configurationWire.precomputed; | ||
const parsedResponse = response; // TODO: use a JSON.parse when the obfuscated version is usable | ||
|
||
try { | ||
const memoryOnlyPrecomputedStore = precomputedFlagsStorageFactory(); | ||
memoryOnlyPrecomputedStore | ||
.setEntries(parsedResponse.flags) | ||
.catch((err) => | ||
applicationLogger.warn('Error setting precomputed assignments for memory-only store', err), | ||
); | ||
|
||
EppoPrecomputedJSClient.instance.setSubjectAndPrecomputedFlagStore( | ||
subjectKey, | ||
subjectAttributes, | ||
memoryOnlyPrecomputedStore, | ||
); | ||
|
||
if (config.assignmentLogger) { | ||
EppoPrecomputedJSClient.instance.setAssignmentLogger(config.assignmentLogger); | ||
} | ||
} catch (error) { | ||
applicationLogger.warn( | ||
'Eppo SDK encountered an error initializing precomputed client, assignment calls will return the default value and not be logged', | ||
); | ||
if (throwOnFailedInitialization) { | ||
throw error; | ||
} | ||
} | ||
|
||
EppoPrecomputedJSClient.instance.setIsObfuscated(parsedResponse.obfuscated); | ||
sameerank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
EppoPrecomputedJSClient.initialized = true; | ||
return EppoPrecomputedJSClient.instance; | ||
} | ||
|
||
/** | ||
* Used to access a singleton SDK precomputed client instance. | ||
* Use the method after calling precomputedInit() to initialize the client. | ||
|
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.
Temporarily using this branch for testing until Eppo-exp/sdk-test-data#89 gets an approval 🙏