Skip to content

feat(cloudflare): Bootstrap Mastra observability in bundled workers - #24381

Merged
msonnb merged 3 commits into
developfrom
feat/cloudflare-mastra-e2e
Sep 16, 2026
Merged

msonnb merged 3 commits into
developfrom
feat/cloudflare-mastra-e2e

Conversation

@mydea

@mydea mydea commented Sep 15, 2026

Copy link
Copy Markdown
Member

Adds Cloudflare coverage for Mastra — the most common non-Node way users run Mastra — as a Vite + workerd e2e app (cloudflare-mastra), and fixes the one thing that stopped it working with zero config.

On Cloudflare the Mastra integration couldn't bootstrap Mastra's observability pipeline: it resolves @mastra/observability via createRequire, which has no on-disk node_modules in workerd, so users had to construct and wire up an Observability themselves. Now @sentry/cloudflare/vite splices a static import * as ns from '@mastra/observability' into Sentry's own Mastra integration module — only when the package resolves — and records the namespace on a generic __SENTRY_ORCHESTRION__.providedModules marker; the integration reads it there before falling back to createRequire. Mastra tracing on Cloudflare now works with no observability config, matching the Node DX. The import lands in Sentry's module (not user code), stays statically analyzable (no lazy import()/createRequire in the bundle), and no-ops when Mastra isn't used.

Requires @mastra/observability to be an app dependency. The SDK never installs or bundles it — it is Mastra's tracing engine (the span factory), and without it Mastra emits only no-op spans, so there is nothing to capture. This is not new or Cloudflare-specific: it is equally required on Node, where the integration bootstraps from the @mastra/observability the app already has. On Cloudflare the Vite plugin's injection is additionally gated on it resolving at build time; if it is absent the plugin no-ops and no spans are produced (same net effect as Node).

The e2e app mirrors the node-mastra assertions (agent/model/tool gen_ai spans, tool-error capture, orchestrion dataloader nesting, an http.server span) against the Cloudflare instrumentation path, where orchestrion runs at build time. Its tool/dataloader assertions run against a live model via the existing E2E_OPENROUTER_API_KEY; the manual-route test needs no key.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 15, 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 29.09 kB - -
@sentry/browser - with treeshaking flags 27.35 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.26 kB - -
@sentry/browser (incl. Tracing) 50.6 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 50.62 kB - -
@sentry/browser (incl. Tracing, Profiling) 53.61 kB - -
@sentry/browser (incl. Tracing, Replay) 90.15 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.25 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 94.85 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 107.83 kB - -
@sentry/browser (incl. Feedback) 46.62 kB - -
@sentry/browser (incl. sendFeedback) 34.15 kB - -
@sentry/browser (incl. FeedbackAsync) 39.26 kB - -
@sentry/browser (incl. Metrics) 30.1 kB - -
@sentry/browser (incl. Logs) 30.35 kB - -
@sentry/browser (incl. Metrics & Logs) 31.02 kB - -
@sentry/react 30.84 kB - -
@sentry/react (incl. Tracing) 52.94 kB - -
@sentry/vue 36.34 kB - -
@sentry/vue (incl. Tracing) 52.91 kB - -
@sentry/svelte 29.11 kB - -
CDN Bundle 30.8 kB - -
CDN Bundle (incl. Tracing) 51.15 kB - -
CDN Bundle (incl. Logs, Metrics) 33.06 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 53.09 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.75 kB - -
CDN Bundle (incl. Tracing, Replay) 88.69 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.63 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 94.73 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.71 kB - -
CDN Bundle - uncompressed 91.16 kB - -
CDN Bundle (incl. Tracing) - uncompressed 152.66 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.73 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 158.61 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 227.14 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 272.23 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 278.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.93 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.86 kB - -
@sentry/nextjs (client) 55.27 kB - -
@sentry/sveltekit (client) 51.05 kB - -
@sentry/core/server 37.13 kB - -
@sentry/core/browser 13.66 kB - -
@sentry/node 132.38 kB +0.03% +30 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.03 kB - -
@sentry/node - without tracing 89.83 kB +0.03% +25 B 🔺
@sentry/node - without channel injection 111.23 kB +0.04% +40 B 🔺
@sentry/aws-serverless 98.07 kB +0.03% +24 B 🔺
@sentry/cloudflare (withSentry) - minified 203.54 kB - -
@sentry/cloudflare (withSentry) 506.83 kB - -

View base workflow run

@mydea
mydea added this pull request to stack #24369 September 15, 2026 08:31
@mydea mydea changed the title test(cloudflare): Add cloudflare-mastra e2e test app feat(cloudflare): Bootstrap Mastra observability in bundled workers Sep 15, 2026
@mydea
mydea force-pushed the feat/cloudflare-mastra-e2e branch from 17f167c to 3541554 Compare September 15, 2026 10:24
@mydea
mydea marked this pull request as ready for review September 15, 2026 11:04
@mydea
mydea requested review from a team as code owners September 15, 2026 11:04
@mydea
mydea requested review from isaacs and removed request for a team September 15, 2026 11:04
Comment thread packages/cloudflare/src/vite/mastraObservability.ts

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

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

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit d7bc942. Configure here.

Comment thread dev-packages/e2e-tests/test-applications/cloudflare-mastra/tests/mastra.test.ts Outdated
@mydea
mydea force-pushed the feat/cloudflare-mastra-e2e branch from 59c24be to 74c48ea Compare September 16, 2026 07:42

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

Approved since it works. I'm still concerned about the new providedModules addition, but we can remove/change this anytime as it is internal code technically

