Skip to content

feat: add user flag implementation#1722

Merged
davidgamez merged 32 commits into
mainfrom
user_feature_flag
Jun 15, 2026
Merged

feat: add user flag implementation#1722
davidgamez merged 32 commits into
mainfrom
user_feature_flag

Conversation

@davidgamez

@davidgamez davidgamez commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary:

This PR adds the necessary endpoints to support user-level feature flags. It also adds endpoints to the operations API supporting assigning and removing feature flags.

Expected behavior:

Testing tips:

Testing Locally

Prerequisites

# Rebuild the local database with test data
./scripts/init-local-folder.sh
./scripts/docker-localdb-rebuild-data.sh --populate-db --populate-test-data

# Start the User Service API (port 8080)
scripts/api-start.sh

# Start the Operations API (port 8081) — separate terminal
scripts/api-operations-start.sh

Note: LOCAL_ENV=True is required in config/.env.local for the Operations API to skip OAuth2 validation locally. This is already set in the default local config.

1. List feature flags

curl -s http://localhost:8081/v1/operations/feature-flags | jq .

Expected: 5 flags (beta_editor, max_results, allowed_formats, ui_theme, extra_config) each with value_type and default_value.

2. Get a user's flag state (admin view)

curl -s http://localhost:8081/v1/operations/users/test_user_bob_00000000000002 | jq '{id, email, features}'

Expected: features show both default_value and user_value side by side.

3. Assign flags to a user

curl -s -X PATCH http://localhost:8081/v1/operations/users/test_user_carol_000000000003/feature-flags \
  -H "Content-Type: application/json" \
  -d '{"assignments": [{"feature_flag_id": "beta_editor", "value": true}]}' | jq .

Expected: response shows Carol with user_value: true for beta_editor. Re-querying the user confirms it persisted.

4. Verify resolved value (user-facing view)

# Carol — has override (beta_editor=true), shows resolved value only
curl -s http://localhost:8080/v1/user \
  -H 'x-goog-authenticated-user-id: test_user_carol_000000000003' | jq '{id, features}'

# Bob — has overrides for beta_editor and allowed_formats
curl -s http://localhost:8080/v1/user \
  -H 'x-goog-authenticated-user-id: test_user_bob_00000000000002' | jq '{id, features}'

