Skip to content

feat(elysia): Elysia SDK#19509

Open
logaretm wants to merge 21 commits intodevelopfrom
awad/js-1542-add-elysia-integration
Open

feat(elysia): Elysia SDK#19509
logaretm wants to merge 21 commits intodevelopfrom
awad/js-1542-add-elysia-integration

Conversation

@logaretm
Copy link
Member

@logaretm logaretm commented Feb 24, 2026

Key Decisions:

  • Built on @sentry/bun since Elysia is Bun-first.
  • We uses Elysia's first-party OTel plugin for lifecycle instrumentation instead of rolling our own.
  • We had to filter out bunServerIntegration to avoid competing root spans, as Elysia creates its own.
  • Error handling: Captures 5xx and <= 299, skips 3xx/4xx. Aligned with Fastify. Overridable via shouldHandleError.
  • Client hooks registered once via module-level guard, safe to call withElysia() multiple times.
  • Elysia produces empty child spans for arrow function handlers. We collect their IDs in spanEnd (still empty at that point) and strip them in beforeSendEvent. Unless the user provides a named function, we will strip them, check the trace below as an example of a named function logRequest vs the stripped event handlers in other life cycle hooks.
CleanShot 2026-03-05 at 15 19 34@2x CleanShot 2026-03-05 at 15 02 12@2x

TODOs:

  • Plugin API to address registration order.
  • Figure out a way to drop the root span or parameterize it.
  • Transform into an SDK

Closes #18956

@linear
Copy link

linear bot commented Feb 24, 2026

JS-1542 Add Elysia

@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 25.69 kB +0.2% +49 B 🔺
@sentry/browser - with treeshaking flags 24.17 kB +0.14% +33 B 🔺
@sentry/browser (incl. Tracing) 42.67 kB +0.13% +54 B 🔺
@sentry/browser (incl. Tracing, Profiling) 47.33 kB +0.12% +55 B 🔺
@sentry/browser (incl. Tracing, Replay) 81.48 kB +0.08% +57 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 71.06 kB +0.1% +69 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 86.17 kB +0.06% +50 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 98.41 kB +0.04% +36 B 🔺
@sentry/browser (incl. Feedback) 42.48 kB +0.08% +30 B 🔺
@sentry/browser (incl. sendFeedback) 30.35 kB +0.15% +43 B 🔺
@sentry/browser (incl. FeedbackAsync) 35.4 kB +0.12% +39 B 🔺
@sentry/browser (incl. Metrics) 26.96 kB +0.15% +38 B 🔺
@sentry/browser (incl. Logs) 27.1 kB +0.12% +32 B 🔺
@sentry/browser (incl. Metrics & Logs) 27.78 kB +0.15% +39 B 🔺
@sentry/react 27.45 kB +0.22% +58 B 🔺
@sentry/react (incl. Tracing) 45.01 kB +0.14% +60 B 🔺
@sentry/vue 30.13 kB +0.16% +46 B 🔺
@sentry/vue (incl. Tracing) 44.52 kB +0.09% +39 B 🔺
@sentry/svelte 25.7 kB +0.16% +40 B 🔺
CDN Bundle 28.35 kB +0.27% +75 B 🔺
CDN Bundle (incl. Tracing) 43.57 kB +0.15% +62 B 🔺
CDN Bundle (incl. Logs, Metrics) 29.22 kB +0.27% +77 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 44.43 kB +0.17% +75 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 68.29 kB +0.13% +85 B 🔺
CDN Bundle (incl. Tracing, Replay) 80.41 kB +0.1% +73 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 81.31 kB +0.1% +76 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 85.97 kB +0.12% +103 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 86.86 kB +0.1% +86 B 🔺
CDN Bundle - uncompressed 82.7 kB +0.1% +77 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 128.62 kB +0.05% +64 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 85.57 kB +0.1% +77 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 131.49 kB +0.05% +64 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 209.22 kB +0.05% +102 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 245.5 kB +0.04% +89 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 248.35 kB +0.04% +89 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 258.41 kB +0.04% +89 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 261.26 kB +0.04% +89 B 🔺
@sentry/nextjs (client) 47.4 kB +0.08% +37 B 🔺
@sentry/sveltekit (client) 43.12 kB +0.12% +51 B 🔺
@sentry/node-core 56.42 kB +0.13% +73 B 🔺
@sentry/node 170.58 kB -1.49% -2.58 kB 🔽
@sentry/node - without tracing 96.45 kB +0.12% +107 B 🔺
@sentry/aws-serverless 113.54 kB +0.18% +203 B 🔺

