Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
TraceDetails.tsx10.3 kB
import { PropsWithChildren, Suspense, useMemo } from "react"; import { Focusable } from "react-aria"; import { graphql, useLazyLoadQuery } from "react-relay"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { useParams, useSearchParams } from "react-router"; import { css } from "@emotion/react"; import { Flex, LinkButton, Loading, RichTooltip, Text, TooltipArrow, TooltipTrigger, View, } from "@phoenix/components"; import { compactResizeHandleCSS } from "@phoenix/components/resize"; import { LatencyText } from "@phoenix/components/trace/LatencyText"; import { SpanStatusCodeIcon } from "@phoenix/components/trace/SpanStatusCodeIcon"; import { TraceTree } from "@phoenix/components/trace/TraceTree"; import { useSpanStatusCodeColor } from "@phoenix/components/trace/useSpanStatusCodeColor"; import { SELECTED_SPAN_NODE_ID_PARAM } from "@phoenix/constants/searchParams"; import { costFormatter } from "@phoenix/utils/numberFormatUtils"; import { RichTokenBreakdown } from "../../components/RichTokenCostBreakdown"; import { TraceDetailsQuery, TraceDetailsQuery$data, } from "./__generated__/TraceDetailsQuery.graphql"; import { SpanDetails } from "./SpanDetails"; import { TraceHeaderRootSpanAnnotations } from "./TraceHeaderRootSpanAnnotations"; type Span = NonNullable< TraceDetailsQuery$data["project"]["trace"] >["spans"]["edges"][number]["span"]; type CostSummary = NonNullable< TraceDetailsQuery$data["project"]["trace"] >["costSummary"]; /** * A root span is defined to be a span whose parent span is not in our collection. * But if more than one such span exists, return null. */ function findRootSpan(spansList: Span[]): Span | null { // If there is a span whose parent is null, then it is the root span. const rootSpan = spansList.find((span) => span.parentId == null); if (rootSpan) return rootSpan; // Otherwise we need to find all spans whose parent span is not in our collection. const spanIds = new Set(spansList.map((span) => span.spanId)); const rootSpans = spansList.filter( (span) => span.parentId != null && !spanIds.has(span.parentId) ); // If only one such span exists, then return it, otherwise, return null. if (rootSpans.length === 1) return rootSpans[0]; return null; } export type TraceDetailsProps = { traceId: string; projectId: string; }; /** * A component that shows the details of a trace (e.g. a collection of spans) */ export function TraceDetails(props: TraceDetailsProps) { const { traceId, projectId } = props; const [searchParams, setSearchParams] = useSearchParams(); const data = useLazyLoadQuery<TraceDetailsQuery>( graphql` query TraceDetailsQuery($traceId: ID!, $id: ID!) { project: node(id: $id) { ... on Project { trace(traceId: $traceId) { projectSessionId spans(first: 1000) { edges { span: node { id spanId name spanKind statusCode startTime endTime parentId latencyMs tokenCountTotal spanAnnotationSummaries { labels count labelCount labelFractions { fraction label } name scoreCount meanScore } } } } latencyMs costSummary { prompt { cost } completion { cost } total { cost } } } } } } `, { traceId: traceId as string, id: projectId as string }, { fetchPolicy: "store-and-network", } ); const traceLatencyMs = data.project.trace?.latencyMs != null ? data.project.trace.latencyMs : null; const costSummary = data?.project?.trace?.costSummary; const spansList: Span[] = useMemo(() => { const gqlSpans = data.project.trace?.spans.edges || []; return gqlSpans.map((node) => node.span); }, [data]); const urlSpanNodeId = searchParams.get(SELECTED_SPAN_NODE_ID_PARAM); const selectedSpanNodeId = urlSpanNodeId ?? spansList[0].id; const rootSpan = useMemo(() => findRootSpan(spansList), [spansList]); return ( <main css={css` flex: 1 1 auto; overflow: hidden; display: flex; flex-direction: column; `} > <TraceHeader rootSpan={rootSpan} latencyMs={traceLatencyMs} costSummary={costSummary} sessionId={data.project.trace?.projectSessionId} /> <PanelGroup direction="horizontal" autoSaveId="trace-panel-group" css={css` flex: 1 1 auto; overflow: hidden; `} > <Panel defaultSize={30} minSize={5}> <ScrollingPanelContent> <TraceTree spans={spansList} selectedSpanNodeId={selectedSpanNodeId} onSpanClick={(span) => { setSearchParams( (searchParams) => { searchParams.set(SELECTED_SPAN_NODE_ID_PARAM, span.id); return searchParams; }, { replace: true } ); }} /> </ScrollingPanelContent> </Panel> <PanelResizeHandle css={compactResizeHandleCSS} /> <Panel> <ScrollingTabsWrapper> {selectedSpanNodeId ? ( <Suspense fallback={<Loading />}> <SpanDetails spanNodeId={selectedSpanNodeId} /> </Suspense> ) : null} </ScrollingTabsWrapper> </Panel> </PanelGroup> </main> ); } function TraceHeader({ rootSpan, latencyMs, costSummary, sessionId, }: { rootSpan: Span | null; latencyMs: number | null; costSummary?: CostSummary | null; sessionId?: string | null; }) { const { projectId } = useParams(); const { statusCode } = rootSpan ?? { statusCode: "UNSET", }; const statusColor = useSpanStatusCodeColor(statusCode); return ( <View paddingTop="size-100" paddingBottom="size-150" paddingX="size-200" borderBottomWidth="thin" borderBottomColor="dark" > <Flex direction="row" gap="size-400" alignItems="center" css={css` box-sizing: content-box; `} > <Flex direction="column"> <Text elementType="h3" size="S" color="text-700"> Trace Status </Text> <Text size="XL"> <Flex direction="row" gap="size-50" alignItems="center"> <SpanStatusCodeIcon statusCode={statusCode} /> <Text size="L" color={statusColor}> {statusCode} </Text> </Flex> </Text> </Flex> <Flex direction="column"> <Text elementType="h3" size="S" color="text-700"> Total Cost </Text> <TooltipTrigger delay={0}> <Focusable> <Text size="L" role="button"> {costFormatter(costSummary?.total?.cost ?? 0)} </Text> </Focusable> <RichTooltip placement="bottom"> <TooltipArrow /> <View width="size-3600"> <RichTokenBreakdown valueLabel="cost" totalValue={costSummary?.total?.cost ?? 0} formatter={costFormatter} segments={[ { name: "Prompt", value: costSummary?.prompt?.cost ?? 0, color: "rgba(254, 119, 99, 1)", }, { name: "Completion", value: costSummary?.completion?.cost ?? 0, color: "rgba(98, 104, 239, 1)", }, ]} /> </View> </RichTooltip> </TooltipTrigger> </Flex> <Flex direction="column"> <Text elementType="h3" size="S" color="text-700"> Latency </Text> <Text size="XL"> {typeof latencyMs === "number" ? ( <LatencyText latencyMs={latencyMs} size="L" /> ) : ( "--" )} </Text> </Flex> {rootSpan ? ( <TraceHeaderRootSpanAnnotations spanId={rootSpan.id} /> ) : null} {sessionId && ( <span css={css` margin-left: auto; `} > <LinkButton size="S" variant="primary" to={`/projects/${projectId}/sessions/${sessionId}`} > View Session </LinkButton> </span> )} </Flex> </View> ); } function ScrollingTabsWrapper({ children }: PropsWithChildren) { return ( <div data-testid="scrolling-tabs-wrapper" css={css` height: 100%; overflow: hidden; .ac-tabs { height: 100%; overflow: hidden; .ac-tabs__extra { width: 100%; padding-right: var(--ac-global-dimension-size-200); padding-bottom: var(--ac-global-dimension-size-50); } .ac-tabs__pane-container { min-height: 100%; height: 100%; overflow-y: auto; div[role="tabpanel"] { height: 100%; } } } `} > {children} </div> ); } function ScrollingPanelContent({ children }: PropsWithChildren) { return ( <div data-testid="scrolling-panel-content" css={css` height: 100%; overflow-y: auto; `} > {children} </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