Skip to content

feat(analytics): add Source discriminated union with Fallback.Reason (SDK-79)#89

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

feat(analytics): add Source discriminated union with Fallback.Reason (SDK-79)#89
tylerjroach wants to merge 4 commits into
masterfrom
fix/sdk-79-variant-source-fallback-reason

Conversation

@tylerjroach

Copy link
Copy Markdown
Contributor

Summary

  • New Source discriminated union (com.mixpanel.mixpanelapi.featureflags.model.Source): Source.Local | Source.Remote | Source.Fallback(Reason). Abstract class + nested static finals — Java 8 source level so no sealed keyword.
  • Source.Fallback.Reason enum: FLAG_NOT_FOUND | MISSING_CONTEXT_KEY | NO_ROLLOUT_MATCH | BACKEND_ERROR | NOT_READY. Constants align with mixpanel-php.
  • SelectedVariant.getSource() replaces the prior variantSource string field. isFallback() now checks source instanceof Source.Fallback.
  • LocalFlagsProvider and RemoteFlagsProvider tag every returned variant: matches with Source.local() / Source.remote(), fallbacks with Source.fallback(reason) carrying the specific reason.

Why

Three behaviorally distinct outcomes — flag-not-found, no-rollout-match, and missing-context-key — previously all returned the bare fallback. A future OpenFeature wrapper (in openfeature-provider/) can now dispatch on the reason and map each to the spec-correct error code instead of collapsing them all to FLAG_NOT_FOUND.

Discriminated union rather than two flat fields because the language is type-safe enough to model it that way; invalid states like "successful evaluation with a fallback reason" are unrepresentable.

Cross-SDK fix tracked at Linear SDK-79. Constant set matches mixpanel-php so callers across SDKs see the same vocabulary.

Follow-up PR

The openfeature-provider module needs a matching update to dispatch on Source.Fallback.Reason — that PR depends on this one shipping in a new mixpanel-java release first (the provider consumes the published Maven coordinate). It'll come right after release.

Test plan

  • 172 tests pass (mvn test -Dgpg.skip=true)
  • Existing LocalFlagsProviderTest (53) and RemoteFlagsProviderTest (8) continue passing — the isFallback() contract is preserved
  • SelectedVariant.equals() updated to include source
  • Wrapper PR pending base SDK release (Phase 2)

🤖 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 tdumitrescu June 29, 2026 15:13
@linear-code

linear-code Bot commented Jun 29, 2026

Copy link
Copy Markdown

SDK-79

@tylerjroach tylerjroach changed the title fix(flags): add Source discriminated union with Fallback.Reason (SDK-79) feat(analytics): add Source discriminated union with Fallback.Reason (SDK-79) Jun 29, 2026

@rahul-mixpanel rahul-mixpanel 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.

Four recommendations left as inline comments — one high, one medium, two low priority.

tylerjroach and others added 3 commits June 30, 2026 11:00
The OpenFeature wrapper short-circuits to PROVIDER_NOT_READY at the
top of resolve via the areFlagsReady check, so no producer ever
constructs a Source.Fallback with Reason.NOT_READY — the case was
dead, same pattern Swift PR #745 / Android PR #981 / Python PR #180
/ Ruby PR #153 / Node PR #277 cleaned up.

Trivial to add back the moment a real call site needs it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Source.Fallback now overrides equals/hashCode so two Fallback(REASON)
  instances compare structurally — otherwise SelectedVariant.equals()
  returns false for any two fallbacks with the same reason.
- Source subclasses get toString() (Local / Remote / Fallback(REASON))
  so SelectedVariant.toString() doesn't print Source\$Fallback@1a2b3c.
- Restore @param Javadoc on both 5-arg and 6-arg SelectedVariant
  constructors.
- Clarify isSuccess()'s source-based check vs the old variantKey-based
  check via a brief Javadoc note.

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

Copy link
Copy Markdown
Contributor Author

Thanks for the review — pushed a58a93d. Threads addressed:

  1. HIGH — Source.Fallback equals/hashCode (line 82): Added. Two Source.fallback(REASON) instances now compare structurally so SelectedVariant.equals() works correctly. Local / Remote stay singleton, so reference equality is structural equality for them.
  2. MEDIUM — isSuccess / isFallback semantic change (SelectedVariant.java:80): Kept the source-based check — the Source is the canonical source-of-truth for whether something is a fallback, and variantKey != null was a proxy that breaks if a caller hands a key-bearing object as the fallback. Added a Javadoc note explaining the intent.
  3. LOW — NOT_READY unused (line 73): Removed entirely in 4e4d08c — the wrapper handles PROVIDER_NOT_READY via areFlagsReady() before invoking the provider, so no producer would construct it. Same cleanup landed across the other server SDKs (Python / Ruby / Node / Go).
  4. LOW — Source.toString() (line 56): Added on all three subclasses. Now prints Local / Remote / Fallback(FLAG_NOT_FOUND) instead of Source$Fallback@1a2b3c.
  5. LOW — Missing @param Javadoc (SelectedVariant.java:46): Restored on both the 5-arg and 6-arg constructors.

Mirrored on mixpanel-android (PR #981, commit 9f89b3c0) — same Java-class structure had the same equals/toString gap on Persistence and Fallback. Other SDKs aren't affected: Swift uses if case pattern matching (no Equatable needed); Python / Ruby / Node / Go use structural value types that already compare correctly.

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