Skip to content

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

Open
tomerqodo wants to merge 8 commits intocoderabbit_full_base_feat_add_scope_configuration_for_feature_opt-in_pr14from
coderabbit_full_head_feat_add_scope_configuration_for_feature_opt-in_pr14
Open

feat: add scope configuration for feature opt-in#41
tomerqodo wants to merge 8 commits intocoderabbit_full_base_feat_add_scope_configuration_for_feature_opt-in_pr14from
coderabbit_full_head_feat_add_scope_configuration_for_feature_opt-in_pr14

Conversation

@tomerqodo
Copy link

@tomerqodo tomerqodo commented Jan 29, 2026

Benchmark PR from agentic-review-benchmarks#14

Summary by CodeRabbit

  • Refactor

    • Enhanced feature management system with support for scope-specific availability at user, team, and organization levels.
    • Updated settings interface to apply granular scope-based feature controls.
    • Restructured service layer to validate scope assignments and query features within their designated scopes.
    • Added error handling for invalid scope-feature combinations.
  • Tests

    • Expanded test coverage for scope-aware feature configuration, state management, and validation.

✏️ Tip: You can customize this high-level summary in your review settings.

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>
@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Walkthrough

The changes introduce scope-aware feature opt-in configuration, allowing features to be gated at organization, team, or user levels. New type definitions, helper functions, and service methods are added to support scope-based feature selection and validation across the system.

Changes

Cohort / File(s) Summary
Type Definitions
packages/features/feature-opt-in/types.ts
Added OptInFeatureScope type with "org", "team", and "user" values to define feature scope levels.
Configuration & Constants
packages/features/feature-opt-in/config.ts
Added OptInFeatureScope to OptInFeatureConfig, introduced ALL_SCOPES constant, added HAS_USER/TEAM/ORG_OPT_IN_FEATURES flags, and scope-filtering helper functions (getOptInFeaturesForScope, isFeatureAllowedForScope).
Service Interfaces
packages/features/feature-opt-in/services/IFeatureOptInService.ts
Updated listFeaturesForTeam and setTeamFeatureState signatures to accept optional scope parameter and modified return types for scope-aware results.
Service Implementation
packages/features/feature-opt-in/services/FeatureOptInService.ts
Modified listFeaturesForUser/listFeaturesForTeam to use scope-filtered features; added scope validation in setUserFeatureState/setTeamFeatureState with error handling; updated logic branching for state inheritance.
Service Tests
packages/features/feature-opt-in/services/FeatureOptInService.test.ts, FeatureOptInService.integration-test.ts
Expanded mocks to support scope validation; added tests for setUserFeatureState and setTeamFeatureState with scope validation, inheritance behavior, and error cases; mocked isFeatureAllowedForScope for test isolation.
Settings UI
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
Replaced single HAS_OPT_IN_FEATURES flag with granular HAS_USER/ORG/TEAM_OPT_IN_FEATURES to conditionally render scope-specific feature tabs in navigation.
TRPC Router
packages/trpc/server/routers/viewer/featureOptIn/_router.ts
Added scope parameters ("org", "team", "user") to listFeaturesForTeam, listForOrganization, setTeamState, and setOrganizationState calls; removed await from setUserState fire-and-forget operation.

Sequence Diagram(s)

sequenceDiagram
    actor User as Settings UI
    participant Router as TRPC Router
    participant Service as FeatureOptInService
    participant Config as Config & Helpers
    participant Repo as Repository

    User->>Router: listForTeam(teamId, scope: "team")
    Router->>Service: listFeaturesForTeam({teamId, scope: "team"})
    Service->>Config: getOptInFeaturesForScope("team")
    Config-->>Service: features scoped to "team"
    Service->>Repo: Query states for scoped features
    Repo-->>Service: Feature states
    Service-->>Router: Scope-aware feature list
    Router-->>User: Display team features

    User->>Router: setTeamState(featureId, state, scope: "team")
    Router->>Service: setTeamFeatureState({featureId, state, scope: "team"})
    Service->>Config: isFeatureAllowedForScope(featureId, "team")
    alt Scope allowed
        Config-->>Service: true
        Service->>Repo: Update team feature state
        Repo-->>Service: Success
        Service-->>Router: void
    else Scope not allowed
        Config-->>Service: false
        Service-->>Router: ErrorWithCode BadRequest
    end
    Router-->>User: State updated or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Scopes align with features fine,