* the integration can read a bundled peer dependency it cannot `createRequire`
* in a bundled runtime such as Cloudflare Workers.
*/
providedModules?: Record<string, Record<string, unknown>>;

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: What is the benefit of having providedModules. As far as I could see it only checks if it is there. Can't we check against the existing runtime and/or bundler instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

no, this is different, this is literally the imported module, think:

import * as imported from 'mastra';

globalObj.__SENTRY_ORCHESTRION__.providedModules['mastra'] = imported;

`import * as ${PROVIDER_IDENTIFIER} from '@mastra/observability';\n` +
'(globalThis.__SENTRY_ORCHESTRION__ = globalThis.__SENTRY_ORCHESTRION__ || {});\n' +
'(globalThis.__SENTRY_ORCHESTRION__.providedModules = globalThis.__SENTRY_ORCHESTRION__.providedModules || {})' +
`['@mastra/observability'] = ${PROVIDER_IDENTIFIER};\n`;

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.

l/m: PROVIDER_IDENTIFIER is technically never used, we actually don't need this, or am I missing something?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

it is used just inside of the snippet, not outside (we import it and then set the global to this variable)

Base automatically changed from feat/mastra-error-capture to develop September 16, 2026 08:21
@mydea
mydea force-pushed the feat/cloudflare-mastra-e2e branch from 74c48ea to 44b6a5d Compare September 16, 2026 08:21
mydea and others added 3 commits September 16, 2026 11:05
Mirror the node-mastra e2e app on Cloudflare Workers (Vite + workerd) to
cover the most common non-Node way users run Mastra. Instrumentation is
wired at build time by @sentry/cloudflare/vite (orchestrion channels
injected into @mastra/core; mastraIntegration self-registers via the
global marker), with options sourced from instrument.server.ts.

Adapts for workerd: the Mastra instance is constructed lazily inside
fetch (so it runs after withSentry has subscribed), uses InMemoryStore
instead of @mastra/libsql, and configures @mastra/observability
explicitly with a placeholder exporter (the SDK's createRequire-based
bootstrap can't run in a bundled worker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Cloudflare the Mastra integration could not bootstrap Mastra's
observability pipeline: it resolves `@mastra/observability` via
`createRequire`, which has no on-disk `node_modules` in workerd, so users
had to construct and wire up an `Observability` themselves.

`@sentry/cloudflare/vite` now splices a static
`import * as ns from '@mastra/observability'` into Sentry's own Mastra
integration module (only when the package resolves) and stashes the
namespace on a generic `__SENTRY_ORCHESTRION__.providedModules` marker.
The integration reads it there before falling back to `createRequire`, so
Mastra tracing works with zero observability config — the same DX as Node.

Simplifies the cloudflare-mastra e2e app to drop its placeholder
observability config accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Mastra integration now reports tool errors as `handled: true` (surfaced via
Mastra's observability rather than crashing), so align this e2e assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JPeer264
JPeer264 force-pushed the feat/cloudflare-mastra-e2e branch from 44b6a5d to 226ea43 Compare September 16, 2026 09:06
@msonnb
msonnb merged commit 5c17dbe into develop Sep 16, 2026
627 of 633 checks passed
@msonnb
msonnb deleted the feat/cloudflare-mastra-e2e branch September 16, 2026 09:30
msonnb pushed a commit that referenced this pull request Sep 16, 2026
…kerd (#24386)

Built on top of #24381 (the Cloudflare Mastra e2e app).

On workerd, `diagnostics_channel` `publish()`/`runStores()` throw at
module/global scope, so a dependency wrapped by build-time
instrumentation and instantiated at module scope (`const app = new
Hono()`, `new Mastra()`) either throws or — because the Cloudflare
client is cached and `init()` runs per-request — is silently never
instrumented: no subscriber exists yet at module-eval time, so the
wrapping event has nowhere to go. #24381 worked around this by lazily
constructing Mastra inside the request handler.

This routes the orchestrion emit side through a workerd-safe
`diagnostics_channel` façade, set via the transformer's global
`dcModule` (a new `@sentry/cloudflare/orchestrion-diagnostics-channel`
subpath). The façade delegates to the real channels, forces the emit
wrapper past its no-subscriber short-circuit so the wrapped call is
always observed, and — when publish can't run at module scope — defers
the event onto a module-scope queue that `wrapRequestHandlerWithInit`
flushes at the top of the first request, after `init()` has subscribed.
Deferral is gated on whether a subscriber was actually present at emit
time, so top-level `init()` delivers exactly once and the per-request
cached-init case delivers exactly once at flush. In a request the façade
is a transparent pass-through, so lifecycle/span channels behave exactly
as on Node. Only `@sentry/cloudflare` changes; `@sentry/server-utils`
gains just the generic `dcModule` plugin option.

The e2e side removes #24381's lazy `getMastra()` workaround and
constructs `new Mastra()` at module scope again, which is what now
exercises the defer→flush path end-to-end (the OpenRouter key is read
from `process.env`, populated at module scope under `nodejs_compat`).

_Root cause_ — verified against a real workerd (`wrangler dev`,
`nodejs_compat`): `Channel.hasSubscribers` is a getter on Node but a
**method** on workerd (naive `!ch.hasSubscribers` is always false);
`publish` throws the global-scope error even with no subscribers, but
only *after* delivering to any subscribers already present; and
module-scope state (the defer queue) survives into request handlers. All
three shape the façade.

_Design boundary_: defer/flush recovers instrumentation that patches the
constructed **instance** (e.g. Mastra's exporter registration). It
cannot recover instrumentation that mutates constructor **arguments**
(e.g. DataLoader wrapping its `batchLoadFn`), which must run
synchronously during construction.

**Still TODO (draft):** run full `build`/`lint`/`test` and the Mastra
e2e; add unit coverage for the façade defer→flush and the `dcModule`
threading.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

3 participants