@workflow/web-shared 5.0.0-beta.41 → 5.0.0-beta.42
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/event-list-view.d.ts +10 -2
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +52 -26
- package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
- package/dist/components/sidebar/attributes-block.js +3 -6
- package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
- package/dist/components/sidebar/conversation-view.js +19 -36
- package/dist/components/sidebar/entity-detail-panel.d.ts +5 -0
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +2 -2
- package/dist/components/sidebar/events-list.d.ts +7 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +6 -5
- package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
- package/dist/components/sidebar/resolve-hook-modal.js +4 -145
- package/dist/components/sidebar/sidebar-data-context.d.ts +6 -0
- 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/stream-viewer-skeleton.js +6 -6
- package/dist/components/stream-viewer.js +2 -2
- package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/detail-panel.js +2 -1
- package/dist/components/trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/timeline.js +13 -22
- package/dist/components/trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/trace-viewer-skeleton.js +58 -18
- package/dist/components/trace-viewer/icons.js +5 -5
- package/dist/components/trace-viewer/utils.d.ts +6 -10
- package/dist/components/trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/trace-viewer/utils.js +14 -24
- package/dist/components/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer.js +12 -4
- package/dist/components/ui/context-card.d.ts.map +1 -1
- package/dist/components/ui/context-card.js +9 -21
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +8 -35
- package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -0
- package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -0
- package/dist/components/ui/duplicate-event-tooltip.js +19 -0
- package/dist/components/ui/menu-dropdown.d.ts.map +1 -1
- package/dist/components/ui/menu-dropdown.js +3 -19
- package/dist/components/ui/skeleton.js +2 -2
- package/dist/components/ui/spinner.d.ts.map +1 -1
- package/dist/components/ui/spinner.js +5 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/duplicate-events.d.ts +27 -0
- package/dist/lib/duplicate-events.d.ts.map +1 -0
- package/dist/lib/duplicate-events.js +166 -0
- package/dist/lib/event-materialization.d.ts +15 -2
- package/dist/lib/event-materialization.d.ts.map +1 -1
- package/dist/lib/event-materialization.js +19 -4
- package/dist/lib/trace-builder.d.ts +16 -1
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +23 -4
- package/package.json +4 -4
- package/src/components/event-list-view.tsx +70 -32
- package/src/components/sidebar/attributes-block.tsx +4 -11
- package/src/components/sidebar/conversation-view.tsx +25 -79
- package/src/components/sidebar/entity-detail-panel.tsx +6 -0
- package/src/components/sidebar/events-list.tsx +21 -3
- package/src/components/sidebar/resolve-hook-modal.tsx +23 -186
- package/src/components/sidebar/sidebar-data-context.tsx +6 -0
- package/src/components/stream-viewer-skeleton.tsx +5 -5
- package/src/components/stream-viewer.tsx +1 -1
- package/src/components/trace-viewer/components/detail-panel.tsx +1 -0
- package/src/components/trace-viewer/components/timeline.tsx +21 -39
- package/src/components/trace-viewer/components/trace-viewer-skeleton.tsx +63 -35
- package/src/components/trace-viewer/icons.tsx +4 -4
- package/src/components/trace-viewer/utils.test.ts +5 -7
- package/src/components/trace-viewer/utils.ts +17 -31
- package/src/components/trace-viewer.tsx +15 -3
- package/src/components/ui/context-card.tsx +16 -26
- package/src/components/ui/data-inspector.tsx +20 -65
- package/src/components/ui/duplicate-event-tooltip.tsx +44 -0
- package/src/components/ui/menu-dropdown.tsx +8 -22
- package/src/components/ui/skeleton.tsx +2 -2
- package/src/components/ui/spinner.tsx +4 -8
- package/src/index.ts +4 -0
- package/src/lib/duplicate-events.test.ts +339 -0
- package/src/lib/duplicate-events.ts +188 -0
- package/src/lib/event-materialization.test.ts +117 -0
- package/src/lib/event-materialization.ts +23 -3
- package/src/lib/trace-builder.test.ts +74 -0
- package/src/lib/trace-builder.ts +30 -3
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
isWaitEventType,
|
|
18
18
|
type StepStatus,
|
|
19
19
|
} from '@workflow/world';
|
|
20
|
+
import { findDuplicateEventIds } from './duplicate-events';
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Materialized entity types
|
|
@@ -105,9 +106,24 @@ function getEventTimestamp(event: Event | undefined): Date | undefined {
|
|
|
105
106
|
*
|
|
106
107
|
* Handles partial event lists gracefully: a step may only have a
|
|
107
108
|
* step_created event with no completion yet.
|
|
109
|
+
*
|
|
110
|
+
* The derived status and timestamps come from the events the run acted on. A
|
|
111
|
+
* repeat of a class the log already records is read past by every replay, so
|
|
112
|
+
* a second terminal event written by a concurrent replay does not move a step
|
|
113
|
+
* off the outcome the first one recorded. Every event stays on the entity's
|
|
114
|
+
* `events` list.
|
|
115
|
+
*
|
|
116
|
+
* That reduction needs the whole log to be sound, so it only runs when
|
|
117
|
+
* `isCompleteHistory` says `events` is it. See {@link findDuplicateEventIds}.
|
|
108
118
|
*/
|
|
109
|
-
export function materializeSteps(
|
|
119
|
+
export function materializeSteps(
|
|
120
|
+
events: Event[],
|
|
121
|
+
{ isCompleteHistory = false }: { isCompleteHistory?: boolean } = {}
|
|
122
|
+
): MaterializedStep[] {
|
|
110
123
|
const groups = groupByCorrelationId(events, isStepEventType);
|
|
124
|
+
const duplicateEventIds = findDuplicateEventIds(events, {
|
|
125
|
+
isCompleteHistory,
|
|
126
|
+
});
|
|
111
127
|
const steps: MaterializedStep[] = [];
|
|
112
128
|
|
|
113
129
|
for (const [correlationId, stepEvents] of groups) {
|
|
@@ -121,6 +137,7 @@ export function materializeSteps(events: Event[]): MaterializedStep[] {
|
|
|
121
137
|
let updatedAt = getEventTimestamp(created) ?? created.createdAt;
|
|
122
138
|
|
|
123
139
|
for (const e of stepEvents) {
|
|
140
|
+
if (duplicateEventIds.has(e.eventId)) continue;
|
|
124
141
|
switch (e.eventType) {
|
|
125
142
|
case 'step_started':
|
|
126
143
|
status = 'running';
|
|
@@ -248,9 +265,12 @@ export function materializeWaits(events: Event[]): MaterializedWait[] {
|
|
|
248
265
|
* Convenience function that materializes all entity types from a flat
|
|
249
266
|
* event list.
|
|
250
267
|
*/
|
|
251
|
-
export function materializeAll(
|
|
268
|
+
export function materializeAll(
|
|
269
|
+
events: Event[],
|
|
270
|
+
options: { isCompleteHistory?: boolean } = {}
|
|
271
|
+
): MaterializedEntities {
|
|
252
272
|
return {
|
|
253
|
-
steps: materializeSteps(events),
|
|
273
|
+
steps: materializeSteps(events, options),
|
|
254
274
|
hooks: materializeHooks(events),
|
|
255
275
|
waits: materializeWaits(events),
|
|
256
276
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Event, EventType, WorkflowRun } from '@workflow/world';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { otelTimeToMs } from '../components/workflow-traces/trace-time-utils';
|
|
4
|
+
import { buildTrace } from './trace-builder';
|
|
5
|
+
|
|
6
|
+
const BASE_TIME = Date.parse('2026-01-01T00:00:00.000Z');
|
|
7
|
+
|
|
8
|
+
let nextId = 0;
|
|
9
|
+
|
|
10
|
+
function event(
|
|
11
|
+
eventType: EventType,
|
|
12
|
+
options: { correlationId?: string; at: number }
|
|
13
|
+
): Event {
|
|
14
|
+
nextId += 1;
|
|
15
|
+
return {
|
|
16
|
+
eventId: `evt_${nextId}`,
|
|
17
|
+
runId: 'run_1',
|
|
18
|
+
eventType,
|
|
19
|
+
correlationId: options.correlationId,
|
|
20
|
+
createdAt: new Date(BASE_TIME + options.at * 1000),
|
|
21
|
+
occurredAt: new Date(BASE_TIME + options.at * 1000),
|
|
22
|
+
eventData: eventType === 'step_created' ? { stepName: 'doWork' } : {},
|
|
23
|
+
} as unknown as Event;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const run = {
|
|
27
|
+
runId: 'run_1',
|
|
28
|
+
workflowName: 'demo',
|
|
29
|
+
status: 'running',
|
|
30
|
+
createdAt: new Date(BASE_TIME),
|
|
31
|
+
} as unknown as WorkflowRun;
|
|
32
|
+
|
|
33
|
+
describe('buildTrace', () => {
|
|
34
|
+
it('ends a step span on the terminal event the run acted on', () => {
|
|
35
|
+
const events = [
|
|
36
|
+
event('run_created', { at: 0 }),
|
|
37
|
+
event('run_started', { at: 0 }),
|
|
38
|
+
event('step_created', { correlationId: 'step_a', at: 1 }),
|
|
39
|
+
event('step_started', { correlationId: 'step_a', at: 1 }),
|
|
40
|
+
event('step_completed', { correlationId: 'step_a', at: 4 }),
|
|
41
|
+
// A concurrent replay commits the same outcome much later. Measuring the
|
|
42
|
+
// span against it would report a 20s step that ran for 3s.
|
|
43
|
+
event('step_completed', { correlationId: 'step_a', at: 20 }),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const trace = buildTrace(run, events, new Date(BASE_TIME + 30_000), {
|
|
47
|
+
isCompleteHistory: true,
|
|
48
|
+
});
|
|
49
|
+
const stepSpan = trace.spans.find((span) => span.resource === 'step');
|
|
50
|
+
|
|
51
|
+
expect(stepSpan).toBeDefined();
|
|
52
|
+
expect(otelTimeToMs(stepSpan?.endTime ?? [0, 0])).toBe(BASE_TIME + 4000);
|
|
53
|
+
expect(trace.knownDurationMs).toBe(4000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('keeps every event in the geometry when the log may be incomplete', () => {
|
|
57
|
+
const events = [
|
|
58
|
+
event('run_created', { at: 0 }),
|
|
59
|
+
event('run_started', { at: 0 }),
|
|
60
|
+
event('step_created', { correlationId: 'step_a', at: 1 }),
|
|
61
|
+
event('step_started', { correlationId: 'step_a', at: 1 }),
|
|
62
|
+
event('step_completed', { correlationId: 'step_a', at: 4 }),
|
|
63
|
+
event('step_completed', { correlationId: 'step_a', at: 20 }),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Without the whole log there is no telling which of the two completions
|
|
67
|
+
// the run acted on, so neither is dropped and the span covers both.
|
|
68
|
+
const trace = buildTrace(run, events, new Date(BASE_TIME + 30_000));
|
|
69
|
+
const stepSpan = trace.spans.find((span) => span.resource === 'step');
|
|
70
|
+
|
|
71
|
+
expect(otelTimeToMs(stepSpan?.endTime ?? [0, 0])).toBe(BASE_TIME + 20_000);
|
|
72
|
+
expect(trace.duplicateEventIds.size).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
package/src/lib/trace-builder.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
waitToSpan,
|
|
23
23
|
} from '../components/workflow-traces/trace-span-construction';
|
|
24
24
|
import { otelTimeToMs } from '../components/workflow-traces/trace-time-utils';
|
|
25
|
+
import { findDuplicateEventIds } from './duplicate-events';
|
|
25
26
|
import type { Span } from './trace-types';
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -188,15 +189,40 @@ export interface TraceWithMeta {
|
|
|
188
189
|
resources: { name: string; attributes: Record<string, string> }[];
|
|
189
190
|
/** Duration in ms from trace start to the latest known event. */
|
|
190
191
|
knownDurationMs: number;
|
|
192
|
+
/**
|
|
193
|
+
* The events left out of the span geometry as repeats. Empty unless the
|
|
194
|
+
* caller vouched for the log being complete. See
|
|
195
|
+
* {@link findDuplicateEventIds}.
|
|
196
|
+
*/
|
|
197
|
+
duplicateEventIds: ReadonlySet<string>;
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
export function buildTrace(
|
|
194
201
|
run: WorkflowRun,
|
|
195
202
|
events: Event[],
|
|
196
|
-
now: Date
|
|
203
|
+
now: Date,
|
|
204
|
+
/**
|
|
205
|
+
* Whether `events` is the run's whole log. Defaults to false, which builds
|
|
206
|
+
* the trace from every event: on a subset there is no way to tell a repeat
|
|
207
|
+
* from the only copy the caller was given, and dropping the wrong one moves
|
|
208
|
+
* a span. See {@link findDuplicateEventIds}.
|
|
209
|
+
*/
|
|
210
|
+
{ isCompleteHistory = false }: { isCompleteHistory?: boolean } = {}
|
|
197
211
|
): TraceWithMeta {
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
// Span geometry comes from what the run acted on. A repeat of a class the
|
|
213
|
+
// log already records is read past by every replay, and letting one through
|
|
214
|
+
// here would stretch a span to whenever a concurrent replay committed it.
|
|
215
|
+
// The event lists still show them, marked as repeats.
|
|
216
|
+
const duplicateEventIds = findDuplicateEventIds(events, {
|
|
217
|
+
isCompleteHistory,
|
|
218
|
+
});
|
|
219
|
+
const actedOnEvents =
|
|
220
|
+
duplicateEventIds.size === 0
|
|
221
|
+
? events
|
|
222
|
+
: events.filter((event) => !duplicateEventIds.has(event.eventId));
|
|
223
|
+
|
|
224
|
+
const groupedEvents = groupEventsByCorrelation(actedOnEvents);
|
|
225
|
+
const latestKnownTime = computeLatestKnownTime(actedOnEvents, run);
|
|
200
226
|
const { runSpan, spans } = buildSpans(
|
|
201
227
|
run,
|
|
202
228
|
groupedEvents,
|
|
@@ -222,5 +248,6 @@ export function buildTrace(
|
|
|
222
248
|
},
|
|
223
249
|
],
|
|
224
250
|
knownDurationMs: Math.max(0, knownDurationMs),
|
|
251
|
+
duplicateEventIds,
|
|
225
252
|
};
|
|
226
253
|
}
|