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/dist/index.cjs CHANGED
@@ -48,7 +48,7 @@ __export(src_exports, {
48
48
  deriveStepResults: () => deriveStepResults,
49
49
  detectCI: () => detectCI4,
50
50
  findGitDir: () => findGitDir,
51
- formatDuration: () => formatDuration,
51
+ formatDuration: () => formatDuration2,
52
52
  generateRunId: () => generateRunId,
53
53
  generateTestCaseId: () => generateTestCaseId,
54
54
  mergeStepResults: () => mergeStepResults,
@@ -65,7 +65,9 @@ __export(src_exports, {
65
65
  readPackageVersion: () => readPackageVersion,
66
66
  resolveAttachment: () => resolveAttachment,
67
67
  resolveAttachments: () => resolveAttachments,
68
+ resolveTraceUrl: () => resolveTraceUrl,
68
69
  slugify: () => slugify,
70
+ tryGetActiveOtelContext: () => tryGetActiveOtelContext,
69
71
  validateCanonicalRun: () => validateCanonicalRun
70
72
  });
71
73
  module.exports = __toCommonJS(src_exports);
@@ -953,20 +955,32 @@ function initCollapse() {
953
955
  }
954
956
  });
955
957
  });
958
+
959
+ document.querySelectorAll('.trace-view-header').forEach(header => {
960
+ header.addEventListener('click', () => {
961
+ toggleCollapse(header, header.closest('.trace-view'));
962
+ });
963
+ header.addEventListener('keydown', (e) => {
964
+ if (e.key === 'Enter' || e.key === ' ') {
965
+ e.preventDefault();
966
+ toggleCollapse(header, header.closest('.trace-view'));
967
+ }
968
+ });
969
+ });
956
970
  }
957
971
 
958
972
  function expandAll() {
959
- document.querySelectorAll('.feature, .scenario').forEach(el => {
973
+ document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
960
974
  el.classList.remove('collapsed');
961
- const header = el.querySelector('.feature-header, .scenario-header');
975
+ const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
962
976
  header?.setAttribute('aria-expanded', 'true');
963
977
  });
964
978
  }
965
979
 
