Load the details run inspector data on demand
This commit is contained in:
@@ -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<typeof loader>();
|
||||
|
||||
useEffect(() => {
|
||||
if (run?.friendlyId === undefined) return;
|
||||
fetcher.load(`/resources/runs/${run.friendlyId}`);
|
||||
}, [run?.friendlyId, run?.updatedAt]);
|
||||
|
||||
if (!run) {
|
||||
return (
|
||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright">
|
||||
@@ -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 (
|
||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_2rem_1fr_3.25rem] overflow-hidden bg-background-bright">
|
||||
@@ -169,35 +185,43 @@ export function RunInspector({
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Version</Property.Label>
|
||||
{/* <Property.Value>
|
||||
{run.version ? (
|
||||
run.version
|
||||
<Property.Value>
|
||||
{clientRunData ? (
|
||||
clientRunData?.version ? (
|
||||
clientRunData.version
|
||||
) : (
|
||||
<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>
|
||||
)
|
||||
) : (
|
||||
<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>
|
||||
<PropertyLoading />
|
||||
)}
|
||||
</Property.Value> */}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>SDK version</Property.Label>
|
||||
{/* <Property.Value>
|
||||
{run.sdkVersion ? (
|
||||
run.sdkVersion
|
||||
<Property.Value>
|
||||
{clientRunData ? (
|
||||
clientRunData?.sdkVersion ? (
|
||||
clientRunData.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>
|
||||
)
|
||||
) : (
|
||||
<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>
|
||||
<PropertyLoading />
|
||||
)}
|
||||
</Property.Value> */}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Test run</Property.Label>
|
||||
@@ -213,35 +237,55 @@ export function RunInspector({
|
||||
</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>
|
||||
|
||||
<Property.Item>
|
||||
<Property.Label>Schedule</Property.Label>
|
||||
<Property.Value>
|
||||
{clientRunData ? (
|
||||
clientRunData.schedule ? (
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-mono">
|
||||
{clientRunData.schedule.generatorExpression}
|
||||
</span>
|
||||
<span>({clientRunData.schedule.timezone})</span>
|
||||
</div>
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<TextLink
|
||||
to={v3SchedulePath(organization, project, clientRunData.schedule)}
|
||||
>
|
||||
{clientRunData.schedule.description}
|
||||
</TextLink>
|
||||
}
|
||||
content={`Go to schedule ${clientRunData.schedule.friendlyId}`}
|
||||
/>
|
||||
</div>
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<TextLink to={v3SchedulePath(organization, project, run.schedule)}>
|
||||
{run.schedule.description}
|
||||
</TextLink>
|
||||
}
|
||||
content={`Go to schedule ${run.schedule.friendlyId}`}
|
||||
/>
|
||||
</div>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
)} */}
|
||||
) : (
|
||||
"No schedule"
|
||||
)
|
||||
) : (
|
||||
<PropertyLoading />
|
||||
)}
|
||||
</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.Value>
|
||||
{clientRunData ? (
|
||||
<>
|
||||
<div>Name: {clientRunData.queue.name}</div>
|
||||
<div>
|
||||
Concurrency key:{" "}
|
||||
{clientRunData.queue.concurrencyKey
|
||||
? clientRunData.queue.concurrencyKey
|
||||
: "–"}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<PropertyLoading />
|
||||
)}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Time to live (TTL)</Property.Label>
|
||||
@@ -249,38 +293,42 @@ export function RunInspector({
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Tags</Property.Label>
|
||||
{/* <Property.Value>
|
||||
{run.tags.length === 0 ? (
|
||||
"–"
|
||||
<Property.Value>
|
||||
{clientRunData ? (
|
||||
clientRunData.tags.length === 0 ? (
|
||||
"–"
|
||||
) : (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs">
|
||||
{clientRunData.tags.map((tag) => (
|
||||
<SimpleTooltip
|
||||
key={tag}
|
||||
button={
|
||||
<Link to={v3RunsPath(organization, project, { tags: [tag] })}>
|
||||
<RunTag tag={tag} />
|
||||
</Link>
|
||||
}
|
||||
content={`Filter runs by ${tag}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs">
|
||||
{run.tags.map((tag) => (
|
||||
<SimpleTooltip
|
||||
key={tag}
|
||||
button={
|
||||
<Link to={v3RunsPath(organization, project, { tags: [tag] })}>
|
||||
<RunTag tag={tag} />
|
||||
</Link>
|
||||
}
|
||||
content={`Filter runs by ${tag}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<PropertyLoading />
|
||||
)}
|
||||
</Property.Value> */}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
{/* {run.links && run.links.length > 0 && (
|
||||
{span?.links && span.links.length > 0 && (
|
||||
<Property.Item>
|
||||
<Property.Label>Links</Property.Label>
|
||||
<Property.Value>
|
||||
<div className="space-y-1">
|
||||
{run.links.map((link, index) => (
|
||||
{span.links.map((link, index) => (
|
||||
<SpanLinkElement key={index} link={link} />
|
||||
))}
|
||||
</div>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
)} */}
|
||||
)}
|
||||
<Property.Item>
|
||||
<Property.Label>Run invocation cost</Property.Label>
|
||||
<Property.Value>
|
||||
@@ -315,7 +363,11 @@ export function RunInspector({
|
||||
</div>
|
||||
) : tab === "context" ? (
|
||||
<div className="flex flex-col gap-4 py-3">
|
||||
{/* <CodeBlock code={run.context} showLineNumbers={false} /> */}
|
||||
{clientRunData ? (
|
||||
<CodeBlock code={clientRunData.context} showLineNumbers={false} />
|
||||
) : (
|
||||
<PropertyLoading />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 pt-3">
|
||||
@@ -326,11 +378,19 @@ export function RunInspector({
|
||||
{run.payload !== undefined && (
|
||||
<PacketDisplay data={run.payload} dataType={run.payloadType} title="Payload" />
|
||||
)}
|
||||
{/* {run.error !== undefined ? (
|
||||
<RunError error={run.error} />
|
||||
) : run.output !== undefined ? (
|
||||
<PacketDisplay data={run.output} dataType={run.outputType} title="Output" />
|
||||
) : null} */}
|
||||
{clientRunData ? (
|
||||
clientRunData.error !== undefined ? (
|
||||
<RunError error={clientRunData.error} />
|
||||
) : clientRunData.output !== undefined ? (
|
||||
<PacketDisplay
|
||||
data={clientRunData.output}
|
||||
dataType={clientRunData.outputType}
|
||||
title="Output"
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<PropertyLoading />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+4
-1
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof loader>;
|
||||
|
||||
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),
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user