Skip to content

Commit d6cc81b

Browse files
committed
test(hono): Add E2E tests for middleware spans
1 parent b2033c0 commit d6cc81b

5 files changed

Lines changed: 202 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { MiddlewareHandler } from 'hono';
2+
3+
export const middlewareA: MiddlewareHandler = async function middlewareA(c, next) {
4+
// Add some delay
5+
await new Promise(resolve => setTimeout(resolve, 50));
6+
await next();
7+
};
8+
9+
export const middlewareB: MiddlewareHandler = async function middlewareB(_c, next) {
10+
// Add some delay
11+
await new Promise(resolve => setTimeout(resolve, 60));
12+
await next();
13+
};
14+
15+
export const failingMiddleware: MiddlewareHandler = async function failingMiddleware(_c, _next) {
16+
throw new Error('Middleware error');
17+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Hono } from 'hono';
2+
3+
const testMiddleware = new Hono();
4+
5+
testMiddleware.get('/named', c => c.json({ middleware: 'named' }));
6+
testMiddleware.get('/anonymous', c => c.json({ middleware: 'anonymous' }));
7+
testMiddleware.get('/multi', c => c.json({ middleware: 'multi' }));
8+
testMiddleware.get('/error', c => c.text('should not reach'));
9+
10+
export { testMiddleware };

dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Hono } from 'hono';
22
import { HTTPException } from 'hono/http-exception';
3+
import { testMiddleware } from './route-groups/test-middleware';
4+
import { middlewareA, middlewareB, failingMiddleware } from './middleware';
35

