Skip to content

feat(node): Add first-party Mastra integration - #23823

Open
RulaKhaled wants to merge 3 commits into
developfrom
rolaabuhasna/mastra-exporter
Open

feat(node): Add first-party Mastra integration#23823
RulaKhaled wants to merge 3 commits into
developfrom
rolaabuhasna/mastra-exporter

Conversation

@RulaKhaled

@RulaKhaled RulaKhaled commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Mastra agent traces now come from the SDK: invoke_agent, chat, and execute_tool spans, on by default for Node-family SDKs.

Ref JS-3455 (dup JSSDK-21).

How it attaches

  • Hooks the Mastra constructor and registers our exporter via registerExporter().
  • If the app already has observability, we attach to it.
  • If it doesn’t, we bootstrap a pipeline from @mastra/observability when that package is already installed. We never add it ourselves.
  • You can also register the exporter by hand.

What we emit

  • Only ops, names, and attributes from sentry-conventions.
  • Span types with no conventional op (workflow_step, memory_operation, processor_run, scorer_*) are dropped; children re-parent onto the nearest exported ancestor.
  • model_inference is dropped too. Mastra nests model_generation > model_step > model_inference, and the inference span repeats the generation’s model/usage — exporting both produced a duplicate nested chat per step. model_generation is the gen_ai.chat span.

Note to reviewer: 🗯️

