@workflow/web-shared 4.1.0-beta.62 → 4.1.0-beta.64
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 +4 -0
- package/dist/components/event-list-view.d.ts +9 -3
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +222 -98
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/run-trace-view.d.ts +1 -3
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +2 -2
- package/dist/components/run-trace-view.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +11 -1
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/detail-card.d.ts.map +1 -1
- package/dist/components/sidebar/detail-card.js +4 -2
- package/dist/components/sidebar/detail-card.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +3 -3
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +43 -26
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts +7 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.js +36 -11
- package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts +3 -4
- package/dist/components/ui/error-stack-block.d.ts.map +1 -1
- package/dist/components/ui/error-stack-block.js +18 -9
- package/dist/components/ui/error-stack-block.js.map +1 -1
- package/dist/components/ui/menu-dropdown.d.ts +16 -0
- package/dist/components/ui/menu-dropdown.d.ts.map +1 -0
- package/dist/components/ui/menu-dropdown.js +50 -0
- package/dist/components/ui/menu-dropdown.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts +3 -3
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +31 -129
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts +18 -5
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +65 -18
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/event-materialization.d.ts +72 -0
- package/dist/lib/event-materialization.d.ts.map +1 -0
- package/dist/lib/event-materialization.js +171 -0
- package/dist/lib/event-materialization.js.map +1 -0
- package/dist/lib/trace-builder.d.ts +32 -0
- package/dist/lib/trace-builder.d.ts.map +1 -0
- package/dist/lib/trace-builder.js +129 -0
- package/dist/lib/trace-builder.js.map +1 -0
- package/package.json +3 -3
- package/src/components/event-list-view.tsx +324 -103
- package/src/components/index.ts +1 -0
- package/src/components/run-trace-view.tsx +0 -6
- package/src/components/sidebar/attribute-panel.tsx +17 -2
- package/src/components/sidebar/detail-card.tsx +10 -2
- package/src/components/sidebar/entity-detail-panel.tsx +59 -21
- package/src/components/trace-viewer/trace-viewer.tsx +47 -2
- package/src/components/ui/error-stack-block.tsx +26 -16
- package/src/components/ui/menu-dropdown.tsx +114 -0
- package/src/components/workflow-trace-view.tsx +95 -195
- package/src/components/workflow-traces/trace-span-construction.ts +85 -32
- package/src/index.ts +13 -0
- package/src/lib/event-materialization.ts +243 -0
- package/src/lib/trace-builder.ts +201 -0
|
@@ -35,14 +35,7 @@ import {
|
|
|
35
35
|
getCustomSpanClassName,
|
|
36
36
|
getCustomSpanEventClassName,
|
|
37
37
|
} from './workflow-traces/trace-colors';
|
|
38
|
-
import {
|
|
39
|
-
hookToSpan,
|
|
40
|
-
runToSpan,
|
|
41
|
-
stepToSpan,
|
|
42
|
-
WORKFLOW_LIBRARY,
|
|
43
|
-
waitToSpan,
|
|
44
|
-
} from './workflow-traces/trace-span-construction';
|
|
45
|
-
import { otelTimeToMs } from './workflow-traces/trace-time-utils';
|
|
38
|
+
import { buildTrace, type TraceWithMeta } from '../lib/trace-builder';
|
|
46
39
|
|
|
47
40
|
/**
|
|
48
41
|
* While a run is live, continuously grow root.duration and rescale so the
|
|
@@ -273,7 +266,6 @@ function SpanContextMenu({
|
|
|
273
266
|
function TraceViewerWithContextMenu({
|
|
274
267
|
trace,
|
|
275
268
|
run,
|
|
276
|
-
hooks,
|
|
277
269
|
isLive,
|
|
278
270
|
onWakeUpSleep,
|
|
279
271
|
onCancelRun,
|
|
@@ -285,7 +277,6 @@ function TraceViewerWithContextMenu({
|
|
|
285
277
|
}: {
|
|
286
278
|
trace: { spans: Span[] };
|
|
287
279
|
run: WorkflowRun;
|
|
288
|
-
hooks: Hook[];
|
|
289
280
|
isLive: boolean;
|
|
290
281
|
onWakeUpSleep?: (
|
|
291
282
|
runId: string,
|
|
@@ -307,7 +298,10 @@ function TraceViewerWithContextMenu({
|
|
|
307
298
|
// Drive active span widths at 60fps without React re-renders
|
|
308
299
|
useLiveTick(isLive);
|
|
309
300
|
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
|
310
|
-
const [resolveHookTarget, setResolveHookTarget] = useState<
|
|
301
|
+
const [resolveHookTarget, setResolveHookTarget] = useState<{
|
|
302
|
+
hookId: string;
|
|
303
|
+
token: string;
|
|
304
|
+
} | null>(null);
|
|
311
305
|
const [resolvingHook, setResolvingHook] = useState(false);
|
|
312
306
|
// Track hooks resolved in this session so the context menu item hides immediately
|
|
313
307
|
const [resolvedHookIds, setResolvedHookIds] = useState<Set<string>>(
|
|
@@ -323,15 +317,6 @@ function TraceViewerWithContextMenu({
|
|
|
323
317
|
return map;
|
|
324
318
|
}, [trace.spans]);
|
|
325
319
|
|
|
326
|
-
// Build a lookup map: hookId -> Hook
|
|
327
|
-
const hookLookup = useMemo(() => {
|
|
328
|
-
const map = new Map<string, Hook>();
|
|
329
|
-
for (const hook of hooks) {
|
|
330
|
-
map.set(hook.hookId, hook);
|
|
331
|
-
}
|
|
332
|
-
return map;
|
|
333
|
-
}, [hooks]);
|
|
334
|
-
|
|
335
320
|
const handleResolveHook = useCallback(
|
|
336
321
|
async (payload: unknown) => {
|
|
337
322
|
if (resolvingHook || !resolveHookTarget || !onResolveHook) return;
|
|
@@ -344,11 +329,7 @@ function TraceViewerWithContextMenu({
|
|
|
344
329
|
}
|
|
345
330
|
try {
|
|
346
331
|
setResolvingHook(true);
|
|
347
|
-
await onResolveHook(
|
|
348
|
-
resolveHookTarget.token,
|
|
349
|
-
payload,
|
|
350
|
-
resolveHookTarget
|
|
351
|
-
);
|
|
332
|
+
await onResolveHook(resolveHookTarget.token, payload);
|
|
352
333
|
toast.success('Hook resolved', {
|
|
353
334
|
description: 'The payload has been sent and the hook resolved.',
|
|
354
335
|
});
|
|
@@ -496,22 +477,24 @@ function TraceViewerWithContextMenu({
|
|
|
496
477
|
|
|
497
478
|
// Hook-specific: Resolve Hook (only on active, unresolved hooks)
|
|
498
479
|
if (menu.resourceType === 'hook' && isRunActive && onResolveHook) {
|
|
499
|
-
const hook = hookLookup.get(menu.spanId);
|
|
500
480
|
const span = spanLookup.get(menu.spanId);
|
|
501
|
-
// Check data-level disposedAt, span events, AND local resolved state
|
|
502
481
|
const hookData = span?.attributes?.data as
|
|
503
|
-
| { disposedAt?: unknown }
|
|
482
|
+
| { token?: string; disposedAt?: unknown }
|
|
504
483
|
| undefined;
|
|
484
|
+
// Check data-level disposedAt, span events, AND local resolved state
|
|
505
485
|
const isDisposed =
|
|
506
486
|
Boolean(hookData?.disposedAt) ||
|
|
507
487
|
Boolean(span?.events?.some((e) => e.name === 'hook_disposed')) ||
|
|
508
488
|
resolvedHookIds.has(menu.spanId);
|
|
509
|
-
if (
|
|
489
|
+
if (hookData?.token && !isDisposed) {
|
|
510
490
|
items.push({
|
|
511
491
|
label: 'Resolve Hook',
|
|
512
492
|
icon: <Send className="h-3.5 w-3.5" />,
|
|
513
493
|
action: () => {
|
|
514
|
-
setResolveHookTarget(
|
|
494
|
+
setResolveHookTarget({
|
|
495
|
+
hookId: menu.spanId,
|
|
496
|
+
token: hookData.token!,
|
|
497
|
+
});
|
|
515
498
|
},
|
|
516
499
|
});
|
|
517
500
|
}
|
|
@@ -582,7 +565,6 @@ function TraceViewerWithContextMenu({
|
|
|
582
565
|
onWakeUpSleep,
|
|
583
566
|
onCancelRun,
|
|
584
567
|
onResolveHook,
|
|
585
|
-
hookLookup,
|
|
586
568
|
spanLookup,
|
|
587
569
|
resolvedHookIds,
|
|
588
570
|
run.runId,
|
|
@@ -610,159 +592,6 @@ function TraceViewerWithContextMenu({
|
|
|
610
592
|
);
|
|
611
593
|
}
|
|
612
594
|
|
|
613
|
-
type GroupedEvents = {
|
|
614
|
-
eventsByStepId: Map<string, Event[]>;
|
|
615
|
-
eventsByHookId: Map<string, Event[]>;
|
|
616
|
-
runLevelEvents: Event[];
|
|
617
|
-
timerEvents: Map<string, Event[]>;
|
|
618
|
-
hookEvents: Map<string, Event[]>;
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
const isTimerEvent = (eventType: string) =>
|
|
622
|
-
eventType === 'wait_created' || eventType === 'wait_completed';
|
|
623
|
-
|
|
624
|
-
const isHookLifecycleEvent = (eventType: string) =>
|
|
625
|
-
eventType === 'hook_received' ||
|
|
626
|
-
eventType === 'hook_created' ||
|
|
627
|
-
eventType === 'hook_disposed';
|
|
628
|
-
|
|
629
|
-
const pushEvent = (
|
|
630
|
-
map: Map<string, Event[]>,
|
|
631
|
-
correlationId: string,
|
|
632
|
-
event: Event
|
|
633
|
-
) => {
|
|
634
|
-
const existing = map.get(correlationId);
|
|
635
|
-
if (existing) {
|
|
636
|
-
existing.push(event);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
map.set(correlationId, [event]);
|
|
640
|
-
};
|
|
641
|
-
|
|
642
|
-
const groupEventsByCorrelation = (
|
|
643
|
-
events: Event[],
|
|
644
|
-
steps: Step[],
|
|
645
|
-
hooks: Hook[]
|
|
646
|
-
): GroupedEvents => {
|
|
647
|
-
const eventsByStepId = new Map<string, Event[]>();
|
|
648
|
-
const eventsByHookId = new Map<string, Event[]>();
|
|
649
|
-
const runLevelEvents: Event[] = [];
|
|
650
|
-
const timerEvents = new Map<string, Event[]>();
|
|
651
|
-
const hookEvents = new Map<string, Event[]>();
|
|
652
|
-
const stepIds = new Set(steps.map((step) => step.stepId));
|
|
653
|
-
const hookIds = new Set(hooks.map((hook) => hook.hookId));
|
|
654
|
-
|
|
655
|
-
for (const event of events) {
|
|
656
|
-
const correlationId = event.correlationId;
|
|
657
|
-
if (!correlationId) {
|
|
658
|
-
runLevelEvents.push(event);
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (isTimerEvent(event.eventType)) {
|
|
663
|
-
pushEvent(timerEvents, correlationId, event);
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (isHookLifecycleEvent(event.eventType)) {
|
|
668
|
-
pushEvent(hookEvents, correlationId, event);
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (stepIds.has(correlationId)) {
|
|
673
|
-
pushEvent(eventsByStepId, correlationId, event);
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (hookIds.has(correlationId)) {
|
|
678
|
-
pushEvent(eventsByHookId, correlationId, event);
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
runLevelEvents.push(event);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
return {
|
|
686
|
-
eventsByStepId,
|
|
687
|
-
eventsByHookId,
|
|
688
|
-
runLevelEvents,
|
|
689
|
-
timerEvents,
|
|
690
|
-
hookEvents,
|
|
691
|
-
};
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
const buildSpans = (
|
|
695
|
-
run: WorkflowRun,
|
|
696
|
-
steps: Step[],
|
|
697
|
-
groupedEvents: GroupedEvents,
|
|
698
|
-
now: Date
|
|
699
|
-
) => {
|
|
700
|
-
const viewerEndTime = new Date(run.completedAt || now);
|
|
701
|
-
const stepSpans = steps.map((step) => {
|
|
702
|
-
const stepEvents = groupedEvents.eventsByStepId.get(step.stepId) || [];
|
|
703
|
-
return stepToSpan(step, stepEvents, now, viewerEndTime);
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
const hookSpans = Array.from(groupedEvents.hookEvents.values())
|
|
707
|
-
.map((events) => hookToSpan(events, run, now))
|
|
708
|
-
.filter((span): span is Span => span !== null);
|
|
709
|
-
|
|
710
|
-
const waitSpans = Array.from(groupedEvents.timerEvents.values())
|
|
711
|
-
.map((events) => waitToSpan(events, run, now))
|
|
712
|
-
.filter((span): span is Span => span !== null);
|
|
713
|
-
|
|
714
|
-
return {
|
|
715
|
-
runSpan: runToSpan(run, groupedEvents.runLevelEvents, now),
|
|
716
|
-
spans: [...stepSpans, ...hookSpans, ...waitSpans],
|
|
717
|
-
};
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
const cascadeSpans = (runSpan: Span, spans: Span[]) => {
|
|
721
|
-
const sortedSpans = [
|
|
722
|
-
runSpan,
|
|
723
|
-
...spans.slice().sort((a, b) => {
|
|
724
|
-
const aStart = otelTimeToMs(a.startTime);
|
|
725
|
-
const bStart = otelTimeToMs(b.startTime);
|
|
726
|
-
return aStart - bStart;
|
|
727
|
-
}),
|
|
728
|
-
];
|
|
729
|
-
|
|
730
|
-
return sortedSpans.map((span, index) => {
|
|
731
|
-
const parentSpanId =
|
|
732
|
-
index === 0 ? undefined : String(sortedSpans[index - 1].spanId);
|
|
733
|
-
return {
|
|
734
|
-
...span,
|
|
735
|
-
parentSpanId,
|
|
736
|
-
};
|
|
737
|
-
});
|
|
738
|
-
};
|
|
739
|
-
|
|
740
|
-
const buildTrace = (
|
|
741
|
-
run: WorkflowRun,
|
|
742
|
-
steps: Step[],
|
|
743
|
-
hooks: Hook[],
|
|
744
|
-
events: Event[],
|
|
745
|
-
now: Date
|
|
746
|
-
) => {
|
|
747
|
-
const groupedEvents = groupEventsByCorrelation(events, steps, hooks);
|
|
748
|
-
const { runSpan, spans } = buildSpans(run, steps, groupedEvents, now);
|
|
749
|
-
const sortedCascadingSpans = cascadeSpans(runSpan, spans);
|
|
750
|
-
|
|
751
|
-
return {
|
|
752
|
-
traceId: run.runId,
|
|
753
|
-
rootSpanId: run.runId,
|
|
754
|
-
spans: sortedCascadingSpans,
|
|
755
|
-
resources: [
|
|
756
|
-
{
|
|
757
|
-
name: 'workflow',
|
|
758
|
-
attributes: {
|
|
759
|
-
'service.name': WORKFLOW_LIBRARY.name,
|
|
760
|
-
},
|
|
761
|
-
},
|
|
762
|
-
],
|
|
763
|
-
};
|
|
764
|
-
};
|
|
765
|
-
|
|
766
595
|
/** Re-export SpanSelectionInfo for consumers */
|
|
767
596
|
export type { SpanSelectionInfo };
|
|
768
597
|
|
|
@@ -870,14 +699,75 @@ function PanelResizeHandle({
|
|
|
870
699
|
);
|
|
871
700
|
}
|
|
872
701
|
|
|
702
|
+
// ---------------------------------------------------------------------------
|
|
703
|
+
// Trace viewer footer — loading / waiting indicator
|
|
704
|
+
// ---------------------------------------------------------------------------
|
|
705
|
+
|
|
706
|
+
function TraceViewerFooter({
|
|
707
|
+
hasMore,
|
|
708
|
+
isLive,
|
|
709
|
+
isInitialLoading,
|
|
710
|
+
}: {
|
|
711
|
+
hasMore: boolean;
|
|
712
|
+
isLive: boolean;
|
|
713
|
+
isInitialLoading: boolean;
|
|
714
|
+
}): ReactNode {
|
|
715
|
+
const style = { color: 'var(--ds-gray-900)' };
|
|
716
|
+
if (hasMore || isInitialLoading) {
|
|
717
|
+
return (
|
|
718
|
+
<div
|
|
719
|
+
className="flex items-center justify-center gap-2 py-3 text-xs"
|
|
720
|
+
style={style}
|
|
721
|
+
>
|
|
722
|
+
<svg
|
|
723
|
+
className="h-3.5 w-3.5 animate-spin"
|
|
724
|
+
viewBox="0 0 24 24"
|
|
725
|
+
fill="none"
|
|
726
|
+
>
|
|
727
|
+
<circle
|
|
728
|
+
className="opacity-25"
|
|
729
|
+
cx="12"
|
|
730
|
+
cy="12"
|
|
731
|
+
r="10"
|
|
732
|
+
stroke="currentColor"
|
|
733
|
+
strokeWidth="4"
|
|
734
|
+
/>
|
|
735
|
+
<path
|
|
736
|
+
className="opacity-75"
|
|
737
|
+
fill="currentColor"
|
|
738
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
739
|
+
/>
|
|
740
|
+
</svg>
|
|
741
|
+
Loading more events…
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
if (isLive) {
|
|
746
|
+
return (
|
|
747
|
+
<div
|
|
748
|
+
className="flex items-center justify-center py-3 text-xs"
|
|
749
|
+
style={style}
|
|
750
|
+
>
|
|
751
|
+
Waiting for more events…
|
|
752
|
+
</div>
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
return (
|
|
756
|
+
<div
|
|
757
|
+
className="flex items-center justify-center py-3 text-xs"
|
|
758
|
+
style={style}
|
|
759
|
+
>
|
|
760
|
+
End of run
|
|
761
|
+
</div>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
873
765
|
// ---------------------------------------------------------------------------
|
|
874
766
|
// Main component
|
|
875
767
|
// ---------------------------------------------------------------------------
|
|
876
768
|
|
|
877
769
|
export const WorkflowTraceViewer = ({
|
|
878
770
|
run,
|
|
879
|
-
steps,
|
|
880
|
-
hooks,
|
|
881
771
|
events,
|
|
882
772
|
isLoading,
|
|
883
773
|
error,
|
|
@@ -894,10 +784,9 @@ export const WorkflowTraceViewer = ({
|
|
|
894
784
|
hasMoreSpans = false,
|
|
895
785
|
isLoadingMoreSpans = false,
|
|
896
786
|
encryptionKey,
|
|
787
|
+
onDecrypt,
|
|
897
788
|
}: {
|
|
898
789
|
run: WorkflowRun;
|
|
899
|
-
steps: Step[];
|
|
900
|
-
hooks: Hook[];
|
|
901
790
|
events: Event[];
|
|
902
791
|
isLoading?: boolean;
|
|
903
792
|
error?: Error | null;
|
|
@@ -932,6 +821,8 @@ export const WorkflowTraceViewer = ({
|
|
|
932
821
|
isLoadingMoreSpans?: boolean;
|
|
933
822
|
/** Encryption key (available after Decrypt), threaded to event list for re-loading */
|
|
934
823
|
encryptionKey?: Uint8Array;
|
|
824
|
+
/** Callback to initiate decryption of encrypted run data */
|
|
825
|
+
onDecrypt?: () => void;
|
|
935
826
|
}) => {
|
|
936
827
|
const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
|
|
937
828
|
null
|
|
@@ -947,13 +838,14 @@ export const WorkflowTraceViewer = ({
|
|
|
947
838
|
|
|
948
839
|
// Build trace only when actual data changes — no timer-driven rebuilds.
|
|
949
840
|
// Active span widths are animated imperatively by useLiveTick at 60fps.
|
|
950
|
-
const
|
|
951
|
-
if (!run) {
|
|
841
|
+
const traceWithMeta: TraceWithMeta | undefined = useMemo(() => {
|
|
842
|
+
if (!run?.runId) {
|
|
952
843
|
return undefined;
|
|
953
844
|
}
|
|
954
|
-
return buildTrace(run,
|
|
845
|
+
return buildTrace(run, events, new Date());
|
|
955
846
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- `new Date()` is intentionally not a dep; useLiveTick handles live growth
|
|
956
|
-
}, [run,
|
|
847
|
+
}, [run, events]);
|
|
848
|
+
const trace = traceWithMeta;
|
|
957
849
|
|
|
958
850
|
useEffect(() => {
|
|
959
851
|
if (error && !isLoading) {
|
|
@@ -1064,7 +956,7 @@ export const WorkflowTraceViewer = ({
|
|
|
1064
956
|
);
|
|
1065
957
|
}, [selectedSpan?.data]);
|
|
1066
958
|
|
|
1067
|
-
if (
|
|
959
|
+
if (!trace) {
|
|
1068
960
|
return (
|
|
1069
961
|
<div className="relative w-full h-full">
|
|
1070
962
|
<div className="border-b border-gray-alpha-400 w-full" />
|
|
@@ -1093,7 +985,6 @@ export const WorkflowTraceViewer = ({
|
|
|
1093
985
|
<TraceViewerWithContextMenu
|
|
1094
986
|
trace={trace}
|
|
1095
987
|
run={run}
|
|
1096
|
-
hooks={hooks}
|
|
1097
988
|
isLive={isLive}
|
|
1098
989
|
onWakeUpSleep={onWakeUpSleep}
|
|
1099
990
|
onCancelRun={onCancelRun}
|
|
@@ -1107,6 +998,15 @@ export const WorkflowTraceViewer = ({
|
|
|
1107
998
|
height="100%"
|
|
1108
999
|
isLive={isLive}
|
|
1109
1000
|
trace={trace}
|
|
1001
|
+
knownDurationMs={traceWithMeta?.knownDurationMs}
|
|
1002
|
+
hasMoreData={hasMoreSpans || Boolean(isLoading)}
|
|
1003
|
+
footer={
|
|
1004
|
+
<TraceViewerFooter
|
|
1005
|
+
hasMore={hasMoreSpans}
|
|
1006
|
+
isLive={isLive}
|
|
1007
|
+
isInitialLoading={Boolean(isLoading)}
|
|
1008
|
+
/>
|
|
1009
|
+
}
|
|
1110
1010
|
/>
|
|
1111
1011
|
</TraceViewerWithContextMenu>
|
|
1112
1012
|
</TraceViewerContextProvider>
|
|
@@ -1246,7 +1146,6 @@ export const WorkflowTraceViewer = ({
|
|
|
1246
1146
|
<ErrorBoundary title="Failed to load entity details">
|
|
1247
1147
|
<EntityDetailPanel
|
|
1248
1148
|
run={run}
|
|
1249
|
-
hooks={hooks}
|
|
1250
1149
|
onStreamClick={onStreamClick}
|
|
1251
1150
|
spanDetailData={spanDetailData ?? null}
|
|
1252
1151
|
spanDetailError={spanDetailError}
|
|
@@ -1256,6 +1155,7 @@ export const WorkflowTraceViewer = ({
|
|
|
1256
1155
|
onLoadEventData={onLoadEventData}
|
|
1257
1156
|
onResolveHook={onResolveHook}
|
|
1258
1157
|
encryptionKey={encryptionKey}
|
|
1158
|
+
onDecrypt={onDecrypt}
|
|
1259
1159
|
selectedSpan={selectedSpan}
|
|
1260
1160
|
/>
|
|
1261
1161
|
</ErrorBoundary>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
6
|
-
import type { Event,
|
|
6
|
+
import type { Event, WorkflowRun } from '@workflow/world';
|
|
7
7
|
import type { Span, SpanEvent } from '../trace-viewer/types';
|
|
8
8
|
import { shouldShowVerticalLine } from './event-colors';
|
|
9
9
|
import { calculateDuration, dateToOtelTime } from './trace-time-utils';
|
|
@@ -81,18 +81,13 @@ export const waitEventsToWaitEntity = (
|
|
|
81
81
|
/**
|
|
82
82
|
* Converts a workflow Wait to an OpenTelemetry Span
|
|
83
83
|
*/
|
|
84
|
-
export function waitToSpan(
|
|
85
|
-
events: Event[],
|
|
86
|
-
run: WorkflowRun,
|
|
87
|
-
nowTime: Date
|
|
88
|
-
): Span | null {
|
|
84
|
+
export function waitToSpan(events: Event[], maxEndTime: Date): Span | null {
|
|
89
85
|
const wait = waitEventsToWaitEntity(events);
|
|
90
86
|
if (!wait) {
|
|
91
87
|
return null;
|
|
92
88
|
}
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const endTime = wait?.completedAt ?? viewerEndTime;
|
|
89
|
+
const startTime = wait.createdAt;
|
|
90
|
+
const endTime = wait.completedAt ?? maxEndTime;
|
|
96
91
|
const start = dateToOtelTime(startTime);
|
|
97
92
|
const end = dateToOtelTime(endTime);
|
|
98
93
|
const duration = calculateDuration(startTime, endTime);
|
|
@@ -117,29 +112,92 @@ export function waitToSpan(
|
|
|
117
112
|
};
|
|
118
113
|
}
|
|
119
114
|
|
|
115
|
+
export const stepEventsToStepEntity = (
|
|
116
|
+
events: Event[]
|
|
117
|
+
): {
|
|
118
|
+
stepId: string;
|
|
119
|
+
runId: string;
|
|
120
|
+
stepName: string;
|
|
121
|
+
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
122
|
+
attempt: number;
|
|
123
|
+
createdAt: Date;
|
|
124
|
+
updatedAt: Date;
|
|
125
|
+
startedAt?: Date;
|
|
126
|
+
completedAt?: Date;
|
|
127
|
+
specVersion?: number;
|
|
128
|
+
} | null => {
|
|
129
|
+
const createdEvent = events.find(
|
|
130
|
+
(event) => event.eventType === 'step_created'
|
|
131
|
+
);
|
|
132
|
+
if (!createdEvent) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
// Walk events in order to derive status, attempt count, and timestamps.
|
|
136
|
+
// Handles both step_retrying and consecutive step_started as retry signals.
|
|
137
|
+
let status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' =
|
|
138
|
+
'pending';
|
|
139
|
+
let attempt = 0;
|
|
140
|
+
let startedAt: Date | undefined;
|
|
141
|
+
let completedAt: Date | undefined;
|
|
142
|
+
|
|
143
|
+
for (const e of events) {
|
|
144
|
+
switch (e.eventType) {
|
|
145
|
+
case 'step_started':
|
|
146
|
+
status = 'running';
|
|
147
|
+
attempt += 1;
|
|
148
|
+
if (!startedAt) startedAt = e.createdAt;
|
|
149
|
+
completedAt = undefined;
|
|
150
|
+
break;
|
|
151
|
+
case 'step_completed':
|
|
152
|
+
status = 'completed';
|
|
153
|
+
completedAt = e.createdAt;
|
|
154
|
+
break;
|
|
155
|
+
case 'step_failed':
|
|
156
|
+
status = 'failed';
|
|
157
|
+
completedAt = e.createdAt;
|
|
158
|
+
break;
|
|
159
|
+
case 'step_retrying':
|
|
160
|
+
status = 'pending';
|
|
161
|
+
completedAt = undefined;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Ensure at least attempt 1 if we never saw step_started
|
|
167
|
+
if (attempt === 0) attempt = 1;
|
|
168
|
+
|
|
169
|
+
const lastEvent = events[events.length - 1];
|
|
170
|
+
return {
|
|
171
|
+
stepId: createdEvent.correlationId,
|
|
172
|
+
runId: createdEvent.runId,
|
|
173
|
+
stepName: createdEvent.eventData?.stepName ?? '',
|
|
174
|
+
status,
|
|
175
|
+
attempt,
|
|
176
|
+
createdAt: createdEvent.createdAt,
|
|
177
|
+
updatedAt: lastEvent?.createdAt ?? createdEvent.createdAt,
|
|
178
|
+
startedAt,
|
|
179
|
+
completedAt,
|
|
180
|
+
specVersion: createdEvent.specVersion,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
120
184
|
/**
|
|
121
|
-
* Converts
|
|
185
|
+
* Converts step events to an OpenTelemetry Span
|
|
122
186
|
*/
|
|
123
|
-
export function stepToSpan(
|
|
124
|
-
step
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
): Span {
|
|
129
|
-
const now = nowTime ?? new Date();
|
|
187
|
+
export function stepToSpan(stepEvents: Event[], maxEndTime: Date): Span | null {
|
|
188
|
+
const step = stepEventsToStepEntity(stepEvents);
|
|
189
|
+
if (!step) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
130
192
|
const parsedName = parseStepName(String(step.stepName));
|
|
131
193
|
|
|
132
|
-
// Only embed identification fields — not the full object with
|
|
133
|
-
// input/output/error which may contain non-cloneable types.
|
|
134
|
-
// The detail panel fetches full data separately via spanDetailData.
|
|
135
|
-
const { input: _i, output: _o, error: _e, ...stepIdentity } = step;
|
|
136
194
|
const attributes = {
|
|
137
195
|
resource: 'step' as const,
|
|
138
|
-
data:
|
|
196
|
+
data: step,
|
|
139
197
|
};
|
|
140
198
|
|
|
141
199
|
const resource = 'step';
|
|
142
|
-
const endTime = new Date(step.completedAt ?? maxEndTime
|
|
200
|
+
const endTime = new Date(step.completedAt ?? maxEndTime);
|
|
143
201
|
|
|
144
202
|
// Include ALL correlated events on the span so the sidebar detail view
|
|
145
203
|
// can display them. The timeline uses the `showVerticalLine` flag to
|
|
@@ -187,6 +245,7 @@ export const hookEventsToHookEntity = (
|
|
|
187
245
|
): {
|
|
188
246
|
hookId: string;
|
|
189
247
|
runId: string;
|
|
248
|
+
token?: string;
|
|
190
249
|
createdAt: Date;
|
|
191
250
|
receivedCount: number;
|
|
192
251
|
lastReceivedAt?: Date;
|
|
@@ -208,6 +267,7 @@ export const hookEventsToHookEntity = (
|
|
|
208
267
|
return {
|
|
209
268
|
hookId: createdEvent.correlationId,
|
|
210
269
|
runId: createdEvent.runId,
|
|
270
|
+
token: createdEvent.eventData?.token,
|
|
211
271
|
createdAt: createdEvent.createdAt,
|
|
212
272
|
receivedCount: receivedEvents.length,
|
|
213
273
|
lastReceivedAt: lastReceivedEvent?.createdAt || undefined,
|
|
@@ -218,11 +278,7 @@ export const hookEventsToHookEntity = (
|
|
|
218
278
|
/**
|
|
219
279
|
* Converts a workflow Hook to an OpenTelemetry Span
|
|
220
280
|
*/
|
|
221
|
-
export function hookToSpan(
|
|
222
|
-
hookEvents: Event[],
|
|
223
|
-
run: WorkflowRun,
|
|
224
|
-
nowTime: Date
|
|
225
|
-
): Span | null {
|
|
281
|
+
export function hookToSpan(hookEvents: Event[], maxEndTime: Date): Span | null {
|
|
226
282
|
const hook = hookEventsToHookEntity(hookEvents);
|
|
227
283
|
if (!hook) {
|
|
228
284
|
return null;
|
|
@@ -231,10 +287,7 @@ export function hookToSpan(
|
|
|
231
287
|
// Convert hook-related events to span events
|
|
232
288
|
const events = convertEventsToSpanEvents(hookEvents, false);
|
|
233
289
|
|
|
234
|
-
|
|
235
|
-
// it's clickable even if there is no
|
|
236
|
-
const viewerEndTime = new Date(run.completedAt || nowTime) ?? nowTime;
|
|
237
|
-
const endTime = hook.disposedAt || viewerEndTime;
|
|
290
|
+
const endTime = hook.disposedAt || maxEndTime;
|
|
238
291
|
|
|
239
292
|
return {
|
|
240
293
|
spanId: String(hook.hookId),
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
|
10
10
|
export * from './components';
|
|
11
11
|
export {
|
|
12
12
|
hookEventsToHookEntity,
|
|
13
|
+
stepEventsToStepEntity,
|
|
13
14
|
waitEventsToWaitEntity,
|
|
14
15
|
} from './components/workflow-traces/trace-span-construction';
|
|
15
16
|
export type { EventAnalysis } from './lib/event-analysis';
|
|
@@ -20,6 +21,18 @@ export {
|
|
|
20
21
|
isTerminalStatus,
|
|
21
22
|
shouldShowReenqueueButton,
|
|
22
23
|
} from './lib/event-analysis';
|
|
24
|
+
export type {
|
|
25
|
+
MaterializedEntities,
|
|
26
|
+
MaterializedHook,
|
|
27
|
+
MaterializedStep,
|
|
28
|
+
MaterializedWait,
|
|
29
|
+
} from './lib/event-materialization';
|
|
30
|
+
export {
|
|
31
|
+
materializeAll,
|
|
32
|
+
materializeHooks,
|
|
33
|
+
materializeSteps,
|
|
34
|
+
materializeWaits,
|
|
35
|
+
} from './lib/event-materialization';
|
|
23
36
|
export type { Revivers, StreamRef } from './lib/hydration';
|
|
24
37
|
export {
|
|
25
38
|
CLASS_INSTANCE_REF_TYPE,
|