46
export function addRoutes(app: Hono<{ Bindings?: { E2E_TEST_DSN: string } }>): void {
57
app.get('/', c => {
@@ -21,4 +23,17 @@ export function addRoutes(app: Hono<{ Bindings?: { E2E_TEST_DSN: string } }>): v
2123
const code = Number(c.req.param('code')) as any;
2224
throw new HTTPException(code, { message: `HTTPException ${code}` });
2325
});
26+
27+
// === Middleware ===
28+
// Middleware is registered on the main app (the patched instance) via `app.use()`
29+
// TODO: In the future, we may want to support middleware registration on sub-apps (route groups)
30+
app.use('/test-middleware/named/*', middlewareA);
31+
app.use('/test-middleware/anonymous/*', async (c, next) => {
32+
c.header('X-Custom', 'anonymous');
33+
await next();
34+
});
35+
app.use('/test-middleware/multi/*', middlewareA, middlewareB);
36+
app.use('/test-middleware/error/*', failingMiddleware);
37+
38+
app.route('/test-middleware', testMiddleware);
2439
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
const APP_NAME = 'hono-4';
5+
6+
test('creates a span for named middleware', async ({ baseURL }) => {
7+
const transactionPromise = waitForTransaction(APP_NAME, event => {
8+
return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-middleware/named';
9+
});
10+
11+
const response = await fetch(`${baseURL}/test-middleware/named`);
12+
expect(response.status).toBe(200);
13+
14+
const transaction = await transactionPromise;
15+
const spans = transaction.spans || [];
16+
17+
const middlewareSpan = spans.find(
18+
(span: { description?: string; op?: string }) =>
19+
span.op === 'middleware.hono' && span.description === 'middlewareA',
20+
);
21+
22+
expect(middlewareSpan).toEqual(
23+
expect.objectContaining({
24+
description: 'middlewareA',
25+
op: 'middleware.hono',
26+
origin: 'auto.middleware.hono',
27+
status: 'ok',
28+
}),
29+
);
30+
31+
// The middleware has a 50ms delay, so the span duration should be at least 50ms (0.05s)
32+
// @ts-expect-error timestamp is defined
33+
const durationMs = (middlewareSpan?.timestamp - middlewareSpan?.start_timestamp) * 1000;
34+
expect(durationMs).toBeGreaterThanOrEqual(50);
35+
});
36+
37+
test('creates a span for anonymous middleware', async ({ baseURL }) => {
38+
const transactionPromise = waitForTransaction(APP_NAME, event => {
39+
return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-middleware/anonymous';
40+
});
41+
42+
const response = await fetch(`${baseURL}/test-middleware/anonymous`);
43+
expect(response.status).toBe(200);
44+
45+
const transaction = await transactionPromise;
46+
const spans = transaction.spans || [];
47+
48+
expect(spans).toContainEqual(
49+
expect.objectContaining({
50+
description: '<anonymous>',
51+
op: 'middleware.hono',
52+
origin: 'auto.middleware.hono',
53+
status: 'ok',
54+
}),
55+
);
56+
});
57+
58+
test('multiple middleware are sibling spans under the same parent', async ({ baseURL }) => {
59+
const transactionPromise = waitForTransaction(APP_NAME, event => {
60+
return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-middleware/multi';
61+
});
62+
63+
const response = await fetch(`${baseURL}/test-middleware/multi`);
64+
expect(response.status).toBe(200);
65+
66+
const transaction = await transactionPromise;
67+
const spans = transaction.spans || [];
68+
69+
const middlewareSpans = spans.filter(
70+
(span: { op?: string; origin?: string }) => span.op === 'middleware.hono' && span.origin === 'auto.middleware.hono',
71+
);
72+
73+
expect(middlewareSpans).toHaveLength(2);
74+
expect(middlewareSpans[0]?.description).toBe('middlewareA');
75+
expect(middlewareSpans[1]?.description).toBe('middlewareB');
76+
77+
// Both middleware spans share the same parent (siblings, not nested)
78+
expect(middlewareSpans[0]?.parent_span_id).toBe(middlewareSpans[1]?.parent_span_id);
79+
80+
// middlewareA has a 50ms delay, middlewareB has a 60ms delay
81+
// @ts-expect-error timestamp is defined
82+
const timestampDurationMs = (middlewareSpans[0]?.timestamp - middlewareSpans[0]?.start_timestamp) * 1000;
83+
// @ts-expect-error timestamp is defined
84+
const authDurationMs = (middlewareSpans[1]?.timestamp - middlewareSpans[1]?.start_timestamp) * 1000;
85+
expect(timestampDurationMs).toBeGreaterThanOrEqual(50);
86+
expect(authDurationMs).toBeGreaterThanOrEqual(60);
87+
});
88+
89+
test('captures error thrown in middleware', async ({ baseURL }) => {
90+
const errorPromise = waitForError(APP_NAME, event => {
91+
return event.exception?.values?.[0]?.value === 'Middleware error';
92+
});
93+
94+
const response = await fetch(`${baseURL}/test-middleware/error`);
95+
expect(response.status).toBe(500);
96+
97+
const errorEvent = await errorPromise;
98+
expect(errorEvent.exception?.values?.[0]?.value).toBe('Middleware error');
99+
expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
100+
expect.objectContaining({
101+
handled: false,
102+
type: 'auto.middleware.hono',
103+
}),
104+
);
105+
});
106+
107+
test('sets error status on middleware span when middleware throws', async ({ baseURL }) => {
108+
const transactionPromise = waitForTransaction(APP_NAME, event => {
109+
return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-middleware/error/*';
110+
});
111+
112+
await fetch(`${baseURL}/test-middleware/error`);
113+
114+
const transaction = await transactionPromise;
115+
const spans = transaction.spans || [];
116+
117+
const failingSpan = spans.find(
118+
(span: { description?: string; op?: string }) =>
119+
span.op === 'middleware.hono' && span.description === 'failingMiddleware',
120+
);
121+
122+
expect(failingSpan).toBeDefined();
123+
expect(failingSpan?.status).toBe('internal_error');
124+
expect(failingSpan?.origin).toBe('auto.middleware.hono');
125+
});
126+
127+
test('includes request data on error events from middleware', async ({ baseURL }) => {
128+
const errorPromise = waitForError(APP_NAME, event => {
129+
return event.exception?.values?.[0]?.value === 'Middleware error';
130+
});
131+
132+
await fetch(`${baseURL}/test-middleware/error`);
133+
134+
const errorEvent = await errorPromise;
135+
expect(errorEvent.request).toEqual(
136+
expect.objectContaining({
137+
method: 'GET',
138+
url: expect.stringContaining('/test-middleware/error'),
139+
}),
140+
);
141+
});

packages/hono/test/shared/patchAppUse.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,23 @@ describe('patchAppUse (middleware spans)', () => {
155155

156156
expect(fakeApp._capturedThis).toBe(fakeApp);
157157
});
158+
159+
// todo: support sub-app (Hono route groups) patching in the future
160+
it('does not wrap middleware on sub-apps (instance-level patching limitation)', async () => {
161+
const app = new Hono();
162+
patchAppUse(app);
163+
164+
// Route Grouping: https://hono.dev/docs/api/routing#grouping
165+
const subApp = new Hono();
166+
subApp.use(async function subMiddleware(_c: unknown, next: () => Promise<void>) {
167+
await next();
168+
});
169+
subApp.get('/', () => new Response('sub'));
170+
171+
app.route('/sub', subApp);
172+
173+
await app.fetch(new Request('http://localhost/sub'));
174+
175+
expect(startInactiveSpanMock).not.toHaveBeenCalledWith(expect.objectContaining({ name: 'subMiddleware' }));
176+
});
158177
});

0 commit comments

Comments
 (0)