Add feature flag system with rollouts, plan gating, and back-office admin#896
Merged
Conversation
…mestamps, and contextual toasts
…ure flag responses
…ings verification
…ble groups, Account rename, and user-scoped detail
…ilters on feature-flag pages
…ice and user-facing surfaces
…stead of bootstrap meta
…4-3, T-R4-4, T-R4-6)
Closed
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary & Motivation
Introduce a full feature flag system spanning the backend (definitions, persistence, evaluator, reconciler), authentication plumbing (JWT refresh, cookie middleware, YARP transform), the SPA infrastructure (typed hook, header-driven cache), the back-office admin surface, and the user-facing settings panels. Flag state is computed server-side at token refresh and propagated to the SPA via the
x-user-feature-flagsresponse header, eliminating per-render polling and keeping evaluation consistent across refreshes, navigations, and cross-tab sessions.Two of the declared flags are load-bearing:
google-oauthgates the OpenID Connect sign-in path andsubscriptionstoggles the Stripe billing surface. The rest (beta-features,sso,account-overview,compact-view,experimental-ui) are illustrative — they demonstrate each subtype and exercise the back-office UI but aren't part of the framework. Downstream products should delete the illustrative ones and declare their own.Definitions and registry
public static readonly FeatureFlagDefinitionfields onSharedKernel.FeatureFlags.FeatureFlags. The registry is reflected from those fields at startup, so adding a flag is a one-line declaration with no manual array maintenance. Definitions live inFeatureFlags.cs; the reflection/validation mechanism lives inFeatureFlagsRegistry.cs(partial class) so the file developers edit stays free of plumbingSystemFeatureFlag,TenantAbTestFlag,UserAbTestFlag,PlanGatedTenantFlag,TenantOwnerConfigurableFlag,UserConfigurableFlag— enforce every cross-property invariant (scope, configurability, AB eligibility, plan tier, kill-switch, stable-module) at compile time.trackInTelemetryandisKillSwitchEnabledare required parameters on every subtype so the decisions are explicit at the call siteGenerateFeatureFlagsManifestMSBuild target which serializes the registry to JSON and then runs a Node script that produceslabels.generated.ts(Linguitmacros for every flag's Label and Description) andregistry.generated.ts(typed runtime registry used byuseFeatureFlag<Key>()). The result: the SAME flag declaration becomes a strongly-typed key union on both sides — removing a flag in C# raises a TypeScript compile error in every SPA call site — and Lingui's extractor picks up the generatedtmacros so each flag's English text lands in the shared.pocatalogue ready for translatorsSchema and bucket assignment
feature_flagstable holds both the global base row per flag and the override rows scoped to tenants and users, deduplicated by a unique(flag_key, tenant_id, user_id)index withNULLS NOT DISTINCTrollout_bucketcolumns are added totenantsandusersand back-filled via a one-shot van der Corput sequence so even a 1% rollout is evenly distributed from day one; subsequent inserts pull from dedicated Postgres sequences to remain race-safe under concurrent signupsEvaluation
FeatureFlagEvaluatorruns at JWT refresh and returns the keys enabled for the (tenant, user) pair. Precedence per flag: manual per-flag override > entity-global A/B inclusion pin > rollout bucket range > plan tier > default offRolloutBucketHasherderives a stable per-flag starting bucket from the flag key and handles the wrap-around case (e.g. range90..10)AlwaysOnforces inclusion regardless of rollout percentage,NeverOnforces exclusion regardless. Use them to escape-hatch a tenant or user out of (or into) a rollout decision without changing the global percentageReconciliation, soft-delete, and orphan handling
FeatureFlagDefinitionReconcilerruns once at startup and converges the database to the C# definitions: missing base rows get inserted, rows whose key was removed from code are marked orphaned, and re-adding the same key auto-restores the base row plus any orphaned tenant/user overridesPlan-gated flags
PlanGatedTenantFlagties a flag to aPlanTier(Basis / Standard / Premium). A background subscription-state evaluator writes plan-driven rows on upgrade and clears them on downgrade, so feature gating survives subscription changes without the customer-facing app having to know about feature flagsAuthentication and header propagation
AuthenticationCookieMiddlewareand the YARP response transform were merged so cookie refresh andx-user-feature-flagsemission happen sequentially in one hook, eliminating the race the split design had between cookie swap and flag evaluation. The refresh token'sjtiis rotated on every inline refresh so the next endpoint-triggered refresh uses a fresh identifierAuthenticationProvider(shared-webapp) reads the header from every authenticated response and pushes it into a React state slice;useFeatureFlagreads straight from that state instead of polling the serverx-user-feature-flagsheader rather than emit a stale claim — the SPA keeps its previous state and reconciles on the next successful refreshBack-office admin UI
Self-service UI
/account/settingsFeatures panel surfaces everyTenantOwnerConfigurableFlagto tenant owners; non-owners see a read-only stateUserConfigurableFlaginstances so each user can toggle preferences independently of the tenantTelemetry
FeatureFlagOverrideTriggeraxis (Internal/Owner/Self) so dashboards can attribute every change to its originator. Plan-source transitions emit their own dedicatedFeatureFlagPlanOverrideActivated/DeactivatedeventsOpenTelemetryEnricherandApplicationInsightsTelemetryInitializeremit a single comma-separateduser.feature_flagsdimension carrying everyTrackInTelemetryflag the user has enabled, so dashboards can group-by feature flag with native KQL. Thefeature_flag.*namespace is intentionally avoided because OpenTelemetry reserves itChecklist