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 {