Skip to content

Commit 993acd4

Browse files
Luca Forstnermydea
andauthored
test: Fix e2e test race condition by buffering events (#12739)
Co-authored-by: Francesco Novy <[email protected]>
1 parent 13ff71f commit 993acd4

File tree

2 files changed

+136
-80
lines changed

2 files changed

+136
-80
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ jobs:
100100
- 'packages/rollup-utils/**'
101101
- 'packages/utils/**'
102102
- 'packages/types/**'
103+
- 'dev-packages/test-utils/**'
103104
browser: &browser
104105
- *shared
105106
- 'packages/browser/**'

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

Lines changed: 135 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable max-lines */
2+
13
import * as fs from 'fs';
24
import * as http from 'http';
35
import type { AddressInfo } from 'net';
@@ -30,12 +32,22 @@ interface SentryRequestCallbackData {
3032
sentryResponseStatusCode?: number;
3133
}
3234

35+
interface EventCallbackListener {
36+
(data: string): void;
37+
}
38+
3339
type OnRequest = (
34-
eventCallbackListeners: Set<(data: string) => void>,
40+
eventCallbackListeners: Set<EventCallbackListener>,
3541
proxyRequest: http.IncomingMessage,
3642
proxyRequestBody: string,
43+
eventBuffer: BufferedEvent[],
3744
) => Promise<[number, string, Record<string, string> | undefined]>;
3845

46+
interface BufferedEvent {
47+
timestamp: number;
48+
data: string;
49+
}
50+
3951
/**
4052
* Start a generic proxy server.
4153
* The `onRequest` callback receives the incoming request and the request body,
@@ -51,7 +63,8 @@ export async function startProxyServer(
5163
},
5264
onRequest?: OnRequest,
5365
): Promise<void> {
54-
const eventCallbackListeners: Set<(data: string) => void> = new Set();
66+
const eventBuffer: BufferedEvent[] = [];
67+
const eventCallbackListeners: Set<EventCallbackListener> = new Set();
5568

5669
const proxyServer = http.createServer((proxyRequest, proxyResponse) => {
5770
const proxyRequestChunks: Uint8Array[] = [];
@@ -76,15 +89,17 @@ export async function startProxyServer(
7689

7790
const callback: OnRequest =
7891
onRequest ||
79-
(async (eventCallbackListeners, proxyRequest, proxyRequestBody) => {
92+
(async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => {
93+
eventBuffer.push({ data: proxyRequestBody, timestamp: Date.now() });
94+
8095
eventCallbackListeners.forEach(listener => {
8196
listener(proxyRequestBody);
8297
});
8398

8499
return [200, '{}', {}];
85100
});
86101

87-
callback(eventCallbackListeners, proxyRequest, proxyRequestBody)
102+
callback(eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer)
88103
.then(([statusCode, responseBody, responseHeaders]) => {
89104
proxyResponse.writeHead(statusCode, responseHeaders);
90105
proxyResponse.write(responseBody, 'utf-8');
@@ -110,12 +125,24 @@ export async function startProxyServer(
110125
eventCallbackResponse.statusCode = 200;
111126
eventCallbackResponse.setHeader('connection', 'keep-alive');
112127

128+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129+
const searchParams = new URL(eventCallbackRequest.url!, 'http://justsomerandombasesothattheurlisparseable.com/')
130+
.searchParams;
131+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
132+
const listenerTimestamp = Number(searchParams.get('timestamp')!);
133+
113134
const callbackListener = (data: string): void => {
114135
eventCallbackResponse.write(data.concat('\n'), 'utf8');
115136
};
116137

117138
eventCallbackListeners.add(callbackListener);
118139

140+
eventBuffer.forEach(bufferedEvent => {
141+
if (bufferedEvent.timestamp >= listenerTimestamp) {
142+
callbackListener(bufferedEvent.data);
143+
}
144+
});
145+
119146
eventCallbackRequest.on('close', () => {
120147
eventCallbackListeners.delete(callbackListener);
121148
});
@@ -142,7 +169,7 @@ export async function startProxyServer(
142169
* option to this server (like this `tunnel: http://localhost:${port option}/`).
143170
*/
144171
export async function startEventProxyServer(options: EventProxyServerOptions): Promise<void> {
145-
await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody) => {
172+
await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => {
146173
const envelopeHeader: EnvelopeItem[0] = JSON.parse(proxyRequestBody.split('\n')[0] as string);
147174

148175
const shouldForwardEventToSentry = options.forwardToSentry != null ? options.forwardToSentry : true;
@@ -199,8 +226,12 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P
199226
sentryResponseStatusCode: res.status,
200227
};
201228

229+
const dataString = Buffer.from(JSON.stringify(data)).toString('base64');
230+
231+
eventBuffer.push({ data: dataString, timestamp: Date.now() });
232+
202233
eventCallbackListeners.forEach(listener => {
203-
listener(Buffer.from(JSON.stringify(data)).toString('base64'));
234+
listener(dataString);
204235
});
205236

206237
const resHeaders: Record<string, string> = {};
@@ -221,24 +252,28 @@ export async function waitForPlainRequest(
221252
const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName);
222253

223254
return new Promise((resolve, reject) => {
224-
const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => {
225-
let eventContents = '';
226-
227-
response.on('error', err => {
228-
reject(err);
229-
});
255+
const request = http.request(
256+
`http://localhost:${eventCallbackServerPort}/?timestamp=${Date.now()}`,
257+
{},
258+
response => {
259+
let eventContents = '';
260+
261+
response.on('error', err => {
262+
reject(err);
263+
});
230264

231-
response.on('data', (chunk: Buffer) => {
232-
const chunkString = chunk.toString('utf8');
265+
response.on('data', (chunk: Buffer) => {
266+
const chunkString = chunk.toString('utf8');
233267

234-
eventContents = eventContents.concat(chunkString);
268+
eventContents = eventContents.concat(chunkString);
235269

236-
if (callback(eventContents)) {
237-
response.destroy();
238-
return resolve(eventContents);
239-
}
240-
});
241-
});
270+
if (callback(eventContents)) {
271+
response.destroy();
272+
return resolve(eventContents);
273+
}
274+
});
275+
},
276+
);
242277

243278
request.end();
244279
});
@@ -248,48 +283,53 @@ export async function waitForPlainRequest(
248283
export async function waitForRequest(
249284
proxyServerName: string,
250285
callback: (eventData: SentryRequestCallbackData) => Promise<boolean> | boolean,
286+
timestamp: number = Date.now(),
251287
): Promise<SentryRequestCallbackData> {
252288
const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName);
253289

254290
return new Promise<SentryRequestCallbackData>((resolve, reject) => {
255-
const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => {
256-
let eventContents = '';
257-
258-
response.on('error', err => {
259-
reject(err);
260-
});
291+
const request = http.request(
292+
`http://localhost:${eventCallbackServerPort}/?timestamp=${timestamp}`,
293+
{},
294+
response => {
295+
let eventContents = '';
296+
297+
response.on('error', err => {
298+
reject(err);
299+
});
261300

262-
response.on('data', (chunk: Buffer) => {
263-
const chunkString = chunk.toString('utf8');
264-
chunkString.split('').forEach(char => {
265-
if (char === '\n') {
266-
const eventCallbackData: SentryRequestCallbackData = JSON.parse(
267-
Buffer.from(eventContents, 'base64').toString('utf8'),
268-
);
269-
const callbackResult = callback(eventCallbackData);
270-
if (typeof callbackResult !== 'boolean') {
271-
callbackResult.then(
272-
match => {
273-
if (match) {
274-
response.destroy();
275-
resolve(eventCallbackData);
276-
}
277-
},
278-
err => {
279-
throw err;
280-
},
301+
response.on('data', (chunk: Buffer) => {
302+
const chunkString = chunk.toString('utf8');
303+
chunkString.split('').forEach(char => {
304+
if (char === '\n') {
305+
const eventCallbackData: SentryRequestCallbackData = JSON.parse(
306+
Buffer.from(eventContents, 'base64').toString('utf8'),
281307
);
282-
} else if (callbackResult) {
283-
response.destroy();
284-
resolve(eventCallbackData);
308+
const callbackResult = callback(eventCallbackData);
309+
if (typeof callbackResult !== 'boolean') {
310+
callbackResult.then(
311+
match => {
312+
if (match) {
313+
response.destroy();
314+
resolve(eventCallbackData);
315+
}
316+
},
317+
err => {
318+
throw err;
319+
},
320+
);
321+
} else if (callbackResult) {
322+
response.destroy();
323+
resolve(eventCallbackData);
324+
}
325+
eventContents = '';
326+
} else {
327+
eventContents = eventContents.concat(char);
285328
}
286-
eventContents = '';
287-
} else {
288-
eventContents = eventContents.concat(char);
289-
}
329+
});
290330
});
291-
});
292-
});
331+
},
332+
);
293333

294334
request.end();
295335
});
@@ -299,18 +339,23 @@ export async function waitForRequest(
299339
export function waitForEnvelopeItem(
300340
proxyServerName: string,
301341
callback: (envelopeItem: EnvelopeItem) => Promise<boolean> | boolean,
342+
timestamp: number = Date.now(),
302343
): Promise<EnvelopeItem> {
303344
return new Promise((resolve, reject) => {
304-
waitForRequest(proxyServerName, async eventData => {
305-
const envelopeItems = eventData.envelope[1];
306-
for (const envelopeItem of envelopeItems) {
307-
if (await callback(envelopeItem)) {
308-
resolve(envelopeItem);
309-
return true;
345+
waitForRequest(
346+
proxyServerName,
347+
async eventData => {
348+
const envelopeItems = eventData.envelope[1];
349+
for (const envelopeItem of envelopeItems) {
350+
if (await callback(envelopeItem)) {
351+
resolve(envelopeItem);
352+
return true;
353+
}
310354
}
311-
}
312-
return false;
313-
}).catch(reject);
355+
return false;
356+
},
357+
timestamp,
358+
).catch(reject);
314359
});
315360
}
316361

@@ -319,15 +364,20 @@ export function waitForError(
319364
proxyServerName: string,
320365
callback: (transactionEvent: Event) => Promise<boolean> | boolean,
321366
): Promise<Event> {
367+
const timestamp = Date.now();
322368
return new Promise((resolve, reject) => {
323-
waitForEnvelopeItem(proxyServerName, async envelopeItem => {
324-
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
325-
if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) {
326-
resolve(envelopeItemBody as Event);
327-
return true;
328-
}
329-
return false;
330-
}).catch(reject);
369+
waitForEnvelopeItem(
370+
proxyServerName,
371+
async envelopeItem => {
372+
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
373+
if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) {
374+
resolve(envelopeItemBody as Event);
375+
return true;
376+
}
377+
return false;
378+
},
379+
timestamp,
380+
).catch(reject);
331381
});
332382
}
333383

@@ -336,15 +386,20 @@ export function waitForTransaction(
336386
proxyServerName: string,
337387
callback: (transactionEvent: Event) => Promise<boolean> | boolean,
338388
): Promise<Event> {
389+
const timestamp = Date.now();
339390
return new Promise((resolve, reject) => {
340-
waitForEnvelopeItem(proxyServerName, async envelopeItem => {
341-
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
342-
if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) {
343-
resolve(envelopeItemBody as Event);
344-
return true;
345-
}
346-
return false;
347-
}).catch(reject);
391+
waitForEnvelopeItem(
392+
proxyServerName,
393+
async envelopeItem => {
394+
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
395+
if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) {
396+
resolve(envelopeItemBody as Event);
397+
return true;
398+
}
399+
return false;
400+
},
401+
timestamp,
402+
).catch(reject);
348403
});
349404
}
350405

0 commit comments

Comments
 (0)