diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 445c57284056..85fc105d0e42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1050,6 +1050,7 @@ jobs: 'node-express-esm-preload', 'node-express-esm-without-loader', 'node-express-cjs-preload', + 'node-otel-sdk-node', 'nextjs-app-dir', 'nextjs-14', 'nextjs-15', diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.gitignore b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.gitignore @@ -0,0 +1 @@ +dist diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.npmrc b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json new file mode 100644 index 000000000000..8a1634725184 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json @@ -0,0 +1,32 @@ +{ + "name": "node-otel-sdk-trace", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "node dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@opentelemetry/sdk-node": "0.52.1", + "@opentelemetry/exporter-trace-otlp-http": "0.52.1", + "@sentry/core": "latest || *", + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@sentry/types": "latest || *", + "@types/express": "4.17.17", + "@types/node": "18.15.1", + "express": "4.19.2", + "typescript": "4.9.5" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/playwright.config.mjs new file mode 100644 index 000000000000..888e61cfb2dc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/playwright.config.mjs @@ -0,0 +1,34 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig( + { + startCommand: `pnpm start`, + }, + { + webServer: [ + { + command: `node ./start-event-proxy.mjs`, + port: 3031, + stdout: 'pipe', + stderr: 'pipe', + }, + { + command: `node ./start-otel-proxy.mjs`, + port: 3032, + stdout: 'pipe', + stderr: 'pipe', + }, + { + command: 'pnpm start', + port: 3030, + stdout: 'pipe', + stderr: 'pipe', + env: { + PORT: 3030, + }, + }, + ], + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/app.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/app.ts new file mode 100644 index 000000000000..26779990f6d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/app.ts @@ -0,0 +1,53 @@ +import './instrument'; + +// Other imports below +import * as Sentry from '@sentry/node'; +import express from 'express'; + +const app = express(); +const port = 3030; + +app.get('/test-success', function (req, res) { + res.send({ version: 'v1' }); +}); + +app.get('/test-param/:param', function (req, res) { + res.send({ paramWas: req.params.param }); +}); + +app.get('/test-transaction', function (req, res) { + Sentry.withActiveSpan(null, async () => { + Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => { + Sentry.startSpan({ name: 'test-span' }, () => undefined); + }); + + await Sentry.flush(); + + res.send({}); + }); +}); + +app.get('/test-error', async function (req, res) { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + res.send({ exceptionId }); +}); + +app.get('/test-exception/:id', function (req, _res) { + throw new Error(`This is an exception with id ${req.params.id}`); +}); + +Sentry.setupExpressErrorHandler(app); + +app.use(function onError(err: unknown, req: any, res: any, next: any) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/instrument.ts new file mode 100644 index 000000000000..fb270e1252d3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/instrument.ts @@ -0,0 +1,35 @@ +const opentelemetry = require('@opentelemetry/sdk-node'); +const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); +const Sentry = require('@sentry/node'); +const { SentrySpanProcessor, SentryPropagator, SentrySampler } = require('@sentry/opentelemetry'); + +const sentryClient = Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: + process.env.E2E_TEST_DSN || + 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + includeLocalVariables: true, + debug: !!process.env.DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + + skipOpenTelemetrySetup: true, +}); + +const sdk = new opentelemetry.NodeSDK({ + sampler: sentryClient ? new SentrySampler(sentryClient) : undefined, + textMapPropagator: new SentryPropagator(), + contextManager: new Sentry.SentryContextManager(), + spanProcessors: [ + new SentrySpanProcessor(), + new opentelemetry.node.BatchSpanProcessor( + new OTLPTraceExporter({ + url: 'http://localhost:3032/', + }), + ), + ], +}); + +sdk.start(); + +Sentry.validateOpenTelemetrySetup(); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-event-proxy.mjs new file mode 100644 index 000000000000..8c74fa842a1b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-otel-sdk-trace', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-otel-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-otel-proxy.mjs new file mode 100644 index 000000000000..1cf9ef3e2c27 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-otel-proxy.mjs @@ -0,0 +1,6 @@ +import { startProxyServer } from '@sentry-internal/test-utils'; + +startProxyServer({ + port: 3032, + proxyServerName: 'node-otel-sdk-trace-otel', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/errors.test.ts new file mode 100644 index 000000000000..9cb97a051476 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends correct error event', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-otel-sdk-trace', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + await fetch(`${baseURL}/test-exception/123`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception/123', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception/:id'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts new file mode 100644 index 000000000000..39a7d27e9cb1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts @@ -0,0 +1,213 @@ +import { expect, test } from '@playwright/test'; +import { waitForPlainRequest, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-otel-sdk-trace', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + // Ensure we also send data to the OTLP endpoint + const otelPromise = waitForPlainRequest('node-otel-sdk-trace-otel', data => { + const json = JSON.parse(data) as any; + + return json.resourceSpans.length > 0; + }); + + await fetch(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + + const otelData = await otelPromise; + + // For now we do not test the actual shape of this, but only existence + expect(otelData).toBeDefined(); + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-transaction', + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + origin: 'auto.http.otel.http', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + 'http.route': '/', + 'express.name': 'query', + 'express.type': 'middleware', + 'otel.kind': 'INTERNAL', + }, + description: 'query', + op: 'middleware.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + 'http.route': '/', + 'express.name': 'expressInit', + 'express.type': 'middleware', + 'otel.kind': 'INTERNAL', + }, + description: 'expressInit', + op: 'middleware.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + 'http.route': '/test-transaction', + 'express.name': '/test-transaction', + 'express.type': 'request_handler', + 'otel.kind': 'INTERNAL', + }, + description: '/test-transaction', + op: 'request_handler.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); +}); + +test('Sends an API route transaction for an errored route', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-otel-sdk-trace', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.transaction === 'GET /test-exception/:id' && + transactionEvent.request?.url === 'http://localhost:3030/test-exception/777' + ); + }); + + await fetch(`${baseURL}/test-exception/777`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); + expect(transactionEvent.transaction).toEqual('GET /test-exception/:id'); + expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error'); + expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500); + + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + 'http.route': '/', + 'express.name': 'query', + 'express.type': 'middleware', + 'otel.kind': 'INTERNAL', + }, + description: 'query', + op: 'middleware.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + 'http.route': '/', + 'express.name': 'expressInit', + 'express.type': 'middleware', + 'otel.kind': 'INTERNAL', + }, + description: 'expressInit', + op: 'middleware.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + 'http.route': '/test-exception/:id', + 'express.name': '/test-exception/:id', + 'express.type': 'request_handler', + 'otel.kind': 'INTERNAL', + }, + description: '/test-exception/:id', + op: 'request_handler.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tsconfig.json new file mode 100644 index 000000000000..8cb64e989ed9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["es2018"], + "strict": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 82693bf4af89..30bedadc38bb 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -1,6 +1,5 @@ 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'; @@ -31,11 +30,27 @@ interface SentryRequestCallbackData { sentryResponseStatusCode?: number; } +type OnRequest = ( + eventCallbackListeners: Set<(data: string) => void>, + proxyRequest: http.IncomingMessage, + proxyRequestBody: string, +) => Promise<[number, string, Record | undefined]>; + /** - * 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}/`). + * Start a generic proxy server. + * The `onRequest` callback receives the incoming request and the request body, + * and should return a promise that resolves to a tuple with: + * statusCode, responseBody, responseHeaders */ -export async function startEventProxyServer(options: EventProxyServerOptions): Promise { +export async function startProxyServer( + options: { + /** 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; + }, + onRequest?: OnRequest, +): Promise { const eventCallbackListeners: Set<(data: string) => void> = new Set(); const proxyServer = http.createServer((proxyRequest, proxyResponse) => { @@ -59,102 +74,29 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() : Buffer.concat(proxyRequestChunks).toString(); - const envelopeHeader: EnvelopeItem[0] = JSON.parse(proxyRequestBody.split('\n')[0] as string); - - const shouldForwardEventToSentry = options.forwardToSentry != null ? options.forwardToSentry : true; - - if (!envelopeHeader.dsn && shouldForwardEventToSentry) { - // eslint-disable-next-line no-console - console.log( - '[event-proxy-server] Warn: No dsn on envelope header. Maybe a client-report was received. Proxy request body:', - proxyRequestBody, - ); - - proxyResponse.writeHead(200); - proxyResponse.write('{}', 'utf-8'); - proxyResponse.end(); - return; - } - - if (!shouldForwardEventToSentry) { - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody: '', - sentryResponseStatusCode: 200, - }; - eventCallbackListeners.forEach(listener => { - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - - proxyResponse.writeHead(200); - proxyResponse.write('{}', 'utf-8'); - proxyResponse.end(); - return; - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn as string); - - 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(); - - try { - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: sentryResponse.statusCode, - }; - - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - } catch (error) { - if (`${error}`.includes('Unexpected token') && proxyRequestBody.includes('{"type":"replay_event"}')) { - // eslint-disable-next-line no-console - console.log('[event-proxy-server] Info: Received replay event, skipping...'); - } else { - // eslint-disable-next-line no-console - console.error( - '[event-proxy-server] Error: Failed to parse Sentry request envelope', - error, - proxyRequestBody, - ); - } - } - }); - proxyResponse.end(); + const callback: OnRequest = + onRequest || + (async (eventCallbackListeners, proxyRequest, proxyRequestBody) => { + eventCallbackListeners.forEach(listener => { + listener(proxyRequestBody); }); - sentryResponse.addListener('error', err => { - // eslint-disable-next-line no-console - console.log('[event-proxy-server] Warn: Proxying to Sentry returned an error!', err); - proxyResponse.writeHead(500); - proxyResponse.write('{}', 'utf-8'); - proxyResponse.end(); - }); - - proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); - }, - ); + return [200, '{}', {}]; + }); - sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); - sentryRequest.end(); + callback(eventCallbackListeners, proxyRequest, proxyRequestBody) + .then(([statusCode, responseBody, responseHeaders]) => { + proxyResponse.writeHead(statusCode, responseHeaders); + proxyResponse.write(responseBody, 'utf-8'); + proxyResponse.end(); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.log('[event-proxy-server] Warn: Proxy server returned an error', error); + proxyResponse.writeHead(500); + proxyResponse.write('{}', 'utf-8'); + proxyResponse.end(); + }); }); }); @@ -193,7 +135,113 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P await eventCallbackServerStartupPromise; await proxyServerStartupPromise; - return; +} + +/** + * 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 { + await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody) => { + const envelopeHeader: EnvelopeItem[0] = JSON.parse(proxyRequestBody.split('\n')[0] as string); + + const shouldForwardEventToSentry = options.forwardToSentry != null ? options.forwardToSentry : true; + + if (!envelopeHeader.dsn && shouldForwardEventToSentry) { + // eslint-disable-next-line no-console + console.log( + '[event-proxy-server] Warn: No dsn on envelope header. Maybe a client-report was received. Proxy request body:', + proxyRequestBody, + ); + + return [200, '{}', {}]; + } + + if (!shouldForwardEventToSentry) { + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody: '', + sentryResponseStatusCode: 200, + }; + eventCallbackListeners.forEach(listener => { + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + + return [200, '{}', {}]; + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn as string); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const reqHeaders: Record = {}; + for (const [key, value] of Object.entries(proxyRequest.headers)) { + reqHeaders[key] = value as string; + } + + // Fetch does not like this + delete reqHeaders['transfer-encoding']; + + return fetch(sentryIngestUrl, { + body: proxyRequestBody, + headers: reqHeaders, + method: proxyRequest.method, + }).then(async res => { + const rawSentryResponseBody = await res.text(); + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: res.status, + }; + + eventCallbackListeners.forEach(listener => { + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + + const resHeaders: Record = {}; + for (const [key, value] of res.headers.entries()) { + resHeaders[key] = value; + } + + return [res.status, rawSentryResponseBody, resHeaders]; + }); + }); +} + +/** Wait for any plain request being made to the proxy. */ +export async function waitForPlainRequest( + proxyServerName: string, + callback: (eventData: string) => 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'); + + eventContents = eventContents.concat(chunkString); + + if (callback(eventContents)) { + response.destroy(); + return resolve(eventContents); + } + }); + }); + + request.end(); + }); } /** Wait for a request to be sent. */ diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index 4425d2688800..49685a6b18c2 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -1,9 +1,11 @@ export { + startProxyServer, startEventProxyServer, waitForEnvelopeItem, waitForError, waitForRequest, waitForTransaction, + waitForPlainRequest, } from './event-proxy-server'; export { getPlaywrightConfig } from './playwright-config';