Expected: each flag shows a single resolved value (user override if set, otherwise the flag's default). No wrapper object, no default_value exposed.

5. Run unit tests

scripts/api-tests.sh                          # User Service — all tests
scripts/api-tests.sh test_user_feature_flags  # Operations API — feature flag tests

From our AI friend

This pull request introduces feature flag support to the user service, enabling user profiles to include feature flag information. It updates the user data model, API implementation, and relevant tests to handle feature flags, and improves database session handling for both synchronous and asynchronous functions. The changes also include some configuration and documentation updates.

Feature flag support in user profiles:

  • The UserProfile model now includes a features field, populated from the user's associated feature flags in the database. The from_orm method in AppUserImpl is updated to map feature flags from the ORM model to the API model, including default and user-specific values.
  • The user API implementation (UsersApiImpl) is updated to eagerly load user feature flags and their definitions when fetching or updating users, ensuring the features field is correctly populated. [1] [2]
  • Feature flag model files are now included in the OpenAPI generator configuration.

Testing improvements for feature flags:

  • Unit tests for the user API are expanded to verify correct mapping of feature flags in user profiles, including cases with no feature flags or missing data. Helper functions are added to mock ORM queries with feature flag data. [1] [2]

Database session decorator enhancements:

  • The with_users_db_session decorator is improved to support both synchronous and asynchronous functions, ensuring correct database session management regardless of function type. [1] [2] [3]

Documentation and configuration updates:

  • The OpenAPI documentation adds a "users" tag for user and feature flag management.
  • The .env.local configuration adds a LOCAL_ENV variable.
    Please make sure these boxes are checked before submitting your pull request - thanks!
  • Run the unit tests with ./scripts/api-tests.sh to make sure you didn't break anything
  • Add or update any needed documentation to the repo
  • Format the title like "feat: [new feature short description]". Title must follow the Conventional Commit Specification(https://www.conventionalcommits.org/en/v1.0.0/).
  • Linked all relevant issues
  • Include screenshot(s) showing how this pull request works and fixes the issue(s)

)
with db.start_db_session() as session:
kwargs["db_session"] = session
if inspect.iscoroutinefunction(func):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Pre-existing bug: the sync wrapper in with_users_db_session returns a coroutine without awaiting it, so the session commits an empty transaction before FastAPI runs the endpoint. Writes never persisted. Fixed here because this PR introduces the first write endpoints that hit this decorator.

@davidgamez davidgamez linked an issue Jun 9, 2026 that may be closed by this pull request
@davidgamez davidgamez marked this pull request as ready for review June 10, 2026 15:33
@davidgamez davidgamez closed this Jun 10, 2026
@davidgamez davidgamez reopened this Jun 10, 2026
@davidgamez

Copy link
Copy Markdown
Member Author

Additional Test Cases

Assumes APIs are running as described above (8080 = User Service, 8081 = Operations API).

Operations API — Feature Flag CRUD

6. Create a new feature flag

curl -s -X POST http://localhost:8081/v1/operations/feature-flags \
  -H "Content-Type: application/json" \
  -d '{"id":"new_feature","name":"New Feature","value_type":"boolean","default_value":false}' | jq .

Expected: 201 with the created flag object, value_type: "boolean", default_value: false, disabled: false.

7. Reject duplicate flag ID

curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8081/v1/operations/feature-flags \
  -H "Content-Type: application/json" \
  -d '{"id":"beta_editor","value_type":"boolean","default_value":false}'

Expected: 409.

8. Reject mismatched default_value type on create

curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8081/v1/operations/feature-flags \
  -H "Content-Type: application/json" \
  -d '{"id":"bad_flag","value_type":"numeric","default_value":"not-a-number"}'

Expected: 422.

9. Update a flag's name and default value

curl -s -X PUT http://localhost:8081/v1/operations/feature-flags/new_feature \
  -H "Content-Type: application/json" \
  -d '{"name":"Renamed Feature","default_value":true}' | jq '{id,name,default_value}'

Expected: name: "Renamed Feature", default_value: true.

10. Reject update when default_value mismatches existing value_type

curl -s -o /dev/null -w "%{http_code}" -X PUT http://localhost:8081/v1/operations/feature-flags/new_feature \
  -H "Content-Type: application/json" \
  -d '{"default_value":"wrong-type"}'

Expected: 422 (flag's value_type is boolean; a string value is rejected).

11. Delete a flag

curl -s -o /dev/null -w "%{http_code}" -X DELETE \
  http://localhost:8081/v1/operations/feature-flags/new_feature

Expected: 204. Re-querying GET /v1/operations/feature-flags confirms it is gone.

12. Delete a non-existent flag

curl -s -o /dev/null -w "%{http_code}" -X DELETE \
  http://localhost:8081/v1/operations/feature-flags/does_not_exist

Expected: 404.

Operations API — User / Flag Assignment Edge Cases

13. Get a non-existent user

curl -s -o /dev/null -w "%{http_code}" \
  http://localhost:8081/v1/operations/users/ghost_user

Expected: 404.

14. Assign a non-existent flag to a user

curl -s -X PATCH \
  http://localhost:8081/v1/operations/users/test_user_bob_00000000000002/feature-flags \
  -H "Content-Type: application/json" \
  -d '{"assignments":[{"feature_flag_id":"no_such_flag","value":true}]}' | jq .

Expected: 404 with a detail message listing no_such_flag.

15. Clear all overrides for a user (empty assignments)

curl -s -X PATCH \
  http://localhost:8081/v1/operations/users/test_user_carol_000000000003/feature-flags \
  -H "Content-Type: application/json" \
  -d '{"assignments":[]}' | jq '{id, features}'

Expected: features shows all flags with user_value: null — no overrides remain.

16. Assignments are replace-not-append (idempotency)

# Assign only max_results; any previous overrides (e.g. beta_editor) must be gone
curl -s -X PATCH \
  http://localhost:8081/v1/operations/users/test_user_bob_00000000000002/feature-flags \
  -H "Content-Type: application/json" \
  -d '{"assignments":[{"feature_flag_id":"max_results","value":200}]}' | jq '{id, features}'

Expected: only max_results has a user_value; beta_editor and allowed_formats now show user_value: null.

17. List users filtered by search query

curl -s "http://localhost:8081/v1/operations/users?search_query=bob" | jq '.[].email'

Expected: only Bob's email is returned.

User Service — /v1/user with features

18. User with no overrides receives only default values

# Use a test user with no assignments
curl -s http://localhost:8080/v1/user \
  -H 'x-goog-authenticated-user-id: test_user_alice_000000000001' | jq '{id, features}'

Expected: features array has one entry per flag; each value equals the flag's default_value. No default_value or user_value keys are exposed — only a single resolved value.

19. User with an override returns the override, not the default

Confirm Carol (who has beta_editor=true while the flag defaults to false) via the user-facing endpoint:

curl -s http://localhost:8080/v1/user \
  -H 'x-goog-authenticated-user-id: test_user_carol_000000000003' | jq '.features[] | select(.id=="beta_editor")'

Expected: value: true.

Comment thread docs/UserServiceAPI.yaml
type: boolean
description: Whether the user has opted in to receive API announcement emails.
default: false
features:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why not feature_flags?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A feature flag is a term used to denote true/false values. Here we have more options, not just boolean values.

Comment thread docs/OperationsAPI.yaml Outdated
"404":
description: User not found.
/v1/operations/users/{user_id}/feature-flags:
patch:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's a detail, but since it replaces everything, maybe a PUT would be more appropriate?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done

raise HTTPException(
status_code=404,
detail=f"Feature flag(s) not found: {', '.join(sorted(missing))}",
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Apparently it should also check that the changed flag corresponds to the immutable value type.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done

@jcpitre jcpitre left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I added a couple of minor comments.

description: 1Password reference for users DB app password (e.g. op://vault/item/password)
required: true
type: string
POSTGRE_DEV_USER_APP_NAME_1PASSWORD:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

DEV shares the QA instance.

status_code=500, detail=f"Internal server error: {str(e)}"
)

async def get_feeds(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was needed because the previous PR downgraded the open api generator

Comment thread docs/UserServiceAPI.yaml
type: boolean
description: Whether the user has opted in to receive API announcement emails.
default: false
features:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A feature flag is a term used to denote true/false values. Here we have more options, not just boolean values.

Comment thread docs/OperationsAPI.yaml Outdated
"404":
description: User not found.
/v1/operations/users/{user_id}/feature-flags:
patch:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done

raise HTTPException(
status_code=404,
detail=f"Feature flag(s) not found: {', '.join(sorted(missing))}",
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done

@davidgamez davidgamez merged commit 4837109 into main Jun 15, 2026
24 of 25 checks passed
@davidgamez davidgamez deleted the user_feature_flag branch June 15, 2026 18:01
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.

[user-service: BE] API-based feature flag infrastructure

2 participants