Skip to content

feat!: flagsmith eval - #53

Open
khvn26 wants to merge 10 commits into
mainfrom
feat/evaluate
Open

feat!: flagsmith eval#53
khvn26 wants to merge 10 commits into
mainfrom
feat/evaluate

Conversation

@khvn26

@khvn26 khvn26 commented Jul 29, 2026

Copy link
Copy Markdown
Member

Closes #48.

flagsmith eval (also evaluate) shows what a Flagsmith SDK would return for the current environment.

RFC §12.

git clone -b feat/evaluate https://github.com/Flagsmith/flagsmith-cli.git
cd flagsmith-cli
go build -o flagsmith .

export FLAGSMITH_ENVIRONMENT_KEY=<a client-side or ser. key>
./flagsmith eval
./flagsmith eval --identity user-123 --trait plan=premium --trait age=42
./flagsmith eval onboarding --test && echo "on"
./flagsmith eval --js > state.json

Notable changes

  • flag list --identity becomes --identities, and flag list refuses a stray positional instead of ignoring it
  • -i addresses the identity on every command that takes one: flag get, flag update, flag delete, flag enable, flag disable, eval. eval --identity also accepts --identifier, hidden, since the flag commands spell it that way
  • CLI's User-Agent cannot be overridden

khvn26 added 5 commits July 28, 2026 21:21
Evaluation runs through the Flagsmith Go SDK against the SDK API, so the
answer is the one an application would get: segment overrides applied,
variants allocated, an identity and its traits overlaid.

The SDK evaluates over the CLI's own transport, which brings retries,
credential-stripping redirects and FLAGSMITH_DEBUG tracing with it. Its
User-Agent is now stamped over whatever a request carries rather than only
filling in a missing one, so a library that sets its own still reaches the
API as the CLI. Its logger is discarded — it writes debug lines straight to
os.Stderr, past the command's writers.

Identity evaluation is a read-only what-if: `transient: true` unless
--persist says otherwise, so neither the identity nor the traits are stored.
Traits without an identity evaluate the anonymous identity.

The environment context may name the environment rather than carry its key,
for `evaluate` and `api --sdk` alike, as it does for every Admin command.
The local name cache turns a name into a key for free, so the common case
reaches the SDK API with no Admin credential at all; only a name the cache
has never seen costs a lookup, and that lookup seeds the cache. A ref of 22
alphanumerics is taken for a key without consulting anything — that is what
the backend mints, and a key nothing has cached yet must not need
credentials. The cache spans an instance rather than a project, so only a
unique name match short-circuits; anything else defers to the Admin API,
which scopes the name properly.

beep boop
`evaluate --json` now emits the result contract every Flagsmith SDK generates
its result type from — sdk/evaluation-result.json — with a `$schema` naming it,
so what the CLI prints and what an SDK resolves are the same document. It is a
subset for now: `segments` and per-flag `reason` are required by the schema but
returned by no SDK API endpoint, and `variant` is on the wire for an identity
evaluation but discarded by the Go SDK before we see it. All three are absent
rather than invented, and arrive as they become available.

`--js` writes what a frontend SDK hydrates from instead: `api` and `flags`, and
deliberately nothing else. An `evaluationContext` there would have to carry the
environment key — a secret when it is a server-side one, and an app passes its
own anyway — and `evaluationEvent`, `ts` and `identity` are bookkeeping an SDK
fills in for itself. An empty environment still writes its state, with a warning:
an SDK given no flags waits for a fetch a hydrating app never makes.

Naming a feature prints that flag alone, so `--jq .value` reads it. `--js`
describes a whole environment and refuses one: a single-flag state would tell an
app the environment has a single flag.

RFC §12 updated to match.

beep boop
`flagsmith eval <feature> --test` turns a lookup into a check: a disabled flag
becomes a failure like any other — exit 1, reason on stderr — so a script can
gate on it with `&& `. The flag still prints first, so whoever ran the check can
see the state that failed it.

The error names the flag and nothing else. Everywhere else the CLI would name the
environment too, but eval never resolves one: it holds only the key, which is a
secret when it is a server-side one and must stay out of stderr and CI logs. It
carries no hint either — a disabled flag is the answer, not something to recover
from.

RFC §12 updated to match.

