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(remix): Update Remix E2E tests #11077

Merged
merged 1 commit into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 0 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ jobs:
- 'packages/profiling-node/**'
- 'dev-packages/e2e-tests/test-applications/node-profiling/**'
profiling_node_bindings:
- *workflow
- 'packages/profiling-node/**'
- 'dev-packages/e2e-tests/test-applications/node-profiling/**'
deno:
Expand Down Expand Up @@ -888,8 +887,6 @@ jobs:
remix: 1
- node: 16
remix: 1
- tracingIntegration: true
remix: 2
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
Expand All @@ -907,7 +904,6 @@ jobs:
env:
NODE_VERSION: ${{ matrix.node }}
REMIX_VERSION: ${{ matrix.remix }}
TRACING_INTEGRATION: ${{ matrix.tracingIntegration }}
run: |
cd packages/remix
yarn test:integration:ci
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Sentry.init({
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
tunnel: 'http://localhost:3031/', // proxy server
});

Sentry.addEventProcessor(event => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Sentry.init({
dsn: process.env.E2E_TEST_DSN,
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
tunnel: 'http://localhost:3031/', // proxy server
});

export const handleError = Sentry.wrapRemixHandleError;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Link } from '@remix-run/react';
import { Link, useSearchParams } from '@remix-run/react';
import * as Sentry from '@sentry/remix';

