diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 6b615168c122..7de1582b2b10 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -607,34 +607,56 @@ export function waitForStreamedSpans( } /** - * Accumulate streamed Span V2 spans across multiple envelopes until `isDone` returns true. + * Accumulate streamed Span V2 spans across envelopes, grouped by trace, and resolve with the spans + * of the first trace that satisfies `isDone`. * - * Unlike {@link waitForStreamedSpans}, which resolves with the spans of a single envelope, this - * collects spans from every Span V2 envelope as they arrive and resolves with the full set once - * `isDone` is satisfied. Streamed spans are flushed in multiple envelopes as they end (a child span - * can be sent before its root segment span), so any assertion that needs the whole trace must - * accumulate rather than snapshot a single envelope. + * A trace reaches the proxy in more than one envelope: the span buffer flushes on a timer, so a + * segment that is still open when its children flush arrives separately, and standalone spans (web + * vitals, INP) bypass the buffer entirely. Anything asserting on a whole trace therefore has to + * accumulate rather than snapshot a single envelope, which is what {@link waitForStreamedSpans} + * gives you. * - * `isDone` receives all spans collected so far. A common predicate is "the segment/root span has - * arrived", since the root ends last and therefore flushes after its children: + * `isDone` receives one trace's spans at a time, never a mixture, so a leftover trace from an + * earlier page load cannot satisfy the predicate on behalf of the trace under test. Note that it + * can still satisfy the predicate in its own right: when several tests exercise the same route, + * the predicate has to name something unique to the request under test. * * @example * ```ts - * const spans = await collectStreamedSpans(PROXY_SERVER_NAME, allSpans => - * allSpans.some(span => span.name === 'GET /nested-layout' && span.is_segment), + * const spans = await collectStreamedSpans(PROXY_SERVER_NAME, spansOfTrace => + * spansOfTrace.some(span => span.name === 'GET /nested-layout' && span.is_segment), * ); * expect(spans.map(span => span.name)).toContainEqual('build component tree'); * ``` */ export function collectStreamedSpans( proxyServerName: string, - isDone: (spans: SerializedStreamedSpan[]) => boolean, + isDone: (spansOfTrace: SerializedStreamedSpan[]) => boolean, ): Promise { - const collected: SerializedStreamedSpan[] = []; + const spansByTrace = new Map(); + let matched: SerializedStreamedSpan[] | undefined; + return waitForStreamedSpans(proxyServerName, spans => { - collected.push(...spans); - return isDone(collected); - }).then(() => collected); + for (const span of spans) { + const spansOfTrace = spansByTrace.get(span.trace_id); + if (spansOfTrace) { + spansOfTrace.push(span); + } else { + spansByTrace.set(span.trace_id, [span]); + } + } + + // Every trace is a candidate, so a trace that never satisfies `isDone` cannot hold up the one + // that does. Insertion order means the earliest-arriving trace wins a tie. + for (const spansOfTrace of spansByTrace.values()) { + if (isDone(spansOfTrace)) { + matched = spansOfTrace; + return true; + } + } + + return false; + }).then(() => matched ?? []); } /** diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index cff6d479d00d..ebb89fd08e5f 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -14,6 +14,7 @@ export { collectStreamedSpans, getSpanOp, } from './event-proxy-server'; +export type { SerializedStreamedSpan } from '@sentry/core'; export { findAbsolutePathImports,