From 482286155ac537cfc96e37ef335973ac17121bb3 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 23 Feb 2023 14:23:47 +0000 Subject: [PATCH 1/7] test: Add test utitilty to intercept sentry requests --- package.json | 1 + packages/event-proxy-server/.eslintrc.js | 9 + packages/event-proxy-server/LICENSE | 14 ++ packages/event-proxy-server/package.json | 24 +++ packages/event-proxy-server/src/index.ts | 209 ++++++++++++++++++++++ packages/event-proxy-server/tsconfig.json | 8 + 6 files changed, 265 insertions(+) create mode 100644 packages/event-proxy-server/.eslintrc.js create mode 100644 packages/event-proxy-server/LICENSE create mode 100644 packages/event-proxy-server/package.json create mode 100644 packages/event-proxy-server/src/index.ts create mode 100644 packages/event-proxy-server/tsconfig.json diff --git a/package.json b/package.json index bd6bd97c3970..48ee8c0f781b 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "packages/ember", "packages/eslint-config-sdk", "packages/eslint-plugin-sdk", + "packages/event-proxy-server", "packages/gatsby", "packages/hub", "packages/integration-tests", diff --git a/packages/event-proxy-server/.eslintrc.js b/packages/event-proxy-server/.eslintrc.js new file mode 100644 index 000000000000..f4142bfee21b --- /dev/null +++ b/packages/event-proxy-server/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + parserOptions: { + sourceType: 'module', + }, +}; diff --git a/packages/event-proxy-server/LICENSE b/packages/event-proxy-server/LICENSE new file mode 100644 index 000000000000..d11896ba1181 --- /dev/null +++ b/packages/event-proxy-server/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/event-proxy-server/package.json b/packages/event-proxy-server/package.json new file mode 100644 index 000000000000..78a111333caa --- /dev/null +++ b/packages/event-proxy-server/package.json @@ -0,0 +1,24 @@ +{ + "name": "@sentry-internal/event-proxy-server", + "version": "7.38.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "type": "module", + "module": "build/index.js", + "private": true, + "scripts": { + "build": "tsc --project tsconfig.json", + "build:transpile": "tsc --project tsconfig.json" + }, + "devDependencies": { + "typescript": "3.8.3", + "express": "4.18.2", + "@sentry/utils": "7.38.0", + "@sentry/types": "7.38.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/event-proxy-server/src/index.ts b/packages/event-proxy-server/src/index.ts new file mode 100644 index 000000000000..1d57a8e1baad --- /dev/null +++ b/packages/event-proxy-server/src/index.ts @@ -0,0 +1,209 @@ +import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import { parseEnvelope } from '@sentry/utils'; +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'; + +interface SentryRequestCallbackData { + envelope: Envelope; + rawProxyRequestBody: string; + rawSentryResponseBody: string; + sentryResponseStatusCode?: number; +} + +export async function startEventProxyServer(options: { port: number; proxyServerName: string }): 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 = Buffer.concat(proxyRequestChunks).toString(); + const envelopeHeader: { dsn?: string } = 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 rawProxyRequestBody = Buffer.concat(proxyRequestChunks).toString(); + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(rawProxyRequestBody, new TextEncoder(), new TextDecoder()), + rawProxyRequestBody, + 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 => { + const listener = eventCallbackServer.listen(0, () => { + const port = String((listener.address() as AddressInfo).port); + const tmpFileWithPort = path.join(os.tmpdir(), `event-proxy-server-${options.proxyServerName}`); + fs.writeFileSync(tmpFileWithPort, port, { encoding: 'utf8' }); + resolve(); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => boolean, +): Promise { + const tmpFileWithPort = path.join(os.tmpdir(), `event-proxy-server-${proxyServerName}`); + const eventCallbackServerPort = fs.readFileSync(tmpFileWithPort, 'utf8'); + + 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'), + ); + if (callback(eventCallbackData)) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError(proxyServerName: string, callback: (transactionEvent: Event) => boolean): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && callback(envelopeItemBody as Event)) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && callback(envelopeItemBody as Event)) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} diff --git a/packages/event-proxy-server/tsconfig.json b/packages/event-proxy-server/tsconfig.json new file mode 100644 index 000000000000..9ab8207a9d81 --- /dev/null +++ b/packages/event-proxy-server/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "outDir": "build" + }, + "include": ["src/**/*.ts"] +} From 2e832cab65feb69475ab362401ac4f678d84cdec Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 23 Feb 2023 14:50:13 +0000 Subject: [PATCH 2/7] Add some comments --- packages/event-proxy-server/src/index.ts | 13 ++++++++++++- packages/event-proxy-server/tsconfig.json | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/event-proxy-server/src/index.ts b/packages/event-proxy-server/src/index.ts index 1d57a8e1baad..35966a7d3e1c 100644 --- a/packages/event-proxy-server/src/index.ts +++ b/packages/event-proxy-server/src/index.ts @@ -7,6 +7,13 @@ import type { AddressInfo } from 'net'; import * as os from 'os'; import * as path from 'path'; +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; @@ -14,7 +21,11 @@ interface SentryRequestCallbackData { sentryResponseStatusCode?: number; } -export async function startEventProxyServer(options: { port: number; proxyServerName: string }): Promise { +/** + * 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) => { diff --git a/packages/event-proxy-server/tsconfig.json b/packages/event-proxy-server/tsconfig.json index 9ab8207a9d81..8b6bbaf70de7 100644 --- a/packages/event-proxy-server/tsconfig.json +++ b/packages/event-proxy-server/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "module": "esnext", + "module": "ES2015", "outDir": "build" }, "include": ["src/**/*.ts"] From b41ce3b208484f1b1902d9e55bcbf04ba579c3f0 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 23 Feb 2023 16:43:05 +0000 Subject: [PATCH 3/7] cjs --- packages/event-proxy-server/package.json | 5 ++--- packages/event-proxy-server/tsconfig.json | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/event-proxy-server/package.json b/packages/event-proxy-server/package.json index 78a111333caa..44eca4eb341c 100644 --- a/packages/event-proxy-server/package.json +++ b/packages/event-proxy-server/package.json @@ -5,8 +5,8 @@ "engines": { "node": ">=10" }, - "type": "module", - "module": "build/index.js", + "main": "build/index.js", + "types": "build/index.d.ts", "private": true, "scripts": { "build": "tsc --project tsconfig.json", @@ -14,7 +14,6 @@ }, "devDependencies": { "typescript": "3.8.3", - "express": "4.18.2", "@sentry/utils": "7.38.0", "@sentry/types": "7.38.0" }, diff --git a/packages/event-proxy-server/tsconfig.json b/packages/event-proxy-server/tsconfig.json index 8b6bbaf70de7..3e5752b2cd24 100644 --- a/packages/event-proxy-server/tsconfig.json +++ b/packages/event-proxy-server/tsconfig.json @@ -1,8 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "module": "ES2015", - "outDir": "build" + "module": "CommonJS", + "outDir": "build", + "declaration": true, + "esModuleInterop": true }, "include": ["src/**/*.ts"] } From 94daab4f0e8e0dc076688b36ee1f76511d8a0a1f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 23 Feb 2023 21:00:31 +0000 Subject: [PATCH 4/7] move --- .../test-utils/event-proxy-server.ts} | 0 packages/event-proxy-server/.eslintrc.js | 9 -------- packages/event-proxy-server/LICENSE | 14 ----------- packages/event-proxy-server/package.json | 23 ------------------- packages/event-proxy-server/tsconfig.json | 10 -------- 5 files changed, 56 deletions(-) rename packages/{event-proxy-server/src/index.ts => e2e-tests/test-utils/event-proxy-server.ts} (100%) delete mode 100644 packages/event-proxy-server/.eslintrc.js delete mode 100644 packages/event-proxy-server/LICENSE delete mode 100644 packages/event-proxy-server/package.json delete mode 100644 packages/event-proxy-server/tsconfig.json diff --git a/packages/event-proxy-server/src/index.ts b/packages/e2e-tests/test-utils/event-proxy-server.ts similarity index 100% rename from packages/event-proxy-server/src/index.ts rename to packages/e2e-tests/test-utils/event-proxy-server.ts diff --git a/packages/event-proxy-server/.eslintrc.js b/packages/event-proxy-server/.eslintrc.js deleted file mode 100644 index f4142bfee21b..000000000000 --- a/packages/event-proxy-server/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - env: { - node: true, - }, - extends: ['../../.eslintrc.js'], - parserOptions: { - sourceType: 'module', - }, -}; diff --git a/packages/event-proxy-server/LICENSE b/packages/event-proxy-server/LICENSE deleted file mode 100644 index d11896ba1181..000000000000 --- a/packages/event-proxy-server/LICENSE +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the -Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/event-proxy-server/package.json b/packages/event-proxy-server/package.json deleted file mode 100644 index 44eca4eb341c..000000000000 --- a/packages/event-proxy-server/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@sentry-internal/event-proxy-server", - "version": "7.38.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "main": "build/index.js", - "types": "build/index.d.ts", - "private": true, - "scripts": { - "build": "tsc --project tsconfig.json", - "build:transpile": "tsc --project tsconfig.json" - }, - "devDependencies": { - "typescript": "3.8.3", - "@sentry/utils": "7.38.0", - "@sentry/types": "7.38.0" - }, - "volta": { - "extends": "../../package.json" - } -} diff --git a/packages/event-proxy-server/tsconfig.json b/packages/event-proxy-server/tsconfig.json deleted file mode 100644 index 3e5752b2cd24..000000000000 --- a/packages/event-proxy-server/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "module": "CommonJS", - "outDir": "build", - "declaration": true, - "esModuleInterop": true - }, - "include": ["src/**/*.ts"] -} From aadeb29d3b00672c86718b3550675759f9063fdf Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 24 Feb 2023 14:38:22 +0100 Subject: [PATCH 5/7] Update package.json Co-authored-by: Abhijeet Prasad --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 48ee8c0f781b..bd6bd97c3970 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "packages/ember", "packages/eslint-config-sdk", "packages/eslint-plugin-sdk", - "packages/event-proxy-server", "packages/gatsby", "packages/hub", "packages/integration-tests", From 9090f797dcbf9986c216b6ee464cc7aa628badcc Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 24 Feb 2023 15:24:19 +0000 Subject: [PATCH 6/7] Extract server port registration into helper function --- .../test-utils/event-proxy-server.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/e2e-tests/test-utils/event-proxy-server.ts b/packages/e2e-tests/test-utils/event-proxy-server.ts index 35966a7d3e1c..7b3251cf0a4c 100644 --- a/packages/e2e-tests/test-utils/event-proxy-server.ts +++ b/packages/e2e-tests/test-utils/event-proxy-server.ts @@ -6,6 +6,10 @@ 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'; + +const readFile = util.promisify(fs.readFile); +const writeFile = util.promisify(fs.writeFile); interface EventProxyServerOptions { /** Port to start the event proxy server at. */ @@ -123,9 +127,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P const eventCallbackServerStartupPromise = new Promise(resolve => { const listener = eventCallbackServer.listen(0, () => { const port = String((listener.address() as AddressInfo).port); - const tmpFileWithPort = path.join(os.tmpdir(), `event-proxy-server-${options.proxyServerName}`); - fs.writeFileSync(tmpFileWithPort, port, { encoding: 'utf8' }); - resolve(); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); }); }); @@ -134,12 +136,11 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P return; } -export function waitForRequest( +export async function waitForRequest( proxyServerName: string, callback: (eventData: SentryRequestCallbackData) => boolean, ): Promise { - const tmpFileWithPort = path.join(os.tmpdir(), `event-proxy-server-${proxyServerName}`); - const eventCallbackServerPort = fs.readFileSync(tmpFileWithPort, 'utf8'); + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); return new Promise((resolve, reject) => { const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { @@ -218,3 +219,15 @@ export function waitForTransaction( }).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'); +} From f2bc116438863fe202c457fcca260de52a87f7a3 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 24 Feb 2023 15:28:14 +0000 Subject: [PATCH 7/7] remove redundant stuff --- packages/e2e-tests/test-utils/event-proxy-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/e2e-tests/test-utils/event-proxy-server.ts b/packages/e2e-tests/test-utils/event-proxy-server.ts index 7b3251cf0a4c..f914f68f8d88 100644 --- a/packages/e2e-tests/test-utils/event-proxy-server.ts +++ b/packages/e2e-tests/test-utils/event-proxy-server.ts @@ -125,8 +125,8 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P }); const eventCallbackServerStartupPromise = new Promise(resolve => { - const listener = eventCallbackServer.listen(0, () => { - const port = String((listener.address() as AddressInfo).port); + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); void registerCallbackServerPort(options.proxyServerName, port).then(resolve); }); });