@@ -6,6 +6,8 @@ import type {
66 SerializedMetric ,
77 SerializedMetricContainer ,
88 SerializedSession ,
9+ SpanV2Envelope ,
10+ SpanV2JSON ,
911} from '@sentry/core' ;
1012import { parseEnvelope } from '@sentry/core' ;
1113import * 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+
430630const TEMP_FILE_PREFIX = 'event-proxy-server-' ;
431631
432632async function registerCallbackServerPort ( serverName : string , port : string ) : Promise < void > {
0 commit comments