@@ -517,6 +517,281 @@ export default function HttpTest() {
517517 />
518518 </ TestSection >
519519
520+ < TestSection
521+ title = "Response Body Capture Tests"
522+ description = "Test that http.response.body is captured on fetch spans. Previously, the async body read raced with span.end() causing the body attribute to be silently dropped."
523+ >
524+ < TestButton
525+ title = "GET Response Body"
526+ description = "http.response.body should contain JSON"
527+ onClick = { async ( ) => {
528+ try {
529+ const response = await fetch (
530+ 'https://jsonplaceholder.typicode.com/posts/1?test=response-body-get' ,
531+ )
532+ const data = await response . json ( )
533+ console . log ( 'GET response body capture test:' , data )
534+ } catch ( e ) {
535+ console . error ( 'Request error:' , e )
536+ }
537+ } }
538+ />
539+
540+ < TestButton
541+ title = "Large Response Body"
542+ description = "Body capture with ~5KB response"
543+ onClick = { async ( ) => {
544+ try {
545+ const response = await fetch (
546+ 'https://jsonplaceholder.typicode.com/posts?test=response-body-large&_limit=10' ,
547+ )
548+ const data = await response . json ( )
549+ console . log (
550+ 'Large response body capture test:' ,
551+ `${ data . length } items, ~${ JSON . stringify ( data ) . length } bytes` ,
552+ )
553+ } catch ( e ) {
554+ console . error ( 'Request error:' , e )
555+ }
556+ } }
557+ />
558+
559+ < TestButton
560+ title = "POST Response Body"
561+ description = "Both request and response bodies captured"
562+ onClick = { async ( ) => {
563+ try {
564+ const response = await fetch (
565+ 'https://jsonplaceholder.typicode.com/posts?test=response-body-post' ,
566+ {
567+ method : 'POST' ,
568+ headers : {
569+ 'Content-Type' : 'application/json' ,
570+ } ,
571+ body : JSON . stringify ( {
572+ title : 'Test' ,
573+ body : 'Verify both request and response bodies are on the span' ,
574+ userId : 1 ,
575+ } ) ,
576+ } ,
577+ )
578+ const data = await response . json ( )
579+ console . log (
580+ 'POST response body capture test:' ,
581+ data ,
582+ )
583+ } catch ( e ) {
584+ console . error ( 'Request error:' , e )
585+ }
586+ } }
587+ />
588+
589+ < TestButton
590+ title = "Concurrent Response Bodies"
591+ description = "5 parallel requests, all should have bodies"
592+ onClick = { async ( ) => {
593+ try {
594+ const promises = [ ]
595+ for ( let i = 1 ; i <= 5 ; i ++ ) {
596+ promises . push (
597+ fetch (
598+ `https://jsonplaceholder.typicode.com/posts/${ i } ?test=response-body-concurrent-${ i } ` ,
599+ ) ,
600+ )
601+ }
602+ const responses = await Promise . all ( promises )
603+ const data = await Promise . all (
604+ responses . map ( ( r ) => r . json ( ) ) ,
605+ )
606+ console . log (
607+ 'Concurrent response body capture test:' ,
608+ `${ data . length } responses with bodies` ,
609+ )
610+ } catch ( e ) {
611+ console . error ( 'Request error:' , e )
612+ }
613+ } }
614+ />
615+ </ TestSection >
616+
617+ < TestSection
618+ title = "Memory Pressure Tests"
619+ description = "Stress test the pendingResponseAttributes map and response body cloning under high concurrency. Use browser DevTools Memory tab to observe heap impact."
620+ >
621+ < TestButton
622+ title = "50 Concurrent Requests"
623+ description = "Flood pendingResponseAttributes map"
624+ onClick = { async ( ) => {
625+ const before = (
626+ performance as unknown as {
627+ memory ?: { usedJSHeapSize : number }
628+ }
629+ ) . memory ?. usedJSHeapSize
630+ try {
631+ const promises = [ ]
632+ for ( let i = 1 ; i <= 50 ; i ++ ) {
633+ promises . push (
634+ fetch (
635+ `https://jsonplaceholder.typicode.com/posts/${ ( i % 100 ) + 1 } ?test=memory-concurrent-${ i } ` ,
636+ ) ,
637+ )
638+ }
639+ const responses = await Promise . all ( promises )
640+ await Promise . all ( responses . map ( ( r ) => r . json ( ) ) )
641+ const after = (
642+ performance as unknown as {
643+ memory ?: { usedJSHeapSize : number }
644+ }
645+ ) . memory ?. usedJSHeapSize
646+ if ( before && after ) {
647+ console . log (
648+ `Memory: 50 concurrent requests — heap delta: ${ ( ( after - before ) / 1024 ) . toFixed ( 0 ) } KB (${ ( before / 1024 / 1024 ) . toFixed ( 1 ) } MB → ${ ( after / 1024 / 1024 ) . toFixed ( 1 ) } MB)` ,
649+ )
650+ } else {
651+ console . log (
652+ 'Memory: 50 concurrent requests completed (enable chrome://flags/#enable-precise-memory-info for heap stats)' ,
653+ )
654+ }
655+ } catch ( e ) {
656+ console . error ( 'Memory test error:' , e )
657+ }
658+ } }
659+ />
660+
661+ < TestButton
662+ title = "Large Bodies x20"
663+ description = "20 concurrent requests with ~30KB bodies"
664+ onClick = { async ( ) => {
665+ const before = (
666+ performance as unknown as {
667+ memory ?: { usedJSHeapSize : number }
668+ }
669+ ) . memory ?. usedJSHeapSize
670+ try {
671+ const promises = [ ]
672+ for ( let i = 0 ; i < 20 ; i ++ ) {
673+ // /comments returns ~30KB
674+ promises . push (
675+ fetch (
676+ `https://jsonplaceholder.typicode.com/comments?test=memory-large-${ i } ` ,
677+ ) ,
678+ )
679+ }
680+ const responses = await Promise . all ( promises )
681+ const bodies = await Promise . all (
682+ responses . map ( ( r ) => r . json ( ) ) ,
683+ )
684+ const totalSize = bodies . reduce (
685+ ( sum , b ) => sum + JSON . stringify ( b ) . length ,
686+ 0 ,
687+ )
688+ const after = (
689+ performance as unknown as {
690+ memory ?: { usedJSHeapSize : number }
691+ }
692+ ) . memory ?. usedJSHeapSize
693+ if ( before && after ) {
694+ console . log (
695+ `Memory: 20 large responses (~${ ( totalSize / 1024 ) . toFixed ( 0 ) } KB total) — heap delta: ${ ( ( after - before ) / 1024 ) . toFixed ( 0 ) } KB (${ ( before / 1024 / 1024 ) . toFixed ( 1 ) } MB → ${ ( after / 1024 / 1024 ) . toFixed ( 1 ) } MB)` ,
696+ )
697+ } else {
698+ console . log (
699+ `Memory: 20 large responses (~${ ( totalSize / 1024 ) . toFixed ( 0 ) } KB total) completed` ,
700+ )
701+ }
702+ } catch ( e ) {
703+ console . error ( 'Memory test error:' , e )
704+ }
705+ } }
706+ />
707+
708+ < TestButton
709+ title = "Sustained Rapid Fire"
710+ description = "10 batches of 10, back-to-back"
711+ onClick = { async ( ) => {
712+ const before = (
713+ performance as unknown as {
714+ memory ?: { usedJSHeapSize : number }
715+ }
716+ ) . memory ?. usedJSHeapSize
717+ try {
718+ for ( let batch = 0 ; batch < 10 ; batch ++ ) {
719+ const promises = [ ]
720+ for ( let i = 0 ; i < 10 ; i ++ ) {
721+ promises . push (
722+ fetch (
723+ `https://jsonplaceholder.typicode.com/posts/${ ( i % 100 ) + 1 } ?test=memory-rapid-b${ batch } -${ i } ` ,
724+ ) ,
725+ )
726+ }
727+ const responses = await Promise . all ( promises )
728+ await Promise . all (
729+ responses . map ( ( r ) => r . json ( ) ) ,
730+ )
731+ }
732+ const after = (
733+ performance as unknown as {
734+ memory ?: { usedJSHeapSize : number }
735+ }
736+ ) . memory ?. usedJSHeapSize
737+ if ( before && after ) {
738+ console . log (
739+ `Memory: 10x10 sustained load — heap delta: ${ ( ( after - before ) / 1024 ) . toFixed ( 0 ) } KB (${ ( before / 1024 / 1024 ) . toFixed ( 1 ) } MB → ${ ( after / 1024 / 1024 ) . toFixed ( 1 ) } MB)` ,
740+ )
741+ } else {
742+ console . log (
743+ 'Memory: 10x10 sustained load completed' ,
744+ )
745+ }
746+ } catch ( e ) {
747+ console . error ( 'Memory test error:' , e )
748+ }
749+ } }
750+ />
751+
752+ < TestButton
753+ title = "Fire-and-Forget (no await body)"
754+ description = "50 requests where body is never read by app"
755+ onClick = { async ( ) => {
756+ const before = (
757+ performance as unknown as {
758+ memory ?: { usedJSHeapSize : number }
759+ }
760+ ) . memory ?. usedJSHeapSize
761+ try {
762+ const promises = [ ]
763+ for ( let i = 0 ; i < 50 ; i ++ ) {
764+ promises . push (
765+ fetch (
766+ `https://jsonplaceholder.typicode.com/posts/${ ( i % 100 ) + 1 } ?test=memory-fire-forget-${ i } ` ,
767+ ) ,
768+ )
769+ }
770+ // Only wait for responses, don't read bodies.
771+ // The SDK still clones + reads each body internally,
772+ // so this tests whether un-consumed bodies leak.
773+ await Promise . all ( promises )
774+ const after = (
775+ performance as unknown as {
776+ memory ?: { usedJSHeapSize : number }
777+ }
778+ ) . memory ?. usedJSHeapSize
779+ if ( before && after ) {
780+ console . log (
781+ `Memory: 50 fire-and-forget — heap delta: ${ ( ( after - before ) / 1024 ) . toFixed ( 0 ) } KB (${ ( before / 1024 / 1024 ) . toFixed ( 1 ) } MB → ${ ( after / 1024 / 1024 ) . toFixed ( 1 ) } MB)` ,
782+ )
783+ } else {
784+ console . log (
785+ 'Memory: 50 fire-and-forget completed' ,
786+ )
787+ }
788+ } catch ( e ) {
789+ console . error ( 'Memory test error:' , e )
790+ }
791+ } }
792+ />
793+ </ TestSection >
794+
520795 < TestSection
521796 title = "Response Tests"
522797 description = "Test different response types and status codes."
@@ -642,6 +917,19 @@ export default function HttpTest() {
642917 Request/response bodies are recorded as
643918 configured
644919 </ li >
920+ < li >
921+ Response Body Capture: each span has an{ ' ' }
922+ < code > http.response.body</ code > attribute with
923+ the full JSON response (not just{ ' ' }
924+ < code > http.response.body.size</ code > )
925+ </ li >
926+ < li >
927+ Memory Pressure: after running stress tests,
928+ check console for heap delta logs and use
929+ DevTools Memory tab to verify the{ ' ' }
930+ < code > pendingResponseAttributes</ code > map
931+ drains and heap does not grow unbounded
932+ </ li >
645933 </ ul >
646934 </ li >
647935 </ ol >
0 commit comments