1521 lines
58 KiB
TypeScript
1521 lines
58 KiB
TypeScript
import {
|
||
ArrowPathIcon,
|
||
BookOpenIcon,
|
||
CheckIcon,
|
||
ChevronUpIcon,
|
||
ClipboardDocumentIcon,
|
||
ClockIcon,
|
||
CloudArrowDownIcon,
|
||
EnvelopeIcon,
|
||
GlobeAltIcon,
|
||
KeyIcon,
|
||
QueueListIcon,
|
||
SignalIcon,
|
||
} from "@heroicons/react/20/solid";
|
||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||
import {
|
||
formatDurationMilliseconds,
|
||
type TaskRunError,
|
||
taskRunErrorEnhancer,
|
||
} from "@trigger.dev/core/v3";
|
||
import { assertNever } from "assert-never";
|
||
import { useEffect } from "react";
|
||
import { typedjson, useTypedFetcher } from "remix-typedjson";
|
||
import { toast } from "sonner";
|
||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||
import { AdminDebugRun } from "~/components/admin/debugRun";
|
||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
|
||
import { Feedback } from "~/components/Feedback";
|
||
import { MachineLabelCombo } from "~/components/MachineLabelCombo";
|
||
import { MachineTooltipInfo } from "~/components/MachineTooltipInfo";
|
||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||
import { Callout } from "~/components/primitives/Callout";
|
||
import { CopyableText } from "~/components/primitives/CopyableText";
|
||
import { CopyTextLink } from "~/components/primitives/CopyTextLink";
|
||
import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime";
|
||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||
import {
|
||
Popover,
|
||
PopoverContent,
|
||
PopoverMenuItem,
|
||
PopoverTrigger,
|
||
} from "~/components/primitives/Popover";
|
||
import * as Property from "~/components/primitives/PropertyTable";
|
||
import { Spinner } from "~/components/primitives/Spinner";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHeader,
|
||
TableHeaderCell,
|
||
TableRow,
|
||
} from "~/components/primitives/Table";
|
||
import { TabButton, TabContainer } from "~/components/primitives/Tabs";
|
||
import { TextLink } from "~/components/primitives/TextLink";
|
||
import { ToastUI } from "~/components/primitives/Toast";
|
||
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
|
||
import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue";
|
||
import { RunTimeline, RunTimelineEvent, SpanTimeline } from "~/components/run/RunTimeline";
|
||
import { AIEmbedSpanDetails, AISpanDetails, AIToolCallSpanDetails } from "~/components/runs/v3/ai";
|
||
import { PacketDisplay } from "~/components/runs/v3/PacketDisplay";
|
||
import { PromptSpanDetails } from "~/components/runs/v3/PromptSpanDetails";
|
||
import { RegionLabel } from "~/components/runs/v3/RegionLabel";
|
||
import { RunIcon } from "~/components/runs/v3/RunIcon";
|
||
import { RunTag } from "~/components/runs/v3/RunTag";
|
||
import { SpanEvents } from "~/components/runs/v3/SpanEvents";
|
||
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
|
||
import { SpanTitle } from "~/components/runs/v3/SpanTitle";
|
||
import { TaskRunAttemptStatusCombo } from "~/components/runs/v3/TaskRunAttemptStatus";
|
||
import {
|
||
descriptionForTaskRunStatus,
|
||
TaskRunStatusCombo,
|
||
} from "~/components/runs/v3/TaskRunStatus";
|
||
import { WaitpointDetailTable } from "~/components/runs/v3/WaitpointDetails";
|
||
import { RuntimeIcon } from "~/components/RuntimeIcon";
|
||
import { SessionStatusCombo } from "~/components/sessions/v1/SessionStatus";
|
||
import { WarmStartCombo } from "~/components/WarmStarts";
|
||
import { useEnvironment } from "~/hooks/useEnvironment";
|
||
import { useOrganization } from "~/hooks/useOrganizations";
|
||
import { useProject } from "~/hooks/useProject";
|
||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||
import { useHasAdminAccess } from "~/hooks/useUser";
|
||
import { redirectWithErrorMessage } from "~/models/message.server";
|
||
import { type Span, SpanPresenter, type SpanRun } from "~/presenters/v3/SpanPresenter.server";
|
||
import { logger } from "~/services/logger.server";
|
||
import { requireUserId } from "~/services/session.server";
|
||
import { cn } from "~/utils/cn";
|
||
import { formatCurrencyAccurate } from "~/utils/numberFormatter";
|
||
import {
|
||
docsPath,
|
||
v3BatchPath,
|
||
v3DeploymentVersionPath,
|
||
v3RunDownloadLogsPath,
|
||
v3RunIdempotencyKeyResetPath,
|
||
v3RunPath,
|
||
v3RunRedirectPath,
|
||
v3RunSpanPath,
|
||
v3RunsPath,
|
||
v3SchedulePath,
|
||
v3SessionPath,
|
||
v3SpanParamsSchema,
|
||
} from "~/utils/pathBuilder";
|
||
import { createTimelineSpanEventsFromSpanEvents } from "~/utils/timelineSpanEvents";
|
||
import type { SpanOverride } from "~/v3/eventRepository/eventRepository.types";
|
||
import { type action as resetIdempotencyKeyAction } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.idempotencyKey.reset";
|
||
import { RealtimeStreamViewer } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route";
|
||
import { CompleteWaitpointForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route";
|
||
|
||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||
const userId = await requireUserId(request);
|
||
const { projectParam, organizationSlug, envParam, runParam, spanParam } =
|
||
v3SpanParamsSchema.parse(params);
|
||
|
||
const url = new URL(request.url);
|
||
const linkedRunId = url.searchParams.get("linkedRunId") ?? undefined;
|
||
|
||
const presenter = new SpanPresenter();
|
||
|
||
try {
|
||
const result = await presenter.call({
|
||
projectSlug: projectParam,
|
||
envSlug: envParam,
|
||
spanId: spanParam,
|
||
runFriendlyId: runParam,
|
||
userId,
|
||
linkedRunId,
|
||
});
|
||
|
||
if (!result) {
|
||
return redirectWithErrorMessage(
|
||
v3RunPath(
|
||
{ slug: organizationSlug },
|
||
{ slug: projectParam },
|
||
{ slug: envParam },
|
||
{ friendlyId: runParam }
|
||
),
|
||
request,
|
||
`Event not found.`
|
||
);
|
||
}
|
||
|
||
// Reconstruct the discriminated union explicitly. Spreading
|
||
// `{ ...result }` collapses the union and loses the
|
||
// `type === "run" | "span"` discriminant downstream in `SpanView`.
|
||
if (result.type === "run") {
|
||
return typedjson({ type: "run" as const, run: result.run });
|
||
}
|
||
return typedjson({ type: "span" as const, span: result.span });
|
||
} catch (error) {
|
||
logger.error("Error loading span", {
|
||
projectParam,
|
||
organizationSlug,
|
||
runParam,
|
||
spanParam,
|
||
linkedRunId,
|
||
error:
|
||
error instanceof Error
|
||
? {
|
||
name: error.name,
|
||
message: error.message,
|
||
stack: error.stack,
|
||
cause:
|
||
error.cause instanceof Error
|
||
? { name: error.cause.name, message: error.cause.message }
|
||
: error.cause,
|
||
}
|
||
: error,
|
||
});
|
||
return redirectWithErrorMessage(
|
||
v3RunPath(
|
||
{ slug: organizationSlug },
|
||
{ slug: projectParam },
|
||
{ slug: envParam },
|
||
{ friendlyId: runParam }
|
||
),
|
||
request,
|
||
`Event not found.`
|
||
);
|
||
}
|
||
};
|
||
|
||
export function SpanView({
|
||
runParam,
|
||
spanId,
|
||
spanOverrides,
|
||
closePanel,
|
||
linkedRunId,
|
||
}: {
|
||
runParam: string;
|
||
spanId: string | undefined;
|
||
spanOverrides?: SpanOverride;
|
||
closePanel?: () => void;
|
||
linkedRunId?: string;
|
||
}) {
|
||
const organization = useOrganization();
|
||
const project = useProject();
|
||
const environment = useEnvironment();
|
||
const fetcher = useTypedFetcher<typeof loader>();
|
||
|
||
useEffect(() => {
|
||
if (spanId === undefined) return;
|
||
const url = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${
|
||
environment.slug
|
||
}/runs/${runParam}/spans/${spanId}${linkedRunId ? `?linkedRunId=${linkedRunId}` : ""}`;
|
||
fetcher.load(url);
|
||
}, [organization.slug, project.slug, environment.slug, runParam, spanId, linkedRunId]);
|
||
|
||
if (spanId === undefined) {
|
||
return null;
|
||
}
|
||
|
||
// Only show loading spinner when there's no data yet, not during revalidation
|
||
if (fetcher.data === undefined) {
|
||
return (
|
||
<div
|
||
className={cn(
|
||
"grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright"
|
||
)}
|
||
>
|
||
<div className="mx-3 flex items-center gap-2 overflow-x-hidden border-b border-grid-dimmed">
|
||
<div className="size-4 bg-grid-dimmed" />
|
||
<div className="h-6 w-[60%] bg-grid-dimmed" />
|
||
</div>
|
||
<div className="flex items-center justify-center">
|
||
<Spinner />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const { type } = fetcher.data;
|
||
|
||
switch (type) {
|
||
case "run": {
|
||
return (
|
||
<RunBody
|
||
run={fetcher.data.run}
|
||
runParam={runParam}
|
||
spanId={spanId}
|
||
closePanel={closePanel}
|
||
/>
|
||
);
|
||
}
|
||
case "span": {
|
||
return (
|
||
<SpanBody
|
||
span={fetcher.data.span}
|
||
spanOverrides={spanOverrides}
|
||
runParam={runParam}
|
||
closePanel={closePanel}
|
||
/>
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
function SpanBody({
|
||
span,
|
||
spanOverrides,
|
||
runParam,
|
||
closePanel,
|
||
}: {
|
||
span: Span;
|
||
spanOverrides?: SpanOverride;
|
||
runParam?: string;
|
||
closePanel?: () => void;
|
||
}) {
|
||
const _organization = useOrganization();
|
||
const _project = useProject();
|
||
const _environment = useEnvironment();
|
||
const { value, replace: _replace } = useSearchParams();
|
||
let tab = value("tab");
|
||
|
||
if (tab === "context") {
|
||
tab = "overview";
|
||
}
|
||
|
||
span = applySpanOverrides(span, spanOverrides);
|
||
|
||
const isAiInspector =
|
||
span.entity?.type === "ai-generation" ||
|
||
span.entity?.type === "ai-summary" ||
|
||
span.entity?.type === "ai-tool-call" ||
|
||
span.entity?.type === "ai-embed" ||
|
||
span.entity?.type === "prompt";
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
"grid h-full max-h-full overflow-hidden bg-background-bright",
|
||
isAiInspector ? "grid-rows-[auto_1fr]" : "grid-rows-[2.5rem_1fr]"
|
||
)}
|
||
>
|
||
<div className="border-b border-grid-bright px-3 pr-2">
|
||
<div className="grid h-10 grid-cols-[minmax(0,1fr)_auto] items-center gap-2">
|
||
<div className="flex min-w-0 items-center gap-1">
|
||
<RunIcon
|
||
name={span.style?.icon}
|
||
spanName={span.message}
|
||
className="size-5 min-h-5 min-w-5"
|
||
/>
|
||
<Header2 className="min-w-0">
|
||
<SpanTitle {...span} size="large" hideAccessory overrideDimmed />
|
||
</Header2>
|
||
</div>
|
||
{runParam && closePanel && (
|
||
<Button
|
||
onClick={closePanel}
|
||
variant="minimal/small"
|
||
TrailingIcon={ExitIcon}
|
||
shortcut={{ key: "esc" }}
|
||
shortcutPosition="before-trailing-icon"
|
||
className="pl-1"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{isAiInspector ? (
|
||
<SpanEntity span={span} />
|
||
) : (
|
||
<div className="scrollbar-gutter-stable overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
|
||
<SpanEntity span={span} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
function applySpanOverrides(span: Span, spanOverrides?: SpanOverride): Span {
|
||
if (!spanOverrides) {
|
||
return span;
|
||
}
|
||
|
||
const newSpan = { ...span };
|
||
|
||
if (spanOverrides.isCancelled) {
|
||
newSpan.isCancelled = true;
|
||
newSpan.isPartial = false;
|
||
newSpan.isError = false;
|
||
} else if (spanOverrides.isError) {
|
||
newSpan.isError = true;
|
||
newSpan.isPartial = false;
|
||
newSpan.isCancelled = false;
|
||
}
|
||
|
||
if (typeof spanOverrides.duration !== "undefined") {
|
||
newSpan.duration = spanOverrides.duration;
|
||
}
|
||
|
||
if (spanOverrides.events) {
|
||
if (newSpan.events) {
|
||
newSpan.events = [...newSpan.events, ...spanOverrides.events];
|
||
} else {
|
||
newSpan.events = spanOverrides.events;
|
||
}
|
||
}
|
||
|
||
return newSpan;
|
||
}
|
||
|
||
function RunBody({
|
||
run,
|
||
runParam,
|
||
spanId,
|
||
closePanel,
|
||
}: {
|
||
run: SpanRun;
|
||
runParam: string;
|
||
spanId: string;
|
||
closePanel?: () => void;
|
||
}) {
|
||
const organization = useOrganization();
|
||
const project = useProject();
|
||
const environment = useEnvironment();
|
||
const isAdmin = useHasAdminAccess();
|
||
const { value, replace } = useSearchParams();
|
||
const tab = value("tab");
|
||
const resetFetcher = useTypedFetcher<typeof resetIdempotencyKeyAction>();
|
||
|
||
return (
|
||
<div className="grid h-full max-h-full grid-rows-[2.5rem_2rem_1fr_minmax(3.25rem,auto)] overflow-hidden bg-background-bright">
|
||
<div className="flex items-center justify-between gap-2 overflow-x-hidden px-3 pr-2">
|
||
<div className="flex items-center gap-1 overflow-x-hidden">
|
||
<RunIcon
|
||
name={
|
||
run.isAgentRun
|
||
? "agent"
|
||
: run.isScheduled
|
||
? "scheduled"
|
||
: run.isCached
|
||
? "task-cached"
|
||
: "task"
|
||
}
|
||
spanName={run.taskIdentifier}
|
||
className="size-5 min-h-5 min-w-5"
|
||
/>
|
||
<Header2
|
||
className={cn(
|
||
"overflow-x-hidden",
|
||
run.isAgentRun ? "text-agents" : run.isScheduled ? "text-schedules" : "text-blue-500"
|
||
)}
|
||
>
|
||
<span className="truncate">
|
||
{run.taskIdentifier}
|
||
{run.isCached ? " (cached)" : null}
|
||
</span>
|
||
</Header2>
|
||
</div>
|
||
{runParam && closePanel && (
|
||
<Button
|
||
onClick={closePanel}
|
||
variant="minimal/small"
|
||
TrailingIcon={ExitIcon}
|
||
shortcut={{ key: "esc" }}
|
||
shortcutPosition="before-trailing-icon"
|
||
className="pl-1"
|
||
/>
|
||
)}
|
||
</div>
|
||
<div className="h-fit overflow-x-auto px-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
|
||
<TabContainer>
|
||
<TabButton
|
||
isActive={!tab || tab === "overview"}
|
||
layoutId="span-run"
|
||
onClick={() => {
|
||
replace({ tab: "overview" });
|
||
}}
|
||
shortcut={{ key: "o" }}
|
||
>
|
||
Overview
|
||
</TabButton>
|
||
<TabButton
|
||
isActive={tab === "detail"}
|
||
layoutId="span-run"
|
||
onClick={() => {
|
||
replace({ tab: "detail" });
|
||
}}
|
||
shortcut={{ key: "d" }}
|
||
>
|
||
Detail
|
||
</TabButton>
|
||
<TabButton
|
||
isActive={tab === "context"}
|
||
layoutId="span-run"
|
||
onClick={() => {
|
||
replace({ tab: "context" });
|
||
}}
|
||
shortcut={{ key: "x" }}
|
||
>
|
||
Context
|
||
</TabButton>
|
||
|
||
<TabButton
|
||
isActive={tab === "metadata"}
|
||
layoutId="span-run"
|
||
onClick={() => {
|
||
replace({ tab: "metadata" });
|
||
}}
|
||
shortcut={{ key: "m" }}
|
||
>
|
||
Metadata
|
||
</TabButton>
|
||
</TabContainer>
|
||
</div>
|
||
<div className="overflow-y-auto px-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
|
||
<div>
|
||
{tab === "detail" ? (
|
||
<div className="flex flex-col gap-4 py-3">
|
||
<Property.Table>
|
||
<Property.Item>
|
||
<Property.Label>Status</Property.Label>
|
||
<Property.Value>
|
||
<SimpleTooltip
|
||
button={<TaskRunStatusCombo status={run.status} />}
|
||
content={descriptionForTaskRunStatus(run.status)}
|
||
disableHoverableContent
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Task</Property.Label>
|
||
<Property.Value>
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink
|
||
to={v3RunsPath(organization, project, environment, {
|
||
tasks: [run.taskIdentifier],
|
||
})}
|
||
>
|
||
<CopyableText
|
||
value={run.taskIdentifier}
|
||
copyValue={run.taskIdentifier}
|
||
asChild
|
||
/>
|
||
</TextLink>
|
||
}
|
||
content={`View runs filtered by ${run.taskIdentifier}`}
|
||
disableHoverableContent
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Run ID</Property.Label>
|
||
<Property.Value>
|
||
<CopyableText value={run.friendlyId} copyValue={run.friendlyId} asChild />
|
||
</Property.Value>
|
||
</Property.Item>
|
||
{run.relationships.root ? (
|
||
run.relationships.root.isParent ? (
|
||
<Property.Item>
|
||
<Property.Label>Root & Parent run</Property.Label>
|
||
<Property.Value>
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink
|
||
to={v3RunSpanPath(
|
||
organization,
|
||
project,
|
||
environment,
|
||
{
|
||
friendlyId: run.relationships.root.friendlyId,
|
||
},
|
||
{ spanId: run.relationships.root.spanId }
|
||
)}
|
||
className="group flex flex-wrap items-center gap-x-1 gap-y-0"
|
||
>
|
||
<CopyableText
|
||
value={run.relationships.root.taskIdentifier}
|
||
copyValue={run.relationships.root.taskIdentifier}
|
||
asChild
|
||
/>
|
||
<span className="break-all text-text-dimmed transition-colors group-hover:text-text-bright/80">
|
||
<CopyableText
|
||
value={run.relationships.root.friendlyId}
|
||
copyValue={run.relationships.root.friendlyId}
|
||
asChild
|
||
/>
|
||
</span>
|
||
</TextLink>
|
||
}
|
||
content={`Jump to root/parent run`}
|
||
disableHoverableContent
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
) : (
|
||
<>
|
||
<Property.Item>
|
||
<Property.Label>Root run</Property.Label>
|
||
<Property.Value>
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink
|
||
to={v3RunSpanPath(
|
||
organization,
|
||
project,
|
||
environment,
|
||
{
|
||
friendlyId: run.relationships.root.friendlyId,
|
||
},
|
||
{ spanId: run.relationships.root.spanId }
|
||
)}
|
||
className="group flex flex-wrap items-center gap-x-1 gap-y-0"
|
||
>
|
||
<CopyableText
|
||
value={run.relationships.root.taskIdentifier}
|
||
copyValue={run.relationships.root.taskIdentifier}
|
||
asChild
|
||
/>
|
||
<span className="break-all text-text-dimmed transition-colors group-hover:text-text-bright/80">
|
||
<CopyableText
|
||
value={run.relationships.root.friendlyId}
|
||
copyValue={run.relationships.root.friendlyId}
|
||
asChild
|
||
/>
|
||
</span>
|
||
</TextLink>
|
||
}
|
||
content={`Jump to root run`}
|
||
disableHoverableContent
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
{run.relationships.parent ? (
|
||
<Property.Item>
|
||
<Property.Label>Parent run</Property.Label>
|
||
<Property.Value>
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink
|
||
to={v3RunSpanPath(
|
||
organization,
|
||
project,
|
||
environment,
|
||
{
|
||
friendlyId: run.relationships.parent.friendlyId,
|
||
},
|
||
{ spanId: run.relationships.parent.spanId }
|
||
)}
|
||
className="group flex flex-wrap items-center gap-x-1 gap-y-0"
|
||
>
|
||
<CopyableText
|
||
value={run.relationships.parent.taskIdentifier}
|
||
copyValue={run.relationships.parent.taskIdentifier}
|
||
asChild
|
||
/>
|
||
<span className="break-all text-text-dimmed transition-colors group-hover:text-text-bright/80">
|
||
<CopyableText
|
||
value={run.relationships.parent.friendlyId}
|
||
copyValue={run.relationships.parent.friendlyId}
|
||
asChild
|
||
/>
|
||
</span>
|
||
</TextLink>
|
||
}
|
||
content={`Jump to parent run`}
|
||
disableHoverableContent
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
) : null}
|
||
</>
|
||
)
|
||
) : null}
|
||
{run.batch && (
|
||
<Property.Item>
|
||
<Property.Label>Batch</Property.Label>
|
||
<Property.Value>
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink to={v3BatchPath(organization, project, environment, run.batch)}>
|
||
<CopyableText
|
||
value={run.batch.friendlyId}
|
||
copyValue={run.batch.friendlyId}
|
||
asChild
|
||
/>
|
||
</TextLink>
|
||
}
|
||
content={`View batches filtered by ${run.batch.friendlyId}`}
|
||
disableHoverableContent
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
)}
|
||
{run.session && (
|
||
<Property.Item>
|
||
<Property.Label>Session</Property.Label>
|
||
<Property.Value>
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink
|
||
to={v3SessionPath(organization, project, environment, {
|
||
friendlyId: run.session.friendlyId,
|
||
})}
|
||
className="group flex flex-wrap items-center gap-x-2 gap-y-0"
|
||
>
|
||
<CopyableText
|
||
value={run.session.externalId ?? run.session.friendlyId}
|
||
copyValue={run.session.externalId ?? run.session.friendlyId}
|
||
asChild
|
||
/>
|
||
<SessionStatusCombo status={run.session.status} />
|
||
</TextLink>
|
||
}
|
||
content={`Jump to session (${run.session.reason})`}
|
||
disableHoverableContent
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
)}
|
||
<Property.Item>
|
||
<Property.Label>
|
||
<div className="flex items-center justify-between">
|
||
<span className="flex items-center gap-1">
|
||
Idempotency
|
||
<InfoIconTooltip
|
||
content={
|
||
<div className="flex max-w-xs flex-col gap-3 p-1 pb-2">
|
||
<div>
|
||
<div className="mb-0.5 flex items-center gap-1.5">
|
||
<KeyIcon className="size-4 text-text-dimmed" />
|
||
<Header3>Idempotency keys</Header3>
|
||
</div>
|
||
<Paragraph variant="small" className="text-wrap! text-text-dimmed">
|
||
Prevent duplicate task runs. If you trigger a task with the same
|
||
key twice, the second request returns the original run.
|
||
</Paragraph>
|
||
</div>
|
||
<div>
|
||
<div className="mb-1 flex items-center gap-1">
|
||
<GlobeAltIcon className="size-4 text-blue-500" />
|
||
<Header3>Scope</Header3>
|
||
</div>
|
||
<div className="flex flex-col gap-0.5 text-sm text-text-dimmed">
|
||
<div>Global: applies across all runs</div>
|
||
<div>Run: unique to a parent run</div>
|
||
<div>Attempt: unique to a specific attempt</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="mb-1 flex items-center gap-1">
|
||
<SignalIcon className="size-4 text-success" />
|
||
<Header3>Status</Header3>
|
||
</div>
|
||
<div className="flex flex-col gap-0.5 text-sm text-text-dimmed">
|
||
<div>Active: duplicates are blocked</div>
|
||
<div>Expired: the TTL has passed</div>
|
||
<div>Inactive: the key was reset or cleared</div>
|
||
</div>
|
||
</div>
|
||
<LinkButton
|
||
to={docsPath("idempotency")}
|
||
variant="docs/small"
|
||
LeadingIcon={BookOpenIcon}
|
||
>
|
||
Read docs
|
||
</LinkButton>
|
||
</div>
|
||
}
|
||
/>
|
||
</span>
|
||
{run.idempotencyKeyStatus === "active" ? (
|
||
<resetFetcher.Form
|
||
method="post"
|
||
action={v3RunIdempotencyKeyResetPath(organization, project, environment, {
|
||
friendlyId: run.friendlyId,
|
||
})}
|
||
>
|
||
<Button
|
||
type="submit"
|
||
variant="minimal/small"
|
||
LeadingIcon={ArrowPathIcon}
|
||
disabled={resetFetcher.state === "submitting"}
|
||
>
|
||
{resetFetcher.state === "submitting" ? "Resetting..." : "Reset"}
|
||
</Button>
|
||
</resetFetcher.Form>
|
||
) : run.idempotencyKeyStatus === "expired" ? (
|
||
<span className="flex items-center gap-1 text-xs text-amber-500">
|
||
<ClockIcon className="size-4" />
|
||
Expired
|
||
</span>
|
||
) : run.idempotencyKeyStatus === "inactive" ? (
|
||
<span className="text-xs text-text-dimmed">Inactive</span>
|
||
) : null}
|
||
</div>
|
||
</Property.Label>
|
||
<Property.Value>
|
||
{run.idempotencyKeyStatus ? (
|
||
<div className="flex flex-col gap-0.5">
|
||
<div>
|
||
<span className="text-text-dimmed">Key: </span>
|
||
{run.idempotencyKey ? (
|
||
<CopyableText
|
||
value={run.idempotencyKey}
|
||
copyValue={run.idempotencyKey}
|
||
asChild
|
||
className="max-h-5"
|
||
/>
|
||
) : (
|
||
"–"
|
||
)}
|
||
</div>
|
||
<div>
|
||
<span className="text-text-dimmed">Scope: </span>
|
||
{run.idempotencyKeyScope ?? "–"}
|
||
</div>
|
||
<div>
|
||
<span className="text-text-dimmed">
|
||
{run.idempotencyKeyStatus === "expired" ? "Expired: " : "Expires: "}
|
||
</span>
|
||
{run.idempotencyKeyExpiresAt ? (
|
||
<DateTime date={run.idempotencyKeyExpiresAt} />
|
||
) : (
|
||
"–"
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
"–"
|
||
)}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Debounce</Property.Label>
|
||
<Property.Value>
|
||
{run.debounce ? (
|
||
<div>
|
||
<div className="break-all">Key: {run.debounce.key}</div>
|
||
<div>Delay: {run.debounce.delay}</div>
|
||
</div>
|
||
) : (
|
||
"–"
|
||
)}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Version</Property.Label>
|
||
<Property.Value>
|
||
{run.version ? (
|
||
environment.type === "DEVELOPMENT" ? (
|
||
<CopyableText value={run.version} copyValue={run.version} asChild />
|
||
) : (
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink
|
||
to={v3DeploymentVersionPath(
|
||
organization,
|
||
project,
|
||
environment,
|
||
run.version
|
||
)}
|
||
className="group flex flex-wrap items-center gap-x-1 gap-y-0"
|
||
>
|
||
<CopyableText value={run.version} copyValue={run.version} asChild />
|
||
</TextLink>
|
||
}
|
||
content={"Jump to deployment"}
|
||
/>
|
||
)
|
||
) : (
|
||
<span className="flex items-center gap-1">
|
||
<span>Never started</span>
|
||
<InfoIconTooltip
|
||
content={"Runs get locked to the latest version when they start."}
|
||
contentClassName="normal-case tracking-normal"
|
||
/>
|
||
</span>
|
||
)}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>SDK version</Property.Label>
|
||
<Property.Value>
|
||
{run.sdkVersion ? (
|
||
run.sdkVersion
|
||
) : (
|
||
<span className="flex items-center gap-1">
|
||
<span>Never started</span>
|
||
<InfoIconTooltip
|
||
content={"Runs get locked to the latest version when they start."}
|
||
contentClassName="normal-case tracking-normal"
|
||
/>
|
||
</span>
|
||
)}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Runtime</Property.Label>
|
||
<Property.Value>
|
||
<RuntimeIcon
|
||
runtime={run.runtime}
|
||
runtimeVersion={run.runtimeVersion}
|
||
withLabel
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Test run</Property.Label>
|
||
<Property.Value>
|
||
{run.isTest ? <CheckIcon className="size-4 text-text-dimmed" /> : "–"}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
{run.replayedFromTaskRunFriendlyId && (
|
||
<Property.Item>
|
||
<Property.Label>Replayed from</Property.Label>
|
||
<Property.Value>
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink
|
||
to={v3RunRedirectPath(organization, project, {
|
||
friendlyId: run.replayedFromTaskRunFriendlyId,
|
||
})}
|
||
>
|
||
<CopyableText
|
||
value={run.replayedFromTaskRunFriendlyId}
|
||
copyValue={run.replayedFromTaskRunFriendlyId}
|
||
asChild
|
||
/>
|
||
</TextLink>
|
||
}
|
||
content={`Jump to replayed run`}
|
||
disableHoverableContent
|
||
/>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
)}
|
||
{environment && (
|
||
<Property.Item>
|
||
<Property.Label>Environment</Property.Label>
|
||
<Property.Value>
|
||
<EnvironmentCombo environment={environment} />
|
||
</Property.Value>
|
||
</Property.Item>
|
||
)}
|
||
|
||
{run.schedule && (
|
||
<Property.Item>
|
||
<Property.Label>Schedule</Property.Label>
|
||
<Property.Value>
|
||
<div>
|
||
<div className="flex items-center gap-1">
|
||
<span className="font-mono">{run.schedule.generatorExpression}</span>
|
||
<span>({run.schedule.timezone})</span>
|
||
</div>
|
||
<SimpleTooltip
|
||
button={
|
||
<TextLink
|
||
to={v3SchedulePath(organization, project, environment, run.schedule)}
|
||
>
|
||
{run.schedule.description}
|
||
</TextLink>
|
||
}
|
||
content={`Go to schedule ${run.schedule.friendlyId}`}
|
||
/>
|
||
</div>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
)}
|
||
<Property.Item>
|
||
<Property.Label>Queue</Property.Label>
|
||
<Property.Value>
|
||
<div>Name: {run.queue.name}</div>
|
||
<div>
|
||
Concurrency key: {run.queue.concurrencyKey ? run.queue.concurrencyKey : "–"}
|
||
</div>
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Time to live (TTL)</Property.Label>
|
||
<Property.Value>{run.ttl ?? "–"}</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Tags</Property.Label>
|
||
<Property.Value>
|
||
{run.tags.length === 0 ? (
|
||
"–"
|
||
) : (
|
||
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs">
|
||
{run.tags.map((tag: string) => (
|
||
<RunTag
|
||
key={tag}
|
||
tag={tag}
|
||
to={v3RunsPath(organization, project, environment, { tags: [tag] })}
|
||
tooltip={`Filter runs by ${tag}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Max duration</Property.Label>
|
||
<Property.Value>
|
||
{run.maxDurationInSeconds
|
||
? `${run.maxDurationInSeconds}s (${formatDurationMilliseconds(
|
||
run.maxDurationInSeconds * 1000,
|
||
{ style: "short" }
|
||
)})`
|
||
: "–"}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>
|
||
<span className="flex items-center gap-1">
|
||
Machine
|
||
<InfoIconTooltip content={<MachineTooltipInfo />} />
|
||
</span>
|
||
</Property.Label>
|
||
<Property.Value className="-ml-0.5">
|
||
<MachineLabelCombo preset={run.machinePreset} />
|
||
</Property.Value>
|
||
</Property.Item>
|
||
{run.region && (
|
||
<Property.Item>
|
||
<Property.Label>Region</Property.Label>
|
||
<Property.Value>
|
||
<RegionLabel region={run.region} />
|
||
</Property.Value>
|
||
</Property.Item>
|
||
)}
|
||
<Property.Item>
|
||
<Property.Label>Run invocation cost</Property.Label>
|
||
<Property.Value>
|
||
{run.baseCostInCents > 0
|
||
? formatCurrencyAccurate(run.baseCostInCents / 100)
|
||
: "–"}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Compute cost</Property.Label>
|
||
<Property.Value>
|
||
{run.costInCents > 0 ? formatCurrencyAccurate(run.costInCents / 100) : "–"}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Total cost</Property.Label>
|
||
<Property.Value>
|
||
{run.costInCents > 0
|
||
? formatCurrencyAccurate((run.baseCostInCents + run.costInCents) / 100)
|
||
: "–"}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Usage duration</Property.Label>
|
||
<Property.Value>
|
||
{run.usageDurationMs > 0
|
||
? formatDurationMilliseconds(run.usageDurationMs, { style: "short" })
|
||
: "–"}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Run Engine</Property.Label>
|
||
<Property.Value>{run.engine}</Property.Value>
|
||
</Property.Item>
|
||
{run.externalTraceId && (
|
||
<Property.Item>
|
||
<Property.Label>External Trace ID</Property.Label>
|
||
<Property.Value>{run.externalTraceId}</Property.Value>
|
||
</Property.Item>
|
||
)}
|
||
{isAdmin && (
|
||
<div className="border-t border-yellow-500/50 pt-2">
|
||
<Paragraph spacing variant="small" className="text-yellow-500">
|
||
Admin only
|
||
</Paragraph>
|
||
<Property.Item>
|
||
<Property.Label>Buffered</Property.Label>
|
||
<Property.Value>{run.isBuffered ? "Yes" : "No"}</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Worker queue</Property.Label>
|
||
<Property.Value>{run.workerQueue}</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Trace ID</Property.Label>
|
||
<Property.Value>{run.traceId}</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Span ID</Property.Label>
|
||
<Property.Value>{run.spanId}</Property.Value>
|
||
</Property.Item>
|
||
<Property.Item>
|
||
<Property.Label>Task event store</Property.Label>
|
||
<Property.Value>{run.taskEventStore}</Property.Value>
|
||
</Property.Item>
|
||
</div>
|
||
)}
|
||
</Property.Table>
|
||
</div>
|
||
) : tab === "context" ? (
|
||
<div className="flex flex-col gap-4 py-3">
|
||
<CodeBlock code={run.context} showLineNumbers={false} showTextWrapping />
|
||
</div>
|
||
) : tab === "metadata" ? (
|
||
<div className="flex flex-col gap-4 py-3">
|
||
{run.metadata ? (
|
||
<CodeBlock code={run.metadata} showLineNumbers={false} showTextWrapping />
|
||
) : (
|
||
<Callout to="https://trigger.dev/docs/runs/metadata" variant="docs">
|
||
No metadata set for this run. View our metadata documentation to learn more.
|
||
</Callout>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-col gap-4 pt-3">
|
||
<div className="border-b border-grid-bright pb-3">
|
||
<SimpleTooltip
|
||
button={<TaskRunStatusCombo status={run.status} className="text-sm" />}
|
||
content={descriptionForTaskRunStatus(run.status)}
|
||
/>
|
||
</div>
|
||
<RunTimeline run={run} />
|
||
|
||
{run.error && <RunError error={run.error} />}
|
||
|
||
{run.payload !== undefined && (
|
||
<PacketDisplay data={run.payload} dataType={run.payloadType} title="Payload" />
|
||
)}
|
||
|
||
{run.error === undefined && run.output !== undefined ? (
|
||
<PacketDisplay data={run.output} dataType={run.outputType} title="Output" />
|
||
) : null}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-grid-dimmed px-2 py-2">
|
||
<div className="flex items-center gap-4">
|
||
{run.friendlyId !== runParam && (
|
||
<LinkButton
|
||
to={v3RunSpanPath(
|
||
organization,
|
||
project,
|
||
environment,
|
||
{ friendlyId: run.friendlyId },
|
||
{ spanId: run.spanId }
|
||
)}
|
||
variant="minimal/medium"
|
||
LeadingIcon={QueueListIcon}
|
||
shortcut={{ key: "f" }}
|
||
>
|
||
{run.isCached ? "Jump to original run" : "Focus on run"}
|
||
</LinkButton>
|
||
)}
|
||
{!run.isBuffered && <AdminDebugRun friendlyId={run.friendlyId} />}
|
||
</div>
|
||
<div className="flex items-center">
|
||
{run.logsDeletedAt === null ? (
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="secondary/medium"
|
||
LeadingIcon={CloudArrowDownIcon}
|
||
leadingIconClassName="text-indigo-400"
|
||
TrailingIcon={ChevronUpIcon}
|
||
>
|
||
Export trace
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="min-w-[180px] p-1" align="end">
|
||
<TraceExportMenuItems runParam={runParam} />
|
||
</PopoverContent>
|
||
</Popover>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Trace export menu items: copy the trace as Markdown (for pasting into an AI
|
||
// assistant) plus a download per format. The export route streams `?format=`
|
||
// server-side.
|
||
function TraceExportMenuItems({ runParam }: { runParam: string }) {
|
||
const downloadPath = v3RunDownloadLogsPath({ friendlyId: runParam });
|
||
|
||
const copyForAI = async () => {
|
||
try {
|
||
// Hand the clipboard a ClipboardItem backed by a promise so access is
|
||
// reserved synchronously during the click. The fetch can then take as long
|
||
// as a large trace needs without the browser revoking the transient user
|
||
// activation, which a fetch-then-writeText sequence trips (notably Safari
|
||
// and Firefox).
|
||
const text = fetch(`${downloadPath}?format=markdown`, { credentials: "include" }).then(
|
||
async (response) => {
|
||
if (!response.ok) {
|
||
throw new Error(`Request failed with ${response.status}`);
|
||
}
|
||
return new Blob([await response.text()], { type: "text/plain" });
|
||
}
|
||
);
|
||
|
||
await navigator.clipboard.write([new ClipboardItem({ "text/plain": text })]);
|
||
toast.custom((t) => (
|
||
<ToastUI variant="success" message="Copied trace as Markdown" t={t as string} />
|
||
));
|
||
} catch {
|
||
toast.custom((t) => (
|
||
<ToastUI variant="error" message="Couldn't copy the trace" t={t as string} />
|
||
));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<PopoverMenuItem
|
||
title="Copy for AI"
|
||
icon={ClipboardDocumentIcon}
|
||
leadingIconClassName="text-emerald-500"
|
||
onClick={copyForAI}
|
||
/>
|
||
<PopoverMenuItem
|
||
to={`${downloadPath}?format=markdown`}
|
||
title="Download · Markdown"
|
||
icon={CloudArrowDownIcon}
|
||
leadingIconClassName="text-indigo-500"
|
||
openInNewTab
|
||
/>
|
||
<PopoverMenuItem
|
||
to={`${downloadPath}?format=log`}
|
||
title="Download · Log"
|
||
icon={CloudArrowDownIcon}
|
||
leadingIconClassName="text-indigo-500"
|
||
openInNewTab
|
||
/>
|
||
<PopoverMenuItem
|
||
to={`${downloadPath}?format=jsonl`}
|
||
title="Download · JSON Lines"
|
||
icon={CloudArrowDownIcon}
|
||
leadingIconClassName="text-indigo-500"
|
||
openInNewTab
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function RunError({ error }: { error: TaskRunError }) {
|
||
const enhancedError = taskRunErrorEnhancer(error);
|
||
|
||
switch (enhancedError.type) {
|
||
case "STRING_ERROR":
|
||
return (
|
||
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
|
||
<Header3 className="text-rose-500">Error</Header3>
|
||
<Callout variant="error">{enhancedError.raw}</Callout>
|
||
</div>
|
||
);
|
||
case "CUSTOM_ERROR": {
|
||
return (
|
||
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
|
||
<CodeBlock
|
||
showCopyButton={false}
|
||
showLineNumbers={false}
|
||
code={enhancedError.raw}
|
||
maxLines={20}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
case "BUILT_IN_ERROR":
|
||
case "INTERNAL_ERROR": {
|
||
const name = "name" in enhancedError ? enhancedError.name : enhancedError.code;
|
||
return (
|
||
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
|
||
<Header3 className="text-rose-500">{name}</Header3>
|
||
{enhancedError.message && (
|
||
<Callout variant="error">
|
||
<pre className="text-wrap font-sans text-sm font-normal text-rose-200 [word-break:break-word]">
|
||
{enhancedError.message}
|
||
</pre>
|
||
</Callout>
|
||
)}
|
||
{enhancedError.link &&
|
||
(enhancedError.link.magic === "CONTACT_FORM" ? (
|
||
<Feedback
|
||
button={
|
||
<Button
|
||
variant="tertiary/medium"
|
||
LeadingIcon={EnvelopeIcon}
|
||
leadingIconClassName="text-blue-400"
|
||
fullWidth
|
||
textAlignLeft
|
||
>
|
||
{enhancedError.link.name}
|
||
</Button>
|
||
}
|
||
/>
|
||
) : (
|
||
<Callout variant="docs" to={enhancedError.link.href}>
|
||
{enhancedError.link.name}
|
||
</Callout>
|
||
))}
|
||
{enhancedError.stackTrace && (
|
||
<CodeBlock
|
||
showCopyButton={false}
|
||
showLineNumbers={false}
|
||
code={enhancedError.stackTrace}
|
||
maxLines={20}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
}
|
||
function SpanEntity({ span }: { span: Span }) {
|
||
const isAdmin = useHasAdminAccess();
|
||
|
||
const organization = useOrganization();
|
||
const project = useProject();
|
||
const environment = useEnvironment();
|
||
|
||
if (!span.entity) {
|
||
//normal span
|
||
return (
|
||
<div className="flex flex-col gap-4 p-3">
|
||
{span.level === "TRACE" ? (
|
||
<>
|
||
<div className="border-b border-grid-bright pb-3">
|
||
<TaskRunAttemptStatusCombo
|
||
status={
|
||
span.isCancelled
|
||
? "CANCELED"
|
||
: span.isError
|
||
? "FAILED"
|
||
: span.isPartial
|
||
? "EXECUTING"
|
||
: "COMPLETED"
|
||
}
|
||
className="text-sm"
|
||
/>
|
||
</div>
|
||
<SpanTimeline
|
||
startTime={new Date(span.startTime)}
|
||
duration={span.duration}
|
||
inProgress={span.isPartial}
|
||
isError={span.isError}
|
||
events={createTimelineSpanEventsFromSpanEvents(span.events, isAdmin)}
|
||
/>
|
||
</>
|
||
) : (
|
||
<div className="min-w-fit max-w-80">
|
||
<RunTimelineEvent
|
||
title="Timestamp"
|
||
subtitle={<DateTimeAccurate date={span.startTime} />}
|
||
variant="dot-solid"
|
||
/>
|
||
</div>
|
||
)}
|
||
<Property.Table>
|
||
<Property.Item>
|
||
<Property.Label className="flex items-center justify-between">
|
||
<span>Message</span>
|
||
<CopyTextLink value={span.message} />
|
||
</Property.Label>
|
||
<Property.Value className="whitespace-pre-wrap wrap-break-word">
|
||
{span.message}
|
||
</Property.Value>
|
||
</Property.Item>
|
||
</Property.Table>
|
||
{span.events.length > 0 && <SpanEvents spanEvents={span.events} />}
|
||
{span.properties !== undefined ? (
|
||
<CodeBlock
|
||
rowTitle="Properties"
|
||
code={span.properties}
|
||
maxLines={20}
|
||
showLineNumbers={false}
|
||
showCopyButton
|
||
showTextWrapping
|
||
showOpenInModal
|
||
/>
|
||
) : null}
|
||
{span.resourceProperties !== undefined ? (
|
||
<CodeBlock
|
||
rowTitle="Resource properties"
|
||
code={span.resourceProperties}
|
||
maxLines={20}
|
||
showLineNumbers={false}
|
||
showCopyButton
|
||
showTextWrapping
|
||
showOpenInModal
|
||
/>
|
||
) : null}
|
||
{span.triggeredRuns.length > 0 && (
|
||
<div className="flex flex-col gap-1.5">
|
||
<Header3>Runs</Header3>
|
||
<Table containerClassName="max-h-50">
|
||
<TableHeader className="bg-background-bright">
|
||
<TableRow>
|
||
<TableHeaderCell>ID</TableHeaderCell>
|
||
<TableHeaderCell>Task</TableHeaderCell>
|
||
<TableHeaderCell>Status</TableHeaderCell>
|
||
<TableHeaderCell>Created</TableHeaderCell>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{span.triggeredRuns.map((run) => {
|
||
const path = v3RunSpanPath(
|
||
organization,
|
||
project,
|
||
environment,
|
||
{ friendlyId: run.friendlyId },
|
||
{ spanId: run.spanId }
|
||
);
|
||
return (
|
||
<TableRow key={run.friendlyId}>
|
||
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
|
||
<TruncatedCopyableValue value={run.friendlyId} />
|
||
</TableCell>
|
||
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
|
||
{run.taskIdentifier}
|
||
</TableCell>
|
||
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
|
||
<TaskRunStatusCombo status={run.status} />
|
||
</TableCell>
|
||
<TableCell to={path} actionClassName="py-1.5" rowHoverStyle="bright">
|
||
<DateTimeAccurate date={run.createdAt} hour12={false} hideDate={true} />
|
||
</TableCell>
|
||
</TableRow>
|
||
);
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
switch (span.entity.type) {
|
||
case "attempt": {
|
||
return (
|
||
<div className="flex flex-col gap-4 p-3">
|
||
<div className="border-b border-grid-bright pb-3">
|
||
<TaskRunAttemptStatusCombo
|
||
status={
|
||
span.isCancelled
|
||
? "CANCELED"
|
||
: span.isError
|
||
? "FAILED"
|
||
: span.isPartial
|
||
? "EXECUTING"
|
||
: "COMPLETED"
|
||
}
|
||
className="text-sm"
|
||
/>
|
||
</div>
|
||
<SpanTimeline
|
||
startTime={new Date(span.startTime)}
|
||
duration={span.duration}
|
||
inProgress={span.isPartial}
|
||
isError={span.isError}
|
||
events={createTimelineSpanEventsFromSpanEvents(span.events, isAdmin)}
|
||
/>
|
||
{span.entity.object.isWarmStart !== undefined ? (
|
||
<WarmStartCombo
|
||
isWarmStart={span.entity.object.isWarmStart}
|
||
showTooltip
|
||
className="my-3"
|
||
/>
|
||
) : null}
|
||
{span.events.length > 0 && <SpanEvents spanEvents={span.events} />}
|
||
{span.properties !== undefined ? (
|
||
<CodeBlock
|
||
rowTitle="Properties"
|
||
code={span.properties}
|
||
maxLines={20}
|
||
showLineNumbers={false}
|
||
showCopyButton
|
||
showTextWrapping
|
||
showOpenInModal
|
||
/>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
case "waitpoint": {
|
||
return (
|
||
<div className="grid h-full grid-rows-[1fr_auto]">
|
||
<div className="flex flex-col gap-4 overflow-y-auto px-3 pt-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
|
||
<div>
|
||
<Header2>Waitpoint</Header2>
|
||
<Paragraph variant="small">
|
||
A waitpoint pauses your code from continuing until the conditions are met.{" "}
|
||
<TextLink to={docsPath("wait")}>View docs</TextLink>.
|
||
</Paragraph>
|
||
</div>
|
||
<WaitpointDetailTable waitpoint={span.entity.object} linkToList />
|
||
</div>
|
||
{span.entity.object.status === "WAITING" && (
|
||
<div>
|
||
<CompleteWaitpointForm waitpoint={span.entity.object} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
case "realtime-stream": {
|
||
return (
|
||
<RealtimeStreamViewer
|
||
runId={span.entity.object.runId}
|
||
streamKey={span.entity.object.streamKey}
|
||
metadata={span.entity.object.metadata}
|
||
displayName={span.entity.object.displayName}
|
||
/>
|
||
);
|
||
}
|
||
case "ai-generation":
|
||
case "ai-summary": {
|
||
return (
|
||
<AISpanDetails
|
||
aiData={span.entity.object}
|
||
promptVersionData={span.entity.promptVersionData}
|
||
rawProperties={
|
||
typeof span.properties === "string"
|
||
? span.properties
|
||
: span.properties != null
|
||
? JSON.stringify(span.properties, null, 2)
|
||
: undefined
|
||
}
|
||
startTime={span.startTime}
|
||
duration={span.duration}
|
||
/>
|
||
);
|
||
}
|
||
case "ai-tool-call": {
|
||
return (
|
||
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
|
||
<div className="px-3">
|
||
<SpanHorizontalTimeline startTime={span.startTime} duration={span.duration} />
|
||
</div>
|
||
<AIToolCallSpanDetails data={span.entity.object} />
|
||
</div>
|
||
);
|
||
}
|
||
case "ai-embed": {
|
||
return (
|
||
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
|
||
<div className="px-3">
|
||
<SpanHorizontalTimeline startTime={span.startTime} duration={span.duration} />
|
||
</div>
|
||
<AIEmbedSpanDetails data={span.entity.object} />
|
||
</div>
|
||
);
|
||
}
|
||
case "prompt": {
|
||
return (
|
||
<PromptSpanDetails
|
||
promptData={span.entity.object}
|
||
startTime={span.startTime}
|
||
duration={span.duration}
|
||
/>
|
||
);
|
||
}
|
||
default: {
|
||
assertNever(span.entity);
|
||
}
|
||
}
|
||
}
|