Skip to content

feat: add scope configuration for feature opt-in#125

Open
tomerqodo wants to merge 8 commits into
qodo_claude_vs_qodo_base_feat_add_scope_configuration_for_feature_opt-in_pr14from
qodo_claude_vs_qodo_head_feat_add_scope_configuration_for_feature_opt-in_pr14
Open

feat: add scope configuration for feature opt-in#125
tomerqodo wants to merge 8 commits into
qodo_claude_vs_qodo_base_feat_add_scope_configuration_for_feature_opt-in_pr14from
qodo_claude_vs_qodo_head_feat_add_scope_configuration_for_feature_opt-in_pr14

Conversation

@tomerqodo

Copy link
Copy Markdown

Benchmark PR from agentic-review-benchmarks#14

devin-ai-integration Bot and others added 8 commits January 25, 2026 12:03
Add scope field to OptInFeatureConfig that allows features to be scoped
to specific levels (org, team, user). This enables features to be shown
only at certain settings pages rather than all three.

Changes:
- Add OptInFeatureScope type with values 'org', 'team', 'user'
- Add optional scope field to OptInFeatureConfig interface
- Add getOptInFeaturesForScope helper function to filter features by scope
- Update FeatureOptInService to filter features based on scope
- Update tRPC router to pass scope parameter for org/team endpoints

Features without a scope field default to all scopes for backward compatibility.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
…tate

- Add isFeatureAllowedForScope helper function to check if a feature is allowed for a scope
- Update setUserFeatureState to reject if feature is not scoped to 'user'
- Update setTeamFeatureState to accept scope parameter and reject if feature is not allowed
- Update tRPC router to pass scope parameter for team and org endpoints
- Fix unit test mock to include new config exports

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
- Replace raw Error with ErrorWithCode using ErrorCode.BadRequest
- Add comprehensive tests for setUserFeatureState scope validation
- Add comprehensive tests for setTeamFeatureState scope validation
- Test both enabled/disabled and inherit state scenarios
- Test error messages include feature ID and scope name

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
…on tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add scope configuration for feature opt-in with validation

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Add scope configuration to feature opt-in system enabling features to be scoped to org/team/user
  levels
• Implement scope validation in setUserFeatureState and setTeamFeatureState with ErrorWithCode
• Add comprehensive unit tests for scope validation across user and team feature state operations
• Update settings menu visibility to use scope-specific feature availability checks
• Filter features by scope in listFeaturesForUser and listFeaturesForTeam methods
Diagram
flowchart LR
  A["OptInFeatureConfig"] -->|"add scope field"| B["OptInFeatureScope type"]
  B -->|"org/team/user"| C["Feature Scoping"]
  C -->|"filter by scope"| D["getOptInFeaturesForScope"]
  D -->|"validate access"| E["isFeatureAllowedForScope"]
  E -->|"throw ErrorWithCode"| F["setUserFeatureState"]
  E -->|"throw ErrorWithCode"| G["setTeamFeatureState"]
  H["Settings Menu"] -->|"use scope checks"| I["HAS_USER/TEAM/ORG_OPT_IN_FEATURES"]
Loading

Grey Divider

File Changes

1. packages/features/feature-opt-in/config.ts ✨ Enhancement +38/-1

Add scope configuration and filtering helpers

• Add OptInFeatureScope type import and ALL_SCOPES constant
• Add optional scope field to OptInFeatureConfig interface with documentation
• Add three scope-specific feature availability constants (HAS_USER/TEAM/ORG_OPT_IN_FEATURES)
• Add getOptInFeaturesForScope helper function to filter features by scope
• Add isFeatureAllowedForScope helper function to validate feature access for a scope
• Include unused constant that should be caught by linting

packages/features/feature-opt-in/config.ts


2. packages/features/feature-opt-in/types.ts ✨ Enhancement +9/-0

Define OptInFeatureScope type

• Add OptInFeatureScope type definition with org/team/user values
• Add comprehensive documentation explaining scope levels and their purposes

