Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@
LOGIN_THROTTLE_RATE = env("LOGIN_THROTTLE_RATE", "20/min")
SIGNUP_THROTTLE_RATE = env("SIGNUP_THROTTLE_RATE", "10000/min")
USER_THROTTLE_RATE = env("USER_THROTTLE_RATE", "500/min")
IDENTITY_SEARCH_THROTTLE_RATE = env("IDENTITY_SEARCH_THROTTLE_RATE", "30/min")
DEFAULT_THROTTLE_CLASSES = env.list("DEFAULT_THROTTLE_CLASSES", subcast=str, default=[])
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
Expand All @@ -320,6 +321,7 @@
"mfa_code": "5/min",
"invite": "10/min",
"user": USER_THROTTLE_RATE,
"identity_search": IDENTITY_SEARCH_THROTTLE_RATE,
},
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_RENDERER_CLASSES": [
Expand Down
3 changes: 3 additions & 0 deletions api/environments/identities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from rest_framework import status, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.throttling import ScopedRateThrottle

from app.pagination import CustomPagination
from core.constants import FLAGSMITH_UPDATED_AT_HEADER, SDK_ENVIRONMENT_KEY_HEADER
Expand All @@ -41,6 +42,8 @@
class IdentityViewSet(viewsets.ModelViewSet): # type: ignore[type-arg]
serializer_class = IdentitySerializer
pagination_class = CustomPagination
throttle_classes = [ScopedRateThrottle]
throttle_scope = "identity_search"

def get_queryset(self): # type: ignore[no-untyped-def]
if getattr(self, "swagger_fake_view", False):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,28 @@ def test_search_identities_still_allows_paging(
assert response2.data["results"]


def test_identity_search_is_throttled(
admin_client: APIClient,
environment: Environment,
settings,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The test is missing the reset_cache fixture parameter which is used in other throttle tests to ensure proper cache cleanup between tests. DRF's ScopedRateThrottle uses Django's cache backend to track request counts, and without cache clearing, throttle state could persist between tests causing flaky test failures.

Add reset_cache to the function parameters (see examples in api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py lines 486-491 and 527-533).

Suggested change
settings,
settings,
reset_cache,

Copilot uses AI. Check for mistakes.
) -> None:
# Given - configure a very restrictive throttle rate for testing
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["identity_search"] = "1/min"
base_url = reverse(
"api-v1:environments:environment-identities-list",
args=[environment.api_key],
)
url = f"{base_url}?q=test"

# When - make 2 requests in quick succession
response1 = admin_client.get(url)
response2 = admin_client.get(url)

# Then - first should succeed, second should be throttled
assert response1.status_code == status.HTTP_200_OK
assert response2.status_code == status.HTTP_429_TOO_MANY_REQUESTS


def test_can_delete_identity(
environment: Environment,
admin_client: APIClient,
Expand Down
4 changes: 2 additions & 2 deletions frontend/common/useDebouncedSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import useDebounce from './useDebounce'
export default function useDebouncedSearch(initialValue = '') {
const [searchInput, setSearchInput] = useState(initialValue)
const [search, setSearch] = useState(initialValue)
const [debounceTime, setDebounceTime] = useState(500)
const [debounceTime, setDebounceTime] = useState(750)

useEffect(() => {
setDebounceTime(searchInput.length < 1 ? 0 : 500)
setDebounceTime(searchInput.length < 1 ? 0 : 750)
Comment on lines +7 to +10
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Increasing the debounce time from 500ms to 750ms will affect all components using useDebouncedSearch, not just identity search. This includes:

  • AuditLog.tsx
  • ConversionEventSelect.tsx
  • CreateSegment.tsx (segment search)
  • SegmentsPage.tsx
  • SplitTestPage.tsx
  • UserPage.tsx
  • UsersPage.tsx (identity search)
  • TableValueFilter.tsx

While this may be acceptable to reduce API calls globally, consider whether a 250ms increase is appropriate for all these use cases. If the intent is to only throttle identity search, consider creating a separate hook like useDebouncedIdentitySearch with the higher debounce time, or make the debounce time configurable via a parameter.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

@Zaimwa9 are you able to chime in here - do you think this is something we need to be concerned about?

}, [searchInput])

const debouncedSearch = useDebounce((value: string) => {
Expand Down