Skip to content

feat(flags): accept a caller default in FeatureFlagEvaluations.IsEnabled - #344

Merged
ioannisj merged 1 commit into
mainfrom
posthog/flag-evaluations-is-enabled-default-value
Oct 22, 2026
Merged

ioannisj merged 1 commit into
mainfrom
posthog/flag-evaluations-is-enabled-default-value

Conversation

@posthog

@posthog posthog Bot commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Contract: is-feature-enabled — "Canonical is-feature-enabled behavior".

The SDK SHALL accept a caller-supplied boolean default (defaultValue) and SHALL return it whenever the flag has no value: flags not loaded yet, a failed flags request, or no flag with that key in the loaded flags. A flag that has a value — including false and variant strings — always wins over the caller-supplied default.

The posthog-dotnet compliance matrix records this as ❌ Fail (note n20):

FeatureFlagEvaluations.IsEnabled(key) returns record is { Enabled: true } — a genuinely-disabled flag and a flag with no value at all are indistinguishable, and there is no way for a caller to override a miss to true.

Remediation: Add FeatureFlagEvaluations.IsEnabled(string key, bool defaultValue) that returns defaultValue only on a genuine miss, distinguishing it from a real false result.

Other SDKs already have this: posthog-ruby (#285), posthog-js/node (#4484), posthog-go (#349).

🔨 What changed

  • New overload FeatureFlagEvaluations.IsEnabled(string key, bool defaultValue). It still records snapshot access and fires $feature_flag_called exactly as before, then returns record is null ? defaultValue : record.Enabled.
  • The existing IsEnabled(string key) now delegates with defaultValue: false, so a present-but-disabled flag returns false and a missing flag returns false — byte-for-byte the old behavior.

Why this is backwards-compatible

It is purely additive via C# overloading. No existing signature, return type, or value changes: the one-argument overload's observable behavior is identical, and the new overload only exists once a caller opts in by passing a default. The PublicAPI.Unshipped.txt diff is a single added line.

💚 How did you test it?

  • Added three cases to TheSnapshotAccessMethods in tests/UnitTests/Features/FeatureFlagEvaluationsTests.cs, mirroring the spec's scenarios: a missing flag resolves to the caller default (false and true), and a present false / variant-a flag wins over an opposing default.
  • The existing UnknownKeyReturnsFalsyValue test continues to pin the no-default behavior.
  • dotnet test tests/UnitTests/UnitTests.csproj — 1239 passed / 0 failed on net8.0, 1229 passed / 0 failed on netcoreapp3.1.
  • Added a minor changeset for PostHog.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

👣 Follow-up work (not in this PR)

Compliance note n20 also flags that IsFeatureEnabledAsync(featureKey, distinctId, options) has no defaultValue on its options object, and that an XML doc on a bool?-returning extension overload claims null-on-miss semantics it does not implement. Both were left out to keep this change to the single canonical surface; they are worth a separate pass.

🤖 Agent context

Autonomy: Fully autonomous

Opened by a scheduled PostHog agent run that reads the PostHog/sdk-specs compliance matrices and implements one backwards-compatible gap per run. This gap was picked over the other open posthog-dotnet gaps because it is a hard SHALL on a widely-used, user-facing flag API, the remediation is concrete and self-contained, and the fix is additive. Candidates in other SDKs for the same contract were skipped as already merged or in flight. Verified locally with the repo's unit test suite; no manual/integration testing was performed.


Created with PostHog Desktop

🤖 Generated with Claude Code

Adds an `IsEnabled(string key, bool defaultValue)` overload that returns the
caller-supplied default when the flag has no value in the snapshot, while a
flag that has a value — including `false` — still wins over the default.

The existing single-argument overload delegates with `defaultValue: false`,
so its behavior is unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>

Generated-By: PostHog Desktop
Task-Id: bcbae7c3-319c-4322-aacc-a2838b43287d
@github-actions

Copy link
Copy Markdown
Contributor

posthog-dotnet Compliance Report

Date: 2026-10-11T06:09:28.797632+00:00
Duration: 125036ms

✅ All Tests Passed!

47/47 tests passed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 264ms
Format Validation.Event Has Uuid ✅ 129ms
Format Validation.Event Has Lib Properties ✅ 116ms
Format Validation.Distinct Id Is String ✅ 115ms
Format Validation.Token Is Present ✅ 111ms
Format Validation.Custom Properties Preserved ✅ 118ms
Format Validation.Event Has Timestamp ✅ 111ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 123ms
Retry Behavior.Retries On 503 ✅ 8124ms
Retry Behavior.Does Not Retry On 400 ✅ 2118ms
Retry Behavior.Does Not Retry On 401 ✅ 2114ms
Retry Behavior.Respects Retry After Header ✅ 8119ms
Retry Behavior.Implements Backoff ✅ 22133ms
Retry Behavior.Retries On 500 ✅ 6118ms
Retry Behavior.Retries On 502 ✅ 6118ms
Retry Behavior.Retries On 504 ✅ 6114ms
Retry Behavior.Max Retries Respected ✅ 22136ms
Deduplication.Generates Unique Uuids ✅ 118ms
Deduplication.Preserves Uuid On Retry ✅ 6118ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 13119ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 6120ms
Deduplication.No Duplicate Events In Batch ✅ 116ms
Deduplication.Different Events Have Different Uuids ✅ 111ms
Compression.Sends Gzip When Enabled ✅ 109ms
Batch Format.Uses Proper Batch Structure ✅ 109ms
Batch Format.Flush With No Events Sends Nothing ✅ 105ms
Batch Format.Multiple Events Batched Together ✅ 115ms
Error Handling.Does Not Retry On 403 ✅ 2112ms
Error Handling.Does Not Retry On 413 ✅ 2111ms
Error Handling.Retries On 408 ✅ 6118ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 150ms
Request Payload.Flags Request Uses V2 Query Param ✅ 110ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 110ms
Request Payload.Flags Request Omits Authorization Header ✅ 110ms
Request Payload.Token In Flags Body Matches Init ✅ 111ms
Request Payload.Groups Round Trip ✅ 114ms
Request Payload.Groups Default To Empty Object ✅ 110ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 111ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 111ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 110ms
Request Lifecycle.No Flags Request On Init Alone ✅ 4ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 108ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 215ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 113ms
Retry Behavior.Retries Flags On 502 ✅ 1116ms
Retry Behavior.Retries Flags On 504 ✅ 1113ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 213ms

@ioannisj ioannisj self-assigned this Oct 12, 2026
@ioannisj

Copy link
Copy Markdown
Contributor

🤖 An AI agent wrote this on behalf of @ioannisj.

⚠️ PUBLIC API CHANGE
FeatureFlagEvaluations.IsEnabled(string key, bool defaultValue) — added (src/PostHog/Features/FeatureFlagEvaluations.cs:95)
Changeset level: minor

Checked the miss paths (unknown key, a failed /flags request, a per-flag failed: true) against posthog-js node's isEnabled(key, { defaultValue }). Same semantics, $feature_flag_called still fires on a miss, and the one-arg IsEnabled(key) behaves exactly as before, so nothing to fix from my side.

One small thing for whoever touches this file next: the <see cref="IsEnabled"/> in the OnlyAccessed doc is now ambiguous between the two overloads. It only warns (CS0419) when XML docs get generated, which we don't do today, so IsEnabled(string) would pin it as a drive-by.

@ioannisj
ioannisj requested review from a team and removed request for a team October 12, 2026 18:22
@ioannisj
ioannisj marked this pull request as ready for review October 17, 2026 01:19
@ioannisj
ioannisj merged commit 79f12cf into main Oct 22, 2026
20 checks passed
@ioannisj
ioannisj deleted the posthog/flag-evaluations-is-enabled-default-value branch October 22, 2026 08:15
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