packages/features/feature-opt-in/types.ts


3. packages/features/feature-opt-in/services/FeatureOptInService.ts ✨ Enhancement +43/-15

Add scope validation and filtering to service

• Import ErrorCode and ErrorWithCode for scope validation errors
• Import scope-related helper functions from config
• Update listFeaturesForUser to filter features scoped to user level
• Update listFeaturesForTeam to accept optional scope parameter and filter accordingly
• Add scope validation to setUserFeatureState throwing ErrorWithCode on invalid scope
• Add scope validation to setTeamFeatureState with scope parameter defaulting to team
• Fix logic error in setUserFeatureState condition (state !== inherit instead of state === inherit)

packages/features/feature-opt-in/services/FeatureOptInService.ts


View more (5)
4. packages/features/feature-opt-in/services/IFeatureOptInService.ts ✨ Enhancement +14/-7

Update interface signatures for scope support

• Import OptInFeatureScope type
• Add scope parameter to listFeaturesForTeam method signature
• Add scope parameter to setTeamFeatureState method signature
• Reformat method signatures for consistency

packages/features/feature-opt-in/services/IFeatureOptInService.ts


5. packages/features/feature-opt-in/services/FeatureOptInService.test.ts 🧪 Tests +233/-8

Add comprehensive scope validation tests

• Add mock features with different scope configurations (org-only, team-only, user-only)
• Add mockIsFeatureAllowedForScope function for testing scope validation
• Update mock config to export scope-related helper functions
• Add comprehensive test suite for setUserFeatureState with scope validation
• Add comprehensive test suite for setTeamFeatureState with scope validation
• Test both enabled/disabled and inherit state scenarios
• Test error messages include feature ID and scope name
• Update existing test expectations to include all mock features

packages/features/feature-opt-in/services/FeatureOptInService.test.ts


6. packages/features/feature-opt-in/services/FeatureOptInService.integration-test.ts 🧪 Tests +11/-1

Mock scope validation in integration tests

• Add vi import from vitest
• Mock isFeatureAllowedForScope to always return true for integration tests
• Add comment explaining that scope validation is tested in unit tests

packages/features/feature-opt-in/services/FeatureOptInService.integration-test.ts


7. packages/trpc/server/routers/viewer/featureOptIn/_router.ts ✨ Enhancement +6/-3

Pass scope parameters to service methods

• Pass scope parameter to listFeaturesForTeam for team endpoint
• Pass scope org to listFeaturesForTeam for organization endpoint
• Add scope team parameter to setTeamFeatureState call
• Add scope org parameter to setTeamFeatureState call for organization endpoint
• Remove await keyword from setUserFeatureState call

packages/trpc/server/routers/viewer/featureOptIn/_router.ts


8. apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx ✨ Enhancement +8/-4

Update settings menu visibility with scope checks

• Replace HAS_OPT_IN_FEATURES with scope-specific constants
• Use HAS_USER_OPT_IN_FEATURES for user account features menu
• Use HAS_ORG_OPT_IN_FEATURES for organization features menu
• Use HAS_TEAM_OPT_IN_FEATURES for team features menu visibility

apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Mar 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. HAS_USER_OPT_IN_FEATURES line too long 📘 Rule violation ✓ Correctness
Description
New constants are written as single lines exceeding the 110 character line-width requirement, which
breaks the standardized Biome formatting rules. This will cause formatter/lint churn and
inconsistent code style.
Code

packages/features/feature-opt-in/config.ts[R55-61]

+export const HAS_USER_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("user"));
+
+/** Whether there are opt-in features available for the team scope */
+export const HAS_TEAM_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("team"));
+
+/** Whether there are opt-in features available for the org scope */
+export const HAS_ORG_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("org"));
Evidence
PR Compliance ID 3 requires a 110 character line width; the added HAS_*_OPT_IN_FEATURES constants
are written as long single-line expressions that exceed that limit.

