Summary
With @astrojs/cloudflare v14, @sentry/astro's server-side init is never applied to the deployed Cloudflare Worker. The Worker's default export is left un-wrapped by withSentry, so server-side error/log capture does not initialize at the fetch boundary — server-side Sentry silently does nothing.
Versions
@sentry/astro 10.62.0
@sentry/cloudflare 10.62.0
@astrojs/cloudflare 14.0.1
astro 7.0.3
output: 'server', Cloudflare Workers (not Pages)
Root cause
sentryCloudflareVitePlugin (packages/astro/src/integration/cloudflare.ts) wraps the Worker by transforming the module whose id contains astrojs-ssr-virtual-entry:
transform(code, id) {
if (!id.includes("astrojs-ssr-virtual-entry")) {
return undefined;
}
// ...rewrites `export default X;` -> `export default withSentry(() => undefined, X);`
}
@astrojs/cloudflare v14 builds the Worker through @cloudflare/vite-plugin, whose entry virtual module id is virtual:cloudflare/worker-entry (its generated body ends with export default mod.default ?? {};). The old astrojs-ssr-virtual-entry id is no longer produced, so the transform guard always returns early and the entry is never wrapped.
Effect
With an app configured per the Astro + Cloudflare docs (the sentry() integration in astro.config plus a sentry.server.config.ts doing export default Sentry.withSentry((env) => ({ ... }), handler)), the generated dist/server/entry.mjs default export is the raw Astro handler. Grepping all of dist/server for withSentry(() => finds no match — the plugin's wrap never ran.
The only remaining server wiring is the injected import "sentry.server.config.(ts|js)" (injectScript("page-ssr", ...)). That runs lazily during a page render, so it doesn't deterministically instrument the Worker's fetch entry:
- errors on the first request (before any page module has evaluated the config) aren't captured;
- routes that don't pull the page-ssr module graph (e.g. API endpoints) aren't covered;
- for a Worker whose first traffic is an API route,
Sentry.init may never run at all.
Repro
- Scaffold an Astro app with
output: 'server' and the @astrojs/cloudflare v14 adapter (Workers, not Pages).
npm i @sentry/astro @sentry/cloudflare; add sentry() to integrations; create sentry.server.config.ts per the docs:
import * as Sentry from '@sentry/cloudflare'
import handler from '@astrojs/cloudflare/entrypoints/server'
export default Sentry.withSentry((env) => ({ dsn: env.SENTRY_DSN, enableLogs: true }), handler)
astro build.
- Inspect
dist/server/entry.mjs: the default export is the un-wrapped Astro handler, and grep -r "withSentry(() =>" dist/server returns nothing.
Suggested fix
Have sentryCloudflareVitePlugin's transform also match the @cloudflare/vite-plugin entry id virtual:cloudflare/worker-entry (whose default-export expression is mod.default ?? {}), in addition to astrojs-ssr-virtual-entry, so the wrap applies on @astrojs/cloudflare v14+.
Workaround
For anyone hitting this before a fix lands: a tiny build-only Vite plugin that transforms the virtual:cloudflare/worker-entry module and wraps its export default mod.default ?? {}; with your own withSentry(optionsCallback, handler) restores deterministic, eager wrapping of the Worker entry.
Summary
With
@astrojs/cloudflarev14,@sentry/astro's server-side init is never applied to the deployed Cloudflare Worker. The Worker's default export is left un-wrapped bywithSentry, so server-side error/log capture does not initialize at the fetch boundary — server-side Sentry silently does nothing.Versions
@sentry/astro10.62.0@sentry/cloudflare10.62.0@astrojs/cloudflare14.0.1astro7.0.3output: 'server', Cloudflare Workers (not Pages)Root cause
sentryCloudflareVitePlugin(packages/astro/src/integration/cloudflare.ts) wraps the Worker by transforming the module whose id containsastrojs-ssr-virtual-entry:@astrojs/cloudflarev14 builds the Worker through@cloudflare/vite-plugin, whose entry virtual module id isvirtual:cloudflare/worker-entry(its generated body ends withexport default mod.default ?? {};). The oldastrojs-ssr-virtual-entryid is no longer produced, so thetransformguard always returns early and the entry is never wrapped.Effect
With an app configured per the Astro + Cloudflare docs (the
sentry()integration inastro.configplus asentry.server.config.tsdoingexport default Sentry.withSentry((env) => ({ ... }), handler)), the generateddist/server/entry.mjsdefault export is the raw Astro handler. Grepping all ofdist/serverforwithSentry(() =>finds no match — the plugin's wrap never ran.The only remaining server wiring is the injected
import "sentry.server.config.(ts|js)"(injectScript("page-ssr", ...)). That runs lazily during a page render, so it doesn't deterministically instrument the Worker's fetch entry:Sentry.initmay never run at all.Repro
output: 'server'and the@astrojs/cloudflarev14 adapter (Workers, not Pages).npm i @sentry/astro @sentry/cloudflare; addsentry()tointegrations; createsentry.server.config.tsper the docs:astro build.dist/server/entry.mjs: the default export is the un-wrapped Astro handler, andgrep -r "withSentry(() =>" dist/serverreturns nothing.Suggested fix
Have
sentryCloudflareVitePlugin'stransformalso match the@cloudflare/vite-pluginentry idvirtual:cloudflare/worker-entry(whose default-export expression ismod.default ?? {}), in addition toastrojs-ssr-virtual-entry, so the wrap applies on@astrojs/cloudflarev14+.Workaround
For anyone hitting this before a fix lands: a tiny build-only Vite plugin that transforms the
virtual:cloudflare/worker-entrymodule and wraps itsexport default mod.default ?? {};with your ownwithSentry(optionsCallback, handler)restores deterministic, eager wrapping of the Worker entry.