Skip to content

fix(astro, cloudflare): Run the Cloudflare SDK on the server for Cloudflare Pages - #26188

Open
logaretm wants to merge 5 commits into
developfrom
awad/astro-cf-pages-server-sdk
Open

logaretm wants to merge 5 commits into
developfrom
awad/astro-cf-pages-server-sdk

Conversation

@logaretm

@logaretm logaretm commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

This PR addresses a few issues I found while investigating some comments on #25999

CF Pages runs the browser SDK on the server

This means it will immediately crash if we ship anything that assumes a server-side SDK is being used, this showed up when I tried to use the serveTunnelIntegration on the other PR, it doesn't exist on the client SDKs.

This happens because we do not have an exports condition for worker/workerd like other SDKs. So @sentry/astro resolves to index.client.js.

// @sentry/astro exports["."]: no workerd entry, so workerd falls through to the client build
{ "node": "./build/esm/index.server.js", "browser": "./build/esm/index.client.js", "import": "./build/esm/index.client.js" }

// @astrojs/cloudflare v12 SSR resolve conditions: no "node"
vite.ssr.resolve.conditions.push("workerd", "worker");

We had no E2E apps covering this so it never came up, also it doesn't happen while using withSentry. But a user trying to call Sentry.init from @sentry/astro will trigger the bug.

I fixed this by adding a new workerd/worker export conditions that point to a new index.worker.ts, which rexports the cloudflare SDK and uses the init from there.

There is a different way to fix it via vite plugins, but I preferred the more "standard" way here.


Cloudflare Client Events are dropped if withSentry is not used

This showed up while testing the first issue, the tests were timing out. This is because in CF pages, the withSentry wrapper is never used so our sends were never registered with waitUntil, so the flush returned early, cutting off the requests.

I fixed this by making the flush wait for the previous sends until they are drained. Which also fixes the issue for every possible usage of @sentry/cloudflare that isn't wrapped by withSentry.

// _registerWithInvocationWaitUntil
const pending = Promise.resolve(promise).then(() => undefined, () => undefined);
this._pendingEagerDrains.add(pending);
void pending.then(() => this._pendingEagerDrains.delete(pending));

// flush(), after super.flush(timeout)
await Promise.all(this._pendingEagerDrains);

There is no risk of hanging here because each send is already capped by 2 seconds.


The Workers wrapper can shadow the sentry.server.config client

Because of the first fix, we now have an order problem. A withSentry wrapper can override the client that was loaded from sentry.server.config which would break all events, since there is no DSN. The override happens because the wrapper runs first. It's not really an override, more of a "shadowing".

This didn't happen before because the first client loaded from the browser bundle and so they never conflicted.

The fix is to ensure the order loads the "correct" init first, which can be arbitrary. I did the following to de-risk the idea:

  • If no sentry.server.config file exists, then it is never added at the top of the import order. So the wrapper client is the only source.
  • If both sentry.server.config file exists along with a wrapper client, prefer the server file client.
  • The Astro worker init reads the bindings from cloudflare:workers and fills unset options the same way the wrap does, so an init without a dsn still picks up SENTRY_DSN. This only lives in the workerd entry since cloudflare:workers doesn't resolve elsewhere.

I did think about applying (3) to the cloudflare SDK in general but that may break apps that run it under Node, so need to figure it out later.

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Compared: 9da6b7b (PR test merge using develop at 5b78f8e)

Against: 5b78f8e (develop) · base run #65360

✅ Baseline matches: this diff contains only this PR's changes.

