Skip to content

Commit 6d3f865

Browse files
authored
Merge branch 'develop' into isaacschlueter/js-3800-vercelaiintegration-experimental_telemetrymetadata-no-longer
2 parents aea4552 + 67d0c98 commit 6d3f865

2 files changed

Lines changed: 176 additions & 1 deletion

File tree

‎packages/bun/src/integrations/bunserver.ts‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import {
2121
filterCollectedUrl,
2222
filterCollectedUrlQuery,
2323
} from '@sentry/core';
24-
import type { ServeOptions } from 'bun';
24+
import type { Server, ServeOptions } from 'bun';
2525
import {
26+
CLIENT_ADDRESS,
27+
CLIENT_PORT,
28+
NETWORK_PROTOCOL_NAME,
2629
SENTRY_OP,
2730
SENTRY_SEGMENT_NAME_SOURCE,
2831
URL_DOMAIN,
@@ -242,6 +245,24 @@ function wrapRequestHandler<T extends RouteHandler = RouteHandler>(
242245
const client = getClient();
243246
const dataCollection = client?.getDataCollectionOptions();
244247

248+
if (dataCollection?.userInfo) {
249+
// `client.address` is the originating client, so a forwarding header wins over the socket, which
250+
// behind a proxy holds the proxy's address.
251+
const forwardedFor = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
252+
// Bun passes the `Server` as the second argument to both `fetch` and route handlers, except
253+
// when the handler runs through `server.fetch()`.
254+
const socketAddress = getRequestIP(args[1], request);
255+
if (forwardedFor || socketAddress?.address) {
256+
attributes[CLIENT_ADDRESS] = forwardedFor || socketAddress?.address;
257+
}
258+
if (socketAddress?.port) {
259+
attributes[CLIENT_PORT] = socketAddress.port;
260+
}
261+
}
262+
263+
// describes the OSI application-layer protocol (http), not the scheme (might be https)
264+
attributes[NETWORK_PROTOCOL_NAME] = 'http';
265+
245266
if (dataCollection) {
246267
Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON(), dataCollection));
247268
}
@@ -308,6 +329,18 @@ function wrapRequestHandler<T extends RouteHandler = RouteHandler>(
308329
});
309330
}
310331

