@@ -37,11 +37,13 @@ import type { JsonValue, EvaluationContext, FlagDetails } from './types';
3737 * Error codes an offline configuration result can carry:
3838 * - `INVALID_CONTEXT`: the active context does not match the precomputed snapshot.
3939 * - `PROVIDER_NOT_READY`: an offline operation ran with no configuration loaded.
40- * - `GENERAL`: the loaded configuration is unusable (malformed/unsupported/undecodable).
40+ * - `PARSE_ERROR`: a supplied configuration has no usable capability.
41+ * - `GENERAL`: an unexpected rules-engine error occurred during evaluation.
4142 */
4243export type ConfigurationErrorCode =
4344 | 'INVALID_CONTEXT'
4445 | 'PROVIDER_NOT_READY'
46+ | 'PARSE_ERROR'
4547 | 'GENERAL' ;
4648
4749/**
@@ -84,34 +86,37 @@ type LoadedConfigurationState =
8486 | { kind : 'none' }
8587 | {
8688 kind : 'configuration' ;
89+ configurationError ?: string ;
8790 precomputed : LoadedBranch < LoadedPrecomputed > ;
8891 rules : LoadedBranch < RulesConfigurationResponse > ;
8992 } ;
9093
9194// TODO(FFL-2837): Delete this legacy `rulesBased` compatibility shape after a
9295// flagging-core release contains DataDog/openfeature-js-client#344 through
93- // `9f794c7 `. Read
96+ // `82bfc2e `. Read
9497// `configuration.rules.response` directly. The configuration is already parsed
9598// from the complete portable envelope. Do not add raw-service-response handling
9699// or envelope construction to `FlagsClient`. PR #344 moves parsing to
97100// `@datadog/flagging-core/configuration`; keep that opt-in import in the local
98101// wire module and keep `FlagsClient` independent of the parser and Protobuf-ES.
99- // Keep `precomputedError` and `precomputed.flagErrors` when the released type
100- // provides them. Do not copy PR #336's precedence that blocks valid rules when
101- // `precomputedError` is present. PR #336 is now restacked at `33113d2`, and its
102- // combined evaluator still has that precedence. Its browser
103- // `DatadogOfflineProvider` uses the combined evaluator, but React Native must keep
104- // its separate paths until that behavior reaches parity. The released evaluator
102+ // Keep `configurationError`, `rulesError`, `precomputedError`, and
103+ // `precomputed.flagErrors` when the released type provides them. PR #336 through
104+ // `4d0f24e` now selects valid matching precomputed data, then valid rules, before
105+ // it returns an applicable parse error. Keep these separate paths for the native
106+ // precomputed cache and tracking behavior, but use the same capability and error
107+ // precedence. Replace compatible lifecycle checks with the upstream
108+ // `getFlagsConfigurationError` helper after publication. The released evaluator
105109// must include PR #344's deterministic flag-scoped
106110// `PARSE_ERROR` results, including unsupported feature levels, unknown-field
107111// tolerance, lossless protobuf integer parsing, and the required SHA-256
108- // digest-length validation. It must also either support integer and shard
109- // evaluation when global `BigInt` is unavailable or document `BigInt` as a
110- // runtime requirement. The smoke test at `9f794c7` does not cover those paths.
111- // `FlagsClient` must not convert a parsed `bigint` or repair an upstream
112- // `GENERAL` result. It must preserve the evaluator's `PARSE_ERROR` when a value
113- // cannot be represented safely as a JavaScript number.
112+ // digest-length validation. Its safe-integer conversion no longer calls global
113+ // `BigInt`; keep coverage for unsafe integers and shard values without that
114+ // global. `FlagsClient` must not convert a parsed `bigint`. It must preserve the
115+ // evaluator's `PARSE_ERROR` when a value cannot be represented safely as a
116+ // JavaScript number.
114117type ConfigurationWithPendingRules = ParsedFlagsConfiguration & {
118+ configurationError ?: string ;
119+ rulesError ?: string ;
115120 rulesBased ?: { response ?: unknown } ;
116121 precomputedError ?: string ;
117122 precomputed ?: ParsedPrecomputedConfiguration & {
@@ -281,7 +286,7 @@ export class FlagsClient {
281286 *
282287 * For a precomputed configuration this decodes the snapshot once and adopts its embedded
283288 * evaluation context when none is set — **no network request is made**. An unusable
284- * configuration reconciles to an error (`GENERAL `); a context mismatch to `INVALID_CONTEXT`.
289+ * configuration reconciles to an error (`PARSE_ERROR `); a context mismatch to `INVALID_CONTEXT`.
285290 *
286291 * @param configuration The configuration to load.
287292 *
@@ -320,12 +325,7 @@ export class FlagsClient {
320325 let precomputedBranch : LoadedBranch < LoadedPrecomputed > = {
321326 status : 'absent'
322327 } ;
323- if ( pendingConfiguration . precomputedError !== undefined ) {
324- precomputedBranch = {
325- status : 'invalid' ,
326- errorMessage : pendingConfiguration . precomputedError
327- } ;
328- } else if ( precomputed ) {
328+ if ( precomputed ) {
329329 try {
330330 precomputedBranch = {
331331 status : 'ready' ,
@@ -348,6 +348,11 @@ export class FlagsClient {
348348 ) ;
349349 precomputedBranch = { status : 'invalid' , errorMessage } ;
350350 }
351+ } else if ( pendingConfiguration . precomputedError !== undefined ) {
352+ precomputedBranch = {
353+ status : 'invalid' ,
354+ errorMessage : pendingConfiguration . precomputedError
355+ } ;
351356 }
352357
353358 let rulesBranch : LoadedBranch < RulesConfigurationResponse > = {
@@ -365,10 +370,16 @@ export class FlagsClient {
365370 status : 'invalid' ,
366371 errorMessage : prepared . errorMessage
367372 } ;
373+ } else if ( pendingConfiguration . rulesError !== undefined ) {
374+ rulesBranch = {
375+ status : 'invalid' ,
376+ errorMessage : pendingConfiguration . rulesError
377+ } ;
368378 }
369379
370380 return {
371381 kind : 'configuration' ,
382+ configurationError : pendingConfiguration . configurationError ,
372383 precomputed : precomputedBranch ,
373384 rules : rulesBranch
374385 } ;
@@ -392,7 +403,7 @@ export class FlagsClient {
392403 ) ;
393404 }
394405
395- const { precomputed, rules } = loaded ;
406+ const { configurationError , precomputed, rules } = loaded ;
396407
397408 if (
398409 precomputed . status === 'ready' &&
@@ -421,18 +432,16 @@ export class FlagsClient {
421432 return this . enterReady ( ) ;
422433 }
423434
435+ if ( configurationError !== undefined ) {
436+ return this . enterError ( 'PARSE_ERROR' , configurationError ) ;
437+ }
438+
424439 if ( rules . status === 'invalid' ) {
425- return this . enterError (
426- 'GENERAL' ,
427- `The rules configuration for '${ this . clientName } ' is not usable: ${ rules . errorMessage } `
428- ) ;
440+ return this . enterError ( 'PARSE_ERROR' , rules . errorMessage ) ;
429441 }
430442
431443 if ( precomputed . status === 'invalid' ) {
432- return this . enterError (
433- 'GENERAL' ,
434- `The precomputed configuration for '${ this . clientName } ' is not usable: ${ precomputed . errorMessage } `
435- ) ;
444+ return this . enterError ( 'PARSE_ERROR' , precomputed . errorMessage ) ;
436445 }
437446
438447 if ( precomputed . status === 'ready' ) {
@@ -443,8 +452,8 @@ export class FlagsClient {
443452 }
444453
445454 return this . enterError (
446- 'GENERAL ' ,
447- `The loaded configuration for ' ${ this . clientName } ' does not contain a usable branch.`
455+ 'PARSE_ERROR ' ,
456+ 'Flags configuration contains no usable capability'
448457 ) ;
449458 } ;
450459
@@ -502,7 +511,6 @@ export class FlagsClient {
502511 errorCode :
503512 | ConfigurationErrorCode
504513 | 'FLAG_NOT_FOUND'
505- | 'PARSE_ERROR'
506514 | 'TARGETING_KEY_MISSING'
507515 | 'TYPE_MISMATCH' ,
508516 errorMessage ?: string
@@ -564,7 +572,6 @@ export class FlagsClient {
564572 ) :
565573 | ConfigurationErrorCode
566574 | 'FLAG_NOT_FOUND'
567- | 'PARSE_ERROR'
568575 | 'TARGETING_KEY_MISSING'
569576 | 'TYPE_MISMATCH' => {
570577 switch ( errorCode ) {
@@ -677,11 +684,20 @@ export class FlagsClient {
677684 ) ;
678685 }
679686
687+ if ( loaded . configurationError !== undefined ) {
688+ return this . errorDetails (
689+ key ,
690+ defaultValue ,
691+ 'PARSE_ERROR' ,
692+ loaded . configurationError
693+ ) ;
694+ }
695+
680696 if ( loaded . rules . status === 'invalid' ) {
681697 return this . errorDetails (
682698 key ,
683699 defaultValue ,
684- 'GENERAL ' ,
700+ 'PARSE_ERROR ' ,
685701 loaded . rules . errorMessage
686702 ) ;
687703 }
@@ -690,7 +706,7 @@ export class FlagsClient {
690706 return this . errorDetails (
691707 key ,
692708 defaultValue ,
693- 'GENERAL ' ,
709+ 'PARSE_ERROR ' ,
694710 loaded . precomputed . errorMessage
695711 ) ;
696712 }
@@ -707,8 +723,8 @@ export class FlagsClient {
707723 return this . errorDetails (
708724 key ,
709725 defaultValue ,
710- 'GENERAL ' ,
711- `The loaded configuration for ' ${ this . clientName } ' does not contain a usable branch.`
726+ 'PARSE_ERROR ' ,
727+ 'Flags configuration contains no usable capability'
712728 ) ;
713729 } ;
714730
@@ -737,7 +753,7 @@ export class FlagsClient {
737753 }
738754
739755 // An offline configuration that cannot be served against the active context surfaces the
740- // precise error code (INVALID_CONTEXT / GENERAL / PROVIDER_NOT_READY) with the coded
756+ // precise error code (INVALID_CONTEXT / PARSE_ERROR / PROVIDER_NOT_READY) with the coded
741757 // default. The OpenFeature provider maps this to a PROVIDER_ERROR / ERROR state.
742758 if ( this . configurationStatus === 'error' && this . configurationError ) {
743759 return this . errorDetails (
0 commit comments