Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(trace): add a span aside with timing info and feedback #4071

Merged
merged 4 commits into from
Jul 30, 2024
Merged
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: 1 addition & 1 deletion app/src/components/trace/SpanItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { css } from "@emotion/react";

import { Flex, Text, View } from "@arizeai/components";

import { TokenCount } from "@phoenix/components/trace/TokenCount";
import { SpanStatusCode } from "@phoenix/pages/project/__generated__/SpansTable_spans.graphql";
import { TokenCount } from "@phoenix/pages/project/TokenCount";

import { LatencyText } from "./LatencyText";
import { SpanKindLabel } from "./SpanKindLabel";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Icon,
Icons,
Text,
TextProps,
Tooltip,
TooltipTrigger,
TriggerWrap,
Expand All @@ -24,6 +25,10 @@ type TokenCountProps = {
* The number of tokens in the completion
*/
tokenCountCompletion: number;
/**
* The size of the icon and text
*/
textSize?: TextProps["textSize"];
};

/**
Expand All @@ -33,7 +38,7 @@ export function TokenCount(props: TokenCountProps) {
return (
<TooltipTrigger>
<TriggerWrap>
<TokenItem>{props.tokenCountTotal}</TokenItem>
<TokenItem textSize={props.textSize}>{props.tokenCountTotal}</TokenItem>
</TriggerWrap>
<Tooltip>
<Flex direction="column" gap="size-50">
Expand All @@ -51,16 +56,22 @@ export function TokenCount(props: TokenCountProps) {
);
}

function TokenItem({ children }: { children: number }) {
function TokenItem({
children,
...textProps
}: {
children: number;
textSize?: TextProps["textSize"];
}) {
return (
<Flex direction="row" gap="size-25" alignItems="center">
<Flex direction="row" gap="size-50" alignItems="center">
<Icon
svg={<Icons.TokensOutline />}
css={css`
color: var(--ac-global-text-color-900);
`}
/>
<Text>{children}</Text>
<Text {...textProps}>{children}</Text>
</Flex>
);
}
2 changes: 1 addition & 1 deletion app/src/components/trace/TraceTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { css } from "@emotion/react";

import { Flex, Text, View } from "@arizeai/components";

import { TokenCount } from "@phoenix/pages/project/TokenCount";
import { TokenCount } from "@phoenix/components/trace/TokenCount";

import { LatencyText } from "./LatencyText";
import { SpanKindIcon } from "./SpanKindIcon";
Expand Down
2 changes: 1 addition & 1 deletion app/src/pages/project/SpansTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { TimestampCell } from "@phoenix/components/table/TimestampCell";
import { LatencyText } from "@phoenix/components/trace/LatencyText";
import { SpanKindLabel } from "@phoenix/components/trace/SpanKindLabel";
import { SpanStatusCodeIcon } from "@phoenix/components/trace/SpanStatusCodeIcon";
import { TokenCount } from "@phoenix/components/trace/TokenCount";
import { useStreamState } from "@phoenix/contexts/StreamStateContext";
import { useTracingContext } from "@phoenix/contexts/TracingContext";

Expand All @@ -52,7 +53,6 @@ import {
EVALS_KEY_SEPARATOR,
getGqlSort,
} from "./tableUtils";
import { TokenCount } from "./TokenCount";
type SpansTableProps = {
project: SpansTable_spans$key;
};
Expand Down
2 changes: 1 addition & 1 deletion app/src/pages/project/TracesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { TimestampCell } from "@phoenix/components/table/TimestampCell";
import { LatencyText } from "@phoenix/components/trace/LatencyText";
import { SpanKindLabel } from "@phoenix/components/trace/SpanKindLabel";
import { SpanStatusCodeIcon } from "@phoenix/components/trace/SpanStatusCodeIcon";
import { TokenCount } from "@phoenix/components/trace/TokenCount";
import { ISpanItem } from "@phoenix/components/trace/types";
import { createSpanTree, SpanTreeNode } from "@phoenix/components/trace/utils";
import { useStreamState } from "@phoenix/contexts/StreamStateContext";
Expand All @@ -59,7 +60,6 @@ import {
EVALS_KEY_SEPARATOR,
getGqlSort,
} from "./tableUtils";
import { TokenCount } from "./TokenCount";
type TracesTableProps = {
project: TracesTable_spans$key;
};
Expand Down
145 changes: 145 additions & 0 deletions app/src/pages/trace/SpanAside.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { PropsWithChildren, useMemo } from "react";
import { graphql, useRefetchableFragment } from "react-relay";
import { css } from "@emotion/react";

import { Flex, Text, View } from "@arizeai/components";

import { AnnotationLabel } from "@phoenix/components/annotation";
import { LatencyText } from "@phoenix/components/trace/LatencyText";
import { SpanStatusCodeIcon } from "@phoenix/components/trace/SpanStatusCodeIcon";
import { TokenCount } from "@phoenix/components/trace/TokenCount";
import { useSpanStatusCodeColor } from "@phoenix/components/trace/useSpanStatusCodeColor";
import { fullTimeFormatter } from "@phoenix/utils/timeFormatUtils";

import { SpanAside_span$key } from "./__generated__/SpanAside_span.graphql";
import { SpanAsideSpanQuery } from "./__generated__/SpanAsideSpanQuery.graphql";

const annotationListCSS = css`
display: flex;
padding-top: var(--ac-global-dimension-size-50);
flex-direction: column;
gap: var(--ac-global-dimension-size-100);
align-items: flex-start;
`;

/**
* A component that shows the details of a span that is supplementary to the main span details
*/
export function SpanAside(props: { span: SpanAside_span$key }) {
const [data] = useRefetchableFragment<SpanAsideSpanQuery, SpanAside_span$key>(
graphql`
fragment SpanAside_span on Span
@refetchable(queryName: "SpanAsideSpanQuery") {
id
code: statusCode
startTime
endTime
tokenCountTotal
tokenCountPrompt
tokenCountCompletion
spanAnnotations {
id
name
label
annotatorKind
score
}
}
`,
props.span
);
const {
startTime,
endTime,
code,
tokenCountCompletion,
tokenCountPrompt,
tokenCountTotal,
} = data;
const startDate = useMemo(() => new Date(startTime), [startTime]);
const endDate = useMemo(
() => (endTime ? new Date(endTime) : null),
[endTime]
);
mikeldking marked this conversation as resolved.
Show resolved Hide resolved
const latencyMs = useMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup, I think it should be there

if (!endDate) return null;
return endDate.getTime() - startDate.getTime();
}, [endDate, startDate]);
const statusColor = useSpanStatusCodeColor(code);
const annotations = data.spanAnnotations;
const hasAnnotations = annotations.length > 0;
return (
<View
padding="size-200"
borderColor="dark"
backgroundColor="light"
borderLeftWidth="thin"
width="210px"
flex="none"
minHeight="100%"
>
<Flex direction="column" gap="size-200">
<LabeledValue label="Status">
<Flex direction="row" gap="size-50" alignItems="center">
<SpanStatusCodeIcon statusCode={code} />
<Text textSize="xlarge" color={statusColor}>
{code}
</Text>
</Flex>
</LabeledValue>
<LabeledValue label="Start Time">
<Text textSize="xlarge">{fullTimeFormatter(startDate)}</Text>
</LabeledValue>
{endDate && (
<LabeledValue label="End Time">
<Text textSize="xlarge">{fullTimeFormatter(endDate)}</Text>
</LabeledValue>
)}
<LabeledValue label="Latency">
<Text textSize="xlarge">
{typeof latencyMs === "number" ? (
<LatencyText latencyMs={latencyMs} textSize="xlarge" />
) : (
"--"
)}
</Text>
</LabeledValue>
{tokenCountTotal ? (
<LabeledValue label="Total Tokens" key="tokens">
<TokenCount
tokenCountTotal={tokenCountTotal}
tokenCountPrompt={tokenCountPrompt ?? 0}
tokenCountCompletion={tokenCountCompletion ?? 0}
textSize="xlarge"
/>
</LabeledValue>
) : null}
{hasAnnotations && (
<LabeledValue label="Feedback">
<ul css={annotationListCSS}>
{annotations.map((annotation) => (
<li key={annotation.id}>
<AnnotationLabel annotation={annotation} />
</li>
))}
</ul>
</LabeledValue>
)}
</Flex>
</View>
);
}

