Skip to content

Commit fe0272a

Browse files
fix(v8/bun): Ensure instrumentation of Bun.serve survives a server reload (#15157)
Co-authored-by: Nathan Kleyn <nathan@nathankleyn.com>
1 parent a3ddff6 commit fe0272a

File tree

3 files changed

+119
-44
lines changed

3 files changed

+119
-44
lines changed

packages/bun/src/integrations/bunserver.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,18 @@ export function instrumentBunServe(): void {
4747
Bun.serve = new Proxy(Bun.serve, {
4848
apply(serveTarget, serveThisArg, serveArgs: Parameters<typeof Bun.serve>) {
4949
instrumentBunServeOptions(serveArgs[0]);
50-
return serveTarget.apply(serveThisArg, serveArgs);
50+
const server: ReturnType<typeof Bun.serve> = serveTarget.apply(serveThisArg, serveArgs);
51+
52+
// A Bun server can be reloaded, re-wrap any fetch function passed to it
53+
// We can't use a Proxy for this as Bun does `instanceof` checks internally that fail if we
54+
// wrap the Server instance.
55+
const originalReload: typeof server.reload = server.reload.bind(server);
56+
server.reload = (serveOptions: Parameters<typeof Bun.serve>[0]) => {
57+
instrumentBunServeOptions(serveOptions);
58+
return originalReload(serveOptions);
59+
};
60+
61+
return server;
5162
},
5263
});
5364
}
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,87 @@
1-
import { beforeAll, beforeEach, describe, expect, test } from 'bun:test';
1+
import { afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
2+
import type { Span } from '@sentry/core';
23
import { getDynamicSamplingContextFromSpan, setCurrentClient, spanIsSampled, spanToJSON } from '@sentry/core';
34

45
import { BunClient } from '../../src/client';
56
import { instrumentBunServe } from '../../src/integrations/bunserver';
67
import { getDefaultBunClientOptions } from '../helpers';
78

8-
// Fun fact: Bun = 2 21 14 :)
9-
const DEFAULT_PORT = 22114;
10-
119
describe('Bun Serve Integration', () => {
1210
let client: BunClient;
11+
// Fun fact: Bun = 2 21 14 :)
12+
let port: number = 22114;
1313

1414
beforeAll(() => {
1515
instrumentBunServe();
1616
});
1717

1818
beforeEach(() => {
19-
const options = getDefaultBunClientOptions({ tracesSampleRate: 1, debug: true });
19+
const options = getDefaultBunClientOptions({ tracesSampleRate: 1 });
2020
client = new BunClient(options);
2121
setCurrentClient(client);
2222
client.init();
2323
});
2424

25+
afterEach(() => {
26+
// Don't reuse the port; Bun server stops lazily so tests may accidentally hit a server still closing from a
27+
// previous test
28+
port += 1;
29+
});
30+
2531
test('generates a transaction around a request', async () => {
32+
let generatedSpan: Span | undefined;
33+
2634
client.on('spanEnd', span => {
27-
expect(spanToJSON(span).status).toBe('ok');
28-
expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200);
29-
expect(spanToJSON(span).op).toEqual('http.server');
30-
expect(spanToJSON(span).description).toEqual('GET /');
35+
generatedSpan = span;
3136
});
3237

3338
const server = Bun.serve({
3439
async fetch(_req) {
3540
return new Response('Bun!');
3641
},
37-
port: DEFAULT_PORT,
42+
port,
3843
});
44+
await fetch(`http://localhost:${port}/`);
45+
server.stop();
3946

40-
await fetch('http://localhost:22114/');
47+
if (!generatedSpan) {
48+
throw 'No span was generated in the test';
49+
}
4150

42-
server.stop();
51+
expect(spanToJSON(generatedSpan).status).toBe('ok');
52+
expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200);
53+
expect(spanToJSON(generatedSpan).op).toEqual('http.server');
54+
expect(spanToJSON(generatedSpan).description).toEqual('GET /');
4355
});
4456

4557
test('generates a post transaction', async () => {
58+
let generatedSpan: Span | undefined;
59+
4660
client.on('spanEnd', span => {
47-
expect(spanToJSON(span).status).toBe('ok');
48-
expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200);
49-
expect(spanToJSON(span).op).toEqual('http.server');
50-
expect(spanToJSON(span).description).toEqual('POST /');
61+
generatedSpan = span;
5162
});
5263

5364
const server = Bun.serve({
5465
async fetch(_req) {
5566
return new Response('Bun!');
5667
},
57-
port: DEFAULT_PORT,
68+
port,
5869
});
5970

60-
await fetch('http://localhost:22114/', {
71+
await fetch(`http://localhost:${port}/`, {
6172
method: 'POST',
6273
});
6374

6475
server.stop();
76+
77+
if (!generatedSpan) {
78+
throw 'No span was generated in the test';
79+
}
80+
81+
expect(spanToJSON(generatedSpan).status).toBe('ok');
82+
expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200);
83+
expect(spanToJSON(generatedSpan).op).toEqual('http.server');
84+
expect(spanToJSON(generatedSpan).description).toEqual('POST /');
6585
});
6686

6787
test('continues a trace', async () => {
@@ -70,55 +90,93 @@ describe('Bun Serve Integration', () => {
7090
const PARENT_SAMPLED = '1';
7191

7292
const SENTRY_TRACE_HEADER = `${TRACE_ID}-${PARENT_SPAN_ID}-${PARENT_SAMPLED}`;
73-
const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production';
93+
const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-sample_rand=0.42,sentry-environment=production';
7494

75-
client.on('spanEnd', span => {
76-
expect(span.spanContext().traceId).toBe(TRACE_ID);
77-
expect(spanToJSON(span).parent_span_id).toBe(PARENT_SPAN_ID);
78-
expect(spanIsSampled(span)).toBe(true);
79-
expect(span.isRecording()).toBe(false);
95+
let generatedSpan: Span | undefined;
8096

81-
expect(getDynamicSamplingContextFromSpan(span)).toStrictEqual({
82-
version: '1.0',
83-
environment: 'production',
84-
});
97+
client.on('spanEnd', span => {
98+
generatedSpan = span;
8599
});
86100

87101
const server = Bun.serve({
88102
async fetch(_req) {
89103
return new Response('Bun!');
90104
},
91-
port: DEFAULT_PORT,
105+
port,
92106
});
93107

94-
await fetch('http://localhost:22114/', {
108+
await fetch(`http://localhost:${port}/`, {
95109
headers: { 'sentry-trace': SENTRY_TRACE_HEADER, baggage: SENTRY_BAGGAGE_HEADER },
96110
});
97111

98112
server.stop();
113+
114+
if (!generatedSpan) {
115+
throw 'No span was generated in the test';
116+
}
117+
118+
expect(generatedSpan.spanContext().traceId).toBe(TRACE_ID);
119+
expect(spanToJSON(generatedSpan).parent_span_id).toBe(PARENT_SPAN_ID);
120+
expect(spanIsSampled(generatedSpan)).toBe(true);
121+
expect(generatedSpan.isRecording()).toBe(false);
122+
123+
expect(getDynamicSamplingContextFromSpan(generatedSpan)).toStrictEqual({
124+
version: '1.0',
125+
sample_rand: '0.42',
126+
environment: 'production',
127+
});
99128
});
100129

101130
test('does not create transactions for OPTIONS or HEAD requests', async () => {
102-
client.on('spanEnd', () => {
103-
// This will never run, but we want to make sure it doesn't run.
104-
expect(false).toEqual(true);
131+
let generatedSpan: Span | undefined;
132+
133+
client.on('spanEnd', span => {
134+
generatedSpan = span;
105135
});
106136

107137
const server = Bun.serve({
108138
async fetch(_req) {
109139
return new Response('Bun!');
110140
},
111-
port: DEFAULT_PORT,
141+
port,
112142
});
113143

114-
await fetch('http://localhost:22114/', {
144+
await fetch(`http://localhost:${port}/`, {
115145
method: 'OPTIONS',
116146
});
117147

118-
await fetch('http://localhost:22114/', {
148+
await fetch(`http://localhost:${port}/`, {
119149
method: 'HEAD',
120150
});
121151

122152
server.stop();
153+
154+
expect(generatedSpan).toBeUndefined();
155+
});
156+
157+
test('intruments the server again if it is reloaded', async () => {
158+
let serverWasInstrumented = false;
159+
client.on('spanEnd', () => {
160+
serverWasInstrumented = true;
161+
});
162+
163+
const server = Bun.serve({
164+
async fetch(_req) {
165+
return new Response('Bun!');
166+
},
167+
port,
168+
});
169+
170+
server.reload({
171+
async fetch(_req) {
172+
return new Response('Reloaded Bun!');
173+
},
174+
});
175+
176+
await fetch(`http://localhost:${port}/`);
177+
178+
server.stop();
179+
180+
expect(serverWasInstrumented).toBeTrue();
123181
});
124182
});

packages/bun/test/sdk.test.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
import { expect, test } from 'bun:test';
1+
import { describe, expect, test } from 'bun:test';
22

33
import { init } from '../src/index';
44

5-
test("calling init shouldn't fail", () => {
6-
init({
5+
describe('Bun SDK', () => {
6+
const initOptions = {
77
dsn: 'https://00000000000000000000000000000000@o000000.ingest.sentry.io/0000000',
8+
tracesSampleRate: 1,
9+
};
10+
11+
test("calling init shouldn't fail", () => {
12+
expect(() => {
13+
init(initOptions);
14+
}).not.toThrow();
815
});
9-
expect(true).toBe(true);
10-
});
1116

12-
test('should return client from init', () => {
13-
expect(init({})).not.toBeUndefined();
17+
test('should return client from init', () => {
18+
expect(init(initOptions)).not.toBeUndefined();
19+
});
1420
});

0 commit comments

Comments
 (0)