Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Fix e2e test race condition by buffering events #12739

Merged
merged 7 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ jobs:
- 'packages/rollup-utils/**'
- 'packages/utils/**'
- 'packages/types/**'
- 'dev-packages/test-utils/**'
browser: &browser
- *shared
- 'packages/browser/**'
Expand Down
215 changes: 135 additions & 80 deletions dev-packages/test-utils/src/event-proxy-server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable max-lines */

import * as fs from 'fs';
import * as http from 'http';
import type { AddressInfo } from 'net';
Expand Down Expand Up @@ -30,12 +32,22 @@ interface SentryRequestCallbackData {
sentryResponseStatusCode?: number;
}

interface EventCallbackListener {
(data: string): void;
}

type OnRequest = (
eventCallbackListeners: Set<(data: string) => void>,
eventCallbackListeners: Set<EventCallbackListener>,
proxyRequest: http.IncomingMessage,
proxyRequestBody: string,
eventBuffer: BufferedEvent[],
) => Promise<[number, string, Record<string, string> | undefined]>;

interface BufferedEvent {
timestamp: number;
data: string;
}

/**
* Start a generic proxy server.
* The `onRequest` callback receives the incoming request and the request body,
Expand All @@ -51,7 +63,8 @@ export async function startProxyServer(
},
onRequest?: OnRequest,
): Promise<void> {
const eventCallbackListeners: Set<(data: string) => void> = new Set();
const eventBuffer: BufferedEvent[] = [];
const eventCallbackListeners: Set<EventCallbackListener> = new Set();

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

const callback: OnRequest =
onRequest ||
(async (eventCallbackListeners, proxyRequest, proxyRequestBody) => {
(async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => {
eventBuffer.push({ data: proxyRequestBody, timestamp: Date.now() });

eventCallbackListeners.forEach(listener => {
listener(proxyRequestBody);
});

return [200, '{}', {}];
});

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

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const searchParams = new URL(eventCallbackRequest.url!, 'http://justsomerandombasesothattheurlisparseable.com/')
.searchParams;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const listenerTimestamp = Number(searchParams.get('timestamp')!);

const callbackListener = (data: string): void => {
eventCallbackResponse.write(data.concat('\n'), 'utf8');
};

eventCallbackListeners.add(callbackListener);

eventBuffer.forEach(bufferedEvent => {
if (bufferedEvent.timestamp > listenerTimestamp) {
lforst marked this conversation as resolved.
Show resolved Hide resolved
callbackListener(bufferedEvent.data);
}
});

eventCallbackRequest.on('close', () => {
eventCallbackListeners.delete(callbackListener);
});
Expand All @@ -142,7 +169,7 @@ export async function startProxyServer(
* option to this server (like this `tunnel: http://localhost:${port option}/`).
*/
export async function startEventProxyServer(options: EventProxyServerOptions): Promise<void> {
await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody) => {
await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => {
const envelopeHeader: EnvelopeItem[0] = JSON.parse(proxyRequestBody.split('\n')[0] as string);

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

const dataString = Buffer.from(JSON.stringify(data)).toString('base64');

eventBuffer.push({ data: dataString, timestamp: Date.now() });

eventCallbackListeners.forEach(listener => {
listener(Buffer.from(JSON.stringify(data)).toString('base64'));
listener(dataString);
});

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

return new Promise((resolve, reject) => {
const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => {
let eventContents = '';

response.on('error', err => {
reject(err);
});
const request = http.request(
`http://localhost:${eventCallbackServerPort}/?timestamp=${Date.now()}`,
{},
response => {
let eventContents = '';

response.on('error', err => {
reject(err);
});

response.on('data', (chunk: Buffer) => {
const chunkString = chunk.toString('utf8');
response.on('data', (chunk: Buffer) => {
const chunkString = chunk.toString('utf8');

eventContents = eventContents.concat(chunkString);
eventContents = eventContents.concat(chunkString);

if (callback(eventContents)) {
response.destroy();
return resolve(eventContents);
}
});
});
if (callback(eventContents)) {
response.destroy();
return resolve(eventContents);
}
});
},
);

request.end();
});
Expand All @@ -248,48 +283,53 @@ export async function waitForPlainRequest(
export async function waitForRequest(
proxyServerName: string,
callback: (eventData: SentryRequestCallbackData) => Promise<boolean> | boolean,
timestamp: number = Date.now(),
): Promise<SentryRequestCallbackData> {
const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName);

return new Promise<SentryRequestCallbackData>((resolve, reject) => {
const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => {
let eventContents = '';

response.on('error', err => {
reject(err);
});
const request = http.request(
`http://localhost:${eventCallbackServerPort}/?timestamp=${timestamp}`,
{},
response => {
let eventContents = '';

response.on('error', err => {
reject(err);
});

response.on('data', (chunk: Buffer) => {
const chunkString = chunk.toString('utf8');
chunkString.split('').forEach(char => {
if (char === '\n') {
const eventCallbackData: SentryRequestCallbackData = JSON.parse(
Buffer.from(eventContents, 'base64').toString('utf8'),
);
const callbackResult = callback(eventCallbackData);
if (typeof callbackResult !== 'boolean') {
callbackResult.then(
match => {
if (match) {
response.destroy();
resolve(eventCallbackData);
}
},
err => {
throw err;
},
response.on('data', (chunk: Buffer) => {
const chunkString = chunk.toString('utf8');
chunkString.split('').forEach(char => {
if (char === '\n') {
const eventCallbackData: SentryRequestCallbackData = JSON.parse(
Buffer.from(eventContents, 'base64').toString('utf8'),
);
} else if (callbackResult) {
response.destroy();
resolve(eventCallbackData);
const callbackResult = callback(eventCallbackData);
if (typeof callbackResult !== 'boolean') {
callbackResult.then(
match => {
if (match) {
response.destroy();
resolve(eventCallbackData);
}
},
err => {
throw err;
},
);
} else if (callbackResult) {
response.destroy();
resolve(eventCallbackData);
}
eventContents = '';
} else {
eventContents = eventContents.concat(char);
}
eventContents = '';
} else {
eventContents = eventContents.concat(char);
}
});
});
});
});
},
);

request.end();
});
Expand All @@ -299,18 +339,23 @@ export async function waitForRequest(
export function waitForEnvelopeItem(
proxyServerName: string,
callback: (envelopeItem: EnvelopeItem) => Promise<boolean> | boolean,
timestamp: number = Date.now(),
): Promise<EnvelopeItem> {
return new Promise((resolve, reject) => {
waitForRequest(proxyServerName, async eventData => {
const envelopeItems = eventData.envelope[1];
for (const envelopeItem of envelopeItems) {
if (await callback(envelopeItem)) {
resolve(envelopeItem);
return true;
waitForRequest(
proxyServerName,
async eventData => {
const envelopeItems = eventData.envelope[1];
for (const envelopeItem of envelopeItems) {
if (await callback(envelopeItem)) {
resolve(envelopeItem);
return true;
}
}
}
return false;
}).catch(reject);
return false;
},
timestamp,
).catch(reject);
});
}

Expand All @@ -319,15 +364,20 @@ export function waitForError(
proxyServerName: string,
callback: (transactionEvent: Event) => Promise<boolean> | boolean,
): Promise<Event> {
const timestamp = Date.now();
return new Promise((resolve, reject) => {
waitForEnvelopeItem(proxyServerName, async envelopeItem => {
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) {
resolve(envelopeItemBody as Event);
return true;
}
return false;
}).catch(reject);
waitForEnvelopeItem(
proxyServerName,
async envelopeItem => {
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) {
resolve(envelopeItemBody as Event);
return true;
}
return false;
},
timestamp,
).catch(reject);
});
}

Expand All @@ -336,15 +386,20 @@ export function waitForTransaction(
proxyServerName: string,
callback: (transactionEvent: Event) => Promise<boolean> | boolean,
): Promise<Event> {
const timestamp = Date.now();
return new Promise((resolve, reject) => {
waitForEnvelopeItem(proxyServerName, async envelopeItem => {
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) {
resolve(envelopeItemBody as Event);
return true;
}
return false;
}).catch(reject);
waitForEnvelopeItem(
proxyServerName,
async envelopeItem => {
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) {
resolve(envelopeItemBody as Event);
return true;
}
return false;
},
timestamp,
).catch(reject);
});
}

Expand Down
Loading