Skip to content

Commit

Permalink
test(remix): Update Remix E2E tests
Browse files Browse the repository at this point in the history
This does two things:

1. Remove an unused remix integration test
2. Add a test that ensures we send correct server & browser transactions that are correctly linked
  • Loading branch information
mydea committed Mar 13, 2024
1 parent 7879791 commit 52beb2f
Show file tree
Hide file tree
Showing 45 changed files with 1,005 additions and 182 deletions.
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

0 comments on commit 52beb2f

Please sign in to comment.