Skip to content

fix(analytics): distinguish fallback reasons + forward backend error message (SDK-79, SDK-83)#180

Open
tylerjroach wants to merge 6 commits into
masterfrom
fix/sdk-79-variant-source-fallback-reason
Open

fix(analytics): distinguish fallback reasons + forward backend error message (SDK-79, SDK-83)#180
tylerjroach wants to merge 6 commits into
masterfrom
fix/sdk-79-variant-source-fallback-reason

Conversation

@tylerjroach

@tylerjroach tylerjroach commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Bundles two related fixes. Both touch the same fallback_reason plumbing so they merge as one PR.

SDK-79 — distinguish fallback reasons (local eval)

Three local-evaluation outcomes — flag-not-found, no-rollout-match, and missing-context-key — previously all returned the bare developer fallback. The OpenFeature wrapper collapsed them to FLAG_NOT_FOUND, sending callers chasing the flag name when the real cause was usually a rule miss or absent context.

SelectedVariant now carries two source fields: variant_source (local / remote / fallback) and fallback_reason (set only when source is fallback). The wrapper dispatches on fallback_reason and maps each to the spec-correct OpenFeature response — most notably, NO_ROLLOUT_MATCH becomes reason: DEFAULT with no error code instead of FLAG_NOT_FOUND.

SDK-83 — forward the backend's error message (remote eval)

When the remote /flags endpoint returned an error response (e.g. HTTP 400 with "distinct_id must be provided in evalContext as a string"), the catch block returned the fallback variant without attaching the cause. The wrapper saw a fallback and translated to FLAG_NOT_FOUND — indistinguishable from a genuinely missing flag. Go propagates these as GENERAL with the full backend message; the goal here is to match that.

To carry the message, FallbackReason is upgraded from a bare string constant to a small value object with kind (the PHP-aligned discriminator) and optional message (set on BACKEND_ERROR and MISSING_CONTEXT_KEY). The remote provider's catch branches tag the fallback with FallbackReason.backend_error(message); the wrapper forwards message into OpenFeature's error_message / errorMessage.

Fix pattern

FallbackReason kinds (PHP-aligned)

Kind When it fires Carries message?
FLAG_NOT_FOUND Flag key not in the ruleset / absent from /flags response no
MISSING_CONTEXT_KEY Required context attribute (distinct_id / targeting key) absent the missing attribute name
NO_ROLLOUT_MATCH Flag exists, user isn't in any rollout no
BACKEND_ERROR Remote /flags error response the backend's response body (SDK-83)
Provider not initialized no

OpenFeature mapping

Kind OpenFeature response
FLAG_NOT_FOUND error_code: FLAG_NOT_FOUND, reason: DEFAULT
MISSING_CONTEXT_KEY error_code: TARGETING_KEY_MISSING, reason: ERROR, error_message: <attribute name>
NO_ROLLOUT_MATCH reason: DEFAULT, no error
BACKEND_ERROR error_code: GENERAL, reason: ERROR, error_message: <backend body>

PROVIDER_NOT_READY is handled by the wrapper's _are_flags_ready short-circuit before invoking the underlying provider, so no producer ever stamps a NOT_READY kind.

Test plan

  • Full base SDK + OpenFeature wrapper test suites pass
  • New BACKEND_ERROR producer-side test verifies the backend response body propagates through to fallback_reason.message
  • Wrapper test verifies error_message / errorMessage forwards the backend message for BACKEND_ERROR and the missing-attribute name for MISSING_CONTEXT_KEY

🤖 Generated with Claude Code

@tylerjroach tylerjroach requested review from a team and ketanmixpanel June 29, 2026 15:09
@linear-code

linear-code Bot commented Jun 29, 2026

Copy link
Copy Markdown

SDK-79

SDK-83

@tylerjroach tylerjroach changed the title fix(flags): tag fallback_reason so OpenFeature can distinguish causes (SDK-79) feat(analytics): tag fallback_reason so OpenFeature can distinguish causes (SDK-79) Jun 29, 2026
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.80240% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.96%. Comparing base (2fd717c) to head (6e0f514).

Files with missing lines Patch % Lines
...ture-provider/src/mixpanel_openfeature/provider.py 91.30% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #180      +/-   ##
==========================================
+ Coverage   95.79%   95.96%   +0.16%     
==========================================
  Files          13       13              
  Lines        2259     2401     +142     
  Branches      129      136       +7     
