@workflow/web-shared 4.1.0-beta.52 → 4.1.0-beta.54

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.
Files changed (145) hide show
  1. package/dist/components/error-boundary.d.ts +15 -20
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +17 -31
  4. package/dist/components/error-boundary.js.map +1 -1
  5. package/dist/components/event-list-view.d.ts +7 -6
  6. package/dist/components/event-list-view.d.ts.map +1 -1
  7. package/dist/components/event-list-view.js +492 -109
  8. package/dist/components/event-list-view.js.map +1 -1
  9. package/dist/components/index.d.ts +1 -0
  10. package/dist/components/index.d.ts.map +1 -1
  11. package/dist/components/index.js +1 -0
  12. package/dist/components/index.js.map +1 -1
  13. package/dist/components/run-trace-view.d.ts +2 -1
  14. package/dist/components/run-trace-view.d.ts.map +1 -1
  15. package/dist/components/run-trace-view.js +2 -2
  16. package/dist/components/run-trace-view.js.map +1 -1
  17. package/dist/components/sidebar/attribute-panel.d.ts +2 -1
  18. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  19. package/dist/components/sidebar/attribute-panel.js +53 -142
  20. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  21. package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
  22. package/dist/components/sidebar/conversation-view.js +3 -17
  23. package/dist/components/sidebar/conversation-view.js.map +1 -1
  24. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  25. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  26. package/dist/components/sidebar/entity-detail-panel.js +63 -10
  27. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  28. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  29. package/dist/components/sidebar/events-list.js +4 -8
  30. package/dist/components/sidebar/events-list.js.map +1 -1
  31. package/dist/components/sidebar/resolve-hook-modal.d.ts +3 -0
  32. package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
  33. package/dist/components/sidebar/resolve-hook-modal.js +152 -3
  34. package/dist/components/sidebar/resolve-hook-modal.js.map +1 -1
  35. package/dist/components/stream-viewer.d.ts +7 -5
  36. package/dist/components/stream-viewer.d.ts.map +1 -1
  37. package/dist/components/stream-viewer.js +54 -22
  38. package/dist/components/stream-viewer.js.map +1 -1
  39. package/dist/components/trace-viewer/components/markers.d.ts +2 -1
  40. package/dist/components/trace-viewer/components/markers.d.ts.map +1 -1
  41. package/dist/components/trace-viewer/components/markers.js +59 -20
  42. package/dist/components/trace-viewer/components/markers.js.map +1 -1
  43. package/dist/components/trace-viewer/components/node.d.ts +5 -1
  44. package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
  45. package/dist/components/trace-viewer/components/node.js +250 -68
  46. package/dist/components/trace-viewer/components/node.js.map +1 -1
  47. package/dist/components/trace-viewer/components/span-content.d.ts +19 -0
  48. package/dist/components/trace-viewer/components/span-content.d.ts.map +1 -0
  49. package/dist/components/trace-viewer/components/span-content.js +137 -0
  50. package/dist/components/trace-viewer/components/span-content.js.map +1 -0
  51. package/dist/components/trace-viewer/components/span-detail-panel.d.ts.map +1 -1
  52. package/dist/components/trace-viewer/components/span-detail-panel.js +3 -2
  53. package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -1
  54. package/dist/components/trace-viewer/components/span-segments.d.ts +50 -0
  55. package/dist/components/trace-viewer/components/span-segments.d.ts.map +1 -0
  56. package/dist/components/trace-viewer/components/span-segments.js +392 -0
  57. package/dist/components/trace-viewer/components/span-segments.js.map +1 -0
  58. package/dist/components/trace-viewer/components/span-strategies.d.ts +46 -0
  59. package/dist/components/trace-viewer/components/span-strategies.d.ts.map +1 -0
  60. package/dist/components/trace-viewer/components/span-strategies.js +108 -0
  61. package/dist/components/trace-viewer/components/span-strategies.js.map +1 -0
  62. package/dist/components/trace-viewer/context.d.ts +7 -6
  63. package/dist/components/trace-viewer/context.d.ts.map +1 -1
  64. package/dist/components/trace-viewer/context.js +47 -18
  65. package/dist/components/trace-viewer/context.js.map +1 -1
  66. package/dist/components/trace-viewer/trace-viewer.d.ts +5 -1
  67. package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
  68. package/dist/components/trace-viewer/trace-viewer.js +87 -11
  69. package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
  70. package/dist/components/trace-viewer/trace-viewer.module.css +179 -6
  71. package/dist/components/trace-viewer/util/timing.d.ts +5 -0
  72. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  73. package/dist/components/trace-viewer/util/timing.js +12 -0
  74. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  75. package/dist/components/trace-viewer/util/use-streaming-spans.d.ts +1 -1
  76. package/dist/components/trace-viewer/util/use-streaming-spans.d.ts.map +1 -1
  77. package/dist/components/trace-viewer/util/use-streaming-spans.js +29 -17
  78. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -1
  79. package/dist/components/trace-viewer/worker.js +3 -1
  80. package/dist/components/trace-viewer/worker.js.map +1 -1
  81. package/dist/components/ui/alert.js +3 -3
  82. package/dist/components/ui/alert.js.map +1 -1
  83. package/dist/components/ui/card.d.ts.map +1 -1
  84. package/dist/components/ui/card.js +2 -2
  85. package/dist/components/ui/card.js.map +1 -1
  86. package/dist/components/ui/data-inspector.d.ts +17 -0
  87. package/dist/components/ui/data-inspector.d.ts.map +1 -0
  88. package/dist/components/ui/data-inspector.js +184 -0
  89. package/dist/components/ui/data-inspector.js.map +1 -0
  90. package/dist/components/ui/error-card.d.ts.map +1 -1
  91. package/dist/components/ui/error-card.js +4 -1
  92. package/dist/components/ui/error-card.js.map +1 -1
  93. package/dist/components/ui/inspector-theme.d.ts +39 -24
  94. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  95. package/dist/components/ui/inspector-theme.js +90 -38
  96. package/dist/components/ui/inspector-theme.js.map +1 -1
  97. package/dist/components/ui/skeleton.d.ts +1 -1
  98. package/dist/components/ui/skeleton.d.ts.map +1 -1
  99. package/dist/components/ui/skeleton.js +2 -2
  100. package/dist/components/ui/skeleton.js.map +1 -1
  101. package/dist/components/workflow-trace-view.d.ts +3 -1
  102. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  103. package/dist/components/workflow-trace-view.js +435 -21
  104. package/dist/components/workflow-trace-view.js.map +1 -1
  105. package/dist/components/workflow-traces/trace-span-construction.d.ts +1 -1
  106. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  107. package/dist/components/workflow-traces/trace-span-construction.js +2 -2
  108. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  109. package/dist/lib/hydration.d.ts.map +1 -1
  110. package/dist/lib/hydration.js +17 -3
  111. package/dist/lib/hydration.js.map +1 -1
  112. package/dist/styles.css +186 -0
  113. package/package.json +8 -7
  114. package/src/components/error-boundary.tsx +29 -40
  115. package/src/components/event-list-view.tsx +1000 -287
  116. package/src/components/index.ts +1 -0
  117. package/src/components/run-trace-view.tsx +3 -0
  118. package/src/components/sidebar/attribute-panel.tsx +58 -258
  119. package/src/components/sidebar/conversation-view.tsx +30 -27
  120. package/src/components/sidebar/entity-detail-panel.tsx +86 -20
  121. package/src/components/sidebar/events-list.tsx +4 -11
  122. package/src/components/sidebar/resolve-hook-modal.tsx +206 -47
  123. package/src/components/stream-viewer.tsx +138 -61
  124. package/src/components/trace-viewer/components/markers.tsx +69 -21
  125. package/src/components/trace-viewer/components/node.tsx +346 -100
  126. package/src/components/trace-viewer/components/span-content.tsx +247 -0
  127. package/src/components/trace-viewer/components/span-detail-panel.tsx +7 -2
  128. package/src/components/trace-viewer/components/span-segments.ts +516 -0
  129. package/src/components/trace-viewer/components/span-strategies.ts +205 -0
  130. package/src/components/trace-viewer/context.tsx +92 -40
  131. package/src/components/trace-viewer/trace-viewer.module.css +179 -6
  132. package/src/components/trace-viewer/trace-viewer.tsx +115 -11
  133. package/src/components/trace-viewer/util/timing.ts +13 -0
  134. package/src/components/trace-viewer/util/use-streaming-spans.ts +28 -17
  135. package/src/components/trace-viewer/worker.ts +4 -1
  136. package/src/components/ui/alert.tsx +3 -3
  137. package/src/components/ui/card.tsx +3 -5
  138. package/src/components/ui/data-inspector.tsx +318 -0
  139. package/src/components/ui/error-card.tsx +17 -6
  140. package/src/components/ui/inspector-theme.ts +127 -39
  141. package/src/components/ui/skeleton.tsx +3 -1
  142. package/src/components/workflow-trace-view.tsx +625 -26
  143. package/src/components/workflow-traces/trace-span-construction.ts +3 -2
  144. package/src/lib/hydration.ts +17 -8
  145. package/src/styles.css +186 -0
