-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: Add loader file to node-based SDKs to support ESM monkeypatching #11338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
57f837b
feat: Add loader file to node-based SDKs to support ESM
lforst a9fc29c
lint
lforst 5a137d8
Add E2E test
lforst 39e2c59
rename
lforst 1f37050
better deps
lforst a5ae9a1
Add disable test to ci
lforst 9236a80
Use `register`
lforst ab9ddc8
Add secondary `hook` to support the --loader usecase
lforst 21f47fb
Merge remote-tracking branch 'origin/develop' into lforst-loader-file
lforst 82d178b
Merge branch 'develop' into lforst-loader-file
lforst File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/.npmrc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
@sentry:registry=http://127.0.0.1:4873 | ||
@sentry-internal:registry=http://127.0.0.1:4873 |
253 changes: 253 additions & 0 deletions
253
dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/event-proxy-server.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,253 @@ | ||
import * as fs from 'fs'; | ||
import * as http from 'http'; | ||
import * as https from 'https'; | ||
import type { AddressInfo } from 'net'; | ||
import * as os from 'os'; | ||
import * as path from 'path'; | ||
import * as util from 'util'; | ||
import * as zlib from 'zlib'; | ||
import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; | ||
import { parseEnvelope } from '@sentry/utils'; | ||
|
||
const readFile = util.promisify(fs.readFile); | ||
const writeFile = util.promisify(fs.writeFile); | ||
|
||
interface EventProxyServerOptions { | ||
/** Port to start the event proxy server at. */ | ||
port: number; | ||
/** The name for the proxy server used for referencing it with listener functions */ | ||
proxyServerName: string; | ||
} | ||
|
||
interface SentryRequestCallbackData { | ||
envelope: Envelope; | ||
rawProxyRequestBody: string; | ||
rawSentryResponseBody: string; | ||
sentryResponseStatusCode?: number; | ||
} | ||
|
||
/** | ||
* Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` | ||
* option to this server (like this `tunnel: http://localhost:${port option}/`). | ||
*/ | ||
export async function startEventProxyServer(options: EventProxyServerOptions): Promise<void> { | ||
const eventCallbackListeners: Set<(data: string) => void> = new Set(); | ||
|
||
const proxyServer = http.createServer((proxyRequest, proxyResponse) => { | ||
const proxyRequestChunks: Uint8Array[] = []; | ||
|
||
proxyRequest.addListener('data', (chunk: Buffer) => { | ||
proxyRequestChunks.push(chunk); | ||
}); | ||
|
||
proxyRequest.addListener('error', err => { | ||
throw err; | ||
}); | ||
|
||
proxyRequest.addListener('end', () => { | ||
const proxyRequestBody = | ||
proxyRequest.headers['content-encoding'] === 'gzip' | ||
? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() | ||
: Buffer.concat(proxyRequestChunks).toString(); | ||
|
||
let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); | ||
|
||
if (!envelopeHeader.dsn) { | ||
throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); | ||
} | ||
|
||
const { origin, pathname, host } = new URL(envelopeHeader.dsn); | ||
|
||
const projectId = pathname.substring(1); | ||
const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; | ||
|
||
proxyRequest.headers.host = host; | ||
|
||
const sentryResponseChunks: Uint8Array[] = []; | ||
|
||
const sentryRequest = https.request( | ||
sentryIngestUrl, | ||
{ headers: proxyRequest.headers, method: proxyRequest.method }, | ||
sentryResponse => { | ||
sentryResponse.addListener('data', (chunk: Buffer) => { | ||
proxyResponse.write(chunk, 'binary'); | ||
sentryResponseChunks.push(chunk); | ||
}); | ||
|
||
sentryResponse.addListener('end', () => { | ||
eventCallbackListeners.forEach(listener => { | ||
const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); | ||
|
||
const data: SentryRequestCallbackData = { | ||
envelope: parseEnvelope(proxyRequestBody), | ||
rawProxyRequestBody: proxyRequestBody, | ||
rawSentryResponseBody, | ||
sentryResponseStatusCode: sentryResponse.statusCode, | ||
}; | ||
|
||
listener(Buffer.from(JSON.stringify(data)).toString('base64')); | ||
}); | ||
proxyResponse.end(); | ||
}); | ||
|
||
sentryResponse.addListener('error', err => { | ||
throw err; | ||
}); | ||
|
||
proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); | ||
}, | ||
); | ||
|
||
sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); | ||
sentryRequest.end(); | ||
}); | ||
}); | ||
|
||
const proxyServerStartupPromise = new Promise<void>(resolve => { | ||
proxyServer.listen(options.port, () => { | ||
resolve(); | ||
}); | ||
}); | ||
|
||
const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { | ||
eventCallbackResponse.statusCode = 200; | ||
eventCallbackResponse.setHeader('connection', 'keep-alive'); | ||
|
||
const callbackListener = (data: string): void => { | ||
eventCallbackResponse.write(data.concat('\n'), 'utf8'); | ||
}; | ||
|
||
eventCallbackListeners.add(callbackListener); | ||
|
||
eventCallbackRequest.on('close', () => { | ||
eventCallbackListeners.delete(callbackListener); | ||
}); | ||
|
||
eventCallbackRequest.on('error', () => { | ||
eventCallbackListeners.delete(callbackListener); | ||
}); | ||
}); | ||
|
||
const eventCallbackServerStartupPromise = new Promise<void>(resolve => { | ||
eventCallbackServer.listen(0, () => { | ||
const port = String((eventCallbackServer.address() as AddressInfo).port); | ||
void registerCallbackServerPort(options.proxyServerName, port).then(resolve); | ||
}); | ||
}); | ||
|
||
await eventCallbackServerStartupPromise; | ||
await proxyServerStartupPromise; | ||
return; | ||
} | ||
|
||
export async function waitForRequest( | ||
proxyServerName: string, | ||
callback: (eventData: SentryRequestCallbackData) => Promise<boolean> | boolean, | ||
): Promise<SentryRequestCallbackData> { | ||
const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); | ||
|
||
return new Promise<SentryRequestCallbackData>((resolve, reject) => { | ||
const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { | ||
let eventContents = ''; | ||
|
||
response.on('error', err => { | ||
reject(err); | ||
}); | ||
|
||
response.on('data', (chunk: Buffer) => { | ||
const chunkString = chunk.toString('utf8'); | ||
chunkString.split('').forEach(char => { | ||
if (char === '\n') { | ||
const eventCallbackData: SentryRequestCallbackData = JSON.parse( | ||
Buffer.from(eventContents, 'base64').toString('utf8'), | ||
); | ||
const callbackResult = callback(eventCallbackData); | ||
if (typeof callbackResult !== 'boolean') { | ||
callbackResult.then( | ||
match => { | ||
if (match) { | ||
response.destroy(); | ||
resolve(eventCallbackData); | ||
} | ||
}, | ||
err => { | ||
throw err; | ||
}, | ||
); | ||
} else if (callbackResult) { | ||
response.destroy(); | ||
resolve(eventCallbackData); | ||
} | ||
eventContents = ''; | ||
} else { | ||
eventContents = eventContents.concat(char); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
request.end(); | ||
}); | ||
} | ||
|
||
export function waitForEnvelopeItem( | ||
proxyServerName: string, | ||
callback: (envelopeItem: EnvelopeItem) => Promise<boolean> | boolean, | ||
): Promise<EnvelopeItem> { | ||
return new Promise((resolve, reject) => { | ||
waitForRequest(proxyServerName, async eventData => { | ||
const envelopeItems = eventData.envelope[1]; | ||
for (const envelopeItem of envelopeItems) { | ||
if (await callback(envelopeItem)) { | ||
resolve(envelopeItem); | ||
return true; | ||
} | ||
} | ||
return false; | ||
}).catch(reject); | ||
}); | ||
} | ||
|
||
export function waitForError( | ||
proxyServerName: string, | ||
callback: (transactionEvent: Event) => Promise<boolean> | boolean, | ||
): Promise<Event> { | ||
return new Promise((resolve, reject) => { | ||
waitForEnvelopeItem(proxyServerName, async envelopeItem => { | ||
const [envelopeItemHeader, envelopeItemBody] = envelopeItem; | ||
if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { | ||
resolve(envelopeItemBody as Event); | ||
return true; | ||
} | ||
return false; | ||
}).catch(reject); | ||
}); | ||
} | ||
|
||
export function waitForTransaction( | ||
proxyServerName: string, | ||
callback: (transactionEvent: Event) => Promise<boolean> | boolean, | ||
): Promise<Event> { | ||
return new Promise((resolve, reject) => { | ||
waitForEnvelopeItem(proxyServerName, async envelopeItem => { | ||
const [envelopeItemHeader, envelopeItemBody] = envelopeItem; | ||
if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { | ||
resolve(envelopeItemBody as Event); | ||
return true; | ||
} | ||
return false; | ||
}).catch(reject); | ||
}); | ||
} | ||
|
||
const TEMP_FILE_PREFIX = 'event-proxy-server-'; | ||
|
||
async function registerCallbackServerPort(serverName: string, port: string): Promise<void> { | ||
const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); | ||
await writeFile(tmpFilePath, port, { encoding: 'utf8' }); | ||
} | ||
|
||
function retrieveCallbackServerPort(serverName: string): Promise<string> { | ||
const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); | ||
return readFile(tmpFilePath, 'utf8'); | ||
} |
27 changes: 27 additions & 0 deletions
27
dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"name": "node-express-app", | ||
"version": "1.0.0", | ||
"private": true, | ||
"scripts": { | ||
"start": "node --import=@sentry/node/register src/app.mjs", | ||
"clean": "npx rimraf node_modules,pnpm-lock.yaml", | ||
"test:build": "pnpm install", | ||
"test:assert": "playwright test" | ||
}, | ||
"dependencies": { | ||
"@sentry/node": "latest || *", | ||
"@sentry/types": "latest || *", | ||
"express": "4.19.2", | ||
"@types/express": "4.17.17", | ||
"@types/node": "18.15.1", | ||
"typescript": "4.9.5" | ||
}, | ||
"devDependencies": { | ||
"@playwright/test": "^1.27.1", | ||
"ts-node": "10.9.1" | ||
}, | ||
"volta": { | ||
"extends": "../../package.json", | ||
"node": "18.19.1" | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
dev-packages/e2e-tests/test-applications/esm-loader-node-express-app/playwright.config.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import type { PlaywrightTestConfig } from '@playwright/test'; | ||
import { devices } from '@playwright/test'; | ||
|
||
// Fix urls not resolving to localhost on Node v17+ | ||
// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 | ||
import { setDefaultResultOrder } from 'dns'; | ||
setDefaultResultOrder('ipv4first'); | ||
|
||
const eventProxyPort = 3031; | ||
const expressPort = 3030; | ||
|
||
/** | ||
* See https://playwright.dev/docs/test-configuration. | ||
*/ | ||
const config: PlaywrightTestConfig = { | ||
testDir: './tests', | ||
/* Maximum time one test can run for. */ | ||
timeout: 150_000, | ||
expect: { | ||
/** | ||
* Maximum time expect() should wait for the condition to be met. | ||
* For example in `await expect(locator).toHaveText();` | ||
*/ | ||
timeout: 5000, | ||
}, | ||
/* Run tests in files in parallel */ | ||
fullyParallel: true, | ||
/* Fail the build on CI if you accidentally left test.only in the source code. */ | ||
forbidOnly: !!process.env.CI, | ||
/* Retry on CI only */ | ||
retries: 0, | ||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ | ||
reporter: 'list', | ||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ | ||
use: { | ||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ | ||
actionTimeout: 0, | ||
|
||
/* Base URL to use in actions like `await page.goto('/')`. */ | ||
baseURL: `http://localhost:${expressPort}`, | ||
}, | ||
|
||
/* Configure projects for major browsers */ | ||
projects: [ | ||
{ | ||
name: 'chromium', | ||
use: { | ||
...devices['Desktop Chrome'], | ||
}, | ||
}, | ||
], | ||
|
||
/* Run your local dev server before starting the tests */ | ||
webServer: [ | ||
{ | ||
command: 'pnpm ts-node-script start-event-proxy.ts', | ||
port: eventProxyPort, | ||
}, | ||
{ | ||
command: 'pnpm start', | ||
port: expressPort, | ||
stdout: 'pipe', | ||
stderr: 'pipe', | ||
}, | ||
], | ||
}; | ||
|
||
export default config; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / CodeQL
Server-side request forgery