diff --git a/apps/webapp/app/assets/icons/TraceIcon.tsx b/apps/webapp/app/assets/icons/TraceIcon.tsx
new file mode 100644
index 0000000000..20eb107848
--- /dev/null
+++ b/apps/webapp/app/assets/icons/TraceIcon.tsx
@@ -0,0 +1,9 @@
+export function TraceIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/apps/webapp/app/assets/icons/WaitpointTokenIcon.tsx b/apps/webapp/app/assets/icons/WaitpointTokenIcon.tsx
index 34ba9438c8..23269fb8f0 100644
--- a/apps/webapp/app/assets/icons/WaitpointTokenIcon.tsx
+++ b/apps/webapp/app/assets/icons/WaitpointTokenIcon.tsx
@@ -4,7 +4,7 @@ export function WaitpointTokenIcon({ className }: { className?: string }) {
diff --git a/apps/webapp/app/assets/icons/WarmStartIcon.tsx b/apps/webapp/app/assets/icons/WarmStartIcon.tsx
new file mode 100644
index 0000000000..211b27a98f
--- /dev/null
+++ b/apps/webapp/app/assets/icons/WarmStartIcon.tsx
@@ -0,0 +1,26 @@
+import { FireIcon } from "@heroicons/react/20/solid";
+import { cn } from "~/utils/cn";
+
+function ColdStartIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+export function WarmStartIcon({
+ isWarmStart,
+ className,
+}: {
+ isWarmStart: boolean;
+ className?: string;
+}) {
+ if (isWarmStart) {
+ return ;
+ }
+ return ;
+}
diff --git a/apps/webapp/app/components/WarmStarts.tsx b/apps/webapp/app/components/WarmStarts.tsx
new file mode 100644
index 0000000000..07a894b9e7
--- /dev/null
+++ b/apps/webapp/app/components/WarmStarts.tsx
@@ -0,0 +1,59 @@
+import { WarmStartIcon } from "~/assets/icons/WarmStartIcon";
+import { InfoIconTooltip, SimpleTooltip } from "./primitives/Tooltip";
+import { cn } from "~/utils/cn";
+import { Paragraph } from "./primitives/Paragraph";
+
+export function WarmStartCombo({
+ isWarmStart,
+ showTooltip = false,
+ className,
+}: {
+ isWarmStart: boolean;
+ showTooltip?: boolean;
+ className?: string;
+}) {
+ return (
+
+
+ {isWarmStart ? "Warm Start" : "Cold Start"}
+ {showTooltip && } />}
+
+ );
+}
+
+export function WarmStartIconWithTooltip({
+ isWarmStart,
+ className,
+}: {
+ isWarmStart: boolean;
+ className?: string;
+}) {
+ return (
+ }
+ content={}
+ />
+ );
+}
+
+function WarmStartTooltipContent() {
+ return (
+
+
+
+
+ A cold start happens when we need to boot up a new machine for your run to execute. This
+ takes longer than a warm start.
+
+
+
+
+
+ A warm start happens when we can reuse a machine from a run that recently finished. This
+ takes less time than a cold start.
+
+
+
+ );
+}
diff --git a/apps/webapp/app/components/primitives/Tooltip.tsx b/apps/webapp/app/components/primitives/Tooltip.tsx
index 80b1427cad..53a5f4959c 100644
--- a/apps/webapp/app/components/primitives/Tooltip.tsx
+++ b/apps/webapp/app/components/primitives/Tooltip.tsx
@@ -37,16 +37,18 @@ const TooltipContent = React.forwardRef<
React.ElementRef,
TooltipContentProps
>(({ className, sideOffset = 4, variant = "basic", ...props }, ref) => (
-
+
+
+
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
diff --git a/apps/webapp/app/components/runs/v3/RunIcon.tsx b/apps/webapp/app/components/runs/v3/RunIcon.tsx
index 903276a539..c03b32731d 100644
--- a/apps/webapp/app/components/runs/v3/RunIcon.tsx
+++ b/apps/webapp/app/components/runs/v3/RunIcon.tsx
@@ -18,6 +18,7 @@ import { MiddlewareIcon } from "~/assets/icons/MiddlewareIcon";
import { FunctionIcon } from "~/assets/icons/FunctionIcon";
import { TriggerIcon } from "~/assets/icons/TriggerIcon";
import { PythonLogoIcon } from "~/assets/icons/PythonLogoIcon";
+import { TraceIcon } from "~/assets/icons/TraceIcon";
type TaskIconProps = {
name: string | undefined;
@@ -65,7 +66,7 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
case "wait":
return ;
case "trace":
- return ;
+ return ;
case "tag":
return ;
case "queue":
diff --git a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts
index 3c09282c04..00f2846072 100644
--- a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts
@@ -2,12 +2,13 @@ import {
isWaitpointOutputTimeout,
type MachinePresetName,
prettyPrintPacket,
+ SemanticInternalAttributes,
TaskRunError,
} from "@trigger.dev/core/v3";
import { getMaxDuration } from "@trigger.dev/core/v3/isomorphic";
import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { logger } from "~/services/logger.server";
-import { eventRepository } from "~/v3/eventRepository.server";
+import { eventRepository, rehydrateAttribute } from "~/v3/eventRepository.server";
import { machinePresetFromName } from "~/v3/machinePresets.server";
import { getTaskEventStoreTableForRun, type TaskEventStoreTable } from "~/v3/taskEventStore.server";
import { isFailedRunStatus, isFinalRunStatus } from "~/v3/taskStatus";
@@ -455,7 +456,7 @@ export class SpanPresenter extends BasePresenter {
};
switch (span.entity.type) {
- case "waitpoint":
+ case "waitpoint": {
if (!span.entity.id) {
logger.error(`SpanPresenter: No waitpoint id`, {
spanId,
@@ -486,7 +487,23 @@ export class SpanPresenter extends BasePresenter {
object: waitpoint,
},
};
+ }
+ case "attempt": {
+ const isWarmStart = rehydrateAttribute(
+ span.metadata,
+ SemanticInternalAttributes.WARM_START
+ );
+ return {
+ ...data,
+ entity: {
+ type: "attempt" as const,
+ object: {
+ isWarmStart,
+ },
+ },
+ };
+ }
default:
return { ...data, entity: null };
}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
index 00ef7dd2ad..24ee6e6123 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
@@ -21,23 +21,18 @@ import {
tryCatch,
} from "@trigger.dev/core/v3";
import { type RuntimeEnvironmentType } from "@trigger.dev/database";
-import { AnimatePresence, motion } from "framer-motion";
+import { motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
-import { DisconnectedIcon } from "~/assets/icons/ConnectionIcons";
+import { redirect } from "remix-typedjson";
import { ShowParentIcon, ShowParentIconSelected } from "~/assets/icons/ShowParentIcon";
import tileBgPath from "~/assets/images/error-banner-tile@2x.png";
-import {
- DevDisconnectedBanner,
- useCrossEngineIsConnected,
- useDevPresence,
-} from "~/components/DevPresence";
+import { DevDisconnectedBanner, useCrossEngineIsConnected } from "~/components/DevPresence";
+import { WarmStartIconWithTooltip } from "~/components/WarmStarts";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { PageBody } from "~/components/layout/AppLayout";
import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
-import { Callout } from "~/components/primitives/Callout";
-import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTimeShort } from "~/components/primitives/DateTime";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { Header3 } from "~/components/primitives/Headers";
@@ -100,8 +95,6 @@ import {
} from "~/utils/pathBuilder";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route";
-import { redirectWithErrorMessage } from "~/models/message.server";
-import { redirect } from "remix-typedjson";
const resizableSettings = {
parent: {
@@ -1045,7 +1038,13 @@ function NodeText({ node }: { node: TraceEvent }) {
function NodeStatusIcon({ node }: { node: TraceEvent }) {
if (node.data.level !== "TRACE") return null;
- if (node.data.style.variant !== "primary") return null;
+ if (!node.data.style.variant) return null;
+
+ if (node.data.style.variant === "warm") {
+ return ;
+ } else if (node.data.style.variant === "cold") {
+ return ;
+ }
if (node.data.isCancelled) {
return (
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
index e11a93f6d2..b6ad7ec01f 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
@@ -4,13 +4,13 @@ import {
EnvelopeIcon,
QueueListIcon,
} from "@heroicons/react/20/solid";
-import { Link } from "@remix-run/react";
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 { ExitIcon } from "~/assets/icons/ExitIcon";
@@ -37,12 +37,16 @@ import { TabButton, TabContainer } from "~/components/primitives/Tabs";
import { TextLink } from "~/components/primitives/TextLink";
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
import { RunTimeline, RunTimelineEvent, SpanTimeline } from "~/components/run/RunTimeline";
+import { PacketDisplay } from "~/components/runs/v3/PacketDisplay";
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, TaskRunStatusReason } from "~/components/runs/v3/TaskRunStatus";
+import { WaitpointDetailTable } from "~/components/runs/v3/WaitpointDetails";
+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";
@@ -50,7 +54,6 @@ 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 {
@@ -63,15 +66,8 @@ import {
v3SchedulePath,
v3SpanParamsSchema,
} from "~/utils/pathBuilder";
-import {
- CompleteWaitpointForm,
- ForceTimeout,
-} from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route";
-import { useEnvironment } from "~/hooks/useEnvironment";
-import { WaitpointStatusCombo } from "~/components/runs/v3/WaitpointStatus";
-import { PacketDisplay } from "~/components/runs/v3/PacketDisplay";
-import { WaitpointDetailTable } from "~/components/runs/v3/WaitpointDetails";
import { createTimelineSpanEventsFromSpanEvents } from "~/utils/timelineSpanEvents";
+import { CompleteWaitpointForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { projectParam, organizationSlug, envParam, runParam, spanParam } =
@@ -935,6 +931,40 @@ function SpanEntity({ span }: { span: Span }) {
}
switch (span.entity.type) {
+ case "attempt": {
+ return (
+
+
+
+
+
+ {span.entity.object.isWarmStart !== undefined ? (
+
+ ) : null}
+
+ );
+ }
case "waitpoint": {
return (
@@ -957,7 +987,7 @@ function SpanEntity({ span }: { span: Span }) {
);
}
default: {
- return
No span for {span.entity.type};
+ assertNever(span.entity);
}
}
}
diff --git a/apps/webapp/app/v3/eventRepository.server.ts b/apps/webapp/app/v3/eventRepository.server.ts
index 990c6127ef..38a515925a 100644
--- a/apps/webapp/app/v3/eventRepository.server.ts
+++ b/apps/webapp/app/v3/eventRepository.server.ts
@@ -1638,7 +1638,7 @@ function rehydrateShow(properties: Prisma.JsonValue): { actions?: boolean } | un
return;
}
-function rehydrateAttribute
(
+export function rehydrateAttribute(
properties: Prisma.JsonValue,
key: string
): T | undefined {
@@ -1656,7 +1656,9 @@ function rehydrateAttribute(
const value = properties[key];
- if (!value) return;
+ if (value === undefined) {
+ return;
+ }
return value as T;
}
diff --git a/packages/core/src/v3/logger/taskLogger.ts b/packages/core/src/v3/logger/taskLogger.ts
index 8fbfd19cc6..aed1c1a7fb 100644
--- a/packages/core/src/v3/logger/taskLogger.ts
+++ b/packages/core/src/v3/logger/taskLogger.ts
@@ -99,7 +99,7 @@ export class OtelTaskLogger implements TaskLogger {
...options,
attributes: {
...options?.attributes,
- ...(options?.icon ? { [SemanticInternalAttributes.STYLE_ICON]: options.icon } : {}),
+ [SemanticInternalAttributes.STYLE_ICON]: options?.icon ?? "trace",
},
};
diff --git a/references/hello-world/src/trigger/example.ts b/references/hello-world/src/trigger/example.ts
index d1b008f417..b72fa03fa9 100644
--- a/references/hello-world/src/trigger/example.ts
+++ b/references/hello-world/src/trigger/example.ts
@@ -12,6 +12,20 @@ export const helloWorldTask = task({
logger.warn("warn: Hello, world!", { payload });
logger.error("error: Hello, world!", { payload });
+ logger.trace("my trace", async (span) => {
+ logger.debug("some log", { span });
+ });
+
+ logger.trace(
+ "my trace",
+ async (span) => {
+ logger.debug("some log", { span });
+ },
+ {
+ icon: "tabler-ad-circle",
+ }
+ );
+
await wait.for({ seconds: 5 });
return {