From 07efef405dc4d804dfb70bfc3f7a966e07b28267 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 23 Feb 2024 09:54:01 +0000 Subject: [PATCH] v3 span details (#906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP on saving the resized state in a cookie * Progress on remembering the panel width * The resizable panel now remembers even when closed * Improvements to the span panel * Hide integrations in the v3 side menu * Added “UTC” to the times * WIP changing how styles work * Reference unflattenAttributes from the package * Unflatten now correctly unflattens arrays * Unflatten typecheck fix * Improvements to the spans and UI for them * Added the “events” to a span, including special styling for errors * Unflatten doesn’t work on an array of Attributes --- apps/webapp/app/components/VersionLabel.tsx | 10 +- .../app/components/navigation/SideMenu.tsx | 16 ++- .../app/components/primitives/Resizable.tsx | 3 +- .../app/components/runs/v3/EventText.tsx | 38 ------ .../app/components/runs/v3/LiveTimer.tsx | 5 +- .../app/components/runs/v3/SpanEvents.tsx | 70 ++++++++++ .../app/components/runs/v3/SpanTitle.tsx | 129 ++++++++++++++++++ .../app/components/runs/v3/TaskPath.tsx | 18 +++ .../app/presenters/v3/RunPresenter.server.ts | 8 +- .../app/presenters/v3/SpanPresenter.server.ts | 97 ++++++++++++- .../route.tsx | 55 ++++++-- .../route.tsx | 82 +++++++---- apps/webapp/app/services/resizablePanel.ts | 31 +++++ apps/webapp/app/v3/eventRepository.server.ts | 4 +- .../app/v3/services/triggerTask.server.ts | 4 +- packages/cli-v3/src/worker-facade.ts | 18 ++- packages/core/src/v3/schemas/style.ts | 25 +++- .../core/src/v3/semanticInternalAttributes.ts | 3 +- .../core/src/v3/utils/flattenAttributes.ts | 4 +- 19 files changed, 504 insertions(+), 116 deletions(-) delete mode 100644 apps/webapp/app/components/runs/v3/EventText.tsx create mode 100644 apps/webapp/app/components/runs/v3/SpanEvents.tsx create mode 100644 apps/webapp/app/components/runs/v3/SpanTitle.tsx create mode 100644 apps/webapp/app/components/runs/v3/TaskPath.tsx create mode 100644 apps/webapp/app/services/resizablePanel.ts diff --git a/apps/webapp/app/components/VersionLabel.tsx b/apps/webapp/app/components/VersionLabel.tsx index e51cbbd61..9c44e765c 100644 --- a/apps/webapp/app/components/VersionLabel.tsx +++ b/apps/webapp/app/components/VersionLabel.tsx @@ -1,12 +1,6 @@ import { RuntimeEnvironment } from "~/models/runtimeEnvironment.server"; -import { - EnvironmentLabel, - environmentBorderClassName, - environmentColorClassName, - environmentTextClassName, - environmentTitle, -} from "./environments/EnvironmentLabel"; import { cn } from "~/utils/cn"; +import { environmentTextClassName, environmentTitle } from "./environments/EnvironmentLabel"; type Environment = Pick; @@ -20,7 +14,7 @@ export function VersionLabel({ environment, userName, version }: VersionLabelPro return (
v{version}
diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 377116d2d..973152bbd 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -138,13 +138,15 @@ export function SideMenu({ user, project, organization, organizations }: SideMen leadingIconClassName="text-indigo-500" /> - + {project.version === "V2" && ( + + )} ); -export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; +export { ResizableHandle, ResizablePanel, ResizablePanelGroup }; diff --git a/apps/webapp/app/components/runs/v3/EventText.tsx b/apps/webapp/app/components/runs/v3/EventText.tsx deleted file mode 100644 index fe3b0c2f3..000000000 --- a/apps/webapp/app/components/runs/v3/EventText.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { TaskEventStyle } from "@trigger.dev/core/v3"; -import { TaskEventLevel } from "@trigger.dev/database"; - -type Event = { - isError: boolean; - style: TaskEventStyle; - level: TaskEventLevel; -}; - -export function eventTextClassName(event: Event) { - if (event.isError) { - return "text-rose-500"; - } - - switch (event.level) { - case "TRACE": { - return classNameForProminence(event.style.prominence); - } - case "LOG": - case "INFO": - case "DEBUG": { - return classNameForProminence(event.style.prominence); - } - case "WARN": { - return "text-amber-400"; - } - case "ERROR": { - return "text-rose-500"; - } - default: { - return classNameForProminence(event.style.prominence); - } - } -} - -function classNameForProminence(prominence: TaskEventStyle["prominence"]) { - return prominence === "high" ? "text-bright" : "text-dimmed"; -} diff --git a/apps/webapp/app/components/runs/v3/LiveTimer.tsx b/apps/webapp/app/components/runs/v3/LiveTimer.tsx index 81ec333fd..f6e5032e5 100644 --- a/apps/webapp/app/components/runs/v3/LiveTimer.tsx +++ b/apps/webapp/app/components/runs/v3/LiveTimer.tsx @@ -1,15 +1,18 @@ import { formatDuration } from "@trigger.dev/core/v3"; import { useState, useEffect } from "react"; import { Paragraph } from "~/components/primitives/Paragraph"; +import { cn } from "~/utils/cn"; export function LiveTimer({ startTime, endTime, updateInterval = 250, + className, }: { startTime: Date; endTime?: Date; updateInterval?: number; + className?: string; }) { const [now, setNow] = useState(); @@ -27,7 +30,7 @@ export function LiveTimer({ }, [startTime]); return ( - + {formatDuration(startTime, now, { style: "short", maxDecimalPoints: 0, diff --git a/apps/webapp/app/components/runs/v3/SpanEvents.tsx b/apps/webapp/app/components/runs/v3/SpanEvents.tsx new file mode 100644 index 000000000..3074c9745 --- /dev/null +++ b/apps/webapp/app/components/runs/v3/SpanEvents.tsx @@ -0,0 +1,70 @@ +import { CodeBlock } from "~/components/code/CodeBlock"; +import { Callout } from "~/components/primitives/Callout"; +import { DateTime } from "~/components/primitives/DateTime"; +import { Header2, Header3 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import type { OtelExceptionProperty, OtelSpanEvent } from "~/presenters/v3/SpanPresenter.server"; + +type SpanEventsProps = { + spanEvents: OtelSpanEvent[]; +}; + +export function SpanEvents({ spanEvents }: SpanEventsProps) { + return ( +
+ {spanEvents.map((event, index) => ( + + ))} +
+ ); +} + +function SpanEventHeader({ + title, + titleClassName, + time, +}: { + title: string; + titleClassName?: string; + time: Date; +}) { + return ( +
+ {title} + + + +
+ ); +} + +function SpanEvent({ spanEvent }: { spanEvent: OtelSpanEvent }) { + if (spanEvent.properties?.exception) { + return ; + } + + return ( +
+ + {spanEvent.properties && ( + + )} +
+ ); +} + +function SpanEventError({ + spanEvent, + exception, +}: { + spanEvent: OtelSpanEvent; + exception: OtelExceptionProperty; +}) { + return ( +
+ + {exception.message && {exception.message}} + {exception.stacktrace && } +
+ ); +} diff --git a/apps/webapp/app/components/runs/v3/SpanTitle.tsx b/apps/webapp/app/components/runs/v3/SpanTitle.tsx new file mode 100644 index 000000000..7397eab61 --- /dev/null +++ b/apps/webapp/app/components/runs/v3/SpanTitle.tsx @@ -0,0 +1,129 @@ +import { ChevronRightIcon } from "@heroicons/react/20/solid"; +import { Span } from "@opentelemetry/sdk-trace-base"; +import { TaskEventStyle } from "@trigger.dev/core/v3"; +import { TaskEventLevel } from "@trigger.dev/database"; +import { Fragment } from "react"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { cn } from "~/utils/cn"; + +type SpanTitleProps = { + message: string; + isError: boolean; + style: TaskEventStyle; + level: TaskEventLevel; + size: "small" | "large"; +}; + +export function SpanTitle(event: SpanTitleProps) { + return ( + + {event.message} + + ); +} + +function SpanAccessory({ + accessory, + size, +}: { + accessory: TaskEventStyle["accessory"]; + size: SpanTitleProps["size"]; +}) { + if (!accessory) { + return null; + } + + switch (accessory.style) { + case "codepath": { + return ( + + ); + } + default: { + return ( +
+ {accessory.items.map((item, index) => ( + + {item.text} + + ))} +
+ ); + } + } +} + +export function SpanCodePathAccessory({ + accessory, + className, +}: { + accessory: NonNullable; + className?: string; +}) { + return ( + + {accessory.items.map((item, index) => ( + + + {item.text} + + {index < accessory.items.length - 1 && ( + + + + )} + + ))} + + ); +} + +function eventTextClassName(event: SpanTitleProps) { + if (event.isError) { + return "text-rose-500"; + } + + switch (event.level) { + case "TRACE": { + return classNameForVariant(event.style.variant); + } + case "LOG": + case "INFO": + case "DEBUG": { + return classNameForVariant(event.style.variant); + } + case "WARN": { + return "text-amber-400"; + } + case "ERROR": { + return "text-rose-500"; + } + default: { + return classNameForVariant(event.style.variant); + } + } +} + +function classNameForVariant(variant: TaskEventStyle["variant"]) { + switch (variant) { + case "primary": { + return "text-blue-500"; + } + default: { + return "text-dimmed"; + } + } +} diff --git a/apps/webapp/app/components/runs/v3/TaskPath.tsx b/apps/webapp/app/components/runs/v3/TaskPath.tsx new file mode 100644 index 000000000..44b80a413 --- /dev/null +++ b/apps/webapp/app/components/runs/v3/TaskPath.tsx @@ -0,0 +1,18 @@ +import { SpanCodePathAccessory } from "./SpanTitle"; + +type TaskPathProps = { + filePath: string; + functionName: string; + className?: string; +}; + +export function TaskPath({ filePath, functionName, className }: TaskPathProps) { + return ( + + ); +} diff --git a/apps/webapp/app/presenters/v3/RunPresenter.server.ts b/apps/webapp/app/presenters/v3/RunPresenter.server.ts index 2d2c7b8c2..f2a6396b1 100644 --- a/apps/webapp/app/presenters/v3/RunPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunPresenter.server.ts @@ -1,4 +1,5 @@ -import { TaskEventStyle } from "@trigger.dev/core/v3"; +import { Attributes } from "@opentelemetry/api"; +import { TaskEventStyle, unflattenAttributes } from "@trigger.dev/core/v3"; import { TaskEvent } from "@trigger.dev/database"; import { createTreeFromFlatItems, flattenTree } from "~/components/primitives/TreeView/TreeView"; import { PrismaClient, prisma } from "~/db.server"; @@ -76,12 +77,15 @@ export class RunPresenter { const tree = createTreeFromFlatItems( events.map((event) => { + const styleUnflattened = unflattenAttributes(event.style as Attributes); + const style = TaskEventStyle.parse(styleUnflattened); + return { id: event.spanId, parentId: event.parentId ?? undefined, data: { message: event.message, - style: TaskEventStyle.parse(event.style), + style, duration: Number(event.duration), isError: event.isError, isPartial: event.isPartial, diff --git a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts index 3ed540b90..95f28287a 100644 --- a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts @@ -1,11 +1,35 @@ import { Attributes } from "@opentelemetry/api"; import { TaskEventStyle } from "@trigger.dev/core/v3"; import { unflattenAttributes } from "@trigger.dev/core/v3"; +import { z } from "zod"; import { PrismaClient, prisma, Prisma } from "~/db.server"; type Result = Awaited>; export type Span = Result["event"]; +const OtelExceptionProperty = z.object({ + type: z.string().optional(), + message: z.string().optional(), + stacktrace: z.string().optional(), +}); + +export type OtelExceptionProperty = z.infer; + +const OtelSpanEvent = z.object({ + name: z.string(), + time: z.coerce.date(), + properties: z + .object({ + exception: OtelExceptionProperty.optional(), + }) + .passthrough() + .optional(), +}); + +const OtelSpanEvents = z.array(OtelSpanEvent).optional(); + +export type OtelSpanEvent = z.infer; + export class SpanPresenter { #prismaClient: PrismaClient; @@ -35,32 +59,68 @@ export class SpanPresenter { } // Find the project scoped to the organization - const events = await this.#prismaClient.taskEvent.findMany({ + const matchingEvents = await this.#prismaClient.taskEvent.findMany({ where: { spanId, projectId: project.id, }, }); - const event = events.length > 1 ? events.find((event) => !event.isPartial) : events.at(0); + const event = + matchingEvents.length > 1 + ? matchingEvents.find((event) => !event.isPartial) + : matchingEvents.at(0); if (!event) { throw new Error("Span not found"); } + const styleUnflattened = unflattenAttributes(event.style as Attributes); + const style = TaskEventStyle.parse(styleUnflattened); + + const eventsUnflattened = event.events + ? (event.events as any[]).map((e) => ({ + ...e, + properties: unflattenAttributes(e.properties as Attributes), + })) + : undefined; + console.log("eventsUnflattened", eventsUnflattened); + const events = OtelSpanEvents.parse(eventsUnflattened); + return { event: { ...event, + events, output: isEmptyJson(event.output) ? null : JSON.stringify(event.output, null, 2), - properties: event.properties - ? JSON.stringify(unflattenAttributes(event.properties as Attributes), null, 2) - : null, - style: TaskEventStyle.parse(event.style), + properties: sanitizedAttributesStringified(event.properties), + style, duration: Number(event.duration), }, }; } } +function sanitizedAttributesStringified(json: Prisma.JsonValue): string | undefined { + const sanitizedAttributesValue = sanitizedAttributes(json); + if (!sanitizedAttributesValue) { + return; + } + + return JSON.stringify(sanitizedAttributesValue, null, 2); +} + +function sanitizedAttributes(json: Prisma.JsonValue): Record | undefined { + if (json === null || json === undefined) { + return; + } + + const withoutPrivateProperties = removePrivateProperties(json as Attributes); + if (!withoutPrivateProperties) { + return; + } + + return unflattenAttributes(withoutPrivateProperties); +} + function isEmptyJson(json: Prisma.JsonValue) { if (json === null) { return true; @@ -71,3 +131,28 @@ function isEmptyJson(json: Prisma.JsonValue) { return false; } + +// removes keys that start with a $ sign. If there are no keys left, return undefined +function removePrivateProperties( + attributes: Attributes | undefined | null +): Attributes | undefined { + if (!attributes) { + return undefined; + } + + const result: Attributes = {}; + + for (const [key, value] of Object.entries(attributes)) { + if (key.startsWith("$")) { + continue; + } + + result[key] = value; + } + + if (Object.keys(result).length === 0) { + return undefined; + } + + return result; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx index 35ed2545a..f10312b42 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx @@ -2,18 +2,22 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDurationNanoseconds, nanosecondsToMilliseconds } from "@trigger.dev/core/v3"; import { ReactNode } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { VersionLabel } from "~/components/VersionLabel"; import { CodeBlock } from "~/components/code/CodeBlock"; +import { InlineCode } from "~/components/code/InlineCode"; import { DateTime } from "~/components/primitives/DateTime"; -import { Header2, Header3 } from "~/components/primitives/Headers"; +import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { ShortcutKey } from "~/components/primitives/ShortcutKey"; -import { eventTextClassName } from "~/components/runs/v3/EventText"; +import { SpanTitle } from "~/components/runs/v3/SpanTitle"; import { LiveTimer } from "~/components/runs/v3/LiveTimer"; import { RunIcon } from "~/components/runs/v3/RunIcon"; import { SpanPresenter } from "~/presenters/v3/SpanPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { v3SpanParamsSchema } from "~/utils/pathBuilder"; +import { TaskPath } from "~/components/runs/v3/TaskPath"; +import { SpanEvents } from "~/components/runs/v3/SpanEvents"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -41,8 +45,8 @@ export default function Page() {
- - {event.message} + +
@@ -52,7 +56,7 @@ export default function Page() {
{event.level === "TRACE" ? ( - + - + UTC )} {event.message} + {event.taskSlug} + {event.taskPath && event.taskExportName && ( + + + + )} + + {event.queueName && {event.queueName}} + {event.workerVersion && ( + + + + )} + {event.events !== undefined && } + {event.output !== null && (
Output
)} - {event.properties !== null && ( + {event.properties !== undefined && (
Properties @@ -89,18 +115,19 @@ export default function Page() { } function PropertyTable({ children, className }: { children: ReactNode; className?: string }) { - return
{children}
; + return
{children}
; } type PropertyProps = { label: ReactNode; + labelClassName?: string; children: ReactNode; }; -function Property({ label, children }: PropertyProps) { +function Property({ label, labelClassName, children }: PropertyProps) { return ( <> -
+
{typeof label === "string" ? {label} : label}
@@ -127,14 +154,12 @@ function Timeline({ startTime, duration, inProgress, isError }: TimelineProps) { const state = isError ? "error" : inProgress ? "pending" : "complete"; return (
-
+
- + UTC {state === "pending" ? ( - - - + ) : ( 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 279aceece..ecd3d0de9 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 @@ -11,7 +11,12 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { ShowParentIcon, ShowParentIconSelected } from "~/assets/icons/ShowParentIcon"; import { PageBody } from "~/components/layout/AppLayout"; import { Input } from "~/components/primitives/Input"; -import { PageHeader, PageTitle, PageTitleRow } from "~/components/primitives/PageHeader"; +import { + PageButtons, + PageHeader, + PageTitle, + PageTitleRow, +} from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { ResizableHandle, @@ -21,7 +26,7 @@ import { import { Spinner } from "~/components/primitives/Spinner"; import { Switch } from "~/components/primitives/Switch"; import { TreeView, useTree } from "~/components/primitives/TreeView/TreeView"; -import { eventTextClassName } from "~/components/runs/v3/EventText"; +import { SpanTitle } from "~/components/runs/v3/SpanTitle"; import { LiveTimer } from "~/components/runs/v3/LiveTimer"; import { RunIcon } from "~/components/runs/v3/RunIcon"; import { useDebounce } from "~/hooks/useDebounce"; @@ -30,9 +35,12 @@ import { usePathName } from "~/hooks/usePathName"; import { useProject } from "~/hooks/useProject"; import { useThrottle } from "~/hooks/useThrottle"; import { RunEvent, RunPresenter } from "~/presenters/v3/RunPresenter.server"; +import { getResizableRunSettings, setResizableRunSettings } from "~/services/resizablePanel"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { v3RunParamsSchema, v3RunPath, v3RunSpanPath } from "~/utils/pathBuilder"; +import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { useUser } from "~/hooks/useUser"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -46,10 +54,14 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { runFriendlyId: runParam, }); + //resizable settings + const resizeSettings = await getResizableRunSettings(request); + return typedjson({ run, events, parentRunFriendlyId, + resizeSettings, }); }; @@ -60,11 +72,12 @@ function getSpanId(path: string): string | undefined { } export default function Page() { - const { run, events, parentRunFriendlyId } = useTypedLoaderData(); + const { run, events, parentRunFriendlyId, resizeSettings } = useTypedLoaderData(); const navigate = useNavigate(); const organization = useOrganization(); const pathName = usePathName(); const project = useProject(); + const user = useUser(); const selectedSpanId = getSpanId(pathName); @@ -72,18 +85,46 @@ export default function Page() { navigate(v3RunSpanPath(organization, project, run, { spanId: selectedSpan })); }, 250); + const usernameForEnv = user.id !== run.environment.userId ? run.environment.userName : undefined; + return ( <> + + + -
- - -
+
+ {selectedSpanId === undefined ? ( + { + //instantly close the panel if no span is selected + if (!selectedSpan) { + navigate(v3RunPath(organization, project, run)); + return; + } + + changeToSpan(selectedSpan); + }} + /> + ) : ( + { + if (layout.length !== 2) return; + setResizableRunSettings(document, layout); + }} + > + -
- - {selectedSpanId !== undefined && ( - <> - - - - - - )} - + + + + + + + )}
@@ -160,7 +197,7 @@ function TasksTreeView({ }); return ( -
+
- {node.data.message} + + ); } diff --git a/apps/webapp/app/services/resizablePanel.ts b/apps/webapp/app/services/resizablePanel.ts new file mode 100644 index 000000000..e3196a1c8 --- /dev/null +++ b/apps/webapp/app/services/resizablePanel.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +const ResizableConfig = z + .object({ layout: z.array(z.number()).optional() }) + .default({ layout: undefined }); +type ResizableConfig = z.infer; + +function getCookieValue(cookieHeader: string | null, cookieName: string): ResizableConfig { + const cookieValue = cookieHeader?.split(`${cookieName}=`)[1]?.split(";")[0]; + if (!cookieValue) { + return { layout: undefined }; + } + try { + const json = JSON.parse(cookieValue); + return ResizableConfig.parse(json); + } catch (e) { + return { layout: undefined }; + } +} + +//run page +const runResizableName = "resizable-panels:run"; + +export async function getResizableRunSettings(request: Request): Promise { + const cookieHeader = request.headers.get("Cookie"); + return getCookieValue(cookieHeader, runResizableName); +} + +export async function setResizableRunSettings(document: Document, layout: number[]) { + document.cookie = `${runResizableName}=${JSON.stringify({ layout })}`; +} diff --git a/apps/webapp/app/v3/eventRepository.server.ts b/apps/webapp/app/v3/eventRepository.server.ts index 810c3921f..6eba04dcb 100644 --- a/apps/webapp/app/v3/eventRepository.server.ts +++ b/apps/webapp/app/v3/eventRepository.server.ts @@ -3,7 +3,7 @@ import { PrismaClient, prisma } from "~/db.server"; import { RandomIdGenerator } from "@opentelemetry/sdk-trace-base"; import { Attributes, ROOT_CONTEXT, propagation, trace } from "@opentelemetry/api"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; -import { HIGH_PROMINENCE, SemanticInternalAttributes } from "@trigger.dev/core/v3"; +import { SemanticInternalAttributes, PRIMARY_VARIANT } from "@trigger.dev/core/v3"; import { flattenAttributes } from "@trigger.dev/core/v3"; import { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { DynamicFlushScheduler } from "./dynamicFlushScheduler.server"; @@ -230,7 +230,7 @@ export class EventRepository { const style = { [SemanticInternalAttributes.STYLE_ICON]: "task", - [SemanticInternalAttributes.STYLE_PROMINENCE]: HIGH_PROMINENCE, + [SemanticInternalAttributes.STYLE_VARIANT]: PRIMARY_VARIANT, }; if (!options.attributes.runId) { diff --git a/apps/webapp/app/v3/services/triggerTask.server.ts b/apps/webapp/app/v3/services/triggerTask.server.ts index be87e5a6e..02cb6b8f3 100644 --- a/apps/webapp/app/v3/services/triggerTask.server.ts +++ b/apps/webapp/app/v3/services/triggerTask.server.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { nanoid } from "nanoid"; import { $transaction, PrismaClient, prisma } from "~/db.server"; import { + PRIMARY_VARIANT, SemanticInternalAttributes, TriggerTaskRequestBody, flattenAttributes, @@ -45,7 +46,7 @@ export class TriggerTaskService extends BaseService { } return await eventRepository.traceEvent( - `Triggering task ${taskId}`, + `${taskId}`, { context: options.traceContext, kind: "SERVER", @@ -57,6 +58,7 @@ export class TriggerTaskService extends BaseService { }, style: { icon: "play", + variant: PRIMARY_VARIANT, }, }, }, diff --git a/packages/cli-v3/src/worker-facade.ts b/packages/cli-v3/src/worker-facade.ts index 03ab38a08..bb1ee1335 100644 --- a/packages/cli-v3/src/worker-facade.ts +++ b/packages/cli-v3/src/worker-facade.ts @@ -41,6 +41,7 @@ import { workerToChildMessages, TaskRunExecutionRetry, calculateNextRetryTimestamp, + Accessory, } from "@trigger.dev/core/v3"; import * as packageJson from "../package.json"; @@ -97,7 +98,7 @@ class TaskExecutor { ) { const parsedPayload = JSON.parse(execution.run.payload); const ctx = TaskRunContext.parse(execution); - const attemptMessage = `Attempt #${execution.attempt.number}`; + const attemptMessage = `Attempt ${execution.attempt.number}`; const output = await taskContextManager.runWith( { @@ -112,13 +113,25 @@ class TaskExecutor { [SemanticInternalAttributes.SDK_LANGUAGE]: "typescript", }); + const accessory: Accessory = { + items: [ + { + text: ctx.task.filePath, + }, + { + text: `${ctx.task.exportName}()`, + }, + ], + style: "codepath", + }; + return await tracer.startActiveSpan( attemptMessage, async (span) => { return await consoleInterceptor.intercept(console, async () => { const output = await this.task.run({ payload: parsedPayload, - ctx: TaskRunContext.parse(execution), + ctx, }); span.setAttributes(flattenAttributes(output, SemanticInternalAttributes.OUTPUT)); @@ -130,6 +143,7 @@ class TaskExecutor { kind: SpanKind.CONSUMER, attributes: { [SemanticInternalAttributes.STYLE_ICON]: "attempt", + ...flattenAttributes(accessory, SemanticInternalAttributes.STYLE_ACCESSORY), }, }, tracer.extractContext(traceContext) diff --git a/packages/core/src/v3/schemas/style.ts b/packages/core/src/v3/schemas/style.ts index c5f9531fe..a2a4f0de2 100644 --- a/packages/core/src/v3/schemas/style.ts +++ b/packages/core/src/v3/schemas/style.ts @@ -1,19 +1,32 @@ import { z } from "zod"; -export const LOW_PROMINENCE = "low"; -export const HIGH_PROMINENCE = "high"; +export const PRIMARY_VARIANT = "primary"; -const Prominence = z.enum([LOW_PROMINENCE, HIGH_PROMINENCE]); -export type Prominence = z.infer; +const Variant = z.enum([PRIMARY_VARIANT]); +export type Variant = z.infer; + +const AccessoryItem = z.object({ + text: z.string(), + variant: z.string().optional(), + url: z.string().optional(), +}); + +const Accessory = z.object({ + items: z.array(AccessoryItem), + style: z.enum(["codepath"]).optional(), +}); + +export type Accessory = z.infer; export const TaskEventStyle = z .object({ icon: z.string().optional(), - prominence: Prominence.default(LOW_PROMINENCE), + variant: Variant.optional(), + accessory: Accessory.optional(), }) .default({ icon: undefined, - prominence: LOW_PROMINENCE, + variant: undefined, }); export type TaskEventStyle = z.infer; diff --git a/packages/core/src/v3/semanticInternalAttributes.ts b/packages/core/src/v3/semanticInternalAttributes.ts index 8852efe7e..b188460ac 100644 --- a/packages/core/src/v3/semanticInternalAttributes.ts +++ b/packages/core/src/v3/semanticInternalAttributes.ts @@ -20,7 +20,8 @@ export const SemanticInternalAttributes = { OUTPUT: "$output", STYLE: "$style", STYLE_ICON: "$style.icon", - STYLE_PROMINENCE: "$style.prominence", + STYLE_VARIANT: "$style.variant", + STYLE_ACCESSORY: "$style.accessory", METADATA: "$metadata", TRIGGER: "$trigger", PAYLOAD: "$payload", diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index cfd2d1c7e..312c2dd38 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -65,8 +65,10 @@ export function unflattenAttributes(obj: Attributes): Record { // Check if part is not undefined and it's a string. if (typeof part === "string") { + const nextPart = parts[i + 1]; + const isArray = nextPart ? parseInt(nextPart, 10).toString() === nextPart : false; if (current[part] == null) { - current[part] = {}; + current[part] = isArray ? [] : {}; } current = current[part] as Record;