332+
function getRequestIP(server: unknown, request: Request): { address: string; port: number } | undefined {
333+
if (typeof (server as Partial<Server> | undefined)?.requestIP !== 'function') {
334+
return undefined;
335+
}
336+
try {
337+
return (server as Server).requestIP(request) ?? undefined;
338+
} catch {
339+
// Defensive: never let a failed lookup break the user's handler.
340+
return undefined;
341+
}
342+
}
343+
311344
function getSpanAttributesFromParsedUrl(
312345
parsedUrl: ReturnType<typeof parseStringToURLObject>,
313346
request: Request,

‎packages/bun/test/integrations/bunserver.test.ts‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,148 @@ describe('Bun Serve Integration', () => {
521521
});
522522

523523
describe('data collection', () => {
524+
test('captures client address, port and protocol by default', async () => {
525+
const server = Bun.serve({
526+
async fetch(_req) {
527+
return new Response('Bun!');
528+
},
529+
port,
530+
});
531+
532+
await fetch(`http://localhost:${port}/`);
533+
534+
await server.stop();
535+
536+
expect(startSpanSpy).toHaveBeenCalledTimes(1);
537+
expect(startSpanSpy).toHaveBeenCalledWith(
538+
expect.objectContaining({
539+
attributes: expect.objectContaining({
540+
'client.address': expect.stringMatching(/^(127\.0\.0\.1|::1|::ffff:127\.0\.0\.1)$/),
541+
'client.port': expect.any(Number),
542+
'network.protocol.name': 'http',
543+
}),
544+
}),
545+
expect.any(Function),
546+
);
547+
});
548+
549+
test('captures client address on route handlers', async () => {
550+
const server = Bun.serve({
551+
routes: {
552+
'/users/:id': req => new Response(`User ${req.params.id}`),
553+
},
554+
port,
555+
});
556+
557+
await fetch(`http://localhost:${port}/users/123`);
558+
559+
await server.stop();
560+
561+
expect(startSpanSpy).toHaveBeenCalledTimes(1);
562+
expect(startSpanSpy).toHaveBeenCalledWith(
563+
expect.objectContaining({
564+
attributes: expect.objectContaining({
565+
'client.address': expect.stringMatching(/^(127\.0\.0\.1|::1|::ffff:127\.0\.0\.1)$/),
566+
'client.port': expect.any(Number),
567+
}),
568+
}),
569+
expect.any(Function),
570+
);
571+
});
572+
573+
test('prefers the first x-forwarded-for address over the socket address', async () => {
574+
const server = Bun.serve({
575+
async fetch(_req) {
576+
return new Response('Bun!');
577+
},
578+
port,
579+
});
580+
581+
await fetch(`http://localhost:${port}/`, {
582+
headers: { 'X-Forwarded-For': '203.0.113.7, 10.0.0.1' },
583+
});
584+
585+
await server.stop();
586+
587+
expect(startSpanSpy).toHaveBeenCalledTimes(1);
588+
expect(startSpanSpy).toHaveBeenCalledWith(
589+
expect.objectContaining({
590+
attributes: expect.objectContaining({
591+
'client.address': '203.0.113.7',
592+
}),
593+
}),
594+
expect.any(Function),
595+
);
596+
});
597+
598+
test('does not capture client address when userInfo collection is disabled', async () => {
599+
setupClient({ dataCollection: { userInfo: false } });
600+
601+
const server = Bun.serve({
602+
async fetch(_req) {
603+
return new Response('Bun!');
604+
},
605+
port,
606+
});
607+
608+
await fetch(`http://localhost:${port}/`, {
609+
headers: { 'X-Forwarded-For': '203.0.113.7' },
610+
});
611+
612+
await server.stop();
613+
614+
expect(startSpanSpy).toHaveBeenCalledTimes(1);
615+
expect(startSpanSpy).toHaveBeenCalledWith(
616+
expect.objectContaining({
617+
attributes: expect.not.objectContaining({
618+
'client.address': expect.anything(),
619+
'client.port': expect.anything(),
620+
}),
621+
}),
622+
expect.any(Function),
623+
);
624+
expect(startSpanSpy).toHaveBeenCalledWith(
625+
expect.objectContaining({
626+
attributes: expect.objectContaining({
627+
'network.protocol.name': 'http',
628+
}),
629+
}),
630+
expect.any(Function),
631+
);
632+
});
633+
634+
test('leaves client address unset when the handler gets no server', async () => {
635+
const server = Bun.serve({
636+
async fetch(_req) {
637+
return new Response('Bun!');
638+
},
639+
port,
640+
});
641+
642+
// `server.fetch()` calls the handler with the request only.
643+
const response = await server.fetch(new Request(`http://localhost:${port}/`));
644+
645+
await server.stop();
646+
647+
expect(await response.text()).toBe('Bun!');
648+
expect(startSpanSpy).toHaveBeenCalledTimes(1);
649+
expect(startSpanSpy).toHaveBeenCalledWith(
650+
expect.objectContaining({
651+
attributes: expect.not.objectContaining({
652+
'client.address': expect.anything(),
653+
'client.port': expect.anything(),
654+
}),
655+
}),
656+
expect.any(Function),
657+
);
658+
expect(startSpanSpy).toHaveBeenCalledWith(
659+
expect.objectContaining({
660+
attributes: expect.objectContaining({ 'network.protocol.name': 'http' }),
661+
}),
662+
expect.any(Function),
663+
);
664+
});
665+
524666
test('keeps PII request headers when dataCollection enables full header collection', async () => {
525667
setupClient({ dataCollection: { httpHeaders: { request: true, response: true } } });
526668

0 commit comments

Comments
 (0)