Skip to content

Commit df2dc5d

Browse files
nabi-noorcodex
andauthored
test(e2e): Add Bun Express test app (#24306)
Adds a dedicated Bun + Express E2E application covering automatic request tracing and error capture. The app exercises both a parameterized success route and an exception route, asserting Express handler spans, route naming, status propagation, the Bun runtime context, and trace correlation. The app is built with `sentryBunPlugin` because Express instrumentation under Bun relies on build-time diagnostics-channel injection. The SDK and Express remain external so the app validates the normal package boundary, while CI now installs its pinned Bun version for this matrix entry. The targeted `bun-express` E2E run passes both Playwright cases on Bun 1.3.14. `yarn format`, `yarn build:dev`, and `yarn build:tarball` also complete successfully. The full local lint and unit-suite runs still surface pre-existing environment/baseline failures outside the files changed here; CI will provide the authoritative full-suite result. Fixes #12732 Co-authored-by: OpenAI Codex <codex@openai.com>
1 parent 87c4eed commit df2dc5d

10 files changed

Lines changed: 161 additions & 1 deletion

File tree

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1056,7 +1056,7 @@ jobs:
10561056
- name: Set up Bun
10571057
if:
10581058
contains(fromJSON('["node-exports-test-app","nextjs-16-bun", "elysia-bun", "elysia-bun-static", "hono-4",
1059-
"bun-bytecode", "bun-mysql"]'), matrix.test-application)
1059+
"bun-bytecode", "bun-express", "bun-mysql"]'), matrix.test-application)
10601060
uses: oven-sh/setup-bun@v2
10611061
with:
10621062
bun-version: '1.3.14'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @ts-ignore -- subpath export resolved by Bun at runtime; the package
2+
// tsconfig's node module resolution can't see `exports` subpaths.
3+
import { sentryBunPlugin } from '@sentry/bun/plugin';
4+
import { join } from 'path';
5+
6+
void (async () => {
7+
const result = await Bun.build({
8+
entrypoints: [join(__dirname, 'src/app.ts')],
9+
target: 'bun',
10+
outdir: join(__dirname, 'dist'),
11+
external: ['@sentry/bun', 'express'],
12+
plugins: [sentryBunPlugin()],
13+
});
14+
15+
if (!result.success) {
16+
// oxlint-disable-next-line no-console
17+
console.error('BUILD_FAILED', result.logs);
18+
process.exit(1);
19+
}
20+
})();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "bun-express",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"start": "bun run dist/app.js",
8+
"test": "playwright test",
9+
"clean": "npx rimraf node_modules pnpm-lock.yaml dist",
10+
"test:build": "pnpm install && bun run build.ts",
11+
"test:assert": "pnpm test"
12+
},
13+
"dependencies": {
14+
"@sentry/bun": "latest || *",
15+
"express": "^5.1.0"
16+
},
17+
"devDependencies": {
18+
"@playwright/test": "~1.56.0",
19+
"@sentry-internal/test-utils": "link:../../../test-utils",
20+
"@types/express": "^5.0.0",
21+
"bun-types": "^1.2.9"
22+
},
23+
"volta": {
24+
"extends": "../../package.json"
25+
}
26+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
export default getPlaywrightConfig({
4+
startCommand: 'pnpm start',
5+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as Sentry from '@sentry/bun';
2+
import express from 'express';
3+
4+
Sentry.init({
5+
environment: 'qa',
6+
dsn: process.env.E2E_TEST_DSN,
7+
tunnel: 'http://localhost:3031/',
8+
tracesSampleRate: 1,
9+
});
10+
11+
const app = express();
12+
13+
app.get('/test-param/:id', (request, response) => {
14+
response.json({ id: request.params.id });
15+
});
16+
17+
app.get('/test-exception/:id', request => {
18+
throw new Error(`This is an exception with id ${request.params.id}`);
19+
});
20+
21+
app.use((_error, _request, response, _next) => {
22+
response.status(500).send('Internal Server Error');
23+
});
24+
25+
app.listen(3030, () => {
26+
// oxlint-disable-next-line no-console
27+
console.log('Bun Express app listening on port 3030');
28+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'bun-express',
6+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test } from '@playwright/test';
2+
import { collectStreamedSpansUntilSegment, waitForError } from '@sentry-internal/test-utils';
3+
4+
test('captures an error thrown in an Express route', async ({ baseURL }) => {
5+
const errorEventPromise = waitForError('bun-express', event => {
6+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
7+
});
8+
const spansPromise = collectStreamedSpansUntilSegment('bun-express', 'GET /test-exception/:id');
9+
10+
const response = await fetch(`${baseURL}/test-exception/123`);
11+
12+
const errorEvent = await errorEventPromise;
13+
const spans = await spansPromise;
14+
15+
expect(response.status).toBe(500);
16+
expect(errorEvent.exception?.values).toEqual([
17+
expect.objectContaining({
18+
value: 'This is an exception with id 123',
19+
mechanism: {
20+
type: 'auto.http.express',
21+
handled: false,
22+
},
23+
}),
24+
]);
25+
expect(errorEvent.transaction).toBe('GET /test-exception/:id');
26+
expect(errorEvent.contexts?.runtime?.name).toBe('bun');
27+
28+
const rootSpan = spans.find(span => span.is_segment);
29+
expect(rootSpan?.name).toBe('GET /test-exception/:id');
30+
expect(rootSpan?.status).toBe('error');
31+
expect(rootSpan?.attributes['http.response.status_code']?.value).toBe(500);
32+
expect(errorEvent.contexts?.trace?.trace_id).toBe(rootSpan?.trace_id);
33+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { expect, test } from '@playwright/test';
2+
import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
3+
4+
test('instruments an Express route under Bun', async ({ baseURL }) => {
5+
const spansPromise = collectStreamedSpansUntilSegment('bun-express', 'GET /test-param/:id');
6+
7+
const response = await fetch(`${baseURL}/test-param/123`);
8+
const spans = await spansPromise;
9+
10+
expect(response.status).toBe(200);
11+
expect(await response.json()).toEqual({ id: '123' });
12+
13+
const rootSpan = spans.find(span => span.is_segment);
14+
expect(rootSpan?.name).toBe('GET /test-param/:id');
15+
expect(rootSpan && getSpanOp(rootSpan)).toBe('http.server');
16+
expect(rootSpan?.status).toBe('ok');
17+
expect(rootSpan?.attributes['sentry.segment.name.source']?.value).toBe('route');
18+
19+
expect(spans).toContainEqual(
20+
expect.objectContaining({
21+
name: '/test-param/:id',
22+
attributes: expect.objectContaining({
23+
'sentry.origin': { value: 'auto.http.express', type: 'string' },
24+
'sentry.op': { value: 'handler', type: 'string' },
25+
'http.route': { value: '/test-param/:id', type: 'string' },
26+
'express.type': { value: 'request_handler', type: 'string' },
27+
}),
28+
}),
29+
);
30+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"types": ["bun-types"],
4+
"esModuleInterop": true,
5+
"lib": ["es2020"],
6+
"strict": true,
7+
"outDir": "dist",
8+
"skipLibCheck": true
9+
},
10+
"include": ["src/**/*.ts", "build.ts"]
11+
}

0 commit comments

Comments
 (0)