Skip to content

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

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

fix(analytics): distinguish fallback reasons + forward backend error message (SDK-79, SDK-83)#153
tylerjroach wants to merge 4 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)
NOT_READY 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>
NOT_READY error_code: PROVIDER_NOT_READY, reason: ERROR

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

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>
@tylerjroach tylerjroach requested review from a team and jakewski 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 93.87755% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.39%. Comparing base (e5edf71) to head (d3565ae).

Files with missing lines Patch % Lines
lib/mixpanel-ruby/flags/types.rb 91.17% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #153      +/-   ##
==========================================
- Coverage   96.64%   96.39%   -0.25%     
==========================================
  Files          14       14              
  Lines         656      694      +38     
==========================================
+ Hits          634      669      +35     
- Misses         22       25       +3     
Flag Coverage Δ
openfeature 100.00% <100.00%> (ø)

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.

…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 module of string constants to a small
value object with `kind` (the discriminator) and optional `message`.
Frozen singletons for the no-detail reasons (flag_not_found,
no_rollout_match, not_ready); factory methods for the ones that
carry detail (missing_context_key(key), backend_error(message)).

SDK-83: RemoteFlagsProvider's rescue MixpanelError now tags the
fallback with FallbackReason.backend_error(e.message) so the wrapper
can forward the backend's response into ResolutionDetails#error_message.
Without this the caller sees a bare GENERAL error and has to dig
through logs to find out the backend rejected the request (e.g.
"distinct_id must be provided in evalContext as a string"). Matches
Go's existing behavior — Python and Node will follow in their PRs.

Wrapper dispatches on reason.kind, forwards reason.message into
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:57
The wrapper short-circuits to PROVIDER_NOT_READY at the top of
resolve when flags_ready? is false, so no producer ever constructs
a FallbackReason with kind :not_ready — the case was dead, same
pattern Swift PR #745 / Android PR #981 / Python PR #180 cleaned up.

Remove :not_ready from the KINDS array, drop the not_ready factory
and NOT_READY singleton, and drop the :not_ready arm from the
wrapper's case dispatch. The 'provider not ready' spec covers the
short-circuit path 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>
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.

2 participants