-
Notifications
You must be signed in to change notification settings - Fork 2
Offline Init Part 3 of 4: Add offlineInit() for synchronous SDK initialization without network #118
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
base: aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration
Are you sure you want to change the base?
Conversation
6109625 to
7eb55bb
Compare
c6bb75d to
a769852
Compare
5c56e10 to
eae1ff4
Compare
a769852 to
b9221c2
Compare
eae1ff4 to
3f88511
Compare
7cc0255 to
0466712
Compare
7396a8f to
128bac6
Compare
0466712 to
065c1b6
Compare
Adds the ability to initialize the SDK with pre-fetched configuration JSON, enabling: - Synchronous initialization without network requests - Use cases where configuration is bootstrapped from another source - Edge/serverless environments where polling isn't desired Also includes: - IOfflineClientConfig interface for offline initialization options - DEFAULT_ASSIGNMENT_CACHE_SIZE constant for consistent cache sizing - Deprecation annotations on event tracking (discontinued feature) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
065c1b6 to
3b021c2
Compare
Use realistic bandit model coefficients from bandit-models-v1.json and test subject "alice" from test-case-banner-bandit.json to verify that getBanditAction returns the expected variation and action after offline initialization. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| * } | ||
| * ``` | ||
| */ | ||
| flagsConfiguration: string; |
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.
Opted to deviate from our client JS SDK and follow in the footsteps of python and iOS using the more user-friendly and information-rich wire format.
| /** | ||
| * Configuration settings for the event dispatcher. | ||
| * @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs. | ||
| */ |
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.
May as well start flagging this as deprecated so we can rip out in a future version! 🪓
| /** | ||
| * @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs. | ||
| */ |
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.
Deprecating this event-related method too! 🪓
- Rename "makes client available via getInstance()" to "can request assignment" - Move "no network requests" test into "basic initialization" section - Add assignment verification to "empty flags configuration" test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| * @returns the initialized client instance | ||
| * @public | ||
| */ | ||
| export function offlineInit(config: IOfflineClientConfig): EppoClient { |
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.
🚀 The main new method!
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.
Pull request overview
This PR adds an offlineInit() function to enable synchronous SDK initialization using pre-fetched configuration, allowing the SDK to operate without network access or achieve faster startup times.
Changes:
- Introduces
offlineInit()function that parses and loads flag and bandit configurations from JSON strings - Adds
IOfflineClientConfiginterface for offline initialization configuration - Extracts
DEFAULT_ASSIGNMENT_CACHE_SIZEconstant to share betweeninit()andofflineInit()
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/index.ts | Adds offlineInit() function, IOfflineClientConfig export, and DEFAULT_ASSIGNMENT_CACHE_SIZE constant |
| src/index.spec.ts | Comprehensive test suite for offlineInit() covering initialization, assignment logging, error handling, and bandit support |
| src/i-client-config.ts | Defines IOfflineClientConfig interface with documentation for offline initialization parameters |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Note: setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally, | ||
| // so there's no race condition. We add .catch() for defensive error handling, matching JS client SDK pattern. |
Copilot
AI
Jan 20, 2026
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.
The comment claims setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally. This is confusing and potentially misleading. If the operations are truly synchronous, the method shouldn't return a promise. Consider clarifying whether this is actually asynchronous or if the promise is just for API consistency.
| // Note: setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally, | |
| // so there's no race condition. We add .catch() for defensive error handling, matching JS client SDK pattern. | |
| // Note: setEntries returns a Promise for API consistency, but MemoryOnlyConfigurationStore | |
| // performs its work synchronously, so there is no race condition. We still attach .catch() | |
| // for defensive error handling in case the Promise is ever rejected. |
| .catch((err) => | ||
| applicationLogger.warn(`Error setting flags for memory-only configuration store: ${err}`), | ||
| ); |
Copilot
AI
Jan 20, 2026
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.
The error message uses string interpolation with ${err} which will result in "[object Object]" for Error objects. Use err.message or handle the error object properly to provide a meaningful error message.
| .catch((err) => | |
| applicationLogger.warn(`Error setting flags for memory-only configuration store: ${err}`), | |
| ); | |
| .catch((err) => { | |
| const message = err instanceof Error ? err.message : String(err); | |
| applicationLogger.warn( | |
| `Error setting flags for memory-only configuration store: ${message}`, | |
| ); | |
| }); |
| banditVariationConfigurationStore | ||
| .setEntries(banditVariationsByFlagKey) | ||
| .catch((err) => | ||
| applicationLogger.warn( | ||
| `Error setting bandit variations for memory-only configuration store: ${err}`, | ||
| ), | ||
| ); |
Copilot
AI
Jan 20, 2026
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.
The error message uses string interpolation with ${err} which will result in "[object Object]" for Error objects. Use err.message or handle the error object properly to provide a meaningful error message.
| banditModelConfigurationStore | ||
| .setEntries(banditsConfigResponse.bandits ?? {}) | ||
| .catch((err) => | ||
| applicationLogger.warn( | ||
| `Error setting bandit models for memory-only configuration store: ${err}`, | ||
| ), | ||
| ); |
Copilot
AI
Jan 20, 2026
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.
The error message uses string interpolation with ${err} which will result in "[object Object]" for Error objects. Use err.message or handle the error object properly to provide a meaningful error message.
| const flagsConfigResponse = JSON.parse(flagsConfiguration) as { | ||
| createdAt?: string; | ||
| format?: string; | ||
| environment?: { name: string }; | ||
| flags: Record<string, Flag>; | ||
| banditReferences?: Record< | ||
| string, | ||
| { | ||
| modelVersion: string; | ||
| flagVariations: BanditVariation[]; | ||
| } | ||
| >; | ||
| }; |
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.
There should never be a situation where a configuration object is missing a key, so why not just validate that each field exists and throw or log an "invalid configuration" error if it's missing anything? That way you can cast to IUniversalFlagConfigResponse for valid configurations and you can remove the guards below. The main benefit here is that you reduce the chance that an invalid configuration causes issues in other parts of the SDK that expect certain fields to be set.
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.
Great call! Will go beyond just parsing to checking expected keys are all there.
greghuels
left a comment
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.
Main issue is that I don't think we shouldn't allow invalid configurations
Summary
offlineInit()function to initialize the SDK with pre-fetched configurationStacked PRs
getFlagsConfiguration()getBanditsConfiguration()offlineInit()(this PR)Test plan
offlineInit()initializes the client with provided configurationgetInstance()returns the client after offline init🤖 Generated with Claude Code