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(); 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 (
); } const { type } = fetcher.data; switch (type) { case "run": { return ( ); } case "span": { return ( ); } } } 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 (
{runParam && closePanel && (
{isAiInspector ? ( ) : (
)}
); } 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(); return (
{run.taskIdentifier} {run.isCached ? " (cached)" : null}
{runParam && closePanel && (
{ replace({ tab: "overview" }); }} shortcut={{ key: "o" }} > Overview { replace({ tab: "detail" }); }} shortcut={{ key: "d" }} > Detail { replace({ tab: "context" }); }} shortcut={{ key: "x" }} > Context { replace({ tab: "metadata" }); }} shortcut={{ key: "m" }} > Metadata
{tab === "detail" ? (
Status } content={descriptionForTaskRunStatus(run.status)} disableHoverableContent /> Task } content={`View runs filtered by ${run.taskIdentifier}`} disableHoverableContent /> Run ID {run.relationships.root ? ( run.relationships.root.isParent ? ( Root & Parent run } content={`Jump to root/parent run`} disableHoverableContent /> ) : ( <> Root run } content={`Jump to root run`} disableHoverableContent /> {run.relationships.parent ? ( Parent run } content={`Jump to parent run`} disableHoverableContent /> ) : null} ) ) : null} {run.batch && ( Batch } content={`View batches filtered by ${run.batch.friendlyId}`} disableHoverableContent /> )} {run.session && ( Session } content={`Jump to session (${run.session.reason})`} disableHoverableContent /> )}
Idempotency
Idempotency keys
Prevent duplicate task runs. If you trigger a task with the same key twice, the second request returns the original run.
Scope
Global: applies across all runs
Run: unique to a parent run
Attempt: unique to a specific attempt
Status
Active: duplicates are blocked
Expired: the TTL has passed
Inactive: the key was reset or cleared
Read docs
} /> {run.idempotencyKeyStatus === "active" ? ( ) : run.idempotencyKeyStatus === "expired" ? ( Expired ) : run.idempotencyKeyStatus === "inactive" ? ( Inactive ) : null}
{run.idempotencyKeyStatus ? (
Key: {run.idempotencyKey ? ( ) : ( "–" )}
Scope: {run.idempotencyKeyScope ?? "–"}
{run.idempotencyKeyStatus === "expired" ? "Expired: " : "Expires: "} {run.idempotencyKeyExpiresAt ? ( ) : ( "–" )}
) : ( "–" )}
Debounce {run.debounce ? (
Key: {run.debounce.key}
Delay: {run.debounce.delay}
) : ( "–" )}
Version {run.version ? ( environment.type === "DEVELOPMENT" ? ( ) : ( } content={"Jump to deployment"} /> ) ) : ( Never started )} SDK version {run.sdkVersion ? ( run.sdkVersion ) : ( Never started )} Runtime Test run {run.isTest ? : "–"} {run.replayedFromTaskRunFriendlyId && ( Replayed from } content={`Jump to replayed run`} disableHoverableContent /> )} {environment && ( Environment )} {run.schedule && ( Schedule
{run.schedule.generatorExpression} ({run.schedule.timezone})
{run.schedule.description} } content={`Go to schedule ${run.schedule.friendlyId}`} />
)} Queue
Name: {run.queue.name}
Concurrency key: {run.queue.concurrencyKey ? run.queue.concurrencyKey : "–"}
Time to live (TTL) {run.ttl ?? "–"} Tags {run.tags.length === 0 ? ( "–" ) : (
{run.tags.map((tag: string) => ( ))}
)}
Max duration {run.maxDurationInSeconds ? `${run.maxDurationInSeconds}s (${formatDurationMilliseconds( run.maxDurationInSeconds * 1000, { style: "short" } )})` : "–"} Machine } /> {run.region && ( Region )} Run invocation cost {run.baseCostInCents > 0 ? formatCurrencyAccurate(run.baseCostInCents / 100) : "–"} Compute cost {run.costInCents > 0 ? formatCurrencyAccurate(run.costInCents / 100) : "–"} Total cost {run.costInCents > 0 ? formatCurrencyAccurate((run.baseCostInCents + run.costInCents) / 100) : "–"} Usage duration {run.usageDurationMs > 0 ? formatDurationMilliseconds(run.usageDurationMs, { style: "short" }) : "–"} Run Engine {run.engine} {run.externalTraceId && ( External Trace ID {run.externalTraceId} )} {isAdmin && (
Admin only Buffered {run.isBuffered ? "Yes" : "No"} Worker queue {run.workerQueue} Trace ID {run.traceId} Span ID {run.spanId} Task event store {run.taskEventStore}
)}
) : tab === "context" ? (
) : tab === "metadata" ? (
{run.metadata ? ( ) : ( No metadata set for this run. View our metadata documentation to learn more. )}
) : (
} content={descriptionForTaskRunStatus(run.status)} />
{run.error && } {run.payload !== undefined && ( )} {run.error === undefined && run.output !== undefined ? ( ) : null}
)}
{run.friendlyId !== runParam && ( {run.isCached ? "Jump to original run" : "Focus on run"} )} {!run.isBuffered && }
{run.logsDeletedAt === null ? ( ) : null}
); } // 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) => ( )); } catch { toast.custom((t) => ( )); } }; return ( <> ); } function RunError({ error }: { error: TaskRunError }) { const enhancedError = taskRunErrorEnhancer(error); switch (enhancedError.type) { case "STRING_ERROR": return (
Error {enhancedError.raw}
); case "CUSTOM_ERROR": { return (
); } case "BUILT_IN_ERROR": case "INTERNAL_ERROR": { const name = "name" in enhancedError ? enhancedError.name : enhancedError.code; return (
{name} {enhancedError.message && (
                {enhancedError.message}
              
)} {enhancedError.link && (enhancedError.link.magic === "CONTACT_FORM" ? ( {enhancedError.link.name} } /> ) : ( {enhancedError.link.name} ))} {enhancedError.stackTrace && ( )}
); } } } function SpanEntity({ span }: { span: Span }) { const isAdmin = useHasAdminAccess(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); if (!span.entity) { //normal span return (
{span.level === "TRACE" ? ( <>
) : (
} variant="dot-solid" />
)} Message {span.message} {span.events.length > 0 && } {span.properties !== undefined ? ( ) : null} {span.resourceProperties !== undefined ? ( ) : null} {span.triggeredRuns.length > 0 && (
Runs ID Task Status Created {span.triggeredRuns.map((run) => { const path = v3RunSpanPath( organization, project, environment, { friendlyId: run.friendlyId }, { spanId: run.spanId } ); return ( {run.taskIdentifier} ); })}
)}
); } switch (span.entity.type) { case "attempt": { return (
{span.entity.object.isWarmStart !== undefined ? ( ) : null} {span.events.length > 0 && } {span.properties !== undefined ? ( ) : null}
); } case "waitpoint": { return (
Waitpoint A waitpoint pauses your code from continuing until the conditions are met.{" "} View docs.
{span.entity.object.status === "WAITING" && (
)}
); } case "realtime-stream": { return ( ); } case "ai-generation": case "ai-summary": { return ( ); } case "ai-tool-call": { return (
); } case "ai-embed": { return (
); } case "prompt": { return ( ); } default: { assertNever(span.entity); } } }