Path Size % Change Change
@sentry/browser 29.92 kB - -
@sentry/browser - with treeshaking flags 28.08 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.97 kB - -
@sentry/browser (incl. Tracing) 52.2 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.22 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.18 kB - -
@sentry/browser (incl. Tracing, Replay) 92.52 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 81.2 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 97.25 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 110.25 kB - -
@sentry/browser (incl. Feedback) 47.52 kB - -
@sentry/browser (incl. sendFeedback) 35.02 kB - -
@sentry/browser (incl. FeedbackAsync) 40.13 kB - -
@sentry/browser (incl. Metrics) 31.48 kB - -
@sentry/browser (incl. Logs) 31.77 kB - -
@sentry/browser (incl. Metrics & Logs) 32.45 kB - -
@sentry/react 31.78 kB - -
@sentry/react (incl. Tracing) 54.62 kB - -
@sentry/vue 38.24 kB - -
@sentry/vue (incl. Tracing) 55.24 kB - -
@sentry/svelte 29.95 kB - -
@sentry/remix (Remix 3 client bundle) 57.83 kB - -
CDN Bundle 31.72 kB - -
CDN Bundle (incl. Tracing) 52.81 kB - -
CDN Bundle (incl. Logs, Metrics) 34.52 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 55.32 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 75.9 kB - -
CDN Bundle (incl. Tracing, Replay) 90.98 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 93.54 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 97.11 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 99.71 kB - -
CDN Bundle - uncompressed 92.76 kB - -
CDN Bundle (incl. Tracing) - uncompressed 155.77 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 101.29 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 163.55 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 232.85 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 277.46 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 285.23 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 291.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 298.92 kB - -
@sentry/nextjs (client) 57.64 kB - -
@sentry/sveltekit (client) 52.73 kB - -
@sentry/core/server 44.19 kB - -
@sentry/core/browser 13.93 kB - -
@sentry/node 157.07 kB +0.02% +19 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.28 kB - -
@sentry/node - without tracing 96.92 kB +0.03% +25 B 🔺
@sentry/node - without channel injection 137.36 kB +0.02% +21 B 🔺
@sentry/aws-serverless 105.5 kB +0.02% +16 B 🔺
@sentry/cloudflare (withSentry) - minified 201.71 kB +0.09% +173 B 🔺
@sentry/cloudflare (withSentry) 500.47 kB +0.06% +277 B 🔺
@sentry/nextjs/cloudflare (withSentry) - minified 223.57 kB +0.08% +173 B 🔺

View base workflow run

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Optional E2E tests

Some optional checks were not successful. They do not block the merge. 6 not successful, 51 successful. See the run.