This diff is large mostly because of Node integration tests and the @mastra/* yarn.lock bump, not because the SDK surface is huge.

The product decisions are in three files:

  • packages/server-utils/src/ai/mastra/utils.ts — span names and gen_ai.* attributes
  • packages/server-utils/src/ai/mastra/index.ts — exporter (drop unmapped types, re-parent children)
  • packages/server-utils/src/integrations/mastra.ts — constructor attach, bootstrap, skip duplicate provider wrapping

If you are checking that auto-instrumentation actually hooks Mastra, also glance at packages/server-utils/src/orchestrion/config/mastra.ts

@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown

JS-3455

@RulaKhaled
RulaKhaled force-pushed the rolaabuhasna/mastra-exporter branch from b5821be to 2c85c6e Compare September 1, 2026 10:11
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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 28.57 kB - -
@sentry/browser - with treeshaking flags 26.94 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 26.83 kB - -
@sentry/browser (incl. Tracing) 48.87 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.88 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.8 kB - -
@sentry/browser (incl. Tracing, Replay) 88.36 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.76 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 93.05 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 106.02 kB - -
@sentry/browser (incl. Feedback) 46.06 kB - -
@sentry/browser (incl. sendFeedback) 33.64 kB - -
@sentry/browser (incl. FeedbackAsync) 38.74 kB - -
@sentry/browser (incl. Metrics) 29.52 kB - -
@sentry/browser (incl. Logs) 29.81 kB - -
@sentry/browser (incl. Metrics & Logs) 30.45 kB - -
@sentry/react 30.32 kB - -
@sentry/react (incl. Tracing) 51.08 kB - -
@sentry/vue 35.74 kB - -
@sentry/vue (incl. Tracing) 51.14 kB - -
@sentry/svelte 28.6 kB - -
CDN Bundle 30.36 kB - -
CDN Bundle (incl. Tracing) 49.52 kB - -
CDN Bundle (incl. Logs, Metrics) 32.59 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 51.43 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.19 kB - -
CDN Bundle (incl. Tracing, Replay) 87 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 88.88 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 92.93 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 94.83 kB - -
CDN Bundle - uncompressed 89.97 kB - -
CDN Bundle (incl. Tracing) - uncompressed 147.57 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 96.27 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 153.26 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.43 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 267.07 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 272.74 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 280.77 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 286.43 kB - -
@sentry/nextjs (client) 53.7 kB - -
@sentry/sveltekit (client) 49.3 kB - -
@sentry/core/server 40.74 kB - -
@sentry/core/browser 13.42 kB - -
@sentry/node 126.89 kB +2.17% +2.69 kB 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.58 kB +0.09% +71 B 🔺
@sentry/node - without tracing 88.36 kB +0.09% +79 B 🔺
@sentry/node - without channel injection 106.2 kB +2.61% +2.7 kB 🔺
@sentry/aws-serverless 96.73 kB +0.1% +87 B 🔺
@sentry/cloudflare (withSentry) - minified 200.81 kB - -
@sentry/cloudflare (withSentry) 499.76 kB - -

View base workflow run

@RulaKhaled
RulaKhaled force-pushed the rolaabuhasna/mastra-exporter branch from 8beaa13 to 18578a1 Compare September 1, 2026 15:38
isaacs added a commit that referenced this pull request Sep 1, 2026
Name `handler` spans after the route they serve when span streaming is
enabled, or `Request handler` if no route set. Static mode left as is.

Drop Hapi method, as the template dictates. NestJS resolves no route
when the span starts. The NestJS callback name stays on
`nestjs.callback`.

Elysia sets `context.route` when the request enters the compiled
handler, which is before the `Handle` phase reports. Read it in the
trace listener so streamed handler spans carry the route instead of the
`Request handler` fallback. The fallback now applies only when the
context has no route.

Set `code.function.name` only when the handler has a name. Static mode
keeps the handler name in the span name, so the attribute adds nothing
there, and an anonymous handler has no name to record. Adding convention
for `code.function.name` in general is tracked in #23823.

Register the Fastify test route from a plugin. Fastify installs the
SDK's `onRoute` hook when it flushes its plugin list, which is after
root-level routes are in place. A root-level route therefore produces no
route handler span, and the test never reached that code path.

Also: correct `REQUEST_HANDLER_SPAN_NAME_FALLBACK`: the conventions
spell the fallback `Request handler`, and its `@see` link pointed at the
resource section.

closes #23533

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mastra is exporter-based, so we hook the constructor, register a Sentry exporter, and bootstrap an observability pipeline when the app has not configured one.
@RulaKhaled
RulaKhaled force-pushed the rolaabuhasna/mastra-exporter branch from 9f48b17 to a45a5db Compare September 1, 2026 21:27
- Stop calling `_INTERNAL_skipAiProviderWrapping`. That was copied from the
  LangChain exporter, where `@langchain/openai` and `@langchain/anthropic`
  depend on the `openai` / `@anthropic-ai/sdk` clients, so LangChain really
  does double-instrument. `@mastra/core` reaches providers through the
  fetch-based `@ai-sdk/*` packages and never imports those SDKs, so there was
  no duplicate span to suppress — only the app's own direct SDK calls were
  being dropped, permanently and process-wide, from the first Mastra event on.

- Store a parentless dropped span under its own id instead of `''`.
  `LRUMap.remove` only deletes truthy values, so the empty-string sentinel was
  never evicted; those entries accumulated to the 1000-entry cap and pushed out
  live dropped-span -> parent mappings, breaking re-parenting.

- Emit `gen_ai.request.stop_sequences` as a list rather than a JSON string, to
  match the `string[]` type in sentry-conventions and the Bedrock integration.
  `gen_ai.response.finish_reasons` stays string-encoded: it is typed `string`
  and the OpenAI integration encodes it the same way.

- Replace the per-span `keys().includes()` scan in `_trackSpan` with a `size`
  check, and end the previous span on a duplicate `span_started` — `LRUMap.set`
  evicts the oldest entry even when overwriting an existing key, which dropped
  a span without ending it.

- Gate `isExportedSpanType` on `hasOwnProperty` so span types named
  `constructor` or `toString` are not treated as exported.

- Widen the orchestrion `filePath` to any `mastra*` chunk under `dist/` so a
  chunk rename inside the supported version range does not silently disable
  instrumentation. Matches the same two files in 1.63.2.

- Revert the unrelated `.unordered()` added to the Cloudflare Workflow test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1cK2XXFg4tCaQYQ5yJjND
@RulaKhaled

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1c544d9. Configure here.

@RulaKhaled

Copy link
Copy Markdown
Collaborator Author

I know this looks big, but the reviewable part is not. most of the diff is Node integration tests and a @mastra/* yarn.lock bump. The product decisions are in three files listed in the description, start with those and the rest should fall into place

youhavetobelieveme-please

@RulaKhaled
RulaKhaled marked this pull request as ready for review September 2, 2026 09:31
@RulaKhaled
RulaKhaled requested review from a team as code owners September 2, 2026 09:31
@RulaKhaled
RulaKhaled requested review from JPeer264, isaacs, nicohrubec and s1gr1d and removed request for a team September 2, 2026 09:31

@JPeer264 JPeer264 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.

Quite some big stuff. Added some comments


public readonly [MASTRA_EXPORTER_BRAND] = true;

private readonly _spans = new LRUMap<string, TrackedSpan>(MAX_TRACKED_MASTRA_SPANS);

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: Do we really want to store the spans in an LRUMap? This would mean that we might just drop random spans right? I could imagine that some child span has then no parent at all, as this might be dropped already

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yah the cap is intentional (prisma does something similar) so a span that never ends cannot leak. dropping is oldest-first so it is not really random, and we end() the span before dropping it. a later child uses getActiveSpan() (the request span if one is active) or becomes a new root, it doesn't keep a dangling parent id

}
}

private _onSpanStarted(span: MastraExportedSpan): void {

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.

super-l: Maybe we can already start using private class methods instead in newer functions? Then we would already modernize our codebase a bit

Suggested change
private _onSpanStarted(span: MastraExportedSpan): void {
#onSpanStarted(span: MastraExportedSpan): void {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

tbh i had this first and switched it, almost everything in server-utils (and the SDK generally) uses private, i wouldn't wanna introduce any new repo-wide convention in such a big PR

Comment thread CHANGELOG.md
- `DenoMongoose` => `Mongoose`
- `DenoMysql` => `Mysql`
- `DenoPostgres` => `Postgres`
- feat(node): Add first-party Mastra integration ([#23823](https://github.com/getsentry/sentry-javascript/pull/23823)). Enabled by default; disable with `defaultIntegrations: integrations => integrations.filter(i => i.name !== 'Mastra')`.

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: Can we enable it by default? It seems Mastra 1.63.2 has an engine of 22.13+: https://github.com/mastra-ai/mastra/blob/003e75745c5fd6a7af8464ece1d2930f81dd15af/packages/core/package.json#L987

Maybe it wouldn't matter as users can't use 1.63.2 anyways when they're not on the supported version and we don't import it in our integration anyways. Just wanted to double check

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yah It's already on by default (in getTracingIntegrations(), same as the other AI integrations). we never import @mastra/core, and if registerExporter is missing we just skip

Comment thread dev-packages/node-integration-tests/suites/tracing/mastra/scenario-auto.mjs Outdated

async function run() {
// Stub of `@mastra/sentry`: same `name: 'sentry'`, no brand. The real package calls `Sentry.init()`.
const communityExporter = {

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.

q: Why do we need a stub here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The current @mastra/sentry exporter calls Sentry.init() and would replace the test client. The stub is only the name: 'sentry' signal so we hit the “community exporter already registered” path and still assert our spans go out

Comment thread packages/server-utils/src/integrations/mastra.ts Outdated
const mastra = instance as MastraInstance;
if (typeof mastra.registerExporter !== 'function') {
DEBUG_BUILD &&
debug.log('[instrumentation:mastra] `Mastra.registerExporter` is missing; needs @mastra/core >= 1.63.2');

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.

} catch {
// cwd is not the app, or `@mastra/core` is ESM-only and not in the CJS resolver.
}
parents.add(join(process.cwd(), 'noop.js'));

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.

q: What is this noop.js?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's a dummy filename, nothing ever loads it, createRequire treats its argument as a filename and resolves from dirname(that), so passing cwd itself would look in cwd's parent. I'll add a comment so that's obvious.

Use the shared channel helper, document why createRequire needs a dummy filename, and drop redundant Sentry.flush from scenarios.
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