@@ -0,0 +1,516 @@
1
+ import type { SpanNode, SpanNodeEvent } from '../types';
2
+ import type { ResourceType } from './span-strategies';
3
+
4
+ // ──────────────────────────────────────────────────────────────────────────
5
+ // Segment types
6
+ // ──────────────────────────────────────────────────────────────────────────
7
+
8
+ /**
9
+ * Visual status of a segment within a span bar.
10
+ *
11
+ * - queued: Waiting to start (hatched pattern)
12
+ * - running: Actively executing (solid blue)
13
+ * - failed: Attempt that ended in failure (soft red)
14
+ * - retrying: Waiting between retry attempts (hatched pattern)
15
+ * - succeeded: Final successful attempt (soft green)
16
+ * - waiting: Passive waiting, e.g. hook waiting for payload (hatched pattern)
17
+ * - sleeping: Sleep in progress (soft amber)
18
+ * - received: Hook received a payload (soft blue)
19
+ */
20
+ export type SegmentStatus =
21
+ | 'queued'
22
+ | 'running'
23
+ | 'failed'
24
+ | 'retrying'
25
+ | 'succeeded'
26
+ | 'waiting'
27
+ | 'sleeping'
28
+ | 'received';
29
+
30
+ export interface Segment {
31
+ /** Fraction of span width where this segment starts (0–1) */
32
+ startFraction: number;
33
+ /** Fraction of span width where this segment ends (0–1) */
34
+ endFraction: number;
35
+ /** Visual status controlling color/pattern */
36
+ status: SegmentStatus;
37
+ }
38
+
39
+ /**
40
+ * A boundary between two segments, representing an event that caused
41
+ * a transition. Rendered as a hoverable divider with a tooltip.
42
+ */
43
+ export interface SegmentBoundary {
44
+ /** Fractional position along the span (0–1) */
45
+ fraction: number;
46
+ /** Display label for the tooltip (e.g. "step_started") */
47
+ label: string;
48
+ /** Formatted timestamp relative to span start */
49
+ offsetMs: number;
50
+ }
51
+
52
+ // ──────────────────────────────────────────────────────────────────────────
53
+ // CSS class mapping (segment status → module CSS class name)
54
+ // ──────────────────────────────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Maps a SegmentStatus to the CSS module class name to use.
58
+ * These class names must exist in trace-viewer.module.css.
59
+ */
60
+ export const SEGMENT_CLASS_MAP: Record<SegmentStatus, string> = {
61
+ queued: 'segQueued',
62
+ running: 'segRunning',
63
+ failed: 'segFailed',
64
+ retrying: 'segRetrying',
65
+ succeeded: 'segSucceeded',
66
+ waiting: 'segWaiting',
67
+ sleeping: 'segSleeping',
68
+ received: 'segReceived',
69
+ };
70
+
71
+ // ──────────────────────────────────────────────────────────────────────────
72
+ // Helpers
73
+ // ──────────────────────────────────────────────────────────────────────────
74
+
75
+ function clampFraction(value: number): number {
76
+ return Math.max(0, Math.min(1, value));
77
+ }
78
+
79
+ function timeToFraction(
80
+ time: number,
81
+ spanStart: number,
82
+ spanDuration: number
83
+ ): number {
84
+ if (spanDuration <= 0) return 0;
85
+ return clampFraction((time - spanStart) / spanDuration);
86
+ }
87
+
88
+ /**
89
+ * Sort events by timestamp ascending.
90
+ */
91
+ function sortedEvents(events: SpanNodeEvent[]): SpanNodeEvent[] {
92
+ return [...events].sort((a, b) => a.timestamp - b.timestamp);
93
+ }
94
+
95
+ // ──────────────────────────────────────────────────────────────────────────
96
+ // Step segments
97
+ // ──────────────────────────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Compute segments for a step span.
101
+ *
102
+ * Timeline: [queued] → [attempt₁ (fail)] → [retry wait] → [attempt₂ (fail)] → ... → [attemptₙ (success)]
103
+ *
104
+ * Events used: step_started, step_retrying, step_failed, step_completed
105
+ * The final segment is 'succeeded' if no trailing step_retrying/step_failed.
106
+ */
107
+ function computeStepSegments(node: SpanNode): Segment[] {
108
+ const events = node.events ? sortedEvents(node.events) : [];
109
+ const { startTime, duration } = node;
110
+ const segments: Segment[] = [];
111
+
112
+ if (duration <= 0) return segments;
113
+
114
+ // Build a timeline of event boundaries
115
+ interface EventMark {
116
+ time: number;
117
+ type: string;
118
+ }
119
+ const marks: EventMark[] = events
120
+ .filter((e) =>
121
+ [
122
+ 'step_started',
123
+ 'step_retrying',
124
+ 'step_failed',
125
+ 'step_completed',
126
+ ].includes(e.event.name)
127
+ )
128
+ .map((e) => ({ time: e.timestamp, type: e.event.name }));
129
+
130
+ if (marks.length === 0) {
131
+ // No events — show entire span as running
132
+ segments.push({ startFraction: 0, endFraction: 1, status: 'running' });
133
+ return segments;
134
+ }
135
+
136
+ // Queued period: from span start to first event
137
+ const firstMark = marks[0];
138
+ const firstFraction = timeToFraction(firstMark.time, startTime, duration);
139
+ if (firstFraction > 0.001) {
140
+ segments.push({
141
+ startFraction: 0,
142
+ endFraction: firstFraction,
143
+ status: 'queued',
144
+ });
145
+ }
146
+
147
+ // Walk through events to build attempt/retry segments
148
+ for (let i = 0; i < marks.length; i++) {
149
+ const mark = marks[i];
150
+ const markFraction = timeToFraction(mark.time, startTime, duration);
151
+ const nextMark = marks[i + 1];
152
+ const nextFraction = nextMark
153
+ ? timeToFraction(nextMark.time, startTime, duration)
154
+ : 1;
155
+
156
+ if (mark.type === 'step_started') {
157
+ // This is the start of an execution attempt.
158
+ // It runs until the next event (step_retrying or step_failed) or end of span.
159
+ const isLastMark = i === marks.length - 1;
160
+ if (isLastMark) {
161
+ // Last event is step_started — this attempt is either still running or succeeded
162
+ segments.push({
163
+ startFraction: markFraction,
164
+ endFraction: 1,
165
+ status: 'succeeded',
166
+ });
167
+ } else {
168
+ // Attempt runs until the next event
169
+ const nextType = nextMark.type;
170
+ const attemptStatus: SegmentStatus =
171
+ nextType === 'step_retrying' || nextType === 'step_failed'
172
+ ? 'failed'
173
+ : nextType === 'step_completed'
174
+ ? 'succeeded'
175
+ : 'running';
176
+ segments.push({
177
+ startFraction: markFraction,
178
+ endFraction: nextFraction,
179
+ status: attemptStatus,
180
+ });
181
+ }
182
+ } else if (mark.type === 'step_retrying') {
183
+ // Retry wait: from this event to the next step_started (or end)
184
+ segments.push({
185
+ startFraction: markFraction,
186
+ endFraction: nextFraction,
187
+ status: 'retrying',
188
+ });
189
+ } else if (mark.type === 'step_failed') {
190
+ // Terminal failure: from this event to the end
191
+ if (markFraction < 0.999) {
192
+ segments.push({
193
+ startFraction: markFraction,
194
+ endFraction: 1,
195
+ status: 'failed',
196
+ });
197
+ }
198
+ } else if (mark.type === 'step_completed') {
199
+ // Terminal success: do nothing here since the preceding step_started
200
+ // segment already terminates at this marker.
201
+ }
202
+ }
203
+
204
+ return segments;
205
+ }
206
+
207
+ // ──────────────────────────────────────────────────────────────────────────
208
+ // Hook segments
209
+ // ──────────────────────────────────────────────────────────────────────────
210
+
211
+ /**
212
+ * Compute segments for a hook span.
213
+ *
214
+ * Timeline: [waiting] → [received] → [disposed]
215
+ */
216
+ function computeHookSegments(node: SpanNode): Segment[] {
217
+ const events = node.events ? sortedEvents(node.events) : [];
218
+ const { startTime, duration } = node;
219
+ const segments: Segment[] = [];
220
+
221
+ if (duration <= 0) return segments;
222
+
223
+ const receivedEvent = events.find((e) => e.event.name === 'hook_received');
224
+ const disposedEvent = events.find((e) => e.event.name === 'hook_disposed');
225
+
226
+ if (!receivedEvent && !disposedEvent) {
227
+ // Still waiting
228
+ segments.push({ startFraction: 0, endFraction: 1, status: 'waiting' });
229
+ return segments;
230
+ }
231
+
232
+ const receivedFraction = receivedEvent
233
+ ? timeToFraction(receivedEvent.timestamp, startTime, duration)
234
+ : null;
235
+ const disposedFraction = disposedEvent
236
+ ? timeToFraction(disposedEvent.timestamp, startTime, duration)
237
+ : null;
238
+
239
+ // Waiting period (before received)
240
+ if (receivedFraction !== null && receivedFraction > 0.001) {
241
+ segments.push({
242
+ startFraction: 0,
243
+ endFraction: receivedFraction,
244
+ status: 'waiting',
245
+ });
246
+ } else if (receivedFraction === null && disposedFraction !== null) {
247
+ // Disposed without receiving — waiting the whole time
248
+ segments.push({
249
+ startFraction: 0,
250
+ endFraction: disposedFraction,
251
+ status: 'waiting',
252
+ });
253
+ }
254
+
255
+ // Received period
256
+ if (receivedFraction !== null) {
257
+ const end = disposedFraction ?? 1;
258
+ segments.push({
259
+ startFraction: receivedFraction,
260
+ endFraction: end,
261
+ status: 'received',
262
+ });
263
+ }
264
+
265
+ // Post-disposed (if there's remaining span after disposal)
266
+ if (disposedFraction !== null && disposedFraction < 0.999) {
267
+ segments.push({
268
+ startFraction: disposedFraction,
269
+ endFraction: 1,
270
+ status: 'succeeded',
271
+ });
272
+ }
273
+
274
+ return segments;
275
+ }
276
+
277
+ // ──────────────────────────────────────────────────────────────────────────
278
+ // Sleep segments
279
+ // ──────────────────────────────────────────────────────────────────────────
280
+
281
+ /**
282
+ * Compute segments for a sleep span.
283
+ *
284
+ * Timeline: [sleeping] → [completed]
285
+ */
286
+ function computeSleepSegments(node: SpanNode): Segment[] {
287
+ const events = node.events ? sortedEvents(node.events) : [];
288
+ const { startTime, duration } = node;
289
+ const segments: Segment[] = [];
290
+
291
+ if (duration <= 0) return segments;
292
+
293
+ const completedEvent = events.find((e) => e.event.name === 'wait_completed');
294
+
295
+ if (!completedEvent) {
296
+ // Still sleeping
297
+ segments.push({ startFraction: 0, endFraction: 1, status: 'sleeping' });
298
+ return segments;
299
+ }
300
+
301
+ const completedFraction = timeToFraction(
302
+ completedEvent.timestamp,
303
+ startTime,
304
+ duration
305
+ );
306
+
307
+ if (completedFraction > 0.001) {
308
+ segments.push({
309
+ startFraction: 0,
310
+ endFraction: completedFraction,
311
+ status: 'sleeping',
312
+ });
313
+ }
314
+
315
+ segments.push({
316
+ startFraction: completedFraction,
317
+ endFraction: 1,
318
+ status: 'succeeded',
319
+ });
320
+
321
+ return segments;
322
+ }
323
+
324
+ // ──────────────────────────────────────────────────────────────────────────
325
+ // Run segments
326
+ // ──────────────────────────────────────────────────────────────────────────
327
+
328
+ /**
329
+ * Compute segments for a run span.
330
+ *
331
+ * Timeline: [queued] → [running] → [completed/failed]
332
+ *
333
+ * The queued period is derived from activeStartTime (run.startedAt).
334
+ * A synthetic run_started event is injected at that timestamp so
335
+ * the boundary is hoverable in the trace viewer.
336
+ */
337
+ function computeRunSegments(node: SpanNode): Segment[] {
338
+ const events = node.events ? sortedEvents(node.events) : [];
339
+ const { startTime, duration, activeStartTime } = node;
340
+ const segments: Segment[] = [];
341
+
342
+ if (duration <= 0) return segments;
343
+
344
+ const failedEvent = events.find((e) => e.event.name === 'run_failed');
345
+ const completedEvent = events.find((e) => e.event.name === 'run_completed');
346
+
347
+ // Queued period (from span start to activeStartTime)
348
+ let cursor = 0;
349
+ if (activeStartTime && activeStartTime > startTime) {
350
+ const queuedFraction = timeToFraction(activeStartTime, startTime, duration);
351
+ if (queuedFraction > 0.001) {
352
+ segments.push({
353
+ startFraction: 0,
354
+ endFraction: queuedFraction,
355
+ status: 'queued',
356
+ });
357
+ cursor = queuedFraction;
358
+ }
359
+ }
360
+
361
+ if (failedEvent) {
362
+ const failedFraction = timeToFraction(
363
+ failedEvent.timestamp,
364
+ startTime,
365
+ duration
366
+ );
367
+ // Running until failure
368
+ if (failedFraction > cursor + 0.001) {
369
+ segments.push({
370
+ startFraction: cursor,
371
+ endFraction: failedFraction,
372
+ status: 'running',
373
+ });
374
+ }
375
+ segments.push({
376
+ startFraction: failedFraction,
377
+ endFraction: 1,
378
+ status: 'failed',
379
+ });
380
+ } else if (completedEvent) {
381
+ const completedFraction = timeToFraction(
382
+ completedEvent.timestamp,
383
+ startTime,
384
+ duration
385
+ );
386
+ if (completedFraction > cursor + 0.001) {
387
+ segments.push({
388
+ startFraction: cursor,
389
+ endFraction: completedFraction,
390
+ status: 'running',
391
+ });
392
+ }
393
+ segments.push({
394
+ startFraction: completedFraction,
395
+ endFraction: 1,
396
+ status: 'succeeded',
397
+ });
398
+ } else {
399
+ // Running to completion
400
+ segments.push({
401
+ startFraction: cursor,
402
+ endFraction: 1,
403
+ status: 'running',
404
+ });
405
+ }
406
+
407
+ return segments;
408
+ }
409
+
410
+ // ──────────────────────────────────────────────────────────────────────────
411
+ // Dispatcher
412
+ // ──────────────────────────────────────────────────────────────────────────
413
+
414
+ export interface SegmentResult {
415
+ segments: Segment[];
416
+ boundaries: SegmentBoundary[];
417
+ }
418
+
419
+ // ──────────────────────────────────────────────────────────────────────────
420
+ // Boundary computation
421
+ // ──────────────────────────────────────────────────────────────────────────
422
+
423
+ /** Event names to human-readable labels */
424
+ const EVENT_LABELS: Record<string, string> = {
425
+ step_started: 'Started',
426
+ step_retrying: 'Retrying',
427
+ step_failed: 'Failed',
428
+ hook_created: 'Created',
429
+ hook_received: 'Received',
430
+ hook_disposed: 'Resolved',
431
+ wait_created: 'Sleep started',
432
+ wait_completed: 'Sleep completed',
433
+ run_completed: 'Completed',
434
+ run_failed: 'Run failed',
435
+ step_completed: 'Completed',
436
+ };
437
+
438
+ /**
439
+ * Extract boundaries from a span's events. Each boundary sits at the
440
+ * fractional position of a relevant event within the span duration.
441
+ * Only includes events that are used as segment boundaries (not at the
442
+ * very start or very end of the span, to avoid edge clutter).
443
+ */
444
+ function computeBoundaries(
445
+ node: SpanNode,
446
+ relevantEventNames: string[]
447
+ ): SegmentBoundary[] {
448
+ const events = node.events ? sortedEvents(node.events) : [];
449
+ const { startTime, duration } = node;
450
+
451
+ if (duration <= 0) return [];
452
+
453
+ const boundaries: SegmentBoundary[] = [];
454
+ for (const e of events) {
455
+ if (!relevantEventNames.includes(e.event.name)) continue;
456
+
457
+ const fraction = timeToFraction(e.timestamp, startTime, duration);
458
+ // Skip boundaries at the very edges (< 1% or > 99%) to avoid clutter
459
+ if (fraction < 0.01 || fraction > 0.99) continue;
460
+
461
+ boundaries.push({
462
+ fraction,
463
+ label: EVENT_LABELS[e.event.name] ?? e.event.name,
464
+ offsetMs: e.timestamp - startTime,
465
+ });
466
+ }
467
+
468
+ return boundaries;
469
+ }
470
+
471
+ // ──────────────────────────────────────────────────────────────────────────
472
+ // Dispatcher
473
+ // ──────────────────────────────────────────────────────────────────────────
474
+
475
+ /**
476
+ * Compute the event-based segments and boundaries for a span based on its resource type.
477
+ * Returns empty arrays for 'default' (generic OTEL) spans.
478
+ */
479
+ export function computeSegments(
480
+ resourceType: ResourceType,
481
+ node: SpanNode
482
+ ): SegmentResult {
483
+ switch (resourceType) {
484
+ case 'step':
485
+ return {
486
+ segments: computeStepSegments(node),
487
+ boundaries: computeBoundaries(node, [
488
+ 'step_started',
489
+ 'step_retrying',
490
+ 'step_failed',
491
+ 'step_completed',
492
+ ]),
493
+ };
494
+ case 'hook':
495
+ return {
496
+ segments: computeHookSegments(node),
497
+ boundaries: computeBoundaries(node, [
498
+ 'hook_created',
499
+ 'hook_received',
500
+ 'hook_disposed',
501
+ ]),
502
+ };
503
+ case 'sleep':
504
+ return {
505
+ segments: computeSleepSegments(node),
506
+ boundaries: computeBoundaries(node, ['wait_created', 'wait_completed']),
507
+ };
508
+ case 'run':
509
+ return {
510
+ segments: computeRunSegments(node),
511
+ boundaries: computeBoundaries(node, ['run_completed', 'run_failed']),
512
+ };
513
+ default:
514
+ return { segments: [], boundaries: [] };
515
+ }
516
+ }