diff --git a/apps/webapp/app/components/runs/v3/RunInspector.tsx b/apps/webapp/app/components/runs/v3/RunInspector.tsx
index e69de29bb..0f563f638 100644
--- a/apps/webapp/app/components/runs/v3/RunInspector.tsx
+++ b/apps/webapp/app/components/runs/v3/RunInspector.tsx
@@ -0,0 +1,715 @@
+import { CheckIcon, ClockIcon, CloudArrowDownIcon, QueueListIcon } from "@heroicons/react/20/solid";
+import {
+ formatDuration,
+ formatDurationMilliseconds,
+ nanosecondsToMilliseconds,
+ TaskRunError,
+} from "@trigger.dev/core/v3";
+import { TaskRun } from "@trigger.dev/database";
+import { ReactNode } from "react";
+import { ExitIcon } from "~/assets/icons/ExitIcon";
+import { CodeBlock } from "~/components/code/CodeBlock";
+import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
+import { Button, LinkButton } from "~/components/primitives/Buttons";
+import { Callout } from "~/components/primitives/Callout";
+import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime";
+import { Header2, Header3 } from "~/components/primitives/Headers";
+import { Paragraph } from "~/components/primitives/Paragraph";
+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 { LiveTimer } from "~/components/runs/v3/LiveTimer";
+import { RunIcon } from "~/components/runs/v3/RunIcon";
+import { useOrganization } from "~/hooks/useOrganizations";
+import { useProject } from "~/hooks/useProject";
+import { useSearchParams } from "~/hooks/useSearchParam";
+import { RawRun } from "~/hooks/useSyncTraceRuns";
+import { cn } from "~/utils/cn";
+import { formatCurrencyAccurate } from "~/utils/numberFormatter";
+import {
+ v3RunDownloadLogsPath,
+ v3RunPath,
+ v3RunSpanPath,
+ v3RunsPath,
+ v3TraceSpanPath,
+} from "~/utils/pathBuilder";
+import { SpanLink } from "~/v3/eventRepository.server";
+import { isFinalRunStatus } from "~/v3/taskStatus";
+import { TaskRunStatusCombo } from "./TaskRunStatus";
+
+/**
+ * The RunInspector displays live information about a run.
+ * Most of that data comes in as params but for some we need to fetch it.
+ */
+export function RunInspector({
+ run,
+ runParam,
+ closePanel,
+}: {
+ run?: RawRun;
+ runParam: string;
+ closePanel?: () => void;
+}) {
+ const organization = useOrganization();
+ const project = useProject();
+ const { value, replace } = useSearchParams();
+ const tab = value("tab");
+
+ if (!run) {
+ return (
+
+
+
+
+
+
+
+
+ {closePanel && (
+
+ )}
+
+
+
+ );
+ }
+
+ const environment = project.environments.find((e) => e.id === run.runtimeEnvironmentId);
+
+ return (
+
+
+
+
+
+ {run.taskIdentifier}
+
+
+ {closePanel && (
+
+ )}
+
+
+
+ {
+ replace({ tab: "overview" });
+ }}
+ shortcut={{ key: "o" }}
+ >
+ Overview
+
+ {
+ replace({ tab: "detail" });
+ }}
+ shortcut={{ key: "d" }}
+ >
+ Detail
+
+ {
+ replace({ tab: "context" });
+ }}
+ shortcut={{ key: "c" }}
+ >
+ Context
+
+
+
+
+
+ {tab === "detail" ? (
+
+
+
+ Status
+
+ {run ? : }
+
+
+
+ Task
+
+
+ {run.taskIdentifier}
+
+ }
+ content={`Filter runs by ${run.taskIdentifier}`}
+ />
+
+
+
+ Version
+ {/*
+ {run.version ? (
+ run.version
+ ) : (
+
+ Never started
+
+
+ )}
+ */}
+
+
+ SDK version
+ {/*
+ {run.sdkVersion ? (
+ run.sdkVersion
+ ) : (
+
+ Never started
+
+
+ )}
+ */}
+
+
+ Test run
+
+ {run.isTest ? : "–"}
+
+
+ {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) => (
+
+
+
+ }
+ content={`Filter runs by ${tag}`}
+ />
+ ))}
+
+ )}
+ */}
+
+ {/* {run.links && run.links.length > 0 && (
+
+ Links
+
+
+ {run.links.map((link, index) => (
+
+ ))}
+
+
+
+ )} */}
+
+ 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" })
+ : "–"}
+
+
+
+
+ ) : tab === "context" ? (
+
+ {/* */}
+
+ ) : (
+
+
+
+
+
+ {run.payload !== undefined && (
+
+ )}
+ {/* {run.error !== undefined ? (
+
+ ) : run.output !== undefined ? (
+
+ ) : null} */}
+
+ )}
+
+
+
+
+ {run.friendlyId !== runParam && (
+
+ Focus on run
+
+ )}
+
+
+ {run.logsDeletedAt === null ? (
+
+ Download logs
+
+ ) : null}
+
+
+
+ );
+}
+
+function PropertyLoading() {
+ return ;
+}
+
+function RunTimeline({ run }: { run: RawRun }) {
+ const createdAt = new Date(run.createdAt);
+ const startedAt = run.startedAt ? new Date(run.startedAt) : null;
+ const delayUntil = run.delayUntil ? new Date(run.delayUntil) : null;
+ const expiredAt = run.expiredAt ? new Date(run.expiredAt) : null;
+ const updatedAt = new Date(run.updatedAt);
+
+ const isFinished = isFinalRunStatus(run.status);
+
+ return (
+
+ }
+ state="complete"
+ />
+ {delayUntil && !expiredAt ? (
+ {formatDuration(createdAt, delayUntil)} delay>
+ ) : (
+
+
+
+ Delayed until {run.ttl && <>(TTL {run.ttl})>}
+
+
+ )
+ }
+ state={run.startedAt ? "complete" : "delayed"}
+ />
+ ) : startedAt ? (
+
+ ) : (
+
+ {" "}
+ {run.ttl && <>(TTL {run.ttl})>}
+ >
+ }
+ state={run.startedAt || run.expiredAt ? "complete" : "inprogress"}
+ />
+ )}
+ {expiredAt ? (
+ }
+ state="error"
+ />
+ ) : startedAt ? (
+ <>
+ }
+ state="complete"
+ />
+ {isFinished ? (
+ <>
+
+ }
+ state="complete"
+ />
+ >
+ ) : (
+
+
+
+
+
+
+ }
+ state={"inprogress"}
+ />
+ )}
+ >
+ ) : null}
+
+ );
+}
+
+type RunTimelineItemProps = {
+ title: ReactNode;
+ subtitle?: ReactNode;
+ state: "complete" | "error";
+};
+
+function RunTimelineEvent({ title, subtitle, state }: RunTimelineItemProps) {
+ return (
+
+
+
+ {title}
+ {subtitle ? {subtitle} : null}
+
+
+ );
+}
+
+type RunTimelineLineProps = {
+ title: ReactNode;
+ state: "complete" | "delayed" | "inprogress";
+};
+
+function RunTimelineLine({ title, state }: RunTimelineLineProps) {
+ return (
+
+ );
+}
+
+function RunError({ error }: { error: TaskRunError }) {
+ switch (error.type) {
+ case "STRING_ERROR":
+ case "CUSTOM_ERROR": {
+ return (
+
+
+
+ );
+ }
+ case "BUILT_IN_ERROR":
+ case "INTERNAL_ERROR": {
+ const name = "name" in error ? error.name : error.code;
+ return (
+
+ {name}
+ {error.message && {error.message}}
+ {error.stackTrace && (
+
+ )}
+
+ );
+ }
+ }
+}
+
+function PacketDisplay({
+ data,
+ dataType,
+ title,
+}: {
+ data: string;
+ dataType: string;
+ title: string;
+}) {
+ switch (dataType) {
+ case "application/store": {
+ return (
+
+
+ {title}
+
+
+ Download
+
+
+ );
+ }
+ case "text/plain": {
+ return (
+
+ );
+ }
+ default: {
+ return (
+
+ );
+ }
+ }
+}
+
+type TimelineProps = {
+ startTime: Date;
+ duration: number;
+ inProgress: boolean;
+ isError: boolean;
+};
+
+type TimelineState = "error" | "pending" | "complete";
+
+function SpanTimeline({ startTime, duration, inProgress, isError }: TimelineProps) {
+ const state = isError ? "error" : inProgress ? "pending" : "complete";
+ return (
+ <>
+
+ }
+ state="complete"
+ />
+ {state === "pending" ? (
+
+
+
+
+
+
+ }
+ state={"inprogress"}
+ />
+ ) : (
+ <>
+
+
+ }
+ state={isError ? "error" : "complete"}
+ />
+ >
+ )}
+
+ >
+ );
+}
+
+function VerticalBar({ state }: { state: TimelineState }) {
+ return ;
+}
+
+function DottedLine() {
+ return (
+
+ );
+}
+
+function classNameForState(state: TimelineState) {
+ switch (state) {
+ case "pending": {
+ return "bg-pending";
+ }
+ case "complete": {
+ return "bg-success";
+ }
+ case "error": {
+ return "bg-error";
+ }
+ }
+}
+
+function SpanLinkElement({ link }: { link: SpanLink }) {
+ const organization = useOrganization();
+ const project = useProject();
+
+ switch (link.type) {
+ case "run": {
+ return (
+
+ {link.title}
+
+ );
+ }
+ case "span": {
+ return (
+
+ {link.title}
+
+ );
+ }
+ }
+
+ return null;
+}
diff --git a/apps/webapp/app/components/runs/v3/SpanInspector.tsx b/apps/webapp/app/components/runs/v3/SpanInspector.tsx
index 32b8e62e8..eb0cd79ab 100644
--- a/apps/webapp/app/components/runs/v3/SpanInspector.tsx
+++ b/apps/webapp/app/components/runs/v3/SpanInspector.tsx
@@ -28,7 +28,7 @@ export function SpanInspector({
runParam,
closePanel,
}: {
- span: TraceSpan;
+ span?: TraceSpan;
runParam?: string;
closePanel?: () => void;
}) {
@@ -41,6 +41,10 @@ export function SpanInspector({
tab = "overview";
}
+ if (span === undefined) {
+ return null;
+ }
+
return (
diff --git a/apps/webapp/app/hooks/useSyncRun.ts b/apps/webapp/app/hooks/useSyncRun.ts
deleted file mode 100644
index 6ec5795a4..000000000
--- a/apps/webapp/app/hooks/useSyncRun.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useShape } from "@electric-sql/react";
-import { TaskRun } from "@trigger.dev/database";
-
-type Params = {
- origin: string;
- runId: string;
-};
-
-export function useSyncRun({ origin, runId }: Params) {
- const { isUpToDate, data } = useShape({
- url: `${origin}/sync/runs/${runId}`,
- });
-
- const run = (data as unknown as TaskRun[])?.at(0);
-
- return { isUpToDate, run };
-}
diff --git a/apps/webapp/app/hooks/useSyncRunPage.ts b/apps/webapp/app/hooks/useSyncRunPage.ts
index 33a6b5daf..a07c41bf5 100644
--- a/apps/webapp/app/hooks/useSyncRunPage.ts
+++ b/apps/webapp/app/hooks/useSyncRunPage.ts
@@ -1,18 +1,16 @@
-import { useSyncRun } from "./useSyncRun";
import { useSyncTrace } from "./useSyncTrace";
+import { useSyncTraceRuns } from "./useSyncTraceRuns";
type Params = {
origin: string;
- runId: string;
traceId: string;
- spanId: string;
};
-export function useSyncRunPage({ origin, runId, traceId, spanId }: Params) {
- const { isUpToDate: isRunUpToDate, run } = useSyncRun({ origin, runId });
- const { isUpToDate: isTraceUpToDate, events } = useSyncTrace({ origin, traceId, spanId });
+export function useSyncRunPage({ origin, traceId }: Params) {
+ const { isUpToDate: isRunUpToDate, runs } = useSyncTraceRuns({ origin, traceId });
+ const { isUpToDate: isTraceUpToDate, events } = useSyncTrace({ origin, traceId });
const isUpToDate = isRunUpToDate && isTraceUpToDate;
- return { isUpToDate, isRunUpToDate, isTraceUpToDate, run, events };
+ return { isUpToDate, isRunUpToDate, isTraceUpToDate, runs, events };
}
diff --git a/apps/webapp/app/hooks/useSyncTrace.ts b/apps/webapp/app/hooks/useSyncTrace.ts
index 5e4c29469..111d5eb33 100644
--- a/apps/webapp/app/hooks/useSyncTrace.ts
+++ b/apps/webapp/app/hooks/useSyncTrace.ts
@@ -5,16 +5,15 @@ import { createTraceTreeFromEvents } from "~/utils/taskEvent";
type Params = {
origin: string;
traceId: string;
- spanId: string;
};
export type Trace = ReturnType
;
-export type TraceEvent = NonNullable;
+export type TraceEvent = Trace["events"][number];
-export function useSyncTrace({ origin, traceId, spanId }: Params) {
+export function useSyncTrace({ origin, traceId }: Params) {
const { isUpToDate, data } = useShape({
url: `${origin}/sync/traces/${traceId}`,
});
- return { isUpToDate, events: data ? (data as unknown as TaskEvent[]) : undefined };
+ return { isUpToDate, events: data ? (data as any as TaskEvent[]) : undefined };
}
diff --git a/apps/webapp/app/hooks/useSyncTraceRuns.ts b/apps/webapp/app/hooks/useSyncTraceRuns.ts
new file mode 100644
index 000000000..6411b2c0d
--- /dev/null
+++ b/apps/webapp/app/hooks/useSyncTraceRuns.ts
@@ -0,0 +1,19 @@
+import { useShape } from "@electric-sql/react";
+import { TaskRun } from "@trigger.dev/database";
+import { SyncedShapeData, useSyncedShape } from "./useSyncedShape";
+import { Prettify } from "@trigger.dev/core";
+
+type Params = {
+ origin: string;
+ traceId: string;
+};
+
+export type RawRun = Prettify>;
+
+export function useSyncTraceRuns({ origin, traceId }: Params) {
+ const { isUpToDate, data } = useSyncedShape({
+ url: `${origin}/sync/traces/runs/${traceId}`,
+ });
+
+ return { isUpToDate, runs: data };
+}
diff --git a/apps/webapp/app/hooks/useSyncedShape.ts b/apps/webapp/app/hooks/useSyncedShape.ts
new file mode 100644
index 000000000..93e014133
--- /dev/null
+++ b/apps/webapp/app/hooks/useSyncedShape.ts
@@ -0,0 +1,69 @@
+import { useShape } from "@electric-sql/react";
+
+export type ShapeInput = Parameters[0];
+export type ShapeOutput = {
+ isError: boolean;
+ isUpToDate: boolean;
+ data: S[];
+};
+
+export type SyncedShapeData = {
+ [K in keyof T]: T[K] extends Date
+ ? string
+ : T[K] extends Date | null
+ ? string | null
+ : T[K] extends BigInt
+ ? number
+ : T[K] extends BigInt | null
+ ? number | null
+ : T[K] extends object
+ ? SyncedShapeData
+ : T[K];
+};
+
+export function useSyncedShape(props: ShapeInput): ShapeOutput> {
+ const output = useShape(props) as any;
+
+ return {
+ isError: output.isError,
+ isUpToDate: output.isUpToDate,
+ data: transformInput(output.data as InputObject) as SyncedShapeData[],
+ };
+}
+
+type InputObject = {
+ [key: string]: Value;
+}[];
+
+type Value =
+ | string
+ | number
+ | boolean
+ | bigint
+ | null
+ | Value[]
+ | {
+ [key: string]: Value;
+ };
+
+function transformInput(input: InputObject): SyncedShapeData {
+ return input.map((value) => transformValue(value));
+}
+
+function transformValue(value: Value): Value {
+ if (Array.isArray(value)) {
+ return value.map(transformValue);
+ } else if (typeof value === "object" && value !== null) {
+ const result: { [key: string]: Value } = {};
+
+ for (const key in value) {
+ result[key] = transformValue(value[key]);
+ }
+
+ return result;
+ } else if (typeof value === "bigint") {
+ return Number(value);
+ } else {
+ return value;
+ }
+}
diff --git a/apps/webapp/app/presenters/v3/RunPresenter.server.ts b/apps/webapp/app/presenters/v3/RunPresenter.server.ts
index 2527c455a..7e1e3ef1b 100644
--- a/apps/webapp/app/presenters/v3/RunPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/RunPresenter.server.ts
@@ -72,6 +72,7 @@ export class RunPresenter {
isFinished: isFinalRunStatus(run.status),
completedAt: run.completedAt,
logsDeletedAt: run.logsDeletedAt,
+
environment: {
id: run.runtimeEnvironment.id,
organizationId: run.runtimeEnvironment.organizationId,
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 ef5d715a6..622e3b1f6 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
@@ -17,7 +17,7 @@ import {
millisecondsToNanoseconds,
nanosecondsToMilliseconds,
} from "@trigger.dev/core/v3";
-import { RuntimeEnvironmentType, TaskRun } from "@trigger.dev/database";
+import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { motion } from "framer-motion";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
@@ -55,6 +55,8 @@ import { NodesState } from "~/components/primitives/TreeView/reducer";
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
import { ReplayRunDialog } from "~/components/runs/v3/ReplayRunDialog";
import { RunIcon } from "~/components/runs/v3/RunIcon";
+import { RunInspector } from "~/components/runs/v3/RunInspector";
+import { SpanInspector } from "~/components/runs/v3/SpanInspector";
import { SpanTitle, eventBackgroundClassName } from "~/components/runs/v3/SpanTitle";
import { TaskRunStatusIcon, runStatusClassNameColor } from "~/components/runs/v3/TaskRunStatus";
import { useAppOrigin } from "~/hooks/useAppOrigin";
@@ -64,9 +66,11 @@ import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useReplaceLocation } from "~/hooks/useReplaceLocation";
import { Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys";
+import { useSyncRunPage } from "~/hooks/useSyncRunPage";
import { Trace, TraceEvent } from "~/hooks/useSyncTrace";
+import { RawRun } from "~/hooks/useSyncTraceRuns";
import { useUser } from "~/hooks/useUser";
-import { RunPresenter } from "~/presenters/v3/RunPresenter.server";
+import { Run, RunPresenter } from "~/presenters/v3/RunPresenter.server";
import { getResizableSnapshot } from "~/services/resizablePanel.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
@@ -78,11 +82,13 @@ import {
v3RunSpanPath,
v3RunsPath,
} from "~/utils/pathBuilder";
+import {
+ TraceSpan,
+ createSpanFromEvents,
+ createTraceTreeFromEvents,
+ prepareTrace,
+} from "~/utils/taskEvent";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
-import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route";
-import { SpanInspector } from "~/components/runs/v3/SpanInspector";
-import { createSpanFromEvents, createTraceTreeFromEvents, prepareTrace } from "~/utils/taskEvent";
-import { useSyncRunPage } from "~/hooks/useSyncRunPage";
const resizableSettings = {
parent: {
@@ -246,21 +252,28 @@ export default function Page() {
);
}
+type InspectorState =
+ | {
+ type: "span";
+ span?: TraceSpan;
+ }
+ | {
+ type: "run";
+ run?: RawRun;
+ }
+ | undefined;
+
function Panels({ resizable, run: originalRun }: LoaderData) {
const { location, replaceSearchParam } = useReplaceLocation();
const selectedSpanId = getSpanId(location);
const appOrigin = useAppOrigin();
- const { isUpToDate, events, run } = useSyncRunPage({
+ const { isUpToDate, events, runs } = useSyncRunPage({
origin: appOrigin,
- runId: originalRun.id,
traceId: originalRun.traceId,
- spanId: originalRun.spanId,
});
- const initialLoad = !isUpToDate || !run;
-
- console.log(run);
+ const initialLoad = !isUpToDate || !runs;
const trace = useMemo(() => {
if (!events) return undefined;
@@ -269,16 +282,39 @@ function Panels({ resizable, run: originalRun }: LoaderData) {
return createTraceTreeFromEvents(preparedEvents, originalRun.spanId);
}, [events]);
- const inspectorSpanId = selectedSpanId
- ? selectedSpanId
- : originalRun.logsDeletedAt
- ? originalRun.spanId
- : undefined;
+ const inspectorState = useMemo(() => {
+ if (originalRun.logsDeletedAt) {
+ return {
+ type: "run",
+ run: runs?.find((r) => r.friendlyId === originalRun.friendlyId),
+ };
+ }
- const selectedSpan = useMemo(() => {
- if (!selectedSpanId || !events) return undefined;
- return createSpanFromEvents(events, selectedSpanId);
- }, [selectedSpanId]);
+ if (selectedSpanId) {
+ if (runs && runs.length > 0) {
+ const spanRun = runs.find((r) => r.spanId === selectedSpanId);
+ if (spanRun) {
+ return {
+ type: "run",
+ run: spanRun,
+ };
+ }
+ }
+
+ if (!events) {
+ return {
+ type: "span",
+ span: undefined,
+ };
+ }
+
+ const span = createSpanFromEvents(events, selectedSpanId);
+ return {
+ type: "span",
+ span,
+ };
+ }
+ }, [selectedSpanId, runs, events]);
return (
) : (
- {inspectorSpanId ? (
+ {inspectorState ? (
- {selectedSpan && run ? (
+ {inspectorState.type === "span" ? (
replaceSearchParam("span") : undefined}
+ runParam={originalRun.friendlyId}
+ span={inspectorState.span}
+ closePanel={!originalRun.logsDeletedAt ? () => replaceSearchParam("span") : undefined}
+ />
+ ) : inspectorState.type === "run" ? (
+ replaceSearchParam("span") : undefined}
/>
) : null}
@@ -321,7 +363,7 @@ function Panels({ resizable, run: originalRun }: LoaderData) {
}
type TraceData = {
- run: TaskRun;
+ run: Run;
environmentType: RuntimeEnvironmentType;
trace?: Trace;
selectedSpanId: string | undefined;
@@ -362,7 +404,7 @@ function TraceView({ run, environmentType, trace, selectedSpanId, replaceSearchP
);
}
-function NoLogsView({ run }: { run: TaskRun }) {
+function NoLogsView({ run }: { run: Run }) {
const plan = useCurrentPlan();
const organization = useOrganization();
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx
deleted file mode 100644
index cfdf46e2a..000000000
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx
+++ /dev/null
@@ -1,780 +0,0 @@
-import { CheckIcon, ClockIcon, CloudArrowDownIcon, QueueListIcon } from "@heroicons/react/20/solid";
-import { Link } from "@remix-run/react";
-import { LoaderFunctionArgs } from "@remix-run/server-runtime";
-import {
- formatDuration,
- formatDurationMilliseconds,
- nanosecondsToMilliseconds,
- TaskRunError,
-} from "@trigger.dev/core/v3";
-import { ReactNode, useEffect } from "react";
-import { typedjson, useTypedFetcher } from "remix-typedjson";
-import { ExitIcon } from "~/assets/icons/ExitIcon";
-import { CodeBlock } from "~/components/code/CodeBlock";
-import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
-import { Button, LinkButton } from "~/components/primitives/Buttons";
-import { Callout } from "~/components/primitives/Callout";
-import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime";
-import { Header2, Header3 } from "~/components/primitives/Headers";
-import { Paragraph } from "~/components/primitives/Paragraph";
-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 { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
-import { LiveTimer } from "~/components/runs/v3/LiveTimer";
-import { RunIcon } from "~/components/runs/v3/RunIcon";
-import { RunTag } from "~/components/runs/v3/RunTag";
-import { SpanEvents } from "~/components/runs/v3/SpanEvents";
-import { SpanTitle } from "~/components/runs/v3/SpanTitle";
-import { TaskRunAttemptStatusCombo } from "~/components/runs/v3/TaskRunAttemptStatus";
-import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
-import { useOrganization } from "~/hooks/useOrganizations";
-import { useProject } from "~/hooks/useProject";
-import { useSearchParams } from "~/hooks/useSearchParam";
-import { redirectWithErrorMessage } from "~/models/message.server";
-import { Span, SpanPresenter, 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 {
- v3RunDownloadLogsPath,
- v3RunPath,
- v3RunSpanPath,
- v3RunsPath,
- v3SchedulePath,
- v3SpanParamsSchema,
- v3TraceSpanPath,
-} from "~/utils/pathBuilder";
-import { SpanLink } from "~/v3/eventRepository.server";
-
-export const loader = async ({ request, params }: LoaderFunctionArgs) => {
- const userId = await requireUserId(request);
- const { projectParam, organizationSlug, runParam, spanParam } = v3SpanParamsSchema.parse(params);
-
- const presenter = new SpanPresenter();
-
- try {
- const result = await presenter.call({
- userId,
- organizationSlug,
- projectSlug: projectParam,
- spanId: spanParam,
- runFriendlyId: runParam,
- });
-
- return typedjson(result);
- } catch (error) {
- logger.error("Error loading span", {
- projectParam,
- organizationSlug,
- runParam,
- spanParam,
- error,
- });
- return redirectWithErrorMessage(
- v3RunPath({ slug: organizationSlug }, { slug: projectParam }, { friendlyId: runParam }),
- request,
- `Event not found.`
- );
- }
-};
-
-export function SpanView({
- runParam,
- spanId,
- closePanel,
-}: {
- runParam: string;
- spanId: string | undefined;
- closePanel?: () => void;
-}) {
- return <>>;
- // const organization = useOrganization();
- // const project = useProject();
- // const fetcher = useTypedFetcher();
-
- // useEffect(() => {
- // if (spanId === undefined) return;
- // fetcher.load(
- // `/resources/orgs/${organization.slug}/projects/v3/${project.slug}/runs/${runParam}/spans/${spanId}`
- // );
- // }, [organization.slug, project.slug, runParam, spanId]);
-
- // if (spanId === undefined) {
- // return null;
- // }
-
- // if (fetcher.state !== "idle" || fetcher.data === undefined) {
- // return (
- //
- // );
- // }
-
- // const { type } = fetcher.data;
-
- // switch (type) {
- // case "run": {
- // return (
- //
- // );
- // }
- // case "span": {
- // return ;
- // }
- // }
-}
-
-// function RunBody({
-// run,
-// runParam,
-// spanId,
-// closePanel,
-// }: {
-// run: SpanRun;
-// runParam: string;
-// spanId: string;
-// closePanel?: () => void;
-// }) {
-// const organization = useOrganization();
-// const project = useProject();
-// const { value, replace } = useSearchParams();
-// const tab = value("tab");
-
-// const environment = project.environments.find((e) => e.id === run.environmentId);
-
-// return (
-//
-//
-//
-//
-//
-// {run.taskIdentifier}
-//
-//
-// {runParam && closePanel && (
-//
-// )}
-//
-//
-//
-// {
-// replace({ tab: "overview" });
-// }}
-// shortcut={{ key: "o" }}
-// >
-// Overview
-//
-// {
-// replace({ tab: "detail" });
-// }}
-// shortcut={{ key: "d" }}
-// >
-// Detail
-//
-// {
-// replace({ tab: "context" });
-// }}
-// shortcut={{ key: "c" }}
-// >
-// Context
-//
-//
-//
-//
-//
-// {tab === "detail" ? (
-//
-//
-//
-// Status
-//
-//
-//
-//
-//
-// Task
-//
-//
-// {run.taskIdentifier}
-//
-// }
-// content={`Filter runs by ${run.taskIdentifier}`}
-// />
-//
-//
-//
-// Version
-//
-// {run.version ? (
-// run.version
-// ) : (
-//
-// Never started
-//
-//
-// )}
-//
-//
-//
-// SDK version
-//
-// {run.sdkVersion ? (
-// run.sdkVersion
-// ) : (
-//
-// Never started
-//
-//
-// )}
-//
-//
-//
-// Test run
-//
-// {run.isTest ? : "–"}
-//
-//
-// {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) => (
-//
-//
-//
-// }
-// content={`Filter runs by ${tag}`}
-// />
-// ))}
-//
-// )}
-//
-//
-// {run.links && run.links.length > 0 && (
-//
-// Links
-//
-//
-// {run.links.map((link, index) => (
-//
-// ))}
-//
-//
-//
-// )}
-//
-// 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" })
-// : "–"}
-//
-//
-//
-//
-// ) : tab === "context" ? (
-//
-//
-//
-// ) : (
-//
-//
-//
-//
-//
-// {run.payload !== undefined && (
-//
-// )}
-// {run.error !== undefined ? (
-//
-// ) : run.output !== undefined ? (
-//
-// ) : null}
-//
-// )}
-//
-//
-//
-//
-// {run.friendlyId !== runParam && (
-//
-// Focus on run
-//
-// )}
-//
-//
-// {run.logsDeletedAt === null ? (
-//
-// Download logs
-//
-// ) : null}
-//
-//
-//
-// );
-// }
-
-// function RunTimeline({ run }: { run: SpanRun }) {
-// return (
-//
-// }
-// state="complete"
-// />
-// {run.delayUntil && !run.expiredAt ? (
-// {formatDuration(run.createdAt, run.delayUntil)} delay>
-// ) : (
-//
-//
-//
-// Delayed until {run.ttl && <>(TTL {run.ttl})>}
-//
-//
-// )
-// }
-// state={run.startedAt ? "complete" : "delayed"}
-// />
-// ) : run.startedAt ? (
-//
-// ) : (
-//
-// {" "}
-// {run.ttl && <>(TTL {run.ttl})>}
-// >
-// }
-// state={run.startedAt || run.expiredAt ? "complete" : "inprogress"}
-// />
-// )}
-// {run.expiredAt ? (
-// }
-// state="error"
-// />
-// ) : run.startedAt ? (
-// <>
-// }
-// state="complete"
-// />
-// {run.isFinished ? (
-// <>
-//
-// }
-// state="complete"
-// />
-// >
-// ) : (
-//
-//
-//
-//
-//
-//
-// }
-// state={"inprogress"}
-// />
-// )}
-// >
-// ) : null}
-//
-// );
-// }
-
-// type RunTimelineItemProps = {
-// title: ReactNode;
-// subtitle?: ReactNode;
-// state: "complete" | "error";
-// };
-
-// function RunTimelineEvent({ title, subtitle, state }: RunTimelineItemProps) {
-// return (
-//
-//
-//
-// {title}
-// {subtitle ? {subtitle} : null}
-//
-//
-// );
-// }
-
-// type RunTimelineLineProps = {
-// title: ReactNode;
-// state: "complete" | "delayed" | "inprogress";
-// };
-
-// function RunTimelineLine({ title, state }: RunTimelineLineProps) {
-// return (
-//
-//
-//
-// {title}
-//
-//
-// );
-// }
-
-// function RunError({ error }: { error: TaskRunError }) {
-// switch (error.type) {
-// case "STRING_ERROR":
-// case "CUSTOM_ERROR": {
-// return (
-//
-//
-//
-// );
-// }
-// case "BUILT_IN_ERROR":
-// case "INTERNAL_ERROR": {
-// const name = "name" in error ? error.name : error.code;
-// return (
-//
-// {name}
-// {error.message && {error.message}}
-// {error.stackTrace && (
-//
-// )}
-//
-// );
-// }
-// }
-// }
-
-// function PacketDisplay({
-// data,
-// dataType,
-// title,
-// }: {
-// data: string;
-// dataType: string;
-// title: string;
-// }) {
-// switch (dataType) {
-// case "application/store": {
-// return (
-//
-//
-// {title}
-//
-//
-// Download
-//
-//
-// );
-// }
-// case "text/plain": {
-// return (
-//
-// );
-// }
-// default: {
-// return (
-//
-// );
-// }
-// }
-// }
-
-// type TimelineProps = {
-// startTime: Date;
-// duration: number;
-// inProgress: boolean;
-// isError: boolean;
-// };
-
-// type TimelineState = "error" | "pending" | "complete";
-
-// function SpanTimeline({ startTime, duration, inProgress, isError }: TimelineProps) {
-// const state = isError ? "error" : inProgress ? "pending" : "complete";
-// return (
-// <>
-//
-// }
-// state="complete"
-// />
-// {state === "pending" ? (
-//
-//
-//
-//
-//
-//
-// }
-// state={"inprogress"}
-// />
-// ) : (
-// <>
-//
-//
-// }
-// state={isError ? "error" : "complete"}
-// />
-// >
-// )}
-//
-// >
-// );
-// }
-
-// function VerticalBar({ state }: { state: TimelineState }) {
-// return ;
-// }
-
-// function DottedLine() {
-// return (
-//
-// );
-// }
-
-// function classNameForState(state: TimelineState) {
-// switch (state) {
-// case "pending": {
-// return "bg-pending";
-// }
-// case "complete": {
-// return "bg-success";
-// }
-// case "error": {
-// return "bg-error";
-// }
-// }
-// }
-
-// function SpanLinkElement({ link }: { link: SpanLink }) {
-// const organization = useOrganization();
-// const project = useProject();
-
-// switch (link.type) {
-// case "run": {
-// return (
-//
-// {link.title}
-//
-// );
-// }
-// case "span": {
-// return (
-//
-// {link.title}
-//
-// );
-// }
-// }
-
-// return null;
-// }
diff --git a/apps/webapp/app/routes/sync.traces.$traceId.ts b/apps/webapp/app/routes/sync.traces.$traceId.ts
index 123fc4ad6..f28e3f791 100644
--- a/apps/webapp/app/routes/sync.traces.$traceId.ts
+++ b/apps/webapp/app/routes/sync.traces.$traceId.ts
@@ -2,7 +2,7 @@ import type { LoaderFunctionArgs } from "@remix-run/node";
import { $replica } from "~/db.server";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
-import { getUserId, requireUserId } from "~/services/session.server";
+import { getUserId } from "~/services/session.server";
import { longPollingFetch } from "~/utils/longPollingFetch";
export async function loader({ params, request }: LoaderFunctionArgs) {
diff --git a/apps/webapp/app/routes/sync.runs.$runId.ts b/apps/webapp/app/routes/sync.traces.runs.$traceId.ts
similarity index 83%
rename from apps/webapp/app/routes/sync.runs.$runId.ts
rename to apps/webapp/app/routes/sync.traces.runs.$traceId.ts
index c8aff9358..ae4f0b57d 100644
--- a/apps/webapp/app/routes/sync.runs.$runId.ts
+++ b/apps/webapp/app/routes/sync.traces.runs.$traceId.ts
@@ -1,14 +1,20 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
+import { z } from "zod";
import { $replica } from "~/db.server";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { getUserId } from "~/services/session.server";
import { longPollingFetch } from "~/utils/longPollingFetch";
+const Params = z.object({
+ traceId: z.string(),
+});
+
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await getUserId(request);
+ const { traceId } = Params.parse(params);
- logger.log(`/sync/runs/${params.runId}`, { userId });
+ logger.log(`/sync/runs/${traceId}`, { userId });
if (!userId) {
return new Response("No user found in cookie", { status: 401 });
@@ -23,7 +29,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
},
},
where: {
- id: params.runId,
+ traceId,
},
});
@@ -48,7 +54,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
originUrl.searchParams.set(key, value);
});
- originUrl.searchParams.set("where", `"id"='${params.runId}'`);
+ originUrl.searchParams.set("where", `"traceId"='${traceId}'`);
return longPollingFetch(originUrl.toString());
}