@workflow/web-shared 5.0.0-beta.46 → 5.0.0-beta.47
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.js +3 -3
- package/dist/components/trace-viewer/components/use-alt-held.d.ts +11 -0
- package/dist/components/trace-viewer/components/use-alt-held.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/use-alt-held.js +48 -0
- package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.js +4 -17
- package/dist/components/trace-viewer.d.ts +4 -1
- package/dist/components/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer.js +4 -3
- package/dist/components/ui/data-inspector.d.ts +3 -2
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +85 -8
- package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +7 -3
- package/dist/lib/duplicate-events.d.ts.map +1 -1
- package/dist/lib/duplicate-events.js +14 -25
- package/dist/lib/trace-builder.d.ts +3 -1
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +5 -5
- package/package.json +5 -5
- package/src/components/event-list-view.tsx +2 -2
- package/src/components/trace-viewer/components/use-alt-held.ts +54 -0
- package/src/components/trace-viewer/trace-viewer.tsx +3 -15
- package/src/components/trace-viewer.tsx +6 -1
- package/src/components/ui/data-inspector.tsx +105 -9
- package/src/components/workflow-traces/trace-span-construction.ts +14 -2
- package/src/lib/duplicate-events.ts +14 -26
- package/src/lib/trace-builder.test.ts +42 -1
- package/src/lib/trace-builder.ts +13 -4
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from './sidebar/sidebar-data-context';
|
|
9
9
|
import { TraceViewerSkeleton } from './trace-viewer/components/trace-viewer-skeleton';
|
|
10
10
|
import { TraceViewer as TraceViewerComponent } from './trace-viewer/trace-viewer';
|
|
11
|
+
import type { GetStepAttributes } from './workflow-traces/trace-span-construction';
|
|
11
12
|
|
|
12
13
|
const TraceViewer = ({
|
|
13
14
|
run,
|
|
@@ -17,6 +18,7 @@ const TraceViewer = ({
|
|
|
17
18
|
hasMore,
|
|
18
19
|
isLoadingMore,
|
|
19
20
|
loading = false,
|
|
21
|
+
getStepAttributes,
|
|
20
22
|
}: {
|
|
21
23
|
run: WorkflowRun;
|
|
22
24
|
events: Event[];
|
|
@@ -25,6 +27,8 @@ const TraceViewer = ({
|
|
|
25
27
|
hasMore?: boolean;
|
|
26
28
|
isLoadingMore?: boolean;
|
|
27
29
|
loading?: boolean;
|
|
30
|
+
/** Adds product-specific attributes to event-derived step span data. */
|
|
31
|
+
getStepAttributes?: GetStepAttributes;
|
|
28
32
|
}) => {
|
|
29
33
|
const trace: TraceWithMeta | undefined = useMemo(() => {
|
|
30
34
|
if (!run?.runId) {
|
|
@@ -35,9 +39,10 @@ const TraceViewer = ({
|
|
|
35
39
|
// repeats with the whole log in hand.
|
|
36
40
|
return buildTrace(run, events, new Date(), {
|
|
37
41
|
isCompleteHistory: !hasMore,
|
|
42
|
+
getStepAttributes,
|
|
38
43
|
});
|
|
39
44
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- `new Date()` is intentionally not a dep
|
|
40
|
-
}, [run, events, hasMore]);
|
|
45
|
+
}, [run, events, hasMore, getStepAttributes]);
|
|
41
46
|
|
|
42
47
|
// The sidebar shows one entity's slice of the log, so it takes the trace's
|
|
43
48
|
// answer rather than recomputing one from the slice.
|
|
@@ -344,7 +344,7 @@ function BytesDisplayValue({ display }: { display: BytesDisplay }) {
|
|
|
344
344
|
// Tree renderer
|
|
345
345
|
// ---------------------------------------------------------------------------
|
|
346
346
|
|
|
347
|
-
type Entry = [field: string | undefined, value: unknown];
|
|
347
|
+
type Entry = [field: string | undefined, value: unknown, key?: string | number];
|
|
348
348
|
|
|
349
349
|
interface NodeContext {
|
|
350
350
|
level: number;
|
|
@@ -357,10 +357,51 @@ function formatField(field: string): string {
|
|
|
357
357
|
return field === '' ? '""' : field;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
function isGenericIterable(
|
|
361
|
+
value: unknown
|
|
362
|
+
): value is object & Iterable<unknown> {
|
|
363
|
+
if (
|
|
364
|
+
value === null ||
|
|
365
|
+
(typeof value !== 'object' && typeof value !== 'function') ||
|
|
366
|
+
Array.isArray(value) ||
|
|
367
|
+
value instanceof Map ||
|
|
368
|
+
value instanceof Set
|
|
369
|
+
) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
return (
|
|
373
|
+
typeof (value as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===
|
|
374
|
+
'function'
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isEntryIterable(
|
|
379
|
+
value: object & Iterable<unknown>
|
|
380
|
+
): value is object & Iterable<unknown> & { entries(): Iterable<unknown> } {
|
|
381
|
+
return typeof (value as { entries?: unknown }).entries === 'function';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function collectEntries(
|
|
385
|
+
iterable: Iterable<unknown>,
|
|
386
|
+
asPairs: boolean
|
|
387
|
+
): Entry[] {
|
|
388
|
+
return Array.from(iterable, (item, index) => {
|
|
389
|
+
if (asPairs && Array.isArray(item) && item.length >= 2) {
|
|
390
|
+
return [String(item[0]), collapseRefs(item[1]), index];
|
|
391
|
+
}
|
|
392
|
+
return [undefined, collapseRefs(item), index];
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isSelfIterableIterator(value: object & Iterable<unknown>): boolean {
|
|
397
|
+
return Object.is(value[Symbol.iterator](), value);
|
|
398
|
+
}
|
|
399
|
+
|
|
360
400
|
/**
|
|
361
|
-
* Describe an object/array/
|
|
362
|
-
* values that should render as a primitive. `prefix` carries a class name
|
|
363
|
-
* before the opening bracket (Map/Set and named class
|
|
401
|
+
* Describe an object/array/iterable as an expandable container. Returns null
|
|
402
|
+
* for values that should render as a primitive. `prefix` carries a class name
|
|
403
|
+
* shown before the opening bracket (Map/Set, generic iterables, and named class
|
|
404
|
+
* instances).
|
|
364
405
|
*/
|
|
365
406
|
function describeContainer(
|
|
366
407
|
value: unknown
|
|
@@ -391,6 +432,25 @@ function describeContainer(
|
|
|
391
432
|
prefix: 'Set',
|
|
392
433
|
};
|
|
393
434
|
}
|
|
435
|
+
if (isGenericIterable(value)) {
|
|
436
|
+
const name = (value as { constructor?: { name?: string } }).constructor
|
|
437
|
+
?.name;
|
|
438
|
+
const prefix = name && name !== 'Object' ? name : undefined;
|
|
439
|
+
if (isEntryIterable(value)) {
|
|
440
|
+
return {
|
|
441
|
+
entries: collectEntries(value.entries(), true),
|
|
442
|
+
open: '{',
|
|
443
|
+
close: '}',
|
|
444
|
+
prefix,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
entries: collectEntries(value, false),
|
|
449
|
+
open: '[',
|
|
450
|
+
close: ']',
|
|
451
|
+
prefix,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
394
454
|
if (value !== null && typeof value === 'object') {
|
|
395
455
|
const name = (value as { constructor?: { name?: string } }).constructor
|
|
396
456
|
?.name;
|
|
@@ -616,9 +676,9 @@ function ExpandableContainer({
|
|
|
616
676
|
{expanded ? (
|
|
617
677
|
// biome-ignore lint/a11y/useSemanticElements: ARIA tree group is the correct role here
|
|
618
678
|
<ul id={contentsId} className={CLS.childFields} role="group">
|
|
619
|
-
{entries.map(([childField, childValue], index) => (
|
|
679
|
+
{entries.map(([childField, childValue, entryKey], index) => (
|
|
620
680
|
<DataRender
|
|
621
|
-
key={childField ?? index}
|
|
681
|
+
key={entryKey ?? childField ?? index}
|
|
622
682
|
field={childField}
|
|
623
683
|
value={childValue}
|
|
624
684
|
isLast={index === lastIndex}
|
|
@@ -820,8 +880,8 @@ function makeBytesDisplay(display: FormattedStreamChunkDisplay): unknown {
|
|
|
820
880
|
* non-expandable versions so the renderer doesn't show their internals.
|
|
821
881
|
* Only recurses into plain objects and arrays to avoid stripping class
|
|
822
882
|
* instances (Date, Error, URL, Headers, etc.) that have their own rendering.
|
|
823
|
-
* Map and Set
|
|
824
|
-
*
|
|
883
|
+
* Map and Set contents are prepared here; other iterable contents are prepared
|
|
884
|
+
* when the renderer traverses them.
|
|
825
885
|
*
|
|
826
886
|
* Exported for testing the typed-array detection path used by hydrated
|
|
827
887
|
* AI agent stream chunks (e.g. `{ delta: new Uint8Array(...) }`).
|
|
@@ -962,7 +1022,28 @@ function isSameBytesDisplay(a: BytesDisplay, b: BytesDisplay): boolean {
|
|
|
962
1022
|
);
|
|
963
1023
|
}
|
|
964
1024
|
|
|
965
|
-
function
|
|
1025
|
+
function haveSameIterableValues(
|
|
1026
|
+
a: Iterable<unknown>,
|
|
1027
|
+
b: Iterable<unknown>,
|
|
1028
|
+
seen: WeakMap<object, object>
|
|
1029
|
+
): boolean {
|
|
1030
|
+
const aIterator = a[Symbol.iterator]();
|
|
1031
|
+
const bIterator = b[Symbol.iterator]();
|
|
1032
|
+
while (true) {
|
|
1033
|
+
const aResult = aIterator.next();
|
|
1034
|
+
const bResult = bIterator.next();
|
|
1035
|
+
if (aResult.done || bResult.done) {
|
|
1036
|
+
return aResult.done === bResult.done;
|
|
1037
|
+
}
|
|
1038
|
+
if (!isDeepEqual(aResult.value, bResult.value, seen)) return false;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
export function isDeepEqual(
|
|
1043
|
+
a: unknown,
|
|
1044
|
+
b: unknown,
|
|
1045
|
+
seen = new WeakMap()
|
|
1046
|
+
): boolean {
|
|
966
1047
|
if (Object.is(a, b)) return true;
|
|
967
1048
|
|
|
968
1049
|
if (isBytesDisplay(a) || isBytesDisplay(b)) {
|
|
@@ -977,6 +1058,21 @@ function isDeepEqual(a: unknown, b: unknown, seen = new WeakMap()): boolean {
|
|
|
977
1058
|
return a.source === b.source && a.flags === b.flags;
|
|
978
1059
|
}
|
|
979
1060
|
|
|
1061
|
+
if (isGenericIterable(a) || isGenericIterable(b)) {
|
|
1062
|
+
if (!isGenericIterable(a) || !isGenericIterable(b)) return false;
|
|
1063
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
1064
|
+
if (isSelfIterableIterator(a) || isSelfIterableIterator(b)) return false;
|
|
1065
|
+
if (seen.get(a) === b) return true;
|
|
1066
|
+
seen.set(a, b);
|
|
1067
|
+
const aHasEntries = isEntryIterable(a);
|
|
1068
|
+
const bHasEntries = isEntryIterable(b);
|
|
1069
|
+
if (aHasEntries !== bHasEntries) return false;
|
|
1070
|
+
if (aHasEntries && bHasEntries) {
|
|
1071
|
+
return haveSameIterableValues(a.entries(), b.entries(), seen);
|
|
1072
|
+
}
|
|
1073
|
+
return haveSameIterableValues(a, b, seen);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
980
1076
|
if (a instanceof Map && b instanceof Map) {
|
|
981
1077
|
if (a.size !== b.size) return false;
|
|
982
1078
|
for (const [key, value] of a.entries()) {
|
|
@@ -150,6 +150,10 @@ export function waitToSpan(
|
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
export type GetStepAttributes = (
|
|
154
|
+
events: Event[]
|
|
155
|
+
) => Record<string, unknown> | undefined;
|
|
156
|
+
|
|
153
157
|
export const stepEventsToStepEntity = (
|
|
154
158
|
events: Event[]
|
|
155
159
|
): {
|
|
@@ -231,7 +235,11 @@ export const stepEventsToStepEntity = (
|
|
|
231
235
|
/**
|
|
232
236
|
* Converts step events to an OpenTelemetry Span
|
|
233
237
|
*/
|
|
234
|
-
export function stepToSpan(
|
|
238
|
+
export function stepToSpan(
|
|
239
|
+
stepEvents: Event[],
|
|
240
|
+
maxEndTime: Date,
|
|
241
|
+
getStepAttributes?: GetStepAttributes
|
|
242
|
+
): Span | null {
|
|
235
243
|
const step = stepEventsToStepEntity(stepEvents);
|
|
236
244
|
if (!step) {
|
|
237
245
|
return null;
|
|
@@ -242,7 +250,11 @@ export function stepToSpan(stepEvents: Event[], maxEndTime: Date): Span | null {
|
|
|
242
250
|
|
|
243
251
|
const attributes = {
|
|
244
252
|
resource: 'step' as const,
|
|
245
|
-
data:
|
|
253
|
+
data: {
|
|
254
|
+
...getStepAttributes?.(stepEvents),
|
|
255
|
+
// Canonical event-derived fields cannot be overridden by extensions.
|
|
256
|
+
...step,
|
|
257
|
+
},
|
|
246
258
|
};
|
|
247
259
|
|
|
248
260
|
const resource = 'step';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
classifyEntityEvent,
|
|
2
3
|
type EntityEventClass,
|
|
3
4
|
type Event,
|
|
4
|
-
entityEventClass,
|
|
5
5
|
isSlotEventId,
|
|
6
|
+
TERMINAL_EVENT_CLASSES,
|
|
6
7
|
} from '@workflow/world';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -10,13 +11,13 @@ import {
|
|
|
10
11
|
*
|
|
11
12
|
* Concurrent replays of one run write to a shared log, so a replay working
|
|
12
13
|
* from a stale prefix can commit a second `step_created` / `step_started` /
|
|
13
|
-
* `wait_created` for an entity the log already records one of.
|
|
14
|
-
* reads the first event of that class at the same position, so a
|
|
15
|
-
* cannot change what the workflow observes.
|
|
14
|
+
* `wait_created` / `attr_set` for an entity the log already records one of.
|
|
15
|
+
* Every replay reads the first event of that class at the same position, so a
|
|
16
|
+
* later one cannot change what the workflow observes.
|
|
16
17
|
*
|
|
17
|
-
* The classification
|
|
18
|
-
*
|
|
19
|
-
* is consumer state: the runtime passes over an event only after every
|
|
18
|
+
* The classification comes from `classifyEntityEvent` in `@workflow/world`,
|
|
19
|
+
* the same function the runtime keys its own duplicate detection on. What it
|
|
20
|
+
* cannot share is consumer state: the runtime passes over an event only after every
|
|
20
21
|
* registered callback has declined it, and a callback registered for a
|
|
21
22
|
* still-open entity legitimately claims a repeat (each retry of a step writes
|
|
22
23
|
* another `step_started`, and a live step consumer absorbs a second
|
|
@@ -25,27 +26,11 @@ import {
|
|
|
25
26
|
* consumer remains.
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
* Classes whose event closes its entity: no consumer is left for it after.
|
|
30
|
-
*
|
|
31
|
-
* The run's own terminal events are absent because `entityEventClass` gives
|
|
32
|
-
* them no class. The runtime exits rather than replaying the body once the log
|
|
33
|
-
* holds one, so nothing ever consumes them and nothing can repeat them.
|
|
34
|
-
*/
|
|
35
|
-
const TERMINAL_EVENT_CLASSES: ReadonlySet<EntityEventClass> = new Set([
|
|
36
|
-
'step_terminal',
|
|
37
|
-
'wait_completed',
|
|
38
|
-
'hook_disposed',
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
29
|
/** Classes with no entity to close first: the log records one per run. */
|
|
42
30
|
const SINGLETON_EVENT_CLASSES: ReadonlySet<EntityEventClass> = new Set([
|
|
43
31
|
'run_started',
|
|
44
32
|
]);
|
|
45
33
|
|
|
46
|
-
/** Entity key for events that carry no correlation ID (the run itself). */
|
|
47
|
-
const RUN_ENTITY_KEY = '';
|
|
48
|
-
|
|
49
34
|
/**
|
|
50
35
|
* Shown against an event this module reports. Deliberately says what the log
|
|
51
36
|
* shows rather than what the runtime did with it: tolerating these repeats is
|
|
@@ -122,10 +107,13 @@ function foldDuplicates(ordered: readonly Event[]): Set<string> {
|
|
|
122
107
|
const closedEntities = new Set<string>();
|
|
123
108
|
|
|
124
109
|
for (const event of ordered) {
|
|
125
|
-
|
|
126
|
-
|
|
110
|
+
// Shared with the runtime's own duplicate detection, deliberately: an
|
|
111
|
+
// event it tracks under no class is one it never reads past, so naming it
|
|
112
|
+
// here would grey out an event the run acted on.
|
|
113
|
+
const classification = classifyEntityEvent(event);
|
|
114
|
+
if (classification === undefined) continue;
|
|
127
115
|
|
|
128
|
-
const entity =
|
|
116
|
+
const { eventClass, entity } = classification;
|
|
129
117
|
const classKey = `${eventClass}:${entity}`;
|
|
130
118
|
const repeatsClass = seenClasses.has(classKey);
|
|
131
119
|
const entityWasClosed = closedEntities.has(entity);
|
|
@@ -9,7 +9,11 @@ let nextId = 0;
|
|
|
9
9
|
|
|
10
10
|
function event(
|
|
11
11
|
eventType: EventType,
|
|
12
|
-
options: {
|
|
12
|
+
options: {
|
|
13
|
+
correlationId?: string;
|
|
14
|
+
at: number;
|
|
15
|
+
externalAttemptId?: string;
|
|
16
|
+
}
|
|
13
17
|
): Event {
|
|
14
18
|
nextId += 1;
|
|
15
19
|
return {
|
|
@@ -20,6 +24,7 @@ function event(
|
|
|
20
24
|
createdAt: new Date(BASE_TIME + options.at * 1000),
|
|
21
25
|
occurredAt: new Date(BASE_TIME + options.at * 1000),
|
|
22
26
|
eventData: eventType === 'step_created' ? { stepName: 'doWork' } : {},
|
|
27
|
+
externalAttemptId: options.externalAttemptId,
|
|
23
28
|
} as unknown as Event;
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -31,6 +36,42 @@ const run = {
|
|
|
31
36
|
} as unknown as WorkflowRun;
|
|
32
37
|
|
|
33
38
|
describe('buildTrace', () => {
|
|
39
|
+
it('adds caller-derived attributes to step span data', () => {
|
|
40
|
+
const events = [
|
|
41
|
+
event('run_created', { at: 0 }),
|
|
42
|
+
event('run_started', { at: 0 }),
|
|
43
|
+
event('step_created', { correlationId: 'step_a', at: 1 }),
|
|
44
|
+
event('step_started', {
|
|
45
|
+
correlationId: 'step_a',
|
|
46
|
+
at: 2,
|
|
47
|
+
externalAttemptId: 'attempt_first',
|
|
48
|
+
}),
|
|
49
|
+
event('step_retrying', { correlationId: 'step_a', at: 3 }),
|
|
50
|
+
event('step_started', {
|
|
51
|
+
correlationId: 'step_a',
|
|
52
|
+
at: 4,
|
|
53
|
+
externalAttemptId: 'attempt_latest',
|
|
54
|
+
}),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const trace = buildTrace(run, events, new Date(BASE_TIME + 5000), {
|
|
58
|
+
getStepAttributes(stepEvents) {
|
|
59
|
+
const latestStart = stepEvents
|
|
60
|
+
.slice()
|
|
61
|
+
.reverse()
|
|
62
|
+
.find((candidate) => candidate.eventType === 'step_started') as
|
|
63
|
+
| (Event & { externalAttemptId?: string })
|
|
64
|
+
| undefined;
|
|
65
|
+
return { externalAttemptId: latestStart?.externalAttemptId };
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const stepSpan = trace.spans.find((span) => span.resource === 'step');
|
|
69
|
+
|
|
70
|
+
expect(stepSpan?.attributes.data).toMatchObject({
|
|
71
|
+
externalAttemptId: 'attempt_latest',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
34
75
|
it('ends a step span on the terminal event the run acted on', () => {
|
|
35
76
|
const events = [
|
|
36
77
|
event('run_created', { at: 0 }),
|
package/src/lib/trace-builder.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type WorkflowRun,
|
|
15
15
|
} from '@workflow/world';
|
|
16
16
|
import {
|
|
17
|
+
type GetStepAttributes,
|
|
17
18
|
getEventTimestamp,
|
|
18
19
|
hookToSpan,
|
|
19
20
|
runToSpan,
|
|
@@ -137,7 +138,8 @@ function buildSpans(
|
|
|
137
138
|
run: WorkflowRun,
|
|
138
139
|
groupedEvents: GroupedEvents,
|
|
139
140
|
now: Date,
|
|
140
|
-
latestKnownTime: Date
|
|
141
|
+
latestKnownTime: Date,
|
|
142
|
+
getStepAttributes?: GetStepAttributes
|
|
141
143
|
) {
|
|
142
144
|
// Active child spans cap at latestKnownTime so they don't extend into
|
|
143
145
|
// unknown territory. Even when the run is completed, we may not have loaded
|
|
@@ -146,7 +148,7 @@ function buildSpans(
|
|
|
146
148
|
const runMaxEnd = run.completedAt ?? now;
|
|
147
149
|
|
|
148
150
|
const stepSpans = Array.from(groupedEvents.eventsByStepId.values())
|
|
149
|
-
.map((events) => stepToSpan(events, childMaxEnd))
|
|
151
|
+
.map((events) => stepToSpan(events, childMaxEnd, getStepAttributes))
|
|
150
152
|
.filter((span): span is Span => span !== null);
|
|
151
153
|
|
|
152
154
|
const hookSpans = Array.from(groupedEvents.hookEvents.values())
|
|
@@ -208,7 +210,13 @@ export function buildTrace(
|
|
|
208
210
|
* from the only copy the caller was given, and dropping the wrong one moves
|
|
209
211
|
* a span. See {@link findDuplicateEventIds}.
|
|
210
212
|
*/
|
|
211
|
-
{
|
|
213
|
+
{
|
|
214
|
+
isCompleteHistory = false,
|
|
215
|
+
getStepAttributes,
|
|
216
|
+
}: {
|
|
217
|
+
isCompleteHistory?: boolean;
|
|
218
|
+
getStepAttributes?: GetStepAttributes;
|
|
219
|
+
} = {}
|
|
212
220
|
): TraceWithMeta {
|
|
213
221
|
// Span geometry comes from what the run acted on. A repeat of a class the
|
|
214
222
|
// log already records is read past by every replay, and letting one through
|
|
@@ -232,7 +240,8 @@ export function buildTrace(
|
|
|
232
240
|
run,
|
|
233
241
|
groupedEvents,
|
|
234
242
|
now,
|
|
235
|
-
latestKnownTime
|
|
243
|
+
latestKnownTime,
|
|
244
|
+
getStepAttributes
|
|
236
245
|
);
|
|
237
246
|
const sortedCascadingSpans = cascadeSpans(runSpan, spans);
|
|
238
247
|
|