11import * as http from 'http' ;
22import { AddressInfo } from 'net' ;
33import * as path from 'path' ;
4+ import { fileURLToPath } from 'url' ;
45import { createRequestHandler } from '@remix-run/express' ;
56import { debug } from '@sentry/core' ;
67import type { EnvelopeItemType , Event , TransactionEvent } from '@sentry/core' ;
@@ -12,7 +13,6 @@ import express from 'express';
1213import type { Express } from 'express' ;
1314import type { HttpTerminator } from 'http-terminator' ;
1415import { createHttpTerminator } from 'http-terminator' ;
15- import nock from 'nock' ;
1616
1717type DataCollectorOptions = {
1818 // Optional custom URL
@@ -107,15 +107,25 @@ class TestEnv {
107107 ? [ options . envelopeType ]
108108 : options . envelopeType || ( [ 'event' ] as EnvelopeItemType [ ] ) ;
109109
110+ // Use a ref to capture startIndex right before making the request
111+ // The claimed indices mechanism and stopping at count will ensure parallel requests don't interfere
112+ const startIndexRef = { startIndex : null as number | null } ;
110113 const resProm = this . setupNock (
111114 options . count || 1 ,
112115 typeof options . endServer === 'undefined' ? true : options . endServer ,
113116 envelopeTypeArray ,
117+ startIndexRef ,
114118 ) ;
115119
116- // eslint-disable-next-line @typescript-eslint/no-floating-promises
117- makeRequest ( options . method , options . url || this . url , this . _axiosConfig ) ;
118- return resProm ;
120+ // Capture startIndex right before making the request
121+ const globalEnvelopesArray = ( globalThis as any ) . __SENTRY_TEST_ENVELOPES__ || [ ] ;
122+ startIndexRef . startIndex = globalEnvelopesArray . length ;
123+ // Wait for the request to complete so Sentry has time to capture events
124+ await makeRequest ( options . method , options . url || this . url , this . _axiosConfig ) ;
125+ // Flush Sentry events to ensure they're sent to the transport
126+ await Sentry . flush ( 2000 ) ;
127+ const result = await resProm ;
128+ return result ;
119129 }
120130
121131 /**
@@ -166,51 +176,104 @@ class TestEnv {
166176 count : number ,
167177 endServer : boolean ,
168178 envelopeType : EnvelopeItemType [ ] ,
179+ startIndexRef ?: { startIndex : number | null } ,
169180 ) : Promise < Record < string , unknown > [ ] [ ] > {
170- return new Promise ( resolve => {
181+ return new Promise ( ( resolve , reject ) => {
171182 const envelopes : Record < string , unknown > [ ] [ ] = [ ] ;
172- const mock = nock ( 'https://dsn.ingest.sentry.io' )
173- . persist ( )
174- . post ( '/api/1337/envelope/' , body => {
175- const envelope = parseEnvelope ( body ) ;
183+ let timeoutId : NodeJS . Timeout | null = null ;
184+ let checkInterval : NodeJS . Timeout | null = null ;
185+
186+ // Track the starting length to only count envelopes added after the request is made
187+ // If startIndexRef is provided, use it (set right before request); otherwise capture now
188+ let startIndex : number ;
189+ if ( startIndexRef ) {
190+ // Wait for startIndex to be set (it will be set right before request is made)
191+ const getStartIndex = ( ) => {
192+ if ( startIndexRef . startIndex === null ) {
193+ const globalEnvelopesArray = ( globalThis as any ) . __SENTRY_TEST_ENVELOPES__ || [ ] ;
194+ return globalEnvelopesArray . length ;
195+ }
196+ return startIndexRef . startIndex ;
197+ } ;
198+ startIndex = getStartIndex ( ) ;
199+ } else {
200+ const globalEnvelopesArray = ( globalThis as any ) . __SENTRY_TEST_ENVELOPES__ || [ ] ;
201+ startIndex = globalEnvelopesArray . length ;
202+ }
176203
177- if ( envelopeType . includes ( envelope [ 1 ] ?. type as EnvelopeItemType ) ) {
178- envelopes . push ( envelope ) ;
179- } else {
180- return false ;
204+ // Use a global Set to track which envelope indices have been claimed
205+ // This prevents parallel setupNock instances from matching the same envelopes
206+ if ( ! ( globalThis as any ) . __SENTRY_TEST_CLAIMED_ENVELOPE_INDICES__ ) {
207+ ( globalThis as any ) . __SENTRY_TEST_CLAIMED_ENVELOPE_INDICES__ = new Set < number > ( ) ;
208+ }
209+ const claimedIndices = ( globalThis as any ) . __SENTRY_TEST_CLAIMED_ENVELOPE_INDICES__ as Set < number > ;
210+
211+ // Poll for envelopes from the custom transport
212+ const checkForEnvelopes = ( ) => {
213+ // If using ref, wait until it's set (set right before request is made)
214+ if ( startIndexRef && startIndexRef . startIndex === null ) {
215+ return ; // Don't check yet, startIndex hasn't been set
216+ }
217+
218+ const globalEnvelopes = ( globalThis as any ) . __SENTRY_TEST_ENVELOPES__ || [ ] ;
219+
220+ // Use the ref value if provided, otherwise use the initial startIndex
221+ const currentStartIndex = startIndexRef ?. startIndex ?? startIndex ;
222+
223+ // Only check envelopes that were added after the request started
224+ // Check each envelope by its index in the global array
225+ // Stop once we have enough envelopes to avoid claiming more than needed
226+ for ( let i = currentStartIndex ; i < globalEnvelopes . length && envelopes . length < count ; i ++ ) {
227+ // Skip if this envelope index has already been claimed by another setupNock
228+ if ( claimedIndices . has ( i ) ) {
229+ continue ;
181230 }
182231
183- if ( count === envelopes . length ) {
184- nock . removeInterceptor ( mock ) ;
185-
186- if ( endServer ) {
187- // Cleaning nock only before the server is closed,
188- // not to break tests that use simultaneous requests to the server.
189- // Ex: Remix scope bleed tests.
190- nock . cleanAll ( ) ;
191-
192- // Abort all pending requests to nock to prevent hanging / flakes.
193- // See: https://github.com/nock/nock/issues/1118#issuecomment-544126948
194- nock . abortPendingRequests ( ) ;
195-
196- this . _closeServer ( )
197- . catch ( e => {
198- debug . warn ( e ) ;
199- } )
200- . finally ( ( ) => {
201- resolve ( envelopes ) ;
202- } ) ;
203- } else {
204- resolve ( envelopes ) ;
232+ const envelope = globalEnvelopes [ i ] ;
233+ // The parsed envelope format is [header, itemHeader, itemPayload]
234+ // where itemHeader has a 'type' property
235+ const itemHeader = envelope [ 1 ] ;
236+ if ( itemHeader && envelopeType . includes ( itemHeader . type as EnvelopeItemType ) ) {
237+ // Check if we've already added this envelope to our local array
238+ if ( ! envelopes . includes ( envelope ) ) {
239+ // Claim this envelope index so other parallel setupNock instances don't match it
240+ claimedIndices . add ( i ) ;
241+ envelopes . push ( envelope ) ;
242+ // Stop if we have enough envelopes
243+ if ( envelopes . length >= count ) {
244+ break ;
245+ }
205246 }
206247 }
248+ }
207249
208- return true ;
209- } ) ;
250+ if ( count === envelopes . length ) {
251+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
252+ if ( checkInterval ) clearInterval ( checkInterval ) ;
253+
254+ if ( endServer ) {
255+ this . _closeServer ( )
256+ . catch ( e => {
257+ debug . warn ( e ) ;
258+ } )
259+ . finally ( ( ) => {
260+ resolve ( envelopes ) ;
261+ } ) ;
262+ } else {
263+ resolve ( envelopes ) ;
264+ }
265+ }
266+ } ;
210267
211- mock
212- . query ( true ) // accept any query params - used for sentry_key param
213- . reply ( 200 ) ;
268+ // Check immediately and then poll every 50ms
269+ checkForEnvelopes ( ) ;
270+ checkInterval = setInterval ( checkForEnvelopes , 50 ) ;
271+
272+ // Add a timeout to detect if Sentry requests never arrive
273+ timeoutId = setTimeout ( ( ) => {
274+ if ( checkInterval ) clearInterval ( checkInterval ) ;
275+ reject ( new Error ( `Timeout waiting for Sentry envelopes. Expected ${ count } , got ${ envelopes . length } ` ) ) ;
276+ } , 5000 ) ;
214277 } ) ;
215278 }
216279
@@ -224,25 +287,27 @@ class TestEnv {
224287 envelopeType : EnvelopeItemType | EnvelopeItemType [ ] ;
225288 } ) : Promise < number > {
226289 return new Promise ( resolve => {
227- let reqCount = 0 ;
290+ const envelopeTypeArray =
291+ typeof options . envelopeType === 'string' ? [ options . envelopeType ] : options . envelopeType ;
228292
229- const mock = nock ( 'https://dsn.ingest.sentry.io' )
230- . persist ( )
231- . post ( '/api/1337/envelope/' , body => {
232- const envelope = parseEnvelope ( body ) ;
293+ // Track the starting length to only count envelopes added after this call
294+ const globalEnvelopesArray = ( globalThis as any ) . __SENTRY_TEST_ENVELOPES__ || [ ] ;
295+ const startIndex = globalEnvelopesArray . length ;
233296
234- if ( options . envelopeType . includes ( envelope [ 1 ] ?. type as EnvelopeItemType ) ) {
297+ setTimeout ( ( ) => {
298+ const globalEnvelopes = ( globalThis as any ) . __SENTRY_TEST_ENVELOPES__ || [ ] ;
299+ // Only count envelopes that were added after this call started
300+ const newEnvelopes = globalEnvelopes . slice ( startIndex ) ;
301+ let reqCount = 0 ;
302+
303+ for ( const envelope of newEnvelopes ) {
304+ // The parsed envelope format is [header, itemHeader, itemPayload]
305+ // where itemHeader has a 'type' property
306+ const itemHeader = envelope [ 1 ] ;
307+ if ( itemHeader && envelopeTypeArray . includes ( itemHeader . type as EnvelopeItemType ) ) {
235308 reqCount ++ ;
236- return true ;
237309 }
238-
239- return false ;
240- } ) ;
241-
242- setTimeout ( ( ) => {
243- nock . removeInterceptor ( mock ) ;
244-
245- nock . cleanAll ( ) ;
310+ }
246311
247312 // eslint-disable-next-line @typescript-eslint/no-floating-promises
248313 this . _closeServer ( ) . then ( ( ) => {
@@ -266,20 +331,36 @@ export class RemixTestEnv extends TestEnv {
266331 }
267332
268333 public static async init ( ) : Promise < RemixTestEnv > {
269- let serverPort ;
270- const server = await new Promise < http . Server > ( async resolve => {
271- const app = express ( ) ;
334+ const app = express ( ) ;
272335
273- // Vite builds to build/server/index.js instead of build/index.js
274- app . all ( '*' , createRequestHandler ( { build : await import ( '../../../build/server/index.js' ) } ) ) ;
336+ // Import the build module dynamically
337+ const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
338+ const buildPath = path . resolve ( __dirname , '../../../build/server/index.js' ) ;
339+ const build = await import ( buildPath ) ;
275340
341+ const handler = createRequestHandler ( { build } ) ;
342+
343+ app . all ( '*' , async ( req , res , next ) => {
344+ try {
345+ await handler ( req , res ) ;
346+ } catch ( e ) {
347+ next ( e ) ;
348+ }
349+ } ) ;
350+
351+ return new Promise ( ( resolve , reject ) => {
276352 const server = app . listen ( 0 , ( ) => {
277- serverPort = ( server . address ( ) as AddressInfo ) . port ;
278- resolve ( server ) ;
353+ const address = server . address ( ) ;
354+ if ( address && typeof address === 'object' ) {
355+ resolve ( new RemixTestEnv ( server , `http://localhost:${ address . port } ` ) ) ;
356+ } else {
357+ server . close ( ) ;
358+ reject ( new Error ( 'Failed to start server: could not determine port' ) ) ;
359+ }
279360 } ) ;
280- } ) ;
281361
282- return new RemixTestEnv ( server , `http://localhost:${ serverPort } ` ) ;
362+ server . on ( 'error' , reject ) ;
363+ } ) ;
283364 }
284365}
285366
0 commit comments