AGENTS.md
packages/features/feature-opt-in/config.ts[55-61]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added `HAS_USER_OPT_IN_FEATURES`, `HAS_TEAM_OPT_IN_FEATURES`, and `HAS_ORG_OPT_IN_FEATURES` constants exceed the 110 character line-width requirement.

## Issue Context
Formatting must follow the standardized Biome configuration (110 char line width).

## Fix Focus Areas
- packages/features/feature-opt-in/config.ts[55-61]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Global filter inverted 🐞 Bug ✓ Correctness
Description
FeatureOptInService.listFeaturesForUser now filters for !state.globalEnabled, returning only
globally-disabled features (often an empty list) instead of globally-enabled ones. This breaks the
user opt-in features listing returned by featureOptInRouter.listForUser.
Code

packages/features/feature-opt-in/services/FeatureOptInService.ts[R191-201]

  async listFeaturesForUser(input: {
    userId: number;
    orgId: number | null;
    teamIds: number[];
  }): Promise<ListFeaturesForUserResult[]> {
    const { userId, orgId, teamIds } = input;
-    const featureIds = OPT_IN_FEATURES.map((config) => config.slug);
+    const userScopedFeatures = getOptInFeaturesForScope("user");
+    const featureIds = userScopedFeatures.map((config) => config.slug);

    const resolvedStates = await this.resolveFeatureStatesAcrossTeams({
      userId,
Evidence
The method contract says it returns globally enabled features, and listFeaturesForTeam still
filters on result.globalEnabled, but listFeaturesForUser negates the check, causing the opposite
behavior. The TRPC router returns this result directly for the user endpoint.

packages/features/feature-opt-in/services/FeatureOptInService.ts[187-208]
packages/features/feature-opt-in/services/FeatureOptInService.ts[210-243]
packages/trpc/server/routers/viewer/featureOptIn/_router.ts[47-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`FeatureOptInService.listFeaturesForUser` incorrectly filters results using `!state.globalEnabled`, which returns only globally-disabled features and breaks the opt-in features list for users.

### Issue Context
The method docstring states it should only return globally enabled features, and `listFeaturesForTeam` still filters on `globalEnabled`.

### Fix Focus Areas
- packages/features/feature-opt-in/services/FeatureOptInService.ts[187-208]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. assignedBy branch swapped 🐞 Bug ✓ Correctness
Description
FeatureOptInService.setUserFeatureState omits assignedBy for enabled/disabled updates and
instead tries to use assignedBy in the inherit branch where it does not exist. This conflicts
with the repository contract (assignedBy required for enabled/disabled, not used for inherit) and
will fail compilation and/or throw at runtime.
Code

packages/features/feature-opt-in/services/FeatureOptInService.ts[R250-267]

  async setUserFeatureState(
    input:
      | { userId: number; featureId: FeatureId; state: "enabled" | "disabled"; assignedBy: number }
      | { userId: number; featureId: FeatureId; state: "inherit" }
  ): Promise<void> {
    const { userId, featureId, state } = input;
-    if (state === "inherit") {
+
+    if (!isFeatureAllowedForScope(featureId, "user")) {
+      throw new ErrorWithCode(
+        ErrorCode.BadRequest,
+        `Feature "${featureId}" is not available at the user scope`
+      );
+    }
+
+    if (state !== "inherit") {
      await this.featuresRepository.setUserFeatureState({ userId, featureId, state });
    } else {
      const { assignedBy } = input;
Evidence
The service calls featuresRepository.setUserFeatureState without assignedBy when state is
enabled/disabled, despite the repository requiring it for those states. Conversely it reads
assignedBy in the inherit branch even though the input type for inherit does not carry it;
repository implementation also shows assignedBy is only used for enabled/disabled and inherit
deletes the row.

packages/features/feature-opt-in/services/FeatureOptInService.ts[250-275]
packages/features/flags/features.repository.interface.ts[14-23]
packages/features/flags/features.repository.ts[350-384]
packages/features/feature-opt-in/services/FeatureOptInService.test.ts[203-238]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`setUserFeatureState` currently sends `assignedBy` in the wrong branch: enabled/disabled updates are missing `assignedBy`, while the `inherit` branch attempts to use `assignedBy` even though it is not part of the inherit input type.

### Issue Context
The repository contract requires `assignedBy` for enabled/disabled and does not use it for inherit (inherit deletes the row).

### Fix Focus Areas
- packages/features/feature-opt-in/services/FeatureOptInService.ts[245-275]
- packages/features/flags/features.repository.interface.ts[14-23]
- packages/features/flags/features.repository.ts[350-384]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Unawaited user state write 🐞 Bug ⛯ Reliability
Description
featureOptInRouter.setUserState no longer awaits featureOptInService.setUserFeatureState, so the
mutation can return success before the DB write finishes and any rejection may become an unhandled
promise. This causes inconsistent behavior and can hide errors from clients.
Code

packages/trpc/server/routers/viewer/featureOptIn/_router.ts[R100-106]

        });
      }

-      await featureOptInService.setUserFeatureState({
+      featureOptInService.setUserFeatureState({
        userId: ctx.user.id,
        featureId: input.slug,
        state: input.state,
Evidence
The router calls an async service method without await and returns success immediately. The
service method is declared async and returns Promise<void>, so failure/latency is not
synchronized with the mutation response.

packages/trpc/server/routers/viewer/featureOptIn/_router.ts[95-111]
packages/features/feature-opt-in/services/FeatureOptInService.ts[250-255]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The `setUserState` mutation does not `await` the async `setUserFeatureState` call, returning success early and potentially dropping errors.

### Issue Context
`FeatureOptInService.setUserFeatureState` returns a Promise and performs DB writes.

### Fix Focus Areas
- packages/trpc/server/routers/viewer/featureOptIn/_router.ts[88-111]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Unused constant left in config 🐞 Bug ⛯ Reliability
Description
packages/features/feature-opt-in/config.ts introduces UNUSED_CONSTANT which is never referenced,
leaving dead code in a core configuration module. This adds noise and can mislead future
maintainers.
Code

packages/features/feature-opt-in/config.ts[R4-5]

+// Unused import that should be caught by linting
+const UNUSED_CONSTANT = "this-should-be-removed";
Evidence
The constant is declared at module scope but not used anywhere in the file or repo usages, making it
dead code.

packages/features/feature-opt-in/config.ts[1-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
An unused constant was added to `feature-opt-in/config.ts`, creating dead code.

### Issue Context
This constant has no functional purpose and should be removed.

### Fix Focus Areas
- packages/features/feature-opt-in/config.ts[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +55 to +61
export const HAS_USER_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("user"));

/** Whether there are opt-in features available for the team scope */
export const HAS_TEAM_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("team"));

/** Whether there are opt-in features available for the org scope */
export const HAS_ORG_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("org"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. has_user_opt_in_features line too long 📘 Rule violation ✓ Correctness

New constants are written as single lines exceeding the 110 character line-width requirement, which
breaks the standardized Biome formatting rules. This will cause formatter/lint churn and
inconsistent code style.
Agent Prompt
## Issue description
The newly added `HAS_USER_OPT_IN_FEATURES`, `HAS_TEAM_OPT_IN_FEATURES`, and `HAS_ORG_OPT_IN_FEATURES` constants exceed the 110 character line-width requirement.

## Issue Context
Formatting must follow the standardized Biome configuration (110 char line width).

## Fix Focus Areas
- packages/features/feature-opt-in/config.ts[55-61]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 191 to 201
async listFeaturesForUser(input: {
userId: number;
orgId: number | null;
teamIds: number[];
}): Promise<ListFeaturesForUserResult[]> {
const { userId, orgId, teamIds } = input;
const featureIds = OPT_IN_FEATURES.map((config) => config.slug);
const userScopedFeatures = getOptInFeaturesForScope("user");
const featureIds = userScopedFeatures.map((config) => config.slug);

const resolvedStates = await this.resolveFeatureStatesAcrossTeams({
userId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Global filter inverted 🐞 Bug ✓ Correctness

FeatureOptInService.listFeaturesForUser now filters for !state.globalEnabled, returning only
globally-disabled features (often an empty list) instead of globally-enabled ones. This breaks the
user opt-in features listing returned by featureOptInRouter.listForUser.
Agent Prompt
### Issue description
`FeatureOptInService.listFeaturesForUser` incorrectly filters results using `!state.globalEnabled`, which returns only globally-disabled features and breaks the opt-in features list for users.

### Issue Context
The method docstring states it should only return globally enabled features, and `listFeaturesForTeam` still filters on `globalEnabled`.

### Fix Focus Areas
- packages/features/feature-opt-in/services/FeatureOptInService.ts[187-208]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 250 to 267
async setUserFeatureState(
input:
| { userId: number; featureId: FeatureId; state: "enabled" | "disabled"; assignedBy: number }
| { userId: number; featureId: FeatureId; state: "inherit" }
): Promise<void> {
const { userId, featureId, state } = input;
if (state === "inherit") {

if (!isFeatureAllowedForScope(featureId, "user")) {
throw new ErrorWithCode(
ErrorCode.BadRequest,
`Feature "${featureId}" is not available at the user scope`
);
}

if (state !== "inherit") {
await this.featuresRepository.setUserFeatureState({ userId, featureId, state });
} else {
const { assignedBy } = input;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Assignedby branch swapped 🐞 Bug ✓ Correctness

FeatureOptInService.setUserFeatureState omits assignedBy for enabled/disabled updates and
instead tries to use assignedBy in the inherit branch where it does not exist. This conflicts
with the repository contract (assignedBy required for enabled/disabled, not used for inherit) and
will fail compilation and/or throw at runtime.
Agent Prompt
### Issue description
`setUserFeatureState` currently sends `assignedBy` in the wrong branch: enabled/disabled updates are missing `assignedBy`, while the `inherit` branch attempts to use `assignedBy` even though it is not part of the inherit input type.

### Issue Context
The repository contract requires `assignedBy` for enabled/disabled and does not use it for inherit (inherit deletes the row).

### Fix Focus Areas
- packages/features/feature-opt-in/services/FeatureOptInService.ts[245-275]
- packages/features/flags/features.repository.interface.ts[14-23]
- packages/features/flags/features.repository.ts[350-384]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 100 to 106
});
}

await featureOptInService.setUserFeatureState({
featureOptInService.setUserFeatureState({
userId: ctx.user.id,
featureId: input.slug,
state: input.state,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Unawaited user state write 🐞 Bug ⛯ Reliability

featureOptInRouter.setUserState no longer awaits featureOptInService.setUserFeatureState, so the
mutation can return success before the DB write finishes and any rejection may become an unhandled
promise. This causes inconsistent behavior and can hide errors from clients.
Agent Prompt
### Issue description
The `setUserState` mutation does not `await` the async `setUserFeatureState` call, returning success early and potentially dropping errors.

### Issue Context
`FeatureOptInService.setUserFeatureState` returns a Promise and performs DB writes.

### Fix Focus Areas
- packages/trpc/server/routers/viewer/featureOptIn/_router.ts[88-111]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +4 to +5
// Unused import that should be caught by linting
const UNUSED_CONSTANT = "this-should-be-removed";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Unused constant left in config 🐞 Bug ⛯ Reliability

packages/features/feature-opt-in/config.ts introduces UNUSED_CONSTANT which is never referenced,
leaving dead code in a core configuration module. This adds noise and can mislead future
maintainers.
Agent Prompt
### Issue description
An unused constant was added to `feature-opt-in/config.ts`, creating dead code.

### Issue Context
This constant has no functional purpose and should be removed.

### Fix Focus Areas
- packages/features/feature-opt-in/config.ts[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.

1 participant