966
980
  function collapseAll() {
967
- document.querySelectorAll('.feature, .scenario').forEach(el => {
981
+ document.querySelectorAll('.feature, .scenario, .trace-view').forEach(el => {
968
982
  el.classList.add('collapsed');
969
- const header = el.querySelector('.feature-header, .scenario-header');
983
+ const header = el.querySelector('.feature-header, .scenario-header, .trace-view-header');
970
984
  header?.setAttribute('aria-expanded', 'false');
971
985
  });
972
986
  }
@@ -1156,6 +1170,7 @@ var CSS_STYLES = `
1156
1170
  --tag-bg: hsl(145 55% 95%);
1157
1171
  --tag-color: hsl(145 63% 30%);
1158
1172
  --tag-border: hsl(145 55% 85%);
1173
+ --step-param-color: hsl(220 70% 50%);
1159
1174
 
1160
1175
  /* Accordion/Collapsible styling */
1161
1176
  --accordion-header-hover: hsl(0 0% 98%);
@@ -1214,6 +1229,7 @@ var CSS_STYLES = `
1214
1229
  --tag-bg: hsl(145 35% 14%);
1215
1230
  --tag-color: hsl(145 63% 60%);
1216
1231
  --tag-border: hsl(145 35% 22%);
1232
+ --step-param-color: hsl(220 70% 70%);
1217
1233
 
1218
1234
  /* Accordion/Collapsible styling */
1219
1235
  --accordion-header-hover: hsl(0 0% 11%);
@@ -1262,6 +1278,7 @@ var CSS_STYLES = `
1262
1278
  --tag-bg: hsl(145 35% 14%);
1263
1279
  --tag-color: hsl(145 63% 60%);
1264
1280
  --tag-border: hsl(145 35% 22%);
1281
+ --step-param-color: hsl(220 70% 70%);
1265
1282
  --accordion-header-hover: hsl(0 0% 11%);
1266
1283
  --accordion-content-bg: hsl(0 0% 7%);
1267
1284
  }
@@ -1807,6 +1824,12 @@ body {
1807
1824
  color: var(--foreground);
1808
1825
  }
1809
1826
 
1827
+ .step-param {
1828
+ font-style: italic;
1829
+ font-weight: 500;
1830
+ color: var(--step-param-color);
1831
+ }
1832
+
1810
1833
  .step-duration {
1811
1834
  color: var(--muted-foreground);
1812
1835
  font-size: 0.6875rem;
@@ -2483,6 +2506,133 @@ body {
2483
2506
  font-family: inherit;
2484
2507
  background: none;
2485
2508
  }
2509
+
2510
+ /* ============================================================================
2511
+ Trace View - OTel span waterfall
2512
+ ============================================================================ */
2513
+ .trace-view {
2514
+ margin-top: 0.75rem;
2515
+ border: 1px solid var(--border);
2516
+ border-radius: calc(var(--radius) - 2px);
2517
+ overflow: hidden;
2518
+ }
2519
+
2520
+ .trace-view-header {
2521
+ display: flex;
2522
+ align-items: center;
2523
+ gap: 0.5rem;
2524
+ padding: 0.5rem 0.75rem;
2525
+ background: var(--card);
2526
+ cursor: pointer;
2527
+ user-select: none;
2528
+ font-size: 0.8125rem;
2529
+ font-weight: 500;
2530
+ color: var(--foreground);
2531
+ transition: background-color 0.15s ease;
2532
+ }
2533
+
2534
+ .trace-view-header:hover {
2535
+ background: var(--accordion-header-hover);
2536
+ }
2537
+
2538
+ .trace-view-count {
2539
+ font-size: 0.6875rem;
2540
+ font-weight: 500;
2541
+ padding: 0.125rem 0.5rem;
2542
+ background: var(--success-light);
2543
+ color: var(--success);
2544
+ border: 1px solid var(--success-border);
2545
+ border-radius: 9999px;
2546
+ font-family: var(--font-mono);
2547
+ }
2548
+
2549
+ .trace-view-content {
2550
+ border-top: 1px solid var(--border);
2551
+ padding: 0.5rem 0.75rem;
2552
+ background: var(--accordion-content-bg);
2553
+ }
2554
+
2555
+ .trace-view.collapsed .trace-view-content {
2556
+ display: none;
2557
+ }
2558
+
2559
+ .trace-view-axis {
2560
+ display: flex;
2561
+ justify-content: space-between;
2562
+ font-size: 0.625rem;
2563
+ font-family: var(--font-mono);
2564
+ color: var(--muted-foreground);
2565
+ padding-bottom: 0.375rem;
2566
+ margin-bottom: 0.375rem;
2567
+ border-bottom: 1px solid var(--border);
2568
+ }
2569
+
2570
+ .trace-view-row {
2571
+ display: flex;
2572
+ align-items: center;
2573
+ gap: 0.5rem;
2574
+ padding: 0.1875rem 0;
2575
+ font-size: 0.75rem;
2576
+ }
2577
+
2578
+ .trace-view-name {
2579
+ width: 35%;
2580
+ flex-shrink: 0;
2581
+ display: flex;
2582
+ align-items: center;
2583
+ gap: 0.375rem;
2584
+ font-family: var(--font-mono);
2585
+ white-space: nowrap;
2586
+ overflow: hidden;
2587
+ text-overflow: ellipsis;
2588
+ color: var(--foreground);
2589
+ }
2590
+
2591
+ .trace-view-status-dot {
2592
+ width: 8px;
2593
+ height: 8px;
2594
+ border-radius: 50%;
2595
+ flex-shrink: 0;
2596
+ }
2597
+
2598
+ .trace-view-status-ok { background: var(--success); }
2599
+ .trace-view-status-error { background: var(--error); }
2600
+ .trace-view-status-unset { background: var(--muted-foreground); }
2601
+
2602
+ .trace-view-bar-container {
2603
+ flex: 1;
2604
+ position: relative;
2605
+ height: 1.25rem;
2606
+ background: var(--muted);
2607
+ border-radius: 2px;
2608
+ }
2609
+
2610
+ .trace-view-bar {
2611
+ position: absolute;
2612
+ top: 0;
2613
+ height: 100%;
2614
+ border-radius: 2px;
2615
+ min-width: 2px;
2616
+ display: flex;
2617
+ align-items: center;
2618
+ padding: 0 0.375rem;
2619
+ font-size: 0.625rem;
2620
+ font-family: var(--font-mono);
2621
+ color: white;
2622
+ white-space: nowrap;
2623
+ overflow: hidden;
2624
+ }
2625
+
2626
+ .trace-view-bar-ok { background: var(--success); }
2627
+ .trace-view-bar-error { background: var(--error); }
2628
+ .trace-view-bar-unset { background: var(--muted-foreground); }
2629
+
2630
+ @media print {
2631
+ .trace-view.collapsed .trace-view-content {
2632
+ display: block;
2633
+ }
2634
+ }
2635
+
2486
2636
  `;
2487
2637
 
2488
2638
  // src/formatters/html/renderers/status.ts
@@ -2727,10 +2877,11 @@ function renderStep(step, stepResult, index, deps) {
2727
2877
  const isContinuation = CONTINUATION_KEYWORDS.includes(keywordTrimmed);
2728
2878
  const stepClass = isContinuation ? "step continuation" : "step";
2729
2879
  const stepDocs = deps.renderDocs(step.docs, "step-docs");
2880
+ const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
2730
2881
  return `<div class="${stepClass}">
2731
2882
  <span class="step-status ${statusClass}">${statusIcon}</span>
2732
2883
  <span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
2733
- <span class="step-text">${deps.escapeHtml(step.text)}</span>
2884
+ <span class="step-text">${textHtml}</span>
2734
2885
  <span class="step-duration">${duration}</span>
2735
2886
  </div>${stepDocs}`;
2736
2887
  }
@@ -2742,6 +2893,30 @@ function renderSteps(args, deps) {
2742
2893
  return `<div class="steps">${stepsHtml}</div>`;
2743
2894
  }
2744
2895
 
2896
+ // src/formatters/html/renderers/step-params.ts
2897
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.\-])\d+(?:\.\d+)?(?![\w.\-])/g;
2898
+ function highlightStepParams(text, deps) {
2899
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
2900
+ if (matches.length === 0) {
2901
+ return deps.escapeHtml(text);
2902
+ }
2903
+ let result = "";
2904
+ let lastIndex = 0;
2905
+ for (const match of matches) {
2906
+ const matchStart = match.index;
2907
+ const matchEnd = matchStart + match[0].length;
2908
+ if (matchStart > lastIndex) {
2909
+ result += deps.escapeHtml(text.slice(lastIndex, matchStart));
2910
+ }
2911
+ result += `<span class="step-param">${deps.escapeHtml(match[0])}</span>`;
2912
+ lastIndex = matchEnd;
2913
+ }
2914
+ if (lastIndex < text.length) {
2915
+ result += deps.escapeHtml(text.slice(lastIndex));
2916
+ }
2917
+ return result;
2918
+ }
2919
+
2745
2920
  // src/formatters/html/renderers/scenario.ts
