mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-04 07:27:51 +00:00
Merge 2bbabfb82f into 2172fe49c3
This commit is contained in:
commit
0828d874fb
8 changed files with 369 additions and 2 deletions
|
|
@ -12,6 +12,7 @@ import type {
|
|||
ModelActivityData,
|
||||
SpendMetrics,
|
||||
} from "@/components/UsagePage/types";
|
||||
import type { DailyActivityTagPageResponse } from "@/components/UsagePage/dailyActivityApi";
|
||||
import EntityUsage from "./EntityUsage";
|
||||
import { getGlobalTopKeys, getTopAPIKeys } from "./entityUsageAggregations";
|
||||
|
||||
|
|
@ -34,6 +35,37 @@ const emptySpendMetrics: SpendMetrics = {
|
|||
cache_creation_input_tokens: 0,
|
||||
};
|
||||
|
||||
const tagSpendRow = (
|
||||
tag: string,
|
||||
spend: number,
|
||||
apiRequests: number,
|
||||
): DailyActivityTagPageResponse["tags"][number] => ({
|
||||
tag,
|
||||
spend,
|
||||
prompt_tokens: apiRequests * 10,
|
||||
completion_tokens: apiRequests * 5,
|
||||
total_tokens: apiRequests * 15,
|
||||
api_requests: apiRequests,
|
||||
successful_requests: apiRequests,
|
||||
failed_requests: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
});
|
||||
|
||||
const tagSpendPage = (offset: number): DailyActivityTagPageResponse => ({
|
||||
tags:
|
||||
offset === 0
|
||||
? [
|
||||
tagSpendRow("project:rag-v2", 0.25, 1),
|
||||
tagSpendRow("cost-center:cc-42", 0.5, 2),
|
||||
...Array.from({ length: 48 }, (_, index) => tagSpendRow(`tag-${index}`, 0.01, 1)),
|
||||
]
|
||||
: Array.from({ length: 10 }, (_, index) => tagSpendRow(`next-${index}`, 0.01, 1)),
|
||||
total_tags: 60,
|
||||
offset,
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
const createKeyMetrics = (spend: number, metadata: KeyMetadata): KeyMetricWithMetadata => ({
|
||||
metrics: { ...emptySpendMetrics, spend },
|
||||
metadata,
|
||||
|
|
@ -68,6 +100,7 @@ vi.mock("@/components/networking", () => ({
|
|||
dailyActivityKeyPageCall: vi.fn(),
|
||||
dailyActivityKeySearchCall: vi.fn(),
|
||||
dailyActivityModelTopKeysCall: vi.fn(),
|
||||
dailyActivityTagPageCall: vi.fn(),
|
||||
dailyActivityExportCall: vi.fn(),
|
||||
}));
|
||||
|
||||
|
|
@ -165,6 +198,7 @@ vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
|
|||
describe("EntityUsage", () => {
|
||||
const mockDailyActivityAggregatedCall = vi.mocked(networking.dailyActivityAggregatedCall);
|
||||
const mockDailyActivityKeyPageCall = vi.mocked(networking.dailyActivityKeyPageCall);
|
||||
const mockDailyActivityTagPageCall = vi.mocked(networking.dailyActivityTagPageCall);
|
||||
const mockTagDailyActivityCall = vi.fn();
|
||||
const mockTeamDailyActivityCall = vi.fn();
|
||||
const mockOrganizationDailyActivityCall = vi.fn();
|
||||
|
|
@ -476,6 +510,7 @@ describe("EntityUsage", () => {
|
|||
beforeEach(() => {
|
||||
mockDailyActivityAggregatedCall.mockReset();
|
||||
mockDailyActivityKeyPageCall.mockReset();
|
||||
mockDailyActivityTagPageCall.mockReset();
|
||||
const emptyKeyPage = {
|
||||
api_keys: [],
|
||||
total_api_keys: 0,
|
||||
|
|
@ -483,6 +518,7 @@ describe("EntityUsage", () => {
|
|||
limit: 50,
|
||||
};
|
||||
mockDailyActivityKeyPageCall.mockResolvedValue(emptyKeyPage);
|
||||
mockDailyActivityTagPageCall.mockImplementation((request) => Promise.resolve(tagSpendPage(request.offset)));
|
||||
mockDailyActivityAggregatedCall.mockImplementation((entity, request) =>
|
||||
(
|
||||
entityMocks[entity] as unknown as (
|
||||
|
|
@ -610,6 +646,89 @@ describe("EntityUsage", () => {
|
|||
expect(screen.queryByText(/^team-user-spend:/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows one selected team's tag page in API order and loads the next page", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StatefulEntityUsage {...defaultProps} entityType="team" />);
|
||||
expect(screen.queryByTestId("team-tag-spend-card")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Team Multi Select" }));
|
||||
|
||||
const card = await screen.findByTestId("team-tag-spend-card");
|
||||
expect(within(card).getByRole("heading", { name: "Spend Per Tag" })).toBeInTheDocument();
|
||||
const rows = (await within(card).findAllByRole("row")).slice(1);
|
||||
expect(rows.slice(0, 2).map((row) => within(row).getAllByRole("cell")[0].textContent)).toEqual([
|
||||
"project:rag-v2",
|
||||
"cost-center:cc-42",
|
||||
]);
|
||||
expect(within(card).getByText("Showing 1-50 of 60")).toBeInTheDocument();
|
||||
expect(mockDailyActivityTagPageCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ teamId: "team-1", offset: 0, limit: 50 }),
|
||||
);
|
||||
|
||||
await user.click(within(card).getByRole("button", { name: "Go to next page" }));
|
||||
await waitFor(() =>
|
||||
expect(mockDailyActivityTagPageCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ teamId: "team-1", offset: 50, limit: 50 }),
|
||||
),
|
||||
);
|
||||
expect(await within(card).findByText("Showing 51-60 of 60")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("refetches the first page at the chosen size when rows per page changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<StatefulEntityUsage {...defaultProps} entityType="team" />);
|
||||
await user.click(screen.getByRole("button", { name: "Team Multi Select" }));
|
||||
const card = await screen.findByTestId("team-tag-spend-card");
|
||||
await user.click(within(card).getByRole("button", { name: "Go to next page" }));
|
||||
expect(await within(card).findByText("Showing 51-60 of 60")).toBeInTheDocument();
|
||||
|
||||
await user.click(within(card).getByTestId("pagination-page-size"));
|
||||
await user.click(await screen.findByRole("option", { name: "100" }));
|
||||
|
||||
expect(await within(card).findByText("Showing 1-60 of 60")).toBeInTheDocument();
|
||||
expect(mockDailyActivityTagPageCall).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ teamId: "team-1", offset: 0, limit: 100 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("asks for tag spend only for exactly one team in the team view", async () => {
|
||||
render(<StatefulEntityUsage {...defaultProps} entityType="team" />);
|
||||
await waitFor(() => {
|
||||
expect(mockTeamDailyActivityCall).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.queryByTestId("team-tag-spend-card")).not.toBeInTheDocument();
|
||||
|
||||
cleanup();
|
||||
render(
|
||||
<EntityUsage
|
||||
{...defaultProps}
|
||||
entityType="team"
|
||||
selectedEntities={["team-1", "team-2"]}
|
||||
onSelectedEntitiesChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockTeamDailyActivityCall).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.queryByTestId("team-tag-spend-card")).not.toBeInTheDocument();
|
||||
|
||||
cleanup();
|
||||
render(
|
||||
<EntityUsage
|
||||
{...defaultProps}
|
||||
entityType="tag"
|
||||
selectedEntities={["tag-1"]}
|
||||
onSelectedEntitiesChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockTagDailyActivityCall).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.queryByTestId("team-tag-spend-card")).not.toBeInTheDocument();
|
||||
|
||||
expect(mockDailyActivityTagPageCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render with organization entity type and call organization API", async () => {
|
||||
render(<StatefulEntityUsage {...defaultProps} entityType="organization" />);
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import {
|
|||
} from "../overview/overviewData";
|
||||
import { Panel, Segmented, Sparkline, Stat } from "../overview/Primitives";
|
||||
import TopModelView from "./TopModelView";
|
||||
import TeamTagSpendCard from "./TeamTagSpendCard";
|
||||
import TeamUserSpendCard from "./TeamUserSpendCard";
|
||||
import { ProviderSpendBreakdown } from "./SpendByProvider";
|
||||
|
||||
|
|
@ -356,6 +357,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
const filterSlot = entityFilterSlots[entityType];
|
||||
|
||||
const capitalizedEntityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
|
||||
const tagSpendTeamId = entityType === "team" && selectedTags.length === 1 ? selectedTags[0] : null;
|
||||
const showFlatCost = entityType === "team" && hasFlatCost(spendData.metadata);
|
||||
const showPtuHours = entityType === "team" && hasPtuHours(spendData.metadata);
|
||||
const userSpendTeamIds = useMemo(
|
||||
|
|
@ -568,6 +570,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
/>
|
||||
</Panel>
|
||||
|
||||
{tagSpendTeamId !== null && (
|
||||
<TeamTagSpendCard accessToken={accessToken} startTime={startTime} endTime={endTime} teamId={tagSpendTeamId} />
|
||||
)}
|
||||
|
||||
{entityType === "team" && (
|
||||
<TeamUserSpendCard
|
||||
accessToken={accessToken}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Tags } from "lucide-react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { DataTable, DataTablePagination, DEFAULT_PAGE_SIZE_OPTIONS } from "@/components/shared/DataTable";
|
||||
import { MoneyCell } from "@/components/shared/table_cells";
|
||||
import type { DailyActivityTagPageResponse, DailyActivityTagSpendRow } from "@/components/UsagePage/dailyActivityApi";
|
||||
|
||||
import { Panel } from "../overview/Primitives";
|
||||
|
||||
import { ENTITY_API } from "./entityFetchFns";
|
||||
|
||||
interface TeamTagSpendCardProps {
|
||||
accessToken: string | null;
|
||||
startTime: Date | null;
|
||||
endTime: Date | null;
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
interface TagPageState {
|
||||
scope: object;
|
||||
pageKey: string;
|
||||
response: DailyActivityTagPageResponse | null;
|
||||
failed: boolean;
|
||||
}
|
||||
|
||||
interface TeamTagSpendPage {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
loading: boolean;
|
||||
failed: boolean;
|
||||
rows: DailyActivityTagSpendRow[];
|
||||
totalTags: number;
|
||||
onPageChange: (nextPage: number) => void;
|
||||
onPageSizeChange: (nextPageSize: number) => void;
|
||||
}
|
||||
|
||||
const QUIET_HEADER = { headerClassName: "font-normal" };
|
||||
const DEFAULT_PAGE_SIZE = 50;
|
||||
|
||||
const columns: ColumnDef<DailyActivityTagSpendRow>[] = [
|
||||
{
|
||||
header: "Tag",
|
||||
accessorKey: "tag",
|
||||
meta: QUIET_HEADER,
|
||||
cell: ({ row }) => <span className="font-medium text-foreground">{row.original.tag}</span>,
|
||||
},
|
||||
{
|
||||
header: "Spend",
|
||||
accessorKey: "spend",
|
||||
meta: { numeric: true, ...QUIET_HEADER },
|
||||
cell: ({ row }) => <MoneyCell value={row.original.spend} decimals={4} />,
|
||||
},
|
||||
{
|
||||
header: "Requests",
|
||||
accessorKey: "api_requests",
|
||||
meta: { numeric: true, ...QUIET_HEADER },
|
||||
cell: ({ row }) => row.original.api_requests.toLocaleString(),
|
||||
},
|
||||
{
|
||||
header: "Successful",
|
||||
accessorKey: "successful_requests",
|
||||
meta: { numeric: true, className: "text-success", ...QUIET_HEADER },
|
||||
cell: ({ row }) => row.original.successful_requests.toLocaleString(),
|
||||
},
|
||||
{
|
||||
header: "Failed",
|
||||
accessorKey: "failed_requests",
|
||||
meta: { numeric: true, className: "text-destructive", ...QUIET_HEADER },
|
||||
cell: ({ row }) => row.original.failed_requests.toLocaleString(),
|
||||
},
|
||||
{
|
||||
header: "Tokens",
|
||||
accessorKey: "total_tokens",
|
||||
meta: { numeric: true, ...QUIET_HEADER },
|
||||
cell: ({ row }) => row.original.total_tokens.toLocaleString(),
|
||||
},
|
||||
];
|
||||
|
||||
const useTeamTagSpendPage = ({ accessToken, startTime, endTime, teamId }: TeamTagSpendCardProps): TeamTagSpendPage => {
|
||||
const startTimestamp = startTime?.getTime();
|
||||
const endTimestamp = endTime?.getTime();
|
||||
const requestScope = useMemo(
|
||||
() => ({ hasAccessToken: accessToken !== null, endTimestamp, startTimestamp, teamId }),
|
||||
[accessToken, endTimestamp, startTimestamp, teamId],
|
||||
);
|
||||
const [pageState, setPageState] = useState<{ scope: object; pageSize: number; page: number }>({
|
||||
scope: {},
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
page: 0,
|
||||
});
|
||||
const [tagPageState, setTagPageState] = useState<TagPageState | null>(null);
|
||||
const pageSize = pageState.pageSize;
|
||||
const page = pageState.scope === requestScope ? pageState.page : 0;
|
||||
const pageKey = `${page}:${pageSize}`;
|
||||
const currentScopeState = tagPageState?.scope === requestScope ? tagPageState : null;
|
||||
const currentPageState = currentScopeState?.pageKey === pageKey ? currentScopeState : null;
|
||||
const enabled = Boolean(accessToken && startTimestamp !== undefined && endTimestamp !== undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken || startTimestamp === undefined || endTimestamp === undefined) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
const tagPageRequest = {
|
||||
accessToken,
|
||||
startTime: new Date(startTimestamp),
|
||||
endTime: new Date(endTimestamp),
|
||||
teamId,
|
||||
offset: page * pageSize,
|
||||
limit: pageSize,
|
||||
signal: controller.signal,
|
||||
};
|
||||
void ENTITY_API.team
|
||||
.tagPage(tagPageRequest)
|
||||
.then((response) => {
|
||||
if (controller.signal.aborted) return;
|
||||
const nextPageState: TagPageState = { scope: requestScope, pageKey, response, failed: false };
|
||||
setTagPageState(nextPageState);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (controller.signal.aborted) return;
|
||||
console.error("Team tag spend page request failed:", error);
|
||||
setTagPageState((current) => ({
|
||||
scope: requestScope,
|
||||
pageKey,
|
||||
response: current?.scope === requestScope ? current.response : null,
|
||||
failed: true,
|
||||
}));
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [accessToken, endTimestamp, page, pageKey, pageSize, requestScope, startTimestamp, teamId]);
|
||||
|
||||
const loading = enabled && currentPageState === null;
|
||||
const failed = currentPageState?.failed ?? false;
|
||||
const rows = failed ? [] : currentPageState?.response?.tags ?? [];
|
||||
const totalTags = currentScopeState?.response?.total_tags ?? 0;
|
||||
const onPageChange = (nextPage: number) => setPageState({ scope: requestScope, pageSize, page: nextPage });
|
||||
const onPageSizeChange = (nextPageSize: number) =>
|
||||
setPageState({ scope: requestScope, pageSize: nextPageSize, page: 0 });
|
||||
|
||||
return { page, pageSize, loading, failed, rows, totalTags, onPageChange, onPageSizeChange };
|
||||
};
|
||||
|
||||
const TeamTagSpendCard: React.FC<TeamTagSpendCardProps> = (props) => {
|
||||
const { page, pageSize, loading, failed, rows, totalTags, onPageChange, onPageSizeChange } =
|
||||
useTeamTagSpendPage(props);
|
||||
return (
|
||||
<Panel icon={Tags} title="Spend Per Tag" testId="team-tag-spend-card">
|
||||
<p className="mb-3 text-xs text-muted-foreground">
|
||||
A request with several tags counts once per tag, so the rows can add up to more than the team total
|
||||
</p>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
getRowId={(row) => row.tag}
|
||||
isLoading={loading}
|
||||
maxBodyHeight={280}
|
||||
noDataMessage={
|
||||
failed ? "Could not load tag spend. Reload the page to try again" : "No tagged spend in this range"
|
||||
}
|
||||
size="compact"
|
||||
/>
|
||||
{totalTags > 0 && (
|
||||
<DataTablePagination
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
rowCount={totalTags}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
pageSizeOptions={DEFAULT_PAGE_SIZE_OPTIONS}
|
||||
isLoading={loading}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamTagSpendCard;
|
||||
|
|
@ -4,6 +4,7 @@ import {
|
|||
dailyActivityKeyPageCall,
|
||||
dailyActivityKeySearchCall,
|
||||
dailyActivityModelTopKeysCall,
|
||||
dailyActivityTagPageCall,
|
||||
} from "@/components/networking";
|
||||
import type { EntityType } from "@/components/EntityUsageExport/types";
|
||||
import type {
|
||||
|
|
@ -12,6 +13,8 @@ import type {
|
|||
DailyActivityKeySearchResponse,
|
||||
DailyActivityKeyPageResponse,
|
||||
DailyActivityRequest,
|
||||
DailyActivityTagPageRequest,
|
||||
DailyActivityTagPageResponse,
|
||||
ExportFormat,
|
||||
ExportType,
|
||||
ModelTopKeysResponse,
|
||||
|
|
@ -30,6 +33,10 @@ export interface EntityApi {
|
|||
exportRows(req: DailyActivityRequest, exportType: ExportType, format: ExportFormat): Promise<Blob>;
|
||||
}
|
||||
|
||||
export interface TeamEntityApi extends EntityApi {
|
||||
tagPage(req: DailyActivityTagPageRequest): Promise<DailyActivityTagPageResponse>;
|
||||
}
|
||||
|
||||
const entityApi = (
|
||||
entity: DailyActivityEntity,
|
||||
defaults?: Pick<DailyActivityRequest, "excludeEntityIds">,
|
||||
|
|
@ -42,9 +49,12 @@ const entityApi = (
|
|||
exportRows: (req, exportType, format) => dailyActivityExportCall(entity, { ...defaults, ...req }, exportType, format),
|
||||
});
|
||||
|
||||
export const ENTITY_API: Record<EntityType, EntityApi> = {
|
||||
export const ENTITY_API: Record<EntityType, EntityApi> & { team: TeamEntityApi } = {
|
||||
tag: entityApi("tag"),
|
||||
team: entityApi("team", { excludeEntityIds: ["litellm-dashboard"] }),
|
||||
team: {
|
||||
...entityApi("team", { excludeEntityIds: ["litellm-dashboard"] }),
|
||||
tagPage: dailyActivityTagPageCall,
|
||||
},
|
||||
organization: entityApi("organization"),
|
||||
customer: entityApi("customer"),
|
||||
agent: entityApi("agent"),
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ beforeAll(() => {
|
|||
vi.mock("@/components/networking", () => ({
|
||||
dailyActivityAggregatedCall: vi.fn(),
|
||||
dailyActivityKeyPageCall: vi.fn(),
|
||||
dailyActivityTagPageCall: vi.fn(),
|
||||
userDailyActivityUserPageCall: vi.fn(),
|
||||
dailyActivityKeySearchCall: vi.fn(),
|
||||
dailyActivityModelTopKeysCall: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -11,11 +11,23 @@ export type KeyActivityRow = components["schemas"]["KeyActivityRow"];
|
|||
export type KeySpendActivityRow = components["schemas"]["KeySpendActivityRow"];
|
||||
export type DailyActivityKeySearchResponse = components["schemas"]["DailyActivityKeySearchResponse"];
|
||||
export type DailyActivityKeyPageResponse = components["schemas"]["DailyActivityKeyPageResponse"];
|
||||
export type DailyActivityTagPageResponse = components["schemas"]["DailyActivityTagPageResponse"];
|
||||
export type DailyActivityTagSpendRow = components["schemas"]["TagSpendRow"];
|
||||
export type ModelTopKeysResponse = components["schemas"]["ModelTopKeysResponse"];
|
||||
export type CacheLeakageKeysResponse = components["schemas"]["CacheLeakageKeysResponse"];
|
||||
export type UserActivityRow = components["schemas"]["UserActivityRow"];
|
||||
export type DailyActivityUserPageResponse = components["schemas"]["DailyActivityUserPageResponse"];
|
||||
|
||||
export interface DailyActivityTagPageRequest {
|
||||
accessToken: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
teamId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface DailyActivityRequest {
|
||||
accessToken: string;
|
||||
startTime: Date;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
dailyActivityExportCall,
|
||||
dailyActivityKeySearchCall,
|
||||
dailyActivityModelTopKeysCall,
|
||||
dailyActivityTagPageCall,
|
||||
} from "./networking";
|
||||
import type { DailyActivityEntity, DailyActivityRequest } from "./UsagePage/dailyActivityApi";
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ describe("dailyActivityAggregatedCall", () => {
|
|||
expect(url.searchParams.has("timezone")).toBe(true);
|
||||
expect(url.searchParams.has("page")).toBe(false);
|
||||
expect(url.searchParams.has("page_size")).toBe(false);
|
||||
expect(url.searchParams.has("group_by")).toBe(false);
|
||||
});
|
||||
|
||||
it("omits the entity filter when no ids are given", async () => {
|
||||
|
|
@ -123,6 +125,29 @@ describe("dailyActivityAggregatedCall", () => {
|
|||
expect(requestedUrl(mockFetch).searchParams.get("include_current_utc_day")).toBe("true");
|
||||
});
|
||||
|
||||
it("GETs the team tags page with team, offset, and limit", async () => {
|
||||
const mockFetch = captureFetch();
|
||||
|
||||
const request = {
|
||||
accessToken: "sk-key",
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
teamId: "team-a",
|
||||
offset: 50,
|
||||
limit: 50,
|
||||
};
|
||||
await dailyActivityTagPageCall(request);
|
||||
|
||||
const url = requestedUrl(mockFetch);
|
||||
expect(url.pathname).toBe("/team/daily/activity/aggregated/tags");
|
||||
expect(url.searchParams.get("team_ids")).toBe("team-a");
|
||||
expect(url.searchParams.get("start_date")).toBe("2025-01-05");
|
||||
expect(url.searchParams.get("end_date")).toBe("2025-01-31");
|
||||
expect(url.searchParams.get("offset")).toBe("50");
|
||||
expect(url.searchParams.get("limit")).toBe("50");
|
||||
expect(url.searchParams.has("group_by")).toBe(false);
|
||||
});
|
||||
|
||||
it("sends api_key_limit only when provided", async () => {
|
||||
const mockFetch = captureFetch();
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,8 @@ import type {
|
|||
DailyActivityKeyPageResponse,
|
||||
DailyActivityKeySearchResponse,
|
||||
DailyActivityRequest,
|
||||
DailyActivityTagPageRequest,
|
||||
DailyActivityTagPageResponse,
|
||||
DailyActivityUserPageResponse,
|
||||
ExportFormat,
|
||||
ExportType,
|
||||
|
|
@ -1405,6 +1407,20 @@ export const dailyActivityKeyPageCall = (
|
|||
query: dailyActivityQuery(entity, req, { offset, limit }),
|
||||
});
|
||||
|
||||
export const dailyActivityTagPageCall = (req: DailyActivityTagPageRequest): Promise<DailyActivityTagPageResponse> => {
|
||||
const dailyActivityRequest: DailyActivityRequest = {
|
||||
accessToken: req.accessToken,
|
||||
startTime: req.startTime,
|
||||
endTime: req.endTime,
|
||||
entityIds: [req.teamId],
|
||||
};
|
||||
return apiClient.get<DailyActivityTagPageResponse>(`/team/daily/activity/aggregated/tags`, {
|
||||
accessToken: req.accessToken,
|
||||
query: dailyActivityQuery("team", dailyActivityRequest, { offset: req.offset, limit: req.limit }),
|
||||
...(req.signal ? { signal: req.signal } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
export const dailyActivityKeySearchCall = (
|
||||
entity: DailyActivityEntity,
|
||||
req: DailyActivityRequest,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue