mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-20 22:24:01 +00:00
Merge pull request #20472 from nina-hu/fix/timezone-daily-spend-filtering
fix(ui): adjust daily spend date filtering for user timezone
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
@@ -336,6 +336,46 @@ async def get_api_key_metadata(
|
||||
}
|
||||
|
||||
|
||||
def _adjust_dates_for_timezone(
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
timezone_offset_minutes: Optional[int],
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Adjust date range to account for timezone differences.
|
||||
|
||||
The database stores dates in UTC. When a user in a different timezone
|
||||
selects a local date range, we need to expand the UTC query range to
|
||||
capture all records that fall within their local date range.
|
||||
|
||||
Args:
|
||||
start_date: Start date in YYYY-MM-DD format (user's local date)
|
||||
end_date: End date in YYYY-MM-DD format (user's local date)
|
||||
timezone_offset_minutes: Minutes behind UTC (positive = west of UTC)
|
||||
This matches JavaScript's Date.getTimezoneOffset() convention.
|
||||
For example: PST = +480 (8 hours * 60 = 480 minutes behind UTC)
|
||||
|
||||
Returns:
|
||||
Tuple of (adjusted_start_date, adjusted_end_date) in YYYY-MM-DD format
|
||||
"""
|
||||
if timezone_offset_minutes is None or timezone_offset_minutes == 0:
|
||||
return start_date, end_date
|
||||
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
end = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
|
||||
if timezone_offset_minutes > 0:
|
||||
# West of UTC (Americas): local evening extends into next UTC day
|
||||
# e.g., Feb 4 23:59 PST = Feb 5 07:59 UTC
|
||||
end = end + timedelta(days=1)
|
||||
else:
|
||||
# East of UTC (Asia/Europe): local morning starts in previous UTC day
|
||||
# e.g., Feb 4 00:00 IST = Feb 3 18:30 UTC
|
||||
start = start - timedelta(days=1)
|
||||
|
||||
return start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _build_where_conditions(
|
||||
*,
|
||||
entity_id_field: str,
|
||||
@@ -345,12 +385,18 @@ def _build_where_conditions(
|
||||
model: Optional[str],
|
||||
api_key: Optional[Union[str, List[str]]],
|
||||
exclude_entity_ids: Optional[List[str]] = None,
|
||||
timezone_offset_minutes: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build prisma where clause for daily activity queries."""
|
||||
# Adjust dates for timezone if provided
|
||||
adjusted_start, adjusted_end = _adjust_dates_for_timezone(
|
||||
start_date, end_date, timezone_offset_minutes
|
||||
)
|
||||
|
||||
where_conditions: Dict[str, Any] = {
|
||||
"date": {
|
||||
"gte": start_date,
|
||||
"lte": end_date,
|
||||
"gte": adjusted_start,
|
||||
"lte": adjusted_end,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,6 +499,7 @@ async def get_daily_activity(
|
||||
page_size: int,
|
||||
exclude_entity_ids: Optional[List[str]] = None,
|
||||
metadata_metrics_func: Optional[Callable[[List[Any]], SpendMetrics]] = None,
|
||||
timezone_offset_minutes: Optional[int] = None,
|
||||
) -> SpendAnalyticsPaginatedResponse:
|
||||
"""Common function to get daily activity for any entity type."""
|
||||
|
||||
@@ -477,6 +524,7 @@ async def get_daily_activity(
|
||||
model=model,
|
||||
api_key=api_key,
|
||||
exclude_entity_ids=exclude_entity_ids,
|
||||
timezone_offset_minutes=timezone_offset_minutes,
|
||||
)
|
||||
|
||||
# Get total count for pagination
|
||||
@@ -542,6 +590,7 @@ async def get_daily_activity_aggregated(
|
||||
model: Optional[str],
|
||||
api_key: Optional[str],
|
||||
exclude_entity_ids: Optional[List[str]] = None,
|
||||
timezone_offset_minutes: Optional[int] = None,
|
||||
) -> SpendAnalyticsPaginatedResponse:
|
||||
"""Aggregated variant that returns the full result set (no pagination).
|
||||
|
||||
@@ -568,6 +617,7 @@ async def get_daily_activity_aggregated(
|
||||
model=model,
|
||||
api_key=api_key,
|
||||
exclude_entity_ids=exclude_entity_ids,
|
||||
timezone_offset_minutes=timezone_offset_minutes,
|
||||
)
|
||||
|
||||
# Fetch all matching results (no pagination)
|
||||
|
||||
@@ -1917,6 +1917,11 @@ async def get_user_daily_activity(
|
||||
page_size: int = fastapi.Query(
|
||||
default=50, description="Items per page", ge=1, le=1000
|
||||
),
|
||||
timezone: Optional[int] = fastapi.Query(
|
||||
default=None,
|
||||
description="Timezone offset in minutes from UTC (e.g., 480 for PST). "
|
||||
"Matches JavaScript's Date.getTimezoneOffset() convention.",
|
||||
),
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> SpendAnalyticsPaginatedResponse:
|
||||
"""
|
||||
@@ -1966,6 +1971,7 @@ async def get_user_daily_activity(
|
||||
api_key=api_key,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
timezone_offset_minutes=timezone,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -2002,6 +2008,11 @@ async def get_user_daily_activity_aggregated(
|
||||
default=None,
|
||||
description="Filter by specific API key",
|
||||
),
|
||||
timezone: Optional[int] = fastapi.Query(
|
||||
default=None,
|
||||
description="Timezone offset in minutes from UTC (e.g., 480 for PST). "
|
||||
"Matches JavaScript's Date.getTimezoneOffset() convention.",
|
||||
),
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
) -> SpendAnalyticsPaginatedResponse:
|
||||
"""
|
||||
@@ -2037,6 +2048,7 @@ async def get_user_daily_activity_aggregated(
|
||||
end_date=end_date,
|
||||
model=model,
|
||||
api_key=api_key,
|
||||
timezone_offset_minutes=timezone,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1703,6 +1703,8 @@ const buildDailyActivityUrl = (
|
||||
params.append("end_date", formatDate(endTime));
|
||||
params.append("page_size", DEFAULT_DAILY_ACTIVITY_PAGE_SIZE);
|
||||
params.append("page", page.toString());
|
||||
// Send timezone offset so backend can adjust date range for UTC storage
|
||||
params.append("timezone", new Date().getTimezoneOffset().toString());
|
||||
|
||||
if (extraQueryParams) {
|
||||
Object.entries(extraQueryParams).forEach(([key, value]) => {
|
||||
@@ -3567,6 +3569,8 @@ export const userDailyActivityAggregatedCall = async (accessToken: string, start
|
||||
};
|
||||
queryParams.append("start_date", formatDate(startTime));
|
||||
queryParams.append("end_date", formatDate(endTime));
|
||||
// Send timezone offset so backend can adjust date range for UTC storage
|
||||
queryParams.append("timezone", new Date().getTimezoneOffset().toString());
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
|
||||
Reference in New Issue
Block a user