Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
ProjectMetricsPage.tsx9.24 kB
import { forwardRef, memo, Suspense, useMemo, useRef } from "react"; import { useParams } from "react-router"; import { css } from "@emotion/react"; import { Flex, Heading, Loading, Text, useTimeRange, View, } from "@phoenix/components"; import { ErrorBoundary } from "@phoenix/components/exception"; import { ONE_MONTH_MS } from "@phoenix/constants/timeConstants"; import { TopModelsByCost } from "@phoenix/pages/project/metrics/TopModelsByCost"; import { TopModelsByToken } from "@phoenix/pages/project/metrics/TopModelsByToken"; import { TraceErrorsTimeSeries } from "@phoenix/pages/project/metrics/TraceErrorsTimeSeries"; import { LLMSpanCountTimeSeries } from "./LLMSpanCountTimeSeries"; import { LLMSpanErrorsTimeSeries } from "./LLMSpanErrorsTimeSeries"; import { SpanAnnotationScoreTimeSeries } from "./SpanAnnotationScoreTimeSeries"; import { ToolSpanCountTimeSeries } from "./ToolSpanCountTimeSeries"; import { ToolSpanErrorsTimeSeries } from "./ToolSpanErrorsTimeSeries"; import { TraceCountTimeSeries } from "./TraceCountTimeSeries"; import { TraceLatencyPercentilesTimeSeries } from "./TraceLatencyPercentilesTimeSeries"; import { TraceTokenCostTimeSeries } from "./TraceTokenCostTimeSeries"; import { TraceTokenCountTimeSeries } from "./TraceTokenCountTimeSeries"; interface MetricPanelHeaderProps { title: string; subtitle?: string; } function MetricPanelHeader({ title, subtitle }: MetricPanelHeaderProps) { return ( <div css={css` padding: var(--ac-global-dimension-size-100) var(--ac-global-dimension-size-200) 0 var(--ac-global-dimension-size-200); display: flex; flex-direction: row; gap: var(--ac-global-dimension-size-100); `} className="dashboard-panel-header" > <Flex direction="column"> <Heading>{title}</Heading> {subtitle && ( <Text size="XS" color="grey-600"> {subtitle} </Text> )} </Flex> </div> ); } interface MetricPanelProps extends MetricPanelHeaderProps { children: React.ReactNode; } export const MetricPanel = forwardRef(function MetricPanel( { title, subtitle, children }: MetricPanelProps, ref: React.Ref<HTMLDivElement> ) { return ( <View borderWidth="thin" borderColor="grey-200" borderRadius="medium" height="100%" width="100%" data-testid={`dashboard-panel`} backgroundColor="grey-75" ref={ref} > <div css={css` display: flex; flex-direction: column; height: 100%; `} > <MetricPanelHeader title={title} subtitle={subtitle} /> <div css={css` flex: 1 1 auto; padding: var(--ac-global-dimension-size-200); height: 190px; overflow: auto; `} > <ErrorBoundary> <Suspense fallback={<Loading />}>{children}</Suspense> </ErrorBoundary> </div> </div> </View> ); }); type EpochTimeRange = { start: number; end: number; }; /** * Hook that converts an open time range from context into a closed time range. * If the time range is already closed, it returns it as-is. * If it's open, it fills in missing start/end values based on a frozen "now" timestamp. * * The "now" timestamp is frozen and only updates when the context time range actually changes, * preventing unnecessary recalculations on every render. */ function useClosedTimeRange(): EpochTimeRange { const { timeRange: contextTimeRange } = useTimeRange(); // Extract and memoize timestamps to get stable primitive values const startMs = useMemo( () => (contextTimeRange.start ? contextTimeRange.start.getTime() : null), [contextTimeRange.start] ); const endMs = useMemo( () => (contextTimeRange.end ? contextTimeRange.end.getTime() : null), [contextTimeRange.end] ); // Use a ref to freeze "now" until the context time range actually changes const lastTimestampsRef = useRef({ startMs, endMs }); // eslint-disable-next-line react-hooks/purity const frozenNowMsRef = useRef<number>(Date.now()); // Only update frozen "now" when timestamps actually change if ( lastTimestampsRef.current.startMs !== startMs || lastTimestampsRef.current.endMs !== endMs ) { lastTimestampsRef.current = { startMs, endMs }; // eslint-disable-next-line react-hooks/purity frozenNowMsRef.current = Date.now(); } const frozenNowMs = frozenNowMsRef.current; const epochTimeRange = useMemo<EpochTimeRange>(() => { let start = startMs; let end = endMs; if (start !== null && end !== null) { // closed range from context return { start, end }; } else if (start === null && end !== null) { return { start: end - ONE_MONTH_MS, end }; } else if (start !== null && end === null) { // If start is in the past, close at "now"; else, one month after start end = start < frozenNowMs ? frozenNowMs : start + ONE_MONTH_MS; return { start, end }; } else { // both null → last month to now end = frozenNowMs; start = end - ONE_MONTH_MS; return { start, end }; } }, [startMs, endMs, frozenNowMs]); return epochTimeRange; } export function ProjectMetricsPage() { const { projectId } = useParams(); if (!projectId) { throw new Error("projectId is required"); } const epochTimeRange = useClosedTimeRange(); return ( <main css={css` width: 100%; height: 100%; box-sizing: border-box; overflow-y: auto; `} > <MetricPanels projectId={projectId} epochTimeRange={epochTimeRange} /> </main> ); } const MetricPanels = memo(function MetricPanels({ projectId, epochTimeRange, }: { projectId: string; epochTimeRange: EpochTimeRange; }) { const timeRange = useMemo( () => ({ start: new Date(epochTimeRange.start), end: new Date(epochTimeRange.end), }), [epochTimeRange] ); return ( <div css={css` display: flex; flex-direction: column; gap: var(--ac-global-dimension-size-200); padding: var(--ac-global-dimension-size-200); `} > <Flex direction="row" gap="size-200"> <MetricPanel title="Traces over time" subtitle="Overall volume of traces" > <TraceCountTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> <MetricPanel title="Traces with errors" subtitle="Overall volume of traces with errors" > <TraceErrorsTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> </Flex> <Flex direction="row" gap="size-200"> <MetricPanel title="Trace Latency" subtitle="Latency percentiles"> <TraceLatencyPercentilesTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> <MetricPanel title="Annotation scores" subtitle="Average annotation scores" > <SpanAnnotationScoreTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> </Flex> <Flex direction="row" gap="size-200"> <MetricPanel title="Cost" subtitle="Estimated cost in USD"> <TraceTokenCostTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> <MetricPanel title="Top models by cost"> <TopModelsByCost projectId={projectId} timeRange={timeRange} /> </MetricPanel> </Flex> <Flex direction="row" gap="size-200"> <MetricPanel title="Token usage" subtitle="Token usage by prompt and completion" > <TraceTokenCountTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> <MetricPanel title="Top models by tokens"> <TopModelsByToken projectId={projectId} timeRange={timeRange} /> </MetricPanel> </Flex> <Flex direction="row" gap="size-200"> <MetricPanel title="LLM spans" subtitle="LLM span count over time"> <LLMSpanCountTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> <MetricPanel title="LLM spans with errors" subtitle="LLM spans with errors over time" > <LLMSpanErrorsTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> </Flex> <Flex direction="row" gap="size-200"> <MetricPanel title="Tool spans" subtitle="Tool span count over time"> <ToolSpanCountTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> <MetricPanel title="Tool spans with errors" subtitle="Tool spans with errors over time" > <ToolSpanErrorsTimeSeries projectId={projectId} timeRange={timeRange} /> </MetricPanel> </Flex> </div> ); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Arize-ai/phoenix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server