Check Status
❌ CI: Optional E2E Tests / E2E cloudflare-pi-durable (latest) Test (optional) Failing after 44s Details
❌ CI: Optional E2E Tests / E2E nextjs-16-cacheComponents (cloudflare) Test (optional) Failing after 10m Details
❌ CI: Optional E2E Tests / E2E node-eve Test (optional) Failing after 2m Details
❌ CI: Optional E2E Tests / E2E node-eve (orchestrion) Test (optional) Failing after 2m Details
❌ CI: Optional E2E Tests / E2E node-eve (latest) Test (optional) Failing after 55s Details
❌ CI: Optional E2E Tests / E2E node-flue (latest) Test (optional) Failing after 31s Details
Successful checks
Check Status
✅ CI: Optional E2E Tests / E2E astro-7-static Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think Test (optional) Successful in 51s Details
✅ CI: Optional E2E Tests / E2E cloudflare-mastra Test (optional) Successful in 57s Details
✅ CI: Optional E2E Tests / E2E deno (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-flue-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E astro-7 Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries Test (optional) Successful in 57s Details
✅ CI: Optional E2E Tests / E2E cloudflare-agent Test (optional) Successful in 38s Details
✅ CI: Optional E2E Tests / E2E nestjs-12 (latest) Test (optional) Successful in 42s Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (cloudflare) Test (optional) Successful in 43s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (deno) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable Test (optional) Successful in 42s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E debug-id-sourcemaps Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry-static Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-pi-durable Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think (ai v6) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-pi-durable (latest) Test (optional) Successful in 38s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (cloudflare, latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16-tunnel (cloudflare) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (latest) Test (optional) Successful in 44s Details
✅ CI: Optional E2E Tests / E2E cloudflare-workers-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-orpc Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v1 Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext, bun) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (turbo) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-mastra Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-mastra (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v2 Test (optional) Successful in 29s Details
✅ CI: Optional E2E Tests / E2E node-flue Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-cloudflare Test (optional) Successful in 40s Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (latest) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E nuxt-5 (tunnel-dev) Test (optional) Successful in 55s Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps Test (optional) Successful in 37s Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 (tracer provider) Test (optional) Successful in 40s Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (static / nuxt generate) Test (optional) Successful in 39s Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions Test (optional) Successful in 29s Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-sourcemaps Test (optional) Successful in 35s Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E supabase-nextjs Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E vue-3 (no Options API) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-cloudflare Test (optional) Successful in 30s Details
✅ CI: Optional E2E Tests / E2E solidstart-2 Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E react-send-to-sentry Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E nuxt-5 Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions (latest) Test (optional) Successful in 35s Details
✅ CI: Optional E2E Tests / E2E react-router-7-framework-instrumentation Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (client source maps kept) Test (optional) Successful in 38s Details

@logaretm
logaretm marked this pull request as ready for review November 6, 2026 15:52
@logaretm
logaretm requested review from a team as code owners November 6, 2026 15:52
@logaretm
logaretm requested review from andreiborza, chargome, msonnb, mydea and s1gr1d and removed request for a team November 6, 2026 15:52

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

Stale Bugbot comment from a previous run.

Comment thread packages/astro/src/index.worker.ts Resolved
Comment thread packages/astro/package.json Resolved
Comment thread packages/astro/package.json Resolved
@logaretm
logaretm requested a review from JPeer264 November 6, 2026 16:51
@logaretm logaretm changed the title fix(astro): Run the Cloudflare SDK on the server for Cloudflare Pages fix(astro, cloudflare): Run the Cloudflare SDK on the server for Cloudflare Pages Nov 6, 2026

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

Stale Bugbot comment from a previous run.

Comment thread packages/astro/src/index.workerd.ts Resolved
@logaretm
logaretm force-pushed the awad/astro-cf-pages-server-sdk branch from aceecc2 to 398034e Compare November 6, 2026 17:26
@logaretm
logaretm requested a review from isaacs November 6, 2026 17:50
`@sentry/astro` had no `workerd` export, so a Pages build resolved it to the
browser SDK and the server config initialized a `BrowserClient`. A new worker
entry backs the server SDK with `@sentry/cloudflare`.

Outside a `withSentry` invocation, `CloudflareClient` sends envelopes eagerly
without a `waitUntil`, and `flush()` returned before those sends finished, so
the runtime cancelled them when the response ended. `flush()` now waits for
them.
…` wrap

The Worker entry's `withSentry` wrap inits a client from the Wrangler bindings on each request, and the
isolate keeps the first client. The user's `sentry.server.config` only loaded later from SSR pages (never
for API routes), so their `Sentry.init` got the bindings-only client back and their options were ignored.
The entry now imports the server init above the wrap.
The new `workerd` entry re-exports `handleRequest`, whose middleware imported core helpers through
`@sentry/node`, pulling the Node SDK into Cloudflare bundles. The bundler entry test now covers
`@sentry/astro` under `workerd` and `worker`, with known violations marked per case so only the
Node entry stays exempt.
Without a `sentry.server.config`, the integration's default init has no DSN. Running it before the
`withSentry` wrap made the isolate keep that client, so the wrap's `SENTRY_DSN` binding was ignored
and nothing was sent. The entry now imports the server init only when the user has a config file.
A `Sentry.init` without a `dsn` relied on the `withSentry` wrap filling it from the bindings. Now that
the user's init runs first and the isolate keeps that client, it ended up without a DSN. A new `workerd`
entry reads the bindings from `cloudflare:workers`, and `init` merges them the same way the wrap does.
The generic `worker` condition keeps the entry without that import, since only Cloudflare's bundlers
resolve it.
@logaretm
logaretm force-pushed the awad/astro-cf-pages-server-sdk branch from 398034e to 0777aa5 Compare November 6, 2026 22:46

@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 0777aa5. Configure here.

: [
sentryCloudflareNodeWarningPlugin(),
sentryCloudflareVitePlugin({ importServerInit: !!pathToServerInit }),
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pages wrap can shadow server init

High Severity

The Workers path now passes importServerInit so sentry.server.config runs before withSentry caches a client. The Pages branch still calls sentryCloudflareVitePlugin() without that flag, even though it applies the same wrap. withSentry inits at the start of fetch, so a lazily loaded middleware init can lose to a bindings-only client and drop the user's dsn, tunnel, and environment.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0777aa5. Configure here.

This branch has not been deployed

No deployments
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.

1 participant