feat(feature-flags): AppConfig-backed gated feature flags#5059
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Adds a new Operator/docs support: Claude/Cursor Reviewed by Cursor Bugbot for commit c82faeb. Bugbot is set up for automated code reviews on this repo. Configure here. |
…ked-feature-flag # Conflicts: # apps/sim/app/(auth)/components/oauth-provider-checker.tsx # apps/sim/app/(auth)/signup/page.tsx # apps/sim/app/api/webhooks/agentmail/route.ts # apps/sim/ee/access-control/utils/permission-check.ts # apps/sim/lib/copilot/tools/handlers/function-execute.ts # apps/sim/lib/copilot/tools/server/files/touch-plan.test.ts # apps/sim/lib/copilot/tools/server/files/touch-plan.ts # apps/sim/lib/copilot/tools/server/router.ts # apps/sim/lib/core/config/feature-flags.ts # apps/sim/lib/core/security/input-validation.server.ts # apps/sim/lib/table/service.ts # apps/sim/lib/webhooks/processor.ts
…a per-flag secret, gating is AppConfig-only
…g requires a fallback secret
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0853240. Configure here.
| parseConfig | ||
| ) | ||
|
|
||
| return value ?? fallbackFlags() |
There was a problem hiding this comment.
Hosted null uses env fallback
Medium Severity
When isAppConfigEnabled is true, getFeatureFlags still returns fallbackFlags() if fetchAppConfigProfile yields null (unseeded profile, cold-start failure, or empty payload). That path turns registered fallback env secrets into globally enabled rules, bypassing AppConfig org/user/admin gating—the opposite of the documented model where secrets apply only when AppConfig is off.
Reviewed by Cursor Bugbot for commit 0853240. Configure here.
…y defines name, description, and fallback in one place
…eof env), resolved to a boolean
|
@greptile review |
Greptile SummaryThis PR introduces a runtime, AppConfig-backed feature flag system (
Confidence Score: 4/5The core flag engine is well-structured and the AppConfig caching layer is solid; the rename sweep is mechanically clean and TypeScript confirms all import paths resolve. The new feature-flag module is a pure addition with no flags registered yet, so there is no production gating logic to regress. The 160+ import renames are mechanical and verified by tsc. The one open question is that normalizeRule stores empty rule objects ({}) for AppConfig entries whose keys are all unrecognized, which could silently swallow operator typos; it evaluates to false (safe), but the discarded config is invisible without server-side logging. apps/sim/lib/core/config/feature-flags.ts — normalizeRule empty-object handling is worth a second look before any real flags are deployed. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller
participant isFeatureEnabled
participant getFeatureFlags
participant fetchAppConfigProfile
participant evaluate
participant isPlatformAdmin
Caller->>isFeatureEnabled: "isFeatureEnabled(flag, { userId, orgId })"
isFeatureEnabled->>getFeatureFlags: getFeatureFlags()
alt isAppConfigEnabled
getFeatureFlags->>fetchAppConfigProfile: fetchAppConfigProfile(ids, parseConfig)
note over fetchAppConfigProfile: Stale-while-revalidate cache (~30s TTL)
fetchAppConfigProfile-->>getFeatureFlags: "FeatureFlagsConfig | null"
else fallback path
getFeatureFlags-->>getFeatureFlags: fallbackFlags() from env secrets
end
getFeatureFlags-->>isFeatureEnabled: "{ flags }"
isFeatureEnabled->>evaluate: evaluate(flags[flag], ctx)
note over evaluate: 1. rule.enabled?
note over evaluate: 2. userId in userIds?
note over evaluate: 3. orgId in orgIds?
alt "rule.admins && no earlier match"
evaluate->>isPlatformAdmin: isPlatformAdmin(userId) via dbReplica
isPlatformAdmin-->>evaluate: boolean
end
evaluate-->>isFeatureEnabled: boolean
isFeatureEnabled-->>Caller: boolean
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Caller
participant isFeatureEnabled
participant getFeatureFlags
participant fetchAppConfigProfile
participant evaluate
participant isPlatformAdmin
Caller->>isFeatureEnabled: "isFeatureEnabled(flag, { userId, orgId })"
isFeatureEnabled->>getFeatureFlags: getFeatureFlags()
alt isAppConfigEnabled
getFeatureFlags->>fetchAppConfigProfile: fetchAppConfigProfile(ids, parseConfig)
note over fetchAppConfigProfile: Stale-while-revalidate cache (~30s TTL)
fetchAppConfigProfile-->>getFeatureFlags: "FeatureFlagsConfig | null"
else fallback path
getFeatureFlags-->>getFeatureFlags: fallbackFlags() from env secrets
end
getFeatureFlags-->>isFeatureEnabled: "{ flags }"
isFeatureEnabled->>evaluate: evaluate(flags[flag], ctx)
note over evaluate: 1. rule.enabled?
note over evaluate: 2. userId in userIds?
note over evaluate: 3. orgId in orgIds?
alt "rule.admins && no earlier match"
evaluate->>isPlatformAdmin: isPlatformAdmin(userId) via dbReplica
isPlatformAdmin-->>evaluate: boolean
end
evaluate-->>isFeatureEnabled: boolean
isFeatureEnabled-->>Caller: boolean
|


Summary
lib/core/config/feature-flags.ts: runtime feature flags gated by org id, user id, or admin — AppConfig-backed on prod (reusesfetchAppConfigProfile), with an in-fileDEFAULT_FEATURE_FLAGSfallback off-prod (no env var)feature-flags.ts→env-flags.ts(it was never gated flags); rewrote all importers + the shared test mock (featureFlagsMock→envFlagsMock)isPlatformAdmin(userId)reading the DB replica (dbReplica); only queries whenadminsis the deciding clauseadd-feature-flagskill for Claude + Cursorfeature-flagsAppConfig ConfigurationProfile alongsideaccess-controlType of Change
Testing
Tested manually. New
feature-flags.test.ts(13 tests) + renamed-mock consumers pass.bun run lint,check:api-validation:strict,tsc --noEmit, andcdk synthall clean.Checklist