2746
2921
  function renderScenario(args, deps) {
2747
2922
  const { tc } = args;
@@ -2749,6 +2924,19 @@ function renderScenario(args, deps) {
2749
2924
  const statusClass = `status-${tc.status}`;
2750
2925
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
2751
2926
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
2927
+ const otelMeta = tc.story.meta?.otel;
2928
+ let traceBadge = "";
2929
+ if (otelMeta?.traceId) {
2930
+ const shortId = otelMeta.traceId.slice(0, 16);
2931
+ const traceLink = tc.story.docs?.find(
2932
+ (d) => d.kind === "link" && d.label === "View Trace"
2933
+ );
2934
+ if (traceLink) {
2935
+ 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>`;
2936
+ } else {
2937
+ traceBadge = `<span class="tag trace-tag" title="${deps.escapeHtml(otelMeta.traceId)}">${deps.escapeHtml(shortId)}\u2026</span>`;
2938
+ }
2939
+ }
2752
2940
  const storyDocs = deps.renderDocs(tc.story.docs, "story-docs");
2753
2941
  const steps = deps.renderSteps(
2754
2942
  { steps: tc.story.steps, stepResults: tc.stepResults },
@@ -2769,6 +2957,10 @@ function renderScenario(args, deps) {
2769
2957
  embedScreenshots: deps.embedScreenshots
2770
2958
  }
2771
2959
  );
2960
+ const traceView = deps.renderTraceView(
2961
+ { spans: tc.story.otelSpans },
2962
+ { escapeHtml: deps.escapeHtml }
2963
+ );
2772
2964
  const collapsedClass = deps.startCollapsed ? " collapsed" : "";
2773
2965
  const ariaExpanded = !deps.startCollapsed;
2774
2966
  return `
@@ -2779,7 +2971,7 @@ function renderScenario(args, deps) {
2779
2971
  <span class="status-icon ${statusClass}">${statusIcon}</span>
2780
2972
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
2781
2973
  </div>
2782
- <div class="scenario-meta">${tags}</div>
2974
+ <div class="scenario-meta">${tags}${traceBadge}</div>
2783
2975
  </div>
2784
2976
  <span class="scenario-duration">${duration}</span>
2785
2977
  </div>
@@ -2788,6 +2980,193 @@ function renderScenario(args, deps) {
2788
2980
  ${steps}
2789
2981
  ${error}
2790
2982
  ${attachments}
2983
+ ${traceView}
2984
+ </div>
2985
+ </div>`;
2986
+ }
2987
+
2988
+ // src/formatters/html/renderers/trace-view.ts
2989
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
2990
+ var TOOLTIP_MAX_LENGTH = 4096;
2991
+ function safeStatus(status) {
2992
+ return VALID_STATUSES.has(status) ? status : "unset";
2993
+ }
2994
+ function formatDuration(ms) {
2995
+ if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
2996
+ return `${ms.toFixed(1)}ms`;
2997
+ }
2998
+ function clamp(value, min, max) {
2999
+ return Math.min(max, Math.max(min, value));
3000
+ }
3001
+ function normalizeSpans(spans) {
3002
+ const result = [];
3003
+ for (const span of spans) {
3004
+ if (!span || typeof span !== "object") continue;
3005
+ if (typeof span.spanId !== "string" || typeof span.name !== "string") continue;
3006
+ let startTimeMs;
3007
+ let durationMs;
3008
+ if (span.startTimeMs != null && span.durationMs != null) {
3009
+ startTimeMs = span.startTimeMs;
3010
+ durationMs = span.durationMs;
3011
+ } else if (span.startTimeUnixNano != null && span.endTimeUnixNano != null) {
3012
+ startTimeMs = span.startTimeUnixNano / 1e6;
3013
+ durationMs = (span.endTimeUnixNano - span.startTimeUnixNano) / 1e6;
3014
+ } else {
3015
+ continue;
3016
+ }
3017
+ durationMs = Math.max(0, durationMs);
3018
+ if (!isFinite(startTimeMs) || !isFinite(durationMs)) continue;
3019
+ result.push({
3020
+ spanId: span.spanId,
3021
+ parentSpanId: span.parentSpanId,
3022
+ name: span.name,
3023
+ startTimeMs,
3024
+ durationMs,
3025
+ status: safeStatus(span.status),
3026
+ statusMessage: span.statusMessage,
3027
+ attributes: span.attributes
3028
+ });
3029
+ }
3030
+ return result;
3031
+ }
3032
+ function buildTree(spans) {
3033
+ const byId = /* @__PURE__ */ new Map();
3034
+ for (const span of spans) {
3035
+ let key = span.spanId;
3036
+ if (byId.has(key)) {
3037
+ let suffix = 2;
3038
+ while (byId.has(`${span.spanId}__dup${suffix}`)) suffix++;
3039
+ key = `${span.spanId}__dup${suffix}`;
3040
+ }
3041
+ byId.set(key, { span: { ...span, spanId: key }, children: [], depth: 0 });
3042
+ }
3043
+ const roots = [];
3044
+ for (const node of byId.values()) {
3045
+ const parentId = node.span.parentSpanId;
3046
+ const parent = parentId ? byId.get(parentId) : void 0;
3047
+ if (parent && parent !== node) {
3048
+ parent.children.push(node);
3049
+ } else {
3050
+ roots.push(node);
3051
+ }
3052
+ }
3053
+ for (const node of byId.values()) {
3054
+ node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
3055
+ }
3056
+ roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
3057
+ const visited = /* @__PURE__ */ new Set();
3058
+ function assignDepth(node, depth) {
3059
+ if (visited.has(node.span.spanId)) return;
3060
+ visited.add(node.span.spanId);
3061
+ node.depth = depth;
3062
+ for (const child of node.children) {
3063
+ assignDepth(child, depth + 1);
3064
+ }
3065
+ }
3066
+ for (const root of roots) {
3067
+ assignDepth(root, 0);
3068
+ }
3069
+ for (const node of byId.values()) {
3070
+ if (!visited.has(node.span.spanId)) {
3071
+ node.children = [];
3072
+ roots.push(node);
3073
+ assignDepth(node, 0);
3074
+ }
3075
+ }
3076
+ roots.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
3077
+ return roots;
3078
+ }
3079
+ function flattenTree(roots) {
3080
+ const result = [];
3081
+ function walk(node) {
3082
+ result.push(node);
3083
+ for (const child of node.children) {
3084
+ walk(child);
3085
+ }
3086
+ }
3087
+ for (const root of roots) {
3088
+ walk(root);
3089
+ }
3090
+ return result;
3091
+ }
3092
+ function buildTooltip(span, escapeHtml2) {
3093
+ const parts = [];
3094
+ parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
3095
+ if (span.statusMessage) {
3096
+ parts.push(`Status: ${span.statusMessage}`);
3097
+ }
3098
+ if (span.attributes) {
3099
+ const keys = Object.keys(span.attributes).sort();
3100
+ for (const key of keys) {
3101
+ const val = span.attributes[key];
3102
+ const formatted = Array.isArray(val) ? `[${val.map((v) => String(v)).join(", ")}]` : String(val);
3103
+ parts.push(`${key}=${formatted}`);
3104
+ }
3105
+ }
3106
+ let text = parts.join("\n");
3107
+ if (text.length > TOOLTIP_MAX_LENGTH) {
3108
+ text = text.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
3109
+ }
3110
+ return escapeHtml2(text);
3111
+ }
3112
+ function renderTraceView(args, deps) {
3113
+ if (!args.spans || args.spans.length === 0) return "";
3114
+ const normalized = normalizeSpans(args.spans);
3115
+ if (normalized.length === 0) return "";
3116
+ const roots = buildTree(normalized);
3117
+ const flat = flattenTree(roots);
3118
+ let minStart = Infinity;
3119
+ let maxEnd = -Infinity;
3120
+ for (const node of flat) {
3121
+ const s = node.span.startTimeMs;
3122
+ const e = s + node.span.durationMs;
3123
+ if (s < minStart) minStart = s;
3124
+ if (e > maxEnd) maxEnd = e;
3125
+ }
3126
+ let totalDuration = maxEnd - minStart;
3127
+ if (totalDuration <= 0) totalDuration = 1;
3128
+ const rows = flat.map((node) => {
3129
+ const { span, depth } = node;
3130
+ const indent = depth * 16;
3131
+ const minWidth = 0.5;
3132
+ let spanLeft = clamp(
3133
+ (span.startTimeMs - minStart) / totalDuration * 100,
3134
+ 0,
3135
+ 100
3136
+ );
3137
+ if (spanLeft + minWidth > 100) {
3138
+ spanLeft = 100 - minWidth;
3139
+ }
3140
+ const spanWidth = clamp(
3141
+ span.durationMs / totalDuration * 100,
3142
+ minWidth,
3143
+ 100 - spanLeft
3144
+ );
3145
+ const tooltip = buildTooltip(span, deps.escapeHtml);
3146
+ const durationLabel = formatDuration(span.durationMs);
3147
+ return ` <div class="trace-view-row">
3148
+ <div class="trace-view-name" style="padding-left: ${indent}px" title="${deps.escapeHtml(span.name)}">
3149
+ <span class="trace-view-status-dot trace-view-status-${span.status}"></span>
3150
+ ${deps.escapeHtml(span.name)}
3151
+ </div>
3152
+ <div class="trace-view-bar-container">
3153
+ <div class="trace-view-bar trace-view-bar-${span.status}" style="left: ${spanLeft.toFixed(2)}%; width: ${spanWidth.toFixed(2)}%" title="${tooltip}">${durationLabel}</div>
3154
+ </div>
3155
+ </div>`;
3156
+ }).join("\n");
3157
+ const axisEnd = formatDuration(maxEnd - minStart);
3158
+ return `<div class="trace-view collapsed">
3159
+ <div class="trace-view-header" role="button" tabindex="0" aria-expanded="false">
3160
+ <span>Spans</span>
3161
+ <span class="trace-view-count">${flat.length}</span>
3162
+ <span class="chevron">&#9660;</span>
3163
+ </div>
3164
+ <div class="trace-view-content">
3165
+ <div class="trace-view-axis">
3166
+ <span>0ms</span>
3167
+ <span>${axisEnd}</span>
3168
+ </div>
3169
+ ${rows}
2791
3170
  </div>
2792
3171
  </div>`;
2793
3172
  }
@@ -2913,7 +3292,8 @@ function createHtmlFormatter(options = {}) {
2913
3292
  const stepsDeps = {
2914
3293
  escapeHtml,
2915
3294
  getStatusIcon,
2916
- renderDocs
3295
+ renderDocs,
3296
+ highlightStepParams: (text) => highlightStepParams(text, { escapeHtml })
2917
3297
  };
2918
3298
  const scenarioDeps = {
2919
3299
  escapeHtml,
@@ -2923,6 +3303,7 @@ function createHtmlFormatter(options = {}) {
2923
3303
  renderDocs,
2924
3304
  renderErrorBox: (args, d) => renderErrorBox(args, d),
2925
3305
  renderAttachments: (args, d) => renderAttachments(args, d),
3306
+ renderTraceView: (args, d) => renderTraceView(args, d),
2926
3307
  embedScreenshots: opts.embedScreenshots
2927
3308
  };
2928
3309
  const featureDeps = {
@@ -3215,6 +3596,7 @@ var MarkdownFormatter = class {
3215
3596
  includeSummaryTable: options.includeSummaryTable ?? false,
3216
3597
  permalinkBaseUrl: options.permalinkBaseUrl,
3217
3598
  ticketUrlTemplate: options.ticketUrlTemplate,
3599
+ traceUrlTemplate: options.traceUrlTemplate,
3218
3600
  includeSourceLinks: options.includeSourceLinks ?? true,
3219
3601
  customRenderers: options.customRenderers
3220
3602
  };
@@ -3437,6 +3819,18 @@ var MarkdownFormatter = class {
3437
3819
  meta.push(`Tickets: ${tc.story.tickets.map((t) => `\`${t}\``).join(", ")}`);
3438
3820
  }
3439
3821
  }
3822
+ const otelMeta = tc.story.meta?.otel;
3823
+ if (otelMeta?.traceId) {
3824
+ const traceTemplate = this.options.traceUrlTemplate;
3825
+ if (traceTemplate) {
3826
+ const url = traceTemplate.replace(/\{traceId\}/g, otelMeta.traceId);
3827
+ meta.push(
3828
+ `Trace: [${otelMeta.traceId.slice(0, 16)}\u2026](${url})`
3829
+ );
3830
+ } else {
3831
+ meta.push(`Trace: \`${otelMeta.traceId}\``);
3832
+ }
3833
+ }
3440
3834
  if (meta.length > 0) {
3441
3835
  lines.push(meta.join(" | "));
3442
3836
  }
@@ -5275,7 +5669,7 @@ function readBranchName(cwd = process.cwd()) {
5275
5669
  }
5276
5670
 
5277
5671
  // src/utils/duration.ts
5278
- function formatDuration(ms) {
5672
+ function formatDuration2(ms) {
5279
5673
  if (ms < 1e3) {
5280
5674
  return `${Math.round(ms)} ms`;
5281
5675
  }
@@ -5379,6 +5773,32 @@ function detectCI4(env = process.env) {
5379
5773
  return void 0;
5380
5774
  }
5381
5775
 
5776
+ // src/utils/otel-detect.ts
5777
+ var import_node_module = require("module");
5778
+ var import_meta2 = {};
5779
+ function getRequire() {
5780
+ const url = import_meta2.url ?? (typeof __filename !== "undefined" ? `file://${__filename}` : void 0);
5781
+ if (!url) throw new Error("Cannot determine module URL");
5782
+ return (0, import_node_module.createRequire)(url);
5783
+ }
5784
+ function tryGetActiveOtelContext() {
5785
+ try {
5786
+ const api = getRequire()("@opentelemetry/api");
5787
+ const span = api.trace?.getActiveSpan?.();
5788
+ if (!span) return void 0;
5789
+ const ctx = span.spanContext?.();
5790
+ if (!ctx?.traceId || ctx.traceId === "00000000000000000000000000000000")
5791
+ return void 0;
5792
+ return { traceId: ctx.traceId, spanId: ctx.spanId };
5793
+ } catch {
5794
+ return void 0;
5795
+ }
5796
+ }
5797
+ function resolveTraceUrl(template, traceId) {
5798
+ if (!template) return void 0;
5799
+ return template.replace(/\{traceId\}/g, traceId);
5800
+ }
5801
+
5382
5802
  // src/index.ts
5383
5803
  var FORMAT_EXTENSIONS = {
5384
5804
  markdown: ".md",
@@ -5569,6 +5989,7 @@ var ReportGenerator = class {
5569
5989
  includeSummaryTable: options.markdown?.includeSummaryTable ?? false,
5570
5990
  permalinkBaseUrl: options.markdown?.permalinkBaseUrl,
5571
5991
  ticketUrlTemplate: options.markdown?.ticketUrlTemplate,
5992
+ traceUrlTemplate: options.markdown?.traceUrlTemplate,
5572
5993
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
5573
5994
  customRenderers: options.markdown?.customRenderers
5574
5995
  }
@@ -5694,6 +6115,7 @@ var ReportGenerator = class {
5694
6115
  includeSummaryTable: this.options.markdown.includeSummaryTable,
5695
6116
  permalinkBaseUrl: this.options.markdown.permalinkBaseUrl,
5696
6117
  ticketUrlTemplate: this.options.markdown.ticketUrlTemplate,
6118
+ traceUrlTemplate: this.options.markdown.traceUrlTemplate,
5697
6119
  includeSourceLinks: this.options.markdown.includeSourceLinks,
5698
6120
  customRenderers: this.options.markdown.customRenderers
5699
6121
  });
@@ -5756,7 +6178,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
5756
6178
  readPackageVersion,
5757
6179
  resolveAttachment,
5758
6180
  resolveAttachments,
6181
+ resolveTraceUrl,
5759
6182
  slugify,
6183
+ tryGetActiveOtelContext,
5760
6184
  validateCanonicalRun
5761
6185
  });
5762
6186
  //# sourceMappingURL=index.cjs.map