function LabeledValue({
label,
children,
}: PropsWithChildren<{ label: string }>) {
return (
<Flex direction="column">
<Text elementType="h3" textSize="medium" color="text-700">
{label}
</Text>
{children}
</Flex>
);
}
81 changes: 71 additions & 10 deletions app/src/pages/trace/SpanDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
TabPane,
Tabs,
Text,
Tooltip,
TooltipTrigger,
View,
ViewProps,
ViewStyleProps,
Expand All @@ -59,9 +61,10 @@ import {
MarkdownDisplayProvider,
} from "@phoenix/components/markdown";
import { SpanKindIcon } from "@phoenix/components/trace";
import { SpanItem } from "@phoenix/components/trace/SpanItem";
import { SpanKindLabel } from "@phoenix/components/trace/SpanKindLabel";
import { useNotifySuccess, useTheme } from "@phoenix/contexts";
import { useFeatureFlag } from "@phoenix/contexts/FeatureFlagsContext";
import { usePreferencesContext } from "@phoenix/contexts/PreferencesContext";
import {
AttributeDocument,
AttributeEmbedding,
Expand All @@ -85,6 +88,7 @@ import {
SpanDetailsQuery$data,
} from "./__generated__/SpanDetailsQuery.graphql";
import { EditSpanAnnotationsButton } from "./EditSpanAnnotationsButton";
import { SpanAside } from "./SpanAside";
import { SpanCodeDropdown } from "./SpanCodeDropdown";
import { SpanFeedback } from "./SpanFeedback";
import { SpanToDatasetExampleDialog } from "./SpanToDatasetExampleDialog";
Expand Down Expand Up @@ -195,6 +199,7 @@ export function SpanDetails({
name
}
...SpanFeedback_annotations
...SpanAside_span
}
}
}
Expand All @@ -214,6 +219,10 @@ export function SpanDetails({
return spanHasException(span);
}, [span]);
const showAnnotations = useFeatureFlag("annotations");
const showSpanAside = usePreferencesContext((store) => store.showSpanAside);
const setShowSpanAside = usePreferencesContext(
(store) => store.setShowSpanAside
);
return (
<Flex direction="column" flex="1 1 auto" height="100%">
<View
Expand All @@ -223,13 +232,17 @@ export function SpanDetails({
paddingEnd="size-200"
flex="none"
>
<Flex
direction="row"
gap="size-200"
justifyContent="space-between"
alignItems="center"
>
<SpanItem {...span} />
<Flex direction="row" gap="size-200" alignItems="center">
<Flex
direction="row"
gap="size-100"
width="100%"
height="100%"
alignItems="center"
>
<SpanKindLabel spanKind={span.spanKind} />
<Text>{span.name}</Text>
</Flex>
<Flex flex="none" direction="row" alignItems="center" gap="size-100">
<SpanCodeDropdown
traceId={span.context.traceId}
Expand All @@ -245,9 +258,35 @@ export function SpanDetails({
</Flex>
</Flex>
</View>
<Tabs>
<Tabs
extra={
<TooltipTrigger placement="start" offset={5}>
<Button
variant="default"
size="compact"
aria-label="Toggle showing span details"
onClick={() => {
setShowSpanAside(!showSpanAside);
}}
icon={
<Icon
svg={showSpanAside ? <Icons.SlideIn /> : <Icons.SlideOut />}
/>
}
/>
<Tooltip>
{showSpanAside ? "Hide Span Details" : "Show Span Details"}
</Tooltip>
</TooltipTrigger>
}
>
<TabPane name={"Info"}>
<SpanInfo span={span} />
<Flex direction="row" height="100%">
<SpanInfoWrap>
<SpanInfo span={span} />
</SpanInfoWrap>
{showSpanAside ? <SpanAside span={span} /> : null}
</Flex>
</TabPane>
<TabPane
name={"Feedback"}
Expand Down Expand Up @@ -287,6 +326,28 @@ export function SpanDetails({
);
}

const spanInfoWrapCSS = css`
flex: 1 1 auto;
overflow-y: auto;
// Overflow fails to take into account padding
Copy link
Contributor

Choose a reason for hiding this comment

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

could this be solved with box-sizing or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried box-sizing and it's not working. Not sure why

& > *:after {
content: "";
display: block;
height: var(--ac-global-dimension-static-size-400);
}
`;

/**
* A wrapper for the span info to style it with the appropriate overflow
*/
function SpanInfoWrap({ children }: PropsWithChildren) {
return (
<div css={spanInfoWrapCSS} data-testid="span-info-wrap">
{children}
</div>
);
}

function AddSpanToDatasetButton({ span }: { span: Span }) {
const [dialog, setDialog] = useState<ReactNode>(null);
const notifySuccess = useNotifySuccess();
Expand Down
Loading
Loading