@workflow/web-shared 5.0.0-beta.3 → 5.0.0-beta.5
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/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/event-list.js +7 -4
- package/dist/components/new-trace-viewer/components/event-list.js.map +1 -1
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js.map +1 -0
- package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/split-pane.js +2 -3
- package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.d.ts +10 -11
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +57 -53
- package/dist/components/new-trace-viewer/components/timeline.js.map +1 -1
- package/dist/components/new-trace-viewer/context.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/context.js +5 -7
- package/dist/components/new-trace-viewer/context.js.map +1 -1
- package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/detail-panel.js +2 -1
- package/dist/components/new-trace-viewer/detail-panel.js.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +44 -18
- package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -1
- package/dist/components/new-trace-viewer/utils.d.ts +3 -8
- package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/utils.js +12 -65
- package/dist/components/new-trace-viewer/utils.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +6 -0
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/trace-viewer/util/timing.d.ts +1 -1
- package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
- package/dist/components/trace-viewer/util/timing.js +5 -8
- package/dist/components/trace-viewer/util/timing.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts.map +1 -1
- package/dist/components/ui/error-stack-block.js +24 -2
- package/dist/components/ui/error-stack-block.js.map +1 -1
- package/dist/components/workflow-trace-view.js +1 -1
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +17 -4
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/lib/hydration.d.ts +6 -0
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +124 -1
- package/dist/lib/hydration.js.map +1 -1
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +2 -1
- package/dist/lib/trace-builder.js.map +1 -1
- package/dist/lib/utils.d.ts +9 -7
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +27 -27
- package/dist/lib/utils.js.map +1 -1
- package/package.json +5 -4
- package/src/components/new-trace-viewer/components/event-list.tsx +26 -19
- package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
- package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
- package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
- package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
- package/src/components/new-trace-viewer/components/split-pane.tsx +2 -3
- package/src/components/new-trace-viewer/components/timeline.tsx +119 -115
- package/src/components/new-trace-viewer/context.tsx +7 -9
- package/src/components/new-trace-viewer/detail-panel.tsx +2 -1
- package/src/components/new-trace-viewer/trace-viewer.tsx +100 -32
- package/src/components/new-trace-viewer/utils.ts +19 -93
- package/src/components/sidebar/attribute-panel.tsx +5 -0
- package/src/components/trace-viewer/util/timing.ts +5 -9
- package/src/components/ui/error-stack-block.tsx +32 -4
- package/src/components/workflow-trace-view.tsx +1 -1
- package/src/components/workflow-traces/trace-span-construction.ts +23 -5
- package/src/lib/hydration.ts +132 -1
- package/src/lib/trace-builder.ts +2 -1
- package/src/lib/utils.ts +29 -29
|
@@ -30,33 +30,11 @@ export function computeRootBounds(spans: Span[]): RootBounds {
|
|
|
30
30
|
return { startTime: minStart, endTime: maxEnd, duration };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
toVisual(time: number): number;
|
|
39
|
-
fromVisual(fraction: number): number;
|
|
40
|
-
isCompressed: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function buildTimeCompression(
|
|
44
|
-
_spans: Span[],
|
|
45
|
-
viewStart: number,
|
|
46
|
-
viewEnd: number
|
|
47
|
-
): TimeCompression {
|
|
48
|
-
const range = viewEnd - viewStart;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
isCompressed: false,
|
|
52
|
-
toVisual(time: number): number {
|
|
53
|
-
if (range <= 0) return 0;
|
|
54
|
-
return Math.min(Math.max((time - viewStart) / range, 0), 1);
|
|
55
|
-
},
|
|
56
|
-
fromVisual(fraction: number): number {
|
|
57
|
-
return viewStart + fraction * range;
|
|
58
|
-
},
|
|
59
|
-
};
|
|
33
|
+
export function getSpanDurationMs(span: Span): number {
|
|
34
|
+
return Math.max(
|
|
35
|
+
0,
|
|
36
|
+
getHighResInMs(span.endTime) - getHighResInMs(span.startTime)
|
|
37
|
+
);
|
|
60
38
|
}
|
|
61
39
|
|
|
62
40
|
// ---------------------------------------------------------------------------
|
|
@@ -66,6 +44,7 @@ export function buildTimeCompression(
|
|
|
66
44
|
export interface TimeMarker {
|
|
67
45
|
position: number;
|
|
68
46
|
label: string;
|
|
47
|
+
value: number;
|
|
69
48
|
}
|
|
70
49
|
|
|
71
50
|
const NICE_INTERVALS = [
|
|
@@ -102,36 +81,7 @@ export function computeTimeMarkers(
|
|
|
102
81
|
markers.push({
|
|
103
82
|
position: Math.min(Math.max(position, 0), 1),
|
|
104
83
|
label: formatDuration(Math.abs(t), true),
|
|
105
|
-
|
|
106
|
-
if (markers.length >= MAX_MARKERS) break;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return markers;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function computeCompressedTimeMarkers(
|
|
113
|
-
compression: TimeCompression,
|
|
114
|
-
viewStart: number,
|
|
115
|
-
viewEnd: number,
|
|
116
|
-
rootStart: number
|
|
117
|
-
): TimeMarker[] {
|
|
118
|
-
const viewDuration = viewEnd - viewStart;
|
|
119
|
-
if (viewDuration <= 0) return [];
|
|
120
|
-
|
|
121
|
-
const maxTicks = 6;
|
|
122
|
-
const interval = pickInterval(viewDuration, maxTicks);
|
|
123
|
-
const offset = viewStart - rootStart;
|
|
124
|
-
|
|
125
|
-
const firstTick = Math.ceil(offset / interval) * interval;
|
|
126
|
-
const markers: TimeMarker[] = [];
|
|
127
|
-
|
|
128
|
-
for (let t = firstTick; t <= offset + viewDuration; t += interval) {
|
|
129
|
-
const absTime = rootStart + t;
|
|
130
|
-
const position = compression.toVisual(absTime);
|
|
131
|
-
if (position < -0.01 || position > 1.01) continue;
|
|
132
|
-
markers.push({
|
|
133
|
-
position: Math.min(Math.max(position, 0), 1),
|
|
134
|
-
label: formatDuration(Math.abs(t), true),
|
|
84
|
+
value: t,
|
|
135
85
|
});
|
|
136
86
|
if (markers.length >= MAX_MARKERS) break;
|
|
137
87
|
}
|
|
@@ -152,8 +102,12 @@ export interface SpanGap {
|
|
|
152
102
|
|
|
153
103
|
export function computeSpanGaps(
|
|
154
104
|
spans: Span[],
|
|
155
|
-
|
|
105
|
+
viewStart: number,
|
|
106
|
+
viewEnd: number
|
|
156
107
|
): SpanGap[] {
|
|
108
|
+
const range = viewEnd - viewStart;
|
|
109
|
+
if (range <= 0) return [];
|
|
110
|
+
|
|
157
111
|
const gaps: SpanGap[] = [];
|
|
158
112
|
for (let i = 0; i < spans.length - 1; i++) {
|
|
159
113
|
const endTime = getHighResInMs(spans[i].endTime);
|
|
@@ -161,8 +115,8 @@ export function computeSpanGaps(
|
|
|
161
115
|
const gapMs = startTime - endTime;
|
|
162
116
|
if (gapMs <= 0) continue;
|
|
163
117
|
|
|
164
|
-
const leftFrac =
|
|
165
|
-
const rightFrac =
|
|
118
|
+
const leftFrac = Math.min(Math.max((endTime - viewStart) / range, 0), 1);
|
|
119
|
+
const rightFrac = Math.min(Math.max((startTime - viewStart) / range, 0), 1);
|
|
166
120
|
if (rightFrac - leftFrac < 0.001) continue;
|
|
167
121
|
|
|
168
122
|
gaps.push({ gapMs, leftFrac, rightFrac, rowIndex: i });
|
|
@@ -379,39 +333,12 @@ function computeHookSegmentsFromSpan(
|
|
|
379
333
|
}
|
|
380
334
|
|
|
381
335
|
function computeSleepSegmentsFromSpan(
|
|
382
|
-
|
|
336
|
+
_startMs: number,
|
|
383
337
|
duration: number,
|
|
384
|
-
|
|
338
|
+
_events: SpanEvent[]
|
|
385
339
|
): Segment[] {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const completedTime = events
|
|
390
|
-
.filter((e) => e.name === 'wait_completed')
|
|
391
|
-
.map((e) => getHighResInMs(e.timestamp))
|
|
392
|
-
.sort((a, b) => a - b)[0];
|
|
393
|
-
|
|
394
|
-
if (completedTime === undefined) {
|
|
395
|
-
segments.push({ startFraction: 0, endFraction: 1, status: 'sleeping' });
|
|
396
|
-
return segments;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const completedFrac = timeToFraction(completedTime, startMs, duration);
|
|
400
|
-
if (completedFrac > 0.001) {
|
|
401
|
-
segments.push({
|
|
402
|
-
startFraction: 0,
|
|
403
|
-
endFraction: completedFrac,
|
|
404
|
-
status: 'sleeping',
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
segments.push({
|
|
409
|
-
startFraction: completedFrac,
|
|
410
|
-
endFraction: 1,
|
|
411
|
-
status: 'succeeded',
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
return segments;
|
|
340
|
+
if (duration <= 0) return [];
|
|
341
|
+
return [{ startFraction: 0, endFraction: 1, status: 'sleeping' }];
|
|
415
342
|
}
|
|
416
343
|
|
|
417
344
|
function computeRunSegmentsFromSpan(
|
|
@@ -497,8 +424,7 @@ function computeV1RunSegments(
|
|
|
497
424
|
*/
|
|
498
425
|
export function computeSpanSegments(span: Span): Segment[] {
|
|
499
426
|
const startMs = getHighResInMs(span.startTime);
|
|
500
|
-
const
|
|
501
|
-
const duration = endMs - startMs;
|
|
427
|
+
const duration = getSpanDurationMs(span);
|
|
502
428
|
const activeStartMs = span.activeStartTime
|
|
503
429
|
? getHighResInMs(span.activeStartTime)
|
|
504
430
|
: undefined;
|
|
@@ -441,6 +441,7 @@ const attributeToDisplayFn: Record<
|
|
|
441
441
|
// Hook details
|
|
442
442
|
token: (value: unknown) => String(value),
|
|
443
443
|
isWebhook: (value: unknown) => String(value),
|
|
444
|
+
isSystem: (value: unknown) => String(value),
|
|
444
445
|
receivedCount: (value: unknown) => String(value),
|
|
445
446
|
lastReceivedAt: localMillisecondTimeOrNull,
|
|
446
447
|
disposedAt: localMillisecondTimeOrNull,
|
|
@@ -624,6 +625,10 @@ const attributeToDisplayFn: Record<
|
|
|
624
625
|
if (!hasDisplayContent(value)) return null;
|
|
625
626
|
return <DetailCard summary="Event Data">{JsonBlock(value)}</DetailCard>;
|
|
626
627
|
},
|
|
628
|
+
errorCode: (value: unknown) => {
|
|
629
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
630
|
+
return String(value);
|
|
631
|
+
},
|
|
627
632
|
};
|
|
628
633
|
|
|
629
634
|
const resolvableAttributes = [
|
|
@@ -14,22 +14,18 @@ export function getMsInHighRes(ms: number): [number, number] {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Formats a duration for timeline display (compact
|
|
17
|
+
* Formats a duration for timeline display (compact format).
|
|
18
18
|
* @deprecated Use formatDuration(ms, true) instead.
|
|
19
19
|
*/
|
|
20
20
|
export const formatDurationForTimeline = (ms: number): string =>
|
|
21
21
|
formatDuration(ms, true);
|
|
22
22
|
|
|
23
|
-
const timeSelectionFormatter = new Intl.NumberFormat(undefined, {
|
|
24
|
-
minimumFractionDigits: 2,
|
|
25
|
-
maximumFractionDigits: 2,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
23
|
export function formatTimeSelection(ms: number): string {
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
const roundedMs = Math.round(ms);
|
|
25
|
+
if (roundedMs < 1000) {
|
|
26
|
+
return `${roundedMs}ms`;
|
|
31
27
|
}
|
|
32
|
-
return `${
|
|
28
|
+
return `${Math.round(ms / 1000)}s`;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
/**
|
|
@@ -18,6 +18,21 @@ export function isStructuredErrorWithStack(
|
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Pull a short, single-line title out of an error message.
|
|
23
|
+
*
|
|
24
|
+
* Workflow's structured error messages are multi-line — the first line is
|
|
25
|
+
* the headline (`Failed to serialize step return value`) and the rest are
|
|
26
|
+
* `╰▶ hint:` / `╰▶ docs:` framed details. The full message belongs in the
|
|
27
|
+
* body of the error block; the title should just be the headline so the
|
|
28
|
+
* card stays scannable.
|
|
29
|
+
*/
|
|
30
|
+
function deriveTitle(message: string): string {
|
|
31
|
+
const firstLine =
|
|
32
|
+
message.split('\n').find((line) => line.trim().length > 0) ?? message;
|
|
33
|
+
return firstLine.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
/**
|
|
22
37
|
* Renders an error with a `stack` field as a visually distinct error block.
|
|
23
38
|
* Shows the error message with an alert icon at the top, separated from
|
|
@@ -31,6 +46,10 @@ export function ErrorStackBlock({
|
|
|
31
46
|
const toast = useToast();
|
|
32
47
|
const stack = value.stack;
|
|
33
48
|
const message = typeof value.message === 'string' ? value.message : undefined;
|
|
49
|
+
// V8's `Error.stack` already starts with `Name: message`, so when the
|
|
50
|
+
// body shows the stack it includes the full multi-line message anyway.
|
|
51
|
+
// The header just needs the first line.
|
|
52
|
+
const title = message ? deriveTitle(message) : undefined;
|
|
34
53
|
const copyText = message ? `${message}\n\n${stack}` : stack;
|
|
35
54
|
|
|
36
55
|
return (
|
|
@@ -65,16 +84,25 @@ export function ErrorStackBlock({
|
|
|
65
84
|
<Copy size={12} />
|
|
66
85
|
</button>
|
|
67
86
|
|
|
68
|
-
{
|
|
87
|
+
{title && (
|
|
69
88
|
<div
|
|
70
|
-
className="flex items-
|
|
89
|
+
className="flex items-center gap-2 px-3 py-2.5 pr-10"
|
|
71
90
|
style={{
|
|
72
91
|
color: 'var(--ds-red-900)',
|
|
73
92
|
borderBottom: '1px solid var(--ds-red-400)',
|
|
74
93
|
}}
|
|
75
94
|
>
|
|
76
|
-
<AlertCircle className="h-4 w-4 shrink-0"
|
|
77
|
-
<p
|
|
95
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
96
|
+
<p
|
|
97
|
+
className="text-xs font-semibold m-0 truncate"
|
|
98
|
+
// The full multi-line message is in the stack body below; the
|
|
99
|
+
// header just shows the first line, single-line, with overflow
|
|
100
|
+
// ellipsised so a long title doesn't push the copy button or
|
|
101
|
+
// wrap into the framed hint/docs lines.
|
|
102
|
+
title={message}
|
|
103
|
+
>
|
|
104
|
+
{title}
|
|
105
|
+
</p>
|
|
78
106
|
</div>
|
|
79
107
|
)}
|
|
80
108
|
<pre
|
|
@@ -974,7 +974,7 @@ export const WorkflowTraceViewer = ({
|
|
|
974
974
|
}
|
|
975
975
|
|
|
976
976
|
return (
|
|
977
|
-
<div className="relative w-full h-full flex flex-
|
|
977
|
+
<div className="relative w-full h-full flex flex-row">
|
|
978
978
|
{/* Timeline (takes remaining space) */}
|
|
979
979
|
<div className="flex-1 min-w-0 relative">
|
|
980
980
|
<TraceViewerContextProvider
|
|
@@ -59,7 +59,7 @@ export const waitEventsToWaitEntity = (
|
|
|
59
59
|
waitId: string;
|
|
60
60
|
runId: string;
|
|
61
61
|
createdAt: Date;
|
|
62
|
-
resumeAt
|
|
62
|
+
resumeAt?: Date;
|
|
63
63
|
completedAt?: Date;
|
|
64
64
|
} | null => {
|
|
65
65
|
const startEvent = events.find((event) => event.eventType === 'wait_created');
|
|
@@ -73,7 +73,9 @@ export const waitEventsToWaitEntity = (
|
|
|
73
73
|
waitId: startEvent.correlationId,
|
|
74
74
|
runId: startEvent.runId,
|
|
75
75
|
createdAt: startEvent.createdAt,
|
|
76
|
-
resumeAt: startEvent.eventData?.resumeAt
|
|
76
|
+
resumeAt: startEvent.eventData?.resumeAt
|
|
77
|
+
? new Date(startEvent.eventData.resumeAt)
|
|
78
|
+
: undefined,
|
|
77
79
|
completedAt: completedEvent?.createdAt,
|
|
78
80
|
};
|
|
79
81
|
};
|
|
@@ -81,13 +83,29 @@ export const waitEventsToWaitEntity = (
|
|
|
81
83
|
/**
|
|
82
84
|
* Converts a workflow Wait to an OpenTelemetry Span
|
|
83
85
|
*/
|
|
84
|
-
export function waitToSpan(
|
|
86
|
+
export function waitToSpan(
|
|
87
|
+
events: Event[],
|
|
88
|
+
maxEndTime: Date,
|
|
89
|
+
fallbackEndTime = maxEndTime
|
|
90
|
+
): Span | null {
|
|
85
91
|
const wait = waitEventsToWaitEntity(events);
|
|
86
92
|
if (!wait) {
|
|
87
93
|
return null;
|
|
88
94
|
}
|
|
89
95
|
const startTime = wait.createdAt;
|
|
90
|
-
const
|
|
96
|
+
const startMs = startTime.getTime();
|
|
97
|
+
let endTime = wait.completedAt;
|
|
98
|
+
if (!endTime) {
|
|
99
|
+
const fallbackCap =
|
|
100
|
+
wait.resumeAt && wait.resumeAt.getTime() < fallbackEndTime.getTime()
|
|
101
|
+
? wait.resumeAt
|
|
102
|
+
: fallbackEndTime;
|
|
103
|
+
endTime =
|
|
104
|
+
maxEndTime.getTime() > startMs &&
|
|
105
|
+
maxEndTime.getTime() < fallbackCap.getTime()
|
|
106
|
+
? maxEndTime
|
|
107
|
+
: fallbackCap;
|
|
108
|
+
}
|
|
91
109
|
const start = dateToOtelTime(startTime);
|
|
92
110
|
const end = dateToOtelTime(endTime);
|
|
93
111
|
const duration = calculateDuration(startTime, endTime);
|
|
@@ -296,7 +314,7 @@ export function hookToSpan(hookEvents: Event[], maxEndTime: Date): Span | null {
|
|
|
296
314
|
|
|
297
315
|
return {
|
|
298
316
|
spanId: String(hook.hookId),
|
|
299
|
-
name: String(hook.hookId),
|
|
317
|
+
name: hook.token ?? String(hook.hookId),
|
|
300
318
|
kind: 1, // INTERNAL span kind
|
|
301
319
|
resource: 'hook',
|
|
302
320
|
library: WORKFLOW_LIBRARY,
|
package/src/lib/hydration.ts
CHANGED
|
@@ -58,12 +58,60 @@ function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
|
58
58
|
// Web revivers (browser-safe, no Buffer dependency)
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Build a reviver for one of the built-in `Error` subclasses (e.g.
|
|
63
|
+
* `TypeError`, `RangeError`). The constructor for the named subclass is
|
|
64
|
+
* resolved off `globalThis` at call time so the produced instance has the
|
|
65
|
+
* correct prototype chain in the consumer realm. Falls back to a generic
|
|
66
|
+
* `Error` (with `name` set) if the global isn't available, which keeps the
|
|
67
|
+
* o11y UI rendering even on exotic browsers.
|
|
68
|
+
*
|
|
69
|
+
* `cause` is passed through `ErrorOptions` to the constructor when present,
|
|
70
|
+
* matching `getCommonRevivers` in `@workflow/core` so the resulting `cause`
|
|
71
|
+
* property has the same semantics (non-enumerable, set by the engine) as a
|
|
72
|
+
* freshly thrown Error in the consumer realm. The `'cause' in value` check
|
|
73
|
+
* preserves the distinction between "no cause" and "cause is undefined".
|
|
74
|
+
*/
|
|
75
|
+
function makeWebErrorSubclassReviver(
|
|
76
|
+
name:
|
|
77
|
+
| 'EvalError'
|
|
78
|
+
| 'RangeError'
|
|
79
|
+
| 'ReferenceError'
|
|
80
|
+
| 'SyntaxError'
|
|
81
|
+
| 'TypeError'
|
|
82
|
+
| 'URIError'
|
|
83
|
+
) {
|
|
84
|
+
return (value: { message: string; stack?: string; cause?: unknown }) => {
|
|
85
|
+
const opts = 'cause' in value ? { cause: value.cause } : undefined;
|
|
86
|
+
const Ctor = (globalThis as Record<string, any>)[name] as
|
|
87
|
+
| ErrorConstructor
|
|
88
|
+
| undefined;
|
|
89
|
+
let error: Error;
|
|
90
|
+
if (typeof Ctor === 'function') {
|
|
91
|
+
error = new Ctor(value.message, opts);
|
|
92
|
+
} else {
|
|
93
|
+
// Fallback path: no built-in subclass available (exotic env). Construct
|
|
94
|
+
// a plain Error with the right `name` and copy `cause` manually since
|
|
95
|
+
// the base Error constructor is what we actually called.
|
|
96
|
+
error = Object.assign(new Error(value.message, opts), { name });
|
|
97
|
+
}
|
|
98
|
+
if (value.stack !== undefined) error.stack = value.stack;
|
|
99
|
+
return error;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
61
103
|
/**
|
|
62
104
|
* Get the web-specific revivers for hydrating serialized data.
|
|
63
105
|
*
|
|
64
106
|
* Uses `atob()` for base64 decoding (no Node.js Buffer dependency).
|
|
65
107
|
* All types are revived as real instances (Date, Map, Set, URL,
|
|
66
108
|
* URLSearchParams, Headers, Error, etc.).
|
|
109
|
+
*
|
|
110
|
+
* NOTE: this set must mirror the keys in `SerializableSpecial` (see
|
|
111
|
+
* `@workflow/core/serialization/types`). Any reducer key added on the
|
|
112
|
+
* serialization side that isn't covered here will cause `devalue.unflatten`
|
|
113
|
+
* to throw `Unknown type X`, which `hydrateResourceIO` swallows and
|
|
114
|
+
* surfaces as a "Failed to load resource details" banner in the o11y UI.
|
|
67
115
|
*/
|
|
68
116
|
export function getWebRevivers(): Revivers {
|
|
69
117
|
function reviveArrayBuffer(value: string): ArrayBuffer {
|
|
@@ -83,10 +131,93 @@ export function getWebRevivers(): Revivers {
|
|
|
83
131
|
BigUint64Array: (value: string) =>
|
|
84
132
|
new BigUint64Array(reviveArrayBuffer(value)),
|
|
85
133
|
Date: (value) => new Date(value),
|
|
134
|
+
|
|
135
|
+
// Error family. The reducer side (see
|
|
136
|
+
// `packages/core/src/serialization/reducers/common.ts`) emits a tagged
|
|
137
|
+
// entry for each built-in Error subclass plus the workflow-specific
|
|
138
|
+
// `FatalError` / `RetryableError` and `AggregateError`. Without
|
|
139
|
+
// matching revivers here, `devalue.unflatten` throws "Unknown type X"
|
|
140
|
+
// — which surfaces in the web o11y UI as "Failed to load resource
|
|
141
|
+
// details: Unknown type FatalError".
|
|
86
142
|
Error: (value) => {
|
|
143
|
+
const opts = 'cause' in value ? { cause: value.cause } : undefined;
|
|
144
|
+
const error = new Error(value.message, opts);
|
|
145
|
+
error.name = value.name;
|
|
146
|
+
if (value.stack !== undefined) error.stack = value.stack;
|
|
147
|
+
return error;
|
|
148
|
+
},
|
|
149
|
+
EvalError: makeWebErrorSubclassReviver('EvalError'),
|
|
150
|
+
RangeError: makeWebErrorSubclassReviver('RangeError'),
|
|
151
|
+
ReferenceError: makeWebErrorSubclassReviver('ReferenceError'),
|
|
152
|
+
SyntaxError: makeWebErrorSubclassReviver('SyntaxError'),
|
|
153
|
+
TypeError: makeWebErrorSubclassReviver('TypeError'),
|
|
154
|
+
URIError: makeWebErrorSubclassReviver('URIError'),
|
|
155
|
+
AggregateError: (value) => {
|
|
156
|
+
const opts = 'cause' in value ? { cause: value.cause } : undefined;
|
|
157
|
+
const Ctor = (
|
|
158
|
+
globalThis as { AggregateError?: AggregateErrorConstructor }
|
|
159
|
+
).AggregateError;
|
|
160
|
+
const error =
|
|
161
|
+
typeof Ctor === 'function'
|
|
162
|
+
? new Ctor(value.errors, value.message, opts)
|
|
163
|
+
: Object.assign(new Error(value.message, opts), {
|
|
164
|
+
name: 'AggregateError',
|
|
165
|
+
errors: value.errors,
|
|
166
|
+
});
|
|
167
|
+
if (value.stack !== undefined) error.stack = value.stack;
|
|
168
|
+
return error;
|
|
169
|
+
},
|
|
170
|
+
// `FatalError` and `RetryableError` are not built-in browser globals,
|
|
171
|
+
// so we can't resolve a constructor from globalThis. The web o11y UI
|
|
172
|
+
// doesn't need `instanceof FatalError` to pass (no user code runs
|
|
173
|
+
// here) — it just needs `name`, `message`, `stack`, and any extra
|
|
174
|
+
// enumerable fields to render. Construct a plain `Error` with `name`
|
|
175
|
+
// set; ObjectInspector reads `constructor.name` for the displayed
|
|
176
|
+
// class label, but we don't have the real class, so we emit a tagged
|
|
177
|
+
// Error whose `name` field carries the class identity. This matches
|
|
178
|
+
// how the existing base `Error` reviver presents unknown subclasses.
|
|
179
|
+
FatalError: (value) => {
|
|
180
|
+
const opts = 'cause' in value ? { cause: value.cause } : undefined;
|
|
181
|
+
const error = new Error(value.message, opts);
|
|
182
|
+
error.name = 'FatalError';
|
|
183
|
+
if (value.stack !== undefined) error.stack = value.stack;
|
|
184
|
+
return error;
|
|
185
|
+
},
|
|
186
|
+
RetryableError: (value) => {
|
|
187
|
+
const opts = 'cause' in value ? { cause: value.cause } : undefined;
|
|
188
|
+
const error = new Error(value.message, opts) as Error & {
|
|
189
|
+
retryAfter?: Date;
|
|
190
|
+
};
|
|
191
|
+
error.name = 'RetryableError';
|
|
192
|
+
if (value.stack !== undefined) error.stack = value.stack;
|
|
193
|
+
// `retryAfter` is serialized as an epoch ms number (see the runtime
|
|
194
|
+
// RetryableError reducer for the rationale around realm-safety).
|
|
195
|
+
// Rehydrate as a Date so o11y consumers can render it directly.
|
|
196
|
+
// Guard against payloads from older runtime versions that predate
|
|
197
|
+
// the field — without this check, `new Date(undefined)` would
|
|
198
|
+
// produce an Invalid Date rather than omitting the property.
|
|
199
|
+
if (value.retryAfter != null) {
|
|
200
|
+
error.retryAfter = new Date(value.retryAfter);
|
|
201
|
+
}
|
|
202
|
+
return error;
|
|
203
|
+
},
|
|
204
|
+
DOMException: (value) => {
|
|
205
|
+
// Modern browsers and Node 18+ expose `DOMException` on globalThis.
|
|
206
|
+
// `AbortController.abort()` with no argument synthesizes one as the
|
|
207
|
+
// signal's reason, so this is a common payload for any aborted step.
|
|
208
|
+
const G = globalThis as { DOMException?: typeof DOMException };
|
|
209
|
+
if (typeof G.DOMException === 'function') {
|
|
210
|
+
const e = new G.DOMException(value.message, value.name);
|
|
211
|
+
if (value.stack !== undefined) e.stack = value.stack;
|
|
212
|
+
if ('cause' in value) (e as { cause?: unknown }).cause = value.cause;
|
|
213
|
+
return e;
|
|
214
|
+
}
|
|
87
215
|
const error = new Error(value.message);
|
|
88
216
|
error.name = value.name;
|
|
89
|
-
error.stack = value.stack;
|
|
217
|
+
if (value.stack !== undefined) error.stack = value.stack;
|
|
218
|
+
if ('cause' in value) {
|
|
219
|
+
(error as Error & { cause?: unknown }).cause = value.cause;
|
|
220
|
+
}
|
|
90
221
|
return error;
|
|
91
222
|
},
|
|
92
223
|
Float32Array: (value: string) => new Float32Array(reviveArrayBuffer(value)),
|
package/src/lib/trace-builder.ts
CHANGED
|
@@ -117,6 +117,7 @@ function buildSpans(
|
|
|
117
117
|
// unknown territory. Even when the run is completed, we may not have loaded
|
|
118
118
|
// all events yet, so always use the latest event we've actually seen.
|
|
119
119
|
const childMaxEnd = latestKnownTime;
|
|
120
|
+
const runMaxEnd = run.completedAt ?? now;
|
|
120
121
|
|
|
121
122
|
const stepSpans = Array.from(groupedEvents.eventsByStepId.values())
|
|
122
123
|
.map((events) => stepToSpan(events, childMaxEnd))
|
|
@@ -127,7 +128,7 @@ function buildSpans(
|
|
|
127
128
|
.filter((span): span is Span => span !== null);
|
|
128
129
|
|
|
129
130
|
const waitSpans = Array.from(groupedEvents.timerEvents.values())
|
|
130
|
-
.map((events) => waitToSpan(events, childMaxEnd))
|
|
131
|
+
.map((events) => waitToSpan(events, childMaxEnd, runMaxEnd))
|
|
131
132
|
.filter((span): span is Span => span !== null);
|
|
132
133
|
|
|
133
134
|
return {
|
package/src/lib/utils.ts
CHANGED
|
@@ -7,15 +7,6 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
7
7
|
return twMerge(clsx(inputs));
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// const compactFormatter = new Intl.NumberFormat(undefined, {
|
|
11
|
-
// maximumFractionDigits: 2,
|
|
12
|
-
// });
|
|
13
|
-
|
|
14
|
-
const secondsFormatter = new Intl.NumberFormat(undefined, {
|
|
15
|
-
minimumFractionDigits: 2,
|
|
16
|
-
maximumFractionDigits: 2,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
10
|
const MS_IN_SECOND = 1000;
|
|
20
11
|
const MS_IN_MINUTE = 60 * MS_IN_SECOND;
|
|
21
12
|
const MS_IN_HOUR = 60 * MS_IN_MINUTE;
|
|
@@ -25,44 +16,53 @@ const MS_IN_DAY = 24 * MS_IN_HOUR;
|
|
|
25
16
|
* Formats a duration in milliseconds to a human-readable string.
|
|
26
17
|
*
|
|
27
18
|
* @param ms - Duration in milliseconds
|
|
28
|
-
* @param compact - If true, returns a
|
|
29
|
-
* If false (default), returns multi-part format (e.g., "1m
|
|
19
|
+
* @param compact - If true, returns a compact format (e.g., "380ms", "2m 30s").
|
|
20
|
+
* If false (default), returns multi-part format (e.g., "1m 13s", "2d 5h 3m 12s").
|
|
30
21
|
*
|
|
31
22
|
* Compact format (timeline markers):
|
|
32
|
-
* - <
|
|
33
|
-
* - <
|
|
34
|
-
* -
|
|
23
|
+
* - < 1s: shows milliseconds (e.g., "380ms")
|
|
24
|
+
* - < 1m: shows seconds (e.g., "45s")
|
|
25
|
+
* - < 1h: shows minutes and seconds (e.g., "2m 30s")
|
|
26
|
+
* - >= 1h: shows hours and minutes (e.g., "2h 30m")
|
|
35
27
|
*
|
|
36
28
|
* Full format:
|
|
37
|
-
* - <
|
|
38
|
-
* -
|
|
29
|
+
* - < 1s: shows milliseconds (e.g., "380ms")
|
|
30
|
+
* - < 1m: shows seconds (e.g., "45s")
|
|
31
|
+
* - >= 1m: shows decomposed format with whole seconds (e.g., "1m 13s", "2m 13s")
|
|
39
32
|
*/
|
|
40
33
|
export function formatDuration(ms: number, compact = false): string {
|
|
41
34
|
if (ms === 0) {
|
|
42
35
|
return '0s';
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
if (ms <
|
|
46
|
-
|
|
38
|
+
if (ms < MS_IN_SECOND) {
|
|
39
|
+
const roundedMs = Math.round(ms);
|
|
40
|
+
return roundedMs < MS_IN_SECOND ? `${roundedMs}ms` : '1s';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const roundedMs = Math.round(ms / MS_IN_SECOND) * MS_IN_SECOND;
|
|
44
|
+
|
|
45
|
+
if (roundedMs < MS_IN_MINUTE) {
|
|
46
|
+
return `${Math.floor(roundedMs / MS_IN_SECOND)}s`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// Compact format: multi-unit without decimals (e.g. "8m 20s", "2h 30m")
|
|
50
50
|
if (compact) {
|
|
51
|
-
if (
|
|
52
|
-
const m = Math.floor(
|
|
53
|
-
const s = Math.floor((
|
|
51
|
+
if (roundedMs < MS_IN_HOUR) {
|
|
52
|
+
const m = Math.floor(roundedMs / MS_IN_MINUTE);
|
|
53
|
+
const s = Math.floor((roundedMs % MS_IN_MINUTE) / MS_IN_SECOND);
|
|
54
54
|
return s > 0 ? `${m}m ${s}s` : `${m}m`;
|
|
55
55
|
}
|
|
56
|
-
const h = Math.floor(
|
|
57
|
-
const m = Math.floor((
|
|
56
|
+
const h = Math.floor(roundedMs / MS_IN_HOUR);
|
|
57
|
+
const m = Math.floor((roundedMs % MS_IN_HOUR) / MS_IN_MINUTE);
|
|
58
58
|
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Full format: decompose into larger units +
|
|
62
|
-
const days = Math.floor(
|
|
63
|
-
const hours = Math.floor((
|
|
64
|
-
const minutes = Math.floor((
|
|
65
|
-
const seconds = (
|
|
61
|
+
// Full format: decompose into larger units + whole seconds.
|
|
62
|
+
const days = Math.floor(roundedMs / MS_IN_DAY);
|
|
63
|
+
const hours = Math.floor((roundedMs % MS_IN_DAY) / MS_IN_HOUR);
|
|
64
|
+
const minutes = Math.floor((roundedMs % MS_IN_HOUR) / MS_IN_MINUTE);
|
|
65
|
+
const seconds = Math.floor((roundedMs % MS_IN_MINUTE) / MS_IN_SECOND);
|
|
66
66
|
|
|
67
67
|
const parts: string[] = [];
|
|
68
68
|
|
|
@@ -75,7 +75,7 @@ export function formatDuration(ms: number, compact = false): string {
|
|
|
75
75
|
if (minutes > 0) {
|
|
76
76
|
parts.push(`${minutes}m`);
|
|
77
77
|
}
|
|
78
|
-
parts.push(`${
|
|
78
|
+
parts.push(`${seconds}s`);
|
|
79
79
|
|
|
80
80
|
return parts.join(' ');
|
|
81
81
|
}
|