Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions api/app/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"invite": "10/min",
"signup": "100/min",
"user": "100000/day",
"identity_search": "100/min",
}

AWS_SSE_LOGS_BUCKET_NAME = "test_bucket"
Expand Down
10 changes: 10 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,15 @@
class IdentityViewSet(viewsets.ModelViewSet): # type: ignore[type-arg]
serializer_class = IdentitySerializer
pagination_class = CustomPagination
throttle_scope = "identity_search"

def get_throttles(self): # type: ignore[no-untyped-def]
"""
Apply identity_search throttle only to list (search) requests.
"""
if getattr(self, "action", None) == "list":
return [ScopedRateThrottle()]
return []
Copy link
Contributor

Choose a reason for hiding this comment

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

Even though the impact is minimal, i would suggest to return the global default throttle super().get_throttles here (defaulting to DEFAULT_THROTTLE_CLASSES). Similarly to what we did here


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,31 @@ def test_search_identities_still_allows_paging(
assert response2.data["results"]


def test_identity_search_is_throttled(
admin_client: APIClient,
environment: Environment,
reset_cache: None,
mocker: MockerFixture,
) -> None:
# Given - mock the throttle rate to be restrictive for testing
mocker.patch(
"rest_framework.throttling.ScopedRateThrottle.get_rate", return_value="1/minute"
)
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?

Copy link
Contributor

Choose a reason for hiding this comment

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

@1-23-smy thanks for the changes, could you explain the rationale behind the increase of the debounce? If this does not conflict with the extra throttling we'd better stick to 500. We aligned on 500 being the right UX time

}, [searchInput])

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