@workflow/web-shared 5.0.0-beta.23 → 5.0.0-beta.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -10
- package/dist/components/event-list-view.d.ts +30 -0
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +27 -12
- package/dist/components/index.d.ts +4 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -3
- package/dist/components/new-trace-viewer/components/copy-button.js +2 -2
- package/dist/components/new-trace-viewer/components/event-list.js +2 -2
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +2 -2
- package/dist/components/new-trace-viewer/components/span-markers.js +3 -3
- package/dist/components/new-trace-viewer/components/split-pane.js +2 -2
- package/dist/components/new-trace-viewer/components/timeline.js +2 -2
- package/dist/components/new-trace-viewer/trace-viewer.js +4 -4
- package/dist/components/run-trace-view.d.ts +5 -7
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +3 -3
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +3 -5
- package/dist/components/sidebar/detail-card.js +2 -2
- package/dist/components/sidebar/entity-detail-panel.d.ts +6 -10
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +8 -60
- package/dist/components/sidebar/events-list.d.ts +3 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +31 -9
- package/dist/components/sidebar/sidebar-data-context.d.ts +5 -6
- package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
- package/dist/components/sidebar/sidebar-data-context.js +1 -1
- package/dist/components/sidebar/span-detail-merge.d.ts +15 -0
- package/dist/components/sidebar/span-detail-merge.d.ts.map +1 -1
- package/dist/components/sidebar/span-detail-merge.js +40 -1
- package/dist/components/sidebar/use-selected-span-detail.d.ts +16 -0
- package/dist/components/sidebar/use-selected-span-detail.d.ts.map +1 -0
- package/dist/components/sidebar/use-selected-span-detail.js +93 -0
- package/dist/components/ui/alert.js +2 -2
- package/dist/components/ui/button.js +2 -2
- package/dist/components/ui/card.js +2 -2
- package/dist/components/ui/context-card.js +2 -2
- package/dist/components/ui/error-card.js +2 -2
- package/dist/components/ui/icon-button.js +2 -2
- package/dist/components/ui/kbd.js +2 -2
- package/dist/components/ui/skeleton.js +2 -2
- package/dist/components/ui/tooltip.js +2 -2
- package/dist/components/workflow-trace-view.d.ts +6 -7
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +3 -6
- package/dist/components/workflow-traces/trace-span-construction.d.ts +4 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +69 -29
- package/dist/lib/cn.d.ts +10 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +83 -0
- package/dist/lib/event-materialization.d.ts.map +1 -1
- package/dist/lib/event-materialization.js +22 -15
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +3 -3
- package/dist/lib/utils.d.ts +0 -2
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +1 -6
- package/package.json +3 -3
- package/src/components/event-list-view.tsx +76 -13
- package/src/components/index.ts +10 -6
- package/src/components/new-trace-viewer/components/copy-button.tsx +1 -1
- package/src/components/new-trace-viewer/components/event-list.tsx +1 -1
- package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +1 -1
- package/src/components/new-trace-viewer/components/span-markers.tsx +2 -2
- package/src/components/new-trace-viewer/components/split-pane.tsx +1 -1
- package/src/components/new-trace-viewer/components/timeline.tsx +1 -1
- package/src/components/new-trace-viewer/trace-viewer.tsx +6 -6
- package/src/components/run-trace-view.tsx +10 -14
- package/src/components/sidebar/attribute-panel.tsx +2 -4
- package/src/components/sidebar/detail-card.tsx +1 -1
- package/src/components/sidebar/entity-detail-panel.tsx +23 -94
- package/src/components/sidebar/events-list.tsx +58 -8
- package/src/components/sidebar/sidebar-data-context.tsx +6 -7
- package/src/components/sidebar/span-detail-merge.ts +69 -0
- package/src/components/sidebar/use-selected-span-detail.ts +146 -0
- package/src/components/ui/alert.tsx +1 -1
- package/src/components/ui/button.tsx +1 -1
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/context-card.tsx +1 -1
- package/src/components/ui/error-card.tsx +1 -1
- package/src/components/ui/icon-button.tsx +1 -1
- package/src/components/ui/kbd.tsx +1 -1
- package/src/components/ui/skeleton.tsx +1 -1
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/components/workflow-trace-view.tsx +11 -21
- package/src/components/workflow-traces/trace-span-construction.test.ts +85 -0
- package/src/components/workflow-traces/trace-span-construction.ts +93 -29
- package/src/lib/cn.ts +93 -0
- package/src/lib/event-materialization.ts +22 -14
- package/src/lib/trace-builder.ts +2 -1
- package/src/lib/utils.ts +0 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
4
|
-
import type { Event, Hook,
|
|
4
|
+
import type { Event, Hook, WorkflowRun } from '@workflow/world';
|
|
5
5
|
import {
|
|
6
6
|
ChevronDown,
|
|
7
7
|
ChevronUp,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type SpanSelectionInfo,
|
|
30
30
|
} from './sidebar/entity-detail-panel';
|
|
31
31
|
import { ResolveHookModal } from './sidebar/resolve-hook-modal';
|
|
32
|
+
import type { FetchSpanDetail } from './sidebar/use-selected-span-detail';
|
|
32
33
|
import {
|
|
33
34
|
TraceViewerContextProvider,
|
|
34
35
|
TraceViewerTimeline,
|
|
@@ -764,15 +765,12 @@ export const WorkflowTraceViewer = ({
|
|
|
764
765
|
events,
|
|
765
766
|
isLoading,
|
|
766
767
|
error,
|
|
767
|
-
|
|
768
|
-
spanDetailLoading,
|
|
769
|
-
spanDetailError,
|
|
768
|
+
fetchSpanDetail,
|
|
770
769
|
onWakeUpSleep,
|
|
771
770
|
onResolveHook,
|
|
772
771
|
onCancelRun,
|
|
773
772
|
onStreamClick,
|
|
774
773
|
onRunClick,
|
|
775
|
-
onSpanSelect,
|
|
776
774
|
onLoadEventData,
|
|
777
775
|
onLoadMoreSpans,
|
|
778
776
|
hasMoreSpans = false,
|
|
@@ -780,14 +778,13 @@ export const WorkflowTraceViewer = ({
|
|
|
780
778
|
encryptionKey,
|
|
781
779
|
onDecrypt,
|
|
782
780
|
isDecrypting = false,
|
|
781
|
+
showSeparateEventOccurrenceTimestamps = false,
|
|
783
782
|
}: {
|
|
784
783
|
run: WorkflowRun;
|
|
785
784
|
events: Event[];
|
|
786
785
|
isLoading?: boolean;
|
|
787
786
|
error?: Error | null;
|
|
788
|
-
|
|
789
|
-
spanDetailLoading?: boolean;
|
|
790
|
-
spanDetailError?: Error | null;
|
|
787
|
+
fetchSpanDetail: FetchSpanDetail;
|
|
791
788
|
onWakeUpSleep?: (
|
|
792
789
|
runId: string,
|
|
793
790
|
correlationId: string
|
|
@@ -803,8 +800,6 @@ export const WorkflowTraceViewer = ({
|
|
|
803
800
|
onStreamClick?: (streamId: string) => void;
|
|
804
801
|
/** Callback when a run reference is clicked in the detail panel */
|
|
805
802
|
onRunClick?: (runId: string) => void;
|
|
806
|
-
/** Callback when a span is selected. */
|
|
807
|
-
onSpanSelect?: (info: SpanSelectionInfo) => void;
|
|
808
803
|
/** Callback to load event data for a specific event (lazy loading in sidebar) */
|
|
809
804
|
onLoadEventData?: (
|
|
810
805
|
correlationId: string,
|
|
@@ -822,6 +817,8 @@ export const WorkflowTraceViewer = ({
|
|
|
822
817
|
onDecrypt?: () => void;
|
|
823
818
|
/** Whether the encryption key is currently being fetched */
|
|
824
819
|
isDecrypting?: boolean;
|
|
820
|
+
/** Show occurredAt separately instead of folding it into the Created timestamp. */
|
|
821
|
+
showSeparateEventOccurrenceTimestamps?: boolean;
|
|
825
822
|
}) => {
|
|
826
823
|
const toast = useToast();
|
|
827
824
|
const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
|
|
@@ -856,13 +853,6 @@ export const WorkflowTraceViewer = ({
|
|
|
856
853
|
}
|
|
857
854
|
}, [error, isLoading]);
|
|
858
855
|
|
|
859
|
-
const handleSpanSelect = useCallback(
|
|
860
|
-
(info: SpanSelectionInfo) => {
|
|
861
|
-
onSpanSelect?.(info);
|
|
862
|
-
},
|
|
863
|
-
[onSpanSelect]
|
|
864
|
-
);
|
|
865
|
-
|
|
866
856
|
const handleSelectionChange = useCallback(
|
|
867
857
|
(info: SelectedSpanInfo | null) => {
|
|
868
858
|
if (info) {
|
|
@@ -1158,10 +1148,7 @@ export const WorkflowTraceViewer = ({
|
|
|
1158
1148
|
run={run}
|
|
1159
1149
|
onStreamClick={onStreamClick}
|
|
1160
1150
|
onRunClick={onRunClick}
|
|
1161
|
-
|
|
1162
|
-
spanDetailError={spanDetailError}
|
|
1163
|
-
spanDetailLoading={spanDetailLoading}
|
|
1164
|
-
onSpanSelect={handleSpanSelect}
|
|
1151
|
+
fetchSpanDetail={fetchSpanDetail}
|
|
1165
1152
|
onWakeUpSleep={onWakeUpSleep}
|
|
1166
1153
|
onLoadEventData={onLoadEventData}
|
|
1167
1154
|
onResolveHook={onResolveHook}
|
|
@@ -1169,6 +1156,9 @@ export const WorkflowTraceViewer = ({
|
|
|
1169
1156
|
onDecrypt={onDecrypt}
|
|
1170
1157
|
isDecrypting={isDecrypting}
|
|
1171
1158
|
selectedSpan={selectedSpan}
|
|
1159
|
+
showSeparateEventOccurrenceTimestamps={
|
|
1160
|
+
showSeparateEventOccurrenceTimestamps
|
|
1161
|
+
}
|
|
1172
1162
|
/>
|
|
1173
1163
|
</ErrorBoundary>
|
|
1174
1164
|
</div>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Event, WorkflowRun } from '@workflow/world';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { runToSpan } from './trace-span-construction';
|
|
4
|
+
import { otelTimeToMs } from './trace-time-utils';
|
|
5
|
+
|
|
6
|
+
const date = (ms: number) => new Date(ms);
|
|
7
|
+
|
|
8
|
+
function run(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
|
|
9
|
+
return {
|
|
10
|
+
runId: 'run_1',
|
|
11
|
+
status: 'completed',
|
|
12
|
+
deploymentId: 'dpl_1',
|
|
13
|
+
workflowName: 'workflow//./workflow//testWorkflow',
|
|
14
|
+
specVersion: 2,
|
|
15
|
+
attributes: {},
|
|
16
|
+
createdAt: date(1_000),
|
|
17
|
+
updatedAt: date(6_000),
|
|
18
|
+
startedAt: date(2_000),
|
|
19
|
+
completedAt: date(6_000),
|
|
20
|
+
output: null,
|
|
21
|
+
...overrides,
|
|
22
|
+
} as WorkflowRun;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function event(overrides: Partial<Event>): Event {
|
|
26
|
+
return {
|
|
27
|
+
runId: 'run_1',
|
|
28
|
+
eventId: `evt_${overrides.eventType ?? 'unknown'}`,
|
|
29
|
+
eventType: 'run_created',
|
|
30
|
+
createdAt: date(1_000),
|
|
31
|
+
specVersion: 2,
|
|
32
|
+
eventData: {
|
|
33
|
+
deploymentId: 'dpl_1',
|
|
34
|
+
workflowName: 'workflow//./workflow//testWorkflow',
|
|
35
|
+
input: null,
|
|
36
|
+
},
|
|
37
|
+
...overrides,
|
|
38
|
+
} as Event;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('runToSpan', () => {
|
|
42
|
+
it('prefers occurredAt for run span timing when available', () => {
|
|
43
|
+
const span = runToSpan(
|
|
44
|
+
run(),
|
|
45
|
+
[
|
|
46
|
+
event({
|
|
47
|
+
eventType: 'run_created',
|
|
48
|
+
createdAt: date(1_000),
|
|
49
|
+
occurredAt: date(100),
|
|
50
|
+
}),
|
|
51
|
+
event({
|
|
52
|
+
eventType: 'run_started',
|
|
53
|
+
createdAt: date(2_000),
|
|
54
|
+
occurredAt: date(200),
|
|
55
|
+
}),
|
|
56
|
+
event({
|
|
57
|
+
eventType: 'run_completed',
|
|
58
|
+
createdAt: date(6_000),
|
|
59
|
+
occurredAt: date(500),
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
62
|
+
date(10_000)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(otelTimeToMs(span.startTime)).toBe(100);
|
|
66
|
+
expect(
|
|
67
|
+
span.activeStartTime ? otelTimeToMs(span.activeStartTime) : undefined
|
|
68
|
+
).toBe(200);
|
|
69
|
+
expect(otelTimeToMs(span.endTime)).toBe(500);
|
|
70
|
+
expect(
|
|
71
|
+
span.events.find((e) => e.name === 'run_created')?.timestamp
|
|
72
|
+
).toEqual([0, 100_000_000]);
|
|
73
|
+
|
|
74
|
+
const data = span.attributes.data as {
|
|
75
|
+
createdAt: Date;
|
|
76
|
+
startedAt?: Date;
|
|
77
|
+
completedAt?: Date;
|
|
78
|
+
occurredAt?: Date;
|
|
79
|
+
};
|
|
80
|
+
expect(data.createdAt).toEqual(date(100));
|
|
81
|
+
expect(data.startedAt).toEqual(date(200));
|
|
82
|
+
expect(data.completedAt).toEqual(date(500));
|
|
83
|
+
expect(data).not.toHaveProperty('occurredAt');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -29,13 +29,24 @@ const MARKER_EVENT_TYPES: Set<Event['eventType']> = new Set([
|
|
|
29
29
|
'attr_set',
|
|
30
30
|
]);
|
|
31
31
|
|
|
32
|
+
export const getEventTimestamp = (
|
|
33
|
+
event: Event | undefined
|
|
34
|
+
): Date | undefined => {
|
|
35
|
+
const value = event?.occurredAt ?? event?.createdAt;
|
|
36
|
+
if (!value) return undefined;
|
|
37
|
+
|
|
38
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
39
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
40
|
+
};
|
|
41
|
+
|
|
32
42
|
/**
|
|
33
43
|
* Convert workflow events to span events
|
|
34
44
|
* Only includes events that should be displayed as markers
|
|
35
45
|
*/
|
|
36
46
|
export function convertEventsToSpanEvents(
|
|
37
47
|
events: Event[],
|
|
38
|
-
filterTypes = true
|
|
48
|
+
filterTypes = true,
|
|
49
|
+
options: { preferOccurredAt?: boolean } = {}
|
|
39
50
|
): SpanEvent[] {
|
|
40
51
|
return events
|
|
41
52
|
.filter((event) =>
|
|
@@ -43,7 +54,11 @@ export function convertEventsToSpanEvents(
|
|
|
43
54
|
)
|
|
44
55
|
.map((event) => ({
|
|
45
56
|
name: event.eventType,
|
|
46
|
-
timestamp: dateToOtelTime(
|
|
57
|
+
timestamp: dateToOtelTime(
|
|
58
|
+
options.preferOccurredAt
|
|
59
|
+
? (getEventTimestamp(event) ?? event.createdAt)
|
|
60
|
+
: event.createdAt
|
|
61
|
+
),
|
|
47
62
|
attributes: {
|
|
48
63
|
eventId: event.eventId,
|
|
49
64
|
correlationId: event.correlationId,
|
|
@@ -73,11 +88,11 @@ export const waitEventsToWaitEntity = (
|
|
|
73
88
|
return {
|
|
74
89
|
waitId: startEvent.correlationId,
|
|
75
90
|
runId: startEvent.runId,
|
|
76
|
-
createdAt: startEvent.createdAt,
|
|
91
|
+
createdAt: getEventTimestamp(startEvent) ?? startEvent.createdAt,
|
|
77
92
|
resumeAt: startEvent.eventData?.resumeAt
|
|
78
93
|
? new Date(startEvent.eventData.resumeAt)
|
|
79
94
|
: undefined,
|
|
80
|
-
completedAt: completedEvent
|
|
95
|
+
completedAt: getEventTimestamp(completedEvent),
|
|
81
96
|
};
|
|
82
97
|
};
|
|
83
98
|
|
|
@@ -169,16 +184,16 @@ export const stepEventsToStepEntity = (
|
|
|
169
184
|
case 'step_started':
|
|
170
185
|
status = 'running';
|
|
171
186
|
attempt += 1;
|
|
172
|
-
if (!startedAt) startedAt = e.createdAt;
|
|
187
|
+
if (!startedAt) startedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
173
188
|
completedAt = undefined;
|
|
174
189
|
break;
|
|
175
190
|
case 'step_completed':
|
|
176
191
|
status = 'completed';
|
|
177
|
-
completedAt = e.createdAt;
|
|
192
|
+
completedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
178
193
|
break;
|
|
179
194
|
case 'step_failed':
|
|
180
195
|
status = 'failed';
|
|
181
|
-
completedAt = e.createdAt;
|
|
196
|
+
completedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
182
197
|
break;
|
|
183
198
|
case 'step_retrying':
|
|
184
199
|
status = 'pending';
|
|
@@ -191,14 +206,18 @@ export const stepEventsToStepEntity = (
|
|
|
191
206
|
if (attempt === 0) attempt = 1;
|
|
192
207
|
|
|
193
208
|
const lastEvent = events[events.length - 1];
|
|
209
|
+
|
|
194
210
|
return {
|
|
195
211
|
stepId: anchorEvent.correlationId ?? '',
|
|
196
212
|
runId: anchorEvent.runId,
|
|
197
213
|
stepName: createdEvent?.eventData?.stepName ?? '',
|
|
198
214
|
status,
|
|
199
215
|
attempt,
|
|
200
|
-
createdAt: anchorEvent.createdAt,
|
|
201
|
-
updatedAt:
|
|
216
|
+
createdAt: getEventTimestamp(anchorEvent) ?? anchorEvent.createdAt,
|
|
217
|
+
updatedAt:
|
|
218
|
+
getEventTimestamp(lastEvent) ??
|
|
219
|
+
lastEvent?.createdAt ??
|
|
220
|
+
anchorEvent.createdAt,
|
|
202
221
|
startedAt,
|
|
203
222
|
completedAt,
|
|
204
223
|
specVersion: anchorEvent.specVersion,
|
|
@@ -221,25 +240,41 @@ export function stepToSpan(stepEvents: Event[], maxEndTime: Date): Span | null {
|
|
|
221
240
|
};
|
|
222
241
|
|
|
223
242
|
const resource = 'step';
|
|
224
|
-
const endTime = new Date(step.completedAt ?? maxEndTime);
|
|
225
243
|
|
|
226
244
|
// Include ALL correlated events on the span so the sidebar detail view
|
|
227
245
|
// can display them. The timeline uses the `showVerticalLine` flag to
|
|
228
246
|
// determine which events appear as markers.
|
|
229
|
-
const events = convertEventsToSpanEvents(stepEvents, false
|
|
247
|
+
const events = convertEventsToSpanEvents(stepEvents, false, {
|
|
248
|
+
preferOccurredAt: true,
|
|
249
|
+
});
|
|
230
250
|
|
|
231
|
-
// Use
|
|
232
|
-
//
|
|
233
|
-
const
|
|
251
|
+
// Use the event occurrence timestamp for step spans when it is available,
|
|
252
|
+
// falling back to the ingest timestamp used by older event streams.
|
|
253
|
+
const spanStartEvent =
|
|
254
|
+
stepEvents.find((event) => event.eventType === 'step_created') ??
|
|
255
|
+
stepEvents[0];
|
|
256
|
+
const spanStartTime =
|
|
257
|
+
getEventTimestamp(spanStartEvent) ?? new Date(step.createdAt);
|
|
234
258
|
let activeStartTime = step.startedAt ? new Date(step.startedAt) : undefined;
|
|
235
259
|
const firstStartEvent = stepEvents.find(
|
|
236
260
|
(event) => event.eventType === 'step_started'
|
|
237
261
|
);
|
|
238
262
|
if (firstStartEvent) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
263
|
+
activeStartTime =
|
|
264
|
+
getEventTimestamp(firstStartEvent) ?? new Date(firstStartEvent.createdAt);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let endTime = new Date(maxEndTime);
|
|
268
|
+
if (step.completedAt) {
|
|
269
|
+
const completedEvent = stepEvents
|
|
270
|
+
.slice()
|
|
271
|
+
.reverse()
|
|
272
|
+
.find(
|
|
273
|
+
(event) =>
|
|
274
|
+
event.eventType === 'step_completed' ||
|
|
275
|
+
event.eventType === 'step_failed'
|
|
276
|
+
);
|
|
277
|
+
endTime = getEventTimestamp(completedEvent) ?? new Date(step.completedAt);
|
|
243
278
|
}
|
|
244
279
|
|
|
245
280
|
return {
|
|
@@ -292,10 +327,10 @@ export const hookEventsToHookEntity = (
|
|
|
292
327
|
hookId: createdEvent.correlationId,
|
|
293
328
|
runId: createdEvent.runId,
|
|
294
329
|
token: createdEvent.eventData?.token,
|
|
295
|
-
createdAt: createdEvent.createdAt,
|
|
330
|
+
createdAt: getEventTimestamp(createdEvent) ?? createdEvent.createdAt,
|
|
296
331
|
receivedCount: receivedEvents.length,
|
|
297
|
-
lastReceivedAt: lastReceivedEvent
|
|
298
|
-
disposedAt: disposedEvents.at(-1)
|
|
332
|
+
lastReceivedAt: getEventTimestamp(lastReceivedEvent),
|
|
333
|
+
disposedAt: getEventTimestamp(disposedEvents.at(-1)),
|
|
299
334
|
};
|
|
300
335
|
};
|
|
301
336
|
|
|
@@ -343,21 +378,50 @@ export function runToSpan(
|
|
|
343
378
|
): Span {
|
|
344
379
|
const now = nowTime ?? new Date();
|
|
345
380
|
|
|
381
|
+
// Prefer the event occurrence timestamp when available so the root span
|
|
382
|
+
// lines up with child spans that already use event occurrence time.
|
|
383
|
+
const runCreatedEvent = runEvents.find(
|
|
384
|
+
(event) => event.eventType === 'run_created'
|
|
385
|
+
);
|
|
386
|
+
const runStartedEvent = runEvents.find(
|
|
387
|
+
(event) => event.eventType === 'run_started'
|
|
388
|
+
);
|
|
389
|
+
const terminalEvent = runEvents
|
|
390
|
+
.slice()
|
|
391
|
+
.reverse()
|
|
392
|
+
.find(
|
|
393
|
+
(event) =>
|
|
394
|
+
event.eventType === 'run_completed' ||
|
|
395
|
+
event.eventType === 'run_failed' ||
|
|
396
|
+
event.eventType === 'run_cancelled'
|
|
397
|
+
);
|
|
398
|
+
const spanStartTime =
|
|
399
|
+
getEventTimestamp(runCreatedEvent) ?? new Date(run.createdAt);
|
|
400
|
+
const activeStartTime =
|
|
401
|
+
getEventTimestamp(runStartedEvent) ??
|
|
402
|
+
(run.startedAt ? new Date(run.startedAt) : undefined);
|
|
403
|
+
const completedAt =
|
|
404
|
+
getEventTimestamp(terminalEvent) ?? run.completedAt ?? undefined;
|
|
405
|
+
const endTime = completedAt ?? now;
|
|
406
|
+
|
|
346
407
|
// Only embed identification fields — not the full object with
|
|
347
|
-
// input/output/error which may contain non-cloneable types.
|
|
408
|
+
// input/output/error which may contain non-cloneable types. Lifecycle
|
|
409
|
+
// timestamps are event-derived so detail rows align with the span timeline.
|
|
348
410
|
const { input: _i, output: _o, error: _e, ...runIdentity } = run;
|
|
349
411
|
const attributes = {
|
|
350
412
|
resource: 'run' as const,
|
|
351
|
-
data:
|
|
413
|
+
data: {
|
|
414
|
+
...runIdentity,
|
|
415
|
+
createdAt: spanStartTime,
|
|
416
|
+
startedAt: activeStartTime,
|
|
417
|
+
completedAt,
|
|
418
|
+
},
|
|
352
419
|
};
|
|
353
420
|
|
|
354
|
-
// Use createdAt as span start time, with activeStartTime for when execution began
|
|
355
|
-
const spanStartTime = new Date(run.createdAt);
|
|
356
|
-
const activeStartTime = run.startedAt ? new Date(run.startedAt) : undefined;
|
|
357
|
-
const endTime = run.completedAt ?? now;
|
|
358
|
-
|
|
359
421
|
// Convert run-level events to span events
|
|
360
|
-
const events = convertEventsToSpanEvents(runEvents, false
|
|
422
|
+
const events = convertEventsToSpanEvents(runEvents, false, {
|
|
423
|
+
preferOccurredAt: true,
|
|
424
|
+
});
|
|
361
425
|
|
|
362
426
|
return {
|
|
363
427
|
spanId: String(run.runId),
|
package/src/lib/cn.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
2
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
const twMergeConfig = {
|
|
5
|
+
extend: {
|
|
6
|
+
classGroups: {
|
|
7
|
+
'text-heading': [
|
|
8
|
+
{
|
|
9
|
+
'text-heading': [
|
|
10
|
+
'72',
|
|
11
|
+
'64',
|
|
12
|
+
'56',
|
|
13
|
+
'48',
|
|
14
|
+
'40',
|
|
15
|
+
'32',
|
|
16
|
+
'24',
|
|
17
|
+
'20',
|
|
18
|
+
'16',
|
|
19
|
+
'14',
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
'text-button': [
|
|
24
|
+
{
|
|
25
|
+
'text-button': ['16', '14', '12'],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
'text-label': [
|
|
29
|
+
{
|
|
30
|
+
'text-label': [
|
|
31
|
+
'20',
|
|
32
|
+
'18',
|
|
33
|
+
'16',
|
|
34
|
+
'14',
|
|
35
|
+
'14-mono',
|
|
36
|
+
'13',
|
|
37
|
+
'13-mono',
|
|
38
|
+
'12',
|
|
39
|
+
'12-mono',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
'text-copy': [
|
|
44
|
+
{
|
|
45
|
+
'text-copy': ['24', '20', '18', '16', '14', '13', '13-mono'],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
material: [
|
|
49
|
+
{
|
|
50
|
+
material: [
|
|
51
|
+
'base',
|
|
52
|
+
'small',
|
|
53
|
+
'medium',
|
|
54
|
+
'large',
|
|
55
|
+
'tooltip',
|
|
56
|
+
'menu',
|
|
57
|
+
'modal',
|
|
58
|
+
'fullscreen',
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
// Make each category conflict with itself, so e.g. text-heading-32 overrides text-heading-24
|
|
64
|
+
conflictingClassGroups: {
|
|
65
|
+
'text-heading': ['text-heading'],
|
|
66
|
+
'text-button': ['text-button'],
|
|
67
|
+
'text-label': ['text-label'],
|
|
68
|
+
'text-copy': ['text-copy'],
|
|
69
|
+
material: ['material'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
type CustomClassGroupIds =
|
|
75
|
+
| 'text-heading'
|
|
76
|
+
| 'text-button'
|
|
77
|
+
| 'text-label'
|
|
78
|
+
| 'text-copy'
|
|
79
|
+
| 'material';
|
|
80
|
+
|
|
81
|
+
const customTwMerge = extendTailwindMerge<CustomClassGroupIds>(twMergeConfig);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Merges Tailwind class names and resolves any conflicts using `clsx` with `tailwind-merge`.
|
|
85
|
+
*
|
|
86
|
+
* @param inputs - An array of class names to merge.
|
|
87
|
+
* @returns A string of merged and optimized class names.
|
|
88
|
+
*/
|
|
89
|
+
function cn(...classes: ClassValue[]) {
|
|
90
|
+
return customTwMerge(clsx(...classes));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { cn };
|
|
@@ -82,6 +82,14 @@ function groupByCorrelationId(
|
|
|
82
82
|
return groups;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function getEventTimestamp(event: Event | undefined): Date | undefined {
|
|
86
|
+
const value = event?.occurredAt ?? event?.createdAt;
|
|
87
|
+
if (!value) return undefined;
|
|
88
|
+
|
|
89
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
90
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
// ---------------------------------------------------------------------------
|
|
86
94
|
// materializeSteps
|
|
87
95
|
// ---------------------------------------------------------------------------
|
|
@@ -104,31 +112,31 @@ export function materializeSteps(events: Event[]): MaterializedStep[] {
|
|
|
104
112
|
let attempt = 0;
|
|
105
113
|
let startedAt: Date | undefined;
|
|
106
114
|
let completedAt: Date | undefined;
|
|
107
|
-
let updatedAt = created.createdAt;
|
|
115
|
+
let updatedAt = getEventTimestamp(created) ?? created.createdAt;
|
|
108
116
|
|
|
109
117
|
for (const e of stepEvents) {
|
|
110
118
|
switch (e.eventType) {
|
|
111
119
|
case 'step_started':
|
|
112
120
|
status = 'running';
|
|
113
121
|
attempt += 1;
|
|
114
|
-
if (!startedAt) startedAt = e.createdAt;
|
|
122
|
+
if (!startedAt) startedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
115
123
|
completedAt = undefined;
|
|
116
|
-
updatedAt = e.createdAt;
|
|
124
|
+
updatedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
117
125
|
break;
|
|
118
126
|
case 'step_completed':
|
|
119
127
|
status = 'completed';
|
|
120
|
-
completedAt = e.createdAt;
|
|
121
|
-
updatedAt = e.createdAt;
|
|
128
|
+
completedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
129
|
+
updatedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
122
130
|
break;
|
|
123
131
|
case 'step_failed':
|
|
124
132
|
status = 'failed';
|
|
125
|
-
completedAt = e.createdAt;
|
|
126
|
-
updatedAt = e.createdAt;
|
|
133
|
+
completedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
134
|
+
updatedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
127
135
|
break;
|
|
128
136
|
case 'step_retrying':
|
|
129
137
|
status = 'pending';
|
|
130
138
|
completedAt = undefined;
|
|
131
|
-
updatedAt = e.createdAt;
|
|
139
|
+
updatedAt = getEventTimestamp(e) ?? e.createdAt;
|
|
132
140
|
break;
|
|
133
141
|
}
|
|
134
142
|
}
|
|
@@ -142,7 +150,7 @@ export function materializeSteps(events: Event[]): MaterializedStep[] {
|
|
|
142
150
|
: correlationId,
|
|
143
151
|
status,
|
|
144
152
|
attempt,
|
|
145
|
-
createdAt: created.createdAt,
|
|
153
|
+
createdAt: getEventTimestamp(created) ?? created.createdAt,
|
|
146
154
|
startedAt,
|
|
147
155
|
completedAt,
|
|
148
156
|
updatedAt,
|
|
@@ -181,10 +189,10 @@ export function materializeHooks(events: Event[]): MaterializedHook[] {
|
|
|
181
189
|
created.eventType === 'hook_created'
|
|
182
190
|
? created.eventData?.token
|
|
183
191
|
: undefined,
|
|
184
|
-
createdAt: created.createdAt,
|
|
192
|
+
createdAt: getEventTimestamp(created) ?? created.createdAt,
|
|
185
193
|
receivedCount: receivedEvents.length,
|
|
186
|
-
lastReceivedAt: lastReceived
|
|
187
|
-
disposedAt: disposed
|
|
194
|
+
lastReceivedAt: getEventTimestamp(lastReceived),
|
|
195
|
+
disposedAt: getEventTimestamp(disposed),
|
|
188
196
|
events: hookEvents,
|
|
189
197
|
});
|
|
190
198
|
}
|
|
@@ -213,12 +221,12 @@ export function materializeWaits(events: Event[]): MaterializedWait[] {
|
|
|
213
221
|
waitId: correlationId,
|
|
214
222
|
runId: created.runId,
|
|
215
223
|
status: completed ? 'completed' : 'waiting',
|
|
216
|
-
createdAt: created.createdAt,
|
|
224
|
+
createdAt: getEventTimestamp(created) ?? created.createdAt,
|
|
217
225
|
resumeAt:
|
|
218
226
|
created.eventType === 'wait_created'
|
|
219
227
|
? created.eventData?.resumeAt
|
|
220
228
|
: undefined,
|
|
221
|
-
completedAt: completed
|
|
229
|
+
completedAt: getEventTimestamp(completed),
|
|
222
230
|
events: waitEvents,
|
|
223
231
|
});
|
|
224
232
|
}
|
package/src/lib/trace-builder.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { Event, WorkflowRun } from '@workflow/world';
|
|
10
10
|
import type { Span } from '../components/trace-viewer/types';
|
|
11
11
|
import {
|
|
12
|
+
getEventTimestamp,
|
|
12
13
|
hookToSpan,
|
|
13
14
|
runToSpan,
|
|
14
15
|
stepToSpan,
|
|
@@ -132,7 +133,7 @@ export function groupEventsByCorrelation(events: Event[]): GroupedEvents {
|
|
|
132
133
|
function computeLatestKnownTime(events: Event[], run: WorkflowRun): Date {
|
|
133
134
|
let latest = new Date(run.createdAt).getTime();
|
|
134
135
|
for (const event of events) {
|
|
135
|
-
const t = new Date(event.createdAt).getTime();
|
|
136
|
+
const t = (getEventTimestamp(event) ?? new Date(event.createdAt)).getTime();
|
|
136
137
|
if (t > latest) latest = t;
|
|
137
138
|
}
|
|
138
139
|
return new Date(latest);
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type { Step } from '@workflow/world';
|
|
2
2
|
import type { ModelMessage } from 'ai';
|
|
3
|
-
import { type ClassValue, clsx } from 'clsx';
|
|
4
|
-
import { twMerge } from 'tailwind-merge';
|
|
5
|
-
|
|
6
|
-
export function cn(...inputs: ClassValue[]) {
|
|
7
|
-
return twMerge(clsx(inputs));
|
|
8
|
-
}
|
|
9
3
|
|
|
10
4
|
const MS_IN_SECOND = 1000;
|
|
11
5
|
const MS_IN_MINUTE = 60 * MS_IN_SECOND;
|