View base workflow run

@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,887 - 8,876 +0%
GET With Sentry 1,639 18% 1,607 +2%
GET With Sentry (error only) 6,069 68% 5,972 +2%
POST Baseline 1,179 - 1,166 +1%
POST With Sentry 575 49% 556 +3%
POST With Sentry (error only) 1,026 87% 1,022 +0%
MYSQL Baseline 3,163 - 3,096 +2%
MYSQL With Sentry 384 12% 366 +5%
MYSQL With Sentry (error only) 2,560 81% 2,510 +2%

View base workflow run

@logaretm logaretm changed the title feat(node-core/bun): Add Elysia Integration feat(elysia): Elysia SDK Mar 5, 2026
@logaretm logaretm force-pushed the awad/js-1542-add-elysia-integration branch 10 times, most recently from 7d220e3 to 672ad18 Compare March 11, 2026 20:32
@logaretm logaretm marked this pull request as ready for review March 12, 2026 03:42
Copilot AI review requested due to automatic review settings March 12, 2026 03:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new @sentry/elysia package to the monorepo to provide first-party Sentry support for the Elysia framework (Bun-first), including SDK initialization, Elysia app wiring, and E2E coverage.

Changes:

  • Introduces the new @sentry/elysia package (build config, exports, types, README/license, unit tests).
  • Implements init() (built on @sentry/bun) and withElysia() (OTel plugin + request/error hooks + span cleanup/enrichment).
  • Adds a Bun + Elysia Playwright E2E test application and wires it into CI/publishing metadata.

Reviewed changes