User, team, and org combine,
Validation checks each layer through,
Granular control—now options grew!
A rabbit's hop to clarity—hooray! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add scope configuration for feature opt-in' accurately describes the main change: introducing scope-based configuration for opt-in features across org/team/user levels.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
packages/trpc/server/routers/viewer/featureOptIn/_router.ts (1)

103-110: Missing await causes silent failures and incorrect success response.

The await keyword was removed from featureOptInService.setUserFeatureState(), making this a fire-and-forget call. This introduces several problems:

  1. Errors (including scope validation errors via ErrorWithCode) become unhandled promise rejections
  2. The response { success: true } is returned before the operation completes, regardless of actual outcome
  3. Inconsistent with setTeamState (Line 131) and setOrganizationState (Line 161) which both properly await
🔧 Proposed fix
-      featureOptInService.setUserFeatureState({
+      await featureOptInService.setUserFeatureState({
         userId: ctx.user.id,
         featureId: input.slug,
         state: input.state,
         assignedBy: ctx.user.id,
       });
packages/features/feature-opt-in/services/FeatureOptInService.ts (2)

187-208: Comment contradicts the filter logic — verify intended behavior.

Line 189 states features should be "globally enabled", but Line 207 filters them out with !state.globalEnabled. This appears to be either:

  1. An inverted filter (should be .filter((state) => state.globalEnabled))
  2. An incorrect comment that should say "not globally enabled"

Please clarify the intended behavior and fix either the comment or the filter logic.


264-275: Critical bug: assignedBy handling logic is inverted.

The type signature shows:

  • Non-inherit states ("enabled" | "disabled") include assignedBy: number
  • Inherit state does NOT include assignedBy

But the code does the opposite:

  • When state !== "inherit" (lines 264-265): ignores assignedBy that IS available
  • When state === "inherit" (lines 267-273): tries to use assignedBy that IS NOT available (will be undefined)

Compare with setTeamFeatureState (lines 297-307) which correctly handles this.

🐛 Proposed fix
     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;
+    if (state === "inherit") {
+      await this.featuresRepository.setUserFeatureState({ userId, featureId, state });
+    } else {
+      const { assignedBy } = input as { assignedBy: number };
       await this.featuresRepository.setUserFeatureState({
         userId,
         featureId,
         state,
         assignedBy: `user-${assignedBy}`,
       });
     }
🤖 Fix all issues with AI agents
In `@packages/features/feature-opt-in/config.ts`:
- Around line 4-5: Remove the dead code declaration UNUSED_CONSTANT from the
module: locate the const UNUSED_CONSTANT = "this-should-be-removed"; in
config.ts (symbol UNUSED_CONSTANT) and delete the unused declaration so the file
no longer contains the unused constant.
🧹 Nitpick comments (1)
packages/features/feature-opt-in/services/FeatureOptInService.test.ts (1)

243-264: Consider consolidating redundant assertions.

The same async operation is awaited twice to check toThrow and toMatchObject separately. This works but calls the service method twice.

💡 Consolidation suggestion
-      await expect(
-        service.setUserFeatureState({
-          userId: 1,
-          featureId: "org-only-feature",
-          state: "enabled",
-          assignedBy: 2,
-        })
-      ).rejects.toThrow(ErrorWithCode);
-
-      await expect(
-        service.setUserFeatureState({
-          userId: 1,
-          featureId: "org-only-feature",
-          state: "enabled",
-          assignedBy: 2,
-        })
-      ).rejects.toMatchObject({
-        code: ErrorCode.BadRequest,
-        message: 'Feature "org-only-feature" is not available at the user scope',
-      });
+      const promise = service.setUserFeatureState({
+        userId: 1,
+        featureId: "org-only-feature",
+        state: "enabled",
+        assignedBy: 2,
+      });
+
+      await expect(promise).rejects.toThrow(ErrorWithCode);
+      // Re-call to verify object properties (or use a single toMatchObject check)
+      await expect(
+        service.setUserFeatureState({
+          userId: 1,
+          featureId: "org-only-feature",
+          state: "enabled",
+          assignedBy: 2,
+        })
+      ).rejects.toMatchObject({
+        code: ErrorCode.BadRequest,
+        message: 'Feature "org-only-feature" is not available at the user scope',
+      });

Alternatively, toMatchObject alone may suffice since it implicitly verifies the error is thrown.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69edd0e and 02f075d.

📒 Files selected for processing (8)
  • apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx
  • packages/features/feature-opt-in/config.ts
  • packages/features/feature-opt-in/services/FeatureOptInService.integration-test.ts
  • packages/features/feature-opt-in/services/FeatureOptInService.test.ts
  • packages/features/feature-opt-in/services/FeatureOptInService.ts
  • packages/features/feature-opt-in/services/IFeatureOptInService.ts
  • packages/features/feature-opt-in/types.ts
  • packages/trpc/server/routers/viewer/featureOptIn/_router.ts
🧰 Additional context used
🧬 Code graph analysis (5)
packages/features/feature-opt-in/services/IFeatureOptInService.ts (2)
packages/features/feature-opt-in/types.ts (1)
  • OptInFeatureScope (21-21)
packages/features/flags/config.ts (2)
  • FeatureId (51-51)
  • FeatureState (49-49)
packages/features/feature-opt-in/config.ts (2)
packages/features/flags/config.ts (1)
  • FeatureId (51-51)
packages/features/feature-opt-in/types.ts (2)
  • OptInFeaturePolicy (12-12)
  • OptInFeatureScope (21-21)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (1)
packages/features/feature-opt-in/config.ts (3)
  • HAS_USER_OPT_IN_FEATURES (55-55)
  • HAS_ORG_OPT_IN_FEATURES (61-61)
  • HAS_TEAM_OPT_IN_FEATURES (58-58)
packages/features/feature-opt-in/services/FeatureOptInService.test.ts (1)
packages/features/feature-opt-in/services/FeatureOptInService.ts (1)
  • FeatureOptInService (72-309)
packages/features/feature-opt-in/services/FeatureOptInService.ts (2)
packages/features/feature-opt-in/config.ts (2)
  • getOptInFeaturesForScope (67-69)
  • isFeatureAllowedForScope (76-80)
packages/features/feature-opt-in/types.ts (1)
  • OptInFeatureScope (21-21)
🔇 Additional comments (22)
packages/features/feature-opt-in/services/FeatureOptInService.integration-test.ts (1)

11-19: LGTM! Good test isolation strategy.

Mocking isFeatureAllowedForScope to always return true in integration tests is a sound approach. This cleanly separates concerns: unit tests cover scope validation logic, while integration tests focus on database behavior without scope-gating interference.

packages/features/feature-opt-in/types.ts (1)

14-21: LGTM! Well-documented type definition.

The OptInFeatureScope type is cleanly defined with comprehensive JSDoc documentation explaining each scope level. The union type approach is appropriate for this fixed set of values.

packages/trpc/server/routers/viewer/featureOptIn/_router.ts (4)

72-72: LGTM!

Correctly passes scope: "team" for team-level feature listing.


81-82: LGTM!

Correctly passes scope: "org" for organization-level feature listing.


136-136: LGTM!

Correctly passes scope: "team" for team feature state mutations.


166-166: LGTM!

Correctly passes scope: "org" for organization feature state mutations.

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

14-18: LGTM! Good refactor to granular scope-based feature flags.

The import change from a single HAS_OPT_IN_FEATURES to three scope-specific flags (HAS_USER_OPT_IN_FEATURES, HAS_TEAM_OPT_IN_FEATURES, HAS_ORG_OPT_IN_FEATURES) correctly aligns with the new scope-aware feature configuration system.


81-89: LGTM!

Correctly uses HAS_USER_OPT_IN_FEATURES to conditionally show the features tab in the user's "my_account" section.


207-215: LGTM!

Correctly uses HAS_ORG_OPT_IN_FEATURES to conditionally show the features tab in the organization section.


663-672: LGTM!

Correctly uses HAS_TEAM_OPT_IN_FEATURES to conditionally show the features tab for teams.

packages/features/feature-opt-in/services/IFeatureOptInService.ts (3)

4-4: LGTM!

Import correctly added for the new OptInFeatureScope type.


28-39: LGTM! Well-structured interface extension.

The listFeaturesForUser and listFeaturesForTeam signatures are cleanly formatted. The optional scope parameter in listFeaturesForTeam maintains backward compatibility while enabling scope-aware filtering.


45-49: LGTM!

The optional scope parameter in both union variants of setTeamFeatureState correctly allows distinguishing between team-level and org-level operations while maintaining backward compatibility.

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

10-28: LGTM! Comprehensive mock setup for scope-aware testing.

The mock configuration correctly includes:

  • Features with different scope configurations (org-only, team-only, user-only)
  • A controllable isFeatureAllowedForScope mock via getter pattern
  • Proper preservation of other config exports

203-279: LGTM! Thorough test coverage for setUserFeatureState.

Tests cover all important scenarios:

  • Successful state changes when scope allows
  • Inherit state handling
  • Error throwing with correct ErrorCode.BadRequest when scope is disallowed
  • Scope validation before inherit state changes

282-406: LGTM! Comprehensive test coverage for setTeamFeatureState.

Tests appropriately cover:

  • Team and org scope validation
  • Default scope behavior (defaults to "team")
  • Error codes and messages for scope violations
  • Inherit state with scope validation
packages/features/feature-opt-in/config.ts (3)

7-17: LGTM!

The interface extension with optional scope field and the ALL_SCOPES constant provide a clean, well-documented foundation for scope-based feature configuration. The JSDoc comment clearly explains the default behavior.


54-61: LGTM!

The per-scope availability constants correctly implement the documented fallback behavior where features without explicit scope are available for all scopes.


63-80: LGTM!

Both helper functions are well-implemented with clear documentation. The isFeatureAllowedForScope correctly returns false for unconfigured features, enforcing explicit configuration as documented.

packages/features/feature-opt-in/services/FeatureOptInService.ts (3)

3-14: LGTM!

Imports are appropriate for the new scope validation functionality.


210-243: LGTM!

The scope parameter is properly integrated with a sensible default. The filter correctly retains only globally enabled features, consistent with the documentation.


277-308: LGTM!

Scope validation and assignedBy handling are correctly implemented. The logic correctly handles inherit vs non-inherit states.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove dead code: UNUSED_CONSTANT is declared but never used.

The comment explicitly states this should be removed. This appears to be leftover debug/test code.

🧹 Proposed fix
-// Unused import that should be caught by linting
-const UNUSED_CONSTANT = "this-should-be-removed";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Unused import that should be caught by linting
const UNUSED_CONSTANT = "this-should-be-removed";
🤖 Prompt for AI Agents
In `@packages/features/feature-opt-in/config.ts` around lines 4 - 5, Remove the
dead code declaration UNUSED_CONSTANT from the module: locate the const
UNUSED_CONSTANT = "this-should-be-removed"; in config.ts (symbol
UNUSED_CONSTANT) and delete the unused declaration so the file no longer
contains the unused constant.

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