Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion packages/bun/src/integrations/bunserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ import {
filterCollectedUrl,
filterCollectedUrlQuery,
} from '@sentry/core';
import type { ServeOptions } from 'bun';
import type { Server, ServeOptions } from 'bun';
import {
CLIENT_ADDRESS,
CLIENT_PORT,
NETWORK_PROTOCOL_NAME,
SENTRY_OP,
SENTRY_SEGMENT_NAME_SOURCE,
URL_DOMAIN,
Expand Down Expand Up @@ -242,6 +245,23 @@ function wrapRequestHandler<T extends RouteHandler = RouteHandler>(
const client = getClient();
const dataCollection = client?.getDataCollectionOptions();

if (dataCollection?.userInfo) {
// `client.address` is the originating client, so a forwarding header wins over the socket, which
// behind a proxy holds the proxy's address.
const forwardedFor = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
// Bun passes the `Server` as the second argument to both `fetch` and route handlers.
const socketAddress = getRequestIP(args[1], request);
if (forwardedFor || socketAddress?.address) {
attributes[CLIENT_ADDRESS] = forwardedFor || socketAddress?.address;
}
if (socketAddress?.port) {
attributes[CLIENT_PORT] = socketAddress.port;
}
}

// describes the OSI application-layer protocol (http), not the scheme (might be https)
attributes[NETWORK_PROTOCOL_NAME] = 'http';

if (dataCollection) {
Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON(), dataCollection));
}
Expand Down Expand Up @@ -308,6 +328,18 @@ function wrapRequestHandler<T extends RouteHandler = RouteHandler>(
});
}

function getRequestIP(server: unknown, request: Request): { address: string; port: number } | undefined {
if (typeof (server as Partial<Server> | undefined)?.requestIP !== 'function') {
return undefined;
}
try {
return (server as Server).requestIP(request) ?? undefined;
} catch {
// `requestIP` throws for requests that did not come from this server's socket.
return undefined;
}
}

function getSpanAttributesFromParsedUrl(
parsedUrl: ReturnType<typeof parseStringToURLObject>,
request: Request,
Expand Down
79 changes: 79 additions & 0 deletions packages/bun/test/integrations/bunserver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,85 @@ describe('Bun Serve Integration', () => {
});

describe('data collection', () => {
test('captures client address, port and protocol by default', async () => {
const server = Bun.serve({
async fetch(_req) {
return new Response('Bun!');
},
port,
});

await fetch(`http://localhost:${port}/`);

await server.stop();

expect(startSpanSpy).toHaveBeenCalledTimes(1);
const attributes = startSpanSpy.mock.calls[0]?.[0]?.attributes;
expect(attributes?.['client.address']).toMatch(/^(127\.0\.0\.1|::1|::ffff:127\.0\.0\.1)$/);
expect(attributes?.['client.port']).toEqual(expect.any(Number));
expect(attributes?.['network.protocol.name']).toBe('http');
});

test('captures client address on route handlers', async () => {
const server = Bun.serve({
routes: {
'/users/:id': req => new Response(`User ${req.params.id}`),
},
port,
});

await fetch(`http://localhost:${port}/users/123`);

await server.stop();

expect(startSpanSpy).toHaveBeenCalledTimes(1);
const attributes = startSpanSpy.mock.calls[0]?.[0]?.attributes;
expect(attributes?.['client.address']).toEqual(expect.any(String));
expect(attributes?.['client.port']).toEqual(expect.any(Number));
});

test('prefers the first x-forwarded-for address over the socket address', async () => {
const server = Bun.serve({
async fetch(_req) {
return new Response('Bun!');
},
port,
});

await fetch(`http://localhost:${port}/`, {
headers: { 'X-Forwarded-For': '203.0.113.7, 10.0.0.1' },
});

await server.stop();

expect(startSpanSpy).toHaveBeenCalledTimes(1);
const attributes = startSpanSpy.mock.calls[0]?.[0]?.attributes;
expect(attributes?.['client.address']).toBe('203.0.113.7');
});

test('does not capture client address when userInfo collection is disabled', async () => {
setupClient({ dataCollection: { userInfo: false } });

const server = Bun.serve({
async fetch(_req) {
return new Response('Bun!');
},
port,
});

await fetch(`http://localhost:${port}/`, {
headers: { 'X-Forwarded-For': '203.0.113.7' },
});

await server.stop();

expect(startSpanSpy).toHaveBeenCalledTimes(1);
const attributes = startSpanSpy.mock.calls[0]?.[0]?.attributes;
expect(attributes?.['client.address']).toBeUndefined();
expect(attributes?.['client.port']).toBeUndefined();
expect(attributes?.['network.protocol.name']).toBe('http');
});

test('keeps PII request headers when dataCollection enables full header collection', async () => {
setupClient({ dataCollection: { httpHeaders: { request: true, response: true } } });

Expand Down