Copilot reviewed 33 out of 34 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
yarn.lock Adds Elysia + OTel plugin dependency graph and related transitive updates.
packages/elysia/tsconfig.types.json Type build config for generating .d.ts output.
packages/elysia/tsconfig.test.json Test TS config (Bun types) for the new package.
packages/elysia/tsconfig.json Package TS config (Bun types) for source compilation.
packages/elysia/test/withElysia.test.ts Unit tests for withElysia() hook registration, header propagation, and error capturing behavior.
packages/elysia/test/sdk.test.ts Unit tests for init() and getDefaultIntegrations() behavior.
packages/elysia/src/withElysia.ts Core Elysia wiring: OTel plugin registration, request metadata, response header propagation, error capture, span filtering/enrichment.
packages/elysia/src/types.ts Defines ElysiaOptions as BunOptions.
packages/elysia/src/sdk.ts Implements init() and Bun default integration filtering.
packages/elysia/src/index.ts Public API surface: re-exports from @sentry/bun plus Elysia-specific exports.
packages/elysia/rollup.npm.config.mjs Rollup config for publishing build artifacts.
packages/elysia/package.json New package manifest (deps/peers/scripts/exports).
packages/elysia/README.md Usage documentation and links for the new SDK.
packages/elysia/LICENSE Package license file.
packages/elysia/.eslintrc.js Package-local ESLint configuration.
package.json Adds packages/elysia workspace + new resolutions for OTel packages.
dev-packages/e2e-tests/verdaccio-config/config.yaml Allows publishing @sentry/elysia to local registry for E2E tests.
dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts Updates export consistency checks (ignore list additions).
dev-packages/e2e-tests/test-applications/bun-elysia/tsconfig.json TS config for the new Bun+Elysia E2E app.
dev-packages/e2e-tests/test-applications/bun-elysia/tests/transactions.test.ts E2E validation for transactions, route parameterization, lifecycle spans, and manual spans.
dev-packages/e2e-tests/test-applications/bun-elysia/tests/propagation.test.ts E2E validation for inbound propagation headers + documented fixmes for outbound propagation gaps.
dev-packages/e2e-tests/test-applications/bun-elysia/tests/isolation.test.ts Documented fixme for per-request isolation behavior.
dev-packages/e2e-tests/test-applications/bun-elysia/tests/errors.test.ts E2E validation for error capture rules and request metadata inclusion.
dev-packages/e2e-tests/test-applications/bun-elysia/start-event-proxy.mjs Starts local event proxy for E2E capture.
dev-packages/e2e-tests/test-applications/bun-elysia/src/app.ts E2E fixture Elysia app exercising routes, errors, propagation, and concurrency.
dev-packages/e2e-tests/test-applications/bun-elysia/playwright.config.mjs Playwright runner config for the new E2E app.
dev-packages/e2e-tests/test-applications/bun-elysia/package.json E2E app manifest (depends on @sentry/elysia).
dev-packages/e2e-tests/test-applications/bun-elysia/.npmrc Points test app to local Verdaccio.
dev-packages/e2e-tests/test-applications/bun-elysia/.gitignore Ignores build output for the E2E app.
README.md Adds @sentry/elysia to the list of SDKs.
.size-limit.js Adjusts bundle size limits (likely due to dependency graph changes).
.github/workflows/build.yml Ensures Bun is set up for the new bun-elysia E2E matrix entry.
.github/ISSUE_TEMPLATE/bug.yml Adds @sentry/elysia to bug report SDK dropdown.
.craft.yml Adds publishing target configuration for @sentry/elysia.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +22 to +59
function getRuntime(): { name: string; version: string } {
if (typeof Bun !== 'undefined') {
return { name: 'bun', version: Bun.version };
}

return { name: 'node', version: process.version };
}

/**
* Initializes the Sentry Elysia SDK.
*
* @example
* ```javascript
* import * as Sentry from '@sentry/elysia';
*
* Sentry.init({
* dsn: '__DSN__',
* tracesSampleRate: 1.0,
* });
* ```
*/
export function init(userOptions: ElysiaOptions = {}): NodeClient | undefined {
applySdkMetadata(userOptions, 'elysia');

const options = {
...userOptions,
platform: 'javascript',
runtime: getRuntime(),
serverName: userOptions.serverName || global.process.env.SENTRY_NAME || os.hostname(),
};

options.transport = options.transport || makeFetchTransport;

if (options.defaultIntegrations === undefined) {
options.defaultIntegrations = getDefaultIntegrations(options);
}

return initNode(options);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRuntime() falls back to { name: 'node', version: process.version } when Bun is undefined, but init() still calls init from @sentry/bun. The Bun SDK’s init references Bun.version internally, so calling @sentry/elysia.init() in a non-Bun runtime will still throw at runtime. Either explicitly enforce Bun-only usage (and throw a clear error when Bun is undefined) or conditionally use a Node-compatible init implementation when running on Node.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be relevant no? Not sure how it behaves actually if Elysia is called in a Node environment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I haven't tried it out, I think I will fix it in the bun SDK and maybe add a node variant test for elysia just in case.

@logaretm logaretm force-pushed the awad/js-1542-add-elysia-integration branch from 19113b3 to 68051af Compare March 12, 2026 04:59
@logaretm logaretm force-pushed the awad/js-1542-add-elysia-integration branch from 68051af to 88952b2 Compare March 16, 2026 14:25
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment on lines +49 to +59
}

