executable-stories-formatters 0.2.0 → 0.4.0
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 +1 -1
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +401 -7
- package/dist/cli.js.map +1 -1
- package/dist/{index-DCJ0NvAp.d.cts → index-DyeUWfYK.d.cts} +26 -1
- package/dist/{index-DCJ0NvAp.d.ts → index-DyeUWfYK.d.ts} +26 -1
- package/dist/index.cjs +433 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +430 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/README.md +7 -12
package/dist/index.js
CHANGED
|
@@ -883,20 +883,32 @@ function initCollapse() {
|
|
|
883
883
|
}
|
|
884
884
|
});
|
|
885
885
|
});
|
|
886
|
+
|
|
887
|
+
document.querySelectorAll('.trace-view-header').forEach(header => {
|
|
888
|
+
header.addEventListener('click', () => {
|
|
889
|
+
toggleCollapse(header, header.closest('.trace-view'));
|
|
890
|
+
});
|
|
891
|
+
header.addEventListener('keydown', (e) => {
|
|
892
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
893
|
+
e.preventDefault();
|
|
894
|
+
toggleCollapse(header, header.closest('.trace-view'));
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
});
|
|
886
898
|
}
|
|
887
899
|
|
|
888
900
|
function expandAll() {
|
|
889
|
-
document.querySelectorAll('.feature, .scenario').forEach(el => {
|
|
901
|
+
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
890
902
|
el.classList.remove('collapsed');
|
|
891
|
-
const header = el.querySelector('.feature-header, .scenario-header');
|
|
903
|
+
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
892
904
|
header?.setAttribute('aria-expanded', 'true');
|
|
893
905
|
});
|
|
894
906
|
}
|
|
895
907
|
|
|
896
908
|
function collapseAll() {
|
|
897
|
-
document.querySelectorAll('.feature, .scenario').forEach(el => {
|
|
909
|
+
document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
|
|
898
910
|
el.classList.add('collapsed');
|
|
899
|
-
const header = el.querySelector('.feature-header, .scenario-header');
|
|
911
|
+
const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
|
|
900
912
|
header?.setAttribute('aria-expanded', 'false');
|
|
901
913
|
});
|
|
902
914
|
}
|
|
@@ -1086,6 +1098,7 @@ var CSS_STYLES = `
|
|
|
1086
1098
|
--tag-bg: hsl(145 55% 95%);
|
|
1087
1099
|
--tag-color: hsl(145 63% 30%);
|
|
1088
1100
|
--tag-border: hsl(145 55% 85%);
|
|
1101
|
+
--step-param-color: hsl(220 70% 50%);
|
|
1089
1102
|
|
|
1090
1103
|
/* Accordion/Collapsible styling */
|
|
1091
1104
|
--accordion-header-hover: hsl(0 0% 98%);
|
|
@@ -1144,6 +1157,7 @@ var CSS_STYLES = `
|
|
|
1144
1157
|
--tag-bg: hsl(145 35% 14%);
|
|
1145
1158
|
--tag-color: hsl(145 63% 60%);
|
|
1146
1159
|
--tag-border: hsl(145 35% 22%);
|
|
1160
|
+
--step-param-color: hsl(220 70% 70%);
|
|
1147
1161
|
|
|
1148
1162
|
/* Accordion/Collapsible styling */
|
|
1149
1163
|
--accordion-header-hover: hsl(0 0% 11%);
|
|
@@ -1192,6 +1206,7 @@ var CSS_STYLES = `
|
|
|
1192
1206
|
--tag-bg: hsl(145 35% 14%);
|
|
1193
1207
|
--tag-color: hsl(145 63% 60%);
|
|
1194
1208
|
--tag-border: hsl(145 35% 22%);
|
|
1209
|
+
--step-param-color: hsl(220 70% 70%);
|
|
1195
1210
|
--accordion-header-hover: hsl(0 0% 11%);
|
|
1196
1211
|
--accordion-content-bg: hsl(0 0% 7%);
|
|
1197
1212
|
}
|
|
@@ -1737,6 +1752,12 @@ body {
|
|
|
1737
1752
|
color: var(--foreground);
|
|
1738
1753
|
}
|
|
1739
1754
|
|
|
1755
|
+
.step-param {
|
|
1756
|
+
font-style: italic;
|
|
1757
|
+
font-weight: 500;
|
|
1758
|
+
color: var(--step-param-color);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1740
1761
|
.step-duration {
|
|
1741
1762
|
color: var(--muted-foreground);
|
|
1742
1763
|
font-size: 0.6875rem;
|
|
@@ -2413,6 +2434,133 @@ body {
|
|
|
2413
2434
|
font-family: inherit;
|
|
2414
2435
|
background: none;
|
|
2415
2436
|
}
|
|
2437
|
+
|
|
2438
|
+
/* ============================================================================
|
|
2439
|
+
Trace View - OTel span waterfall
|
|
2440
|
+
============================================================================ */
|
|
2441
|
+
.trace-view {
|
|
2442
|
+
margin-top: 0.75rem;
|
|
2443
|
+
border: 1px solid var(--border);
|
|
2444
|
+
border-radius: calc(var(--radius) - 2px);
|
|
2445
|
+
overflow: hidden;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
.trace-view-header {
|
|
2449
|
+
display: flex;
|
|
2450
|
+
align-items: center;
|
|
2451
|
+
gap: 0.5rem;
|
|
2452
|
+
padding: 0.5rem 0.75rem;
|
|
2453
|
+
background: var(--card);
|
|
2454
|
+
cursor: pointer;
|
|
2455
|
+
user-select: none;
|
|
2456
|
+
font-size: 0.8125rem;
|
|
2457
|
+
font-weight: 500;
|
|
2458
|
+
color: var(--foreground);
|
|
2459
|
+
transition: background-color 0.15s ease;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
.trace-view-header:hover {
|
|
2463
|
+
background: var(--accordion-header-hover);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.trace-view-count {
|
|
2467
|
+
font-size: 0.6875rem;
|
|
2468
|
+
font-weight: 500;
|
|
2469
|
+
padding: 0.125rem 0.5rem;
|
|
2470
|
+
background: var(--success-light);
|
|
2471
|
+
color: var(--success);
|
|
2472
|
+
border: 1px solid var(--success-border);
|
|
2473
|
+
border-radius: 9999px;
|
|
2474
|
+
font-family: var(--font-mono);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
.trace-view-content {
|
|
2478
|
+
border-top: 1px solid var(--border);
|
|
2479
|
+
padding: 0.5rem 0.75rem;
|
|
2480
|
+
background: var(--accordion-content-bg);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
.trace-view.collapsed .trace-view-content {
|
|
2484
|
+
display: none;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
.trace-view-axis {
|
|
2488
|
+
display: flex;
|
|
2489
|
+
justify-content: space-between;
|
|
2490
|
+
font-size: 0.625rem;
|
|
2491
|
+
font-family: var(--font-mono);
|
|
2492
|
+
color: var(--muted-foreground);
|
|
2493
|
+
padding-bottom: 0.375rem;
|
|
2494
|
+
margin-bottom: 0.375rem;
|
|
2495
|
+
border-bottom: 1px solid var(--border);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
.trace-view-row {
|
|
2499
|
+
display: flex;
|
|
2500
|
+
align-items: center;
|
|
2501
|
+
gap: 0.5rem;
|
|
2502
|
+
padding: 0.1875rem 0;
|
|
2503
|
+
font-size: 0.75rem;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
.trace-view-name {
|
|
2507
|
+
width: 35%;
|
|
2508
|
+
flex-shrink: 0;
|
|
2509
|
+
display: flex;
|
|
2510
|
+
align-items: center;
|
|
2511
|
+
gap: 0.375rem;
|
|
2512
|
+
font-family: var(--font-mono);
|
|
2513
|
+
white-space: nowrap;
|
|
2514
|
+
overflow: hidden;
|
|
2515
|
+
text-overflow: ellipsis;
|
|
2516
|
+
color: var(--foreground);
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
.trace-view-status-dot {
|
|
2520
|
+
width: 8px;
|
|
2521
|
+
height: 8px;
|
|
2522
|
+
border-radius: 50%;
|
|
2523
|
+
flex-shrink: 0;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
.trace-view-status-ok { background: var(--success); }
|
|
2527
|
+
.trace-view-status-error { background: var(--error); }
|
|
2528
|
+
.trace-view-status-unset { background: var(--muted-foreground); }
|
|
2529
|
+
|
|
2530
|
+
.trace-view-bar-container {
|
|
2531
|
+
flex: 1;
|
|
2532
|
+
position: relative;
|
|
2533
|
+
height: 1.25rem;
|
|
2534
|
+
background: var(--muted);
|
|
2535
|
+
border-radius: 2px;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
.trace-view-bar {
|
|
2539
|
+
position: absolute;
|
|
2540
|
+
top: 0;
|
|
2541
|
+
height: 100%;
|
|
2542
|
+
border-radius: 2px;
|
|
2543
|
+
min-width: 2px;
|
|
2544
|
+
display: flex;
|
|
2545
|
+
align-items: center;
|
|
2546
|
+
padding: 0 0.375rem;
|
|
2547
|
+
font-size: 0.625rem;
|
|
2548
|
+
font-family: var(--font-mono);
|
|
2549
|
+
color: white;
|
|
2550
|
+
white-space: nowrap;
|
|
2551
|
+
overflow: hidden;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
.trace-view-bar-ok { background: var(--success); }
|
|
2555
|
+
.trace-view-bar-error { background: var(--error); }
|
|
2556
|
+
.trace-view-bar-unset { background: var(--muted-foreground); }
|
|
2557
|
+
|
|
2558
|
+
@media print {
|
|
2559
|
+
.trace-view.collapsed .trace-view-content {
|
|
2560
|
+
display: block;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2416
2564
|
`;
|
|
2417
2565
|
|
|
2418
2566
|
// src/formatters/html/renderers/status.ts
|
|
@@ -2657,10 +2805,11 @@ function renderStep(step, stepResult, index, deps) {
|
|
|
2657
2805
|
const isContinuation = CONTINUATION_KEYWORDS.includes(keywordTrimmed);
|
|
2658
2806
|
const stepClass = isContinuation ? "step continuation" : "step";
|
|
2659
2807
|
const stepDocs = deps.renderDocs(step.docs, "step-docs");
|
|
2808
|
+
const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
|
|
2660
2809
|
return `<div class="${stepClass}">
|
|
2661
2810
|
<span class="step-status ${statusClass}">${statusIcon}</span>
|
|
2662
2811
|
<span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
|
|
2663
|
-
<span class="step-text">${
|
|
2812
|
+
<span class="step-text">${textHtml}</span>
|
|
2664
2813
|
<span class="step-duration">${duration}</span>
|
|
2665
2814
|
</div>${stepDocs}`;
|
|
2666
2815
|
}
|
|
@@ -2672,6 +2821,30 @@ function renderSteps(args, deps) {
|
|
|
2672
2821
|
return `<div class="steps">${stepsHtml}</div>`;
|
|
2673
2822
|
}
|
|
2674
2823
|
|
|
2824
|
+
// src/formatters/html/renderers/step-params.ts
|
|
2825
|
+
var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.\-])\d+(?:\.\d+)?(?![\w.\-])/g;
|
|
2826
|
+
function highlightStepParams(text, deps) {
|
|
2827
|
+
const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
|
|
2828
|
+
if (matches.length === 0) {
|
|
2829
|
+
return deps.escapeHtml(text);
|
|
2830
|
+
}
|
|
2831
|
+
let result = "";
|
|
2832
|
+
let lastIndex = 0;
|
|
2833
|
+
for (const match of matches) {
|
|
2834
|
+
const matchStart = match.index;
|
|
2835
|
+
const matchEnd = matchStart + match[0].length;
|
|
2836
|
+
if (matchStart > lastIndex) {
|
|
2837
|
+
result += deps.escapeHtml(text.slice(lastIndex, matchStart));
|
|
2838
|
+
}
|
|
2839
|
+
result += `<span class="step-param">${deps.escapeHtml(match[0])}</span>`;
|
|
2840
|
+
lastIndex = matchEnd;
|
|
2841
|
+
}
|
|
2842
|
+
if (lastIndex < text.length) {
|
|
2843
|
+
result += deps.escapeHtml(text.slice(lastIndex));
|
|
2844
|
+
}
|
|
2845
|
+
return result;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2675
2848
|
// src/formatters/html/renderers/scenario.ts
|
|
2676
2849
|
function renderScenario(args, deps) {
|
|
2677
2850
|
const { tc } = args;
|
|
@@ -2679,6 +2852,19 @@ function renderScenario(args, deps) {
|
|
|
2679
2852
|
const statusClass = `status-${tc.status}`;
|
|
2680
2853
|
const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
|
|
2681
2854
|
const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
|
|
2855
|
+
const otelMeta = tc.story.meta?.otel;
|
|
2856
|
+
let traceBadge = "";
|
|
2857
|
+
if (otelMeta?.traceId) {
|
|
2858
|
+
const shortId = otelMeta.traceId.slice(0, 16);
|
|
2859
|
+
const traceLink = tc.story.docs?.find(
|
|
2860
|
+
(d) => d.kind === "link" && d.label === "View Trace"
|
|
2861
|
+
);
|
|
2862
|
+
if (traceLink) {
|
|
2863
|
+
traceBadge = `<a class="tag trace-tag" href="${deps.escapeHtml(traceLink.url)}" title="${deps.escapeHtml(otelMeta.traceId)}" target="_blank" rel="noopener">${deps.escapeHtml(shortId)}\u2026</a>`;
|
|
2864
|
+
} else {
|
|
2865
|
+
traceBadge = `<span class="tag trace-tag" title="${deps.escapeHtml(otelMeta.traceId)}">${deps.escapeHtml(shortId)}\u2026</span>`;
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2682
2868
|
const storyDocs = deps.renderDocs(tc.story.docs, "story-docs");
|
|
2683
2869
|
const steps = deps.renderSteps(
|
|
2684
2870
|
{ steps: tc.story.steps, stepResults: tc.stepResults },
|
|
@@ -2699,6 +2885,10 @@ function renderScenario(args, deps) {
|
|
|
2699
2885
|
embedScreenshots: deps.embedScreenshots
|
|
2700
2886
|
}
|
|
2701
2887
|
);
|
|
2888
|
+
const traceView = deps.renderTraceView(
|
|
2889
|
+
{ spans: tc.story.otelSpans },
|
|
2890
|
+
{ escapeHtml: deps.escapeHtml }
|
|
2891
|
+
);
|
|
2702
2892
|
const collapsedClass = deps.startCollapsed ? " collapsed" : "";
|
|
2703
2893
|
const ariaExpanded = !deps.startCollapsed;
|
|
2704
2894
|
return `
|
|
@@ -2709,7 +2899,7 @@ function renderScenario(args, deps) {
|
|
|
2709
2899
|
<span class="status-icon ${statusClass}">${statusIcon}</span>
|
|
2710
2900
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
2711
2901
|
</div>
|
|
2712
|
-
<div class="scenario-meta">${tags}</div>
|
|
2902
|
+
<div class="scenario-meta">${tags}${traceBadge}</div>
|
|
2713
2903
|
</div>
|
|
2714
2904
|
<span class="scenario-duration">${duration}</span>
|
|
2715
2905
|
</div>
|
|
@@ -2718,6 +2908,193 @@ function renderScenario(args, deps) {
|
|
|
2718
2908
|
${steps}
|
|
2719
2909
|
${error}
|
|
2720
2910
|
${attachments}
|
|
2911
|
+
${traceView}
|
|
2912
|
+
</div>
|
|
2913
|
+
</div>`;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
// src/formatters/html/renderers/trace-view.ts
|
|
2917
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
|
|
2918
|
+
var TOOLTIP_MAX_LENGTH = 4096;
|
|
2919
|
+
function safeStatus(status) {
|
|
2920
|
+
return VALID_STATUSES.has(status) ? status : "unset";
|
|
2921
|
+
}
|
|
2922
|
+
function formatDuration(ms) {
|
|
2923
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
|
|
2924
|
+
return `${ms.toFixed(1)}ms`;
|
|
2925
|
+
}
|
|
2926
|
+
function clamp(value, min, max) {
|
|
2927
|
+
return Math.min(max, Math.max(min, value));
|
|
2928
|
+
}
|
|
2929
|
+
function normalizeSpans(spans) {
|
|
2930
|
+
const result = [];
|
|
2931
|
+
for (const span of spans) {
|
|
2932
|
+
if (!span || typeof span !== "object") continue;
|
|
2933
|
+
if (typeof span.spanId !== "string" || typeof span.name !== "string") continue;
|
|
2934
|
+
let startTimeMs;
|
|
2935
|
+
let durationMs;
|
|
2936
|
+
if (span.startTimeMs != null && span.durationMs != null) {
|
|
2937
|
+
startTimeMs = span.startTimeMs;
|
|
2938
|
+
durationMs = span.durationMs;
|
|
2939
|
+
} else if (span.startTimeUnixNano != null && span.endTimeUnixNano != null) {
|
|
2940
|
+
startTimeMs = span.startTimeUnixNano / 1e6;
|
|
2941
|
+
durationMs = (span.endTimeUnixNano - span.startTimeUnixNano) / 1e6;
|
|
2942
|
+
} else {
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
durationMs = Math.max(0, durationMs);
|
|
2946
|
+
if (!isFinite(startTimeMs) || !isFinite(durationMs)) continue;
|
|
2947
|
+
result.push({
|
|
2948
|
+
spanId: span.spanId,
|
|
2949
|
+
parentSpanId: span.parentSpanId,
|
|
2950
|
+
name: span.name,
|
|
2951
|
+
startTimeMs,
|
|
2952
|
+
durationMs,
|
|
2953
|
+
status: safeStatus(span.status),
|
|
2954
|
+
statusMessage: span.statusMessage,
|
|
2955
|
+
attributes: span.attributes
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
return result;
|
|
2959
|
+
}
|
|
2960
|
+
function buildTree(spans) {
|
|
2961
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2962
|
+
for (const span of spans) {
|
|
2963
|
+
let key = span.spanId;
|
|
2964
|
+
if (byId.has(key)) {
|
|
2965
|
+
let suffix = 2;
|
|
2966
|
+
while (byId.has(`${span.spanId}__dup${suffix}`)) suffix++;
|
|
2967
|
+
key = `${span.spanId}__dup${suffix}`;
|
|
2968
|
+
}
|
|
2969
|
+
byId.set(key, { span: { ...span, spanId: key }, children: [], depth: 0 });
|
|
2970
|
+
}
|
|
2971
|
+
const roots = [];
|
|
2972
|
+
for (const node of byId.values()) {
|
|
2973
|
+
const parentId = node.span.parentSpanId;
|
|
2974
|
+
const parent = parentId ? byId.get(parentId) : void 0;
|
|
2975
|
+
if (parent && parent !== node) {
|
|
2976
|
+
parent.children.push(node);
|
|
2977
|
+
} else {
|
|
2978
|
+
roots.push(node);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
for (const node of byId.values()) {
|
|
2982
|
+
node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
2983
|
+
}
|
|
2984
|
+
roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
2985
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2986
|
+
function assignDepth(node, depth) {
|
|
2987
|
+
if (visited.has(node.span.spanId)) return;
|
|
2988
|
+
visited.add(node.span.spanId);
|
|
2989
|
+
node.depth = depth;
|
|
2990
|
+
for (const child of node.children) {
|
|
2991
|
+
assignDepth(child, depth + 1);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
for (const root of roots) {
|
|
2995
|
+
assignDepth(root, 0);
|
|
2996
|
+
}
|
|
2997
|
+
for (const node of byId.values()) {
|
|
2998
|
+
if (!visited.has(node.span.spanId)) {
|
|
2999
|
+
node.children = [];
|
|
3000
|
+
roots.push(node);
|
|
3001
|
+
assignDepth(node, 0);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
3005
|
+
return roots;
|
|
3006
|
+
}
|
|
3007
|
+
function flattenTree(roots) {
|
|
3008
|
+
const result = [];
|
|
3009
|
+
function walk(node) {
|
|
3010
|
+
result.push(node);
|
|
3011
|
+
for (const child of node.children) {
|
|
3012
|
+
walk(child);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
for (const root of roots) {
|
|
3016
|
+
walk(root);
|
|
3017
|
+
}
|
|
3018
|
+
return result;
|
|
3019
|
+
}
|
|
3020
|
+
function buildTooltip(span, escapeHtml2) {
|
|
3021
|
+
const parts = [];
|
|
3022
|
+
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
3023
|
+
if (span.statusMessage) {
|
|
3024
|
+
parts.push(`Status: ${span.statusMessage}`);
|
|
3025
|
+
}
|
|
3026
|
+
if (span.attributes) {
|
|
3027
|
+
const keys = Object.keys(span.attributes).sort();
|
|
3028
|
+
for (const key of keys) {
|
|
3029
|
+
const val = span.attributes[key];
|
|
3030
|
+
const formatted = Array.isArray(val) ? `[${val.map((v) => String(v)).join(", ")}]` : String(val);
|
|
3031
|
+
parts.push(`${key}=${formatted}`);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
let text = parts.join("\n");
|
|
3035
|
+
if (text.length > TOOLTIP_MAX_LENGTH) {
|
|
3036
|
+
text = text.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
3037
|
+
}
|
|
3038
|
+
return escapeHtml2(text);
|
|
3039
|
+
}
|
|
3040
|
+
function renderTraceView(args, deps) {
|
|
3041
|
+
if (!args.spans || args.spans.length === 0) return "";
|
|
3042
|
+
const normalized = normalizeSpans(args.spans);
|
|
3043
|
+
if (normalized.length === 0) return "";
|
|
3044
|
+
const roots = buildTree(normalized);
|
|
3045
|
+
const flat = flattenTree(roots);
|
|
3046
|
+
let minStart = Infinity;
|
|
3047
|
+
let maxEnd = -Infinity;
|
|
3048
|
+
for (const node of flat) {
|
|
3049
|
+
const s = node.span.startTimeMs;
|
|
3050
|
+
const e = s + node.span.durationMs;
|
|
3051
|
+
if (s < minStart) minStart = s;
|
|
3052
|
+
if (e > maxEnd) maxEnd = e;
|
|
3053
|
+
}
|
|
3054
|
+
let totalDuration = maxEnd - minStart;
|
|
3055
|
+
if (totalDuration <= 0) totalDuration = 1;
|
|
3056
|
+
const rows = flat.map((node) => {
|
|
3057
|
+
const { span, depth } = node;
|
|
3058
|
+
const indent = depth * 16;
|
|
3059
|
+
const minWidth = 0.5;
|
|
3060
|
+
let spanLeft = clamp(
|
|
3061
|
+
(span.startTimeMs - minStart) / totalDuration * 100,
|
|
3062
|
+
0,
|
|
3063
|
+
100
|
|
3064
|
+
);
|
|
3065
|
+
if (spanLeft + minWidth > 100) {
|
|
3066
|
+
spanLeft = 100 - minWidth;
|
|
3067
|
+
}
|
|
3068
|
+
const spanWidth = clamp(
|
|
3069
|
+
span.durationMs / totalDuration * 100,
|
|
3070
|
+
minWidth,
|
|
3071
|
+
100 - spanLeft
|
|
3072
|
+
);
|
|
3073
|
+
const tooltip = buildTooltip(span, deps.escapeHtml);
|
|
3074
|
+
const durationLabel = formatDuration(span.durationMs);
|
|
3075
|
+
return ` <div class="trace-view-row">
|
|
3076
|
+
<div class="trace-view-name" style="padding-left: ${indent}px" title="${deps.escapeHtml(span.name)}">
|
|
3077
|
+
<span class="trace-view-status-dot trace-view-status-${span.status}"></span>
|
|
3078
|
+
${deps.escapeHtml(span.name)}
|
|
3079
|
+
</div>
|
|
3080
|
+
<div class="trace-view-bar-container">
|
|
3081
|
+
<div class="trace-view-bar trace-view-bar-${span.status}" style="left: ${spanLeft.toFixed(2)}%; width: ${spanWidth.toFixed(2)}%" title="${tooltip}">${durationLabel}</div>
|
|
3082
|
+
</div>
|
|
3083
|
+
</div>`;
|
|
3084
|
+
}).join("\n");
|
|
3085
|
+
const axisEnd = formatDuration(maxEnd - minStart);
|
|
3086
|
+
return `<div class="trace-view collapsed">
|
|
3087
|
+
<div class="trace-view-header" role="button" tabindex="0" aria-expanded="false">
|
|
3088
|
+
<span>Spans</span>
|
|
3089
|
+
<span class="trace-view-count">${flat.length}</span>
|
|
3090
|
+
<span class="chevron">▼</span>
|
|
3091
|
+
</div>
|
|
3092
|
+
<div class="trace-view-content">
|
|
3093
|
+
<div class="trace-view-axis">
|
|
3094
|
+
<span>0ms</span>
|
|
3095
|
+
<span>${axisEnd}</span>
|
|
3096
|
+
</div>
|
|
3097
|
+
${rows}
|
|
2721
3098
|
</div>
|
|
2722
3099
|
</div>`;
|
|
2723
3100
|
}
|
|
@@ -2843,7 +3220,8 @@ function createHtmlFormatter(options = {}) {
|
|
|
2843
3220
|
const stepsDeps = {
|
|
2844
3221
|
escapeHtml,
|
|
2845
3222
|
getStatusIcon,
|
|
2846
|
-
renderDocs
|
|
3223
|
+
renderDocs,
|
|
3224
|
+
highlightStepParams: (text) => highlightStepParams(text, { escapeHtml })
|
|
2847
3225
|
};
|
|
2848
3226
|
const scenarioDeps = {
|
|
2849
3227
|
escapeHtml,
|
|
@@ -2853,6 +3231,7 @@ function createHtmlFormatter(options = {}) {
|
|
|
2853
3231
|
renderDocs,
|
|
2854
3232
|
renderErrorBox: (args, d) => renderErrorBox(args, d),
|
|
2855
3233
|
renderAttachments: (args, d) => renderAttachments(args, d),
|
|
3234
|
+
renderTraceView: (args, d) => renderTraceView(args, d),
|
|
2856
3235
|
embedScreenshots: opts.embedScreenshots
|
|
2857
3236
|
};
|
|
2858
3237
|
const featureDeps = {
|
|
@@ -3145,6 +3524,7 @@ var MarkdownFormatter = class {
|
|
|
3145
3524
|
includeSummaryTable: options.includeSummaryTable ?? false,
|
|
3146
3525
|
permalinkBaseUrl: options.permalinkBaseUrl,
|
|
3147
3526
|
ticketUrlTemplate: options.ticketUrlTemplate,
|
|
3527
|
+
traceUrlTemplate: options.traceUrlTemplate,
|
|
3148
3528
|
includeSourceLinks: options.includeSourceLinks ?? true,
|
|
3149
3529
|
customRenderers: options.customRenderers
|
|
3150
3530
|
};
|
|
@@ -3367,6 +3747,18 @@ var MarkdownFormatter = class {
|
|
|
3367
3747
|
meta.push(`Tickets: ${tc.story.tickets.map((t) => `\`${t}\``).join(", ")}`);
|
|
3368
3748
|
}
|
|
3369
3749
|
}
|
|
3750
|
+
const otelMeta = tc.story.meta?.otel;
|
|
3751
|
+
if (otelMeta?.traceId) {
|
|
3752
|
+
const traceTemplate = this.options.traceUrlTemplate;
|
|
3753
|
+
if (traceTemplate) {
|
|
3754
|
+
const url = traceTemplate.replace(/\{traceId\}/g, otelMeta.traceId);
|
|
3755
|
+
meta.push(
|
|
3756
|
+
`Trace: [${otelMeta.traceId.slice(0, 16)}\u2026](${url})`
|
|
3757
|
+
);
|
|
3758
|
+
} else {
|
|
3759
|
+
meta.push(`Trace: \`${otelMeta.traceId}\``);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3370
3762
|
if (meta.length > 0) {
|
|
3371
3763
|
lines.push(meta.join(" | "));
|
|
3372
3764
|
}
|
|
@@ -5205,7 +5597,7 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
5205
5597
|
}
|
|
5206
5598
|
|
|
5207
5599
|
// src/utils/duration.ts
|
|
5208
|
-
function
|
|
5600
|
+
function formatDuration2(ms) {
|
|
5209
5601
|
if (ms < 1e3) {
|
|
5210
5602
|
return `${Math.round(ms)} ms`;
|
|
5211
5603
|
}
|
|
@@ -5309,6 +5701,31 @@ function detectCI4(env = process.env) {
|
|
|
5309
5701
|
return void 0;
|
|
5310
5702
|
}
|
|
5311
5703
|
|
|
5704
|
+
// src/utils/otel-detect.ts
|
|
5705
|
+
import { createRequire } from "module";
|
|
5706
|
+
function getRequire() {
|
|
5707
|
+
const url = import.meta.url ?? (typeof __filename !== "undefined" ? `file://${__filename}` : void 0);
|
|
5708
|
+
if (!url) throw new Error("Cannot determine module URL");
|
|
5709
|
+
return createRequire(url);
|
|
5710
|
+
}
|
|
5711
|
+
function tryGetActiveOtelContext() {
|
|
5712
|
+
try {
|
|
5713
|
+
const api = getRequire()("@opentelemetry/api");
|
|
5714
|
+
const span = api.trace?.getActiveSpan?.();
|
|
5715
|
+
if (!span) return void 0;
|
|
5716
|
+
const ctx = span.spanContext?.();
|
|
5717
|
+
if (!ctx?.traceId || ctx.traceId === "00000000000000000000000000000000")
|
|
5718
|
+
return void 0;
|
|
5719
|
+
return { traceId: ctx.traceId, spanId: ctx.spanId };
|
|
5720
|
+
} catch {
|
|
5721
|
+
return void 0;
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
function resolveTraceUrl(template, traceId) {
|
|
5725
|
+
if (!template) return void 0;
|
|
5726
|
+
return template.replace(/\{traceId\}/g, traceId);
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5312
5729
|
// src/index.ts
|
|
5313
5730
|
var FORMAT_EXTENSIONS = {
|
|
5314
5731
|
markdown: ".md",
|
|
@@ -5499,6 +5916,7 @@ var ReportGenerator = class {
|
|
|
5499
5916
|
includeSummaryTable: options.markdown?.includeSummaryTable ?? false,
|
|
5500
5917
|
permalinkBaseUrl: options.markdown?.permalinkBaseUrl,
|
|
5501
5918
|
ticketUrlTemplate: options.markdown?.ticketUrlTemplate,
|
|
5919
|
+
traceUrlTemplate: options.markdown?.traceUrlTemplate,
|
|
5502
5920
|
includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
|
|
5503
5921
|
customRenderers: options.markdown?.customRenderers
|
|
5504
5922
|
}
|
|
@@ -5624,6 +6042,7 @@ var ReportGenerator = class {
|
|
|
5624
6042
|
includeSummaryTable: this.options.markdown.includeSummaryTable,
|
|
5625
6043
|
permalinkBaseUrl: this.options.markdown.permalinkBaseUrl,
|
|
5626
6044
|
ticketUrlTemplate: this.options.markdown.ticketUrlTemplate,
|
|
6045
|
+
traceUrlTemplate: this.options.markdown.traceUrlTemplate,
|
|
5627
6046
|
includeSourceLinks: this.options.markdown.includeSourceLinks,
|
|
5628
6047
|
customRenderers: this.options.markdown.customRenderers
|
|
5629
6048
|
});
|
|
@@ -5668,7 +6087,7 @@ export {
|
|
|
5668
6087
|
deriveStepResults,
|
|
5669
6088
|
detectCI4 as detectCI,
|
|
5670
6089
|
findGitDir,
|
|
5671
|
-
formatDuration,
|
|
6090
|
+
formatDuration2 as formatDuration,
|
|
5672
6091
|
generateRunId,
|
|
5673
6092
|
generateTestCaseId,
|
|
5674
6093
|
mergeStepResults,
|
|
@@ -5685,7 +6104,9 @@ export {
|
|
|
5685
6104
|
readPackageVersion,
|
|
5686
6105
|
resolveAttachment,
|
|
5687
6106
|
resolveAttachments,
|
|
6107
|
+
resolveTraceUrl,
|
|
5688
6108
|
slugify,
|
|
6109
|
+
tryGetActiveOtelContext,
|
|
5689
6110
|
validateCanonicalRun
|
|
5690
6111
|
};
|
|
5691
6112
|
//# sourceMappingURL=index.js.map
|