feat(cloudflare): Bootstrap Mastra observability in bundled workers - #24381
Conversation
size-limit report 📦
|
17f167c to
3541554
Compare
3541554 to
d7bc942
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
59c24be to
74c48ea
Compare
| * 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>>; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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`; |
There was a problem hiding this comment.
l/m: PROVIDER_IDENTIFIER is technically never used, we actually don't need this, or am I missing something?
There was a problem hiding this comment.
it is used just inside of the snippet, not outside (we import it and then set the global to this variable)
74c48ea to
44b6a5d
Compare
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>
44b6a5d to
226ea43
Compare
…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>

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/observabilityviacreateRequire, which has no on-disknode_modulesin workerd, so users had to construct and wire up anObservabilitythemselves. Now@sentry/cloudflare/vitesplices a staticimport * 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__.providedModulesmarker; the integration reads it there before falling back tocreateRequire. 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 lazyimport()/createRequirein the bundle), and no-ops when Mastra isn't used.Requires
@mastra/observabilityto 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/observabilitythe 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-mastraassertions (agent/model/toolgen_aispans, tool-error capture, orchestriondataloadernesting, anhttp.serverspan) against the Cloudflare instrumentation path, where orchestrion runs at build time. Its tool/dataloader assertions run against a live model via the existingE2E_OPENROUTER_API_KEY; themanual-routetest needs no key.🤖 Generated with Claude Code