@@ -94,6 +94,62 @@ interface BanditsConfigurationResponse {
9494 */
9595const DEFAULT_ASSIGNMENT_CACHE_SIZE = 50_000 ;
9696
97+ /**
98+ * Validates that the parsed flags configuration has all required fields.
99+ * Returns an array of validation error messages, or empty array if valid.
100+ */
101+ function validateFlagsConfiguration ( config : unknown ) : string [ ] {
102+ const errors : string [ ] = [ ] ;
103+
104+ if ( ! config || typeof config !== 'object' ) {
105+ errors . push ( 'Configuration must be an object' ) ;
106+ return errors ;
107+ }
108+
109+ const cfg = config as Record < string , unknown > ;
110+
111+ if ( typeof cfg . createdAt !== 'string' ) {
112+ errors . push ( 'Missing or invalid "createdAt" field' ) ;
113+ }
114+ if ( typeof cfg . format !== 'string' ) {
115+ errors . push ( 'Missing or invalid "format" field' ) ;
116+ }
117+ if ( ! cfg . environment || typeof cfg . environment !== 'object' ) {
118+ errors . push ( 'Missing or invalid "environment" field' ) ;
119+ } else if ( typeof ( cfg . environment as Record < string , unknown > ) . name !== 'string' ) {
120+ errors . push ( 'Missing or invalid "environment.name" field' ) ;
121+ }
122+ if ( ! cfg . flags || typeof cfg . flags !== 'object' ) {
123+ errors . push ( 'Missing or invalid "flags" field' ) ;
124+ }
125+ if ( ! cfg . banditReferences || typeof cfg . banditReferences !== 'object' ) {
126+ errors . push ( 'Missing or invalid "banditReferences" field' ) ;
127+ }
128+
129+ return errors ;
130+ }
131+
132+ /**
133+ * Validates that the parsed bandits configuration has all required fields.
134+ * Returns an array of validation error messages, or empty array if valid.
135+ */
136+ function validateBanditsConfiguration ( config : unknown ) : string [ ] {
137+ const errors : string [ ] = [ ] ;
138+
139+ if ( ! config || typeof config !== 'object' ) {
140+ errors . push ( 'Configuration must be an object' ) ;
141+ return errors ;
142+ }
143+
144+ const cfg = config as Record < string , unknown > ;
145+
146+ if ( ! cfg . bandits || typeof cfg . bandits !== 'object' ) {
147+ errors . push ( 'Missing or invalid "bandits" field' ) ;
148+ }
149+
150+ return errors ;
151+ }
152+
97153/**
98154 * @deprecated Eppo has discontinued eventing support. Event tracking will be handled by Datadog SDKs.
99155 */
@@ -308,53 +364,49 @@ export function offlineInit(config: IOfflineClientConfig): EppoClient {
308364 throwOnFailedInitialization = true ,
309365 } = config ;
310366
367+ // Create memory-only configuration stores
368+ flagConfigurationStore = new MemoryOnlyConfigurationStore < Flag > ( ) ;
369+ banditVariationConfigurationStore = new MemoryOnlyConfigurationStore < BanditVariation [ ] > ( ) ;
370+ banditModelConfigurationStore = new MemoryOnlyConfigurationStore < BanditParameters > ( ) ;
371+
311372 try {
312- // Parse the flags configuration JSON
313- const flagsConfigResponse = JSON . parse ( flagsConfiguration ) as {
314- createdAt ?: string ;
315- format ?: string ;
316- environment ?: { name : string } ;
317- flags : Record < string , Flag > ;
318- banditReferences ?: Record <
319- string ,
320- {
321- modelVersion : string ;
322- flagVariations : BanditVariation [ ] ;
323- }
324- > ;
325- } ;
326-
327- // Create memory-only configuration stores
328- flagConfigurationStore = new MemoryOnlyConfigurationStore < Flag > ( ) ;
329- banditVariationConfigurationStore = new MemoryOnlyConfigurationStore < BanditVariation [ ] > ( ) ;
330- banditModelConfigurationStore = new MemoryOnlyConfigurationStore < BanditParameters > ( ) ;
331-
332- // Set format from the configuration (default to SERVER)
333- const format = ( flagsConfigResponse . format as FormatEnum ) ?? FormatEnum . SERVER ;
334- flagConfigurationStore . setFormat ( format ) ;
335-
336- // Load flag configurations into store
337- // Note: setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally,
338- // so there's no race condition. We add .catch() for defensive error handling, matching JS client SDK pattern.
339- flagConfigurationStore
340- . setEntries ( flagsConfigResponse . flags ?? { } )
341- . catch ( ( err ) =>
342- applicationLogger . warn ( `Error setting flags for memory-only configuration store: ${ err } ` ) ,
373+ // Parse and validate the flags configuration JSON
374+ const parsedFlagsConfig = JSON . parse ( flagsConfiguration ) ;
375+ const flagsValidationErrors = validateFlagsConfiguration ( parsedFlagsConfig ) ;
376+
377+ if ( flagsValidationErrors . length > 0 ) {
378+ const errorMessage = `Invalid flags configuration: ${ flagsValidationErrors . join ( ', ' ) } ` ;
379+ if ( throwOnFailedInitialization ) {
380+ throw new Error ( errorMessage ) ;
381+ }
382+ applicationLogger . warn (
383+ `${ errorMessage } . Using empty configuration - all assignments will return default values.` ,
343384 ) ;
385+ // Skip loading flags config, stores remain empty
386+ } else {
387+ // Cast to typed response after validation
388+ const flagsConfigResponse = parsedFlagsConfig as FlagsConfigurationResponse ;
389+
390+ // Set format from the configuration
391+ flagConfigurationStore . setFormat ( flagsConfigResponse . format ) ;
392+
393+ // Load flag configurations into store
394+ // Note: setEntries is async but MemoryOnlyConfigurationStore performs synchronous operations internally,
395+ // so there's no race condition. We add .catch() for defensive error handling, matching JS client SDK pattern.
396+ flagConfigurationStore
397+ . setEntries ( flagsConfigResponse . flags )
398+ . catch ( ( err ) =>
399+ applicationLogger . warn ( `Error setting flags for memory-only configuration store: ${ err } ` ) ,
400+ ) ;
344401
345- // Set configuration timestamp if available
346- if ( flagsConfigResponse . createdAt ) {
402+ // Set configuration timestamp
347403 flagConfigurationStore . setConfigPublishedAt ( flagsConfigResponse . createdAt ) ;
348- }
349404
350- // Set environment if available
351- if ( flagsConfigResponse . environment ) {
405+ // Set environment
352406 flagConfigurationStore . setEnvironment ( flagsConfigResponse . environment ) ;
353- }
354407
355- // Process bandit references from the flags configuration
356- // Index by flag key for quick lookup (instead of by bandit key)
357- if ( flagsConfigResponse . banditReferences ) {
408+ // Process bandit references from the flags configuration
409+ // Index by flag key for quick lookup (instead of by bandit key)
358410 const banditVariationsByFlagKey : Record < string , BanditVariation [ ] > = { } ;
359411 for ( const banditReference of Object . values ( flagsConfigResponse . banditReferences ) ) {
360412 for ( const flagVariation of banditReference . flagVariations ) {
@@ -376,17 +428,28 @@ export function offlineInit(config: IOfflineClientConfig): EppoClient {
376428
377429 // Parse and load bandit models if provided
378430 if ( banditsConfiguration ) {
379- const banditsConfigResponse = JSON . parse ( banditsConfiguration ) as {
380- updatedAt ?: string ;
381- bandits : Record < string , BanditParameters > ;
382- } ;
383- banditModelConfigurationStore
384- . setEntries ( banditsConfigResponse . bandits ?? { } )
385- . catch ( ( err ) =>
386- applicationLogger . warn (
387- `Error setting bandit models for memory-only configuration store: ${ err } ` ,
388- ) ,
431+ const parsedBanditsConfig = JSON . parse ( banditsConfiguration ) ;
432+ const banditsValidationErrors = validateBanditsConfiguration ( parsedBanditsConfig ) ;
433+
434+ if ( banditsValidationErrors . length > 0 ) {
435+ const errorMessage = `Invalid bandits configuration: ${ banditsValidationErrors . join ( ', ' ) } ` ;
436+ if ( throwOnFailedInitialization ) {
437+ throw new Error ( errorMessage ) ;
438+ }
439+ applicationLogger . warn (
440+ ` ${ errorMessage } . Skipping bandit configuration - bandit assignments will not work.` ,
389441 ) ;
442+ // Skip loading bandits config, store remains empty
443+ } else {
444+ const banditsConfigResponse = parsedBanditsConfig as BanditsConfigurationResponse ;
445+ banditModelConfigurationStore
446+ . setEntries ( banditsConfigResponse . bandits )
447+ . catch ( ( err ) =>
448+ applicationLogger . warn (
449+ `Error setting bandit models for memory-only configuration store: ${ err } ` ,
450+ ) ,
451+ ) ;
452+ }
390453 }
391454
392455 // Create client without request parameters (offline mode - no polling)
0 commit comments