Skip to content

Conversation

@aarsilv
Copy link
Contributor

@aarsilv aarsilv commented Jan 20, 2026

Summary

  • Adds offlineInit() function to initialize the SDK with pre-fetched configuration
  • Enables SDK usage in environments without network access or for faster startup

Stacked PRs

Test plan

  • Verify offlineInit() initializes the client with provided configuration
  • Verify flag assignments work correctly after offline initialization
  • Verify getInstance() returns the client after offline init

🤖 Generated with Claude Code

@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from 6109625 to 7eb55bb Compare January 20, 2026 03:04
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch 2 times, most recently from c6bb75d to a769852 Compare January 20, 2026 03:08
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from 5c56e10 to eae1ff4 Compare January 20, 2026 03:10
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch from a769852 to b9221c2 Compare January 20, 2026 03:10
@aarsilv aarsilv changed the title Add offlineInit() for synchronous SDK initialization without network Offline Init Part 3 of 4: Add offlineInit() for synchronous SDK initialization without network Jan 20, 2026
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from eae1ff4 to 3f88511 Compare January 20, 2026 03:20
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch 2 times, most recently from 7cc0255 to 0466712 Compare January 20, 2026 03:24
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part2-get-bandits-configuration branch from 7396a8f to 128bac6 Compare January 20, 2026 03:41
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch from 0466712 to 065c1b6 Compare January 20, 2026 03:41
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>
@aarsilv aarsilv force-pushed the aarsilv/ffesupport-461-offline-init-part3-offline-init branch from 065c1b6 to 3b021c2 Compare January 20, 2026 03:48
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;
Copy link
Contributor Author

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.

Comment on lines +55 to +58
/**
* Configuration settings for the event dispatcher.
* @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs.
*/
Copy link
Contributor Author

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! 🪓

Comment on lines +423 to +425
/**
* @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs.
*/
Copy link
Contributor Author

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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 The main new method!

@aarsilv aarsilv marked this pull request as ready for review January 20, 2026 04:12
Copy link

Copilot AI left a 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 IOfflineClientConfig interface for offline initialization configuration
  • Extracts DEFAULT_ASSIGNMENT_CACHE_SIZE constant to share between init() and offlineInit()

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.

Comment on lines +335 to +336
// 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.
Copy link

Copilot AI Jan 20, 2026

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.

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
Comment on lines +339 to +341
.catch((err) =>
applicationLogger.warn(`Error setting flags for memory-only configuration store: ${err}`),
);
Copy link

Copilot AI Jan 20, 2026

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.

Suggested change
.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}`,
);
});

Copilot uses AI. Check for mistakes.
Comment on lines +366 to +372
banditVariationConfigurationStore
.setEntries(banditVariationsByFlagKey)
.catch((err) =>
applicationLogger.warn(
`Error setting bandit variations for memory-only configuration store: ${err}`,
),
);
Copy link

Copilot AI Jan 20, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +381 to +387
banditModelConfigurationStore
.setEntries(banditsConfigResponse.bandits ?? {})
.catch((err) =>
applicationLogger.warn(
`Error setting bandit models for memory-only configuration store: ${err}`,
),
);
Copy link

Copilot AI Jan 20, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +311 to +323
const flagsConfigResponse = JSON.parse(flagsConfiguration) as {
createdAt?: string;
format?: string;
environment?: { name: string };
flags: Record<string, Flag>;
banditReferences?: Record<
string,
{
modelVersion: string;
flagVariations: BanditVariation[];
}
>;
};
Copy link
Contributor

@greghuels greghuels Jan 20, 2026

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@greghuels greghuels left a 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

@greghuels greghuels assigned aarsilv and unassigned greghuels Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants