Merge origin/main into feature/issues-tui
Build and Publish / Metadata (push) Successful in 5s
Build and Publish / BuildAndPushAmd64 (push) Failing after 1m42s

This commit is contained in:
mats committed 2026-11-01 11:40:57 +01:00
commit 7b68f60202
6 files changed
+155 -52

No files matched your search

@@ -183,8 +183,8 @@ export function CspReportsVisualizer() {
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatCard label="Reports in window" value={summary ? summary.total.toLocaleString() : "—"} />
<StatCard label="Sites reporting" value={summary ? summary.sites.toLocaleString() : "—"} />
<StatCard label="Top directive" value={summary?.byDirective[0]?.name ?? "—"} />
<StatCard label="Top blocked host" value={summary?.byBlockedHost[0]?.name ?? "—"} />
<StatCard label="Top directive" value={summary?.byDirective?.[0]?.name ?? "—"} />
<StatCard label="Top blocked host" value={summary?.byBlockedHost?.[0]?.name ?? "—"} />
</div>
<FilterBar
+36 -26
View File
@@ -1,4 +1,4 @@
import { useMemo, useState, useRef, useEffect } from "react";
import { useMemo, useState, useRef, useEffect, useDeferredValue } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import {
Sparkles,
@@ -54,6 +54,8 @@ export type ProcessedTrace = {
endpoint: string;
};
const EMPTY_TRACES: TraceData[] = [];
// Flattened item for virtualized tree rendering
interface FlattenedSpanItem {
span: ProcessedSpan;
@@ -96,14 +98,24 @@ const flattenSpanTree = (
collapsedSet: Set<string>,
traceMinTime: number,
traceTotalRange: number,
depth = 0,
initialDepth = 0,
): FlattenedSpanItem[] => {
const result: FlattenedSpanItem[] = [];
const pending: Array<{ span: ProcessedSpan; depth: number }> = [];
for (const span of nodes) {
const hasChildren = Boolean(span.children && span.children.length > 0);
for (let index = nodes.length - 1; index >= 0; index -= 1) {
pending.push({ span: nodes[index], depth: initialDepth });
}
// Avoid recursively spreading descendant arrays: deep parent/child chains
// otherwise copy the same nodes once per ancestor.
while (pending.length > 0) {
const current = pending.pop();
if (!current) break;
const { span, depth } = current;
const children = span.children ?? [];
const hasChildren = children.length > 0;
const isCollapsed = collapsedSet.has(span.spanId);
const offsetPercent =
traceTotalRange === 0 ? 0 : ((span.startTime - traceMinTime) / traceTotalRange) * 100;
const widthPercent =
@@ -118,16 +130,10 @@ const flattenSpanTree = (
widthPercent: Math.max(0.5, Math.min(widthPercent, 100 - offsetPercent)),
});
if (hasChildren && !isCollapsed && span.children) {
result.push(
...flattenSpanTree(
span.children,
collapsedSet,
traceMinTime,
traceTotalRange,
depth + 1,
),
);
if (hasChildren && !isCollapsed) {
for (let index = children.length - 1; index >= 0; index -= 1) {
pending.push({ span: children[index], depth: depth + 1 });
}
}
}
@@ -539,6 +545,7 @@ const VirtualizedSpanWaterfall = ({
const [searchQuery, setSearchQuery] = useState("");
const [errorsOnly, setErrorsOnly] = useState(false);
const [minDurationMsFilter, setMinDurationMsFilter] = useState<number>(0);
const deferredSearchQuery = useDeferredValue(searchQuery);
const parentRef = useRef<HTMLDivElement>(null);
// Build tree
@@ -563,8 +570,8 @@ const VirtualizedSpanWaterfall = ({
const durMs = item.span.duration / 1_000_000;
if (durMs < minDurationMsFilter) return false;
}
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim();
if (deferredSearchQuery.trim()) {
const query = deferredSearchQuery.toLowerCase().trim();
const matchesName = item.span.name.toLowerCase().includes(query);
const matchesService = item.span.service.toLowerCase().includes(query);
const matchesAttrs = Object.values(item.span.formattedAttributes).some((v) =>
@@ -574,7 +581,7 @@ const VirtualizedSpanWaterfall = ({
}
return true;
});
}, [flattenedItems, filterService, errorsOnly, minDurationMsFilter, searchQuery]);
}, [flattenedItems, filterService, errorsOnly, minDurationMsFilter, deferredSearchQuery]);
// Virtualizer
@@ -1683,10 +1690,12 @@ export const processTraces = (
),
);
const uniqueSpans = spanAccumulator.filter(
(span, index, arr) =>
arr.findIndex((candidate) => candidate.spanId === span.spanId) === index,
);
const seenSpanIds = new Set<string>();
const uniqueSpans = spanAccumulator.filter((span) => {
if (seenSpanIds.has(span.spanId)) return false;
seenSpanIds.add(span.spanId);
return true;
});
const traceGroups = uniqueSpans.reduce<Record<string, ProcessedSpan[]>>((acc, span) => {
if (!acc[span.traceId]) acc[span.traceId] = [];
@@ -1739,7 +1748,7 @@ export interface TraceVisualizationProps {
}
export const TraceVisualization = ({ traces }: TraceVisualizationProps) => {
const safeTraces = traces ?? [];
const safeTraces = traces ?? EMPTY_TRACES;
const {
logCountBySpanId,
activeTraceId,
@@ -1756,6 +1765,7 @@ export const TraceVisualization = ({ traces }: TraceVisualizationProps) => {
"recent" | "duration-desc" | "duration-asc" | "errors" | "spans"
>("recent");
const [groupByEndpoint, setGroupByEndpoint] = useState(false);
const deferredTraceSearchQuery = useDeferredValue(traceSearchQuery);
const orderedTraces: ProcessedTrace[] = useMemo(
() => processTraces(safeTraces, logCountBySpanId),
@@ -1774,8 +1784,8 @@ export const TraceVisualization = ({ traces }: TraceVisualizationProps) => {
if (traceFilter === "errors" && !t.hasError) return false;
if (traceFilter === "slow" && t.duration / 1_000_000 < 200) return false;
if (traceServiceFilter !== "all" && !t.services.includes(traceServiceFilter)) return false;
if (traceSearchQuery.trim()) {
const q = traceSearchQuery.toLowerCase().trim();
if (deferredTraceSearchQuery.trim()) {
const q = deferredTraceSearchQuery.toLowerCase().trim();
const matchName = t.name.toLowerCase().includes(q);
const matchId = t.id.toLowerCase().includes(q);
const matchService = t.services.some((s) => s.toLowerCase().includes(q));
@@ -1791,7 +1801,7 @@ export const TraceVisualization = ({ traces }: TraceVisualizationProps) => {
if (traceSortBy === "spans") return b.spans.length - a.spans.length;
return b.startTime - a.startTime; // recent
});
}, [orderedTraces, traceFilter, traceServiceFilter, traceSearchQuery, traceSortBy]);
}, [orderedTraces, traceFilter, traceServiceFilter, deferredTraceSearchQuery, traceSortBy]);
const groupCounts = useMemo(() => {
const counts: Record<string, number> = {};