export default function Index() {
const [searchParams] = useSearchParams();

if (searchParams.get('tag')) {
Sentry.setTag('sentry_test', searchParams.get('tag'));
}

return (
<div>
<input
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import * as fs from 'fs';
import * as http from 'http';
import * as https from 'https';
import type { AddressInfo } from 'net';
import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import * as zlib from 'zlib';
import type { Envelope, EnvelopeItem, Event } from '@sentry/types';
import { parseEnvelope } from '@sentry/utils';

const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);

interface EventProxyServerOptions {
/** Port to start the event proxy server at. */
port: number;
/** The name for the proxy server used for referencing it with listener functions */
proxyServerName: string;
}

interface SentryRequestCallbackData {
envelope: Envelope;
rawProxyRequestBody: string;
rawSentryResponseBody: string;
sentryResponseStatusCode?: number;
}

/**
* Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel`
* option to this server (like this `tunnel: http://localhost:${port option}/`).
*/
export async function startEventProxyServer(options: EventProxyServerOptions): Promise<void> {
const eventCallbackListeners: Set<(data: string) => void> = new Set();

const proxyServer = http.createServer((proxyRequest, proxyResponse) => {
const proxyRequestChunks: Uint8Array[] = [];

proxyRequest.addListener('data', (chunk: Buffer) => {
proxyRequestChunks.push(chunk);
});

proxyRequest.addListener('error', err => {
throw err;
});

proxyRequest.addListener('end', () => {
const proxyRequestBody =
proxyRequest.headers['content-encoding'] === 'gzip'
? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString()
: Buffer.concat(proxyRequestChunks).toString();

let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]);

if (!envelopeHeader.dsn) {
throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.');
}

const { origin, pathname, host } = new URL(envelopeHeader.dsn);

const projectId = pathname.substring(1);
const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`;

proxyRequest.headers.host = host;

const sentryResponseChunks: Uint8Array[] = [];

const sentryRequest = https.request(
sentryIngestUrl,
{ headers: proxyRequest.headers, method: proxyRequest.method },
sentryResponse => {
sentryResponse.addListener('data', (chunk: Buffer) => {
proxyResponse.write(chunk, 'binary');
sentryResponseChunks.push(chunk);
});

sentryResponse.addListener('end', () => {
eventCallbackListeners.forEach(listener => {
const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString();

const data: SentryRequestCallbackData = {
envelope: parseEnvelope(proxyRequestBody),
rawProxyRequestBody: proxyRequestBody,
rawSentryResponseBody,
sentryResponseStatusCode: sentryResponse.statusCode,
};

listener(Buffer.from(JSON.stringify(data)).toString('base64'));
});
proxyResponse.end();
});

sentryResponse.addListener('error', err => {
throw err;
});

proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers);
},
);

sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary');
sentryRequest.end();
});
});

const proxyServerStartupPromise = new Promise<void>(resolve => {
proxyServer.listen(options.port, () => {
resolve();
});
});

const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => {
eventCallbackResponse.statusCode = 200;
eventCallbackResponse.setHeader('connection', 'keep-alive');

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

eventCallbackListeners.add(callbackListener);

eventCallbackRequest.on('close', () => {
eventCallbackListeners.delete(callbackListener);
});

eventCallbackRequest.on('error', () => {
eventCallbackListeners.delete(callbackListener);
});
});

const eventCallbackServerStartupPromise = new Promise<void>(resolve => {
eventCallbackServer.listen(0, () => {
const port = String((eventCallbackServer.address() as AddressInfo).port);
void registerCallbackServerPort(options.proxyServerName, port).then(resolve);
});
});

await eventCallbackServerStartupPromise;
await proxyServerStartupPromise;
return;
}

export async function waitForRequest(
proxyServerName: string,
callback: (eventData: SentryRequestCallbackData) => Promise<boolean> | boolean,
): 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);
});

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;
},
);
} else if (callbackResult) {
response.destroy();
resolve(eventCallbackData);
}
eventContents = '';
} else {
eventContents = eventContents.concat(char);
}
});
});
});

request.end();
});
}

export function waitForEnvelopeItem(
proxyServerName: string,
callback: (envelopeItem: EnvelopeItem) => Promise<boolean> | boolean,
): 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;
}
}
return false;
}).catch(reject);
});
}

export function waitForError(
proxyServerName: string,
callback: (transactionEvent: Event) => Promise<boolean> | boolean,
): Promise<Event> {
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);
});
}

export function waitForTransaction(
proxyServerName: string,
callback: (transactionEvent: Event) => Promise<boolean> | boolean,
): Promise<Event> {
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);
});
}

const TEMP_FILE_PREFIX = 'event-proxy-server-';

async function registerCallbackServerPort(serverName: string, port: string): Promise<void> {
const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
await writeFile(tmpFilePath, port, { encoding: 'utf8' });
}

function retrieveCallbackServerPort(serverName: string): Promise<string> {
const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
return readFile(tmpFilePath, 'utf8');
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"devDependencies": {
"@playwright/test": "^1.36.2",
"@remix-run/dev": "^2.7.2",
"@sentry/types": "latest || *",
"@sentry/utils": "latest || *",
"@types/compression": "^1.7.5",
"@types/express": "^4.17.20",
"@types/morgan": "^1.9.9",
Expand All @@ -43,7 +45,8 @@
"eslint-plugin-react-hooks": "^4.6.0",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1"
"vite-tsconfig-paths": "^4.2.1",
"ts-node": "10.9.1"
},
"engines": {
"node": ">=18.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';

const port = 3030;
const eventProxyPort = 3031;

/**
* See https://playwright.dev/docs/test-configuration.
Expand Down Expand Up @@ -34,6 +35,9 @@ const config: PlaywrightTestConfig = {

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',

/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: `http://localhost:${port}`,
},

/* Configure projects for major browsers */
Expand All @@ -44,15 +48,19 @@ const config: PlaywrightTestConfig = {
...devices['Desktop Chrome'],
},
},
// For now we only test Chrome!
],

/* Run your local dev server before starting the tests */
webServer: {
// This test app is testing the Vite dev server, so we need to run it before the tests.
command: `PORT=${port} pnpm dev`,
port,
},
webServer: [
{
command: 'pnpm ts-node --project="tsconfig.event-proxy-server.json" ./start-event-proxy.ts',
port: eventProxyPort,
},
{
command: `PORT=${port} pnpm dev`,
port: port,
},
],
};

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { startEventProxyServer } from './event-proxy-server';
startEventProxyServer({
port: 3031,
proxyServerName: 'create-remix-app-express-vite-dev',
});
Loading
Loading