Skip to content

feat(server-utils): Capture errors thrown in Mastra - #24374

Open
mydea wants to merge 1 commit into
feat/mastra-active-spansfrom
feat/mastra-error-capture
Open

feat(server-utils): Capture errors thrown in Mastra#24374
mydea wants to merge 1 commit into
feat/mastra-active-spansfrom
feat/mastra-error-capture

Conversation

@mydea

@mydea mydea commented Sep 14, 2026

Copy link
Copy Markdown
Member

Stacked on #24368.

When a Mastra tool or model operation throws, the exporter only reflected it on the span (error status + a stack-less error.type, from Mastra's serialized errorInfo) — the error never became a Sentry issue.

Mastra runs each operation's work inside executeWithContext({ span, fn }) (the same seam #24368 uses for nesting). When fn rejects, that channel's error event carries the real Error, stack and all. The integration now subscribes to it and captures the error with captureException (mechanism auto.ai.mastra, handled: false), inside withActiveSpan of the exporter's span for that operation so the issue lands on the right trace. captureException dedupes on the error instance, so an error re-thrown through outer operations is captured only once.

Capturing needs no async-context binding, so it rides the attach-only path next to the exporter registration (not gated behind waitForTracingChannelBinding).

Root cause: the exporter receives errorInfo ({ name, message }), not the thrown Error, so it can only set span status — there's no stack to build a real issue from. The actual Error is only observable at the throw site, which executeWithContext's channel error surfaces.

Verified in the node integration suite (Node 22, ESM + CJS): a thrown tool error is captured as an issue with type: Error, the real message, a non-empty stack, and mechanism auto.ai.mastra/handled: false. The node-mastra e2e error test now also asserts the captured issue (not just the errored span), and the corresponding TODO is dropped.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.96 kB - -
@sentry/browser - with treeshaking flags 27.26 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.15 kB - -
@sentry/browser (incl. Tracing) 50.51 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 50.52 kB - -
@sentry/browser (incl. Tracing, Profiling) 53.5 kB - -
@sentry/browser (incl. Tracing, Replay) 90.07 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.16 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 94.77 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 107.73 kB - -
@sentry/browser (incl. Feedback) 46.46 kB - -
@sentry/browser (incl. sendFeedback) 34.01 kB - -
@sentry/browser (incl. FeedbackAsync) 39.12 kB - -
@sentry/browser (incl. Metrics) 29.98 kB - -
@sentry/browser (incl. Logs) 30.24 kB - -
@sentry/browser (incl. Metrics & Logs) 30.91 kB - -
@sentry/react 30.72 kB - -
@sentry/react (incl. Tracing) 52.81 kB - -
@sentry/vue 36.2 kB - -
@sentry/vue (incl. Tracing) 52.76 kB - -
@sentry/svelte 28.98 kB - -
CDN Bundle 30.7 kB - -
CDN Bundle (incl. Tracing) 51.01 kB - -
CDN Bundle (incl. Logs, Metrics) 32.98 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 52.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.67 kB - -
CDN Bundle (incl. Tracing, Replay) 88.56 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.53 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 94.64 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.63 kB - -
CDN Bundle - uncompressed 90.83 kB - -
CDN Bundle (incl. Tracing) - uncompressed 152.33 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.41 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 158.29 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.82 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 271.9 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 277.85 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.6 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.54 kB - -
@sentry/nextjs (client) 55.13 kB - -
@sentry/sveltekit (client) 50.93 kB - -
@sentry/core/server 37.13 kB - -
@sentry/core/browser 13.66 kB - -
@sentry/node 130.18 kB +0.22% +276 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.88 kB +0.07% +50 B 🔺
@sentry/node - without tracing 89.67 kB +0.1% +86 B 🔺
@sentry/node - without channel injection 109.1 kB +0.28% +303 B 🔺
@sentry/aws-serverless 97.9 kB +0.09% +80 B 🔺
@sentry/cloudflare (withSentry) - minified 203.36 kB - -
@sentry/cloudflare (withSentry) 506.44 kB - -

View base workflow run

@mydea
mydea added this pull request to stack #24369 September 14, 2026 12:41
When a Mastra tool or model operation throws, the exporter only reflected it on
the span (error status + stack-less `error.type` from Mastra's serialized
`errorInfo`); the error never became a Sentry issue.

Mastra runs each operation inside `executeWithContext({ span, fn })`, and when
`fn` rejects that channel's `error` event carries the real `Error` — stack and
all. The integration now subscribes to it and captures the error with
`captureException` (mechanism `auto.ai.mastra`, handled: false), associated with
the exporter's span for that operation so it lands on the right trace.
`captureException` dedupes on the error instance, so an error re-thrown through
outer operations is captured once. Capturing needs no async-context binding, so
it rides the attach-only path alongside the exporter registration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mydea
mydea force-pushed the feat/mastra-error-capture branch from 30f6e44 to a2593c6 Compare September 14, 2026 12:43
RulaKhaled added a commit that referenced this pull request Sep 14, 2026
Matches #24374: mechanism `auto.ai.flue` with `handled: false`, and the capture
runs under the operation's own span so the issue lands on the right trace.

Unlike Mastra, the rebuild from `errorInfo` stays. Mastra's `errorInfo` is
`{ name, message }` with no stack, which is why it has to reach for the channel's
real `Error`; Flue's carries the original stack, so there is nothing to gain from
a second seam that might not fire on every path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mydea
mydea marked this pull request as ready for review September 14, 2026 14:26
@mydea
mydea requested review from a team as code owners September 14, 2026 14:26
@mydea
mydea requested review from a team, JPeer264, RulaKhaled and isaacs and removed request for a team September 14, 2026 14:26
@mydea mydea changed the title feat(server-utils): Capture errors thrown in Mastra as Sentry issues feat(server-utils): Capture errors thrown in Mastra Sep 14, 2026
RulaKhaled added a commit that referenced this pull request Sep 14, 2026
Matches #24374: mechanism `auto.ai.flue` with `handled: false`, and the capture
runs under the operation's own span so the issue lands on the right trace.

Unlike Mastra, the rebuild from `errorInfo` stays. Mastra's `errorInfo` is
`{ name, message }` with no stack, which is why it has to reach for the channel's
real `Error`; Flue's carries the original stack, so there is nothing to gain from
a second seam that might not fire on every path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@logaretm logaretm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Got a couple of points, could be I'm missing something but I see a potential issue here when it comes to reporting errors off the tracing channel error event which we do not generally do.

const span = id ? getSentrySpanForMastraId(id) : undefined;
// `captureException` dedupes on the error instance, so the same error re-thrown through outer
// `executeWithContext` calls is captured only once.
const capture = (): string => captureException(error, { mechanism: { type: 'auto.ai.mastra', handled: false } });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

h: I think we should avoid capturing errors on the channel subscriber like we do with other subscriptions.

The tracing channel tells us that the promise/call rejected but doesn't guarantee it was handled by try/catch or .then/.catch or not.

The error subscriber is only good for setting span status, not capturing errors, which should still happen on global unhandled hooks.

function captureMastraError(error: unknown, params: unknown): void {
const id = isObjectLike(params) ? mastraSpanId(params.span) : undefined;
const span = id ? getSentrySpanForMastraId(id) : undefined;
// `captureException` dedupes on the error instance, so the same error re-thrown through outer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

m: clanker raised this, Mastra seems to wrap errors with new MastraError so they would probably escape the dedup-by-identity logic we have.

I raised another point about not capturing at all which would make this pointless anyways.

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.

2 participants