@workflow/web-shared 5.0.0-beta.17 → 5.0.0-beta.18
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/span-markers.d.ts +46 -0
- package/dist/components/new-trace-viewer/components/span-markers.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/span-markers.js +85 -0
- package/dist/components/new-trace-viewer/components/timeline.d.ts +3 -1
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +34 -51
- package/dist/components/new-trace-viewer/components/use-row-window.d.ts +14 -0
- package/dist/components/new-trace-viewer/components/use-row-window.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/use-row-window.js +39 -1
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +87 -23
- package/dist/components/new-trace-viewer/utils.d.ts +15 -0
- package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/utils.js +37 -31
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +3 -25
- package/dist/components/sidebar/attributes-block.d.ts +1 -1
- package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
- package/dist/components/sidebar/attributes-block.js +5 -7
- package/dist/components/trace-viewer/trace-viewer.module.css +4 -4
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +6 -1
- package/dist/lib/zstd-browser-decoder.d.ts +7 -0
- package/dist/lib/zstd-browser-decoder.d.ts.map +1 -0
- package/dist/lib/zstd-browser-decoder.js +41 -0
- package/package.json +4 -3
- package/src/components/new-trace-viewer/components/span-markers.tsx +173 -0
- package/src/components/new-trace-viewer/components/timeline.tsx +142 -95
- package/src/components/new-trace-viewer/components/use-row-window.ts +47 -0
- package/src/components/new-trace-viewer/trace-viewer.tsx +108 -23
- package/src/components/new-trace-viewer/utils.test.ts +150 -0
- package/src/components/new-trace-viewer/utils.ts +61 -34
- package/src/components/sidebar/attribute-panel.tsx +5 -43
- package/src/components/sidebar/attributes-block.tsx +9 -18
- package/src/components/trace-viewer/trace-viewer.module.css +4 -4
- package/src/lib/hydration.ts +7 -0
- package/src/lib/zstd-browser-decoder.ts +42 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { Span, SpanEvent } from './types';
|
|
3
|
+
import {
|
|
4
|
+
computeOffscreenMarkers,
|
|
5
|
+
computeSpanMarkers,
|
|
6
|
+
computeSpanSegments,
|
|
7
|
+
} from './utils';
|
|
8
|
+
|
|
9
|
+
/** Build a high-res timestamp tuple ([seconds, nanoseconds]) for a given ms. */
|
|
10
|
+
function ts(ms: number): [number, number] {
|
|
11
|
+
return [Math.floor(ms / 1000), (ms % 1000) * 1e6];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hookSpan(opts: {
|
|
15
|
+
startMs: number;
|
|
16
|
+
endMs: number;
|
|
17
|
+
receivesMs: number[];
|
|
18
|
+
disposedMs?: number;
|
|
19
|
+
attrSetMs?: number[];
|
|
20
|
+
}): Span {
|
|
21
|
+
const events: SpanEvent[] = [
|
|
22
|
+
{ name: 'hook_created', timestamp: ts(opts.startMs), attributes: {} },
|
|
23
|
+
...opts.receivesMs.map((m) => ({
|
|
24
|
+
name: 'hook_received',
|
|
25
|
+
timestamp: ts(m),
|
|
26
|
+
attributes: {},
|
|
27
|
+
})),
|
|
28
|
+
...(opts.attrSetMs ?? []).map((m) => ({
|
|
29
|
+
name: 'attr_set',
|
|
30
|
+
timestamp: ts(m),
|
|
31
|
+
attributes: {},
|
|
32
|
+
})),
|
|
33
|
+
...(opts.disposedMs !== undefined
|
|
34
|
+
? [
|
|
35
|
+
{
|
|
36
|
+
name: 'hook_disposed',
|
|
37
|
+
timestamp: ts(opts.disposedMs),
|
|
38
|
+
attributes: {},
|
|
39
|
+
} satisfies SpanEvent,
|
|
40
|
+
]
|
|
41
|
+
: []),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
name: 'hook',
|
|
46
|
+
kind: 0,
|
|
47
|
+
resource: 'hook',
|
|
48
|
+
library: { name: 'workflow' },
|
|
49
|
+
spanId: 'hook-1',
|
|
50
|
+
status: { code: 1 },
|
|
51
|
+
traceFlags: 0,
|
|
52
|
+
attributes: {},
|
|
53
|
+
links: [],
|
|
54
|
+
events,
|
|
55
|
+
startTime: ts(opts.startMs),
|
|
56
|
+
endTime: ts(opts.endMs),
|
|
57
|
+
duration: ts(opts.endMs - opts.startMs),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('computeSpanSegments (hook)', () => {
|
|
62
|
+
it('renders a single waiting segment for a hook resumed many times but not disposed', () => {
|
|
63
|
+
const span = hookSpan({
|
|
64
|
+
startMs: 0,
|
|
65
|
+
endMs: 100_000,
|
|
66
|
+
receivesMs: [1_000, 50_000, 99_000],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// A hook resumed N times still re-suspends after every resumption, so the
|
|
70
|
+
// bar must stay "waiting" for its whole life — not flip to a filled
|
|
71
|
+
// "received" segment after the first receive (which hid resumptions 2..N).
|
|
72
|
+
expect(computeSpanSegments(span)).toEqual([
|
|
73
|
+
{ startFraction: 0, endFraction: 1, status: 'waiting' },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('ends the waiting segment at disposal and appends a succeeded tail', () => {
|
|
78
|
+
const span = hookSpan({
|
|
79
|
+
startMs: 0,
|
|
80
|
+
endMs: 100_000,
|
|
81
|
+
receivesMs: [1_000, 50_000],
|
|
82
|
+
disposedMs: 80_000,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(computeSpanSegments(span)).toEqual([
|
|
86
|
+
{ startFraction: 0, endFraction: 0.8, status: 'waiting' },
|
|
87
|
+
{ startFraction: 0.8, endFraction: 1, status: 'succeeded' },
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('treats a never-resolved hook as fully waiting', () => {
|
|
92
|
+
const span = hookSpan({ startMs: 0, endMs: 100_000, receivesMs: [] });
|
|
93
|
+
|
|
94
|
+
expect(computeSpanSegments(span)).toEqual([
|
|
95
|
+
{ startFraction: 0, endFraction: 1, status: 'waiting' },
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('computeSpanMarkers', () => {
|
|
101
|
+
it('emits one marker per resumption, including those at the temporal edges', () => {
|
|
102
|
+
const span = hookSpan({
|
|
103
|
+
startMs: 0,
|
|
104
|
+
endMs: 100_000,
|
|
105
|
+
receivesMs: [1_000, 50_000, 99_000],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const markers = computeSpanMarkers(span);
|
|
109
|
+
expect(markers.map((m) => m.timeMs)).toEqual([1_000, 50_000, 99_000]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('merges hook_received and attr_set events, sorted by time', () => {
|
|
113
|
+
const span = hookSpan({
|
|
114
|
+
startMs: 0,
|
|
115
|
+
endMs: 100_000,
|
|
116
|
+
receivesMs: [50_000],
|
|
117
|
+
attrSetMs: [10_000, 70_000],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(computeSpanMarkers(span).map((m) => m.timeMs)).toEqual([
|
|
121
|
+
10_000, 50_000, 70_000,
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns no markers when the span has no marker events', () => {
|
|
126
|
+
const span = hookSpan({ startMs: 0, endMs: 100_000, receivesMs: [] });
|
|
127
|
+
expect(computeSpanMarkers(span)).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('computeOffscreenMarkers', () => {
|
|
132
|
+
const mk = (timeMs: number) => ({ timeMs });
|
|
133
|
+
|
|
134
|
+
it('partitions markers by side with the nearest one per side', () => {
|
|
135
|
+
const markers = [mk(5), mk(8), mk(50), mk(92), mk(99)];
|
|
136
|
+
// Visible window [10, 90]: 5 & 8 off left (nearest 8), 92 & 99 off right
|
|
137
|
+
// (nearest 92), 50 in view.
|
|
138
|
+
expect(computeOffscreenMarkers(markers, 10, 90)).toEqual({
|
|
139
|
+
left: { count: 2, nearestMs: 8 },
|
|
140
|
+
right: { count: 2, nearestMs: 92 },
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns null for a side with nothing off-screen', () => {
|
|
145
|
+
expect(computeOffscreenMarkers([mk(20), mk(50)], 10, 90)).toEqual({
|
|
146
|
+
left: null,
|
|
147
|
+
right: null,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Span, SpanEvent } from './types';
|
|
2
1
|
import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
|
|
2
|
+
import type { Span, SpanEvent } from './types';
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Root bounds
|
|
@@ -243,7 +243,7 @@ function computeStepSegmentsFromSpan(
|
|
|
243
243
|
]);
|
|
244
244
|
|
|
245
245
|
if (marks.length === 0) {
|
|
246
|
-
segments.push({ startFraction: 0, endFraction: 1, status: '
|
|
246
|
+
segments.push({ startFraction: 0, endFraction: 1, status: 'queued' });
|
|
247
247
|
return segments;
|
|
248
248
|
}
|
|
249
249
|
|
|
@@ -278,7 +278,7 @@ function computeStepSegmentsFromSpan(
|
|
|
278
278
|
? 'failed'
|
|
279
279
|
: nextType === 'step_completed'
|
|
280
280
|
? 'succeeded'
|
|
281
|
-
: '
|
|
281
|
+
: 'retrying';
|
|
282
282
|
segments.push({
|
|
283
283
|
startFraction: markFrac,
|
|
284
284
|
endFraction: nextFrac,
|
|
@@ -313,47 +313,20 @@ function computeHookSegmentsFromSpan(
|
|
|
313
313
|
const segments: Segment[] = [];
|
|
314
314
|
if (duration <= 0) return segments;
|
|
315
315
|
|
|
316
|
-
const
|
|
317
|
-
.map((e) => ({ name: e.name, time: getHighResInMs(e.timestamp) }))
|
|
318
|
-
.sort((a, b) => a.time - b.time);
|
|
319
|
-
|
|
320
|
-
const received = sorted.find((e) => e.name === 'hook_received');
|
|
321
|
-
const disposed = sorted.find((e) => e.name === 'hook_disposed');
|
|
322
|
-
|
|
323
|
-
if (!received && !disposed) {
|
|
324
|
-
segments.push({ startFraction: 0, endFraction: 1, status: 'waiting' });
|
|
325
|
-
return segments;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const receivedFrac = received
|
|
329
|
-
? timeToFraction(received.time, startMs, duration)
|
|
330
|
-
: null;
|
|
316
|
+
const disposed = sortedEventMarks(events, ['hook_disposed'])[0];
|
|
331
317
|
const disposedFrac = disposed
|
|
332
318
|
? timeToFraction(disposed.time, startMs, duration)
|
|
333
319
|
: null;
|
|
334
320
|
|
|
335
|
-
|
|
321
|
+
const waitingEnd = disposedFrac ?? 1;
|
|
322
|
+
if (waitingEnd > 0.001) {
|
|
336
323
|
segments.push({
|
|
337
324
|
startFraction: 0,
|
|
338
|
-
endFraction:
|
|
339
|
-
status: 'waiting',
|
|
340
|
-
});
|
|
341
|
-
} else if (receivedFrac === null && disposedFrac !== null) {
|
|
342
|
-
segments.push({
|
|
343
|
-
startFraction: 0,
|
|
344
|
-
endFraction: disposedFrac,
|
|
325
|
+
endFraction: waitingEnd,
|
|
345
326
|
status: 'waiting',
|
|
346
327
|
});
|
|
347
328
|
}
|
|
348
329
|
|
|
349
|
-
if (receivedFrac !== null) {
|
|
350
|
-
segments.push({
|
|
351
|
-
startFraction: receivedFrac,
|
|
352
|
-
endFraction: disposedFrac ?? 1,
|
|
353
|
-
status: 'received',
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
330
|
if (disposedFrac !== null && disposedFrac < 0.999) {
|
|
358
331
|
segments.push({
|
|
359
332
|
startFraction: disposedFrac,
|
|
@@ -488,3 +461,57 @@ export function computeSpanSegments(span: Span): Segment[] {
|
|
|
488
461
|
return [];
|
|
489
462
|
}
|
|
490
463
|
}
|
|
464
|
+
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
// Span markers — point-in-time events rendered as ticks on top of a bar
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
export interface SpanMarker {
|
|
470
|
+
timeMs: number;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// `hook_received` = a resumption; `attr_set` = attributes written mid-span.
|
|
474
|
+
const MARKER_EVENT_NAMES = ['hook_received', 'attr_set'];
|
|
475
|
+
|
|
476
|
+
export function computeSpanMarkers(span: Span): SpanMarker[] {
|
|
477
|
+
return sortedEventMarks(span.events, MARKER_EVENT_NAMES).map((mark) => ({
|
|
478
|
+
timeMs: mark.time,
|
|
479
|
+
}));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export interface OffscreenSide {
|
|
483
|
+
count: number;
|
|
484
|
+
/** Nearest off-screen marker — the one a reveal jumps to. */
|
|
485
|
+
nearestMs: number;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export interface OffscreenMarkers {
|
|
489
|
+
left: OffscreenSide | null;
|
|
490
|
+
right: OffscreenSide | null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Partition markers outside `[visibleStartMs, visibleEndMs]` by side. */
|
|
494
|
+
export function computeOffscreenMarkers(
|
|
495
|
+
markers: SpanMarker[],
|
|
496
|
+
visibleStartMs: number,
|
|
497
|
+
visibleEndMs: number
|
|
498
|
+
): OffscreenMarkers {
|
|
499
|
+
let leftCount = 0;
|
|
500
|
+
let rightCount = 0;
|
|
501
|
+
let nearestLeft = Number.NEGATIVE_INFINITY;
|
|
502
|
+
let nearestRight = Number.POSITIVE_INFINITY;
|
|
503
|
+
for (const { timeMs } of markers) {
|
|
504
|
+
if (timeMs < visibleStartMs) {
|
|
505
|
+
leftCount++;
|
|
506
|
+
if (timeMs > nearestLeft) nearestLeft = timeMs;
|
|
507
|
+
} else if (timeMs > visibleEndMs) {
|
|
508
|
+
rightCount++;
|
|
509
|
+
if (timeMs < nearestRight) nearestRight = timeMs;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
left: leftCount > 0 ? { count: leftCount, nearestMs: nearestLeft } : null,
|
|
514
|
+
right:
|
|
515
|
+
rightCount > 0 ? { count: rightCount, nearestMs: nearestRight } : null,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
@@ -7,7 +7,6 @@ import { format } from 'date-fns';
|
|
|
7
7
|
import type { KeyboardEvent, ReactNode } from 'react';
|
|
8
8
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
|
9
9
|
import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
|
|
10
|
-
import { useToast } from '../../lib/toast';
|
|
11
10
|
import { extractConversation, isDoStreamStep } from '../../lib/utils';
|
|
12
11
|
import { CopyButton } from '../new-trace-viewer/components/copy-button';
|
|
13
12
|
import { MiddleTruncate } from '../new-trace-viewer/components/middle-truncate/middle-truncate';
|
|
@@ -616,6 +615,7 @@ const copyableBasicAttributes = new Set<AttributeKey>([
|
|
|
616
615
|
'hookId',
|
|
617
616
|
'eventId',
|
|
618
617
|
'deploymentId',
|
|
618
|
+
'moduleSpecifier',
|
|
619
619
|
]);
|
|
620
620
|
|
|
621
621
|
export const AttributeBlock = ({
|
|
@@ -733,7 +733,6 @@ export const AttributePanel = ({
|
|
|
733
733
|
/** Resource type of the selected span — used to show targeted loading skeletons. */
|
|
734
734
|
resource?: string;
|
|
735
735
|
}) => {
|
|
736
|
-
const toast = useToast();
|
|
737
736
|
// Extract workflowCoreVersion from executionContext for display
|
|
738
737
|
const displayData = useMemo(() => {
|
|
739
738
|
const result = { ...data };
|
|
@@ -819,17 +818,6 @@ export const AttributePanel = ({
|
|
|
819
818
|
}),
|
|
820
819
|
[displayData.stepName]
|
|
821
820
|
);
|
|
822
|
-
const handleCopyModuleSpecifier = useCallback((value: string) => {
|
|
823
|
-
navigator.clipboard
|
|
824
|
-
.writeText(value)
|
|
825
|
-
.then(() => {
|
|
826
|
-
toast.success('moduleSpecifier copied');
|
|
827
|
-
})
|
|
828
|
-
.catch(() => {
|
|
829
|
-
toast.error('Failed to copy moduleSpecifier');
|
|
830
|
-
});
|
|
831
|
-
}, []);
|
|
832
|
-
|
|
833
821
|
const outerDecryptCtx = useContext(DecryptClickContext);
|
|
834
822
|
const decryptValue = onDecrypt
|
|
835
823
|
? {
|
|
@@ -850,16 +838,9 @@ export const AttributePanel = ({
|
|
|
850
838
|
const displayValue = attributeToDisplayFn[
|
|
851
839
|
attribute as keyof typeof attributeToDisplayFn
|
|
852
840
|
]?.(displayData[attribute as keyof typeof displayData]);
|
|
853
|
-
const isModuleSpecifier = attribute === 'moduleSpecifier';
|
|
854
841
|
const isCopyableBasicAttribute =
|
|
855
842
|
copyableBasicAttributes.has(attribute as AttributeKey) &&
|
|
856
843
|
typeof displayValue === 'string';
|
|
857
|
-
const moduleSpecifierValue =
|
|
858
|
-
typeof displayValue === 'string'
|
|
859
|
-
? displayValue
|
|
860
|
-
: String(
|
|
861
|
-
displayValue ?? displayData.moduleSpecifier ?? ''
|
|
862
|
-
);
|
|
863
844
|
|
|
864
845
|
return (
|
|
865
846
|
<div
|
|
@@ -869,34 +850,15 @@ export const AttributePanel = ({
|
|
|
869
850
|
<span className="text-label-14 text-gray-900">
|
|
870
851
|
{getAttributeDisplayName(attribute)}
|
|
871
852
|
</span>
|
|
872
|
-
{
|
|
873
|
-
<button
|
|
874
|
-
type="button"
|
|
875
|
-
className="min-w-0 max-w-[70%] truncate text-right text-label-13 font-mono"
|
|
876
|
-
style={{
|
|
877
|
-
color: 'var(--ds-gray-1000)',
|
|
878
|
-
background: 'transparent',
|
|
879
|
-
border: 'none',
|
|
880
|
-
padding: 0,
|
|
881
|
-
}}
|
|
882
|
-
title={moduleSpecifierValue}
|
|
883
|
-
onClick={() =>
|
|
884
|
-
handleCopyModuleSpecifier(moduleSpecifierValue)
|
|
885
|
-
}
|
|
886
|
-
>
|
|
887
|
-
{moduleSpecifierValue}
|
|
888
|
-
</button>
|
|
889
|
-
) : isCopyableBasicAttribute ? (
|
|
853
|
+
{isCopyableBasicAttribute ? (
|
|
890
854
|
<div
|
|
891
|
-
className="flex min-w-0 max-w-[70%] items-center justify-end gap-1 text-
|
|
892
|
-
style={{
|
|
893
|
-
color: 'var(--ds-gray-1000)',
|
|
894
|
-
}}
|
|
855
|
+
className="flex min-w-0 max-w-[70%] items-center justify-end gap-1 text-[13px] font-mono text-gray-1000"
|
|
895
856
|
title={displayValue}
|
|
896
857
|
>
|
|
897
858
|
<MiddleTruncate
|
|
898
859
|
value={displayValue}
|
|
899
|
-
className="
|
|
860
|
+
className="text-right"
|
|
861
|
+
style={{ gridTemplateColumns: 'minmax(0, 1fr)' }}
|
|
900
862
|
/>
|
|
901
863
|
<CopyButton
|
|
902
864
|
copyText={displayValue}
|
|
@@ -41,26 +41,21 @@ function AttributeRow({
|
|
|
41
41
|
}) {
|
|
42
42
|
const reserved = isReservedAttributeKey(attributeKey);
|
|
43
43
|
return (
|
|
44
|
-
<div className="flex items-center justify-between gap-3 py-
|
|
44
|
+
<div className="flex items-center justify-between gap-3 py-0.5">
|
|
45
45
|
<span
|
|
46
|
-
className="flex min-w-0 items-center gap-1.5 text-label-
|
|
47
|
-
style={{
|
|
48
|
-
color: reserved ? 'var(--ds-gray-700)' : 'var(--ds-gray-900)',
|
|
49
|
-
}}
|
|
46
|
+
className="flex min-w-0 items-center gap-1.5 text-label-13 text-gray-900"
|
|
47
|
+
style={reserved ? { color: 'var(--ds-gray-700)' } : undefined}
|
|
50
48
|
>
|
|
51
49
|
<span className="truncate">{attributeKey}</span>
|
|
52
50
|
{reserved && <ReservedBadge />}
|
|
53
51
|
</span>
|
|
54
52
|
{removed ? (
|
|
55
|
-
<span
|
|
56
|
-
className="shrink-0 text-label-12 italic"
|
|
57
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
58
|
-
>
|
|
53
|
+
<span className="shrink-0 text-copy-13 italic text-gray-700">
|
|
59
54
|
removed
|
|
60
55
|
</span>
|
|
61
56
|
) : (
|
|
62
57
|
<span
|
|
63
|
-
className="max-w-[60%] truncate text-
|
|
58
|
+
className="max-w-[60%] truncate text-copy-13 text-gray-1000"
|
|
64
59
|
title={value}
|
|
65
60
|
>
|
|
66
61
|
{value}
|
|
@@ -85,7 +80,7 @@ export function sortAttributeKeys(keys: string[]): string[] {
|
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
/**
|
|
88
|
-
* Collapsible
|
|
83
|
+
* Collapsible section showing a run's materialized attributes as key-value
|
|
89
84
|
* rows. Reserved (`$`-prefixed) keys are visually de-emphasized with a
|
|
90
85
|
* badge and sorted after user keys.
|
|
91
86
|
*/
|
|
@@ -98,12 +93,8 @@ export function RunAttributesCard({
|
|
|
98
93
|
if (keys.length === 0) return null;
|
|
99
94
|
|
|
100
95
|
return (
|
|
101
|
-
<DetailCard
|
|
102
|
-
|
|
103
|
-
defaultOpen
|
|
104
|
-
contentClassName="mb-0"
|
|
105
|
-
>
|
|
106
|
-
<div className="flex flex-col divide-y divide-gray-alpha-400">
|
|
96
|
+
<DetailCard summary="Attributes" defaultOpen contentClassName="mb-4">
|
|
97
|
+
<div className="flex flex-col">
|
|
107
98
|
{keys.map((key) => (
|
|
108
99
|
<AttributeRow attributeKey={key} key={key} value={attributes[key]} />
|
|
109
100
|
))}
|
|
@@ -156,7 +147,7 @@ export function AttrSetEventBlock({ data }: { data: unknown }) {
|
|
|
156
147
|
|
|
157
148
|
return (
|
|
158
149
|
<div className="flex flex-col px-3 py-1">
|
|
159
|
-
<div className="flex flex-col
|
|
150
|
+
<div className="flex flex-col">
|
|
160
151
|
{data.changes.map((change, index) => (
|
|
161
152
|
<AttributeRow
|
|
162
153
|
attributeKey={change.key}
|
|
@@ -1314,10 +1314,10 @@
|
|
|
1314
1314
|
.segQueued {
|
|
1315
1315
|
background: repeating-linear-gradient(
|
|
1316
1316
|
45deg,
|
|
1317
|
-
var(--ds-gray-
|
|
1318
|
-
var(--ds-gray-
|
|
1319
|
-
var(--ds-gray-
|
|
1320
|
-
var(--ds-gray-
|
|
1317
|
+
var(--ds-gray-200),
|
|
1318
|
+
var(--ds-gray-200) 6px,
|
|
1319
|
+
var(--ds-gray-100) 6px,
|
|
1320
|
+
var(--ds-gray-100) 12px
|
|
1321
1321
|
);
|
|
1322
1322
|
}
|
|
1323
1323
|
|
package/src/lib/hydration.ts
CHANGED
|
@@ -476,6 +476,13 @@ export async function hydrateResourceIOWithKey<T>(
|
|
|
476
476
|
'@workflow/core/serialization-format'
|
|
477
477
|
);
|
|
478
478
|
const { importKey } = await import('@workflow/core/encryption');
|
|
479
|
+
// Payloads may be zstd-compressed (the Web DecompressionStream has no zstd);
|
|
480
|
+
// register the WASM-backed browser decoder before hydrating. Idempotent and
|
|
481
|
+
// lazy — the WASM is only compiled when a zstd payload is actually decoded.
|
|
482
|
+
const { ensureZstdDecoderRegistered } = await import(
|
|
483
|
+
'./zstd-browser-decoder.js'
|
|
484
|
+
);
|
|
485
|
+
ensureZstdDecoderRegistered();
|
|
479
486
|
const cryptoKey = await importKey(key);
|
|
480
487
|
const revivers = getRevivers();
|
|
481
488
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser zstd decoder for the o11y read path.
|
|
3
|
+
*
|
|
4
|
+
* The Web `DecompressionStream` has no zstd support, so `@workflow/core`'s
|
|
5
|
+
* `hydrateDataWithKey` delegates zstd inflation to a decoder registered via
|
|
6
|
+
* `registerZstdDecoder`. This module supplies that decoder, backed by the
|
|
7
|
+
* `@tootallnate/zstd-wasm` single-file WASM decoder.
|
|
8
|
+
*
|
|
9
|
+
* The package leaves WASM sourcing to the caller; we resolve the shipped
|
|
10
|
+
* `zstd.wasm` as a bundler asset (`new URL(..., import.meta.url)`, the same
|
|
11
|
+
* pattern the trace-viewer Worker uses) and compile it once, lazily — the
|
|
12
|
+
* WASM is fetched only the first time a zstd payload is actually decoded.
|
|
13
|
+
*/
|
|
14
|
+
import { registerZstdDecoder } from '@workflow/core/serialization-format';
|
|
15
|
+
|
|
16
|
+
let registered = false;
|
|
17
|
+
let modulePromise: Promise<WebAssembly.Module> | undefined;
|
|
18
|
+
|
|
19
|
+
function loadWasmModule(): Promise<WebAssembly.Module> {
|
|
20
|
+
if (!modulePromise) {
|
|
21
|
+
const url = new URL('@tootallnate/zstd-wasm/zstd.wasm', import.meta.url);
|
|
22
|
+
modulePromise = fetch(url)
|
|
23
|
+
.then((res) => res.arrayBuffer())
|
|
24
|
+
.then((bytes) => WebAssembly.compile(bytes));
|
|
25
|
+
}
|
|
26
|
+
return modulePromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register the browser zstd decoder with `@workflow/core` (idempotent).
|
|
31
|
+
* Call this before hydrating payloads that may be zstd-compressed; the
|
|
32
|
+
* actual WASM compile + decode happens lazily on first use.
|
|
33
|
+
*/
|
|
34
|
+
export function ensureZstdDecoderRegistered(): void {
|
|
35
|
+
if (registered) return;
|
|
36
|
+
registered = true;
|
|
37
|
+
registerZstdDecoder(async (payload) => {
|
|
38
|
+
const { decompressBytes } = await import('@tootallnate/zstd-wasm');
|
|
39
|
+
const wasmModule = await loadWasmModule();
|
|
40
|
+
return decompressBytes(wasmModule, payload);
|
|
41
|
+
});
|
|
42
|
+
}
|