beep boop
Also drops the Design section: it linked twelve docs/design/*.md files that
92c69d6 deleted when the Notion RFC became the single source of truth.

beep boop
Every command that addresses one identity now takes `-i` — `flag get`,
`flag update`, `flag delete`, `flag enable`, `flag disable`, and `eval`. `eval`
also accepts `--identifier` as a hidden alias, because the flag commands spell it
that way and nobody should have to remember which command uses which.

`flag list --identity` becomes `--identities`. It is a switch selecting a
dimension to list, not a value naming one, and the plural says so; `--segment
<id>` stays singular because it does name one. The old spelling taught the wrong
shape — `flag list --identity user-1` parses `--identity` as the boolean it is
and leaves `user-1` behind as a positional.

Which `flag list` then silently ignored, listing every flag in the environment: a
command with no Args validator inherits cobra's ArbitraryArgs. It now refuses a
stray argument and exits 2.

RFC §7.3 updated to match.

beep boop
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@khvn26, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f4bdee71-1aac-4be7-a7c4-6939ca19de89

📥 Commits

Reviewing files that changed from the base of the PR and between 1c08972 and b076909.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • go.mod
  • internal/cmd/cmd_test.go
  • internal/cmd/evaluate.go
📝 Walkthrough

Walkthrough

Adds a new flagsmith evaluate/eval command supporting environment and identity flag evaluation, traits, persistence, JSON, SDK hydration, feature testing, and environment-name resolution. Adds SDK request fixtures and extensive evaluation tests. Updates CLI identity flag naming and shorthand support, strengthens User-Agent handling, adds SDK-related dependencies and error hints, and documents the new command.

Estimated code review effort: 4 (Complex) | ~45 minutes

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The SDK was left recorded as an indirect dependency: it was added with
`go get` after a `go mod tidy` had dropped it, at a point when nothing imported
it yet, and never re-tidied once `internal/cmd/evaluate.go` did. Tidying also
records the three transitive checksums that were missing from go.sum, which is
what CI's `go mod tidy -diff` was failing on.

beep boop

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

52-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document --sdk-api-url alongside --api-url.

eval talks to the SDK API, and the new error hints point users at --sdk-api-url / sdkApiUrl, but the Conventions section only names --api-url/FLAGSMITH_API_URL. Self-hosted users following the README will miss the setting the new command depends on.

📝 Proposed addition
-- Self-hosted: `--api-url` or `FLAGSMITH_API_URL`.
+- Self-hosted: `--api-url` or `FLAGSMITH_API_URL`; `--sdk-api-url` (or `sdkApiUrl`) for the SDK API that `eval` evaluates against.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9d25ff54-7e37-44a8-8b8f-36b8ed90c1f3

📥 Commits

Reviewing files that changed from the base of the PR and between 93ecd2d and 1c08972.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • README.md
  • go.mod
  • internal/cmd/api.go
  • internal/cmd/cmd_test.go
  • internal/cmd/errors.go
  • internal/cmd/evaluate.go
  • internal/cmd/flag_update.go
  • internal/cmd/flags.go
  • internal/cmd/resolve.go
  • internal/httpx/client.go
  • internal/httpx/client_test.go

Comment thread internal/cmd/cmd_test.go
Comment thread internal/cmd/cmd_test.go
Comment thread internal/cmd/cmd_test.go
Comment thread internal/cmd/cmd_test.go
Comment on lines +6020 to +6024
t.Run("the invocation deadline bounds evaluation", func(t *testing.T) {
// Given an SDK API slower than the deadline the user asked for.
f := evalEnv(t)
f.sdkDelay = 1200 * time.Millisecond
t.Setenv("FLAGSMITH_TIMEOUT", "1")

@coderabbitai coderabbitai Bot Jul 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unsynchronised SDK fixture mutations in the new evaluation tests. The new SDK handlers (lines 1309-1350) read sdkStatus, sdkDelay, sdkEnvFlags and sdkIdentityFlags under f.mu, but the new tests set them from the test goroutine without the lock — the same omission repeated in three places. Either guard each write with f.mu, or add a small f.configureSDK(...) helper that does it once.

  • internal/cmd/cmd_test.go#L6020-L6024: wrap the f.sdkDelay assignment in f.mu.Lock()/Unlock().
  • internal/cmd/cmd_test.go#L6394-L6397: wrap the f.sdkStatus assignment in f.mu.Lock()/Unlock().
  • internal/cmd/cmd_test.go#L6429-L6430: wrap the sdkEnvFlags delete/assign (and the sdkIdentityFlags writes in TestEvaluateIdentity) in f.mu.Lock()/Unlock().
📍 Affects 1 file
  • internal/cmd/cmd_test.go#L6020-L6024 (this comment)
  • internal/cmd/cmd_test.go#L6394-L6397
  • internal/cmd/cmd_test.go#L6429-L6430

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't care about broken concurrency in test code. --race should ensure it's adequate.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@khvn26, understood. I’ll treat race-detector coverage as sufficient here and won’t pursue test-only synchronization changes.

🐇 hops quietly onward


✏️ Learnings added
Learnt from: khvn26
Repo: Flagsmith/flagsmith-cli PR: 53
File: internal/cmd/cmd_test.go:6020-6024
Timestamp: 2026-07-29T09:59:44.020Z
Learning: For the Flagsmith CLI repository, khvn26 considers `go test --race` adequate for test-only concurrency concerns and does not want additional synchronization changes solely for test fixtures.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread internal/cmd/evaluate.go
Comment thread internal/cmd/evaluate.go
khvn26 added 4 commits July 29, 2026 10:29
newSDKClient rebuilt the string sdkAPIBase already returns, so the URL the SDK
evaluates against and the `api` field `--js` writes could have drifted apart —
including the trailing slash, which the JS SDK concatenates onto without
normalising.

beep boop
captureOSStderr swapped os.Stderr back on the happy path only. A t.Fatal inside
the captured func — which is exactly what the callers do on error — leaves the
process's stderr pointing at a closed pipe for the rest of the package run,
hiding the output of every test after it.

beep boop
…output

The subtest checked the command's output and the hint. The output does contain
the rendered error — cobra prints it to the same buffer — but asserting on
err.Error() directly stops the guarantee resting on that.

beep boop
Only the flags endpoint slept, so a deadline test written against the identity
path would have passed without ever exercising the delay it set.

beep boop
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.

Implement flagsmith eval

1 participant