Skip to content

Commit 88952b2

Browse files
logaretmclaude
andcommitted
feat(elysia): Add Elysia SDK
Adds a new @sentry/elysia package providing Sentry instrumentation for the Elysia framework running on Bun. Includes request/response tracing, error capturing, span filtering for lifecycle hooks, trace propagation via response headers, and comprehensive unit + e2e tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 372b23f commit 88952b2

35 files changed

Lines changed: 1942 additions & 68 deletions

.craft.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ targets:
9494
- name: npm
9595
id: '@sentry/bun'
9696
includeNames: /^sentry-bun-\d.*\.tgz$/
97+
- name: npm
98+
id: '@sentry/elysia'
99+
includeNames: /^sentry-elysia-\d.*\.tgz$/
97100
- name: npm
98101
id: '@sentry/hono'
99102
includeNames: /^sentry-hono-\d.*\.tgz$/
@@ -194,6 +197,8 @@ targets:
194197
onlyIfPresent: /^sentry-cloudflare-\d.*\.tgz$/
195198
'npm:@sentry/deno':
196199
onlyIfPresent: /^sentry-deno-\d.*\.tgz$/
200+
'npm:@sentry/elysia':
201+
onlyIfPresent: /^sentry-elysia-\d.*\.tgz$/
197202
'npm:@sentry/ember':
198203
onlyIfPresent: /^sentry-ember-\d.*\.tgz$/
199204
'npm:@sentry/gatsby':

.github/ISSUE_TEMPLATE/bug.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ body:
4545
- '@sentry/cloudflare'
4646
- '@sentry/cloudflare - hono'
4747
- '@sentry/deno'
48+
- '@sentry/elysia'
4849
- '@sentry/ember'
4950
- '@sentry/gatsby'
5051
- '@sentry/google-cloud-serverless'

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,7 @@ jobs:
10091009
with:
10101010
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
10111011
- name: Set up Bun
1012-
if: contains(fromJSON('["node-exports-test-app","nextjs-16-bun"]'), matrix.test-application)
1012+
if: contains(fromJSON('["node-exports-test-app","nextjs-16-bun", "bun-elysia"]'), matrix.test-application)
10131013
uses: oven-sh/setup-bun@v2
10141014
- name: Set up AWS SAM
10151015
if: matrix.test-application == 'aws-serverless'