// Decide whether to keep the span
if (
(!span.description || span.description === '<unknown>') &&
span.parent_span_id &&
elysiaSpanIds.has(span.parent_span_id)
) {
continue; // filter out
}
filteredSpans.push(span);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The span filtering logic in beforeSendEvent assumes parent spans precede children. When >1000 spans exist, they are sorted by timestamp, breaking this assumption and causing incorrect filtering.
Severity: MEDIUM

Suggested Fix

A multi-pass approach is needed. First, iterate through all spans to collect all elysiaSpanIds. Then, perform a second pass to filter out spans whose parent_span_id is in the collected set. This ensures all parent IDs are known before any filtering decisions are made, regardless of span order.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/elysia/src/clientHooks.ts#L44-L59

Potential issue: The `beforeSendEvent` hook iterates through spans to filter out
children of Elysia lifecycle spans. It does this by accumulating parent span IDs in a
set (`elysiaSpanIds`) and then checking if a span's `parent_span_id` is in that set.
This logic assumes parents are always processed before their children. However, when a
transaction has more than 1000 spans, the SDK sorts the `spans` array by
`start_timestamp`. This can place a child span before its parent in the array, causing
the parent ID check to fail. As a result, empty anonymous Elysia spans are not filtered
out and are incorrectly included in the final transaction event.

Comment on lines +72 to +76
app.onRequest(context => {
getIsolationScope().setSDKProcessingMetadata({
normalizedRequest: winterCGRequestToRequestData(context.request),
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The onRequest hook is missing the { as: 'global' } option, preventing it from running on routes defined after withElysia() is called.
Severity: MEDIUM

Suggested Fix

Add the { as: 'global' } option as the first argument to the app.onRequest call. This will ensure the hook is applied globally to all routes, including those defined after the plugin has been initialized. The updated call should be app.onRequest({ as: 'global' }, ...). This makes it consistent with the onAfterHandle and onError hooks.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/elysia/src/withElysia.ts#L72-L76

Potential issue: The `onRequest` hook is initialized without the `{ as: 'global' }`
option. According to Elysia's lifecycle documentation, hooks are isolated by default and
will not be inherited by parent instances unless explicitly marked as global. Because
the standard usage pattern is to define routes after initializing the Sentry plugin via
`withElysia()`, this `onRequest` hook will not execute for those routes. This will cause
Sentry error events to be captured without crucial request context, such as the URL,
method, and headers, as the `setSDKProcessingMetadata` function will not be called for
those requests.

Copy link
Member

@JPeer264 JPeer264 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I think it is ok to be shipped as for now it is alpha. I'm still worried about the modiefied .size-limit.js

"sucrase": "getsentry/sucrase#es2020-polyfills",
"**/express/path-to-regexp": "0.1.12"
"**/express/path-to-regexp": "0.1.12",
"**/@opentelemetry/core": "2.6.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Since I just saw this. Why exactly was this added?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So Elysia comes with its own openetelemetry package, which has dependencies on opentelemetry packages that didn't match ours, forcing us to pull them in and duplicating them, causing all server bundles sizes to jump 20kb+

I had to lock them down to specific versions we use today to avoid this, ideally once I add tracing channels to Elysia I will drop all of this completely.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is really needed we would need to update our upgrade-otel skill to also check this part. Then we are on the safe side.

logaretm and others added 21 commits March 23, 2026 14:18
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>
On Node.js, the root HTTP span is created by Node's HTTP instrumentation
with only the raw URL. The Elysia OTel plugin creates a child span with
route info but doesn't propagate it up. This updates the root span and
isolation scope with the parameterized route name in onAfterHandle (for
successful requests on Node.js) and onError (for all runtimes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node 20 does not support running .ts files directly. Node 24 has
TypeScript stripping enabled by default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@logaretm logaretm force-pushed the awad/js-1542-add-elysia-integration branch from e35f523 to 1d4ea2a Compare March 23, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Elysia

5 participants