diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d617b09bea01..bcbef8e06a8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1004,6 +1004,7 @@ jobs: 'create-remix-app-express-vite-dev', 'debug-id-sourcemaps', 'node-express-esm-loader', + 'node-express-esm-without-loader', 'nextjs-app-dir', 'nextjs-14', 'react-create-hash-router', diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/.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-express-esm-without-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json new file mode 100644 index 000000000000..b339fa65d2a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json @@ -0,0 +1,24 @@ +{ + "name": "node-express-esm-without-loader", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "node src/app.mjs", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install", + "test:assert": "playwright test" + }, + "dependencies": { + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "express": "4.19.2" + }, + "devDependencies": { + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", + "@playwright/test": "^1.27.1" + }, + "volta": { + "extends": "../../package.json", + "node": "18.19.1" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/playwright.config.ts b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/playwright.config.ts new file mode 100644 index 000000000000..5e672ed97676 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/playwright.config.ts @@ -0,0 +1,70 @@ +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: 'node start-event-proxy.mjs', + port: eventProxyPort, + stdout: 'pipe', + stderr: 'pipe', + }, + { + command: 'pnpm start', + port: expressPort, + stdout: 'pipe', + stderr: 'pipe', + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/app.mjs new file mode 100644 index 000000000000..0d318ab5fc13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/app.mjs @@ -0,0 +1,46 @@ +import './instrument.mjs'; + +// Below other imports +import * as Sentry from '@sentry/node'; +import express from 'express'; + +const app = express(); +const port = 3030; + +app.get('/test-success', function (req, res) { + setTimeout(() => { + res.status(200).end(); + }, 100); +}); + +app.get('/test-params/:param', function (req, res) { + const { param } = req.params; + Sentry.setTag(`param-${param}`, 'yes'); + Sentry.captureException(new Error(`Error for param ${param}`)); + + setTimeout(() => { + res.status(200).end(); + }, 100); +}); + +app.get('/test-error', function (req, res) { + Sentry.captureException(new Error('This is an error')); + setTimeout(() => { + Sentry.flush(2000).then(() => { + res.status(200).end(); + }); + }, 100); +}); + +Sentry.setupExpressErrorHandler(app); + +app.use(function onError(err, req, res, next) { + // 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-express-esm-without-loader/src/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/instrument.mjs new file mode 100644 index 000000000000..1636d10e836c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/instrument.mjs @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/start-event-proxy.mjs new file mode 100644 index 000000000000..df0fdb65c929 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-express-esm-without-loader', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/tests/server.test.ts new file mode 100644 index 000000000000..c2c076f13e4e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/tests/server.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; + +test('Should record exceptions captured inside handlers', async ({ request }) => { + const errorEventPromise = waitForError('node-express-esm-without-loader', errorEvent => { + return !!errorEvent?.exception?.values?.[0]?.value?.includes('This is an error'); + }); + + await request.get('/test-error'); + + await expect(errorEventPromise).resolves.toBeDefined(); +}); + +test('Isolates requests', async ({ request }) => { + const errorEventPromise = waitForError('node-express-esm-without-loader', errorEvent => { + return !!errorEvent?.exception?.values?.[0]?.value?.includes('Error for param 1'); + }); + + const errorEventPromise2 = waitForError('node-express-esm-without-loader', errorEvent => { + return !!errorEvent?.exception?.values?.[0]?.value?.includes('Error for param 2'); + }); + + await request.get('/test-params/1'); + await request.get('/test-params/2'); + + const errorEvent1 = await errorEventPromise; + const errorEvent2 = await errorEventPromise2; + + expect(errorEvent1.tags).toEqual({ 'param-1': 'yes' }); + expect(errorEvent2.tags).toEqual({ 'param-2': 'yes' }); + + // Transaction is not set, since we have no expressIntegration by default + expect(errorEvent1.transaction).toBeUndefined(); + expect(errorEvent2.transaction).toBeUndefined(); +});