Skip to content

Commit d02eef1

Browse files
committed
feat(webapp,clickhouse): export run traces as log, markdown, or jsonl
Adds a trace export to the run page: copy a run's trace to the clipboard as Markdown for pasting into an AI assistant, or download it as a flat log, a Markdown table, or JSON Lines. The export streams events straight from the store one at a time and never builds the span tree in memory, so arbitrarily large traces export with bounded memory.
1 parent 707bf1a commit d02eef1

15 files changed

Lines changed: 897 additions & 181 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Export a run's full trace from the run page as a downloadable Log, Markdown, or JSON Lines file, or copy it to the clipboard for pasting into an AI assistant. The export streams straight from the store, so even very large runs export reliably.

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 72 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
ArrowPathIcon,
3-
ArrowRightIcon,
43
BookOpenIcon,
54
CheckIcon,
65
ChevronUpIcon,
6+
ClipboardDocumentIcon,
77
ClockIcon,
88
CloudArrowDownIcon,
99
EnvelopeIcon,
@@ -42,6 +42,8 @@ import {
4242
PopoverMenuItem,
4343
PopoverTrigger,
4444
} from "~/components/primitives/Popover";
45+
import { ToastUI } from "~/components/primitives/Toast";
46+
import { toast } from "sonner";
4547
import * as Property from "~/components/primitives/PropertyTable";
4648
import { Spinner } from "~/components/primitives/Spinner";
4749
import {
@@ -79,7 +81,6 @@ import { useOrganization } from "~/hooks/useOrganizations";
7981
import { useProject } from "~/hooks/useProject";
8082
import { useSearchParams } from "~/hooks/useSearchParam";
8183
import { useHasAdminAccess } from "~/hooks/useUser";
82-
import { useCanViewLogsPage } from "~/hooks/useCanViewLogsPage";
8384
import { redirectWithErrorMessage } from "~/models/message.server";
8485
import { type Span, SpanPresenter, type SpanRun } from "~/presenters/v3/SpanPresenter.server";
8586
import { logger } from "~/services/logger.server";
@@ -91,7 +92,6 @@ import {
9192
v3BatchPath,
9293
v3SessionPath,
9394
v3DeploymentVersionPath,
94-
v3LogsPath,
9595
v3RunDownloadLogsPath,
9696
v3RunIdempotencyKeyResetPath,
9797
v3RunPath,
@@ -386,7 +386,6 @@ function RunBody({
386386
const { value, replace } = useSearchParams();
387387
const tab = value("tab");
388388
const resetFetcher = useTypedFetcher<typeof resetIdempotencyKeyAction>();
389-
const canViewLogsPage = useCanViewLogsPage();
390389

391390
return (
392391
<div className="grid h-full max-h-full grid-rows-[2.5rem_2rem_1fr_minmax(3.25rem,auto)] overflow-hidden bg-background-bright">
@@ -1105,64 +1104,84 @@ function RunBody({
11051104
</div>
11061105
<div className="flex items-center">
11071106
{run.logsDeletedAt === null ? (
1108-
canViewLogsPage ? (
1109-
<div className="flex">
1110-
<LinkButton
1111-
to={`${v3LogsPath(organization, project, environment)}?runId=${runParam}&from=${
1112-
new Date(run.createdAt).getTime() - 60000
1113-
}`}
1107+
<Popover>
1108+
<PopoverTrigger asChild>
1109+
<Button
11141110
variant="secondary/medium"
1115-
className="rounded-r-none border-r-0"
1111+
LeadingIcon={CloudArrowDownIcon}
1112+
leadingIconClassName="text-indigo-400"
1113+
TrailingIcon={ChevronUpIcon}
11161114
>
1117-
View logs
1118-
</LinkButton>
1119-
<Popover>
1120-
<PopoverTrigger asChild>
1121-
<Button
1122-
variant="secondary/medium"
1123-
className="rounded-l-none border-l-charcoal-700 px-1.5"
1124-
>
1125-
<ChevronUpIcon className="size-4 transition group-hover/button:text-text-bright" />
1126-
</Button>
1127-
</PopoverTrigger>
1128-
<PopoverContent className="min-w-[140px] p-1" align="end">
1129-
<PopoverMenuItem
1130-
to={`${v3LogsPath(
1131-
organization,
1132-
project,
1133-
environment
1134-
)}?runId=${runParam}&from=${new Date(run.createdAt).getTime() - 60000}`}
1135-
title="View logs"
1136-
icon={ArrowRightIcon}
1137-
leadingIconClassName="text-blue-500"
1138-
/>
1139-
<PopoverMenuItem
1140-
to={v3RunDownloadLogsPath({ friendlyId: runParam })}
1141-
title="Download logs"
1142-
icon={CloudArrowDownIcon}
1143-
leadingIconClassName="text-indigo-500"
1144-
openInNewTab
1145-
/>
1146-
</PopoverContent>
1147-
</Popover>
1148-
</div>
1149-
) : (
1150-
<LinkButton
1151-
to={v3RunDownloadLogsPath({ friendlyId: runParam })}
1152-
LeadingIcon={CloudArrowDownIcon}
1153-
leadingIconClassName="text-indigo-400"
1154-
variant="secondary/medium"
1155-
>
1156-
Download logs
1157-
</LinkButton>
1158-
)
1115+
Export trace
1116+
</Button>
1117+
</PopoverTrigger>
1118+
<PopoverContent className="min-w-[180px] p-1" align="end">
1119+
<TraceExportMenuItems runParam={runParam} />
1120+
</PopoverContent>
1121+
</Popover>
11591122
) : null}
11601123
</div>
11611124
</div>
11621125
</div>
11631126
);
11641127
}
11651128

1129+
// Trace export menu items: copy the trace as Markdown (for pasting into an AI
1130+
// assistant) plus a download per format. The export route streams `?format=`
1131+
// server-side.
1132+
function TraceExportMenuItems({ runParam }: { runParam: string }) {
1133+
const downloadPath = v3RunDownloadLogsPath({ friendlyId: runParam });
1134+
1135+
const copyForAI = async () => {
1136+
try {
1137+
const response = await fetch(`${downloadPath}?format=markdown`, { credentials: "include" });
1138+
if (!response.ok) {
1139+
throw new Error(`Request failed with ${response.status}`);
1140+
}
1141+
await navigator.clipboard.writeText(await response.text());
1142+
toast.custom((t) => (
1143+
<ToastUI variant="success" message="Copied trace as Markdown" t={t as string} />
1144+
));
1145+
} catch {
1146+
toast.custom((t) => (
1147+
<ToastUI variant="error" message="Couldn't copy the trace" t={t as string} />
1148+
));
1149+
}
1150+
};
1151+
1152+
return (
1153+
<>
1154+
<PopoverMenuItem
1155+
title="Copy for AI"
1156+
icon={ClipboardDocumentIcon}
1157+
leadingIconClassName="text-emerald-500"
1158+
onClick={copyForAI}
1159+
/>
1160+
<PopoverMenuItem
1161+
to={`${downloadPath}?format=markdown`}
1162+
title="Download · Markdown"
1163+
icon={CloudArrowDownIcon}
1164+
leadingIconClassName="text-indigo-500"
1165+
openInNewTab
1166+
/>
1167+
<PopoverMenuItem
1168+
to={`${downloadPath}?format=log`}
1169+
title="Download · Log"
1170+
icon={CloudArrowDownIcon}
1171+
leadingIconClassName="text-indigo-500"
1172+
openInNewTab
1173+
/>
1174+
<PopoverMenuItem
1175+
to={`${downloadPath}?format=jsonl`}
1176+
title="Download · JSON Lines"
1177+
icon={CloudArrowDownIcon}
1178+
leadingIconClassName="text-indigo-500"
1179+
openInNewTab
1180+
/>
1181+
</>
1182+
);
1183+
}
1184+
11661185
function RunError({ error }: { error: TaskRunError }) {
11671186
const enhancedError = taskRunErrorEnhancer(error);
11681187

0 commit comments

Comments
 (0)