Skip to content

Commit 0a91e7e

Browse files
committed
fix(flags): align configuration error precedence
1 parent babb7b9 commit 0a91e7e

3 files changed

Lines changed: 177 additions & 57 deletions

File tree

packages/core/src/flags/FlagsClient.ts

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
4243
export 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.
114117
type 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

Comments
 (0)