From f406c98874c28cb5701961c0ff074a05d2d1ceed Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 4 Apr 2024 14:04:05 +0200 Subject: [PATCH 1/3] test(e2e): Extract event-proxy-server into dev package So we can avoid duplicating this logic everywhere. --- .../test-applications/angular-17/package.json | 1 + .../angular-17/start-event-proxy.ts | 2 +- .../angular-17/tests/errors.test.ts | 2 +- .../angular-17/tests/performance.test.ts | 2 +- .../event-proxy-server.ts | 253 ------------------ .../package.json | 1 + .../start-event-proxy.ts | 2 +- .../tests/behaviour-server.test.ts | 2 +- .../create-remix-app-v2/event-proxy-server.ts | 253 ------------------ .../create-remix-app-v2/package.json | 1 + .../create-remix-app-v2/start-event-proxy.ts | 2 +- .../tests/behaviour-server.test.ts | 2 +- .../create-remix-app/event-proxy-server.ts | 253 ------------------ .../create-remix-app/package.json | 1 + .../create-remix-app/start-event-proxy.ts | 2 +- .../tests/behaviour-server.test.ts | 2 +- .../event-proxy-server.ts | 253 ------------------ .../esm-loader-node-express-app/package.json | 1 + .../start-event-proxy.ts | 2 +- .../tests/server.test.ts | 2 +- .../nextjs-14/event-proxy-server.ts | 253 ------------------ .../test-applications/nextjs-14/package.json | 1 + .../nextjs-14/start-event-proxy.ts | 2 +- .../tests/generation-functions.test.ts | 2 +- .../tests/request-instrumentation.test.ts | 2 +- .../nextjs-app-dir/event-proxy-server.ts | 253 ------------------ .../nextjs-app-dir/package.json | 1 + .../nextjs-app-dir/start-event-proxy.ts | 2 +- .../tests/async-context-edge.test.ts | 2 +- ...client-app-routing-instrumentation.test.ts | 2 +- .../connected-servercomponent-trace.test.ts | 2 +- .../tests/devErrorSymbolification.test.ts | 2 +- .../nextjs-app-dir/tests/edge-route.test.ts | 2 +- .../nextjs-app-dir/tests/edge.test.ts | 2 +- .../nextjs-app-dir/tests/exceptions.test.ts | 2 +- .../nextjs-app-dir/tests/middleware.test.ts | 2 +- .../tests/pages-ssr-errors.test.ts | 2 +- .../tests/request-instrumentation.test.ts | 2 +- .../tests/route-handlers.test.ts | 2 +- .../nextjs-app-dir/tests/transactions.test.ts | 2 +- .../node-express-app/event-proxy-server.ts | 253 ------------------ .../node-express-app/package.json | 1 + .../node-express-app/start-event-proxy.ts | 2 +- .../node-express-app/tests/server.test.ts | 2 +- .../node-fastify-app/event-proxy-server.ts | 253 ------------------ .../node-fastify-app/package.json | 1 + .../node-fastify-app/start-event-proxy.ts | 2 +- .../node-fastify-app/tests/errors.test.ts | 2 +- .../tests/propagation.test.ts | 2 +- .../tests/transactions.test.ts | 2 +- .../node-hapi-app/event-proxy-server.ts | 253 ------------------ .../node-hapi-app/package.json | 1 + .../node-hapi-app/start-event-proxy.ts | 2 +- .../node-hapi-app/tests/server.test.ts | 2 +- .../sveltekit-2/event-proxy-server.ts | 253 ------------------ .../sveltekit-2/package.json | 1 + .../sveltekit-2/start-event-proxy.ts | 2 +- .../sveltekit-2/test/errors.client.test.ts | 2 +- .../sveltekit-2/test/errors.server.test.ts | 2 +- .../test/performance.client.test.ts | 2 +- .../test/performance.server.test.ts | 2 +- .../sveltekit-2/test/performance.test.ts | 2 +- .../sveltekit-2/test/utils.ts | 2 +- .../sveltekit/event-proxy-server.ts | 253 ------------------ .../test-applications/sveltekit/package.json | 1 + .../sveltekit/start-event-proxy.ts | 2 +- .../sveltekit/test/errors.client.test.ts | 2 +- .../sveltekit/test/errors.server.test.ts | 2 +- .../sveltekit/test/performance.server.test.ts | 2 +- .../test-applications/sveltekit/utils.ts | 2 +- .../vue-3/event-proxy-server.ts | 253 ------------------ .../test-applications/vue-3/package.json | 1 + .../vue-3/start-event-proxy.ts | 2 +- .../vue-3/tests/errors.test.ts | 2 +- .../vue-3/tests/performance.test.ts | 2 +- dev-packages/event-proxy-server/.eslintrc.js | 7 + dev-packages/event-proxy-server/package.json | 50 ++++ .../event-proxy-server/rollup.npm.config.mjs | 13 + .../src}/event-proxy-server.ts | 9 +- dev-packages/event-proxy-server/src/index.ts | 7 + dev-packages/event-proxy-server/tsconfig.json | 5 + .../event-proxy-server/tsconfig.types.json | 10 + package.json | 1 + packages/node/package.json | 10 +- yarn.lock | 2 + 85 files changed, 170 insertions(+), 3093 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-v2/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts delete mode 100644 dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts create mode 100644 dev-packages/event-proxy-server/.eslintrc.js create mode 100644 dev-packages/event-proxy-server/package.json create mode 100644 dev-packages/event-proxy-server/rollup.npm.config.mjs rename dev-packages/{e2e-tests/test-applications/angular-17 => event-proxy-server/src}/event-proxy-server.ts (95%) create mode 100644 dev-packages/event-proxy-server/src/index.ts create mode 100644 dev-packages/event-proxy-server/tsconfig.json create mode 100644 dev-packages/event-proxy-server/tsconfig.types.json diff --git a/dev-packages/e2e-tests/test-applications/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json index e5609c250659..470633dcc2cd 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json @@ -29,6 +29,7 @@ "zone.js": "~0.14.3" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@angular-devkit/build-angular": "^17.1.1", "@angular/cli": "^17.1.1", "@angular/compiler-cli": "^17.1.0", diff --git a/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts index 56fe43416adc..7bf4c4417f3c 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts index 4666893b1882..28e07284b435 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('sends an error', async ({ page }) => { const errorPromise = waitForError('angular-17', async errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts index 9fc4b74e779f..7873af286315 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { waitForTransaction } from '../event-proxy-server'; test('sends a pageload transaction with a parameterized URL', async ({ page }) => { const transactionPromise = waitForTransaction('angular-17', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json index 4310039b0638..9d1dc24b3bc4 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json @@ -25,6 +25,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "^2.7.2", "@sentry/types": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts index e56a52190e63..8ba8517f5f8a 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, proxyServerName: 'create-remix-app-express-vite-dev', diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts index 09cee3ca79bc..428dcb6d8668 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { uuid4 } from '@sentry/utils'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { // We use this to identify the transactions diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json index 646bc5f21e25..c045592e830a 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json @@ -21,6 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "2.7.2", "@remix-run/eslint-config": "2.7.2", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts index cc810192de58..069eee5c7f34 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, proxyServerName: 'create-remix-app-v2', diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts index 992a315af3d3..42a5344c5e79 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { uuid4 } from '@sentry/utils'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { // We use this to identify the transactions diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json index 365fd9fb0bac..886ced1679b1 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json @@ -21,6 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "^1.19.3", "@remix-run/eslint-config": "^1.19.3", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts index 93755c9d232e..55a03fbb64d9 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, proxyServerName: 'create-remix-app', diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts index d0d737e44a69..3001f3c559ff 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { uuid4 } from '@sentry/utils'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { // We use this to identify the transactions diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json index 8ed10b0e6c58..279da0fbd3e8 100644 --- a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json @@ -17,6 +17,7 @@ "typescript": "4.9.5" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.27.1", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/start-event-proxy.ts index 25ef6807cb93..8ea4d3e9a251 100644 --- a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/tests/server.test.ts index a41034ca1af4..b11e0849f63a 100644 --- a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/tests/server.test.ts +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/tests/server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should record exceptions captured inside handlers', async ({ request }) => { const errorEventPromise = waitForError('esm-loader-node-express-app', errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index d0c41456d260..cd6754c6a822 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -26,6 +26,7 @@ "wait-port": "1.0.4" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@sentry-internal/feedback": "latest || *", "@sentry-internal/replay-canvas": "latest || *", "@sentry-internal/tracing": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts index eb83fd6fb82d..476672c34359 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts index e52dde8db258..52c28e1d974a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should send a transaction event for a generateMetadata() function invokation', async ({ page }) => { const testTitle = 'foobarasdf'; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts index cba6fd0f8699..c0a24f747d56 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should send a transaction with a fetch span', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/event-proxy-server.ts deleted file mode 100644 index 9d839e6c197b..000000000000 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); - - return new Promise((resolve, reject) => { - const request = http.request(`http://127.0.0.1:${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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 2a9377a52319..a327ae517997 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -29,6 +29,7 @@ "@playwright/test": "^1.27.1" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@sentry-internal/feedback": "latest || *", "@sentry-internal/replay-canvas": "latest || *", "@sentry-internal/tracing": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts index ef8d204c5224..d908b1d1c737 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts index 1465c560a36c..4696534e1733 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should allow for async context isolation in the edge SDK', async ({ request }) => { // test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts index d52cd4f18893..6fd69315b264 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Creates a pageload transaction for app router routes', async ({ page }) => { const randomRoute = String(Math.random()); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts index 4acc41814d3c..c63348304cda 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Will capture a connected trace for all server components and generation functions when visiting a page', async ({ page, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts index 09e8db8b2abf..cb2612c09403 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; test.describe('dev mode error symbolification', () => { if (process.env.TEST_ENV !== 'development') { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts index 9c19506392f8..cbe9dcafae71 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should create a transaction for edge routes', async ({ request }) => { const edgerouteTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts index 83dc125f2d4d..4e69abbdd3e2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should record exceptions for faulty edge server components', async ({ page }) => { const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts index 6e50d10fb333..a9d1abbaf2aa 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForError } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts index 2dd20e173d98..240e04ebe37f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should create a transaction for middleware', async ({ request }) => { const middlewareTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts index 2dbefd1441e2..73f8bd5e31b9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Will capture error for SSR rendering error with a connected trace (Class Component)', async ({ page }) => { const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts index 6ee318fe8e91..bd6a27cecced 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; // Note(lforst): I officially declare bancruptcy on this test. I tried a million ways to make it work but it kept flaking. // Sometimes the request span was included in the handler span, more often it wasn't. I have no idea why. Maybe one day we will diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 9bb784c39271..70f9bb32d3bc 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; test('Should create a transaction for route handlers', async ({ request }) => { const routehandlerTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index 3e146433defc..57ddb57f75cf 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForTransaction } from '../event-proxy-server'; const packageJson = require('../package.json'); diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-express-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/package.json b/dev-packages/e2e-tests/test-applications/node-express-app/package.json index ca4849e3f090..b4681d9ea515 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-app/package.json @@ -23,6 +23,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.27.1", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts index 376afc851351..369041a9c792 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts index 3f146319ed58..b08dca4dd299 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/tests/server.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import axios, { AxiosError, AxiosResponse } from 'axios'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; import type { AppRouter } from '../src/app'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json index c7ea9cac71ad..c0fa779050d8 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json @@ -21,6 +21,7 @@ "ts-node": "10.9.1" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.38.1" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts index 2ab9be450dcd..cb3a189ed920 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts index b2ef0649472f..e9ee378fa00f 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForError } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts index 411863f54cfb..11d8e896b2aa 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts @@ -1,8 +1,8 @@ import crypto from 'crypto'; import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { SpanJSON } from '@sentry/types'; import axios from 'axios'; -import { waitForTransaction } from '../event-proxy-server'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts index 54f6916de36c..6d3ca7e8d6fd 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForTransaction } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json b/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json index e463d02a73e6..964cdef4291e 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json @@ -18,6 +18,7 @@ "typescript": "4.9.5" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.27.1", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts index 7a3ed463e2ae..f1f5cf4b3316 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts index 061278dded50..d50c68ac4e72 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; import axios, { AxiosError } from 'axios'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index deac95bf05c0..1559503c4cfa 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -18,6 +18,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts index 3af64eb5960a..fcf2c0b9addc 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts index 796d34d7623a..10bd64a097f2 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from './utils'; test.describe('client-side errors', () => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts index 5240489b0934..ffdfad2932c8 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; test.describe('server-side errors', () => { test('captures universal load error', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts index 66be1892123d..11e647ff07ff 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from './utils'; test.describe('client-specific performance events', () => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.server.test.ts index 7aec23b30d7a..e04a056ca875 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('server pageload request span has nested request span for sub request', async ({ page }) => { const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts index c7b895f90b6c..adfeb4e31d85 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from './utils'; test.describe('performance events', () => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts index b48b949abdd5..6aab192a18af 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts @@ -1,5 +1,5 @@ import { Page } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; /** * Helper function that waits for the initial pageload to complete. diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts deleted file mode 100644 index eba9ffef8682..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -async function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return await readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/package.json b/dev-packages/e2e-tests/test-applications/sveltekit/package.json index 5a6b2d4d083c..80d5e1444ada 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit/package.json @@ -17,6 +17,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.41.1", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts index db7640e0d825..cb0fd75c1530 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts index ee366338391d..47f4c9d9e380 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from '../utils'; test.describe('client-side errors', () => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts index c36d44c80068..6274239a936b 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; test.describe('server-side errors', () => { test('captures universal load error', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts index 49fde5f01045..a363d28f291b 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('server pageload request span has nested request span for sub request', async ({ page }) => { const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts index 2886873bb8fb..a056c030a491 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts @@ -1,5 +1,5 @@ import { Page } from '@playwright/test'; -import { waitForTransaction } from './event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; /** * Helper function that waits for the initial pageload to complete. diff --git a/dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts deleted file mode 100644 index d14ca5cb5e72..000000000000 --- a/dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts +++ /dev/null @@ -1,253 +0,0 @@ -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 { - 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(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(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, -): Promise { - 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); - }); - - 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, -): Promise { - 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, -): Promise { - 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, -): Promise { - 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 { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - await writeFile(tmpFilePath, port, { encoding: 'utf8' }); -} - -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); -} diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json index 5de1fdc7d2e9..138acb88c61e 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -20,6 +20,7 @@ "vue-router": "^4.2.5" }, "devDependencies": { + "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", "@playwright/test": "^1.41.1", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts index 6435984ad069..e8c8fdf3cd46 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts @@ -1,4 +1,4 @@ -import { startEventProxyServer } from './event-proxy-server'; +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; startEventProxyServer({ port: 3031, diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts index b9933299c8c0..1eadf844b4a2 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError } from '@sentry-internal/event-proxy-server'; test('sends an error', async ({ page }) => { const errorPromise = waitForError('vue-3', async errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts index dc5bd500eee3..aded68211784 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; test('sends a pageload transaction with a parameterized URL', async ({ page }) => { const transactionPromise = waitForTransaction('vue-3', async transactionEvent => { diff --git a/dev-packages/event-proxy-server/.eslintrc.js b/dev-packages/event-proxy-server/.eslintrc.js new file mode 100644 index 000000000000..175b9389af00 --- /dev/null +++ b/dev-packages/event-proxy-server/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [], +}; diff --git a/dev-packages/event-proxy-server/package.json b/dev-packages/event-proxy-server/package.json new file mode 100644 index 000000000000..3083b97a2b7e --- /dev/null +++ b/dev-packages/event-proxy-server/package.json @@ -0,0 +1,50 @@ +{ + "private": true, + "version": "8.0.0-alpha.7", + "name": "@sentry-internal/event-proxy-server", + + "author": "Sentry", + "license": "MIT", + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "files": [ + "cjs", + "esm", + "types", + "types-ts3.8" + ], + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, + "sideEffects": false, + "engines": { + "node": ">=14.18" + }, + "scripts": { + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "build": "run-s build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "tsc -p tsconfig.types.json", + "clean": "rimraf -g ./node_modules ./build" + }, + "dependencies": { + "@sentry/types": "8.0.0-alpha.7", + "@sentry/utils": "8.0.0-alpha.7" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/event-proxy-server/rollup.npm.config.mjs b/dev-packages/event-proxy-server/rollup.npm.config.mjs new file mode 100644 index 000000000000..b684e2efe16b --- /dev/null +++ b/dev-packages/event-proxy-server/rollup.npm.config.mjs @@ -0,0 +1,13 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + preserveModules: false, + }, + }, + }), +); diff --git a/dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts b/dev-packages/event-proxy-server/src/event-proxy-server.ts similarity index 95% rename from dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts rename to dev-packages/event-proxy-server/src/event-proxy-server.ts index d14ca5cb5e72..36be080fb097 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts +++ b/dev-packages/event-proxy-server/src/event-proxy-server.ts @@ -50,13 +50,13 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() : Buffer.concat(proxyRequestChunks).toString(); - let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + const envelopeHeader: EnvelopeItem[0] = 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 { origin, pathname, host } = new URL(envelopeHeader.dsn as string); const projectId = pathname.substring(1); const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; @@ -131,6 +131,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P const eventCallbackServerStartupPromise = new Promise(resolve => { eventCallbackServer.listen(0, () => { const port = String((eventCallbackServer.address() as AddressInfo).port); + // eslint-disable-next-line @typescript-eslint/no-floating-promises void registerCallbackServerPort(options.proxyServerName, port).then(resolve); }); }); @@ -140,6 +141,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P return; } +/** Wait for a request to be sent. */ export async function waitForRequest( proxyServerName: string, callback: (eventData: SentryRequestCallbackData) => Promise | boolean, @@ -190,6 +192,7 @@ export async function waitForRequest( }); } +/** Wait for a specific envelope item to be sent. */ export function waitForEnvelopeItem( proxyServerName: string, callback: (envelopeItem: EnvelopeItem) => Promise | boolean, @@ -208,6 +211,7 @@ export function waitForEnvelopeItem( }); } +/** Wait for an error to be sent. */ export function waitForError( proxyServerName: string, callback: (transactionEvent: Event) => Promise | boolean, @@ -224,6 +228,7 @@ export function waitForError( }); } +/** Wait for a transaction to be sent. */ export function waitForTransaction( proxyServerName: string, callback: (transactionEvent: Event) => Promise | boolean, diff --git a/dev-packages/event-proxy-server/src/index.ts b/dev-packages/event-proxy-server/src/index.ts new file mode 100644 index 000000000000..9ee4dfc54520 --- /dev/null +++ b/dev-packages/event-proxy-server/src/index.ts @@ -0,0 +1,7 @@ +export { + startEventProxyServer, + waitForEnvelopeItem, + waitForError, + waitForRequest, + waitForTransaction, +} from './event-proxy-server'; diff --git a/dev-packages/event-proxy-server/tsconfig.json b/dev-packages/event-proxy-server/tsconfig.json new file mode 100644 index 000000000000..825380109ceb --- /dev/null +++ b/dev-packages/event-proxy-server/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} diff --git a/dev-packages/event-proxy-server/tsconfig.types.json b/dev-packages/event-proxy-server/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/dev-packages/event-proxy-server/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/package.json b/package.json index ca479f3fc046..6ef476c56358 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "dev-packages/e2e-tests", "dev-packages/node-integration-tests", "dev-packages/overhead-metrics", + "dev-packages/event-proxy-server", "dev-packages/size-limit-gh-action", "dev-packages/rollup-utils" ], diff --git a/packages/node/package.json b/packages/node/package.json index 938dd837ad81..61c6f7d932d4 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -36,11 +36,11 @@ "default": "./build/register.mjs" } }, -"./hook": { - "import": { - "default": "./build/hook.mjs" - } -} + "./hook": { + "import": { + "default": "./build/hook.mjs" + } + } }, "typesVersions": { "<4.9": { diff --git a/yarn.lock b/yarn.lock index 9dcb472bcfd4..64bc3a06b8bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6647,6 +6647,7 @@ "@types/unist" "*" "@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": + name "@types/history-4" version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -25275,6 +25276,7 @@ react-is@^18.0.0: "@remix-run/router" "1.0.2" "react-router-6@npm:react-router@6.3.0", react-router@6.3.0: + name react-router-6 version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== From 4fd241580e360c5e62b6d451459c586ee68f1b7b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 4 Apr 2024 15:37:59 +0200 Subject: [PATCH 2/3] use link instead of file --- .../e2e-tests/test-applications/angular-17/package.json | 2 +- .../create-remix-app-express-vite-dev/package.json | 2 +- .../test-applications/create-remix-app-v2/package.json | 2 +- .../test-applications/create-remix-app/package.json | 2 +- .../esm-loader-node-express-app/package.json | 2 +- .../e2e-tests/test-applications/nextjs-14/package.json | 2 +- .../e2e-tests/test-applications/nextjs-app-dir/package.json | 2 +- .../test-applications/node-express-app/package.json | 2 +- .../test-applications/node-fastify-app/package.json | 2 +- .../e2e-tests/test-applications/node-hapi-app/package.json | 2 +- .../e2e-tests/test-applications/sveltekit-2/package.json | 2 +- .../e2e-tests/test-applications/sveltekit/package.json | 2 +- dev-packages/e2e-tests/test-applications/vue-3/package.json | 2 +- .../e2e-tests/test-applications/vue-3/playwright.config.ts | 5 +---- dev-packages/event-proxy-server/package.json | 1 - 15 files changed, 14 insertions(+), 18 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json index 470633dcc2cd..dee9c2921abe 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json @@ -29,7 +29,7 @@ "zone.js": "~0.14.3" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@angular-devkit/build-angular": "^17.1.1", "@angular/cli": "^17.1.1", "@angular/compiler-cli": "^17.1.0", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json index 9d1dc24b3bc4..98b587aa96cb 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json @@ -25,7 +25,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "^2.7.2", "@sentry/types": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json index c045592e830a..25f13aa5fd3f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json @@ -21,7 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "2.7.2", "@remix-run/eslint-config": "2.7.2", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json index 886ced1679b1..67d2f5ba4564 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json @@ -21,7 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@remix-run/dev": "^1.19.3", "@remix-run/eslint-config": "^1.19.3", diff --git a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json index 279da0fbd3e8..70055cdf8159 100644 --- a/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json +++ b/dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json @@ -17,7 +17,7 @@ "typescript": "4.9.5" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.27.1", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index cd6754c6a822..87ec5b57b91d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -26,7 +26,7 @@ "wait-port": "1.0.4" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@sentry-internal/feedback": "latest || *", "@sentry-internal/replay-canvas": "latest || *", "@sentry-internal/tracing": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index a327ae517997..501b9615dede 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -29,7 +29,7 @@ "@playwright/test": "^1.27.1" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@sentry-internal/feedback": "latest || *", "@sentry-internal/replay-canvas": "latest || *", "@sentry-internal/tracing": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/package.json b/dev-packages/e2e-tests/test-applications/node-express-app/package.json index b4681d9ea515..3ba52ef6f876 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-app/package.json @@ -23,7 +23,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.27.1", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json index c0fa779050d8..9e8779cf9bdd 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json @@ -21,7 +21,7 @@ "ts-node": "10.9.1" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.38.1" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json b/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json index 964cdef4291e..d63a3a6c457f 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-hapi-app/package.json @@ -18,7 +18,7 @@ "typescript": "4.9.5" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.27.1", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index 1559503c4cfa..42ac39f210a9 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -18,7 +18,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.36.2", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/package.json b/dev-packages/e2e-tests/test-applications/sveltekit/package.json index 80d5e1444ada..1fbbcfbcefac 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit/package.json @@ -17,7 +17,7 @@ "@sentry/sveltekit": "latest || *" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", "@playwright/test": "^1.41.1", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json index 138acb88c61e..a40000731607 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -20,7 +20,7 @@ "vue-router": "^4.2.5" }, "devDependencies": { - "@sentry-internal/event-proxy-server": "file:../../../event-proxy-server", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server/", "@playwright/test": "^1.41.1", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts b/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts index 16dd640e58ef..a5ebf85ba99c 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts @@ -65,10 +65,7 @@ const config: PlaywrightTestConfig = { port: eventProxyPort, }, { - command: - testEnv === 'development' - ? `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${vuePort}` - : `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${vuePort}`, + command: `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${vuePort}`, port: vuePort, }, ], diff --git a/dev-packages/event-proxy-server/package.json b/dev-packages/event-proxy-server/package.json index 3083b97a2b7e..78c58b271050 100644 --- a/dev-packages/event-proxy-server/package.json +++ b/dev-packages/event-proxy-server/package.json @@ -2,7 +2,6 @@ "private": true, "version": "8.0.0-alpha.7", "name": "@sentry-internal/event-proxy-server", - "author": "Sentry", "license": "MIT", "main": "build/cjs/index.js", From 3bfaf712b21e74c56f0446ac038a9f1ef572c13d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 4 Apr 2024 16:16:42 +0200 Subject: [PATCH 3/3] fix test? --- .../test-applications/sveltekit/test/performance.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts index 568423b3ea17..cb2ac4446a49 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server.js'; +import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { waitForInitialPageload } from '../utils.js'; test('sends a pageload transaction', async ({ page }) => {