Skip to content

Commit b3df93c

Browse files
JPeer264Lms24
authored andcommitted
feat(test-utils): Add a way to wait for single spans for Span streaming (#18986)
How it should be used is in the JSDoc. It worked quite well for my Cloudflare tests Closes #18987 (added automatically)
1 parent bcc15b1 commit b3df93c

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

dev-packages/test-utils/src/event-proxy-server.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type {
66
SerializedMetric,
77
SerializedMetricContainer,
88
SerializedSession,
9+
SpanV2Envelope,
10+
SpanV2JSON,
911
} from '@sentry/core';
1012
import { parseEnvelope } from '@sentry/core';
1113
import * as fs from 'fs';
@@ -427,6 +429,204 @@ export function waitForMetric(
427429
});
428430
}
429431

432+
/**
433+
* Check if an envelope item is a Span V2 container item.
434+
*/
435+
function isSpanV2EnvelopeItem(
436+
envelopeItem: EnvelopeItem,
437+
): envelopeItem is [
438+
{ type: 'span'; content_type: 'application/vnd.sentry.items.span.v2+json'; item_count: number },
439+
{ items: SpanV2JSON[] },
440+
] {
441+
const [header] = envelopeItem;
442+
return (
443+
header.type === 'span' &&
444+
'content_type' in header &&
445+
header.content_type === 'application/vnd.sentry.items.span.v2+json'
446+
);
447+
}
448+
449+
/**
450+
* Wait for a Span V2 envelope to be sent.
451+
* Returns the first Span V2 envelope that is sent that matches the callback.
452+
* If no callback is provided, returns the first Span V2 envelope that is sent.
453+
*
454+
* @example
455+
* ```ts
456+
* const envelope = await waitForSpanV2Envelope(PROXY_SERVER_NAME);
457+
* const spans = envelope[1][0][1].items;
458+
* expect(spans.length).toBeGreaterThan(0);
459+
* ```
460+
*
461+
* @example
462+
* ```ts
463+
* // With a filter callback
464+
* const envelope = await waitForSpanV2Envelope(PROXY_SERVER_NAME, envelope => {
465+
* return envelope[1][0][1].items.length > 5;
466+
* });
467+
* ```
468+
*/
469+
export function waitForSpanV2Envelope(
470+
proxyServerName: string,
471+
callback?: (spanEnvelope: SpanV2Envelope) => Promise<boolean> | boolean,
472+
): Promise<SpanV2Envelope> {
473+
const timestamp = getNanosecondTimestamp();
474+
return new Promise((resolve, reject) => {
475+
waitForRequest(
476+
proxyServerName,
477+
async eventData => {
478+
const envelope = eventData.envelope;
479+
const envelopeItems = envelope[1];
480+
481+
// Check if this is a Span V2 envelope by looking for a Span V2 item
482+
const hasSpanV2Item = envelopeItems.some(item => isSpanV2EnvelopeItem(item));
483+
if (!hasSpanV2Item) {
484+
return false;
485+
}
486+
487+
const spanV2Envelope = envelope as SpanV2Envelope;
488+
489+
if (callback) {
490+
return callback(spanV2Envelope);
491+
}
492+
493+
return true;
494+
},
495+
timestamp,
496+
)
497+
.then(eventData => resolve(eventData.envelope as SpanV2Envelope))
498+
.catch(reject);
499+
});
500+
}
501+
502+
/**
503+
* Wait for a single Span V2 to be sent that matches the callback.
504+
* Returns the first Span V2 that is sent that matches the callback.
505+
* If no callback is provided, returns the first Span V2 that is sent.
506+
*
507+
* @example
508+
* ```ts
509+
* const span = await waitForSpanV2(PROXY_SERVER_NAME, span => {
510+
* return span.name === 'GET /api/users';
511+
* });
512+
* expect(span.status).toBe('ok');
513+
* ```
514+
*
515+
* @example
516+
* ```ts
517+
* // Using the getSpanV2Op helper
518+
* const span = await waitForSpanV2(PROXY_SERVER_NAME, span => {
519+
* return getSpanV2Op(span) === 'http.client';
520+
* });
521+
* ```
522+
*/
523+
export function waitForSpanV2(
524+
proxyServerName: string,
525+
callback: (span: SpanV2JSON) => Promise<boolean> | boolean,
526+
): Promise<SpanV2JSON> {
527+
const timestamp = getNanosecondTimestamp();
528+
return new Promise((resolve, reject) => {
529+
waitForRequest(
530+
proxyServerName,
531+
async eventData => {
532+
const envelope = eventData.envelope;
533+
const envelopeItems = envelope[1];
534+
535+
for (const envelopeItem of envelopeItems) {
536+
if (!isSpanV2EnvelopeItem(envelopeItem)) {
537+
return false
538+
}
539+
540+
const spans = envelopeItem[1].items;
541+
542+
for (const span of spans) {
543+
if (await callback(span)) {
544+
resolve(span);
545+
return true;
546+
}
547+
}
548+
}
549+
return false;
550+
},
551+
timestamp,
552+
).catch(reject);
553+
});
554+
}
555+
556+
/**
557+
* Wait for Span V2 spans to be sent. Returns all matching spans from the first envelope that has at least one match.
558+
* The callback receives individual spans (not an array), making it consistent with `waitForSpanV2`.
559+
* If no callback is provided, returns all spans from the first Span V2 envelope.
560+
*
561+
* @example
562+
* ```ts
563+
* // Get all spans from the first envelope
564+
* const spans = await waitForSpansV2(PROXY_SERVER_NAME);
565+
* expect(spans.length).toBeGreaterThan(0);
566+
* ```
567+
*
568+
* @example
569+
* ```ts
570+
* // Filter for specific spans (same callback style as waitForSpanV2)
571+
* const httpSpans = await waitForSpansV2(PROXY_SERVER_NAME, span => {
572+
* return getSpanV2Op(span) === 'http.client';
573+
* });
574+
* expect(httpSpans.length).toBe(2);
575+
* ```
576+
*/
577+
export function waitForSpansV2(
578+
proxyServerName: string,
579+
callback?: (span: SpanV2JSON) => Promise<boolean> | boolean,
580+
): Promise<SpanV2JSON[]> {
581+
const timestamp = getNanosecondTimestamp();
582+
return new Promise((resolve, reject) => {
583+
waitForRequest(
584+
proxyServerName,
585+
async eventData => {
586+
const envelope = eventData.envelope;
587+
const envelopeItems = envelope[1];
588+
589+
for (const envelopeItem of envelopeItems) {
590+
if (isSpanV2EnvelopeItem(envelopeItem)) {
591+
const spans = envelopeItem[1].items;
592+
if (callback) {
593+
const matchingSpans: SpanV2JSON[] = [];
594+
for (const span of spans) {
595+
if (await callback(span)) {
596+
matchingSpans.push(span);
597+
}
598+
}
599+
if (matchingSpans.length > 0) {
600+
resolve(matchingSpans);
601+
return true;
602+
}
603+
} else {
604+
resolve(spans);
605+
return true;
606+
}
607+
}
608+
}
609+
return false;
610+
},
611+
timestamp,
612+
).catch(reject);
613+
});
614+
}
615+
616+
/**
617+
* Helper to get the span operation from a Span V2 JSON object.
618+
*
619+
* @example
620+
* ```ts
621+
* const span = await waitForSpanV2(PROXY_SERVER_NAME, span => {
622+
* return getSpanV2Op(span) === 'http.client';
623+
* });
624+
* ```
625+
*/
626+
export function getSpanV2Op(span: SpanV2JSON): string | undefined {
627+
return span.attributes?.['sentry.op']?.type === 'string' ? span.attributes['sentry.op'].value : undefined;
628+
}
629+
430630
const TEMP_FILE_PREFIX = 'event-proxy-server-';
431631

432632
async function registerCallbackServerPort(serverName: string, port: string): Promise<void> {

dev-packages/test-utils/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export {
88
waitForSession,
99
waitForPlainRequest,
1010
waitForMetric,
11+
waitForSpanV2,
12+
waitForSpansV2,
13+
waitForSpanV2Envelope,
14+
getSpanV2Op,
1115
} from './event-proxy-server';
1216

1317
export { getPlaywrightConfig } from './playwright-config';

0 commit comments

Comments
 (0)