==========================================
+ Hits         2164     2304     +140     
- Misses         62       63       +1     
- Partials       33       34       +1     
Flag Coverage Δ
openfeature-provider 54.76% <81.73%> (+2.76%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tylerjroach and others added 2 commits June 29, 2026 12:18
SelectedVariant now carries two source fields: `variant_source`
(local | remote | fallback) and `fallback_reason` (FLAG_NOT_FOUND |
MISSING_CONTEXT_KEY | NO_ROLLOUT_MATCH | BACKEND_ERROR | NOT_READY,
set only when source is fallback).

Three behaviorally distinct outcomes — flag-not-found, no-rollout-match,
and missing-context-key — previously all returned the bare fallback. The
OpenFeature wrapper collapsed them to FLAG_NOT_FOUND, sending callers
chasing the flag name when the real cause was usually a rule miss or
absent context.

The wrapper now dispatches on fallback_reason and maps each to the
spec-correct OpenFeature response. Most notably, NO_ROLLOUT_MATCH
becomes `reason: DEFAULT` with no error code instead of FLAG_NOT_FOUND.

Constant names align with mixpanel-php for consistency across SDKs.

Linear: SDK-79

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tylerjroach tylerjroach force-pushed the fix/sdk-79-variant-source-fallback-reason branch from d07720e to 4d8ba39 Compare June 29, 2026 16:19
…end message (SDK-79, SDK-83)

SDK-79 made fallback_reason a PHP-aligned string constant on every
returned fallback variant. That covers the local-eval cases (flag
not found, no rollout match, missing context key) but couldn't carry
detail — when the remote /flags endpoint returned an error response,
the SDK still had nowhere to attach the backend's message.

Upgrade FallbackReason from a class of string constants to a frozen
Pydantic value object with `kind` (the discriminator) and optional
`message`. Frozen singletons for the no-detail reasons; factory
methods for the ones that carry detail.

SDK-83: RemoteFeatureFlagsProvider's except branches now tag the
fallback with FallbackReason.backend_error(message). For httpx
HTTPStatusError specifically, the response body is extracted via
_describe_backend_error so the actionable detail ("distinct_id must
be provided in evalContext as a string") reaches the wrapper —
str(exc) alone only yields httpx's standardized status line.

Wrapper dispatches on reason.kind, forwards reason.message into
FlagResolutionDetails.error_message for backend_error and
missing_context_key.

Linear: SDK-79, SDK-83

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tylerjroach tylerjroach changed the title feat(analytics): tag fallback_reason so OpenFeature can distinguish causes (SDK-79) fix(analytics): distinguish fallback reasons + forward backend error message (SDK-79, SDK-83) Jun 29, 2026
tylerjroach and others added 2 commits June 30, 2026 10:54
The wrapper short-circuits to PROVIDER_NOT_READY at the top of
resolve when areFlagsReady() is false (see _are_flags_ready), so
no producer ever constructs a FallbackReason with kind NOT_READY —
the case was dead, same pattern Swift PR #745 and Android PR #981
just cleaned up.

Remove the kind from the Literal type, drop the not_ready() factory
and _NOT_READY singleton, and drop the NOT_READY dispatch arm from
the wrapper's error_mapping. The provider_not_ready short-circuit
test path is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comments were explaining the absence of a case to a hypothetical
cross-SDK reader. The absence is self-explanatory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 updates the Mixpanel Python SDK’s feature-flag “fallback” plumbing so callers (especially via the OpenFeature provider) can distinguish why a fallback occurred and, for remote evaluation errors, see the backend’s error message rather than an undifferentiated “flag not found”.

Changes:

  • Introduces VariantSource and a structured FallbackReason(kind, message) on SelectedVariant, with helper methods to tag successful variants vs. fallbacks.
  • Tags local and remote flag evaluation results with variant_source / fallback_reason, including propagating remote backend error bodies into FallbackReason.message.
  • Updates the OpenFeature provider to map each fallback kind to the spec-appropriate FlagResolutionDetails (error code/reason/message), and adds/extends tests for the new mapping behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
openfeature-provider/tests/test_provider.py Extends wrapper tests to cover new fallback kinds and forwarded backend/missing-key messages.
openfeature-provider/src/mixpanel_openfeature/provider.py Maps SDK fallback reasons to OpenFeature responses and forwards error messages.
mixpanel/flags/types.py Adds VariantSource, FallbackReason, and SelectedVariant helpers for tagging evaluation outcomes.
mixpanel/flags/test_remote_feature_flags.py Adds test ensuring backend HTTP error text is preserved through FallbackReason.message.
mixpanel/flags/test_local_feature_flags.py Adds tests validating local provider tags match/fallback cases with correct reason/message.
mixpanel/flags/remote_feature_flags.py Tags remote results as REMOTE or FALLBACK, and captures backend error bodies for fallbacks.
mixpanel/flags/local_feature_flags.py Tags local results as LOCAL or FALLBACK with specific fallback kinds.

Comment on lines 30 to 34
def __init__(
self,
flags_provider: typing.Any,
mixpanel_instance: Optional[Mixpanel] = None,
mixpanel_instance: Mixpanel | None = None,
) -> None:

@property
def mixpanel(self) -> Optional[Mixpanel]:
def mixpanel(self) -> Mixpanel | None:
flag_key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
evaluation_context: EvaluationContext | None = None,
flag_key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
evaluation_context: EvaluationContext | None = None,
flag_key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
evaluation_context: EvaluationContext | None = None,
Comment on lines 145 to 148
default_value: typing.Any,
expected_type: typing.Optional[type],
evaluation_context: typing.Optional[EvaluationContext] = None,
expected_type: type | None,
evaluation_context: EvaluationContext | None = None,
) -> FlagResolutionDetails:
Comment on lines +222 to +224
def _fallback_details(
fallback_reason: FallbackReason | None, default_value: typing.Any
) -> FlagResolutionDetails | None:
Comment on lines +168 to +172
fallback_details = self._fallback_details(
result.fallback_reason, default_value
)
if fallback_details is not None:
return fallback_details
Comment thread mixpanel/flags/types.py
Comment on lines +69 to +71
Set by the providers on every returned variant — coarse-grained
(local / remote / fallback). For the specific reason behind a fallback,
see FallbackReason.
Comment thread mixpanel/flags/types.py
Comment on lines +93 to +98
kind: Literal[
"FLAG_NOT_FOUND",
"MISSING_CONTEXT_KEY",
"NO_ROLLOUT_MATCH",
"BACKEND_ERROR",
]

@ketanmixpanel ketanmixpanel left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@tylerjroach Take a look at the copilot comments, apart from that looks good to me.

…ndling

Two review-driven fixes:

1. RemoteFeatureFlagsProvider.{get,aget}_all_variants now stamp
   variant_source=REMOTE on every returned variant, matching the
   docstring's promise that VariantSource is set on "every returned
   variant". Previously these methods returned the raw decoded
   SelectedVariants and undercut the wrapper's source-based dispatch.

2. The OpenFeature wrapper now treats `result is fallback &&
   fallback_reason is None` as FLAG_NOT_FOUND. Defends against custom
   flags providers written against the pre-SDK-79 contract that return
   the fallback object unchanged — otherwise the wrapper would
   incorrectly report a successful targeting match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tylerjroach

Copy link
Copy Markdown
Contributor Author

Thanks @ketanmixpanel — pushed 6e0f514 addressing both substantive Copilot threads:

1. RemoteFeatureFlagsProvider.{get,aget}_all_variants not tagging variant_source (types.py:71 — Copilot ID 3501288521): Real bug — fixed. Both methods now stamp variant_source=REMOTE on every returned variant. Updated the two existing _returns_all_variants_from_api tests to assert the tagging.

2. Wrapper treating bare fallback as a successful match (provider.py:172 — Copilot ID 3501288498): Real regression risk for any custom flags provider written against the pre-SDK-79 contract that returns the fallback object unchanged. Added a defensive result is fallback and fallback_reason is None → FLAG_NOT_FOUND guard plus a test_bare_fallback_treated_as_flag_not_found covering it.

3. NOT_READY mention in PR description (types.py:98 — Copilot ID 3501288546): The kind was removed entirely in 54c8e5f earlier in this PR — the wrapper handles PROVIDER_NOT_READY via its _are_flags_ready short-circuit, so no producer ever stamps NOT_READY. Same cleanup landed in mixpanel-ruby PR #153 / mixpanel-node PR #277 / mixpanel-java PR #89 / mixpanel-go PR #98. I'll update the PR description to reflect the implemented set of kinds.

4. PEP 604 union syntax warnings on Python 3.9 (multiple Copilot threads on provider.py): These are spurious — provider.py has from __future__ import annotations at the top, which turns all annotations into strings at parse time. PEP 604 unions like Mixpanel | None in annotation positions are safe on 3.9 under this import; they're only evaluated when something calls typing.get_type_hints(), which the wrapper doesn't do. Verified: the full test suite passes on Python 3.9.

Tests: 148 passed, ruff clean.

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

Safe to merge — the fallback reason plumbing is correct end-to-end and the backward-compat guard keeps pre-SDK-79 custom providers working

The two findings are non-blocking: missing_context_key's optional key parameter is always supplied at current call sites, and the _fallback_details fall-through can only fire for a kind value that the Literal type currently prevents from being constructed. The core logic — tagging variants, mapping kinds to OpenFeature codes, extracting backend error bodies — is correct across both sync and async paths and is well covered by tests.

types.py (missing_context_key signature) and provider.py (_fallback_details exhaustiveness)

Important Files Changed

Filename Overview
mixpanel/flags/types.py Adds VariantSource, FallbackReason (frozen Pydantic model with factory singletons), and with_source/as_fallback helpers on SelectedVariant — well-structured, one minor API looseness on missing_context_key's optional key parameter
openfeature-provider/src/mixpanel_openfeature/provider.py Dispatches on FallbackReason.kind to produce spec-correct OpenFeature responses; backward-compat guard for pre-SDK-79 providers preserved; _fallback_details lacks an explicit exhaustive fallthrough for future kinds
mixpanel/flags/remote_feature_flags.py Adds _describe_backend_error to extract HTTP response body from httpx.HTTPStatusError, tags all returned variants with REMOTE source, and propagates exception as BACKEND_ERROR fallback — logic is correct for both sync and async paths
mixpanel/flags/local_feature_flags.py Tags all three fallback exit paths (flag-not-found, missing-context-key, no-rollout-match) with appropriate FallbackReason, and tags successful variants with LOCAL source — straightforward and correct
mixpanel/flags/test_local_feature_flags.py Adds four new tests covering local-eval tagging (match, missing flag, missing context, no rollout) — good coverage of all new SDK-79 paths
mixpanel/flags/test_remote_feature_flags.py Updates existing variant-equality assertions to check keys/values individually, adds SDK-83 backend-error propagation test — changes are appropriate
openfeature-provider/tests/test_provider.py New helper setup_fallback, updated setup_flag to tag variants correctly, and new tests for NO_ROLLOUT_MATCH, MISSING_CONTEXT_KEY, BACKEND_ERROR, and bare-fallback compat path — comprehensive

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant OF as OpenFeature caller
    participant P as MixpanelProvider._resolve
    participant FP as FlagsProvider (local/remote)
    participant SV as SelectedVariant

    OF->>P: "resolve_*_details(flag_key, default)"

    alt provider not ready
        P-->>OF: FlagResolutionDetails(PROVIDER_NOT_READY, ERROR)
    end

    P->>FP: get_variant(flag_key, fallback, ctx)

    alt exception thrown
        FP-->>P: raise Exception
        P-->>OF: "FlagResolutionDetails(GENERAL, ERROR, error_message=str(exc))"
    else flag found / rollout matched
        FP->>SV: "selected_variant.with_source(LOCAL|REMOTE)"
        SV-->>FP: "SelectedVariant(variant_source=local|remote, fallback_reason=None)"
        FP-->>P: tagged SelectedVariant
        P-->>OF: FlagResolutionDetails(value, TARGETING_MATCH)
    else fallback — no rollout match
        FP->>SV: fallback.as_fallback(NO_ROLLOUT_MATCH)
        FP-->>P: "SelectedVariant(source=FALLBACK, kind=NO_ROLLOUT_MATCH)"
        P-->>OF: FlagResolutionDetails(default, DEFAULT, no error_code)
    else fallback — flag not found
        FP->>SV: fallback.as_fallback(FLAG_NOT_FOUND)
        FP-->>P: "SelectedVariant(source=FALLBACK, kind=FLAG_NOT_FOUND)"
        P-->>OF: FlagResolutionDetails(default, FLAG_NOT_FOUND, DEFAULT)
    else fallback — missing context key
        FP->>SV: "fallback.as_fallback(MISSING_CONTEXT_KEY, msg=attr)"
        FP-->>P: "SelectedVariant(source=FALLBACK, kind=MISSING_CONTEXT_KEY)"
        P-->>OF: "FlagResolutionDetails(default, TARGETING_KEY_MISSING, ERROR, error_message=attr)"
    else fallback — backend error (SDK-83)
        FP->>SV: "fallback.as_fallback(BACKEND_ERROR, msg=body)"
        FP-->>P: "SelectedVariant(source=FALLBACK, kind=BACKEND_ERROR)"
        P-->>OF: "FlagResolutionDetails(default, GENERAL, ERROR, error_message=body)"
    else untagged fallback (pre-SDK-79 compat)
        FP-->>P: returns fallback object unchanged
        P-->>OF: FlagResolutionDetails(default, FLAG_NOT_FOUND, DEFAULT)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant OF as OpenFeature caller
    participant P as MixpanelProvider._resolve
    participant FP as FlagsProvider (local/remote)
    participant SV as SelectedVariant

    OF->>P: "resolve_*_details(flag_key, default)"

    alt provider not ready
        P-->>OF: FlagResolutionDetails(PROVIDER_NOT_READY, ERROR)
    end

    P->>FP: get_variant(flag_key, fallback, ctx)

    alt exception thrown
        FP-->>P: raise Exception
        P-->>OF: "FlagResolutionDetails(GENERAL, ERROR, error_message=str(exc))"
    else flag found / rollout matched
        FP->>SV: "selected_variant.with_source(LOCAL|REMOTE)"
        SV-->>FP: "SelectedVariant(variant_source=local|remote, fallback_reason=None)"
        FP-->>P: tagged SelectedVariant
        P-->>OF: FlagResolutionDetails(value, TARGETING_MATCH)
    else fallback — no rollout match
        FP->>SV: fallback.as_fallback(NO_ROLLOUT_MATCH)
        FP-->>P: "SelectedVariant(source=FALLBACK, kind=NO_ROLLOUT_MATCH)"
        P-->>OF: FlagResolutionDetails(default, DEFAULT, no error_code)
    else fallback — flag not found
        FP->>SV: fallback.as_fallback(FLAG_NOT_FOUND)
        FP-->>P: "SelectedVariant(source=FALLBACK, kind=FLAG_NOT_FOUND)"
        P-->>OF: FlagResolutionDetails(default, FLAG_NOT_FOUND, DEFAULT)
    else fallback — missing context key
        FP->>SV: "fallback.as_fallback(MISSING_CONTEXT_KEY, msg=attr)"
        FP-->>P: "SelectedVariant(source=FALLBACK, kind=MISSING_CONTEXT_KEY)"
        P-->>OF: "FlagResolutionDetails(default, TARGETING_KEY_MISSING, ERROR, error_message=attr)"
    else fallback — backend error (SDK-83)
        FP->>SV: "fallback.as_fallback(BACKEND_ERROR, msg=body)"
        FP-->>P: "SelectedVariant(source=FALLBACK, kind=BACKEND_ERROR)"
        P-->>OF: "FlagResolutionDetails(default, GENERAL, ERROR, error_message=body)"
    else untagged fallback (pre-SDK-79 compat)
        FP-->>P: returns fallback object unchanged
        P-->>OF: FlagResolutionDetails(default, FLAG_NOT_FOUND, DEFAULT)
    end
Loading

Reviews (1): Last reviewed commit: "fix(flags): tag get_all_variants results..." | Re-trigger Greptile

Comment thread mixpanel/flags/types.py
Comment on lines +112 to +113
def missing_context_key(cls, key: Optional[str] = None) -> "FallbackReason":
return cls(kind="MISSING_CONTEXT_KEY", message=key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 missing_context_key accepts None key silently

The key parameter defaults to None, so FallbackReason.missing_context_key() produces a MISSING_CONTEXT_KEY reason with message=None. The OpenFeature wrapper then forwards None as error_message, giving callers no indication of which attribute is missing — defeating the purpose of SDK-79's richer error propagation. All current call sites pass the key correctly, but the signature invites misuse. Consider removing the default or asserting key is not None.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +248 to +268
"""
if fallback_reason is None:
return None
# Flag exists, user just didn't match any rollout — per the
# OpenFeature spec this is `reason: DEFAULT` with no error.
if fallback_reason.kind == "NO_ROLLOUT_MATCH":
return FlagResolutionDetails(value=default_value, reason=Reason.DEFAULT)
error_mapping = {
"FLAG_NOT_FOUND": (ErrorCode.FLAG_NOT_FOUND, Reason.DEFAULT),
"MISSING_CONTEXT_KEY": (ErrorCode.TARGETING_KEY_MISSING, Reason.ERROR),
"BACKEND_ERROR": (ErrorCode.GENERAL, Reason.ERROR),
}
if fallback_reason.kind in error_mapping:
error_code, reason = error_mapping[fallback_reason.kind]
return FlagResolutionDetails(
value=default_value,
error_code=error_code,
error_message=fallback_reason.message,
reason=reason,
)
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Silent fall-through for unrecognised fallback_reason.kind

If fallback_reason is set (not None) but its kind isn't NO_ROLLOUT_MATCH and isn't in error_mapping, _fallback_details returns None. The caller in _resolve then falls through to the success path and returns reason=TARGETING_MATCH with the fallback value — a false positive. All four current Literal kinds are handled, so this doesn't fire today, but adding a new kind to the Literal without updating this mapping would silently misreport it as a successful evaluation. A final else branch returning a GENERAL error (or an assert_never) would make the exhaustiveness explicit.

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