diff --git a/apps/webapp/app/components/runs/v3/RunInspector.tsx b/apps/webapp/app/components/runs/v3/RunInspector.tsx index 0f563f638..931a67bea 100644 --- a/apps/webapp/app/components/runs/v3/RunInspector.tsx +++ b/apps/webapp/app/components/runs/v3/RunInspector.tsx @@ -5,8 +5,7 @@ import { nanosecondsToMilliseconds, TaskRunError, } from "@trigger.dev/core/v3"; -import { TaskRun } from "@trigger.dev/database"; -import { ReactNode } from "react"; +import { ReactNode, useEffect } from "react"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { CodeBlock } from "~/components/code/CodeBlock"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; @@ -19,7 +18,7 @@ import * as Property from "~/components/primitives/PropertyTable"; import { Spinner } from "~/components/primitives/Spinner"; import { TabButton, TabContainer } from "~/components/primitives/Tabs"; import { TextLink } from "~/components/primitives/TextLink"; -import { SimpleTooltip } from "~/components/primitives/Tooltip"; +import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip"; import { LiveTimer } from "~/components/runs/v3/LiveTimer"; import { RunIcon } from "~/components/runs/v3/RunIcon"; import { useOrganization } from "~/hooks/useOrganizations"; @@ -33,11 +32,18 @@ import { v3RunPath, v3RunSpanPath, v3RunsPath, + v3SchedulePath, v3TraceSpanPath, } from "~/utils/pathBuilder"; import { SpanLink } from "~/v3/eventRepository.server"; import { isFinalRunStatus } from "~/v3/taskStatus"; import { TaskRunStatusCombo } from "./TaskRunStatus"; +import { useTypedFetcher } from "remix-typedjson"; +import { RunInspectorData } from "~/routes/resources.runs.$runParam"; +import { loader } from "~/routes/resources.runs.$runParam"; +import { Link } from "@remix-run/react"; +import { RunTag } from "./RunTag"; +import { TraceSpan } from "~/utils/taskEvent"; /** * The RunInspector displays live information about a run. @@ -45,10 +51,12 @@ import { TaskRunStatusCombo } from "./TaskRunStatus"; */ export function RunInspector({ run, + span, runParam, closePanel, }: { run?: RawRun; + span?: TraceSpan; runParam: string; closePanel?: () => void; }) { @@ -57,6 +65,13 @@ export function RunInspector({ const { value, replace } = useSearchParams(); const tab = value("tab"); + const fetcher = useTypedFetcher(); + + useEffect(() => { + if (run?.friendlyId === undefined) return; + fetcher.load(`/resources/runs/${run.friendlyId}`); + }, [run?.friendlyId, run?.updatedAt]); + if (!run) { return (
@@ -82,6 +97,7 @@ export function RunInspector({ } const environment = project.environments.find((e) => e.id === run.runtimeEnvironmentId); + const clientRunData = fetcher.state === "idle" ? fetcher.data : undefined; return (
@@ -169,35 +185,43 @@ export function RunInspector({ Version - {/* - {run.version ? ( - run.version + + {clientRunData ? ( + clientRunData?.version ? ( + clientRunData.version + ) : ( + + Never started + + + ) ) : ( - - Never started - - + )} - */} + SDK version - {/* - {run.sdkVersion ? ( - run.sdkVersion + + {clientRunData ? ( + clientRunData?.sdkVersion ? ( + clientRunData.sdkVersion + ) : ( + + Never started + + + ) ) : ( - - Never started - - + )} - */} + Test run @@ -213,35 +237,55 @@ export function RunInspector({ )} - {/* {run.schedule && ( - - Schedule - -
-
- {run.schedule.generatorExpression} - ({run.schedule.timezone}) + + + Schedule + + {clientRunData ? ( + clientRunData.schedule ? ( +
+
+ + {clientRunData.schedule.generatorExpression} + + ({clientRunData.schedule.timezone}) +
+ + {clientRunData.schedule.description} + + } + content={`Go to schedule ${clientRunData.schedule.friendlyId}`} + />
- - {run.schedule.description} - - } - content={`Go to schedule ${run.schedule.friendlyId}`} - /> -
- - - )} */} + ) : ( + "No schedule" + ) + ) : ( + + )} + + Queue - {/* -
Name: {run.queue.name}
-
- Concurrency key: {run.queue.concurrencyKey ? run.queue.concurrencyKey : "–"} -
-
*/} + + {clientRunData ? ( + <> +
Name: {clientRunData.queue.name}
+
+ Concurrency key:{" "} + {clientRunData.queue.concurrencyKey + ? clientRunData.queue.concurrencyKey + : "–"} +
+ + ) : ( + + )} +
Time to live (TTL) @@ -249,38 +293,42 @@ export function RunInspector({ Tags - {/* - {run.tags.length === 0 ? ( - "–" + + {clientRunData ? ( + clientRunData.tags.length === 0 ? ( + "–" + ) : ( +
+ {clientRunData.tags.map((tag) => ( + + + + } + content={`Filter runs by ${tag}`} + /> + ))} +
+ ) ) : ( -
- {run.tags.map((tag) => ( - - - - } - content={`Filter runs by ${tag}`} - /> - ))} -
+ )} -
*/} +
- {/* {run.links && run.links.length > 0 && ( + {span?.links && span.links.length > 0 && ( Links
- {run.links.map((link, index) => ( + {span.links.map((link, index) => ( ))}
- )} */} + )} Run invocation cost @@ -315,7 +363,11 @@ export function RunInspector({
) : tab === "context" ? (
- {/* */} + {clientRunData ? ( + + ) : ( + + )}
) : (
@@ -326,11 +378,19 @@ export function RunInspector({ {run.payload !== undefined && ( )} - {/* {run.error !== undefined ? ( - - ) : run.output !== undefined ? ( - - ) : null} */} + {clientRunData ? ( + clientRunData.error !== undefined ? ( + + ) : clientRunData.output !== undefined ? ( + + ) : null + ) : ( + + )}
)}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx index 9ef54490b..f9f975e04 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx @@ -260,6 +260,7 @@ type InspectorState = | { type: "run"; run?: RawRun; + span?: TraceSpan; } | undefined; @@ -293,10 +294,12 @@ function Panels({ resizable, run: originalRun }: LoaderData) { if (selectedSpanId) { if (runs && runs.length > 0) { const spanRun = runs.find((r) => r.spanId === selectedSpanId); - if (spanRun) { + if (spanRun && events) { + const span = createSpanFromEvents(events, selectedSpanId); return { type: "run", run: spanRun, + span, }; } } diff --git a/apps/webapp/app/routes/resources.runs.$runParam.ts b/apps/webapp/app/routes/resources.runs.$runParam.ts new file mode 100644 index 000000000..744dff796 --- /dev/null +++ b/apps/webapp/app/routes/resources.runs.$runParam.ts @@ -0,0 +1,240 @@ +import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { MachinePresetName, prettyPrintPacket, TaskRunError } from "@trigger.dev/core/v3"; +import { typedjson, UseDataFunctionReturn } from "remix-typedjson"; +import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus"; +import { $replica } from "~/db.server"; +import { requireUserId } from "~/services/session.server"; +import { v3RunParamsSchema } from "~/utils/pathBuilder"; +import { machinePresetFromName } from "~/v3/machinePresets.server"; +import { FINAL_ATTEMPT_STATUSES, isFinalRunStatus } from "~/v3/taskStatus"; + +export type RunInspectorData = UseDataFunctionReturn; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const userId = await requireUserId(request); + const parsedParams = v3RunParamsSchema.pick({ runParam: true }).parse(params); + + const run = await $replica.taskRun.findFirst({ + select: { + id: true, + traceId: true, + //metadata + number: true, + taskIdentifier: true, + friendlyId: true, + isTest: true, + tags: { + select: { + name: true, + }, + }, + machinePreset: true, + lockedToVersion: { + select: { + version: true, + sdkVersion: true, + }, + }, + //status + duration + status: true, + startedAt: true, + createdAt: true, + updatedAt: true, + queuedAt: true, + completedAt: true, + logsDeletedAt: true, + //idempotency + idempotencyKey: true, + //delayed + delayUntil: true, + //ttl + ttl: true, + expiredAt: true, + //queue + queue: true, + concurrencyKey: true, + //schedule + schedule: { + select: { + friendlyId: true, + generatorExpression: true, + timezone: true, + generatorDescription: true, + }, + }, + //usage + baseCostInCents: true, + costInCents: true, + usageDurationMs: true, + //env + runtimeEnvironment: { + select: { id: true, slug: true, type: true }, + }, + payload: true, + payloadType: true, + maxAttempts: true, + project: { + include: { + organization: true, + }, + }, + lockedBy: { + select: { + filePath: true, + exportName: true, + }, + }, + }, + where: { + friendlyId: parsedParams.runParam, + project: { + organization: { + members: { + some: { + userId, + }, + }, + }, + }, + }, + }); + + if (!run) { + throw new Response("Not found", { status: 404 }); + } + + const isFinished = isFinalRunStatus(run.status); + + const finishedAttempt = isFinished + ? await $replica.taskRunAttempt.findFirst({ + select: { + output: true, + outputType: true, + error: true, + }, + where: { + status: { in: FINAL_ATTEMPT_STATUSES }, + taskRunId: run.id, + }, + orderBy: { + createdAt: "desc", + }, + }) + : null; + + const output = + finishedAttempt === null + ? undefined + : finishedAttempt.outputType === "application/store" + ? `/resources/packets/${run.runtimeEnvironment.id}/${finishedAttempt.output}` + : typeof finishedAttempt.output !== "undefined" && finishedAttempt.output !== null + ? await prettyPrintPacket(finishedAttempt.output, finishedAttempt.outputType ?? undefined) + : undefined; + + const payload = + run.payloadType === "application/store" + ? `/resources/packets/${run.runtimeEnvironment.id}/${run.payload}` + : typeof run.payload !== "undefined" && run.payload !== null + ? await prettyPrintPacket(run.payload, run.payloadType ?? undefined) + : undefined; + + let error: TaskRunError | undefined = undefined; + if (finishedAttempt?.error) { + const result = TaskRunError.safeParse(finishedAttempt.error); + if (result.success) { + error = result.data; + } else { + error = { + type: "CUSTOM_ERROR", + raw: JSON.stringify(finishedAttempt.error), + }; + } + } + + const context = { + task: { + id: run.taskIdentifier, + filePath: run.lockedBy?.filePath, + exportName: run.lockedBy?.exportName, + }, + run: { + id: run.friendlyId, + createdAt: run.createdAt, + tags: run.tags.map((tag) => tag.name), + isTest: run.isTest, + idempotencyKey: run.idempotencyKey ?? undefined, + startedAt: run.startedAt ?? run.createdAt, + durationMs: run.usageDurationMs, + costInCents: run.costInCents, + baseCostInCents: run.baseCostInCents, + maxAttempts: run.maxAttempts ?? undefined, + version: run.lockedToVersion?.version, + }, + queue: { + name: run.queue, + }, + environment: { + id: run.runtimeEnvironment.id, + slug: run.runtimeEnvironment.slug, + type: run.runtimeEnvironment.type, + }, + organization: { + id: run.project.organization.id, + slug: run.project.organization.slug, + name: run.project.organization.title, + }, + project: { + id: run.project.id, + ref: run.project.externalRef, + slug: run.project.slug, + name: run.project.name, + }, + machine: run.machinePreset + ? machinePresetFromName(run.machinePreset as MachinePresetName) + : undefined, + }; + + return typedjson({ + friendlyId: run.friendlyId, + status: run.status, + createdAt: run.createdAt, + startedAt: run.startedAt, + updatedAt: run.updatedAt, + delayUntil: run.delayUntil, + expiredAt: run.expiredAt, + completedAt: run.completedAt, + logsDeletedAt: run.logsDeletedAt, + ttl: run.ttl, + taskIdentifier: run.taskIdentifier, + version: run.lockedToVersion?.version, + sdkVersion: run.lockedToVersion?.sdkVersion, + isTest: run.isTest, + environmentId: run.runtimeEnvironment.id, + schedule: run.schedule + ? { + friendlyId: run.schedule.friendlyId, + generatorExpression: run.schedule.generatorExpression, + description: run.schedule.generatorDescription, + timezone: run.schedule.timezone, + } + : undefined, + queue: { + name: run.queue, + isCustomQueue: !run.queue.startsWith("task/"), + concurrencyKey: run.concurrencyKey, + }, + tags: run.tags.map((tag) => tag.name), + baseCostInCents: run.baseCostInCents, + costInCents: run.costInCents, + totalCostInCents: run.costInCents + run.baseCostInCents, + usageDurationMs: run.usageDurationMs, + isFinished, + isRunning: RUNNING_STATUSES.includes(run.status), + payload, + payloadType: run.payloadType, + output, + outputType: finishedAttempt?.outputType ?? "application/json", + error, + context: JSON.stringify(context, null, 2), + }); +};