.size-limit.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ module.exports = [
184184
name: 'CDN Bundle (incl. Tracing)',
185185
path: createCDNPath('bundle.tracing.min.js'),
186186
gzip: true,
187-
limit: '44 KB',
187+
limit: '45 KB',
188188
},
189189
{
190190
name: 'CDN Bundle (incl. Logs, Metrics)',
@@ -196,37 +196,37 @@ module.exports = [
196196
name: 'CDN Bundle (incl. Tracing, Logs, Metrics)',
197197
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
198198
gzip: true,
199-
limit: '45 KB',
199+
limit: '46 KB',
200200
},
201201
{
202202
name: 'CDN Bundle (incl. Replay, Logs, Metrics)',
203203
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
204204
gzip: true,
205-
limit: '69 KB',
205+
limit: '70 KB',
206206
},
207207
{
208208
name: 'CDN Bundle (incl. Tracing, Replay)',
209209
path: createCDNPath('bundle.tracing.replay.min.js'),
210210
gzip: true,
211-
limit: '81 KB',
211+
limit: '82 KB',
212212
},
213213
{
214214
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics)',
215215
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
216216
gzip: true,
217-
limit: '82 KB',
217+
limit: '83 KB',
218218
},
219219
{
220220
name: 'CDN Bundle (incl. Tracing, Replay, Feedback)',
221221
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
222222
gzip: true,
223-
limit: '86 KB',
223+
limit: '88 KB',
224224
},
225225
{
226226
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
227227
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
228228
gzip: true,
229-
limit: '87 KB',
229+
limit: '88 KB',
230230
},
231231
// browser CDN bundles (non-gzipped)
232232
{
@@ -241,7 +241,7 @@ module.exports = [
241241
path: createCDNPath('bundle.tracing.min.js'),
242242
gzip: false,
243243
brotli: false,
244-
limit: '129 KB',
244+
limit: '133 KB',
245245
},
246246
{
247247
name: 'CDN Bundle (incl. Logs, Metrics) - uncompressed',
@@ -255,28 +255,28 @@ module.exports = [
255255
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
256256
gzip: false,
257257
brotli: false,
258-
limit: '132 KB',
258+
limit: '136 KB',
259259
},
260260
{
261261
name: 'CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed',
262262
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
263263
gzip: false,
264264
brotli: false,
265-
limit: '210 KB',
265+
limit: '212 KB',
266266
},
267267
{
268268
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',
269269
path: createCDNPath('bundle.tracing.replay.min.js'),
270270
gzip: false,
271271
brotli: false,
272-
limit: '246 KB',
272+
limit: '250 KB',
273273
},
274274
{
275275
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed',
276276
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
277277
gzip: false,
278278
brotli: false,
279-
limit: '250 KB',
279+
limit: '253 KB',
280280
},
281281
{
282282
name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed',
@@ -290,7 +290,7 @@ module.exports = [
290290
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
291291
gzip: false,
292292
brotli: false,
293-
limit: '264 KB',
293+
limit: '266 KB',
294294
},
295295
// Next.js SDK (ESM)
296296
{

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ package. Please refer to the README and instructions of those SDKs for more deta
7070
- [`@sentry/capacitor`](https://github.com/getsentry/sentry-capacitor): SDK for Capacitor Apps and Ionic with support
7171
for native crashes
7272
- [`@sentry/bun`](https://github.com/getsentry/sentry-javascript/tree/master/packages/bun): SDK for Bun
73+
- [`@sentry/elysia`](https://github.com/getsentry/sentry-javascript/tree/master/packages/elysia): SDK for Elysia
7374
- [`@sentry/deno`](https://github.com/getsentry/sentry-javascript/tree/master/packages/deno): SDK for Deno
7475
- [`@sentry/cloudflare`](https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare): SDK for
7576
Cloudflare
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "bun-elysia-app",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"start": "bun src/app.ts",
7+
"test": "playwright test",
8+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
9+
"test:build": "pnpm install",
10+
"test:assert": "pnpm test"
11+
},
12+
"dependencies": {
13+
"@elysiajs/opentelemetry": "^1.4.0",
14+
"@sentry/elysia": "latest || *",
15+
"elysia": "^1.4.0"
16+
},
17+
"devDependencies": {
18+
"@playwright/test": "~1.56.0",
19+
"@sentry-internal/test-utils": "link:../../../test-utils",
20+
"bun-types": "^1.2.9"
21+
},
22+
"volta": {
23+
"extends": "../../package.json"
24+
}
25+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `bun src/app.ts`,
5+
});
6+
7+
export default config;
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import * as Sentry from '@sentry/elysia';
2+
import { Elysia } from 'elysia';
3+
4+
Sentry.init({
5+
environment: 'qa', // dynamic sampling bias to keep transactions
6+
dsn: process.env.E2E_TEST_DSN,
7+
tunnel: `http://localhost:3031/`, // proxy server
8+
tracesSampleRate: 1,
9+
tracePropagationTargets: ['http://localhost:3030', '/external-allowed'],
10+
});
11+
12+
const app = Sentry.withElysia(new Elysia());
13+
14+
// Simple success route
15+
app.get('/test-success', () => ({ version: 'v1' }));
16+
17+
// Parameterized route
18+
app.get('/test-param/:param', ({ params }) => ({ paramWas: params.param }));
19+
20+
// Multiple params
21+
app.get('/test-multi-param/:param1/:param2', ({ params }) => ({
22+
param1: params.param1,
23+
param2: params.param2,
24+
}));
25+
26+
// Route that throws an error (will be caught by onError)
27+
app.get('/test-exception/:id', ({ params }) => {
28+
throw new Error(`This is an exception with id ${params.id}`);
29+
});
30+
31+
// Route with a custom span
32+
app.get('/test-transaction', () => {
33+
Sentry.startSpan({ name: 'test-span' }, () => {
34+
Sentry.startSpan({ name: 'child-span' }, () => {});
35+
});
36+
return { status: 'ok' };
37+
});
38+
39+
// Route with specific middleware via .guard or .use
40+
app.group('/with-middleware', app =>
41+
app
42+
.onBeforeHandle(() => {
43+
// This is a route-specific middleware
44+
})
45+
.get('/test', () => ({ middleware: true })),
46+
);
47+
48+
// Error with specific status code
49+
app.post('/test-post-error', () => {
50+
throw new Error('Post error');
51+
});
52+
53+
// Route that returns a non-500 error
54+
app.get('/test-4xx', ({ set }) => {
55+
set.status = 400;
56+
return { error: 'Bad Request' };
57+
});
58+
59+
// Error that reaches the error handler with status still set to 200 (unusual, should still be captured)
60+
app.get('/test-error-with-200-status', ({ set }) => {
61+
set.status = 200;
62+
throw new Error('Error with 200 status');
63+
});
64+
65+
// POST route that echoes body
66+
app.post('/test-post', ({ body }) => ({ status: 'ok', body }));
67+
68+
// Route that returns inbound headers (for propagation tests)
69+
app.get('/test-inbound-headers/:id', ({ params, request }) => {
70+
const headers = Object.fromEntries(request.headers.entries());
71+
return { headers, id: params.id };
72+
});
73+
74+
// Outgoing fetch propagation
75+
app.get('/test-outgoing-fetch/:id', async ({ params }) => {
76+
const id = params.id;
77+
const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`);
78+
const data = await response.json();
79+
return data;
80+
});
81+
82+
// Outgoing fetch to external (allowed by tracePropagationTargets)
83+
app.get('/test-outgoing-fetch-external-allowed', async () => {
84+
const response = await fetch(`http://localhost:3040/external-allowed`);
85+
const data = await response.json();
86+
return data;
87+
});
88+
89+
// Outgoing fetch to external (disallowed by tracePropagationTargets)
90+
app.get('/test-outgoing-fetch-external-disallowed', async () => {
91+
const response = await fetch(`http://localhost:3040/external-disallowed`);
92+
const data = await response.json();
93+
return data;
94+
});
95+
96+
// Route that throws a string (not an Error object)
97+
app.get('/test-string-error', () => {
98+
// eslint-disable-next-line no-throw-literal
99+
throw 'String error message';
100+
});
101+
102+
// Route for concurrent isolation tests — returns scope data in response
103+
app.get('/test-isolation/:userId', async ({ params }) => {
104+
Sentry.setUser({ id: params.userId });
105+
Sentry.setTag('user_id', params.userId);
106+
107+
// Simulate async work to increase overlap between concurrent requests
108+
await new Promise(resolve => setTimeout(resolve, 200));
109+
110+
return {
111+
userId: params.userId,
112+
isolationScopeUserId: Sentry.getIsolationScope().getUser()?.id,
113+
isolationScopeTag: Sentry.getIsolationScope().getScopeData().tags?.user_id,
114+
};
115+
});
116+
117+
// Flush route for waiting on events
118+
app.get('/flush', async () => {
119+
await Sentry.flush();
120+
return { ok: true };
121+
});
122+
123+
app.listen(3030, () => {
124+
console.log('Elysia app listening on port 3030');
125+
});
126+
127+
// Second app for external propagation tests
128+
const app2 = new Elysia();
129+
130+
app2.get('/external-allowed', ({ request }) => {
131+
const headers = Object.fromEntries(request.headers.entries());
132+
return { headers, route: '/external-allowed' };
133+
});
134+
135+
app2.get('/external-disallowed', ({ request }) => {
136+
const headers = Object.fromEntries(request.headers.entries());
137+
return { headers, route: '/external-disallowed' };
138+
});
139+
140+
app2.listen(3040, () => {
141+
console.log('External app listening on port 3040');
142+
});

0 commit comments

Comments
 (0)