Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fruity-stars-bet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/web-shared": patch
---

Show event markers for step_started events
38 changes: 31 additions & 7 deletions packages/web-shared/src/sidebar/attribute-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,30 @@ const sortByAttributeOrder = (a: string, b: string): number => {
return aIndex - bIndex;
};

export const localMillisecondTime = (value: unknown): string => {
let date: Date;
if (value instanceof Date) {
date = value;
} else if (typeof value === 'number') {
date = new Date(value);
} else if (typeof value === 'string') {
date = new Date(value);
} else {
date = new Date(String(value));
}

// e.g. 12/17/2025, 9:08:55.182 AM
return date.toLocaleString(undefined, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
fractionalSecondDigits: 3,
});
};

interface DisplayContext {
stepName?: string;
}
Expand Down Expand Up @@ -356,13 +380,13 @@ const attributeToDisplayFn: Record<
executionContext: (_value: unknown) => null,
// Dates
// TODO: relative time with tooltips for ISO times
createdAt: (value: unknown) => new Date(String(value)).toLocaleString(),
startedAt: (value: unknown) => new Date(String(value)).toLocaleString(),
updatedAt: (value: unknown) => new Date(String(value)).toLocaleString(),
completedAt: (value: unknown) => new Date(String(value)).toLocaleString(),
expiredAt: (value: unknown) => new Date(String(value)).toLocaleString(),
retryAfter: (value: unknown) => new Date(String(value)).toLocaleString(),
resumeAt: (value: unknown) => new Date(String(value)).toLocaleString(),
createdAt: localMillisecondTime,
startedAt: localMillisecondTime,
updatedAt: localMillisecondTime,
completedAt: localMillisecondTime,
expiredAt: localMillisecondTime,
retryAfter: localMillisecondTime,
resumeAt: localMillisecondTime,
// Resolved attributes, won't actually use this function
metadata: JsonBlock,
input: (value: unknown, context?: DisplayContext) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/web-shared/src/sidebar/events-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert';
import type { SpanEvent } from '../trace-viewer/types';
import { convertEventsToSpanEvents } from '../workflow-traces/trace-span-construction';
import { AttributeBlock } from './attribute-panel';
import { AttributeBlock, localMillisecondTime } from './attribute-panel';
import { DetailCard } from './detail-card';

export function EventsList({
Expand Down Expand Up @@ -90,9 +90,9 @@ export function EventsList({
</span>{' '}
-{' '}
<span style={{ color: 'var(--ds-gray-700)' }}>
{new Date(
{localMillisecondTime(
event.timestamp[0] * 1000 + event.timestamp[1] / 1e6
).toLocaleString()}
)}
</span>
</>
}
Expand Down
15 changes: 1 addition & 14 deletions packages/web-shared/src/trace-viewer/components/node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,22 +134,11 @@ export const SpanComponent = memo(function SpanComponent({
? customSpanClassNameFunc(node)
: '';

// Calculate the "queued" width if there's an activeStartTime
const hasQueuedTime =
node.activeStartTime !== undefined && node.activeStartTime > node.startTime;
const queuedWidth = hasQueuedTime
? (node.activeStartTime! - node.startTime) * scale
: 0;

return (
<>
<button
aria-label={`${span.name} - ${duration}`}
className={clsx(
getSpanClassName(node, scale),
customClassName,
hasQueuedTime && styles.hasQueuedTime
)}
className={clsx(getSpanClassName(node, scale), customClassName)}
data-span-id={span.spanId}
data-start-time={node.startTime - root.startTime}
data-right-side={isNearRightSide}
Expand All @@ -158,8 +147,6 @@ export const SpanComponent = memo(function SpanComponent({
{
// Use actualWidth for CSS variable so hover expansion is accurate
'--span-width': `${Math.max(actualWidth, 1)}px`,
// Width of the "queued" portion (before activeStartTime)
'--queued-width': hasQueuedTime ? `${queuedWidth}px` : undefined,
minWidth: isHovered ? width : undefined,
width: isHovered ? undefined : width,
height,
Expand Down
39 changes: 3 additions & 36 deletions packages/web-shared/src/trace-viewer/trace-viewer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -1226,47 +1226,14 @@
}

.hasQueuedTime {
/* Shift the background to start after the queued portion */
background: linear-gradient(
90deg,
transparent var(--queued-width),
var(--span-background) var(--queued-width)
);
padding-left: var(--queued-width);

/* Dotted line for the queued/dead time */
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: var(--queued-width);
height: 0;
border-top: 2px dotted var(--ds-gray-400);
left: var(--queued-width);
height: 100%;
border-left: 2px dotted var(--ds-gray-700);
pointer-events: none;
}

/* When hovering, combine queued indicator with expansion gradient */
&.xHover {
background: linear-gradient(
90deg,
transparent var(--queued-width),
var(--span-background) var(--queued-width),
var(--span-background) calc(var(--span-width) - 1px),
var(--span-line),
var(--geist-background) calc(var(--span-width) + 1px)
);

&[data-right-side="true"] {
background: linear-gradient(
270deg,
var(--span-background) calc(var(--span-width) - 1px),
var(--span-line),
var(--geist-background) calc(var(--span-width) + 1px),
transparent calc(100% - var(--queued-width))
);
}
}
}

/* Sleep step - light yellow/orange */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const MARKER_EVENT_TYPES: Set<Event['eventType']> = new Set([
'hook_created',
'hook_received',
'hook_disposed',
'step_started',
'step_retrying',
'step_failed',
'workflow_failed',
Expand Down Expand Up @@ -108,7 +109,7 @@ export function stepToSpan(
};

const resource = 'step';
const endTime = step.completedAt ?? now;
const endTime = new Date(step.completedAt ?? now);

// Convert step-related events to span events (for markers like hook_created, step_retrying, etc.)
// This determines which events are displayed as markers. In the detail view,
Expand All @@ -118,7 +119,16 @@ export function stepToSpan(
// Use createdAt as span start time, with activeStartTime for when execution began
// This allows visualization of the "queued" period before execution
const spanStartTime = new Date(step.createdAt);
const activeStartTime = step.startedAt ? new Date(step.startedAt) : undefined;
let activeStartTime = step.startedAt ? new Date(step.startedAt) : undefined;
const firstStartEvent = stepEvents.find(
(event) => event.eventType === 'step_started'
);
if (firstStartEvent) {
// `step.startedAt` is the server-side creation timestamp, and `event.createdAt` is
// the client-side creation timestamp. For now, to align the event marker with the
// line we show for step.startedAt, we overwrite here to always use client-side time.
activeStartTime = new Date(firstStartEvent.createdAt);
}

return {
spanId: String(step.stepId),
Expand Down
Loading