diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.gitignore b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.gitignore
new file mode 100644
index 000000000000..560782d47d98
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.gitignore
@@ -0,0 +1,26 @@
+# build output
+dist/
+
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
+
+test-results
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.npmrc b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/astro.config.mjs b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/astro.config.mjs
new file mode 100644
index 000000000000..4de1fcb44fc6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/astro.config.mjs
@@ -0,0 +1,18 @@
+import cloudflare from '@astrojs/cloudflare';
+import sentry from '@sentry/astro';
+// @ts-check
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [
+ sentry({
+ debug: true,
+ sourceMapsUploadOptions: {
+ enabled: false,
+ },
+ }),
+ ],
+ output: 'server',
+ adapter: cloudflare(),
+});
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json
new file mode 100644
index 000000000000..b74b36c9d314
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "astro-5-cf-workers",
+ "type": "module",
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "wrangler dev --port 3030",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "TEST_ENV=production playwright test"
+ },
+ "dependencies": {
+ "@astrojs/cloudflare": "^12.6.12",
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/astro": "latest || *",
+ "@sentry/cloudflare": "latest || *",
+ "astro": "^5.17.1"
+ },
+ "devDependencies": {
+ "wrangler": "^4.63.0"
+ },
+ "pnpm": {
+ "overrides": {
+ "esbuild": "0.24.0"
+ }
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/playwright.config.mjs
new file mode 100644
index 000000000000..3cdf5850b613
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/playwright.config.mjs
@@ -0,0 +1,14 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const config = getPlaywrightConfig({
+ startCommand: 'pnpm preview',
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js
new file mode 100644
index 000000000000..2b79ec0ed337
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js
@@ -0,0 +1,8 @@
+import * as Sentry from '@sentry/astro';
+
+Sentry.init({
+ dsn: import.meta.env.PUBLIC_E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/', // proxy server
+});
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js
new file mode 100644
index 000000000000..2b79ec0ed337
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js
@@ -0,0 +1,8 @@
+import * as Sentry from '@sentry/astro';
+
+Sentry.init({
+ dsn: import.meta.env.PUBLIC_E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/', // proxy server
+});
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/actions/index.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/actions/index.ts
new file mode 100644
index 000000000000..47e5386981fc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/actions/index.ts
@@ -0,0 +1,24 @@
+import { defineAction, ActionError } from 'astro:actions';
+import { z } from 'astro:schema';
+
+export const server = {
+ testAction: defineAction({
+ input: z.object({
+ name: z.string(),
+ shouldError: z.boolean().optional(),
+ }),
+ handler: async input => {
+ if (input.shouldError) {
+ throw new ActionError({
+ code: 'BAD_REQUEST',
+ message: 'Test Action Error',
+ });
+ }
+
+ return {
+ status: 'success',
+ name: input.name,
+ };
+ },
+ }),
+};
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/layouts/Layout.astro b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/layouts/Layout.astro
new file mode 100644
index 000000000000..6105f48ffd35
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/layouts/Layout.astro
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+ Astro Basics
+
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/action-test/index.astro b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/action-test/index.astro
new file mode 100644
index 000000000000..ba2f876a78da
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/action-test/index.astro
@@ -0,0 +1,31 @@
+---
+import Layout from '../../layouts/Layout.astro';
+
+export const prerender = false;
+---
+
+
+ Action Test Page
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/api/test-error.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/api/test-error.ts
new file mode 100644
index 000000000000..24ac1b4d39ec
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/api/test-error.ts
@@ -0,0 +1,7 @@
+import type { APIRoute } from 'astro';
+
+export const prerender = false;
+
+export const GET: APIRoute = () => {
+ throw new Error('This is a test error from an API route');
+};
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/endpoint-error/api.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/endpoint-error/api.ts
new file mode 100644
index 000000000000..a76accdba010
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/endpoint-error/api.ts
@@ -0,0 +1,15 @@
+import type { APIRoute } from 'astro';
+
+export const prerender = false;
+
+export const GET: APIRoute = ({ request, url }) => {
+ if (url.searchParams.has('error')) {
+ throw new Error('Endpoint Error');
+ }
+ return new Response(
+ JSON.stringify({
+ search: url.search,
+ sp: url.searchParams,
+ }),
+ );
+};
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/endpoint-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/endpoint-error/index.astro
new file mode 100644
index 000000000000..ecfb0641144e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/endpoint-error/index.astro
@@ -0,0 +1,9 @@
+---
+import Layout from '../../layouts/Layout.astro';
+
+export const prerender = false;
+---
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/index.astro b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/index.astro
new file mode 100644
index 000000000000..90a5b300a178
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/index.astro
@@ -0,0 +1,15 @@
+---
+import Layout from '../layouts/Layout.astro';
+---
+
+
+
+ Astro CF Workers E2E Test App
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/ssr-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/ssr-error/index.astro
new file mode 100644
index 000000000000..fc42bcbae4f7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/src/pages/ssr-error/index.astro
@@ -0,0 +1,11 @@
+---
+import Layout from '../../layouts/Layout.astro';
+
+const a = {} as any;
+console.log(a.foo.x);
+export const prerender = false;
+---
+
+
+ Page with SSR error
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/start-event-proxy.mjs
new file mode 100644
index 000000000000..335219f1f1d4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'astro-5-cf-workers',
+});
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts
new file mode 100644
index 000000000000..2a964941217a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts
@@ -0,0 +1,43 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test.describe('Astro actions', () => {
+ test('captures transaction for action call', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => {
+ return transactionEvent.transaction === 'GET /action-test';
+ });
+
+ await page.goto('/action-test');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toMatchObject({
+ transaction: 'GET /action-test',
+ });
+
+ const traceId = transactionEvent.contexts?.trace?.trace_id;
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+ });
+
+ test('action submission creates a transaction', async ({ page }) => {
+ await page.goto('/action-test');
+
+ const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => {
+ return (
+ transactionEvent.transaction?.includes('action-test') && transactionEvent.transaction !== 'GET /action-test'
+ );
+ });
+
+ await page.getByText('Submit Action').click();
+
+ // Wait for the result to appear on the page
+ await page.waitForSelector('#result:not(:empty)');
+
+ const resultText = await page.locator('#result').textContent();
+ expect(resultText).toContain('success');
+
+ const transactionEvent = await transactionEventPromise;
+ expect(transactionEvent).toBeDefined();
+ expect(transactionEvent.contexts?.trace?.trace_id).toMatch(/[a-f0-9]{32}/);
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/cloudflare-runtime.test.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/cloudflare-runtime.test.ts
new file mode 100644
index 000000000000..516b97b4988d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/cloudflare-runtime.test.ts
@@ -0,0 +1,55 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test.describe('Cloudflare Runtime', () => {
+ test('Should report cloudflare as the runtime in SSR error events', async ({ page }) => {
+ const errorEventPromise = waitForError('astro-5-cf-workers', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')";
+ });
+
+ await page.goto('/ssr-error').catch(() => {
+ // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE
+ });
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.contexts?.runtime).toEqual({
+ name: 'cloudflare',
+ });
+
+ // The SDK info should include cloudflare in the packages
+ expect(errorEvent.sdk?.packages).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'npm:@sentry/cloudflare',
+ }),
+ ]),
+ );
+ });
+
+ test('Should report cloudflare as the runtime in API route error events', async ({ request }) => {
+ const errorEventPromise = waitForError('astro-5-cf-workers', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value =>
+ value.value?.includes('This is a test error from an API route'),
+ );
+ });
+
+ request.get('/api/test-error').catch(() => {
+ // Expected to fail
+ });
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.contexts?.runtime).toEqual({
+ name: 'cloudflare',
+ });
+
+ expect(errorEvent.sdk?.packages).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'npm:@sentry/cloudflare',
+ }),
+ ]),
+ );
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts
new file mode 100644
index 000000000000..df23d740e830
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts
@@ -0,0 +1,158 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test.describe('server-side errors', () => {
+ test('captures SSR error', async ({ page }) => {
+ const errorEventPromise = waitForError('astro-5-cf-workers', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')";
+ });
+
+ const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => {
+ return transactionEvent.transaction === 'GET /ssr-error';
+ });
+
+ // This page returns an error status code, so we need to catch the navigation error
+ await page.goto('/ssr-error').catch(() => {
+ // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE in newer Chromium versions
+ });
+
+ const errorEvent = await errorEventPromise;
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toMatchObject({
+ transaction: 'GET /ssr-error',
+ });
+
+ const traceId = transactionEvent.contexts?.trace?.trace_id;
+ const spanId = transactionEvent.contexts?.trace?.span_id;
+
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+ expect(spanId).toMatch(/[a-f0-9]{16}/);
+
+ expect(errorEvent).toMatchObject({
+ contexts: {
+ trace: {
+ span_id: spanId,
+ trace_id: traceId,
+ },
+ },
+ environment: 'qa',
+ event_id: expect.stringMatching(/[a-f0-9]{32}/),
+ exception: {
+ values: [
+ {
+ mechanism: expect.objectContaining({
+ handled: false,
+ }),
+ stacktrace: expect.any(Object),
+ type: 'TypeError',
+ value: "Cannot read properties of undefined (reading 'x')",
+ },
+ ],
+ },
+ request: {
+ headers: expect.objectContaining({
+ host: expect.any(String),
+ 'user-agent': expect.any(String),
+ }),
+ method: 'GET',
+ url: expect.stringContaining('/ssr-error'),
+ },
+ sdk: {
+ integrations: expect.any(Array),
+ name: 'sentry.javascript.cloudflare',
+ packages: expect.any(Array),
+ version: expect.any(String),
+ },
+ timestamp: expect.any(Number),
+ transaction: 'GET /ssr-error',
+ });
+ });
+
+ test('captures endpoint error', async ({ page }) => {
+ const errorEventPromise = waitForError('astro-5-cf-workers', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error';
+ });
+ const transactionEventApiPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => {
+ return transactionEvent.transaction === 'GET /endpoint-error/api';
+ });
+ const transactionEventEndpointPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => {
+ return transactionEvent.transaction === 'GET /endpoint-error';
+ });
+
+ await page.goto('/endpoint-error');
+ await page.getByText('Get Data').click();
+
+ const errorEvent = await errorEventPromise;
+ const transactionEventApi = await transactionEventApiPromise;
+ const transactionEventEndpoint = await transactionEventEndpointPromise;
+
+ expect(transactionEventEndpoint).toMatchObject({
+ transaction: 'GET /endpoint-error',
+ });
+
+ const traceId = transactionEventEndpoint.contexts?.trace?.trace_id;
+
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+
+ expect(transactionEventApi).toMatchObject({
+ transaction: 'GET /endpoint-error/api',
+ });
+
+ expect(errorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ mechanism: expect.objectContaining({
+ handled: false,
+ }),
+ stacktrace: expect.any(Object),
+ type: 'Error',
+ value: 'Endpoint Error',
+ },
+ ],
+ },
+ request: {
+ headers: expect.objectContaining({
+ accept: expect.any(String),
+ }),
+ method: 'GET',
+ url: expect.stringContaining('endpoint-error/api?error=1'),
+ },
+ transaction: 'GET /endpoint-error/api',
+ });
+ });
+
+ test('captures API route error', async ({ request }) => {
+ const errorEventPromise = waitForError('astro-5-cf-workers', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value =>
+ value.value?.includes('This is a test error from an API route'),
+ );
+ });
+
+ request.get('/api/test-error').catch(() => {
+ // Expected to fail
+ });
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ mechanism: expect.objectContaining({
+ handled: false,
+ }),
+ stacktrace: expect.any(Object),
+ type: 'Error',
+ value: 'This is a test error from an API route',
+ },
+ ],
+ },
+ request: {
+ method: 'GET',
+ url: expect.stringContaining('/api/test-error'),
+ },
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tsconfig.json b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tsconfig.json
new file mode 100644
index 000000000000..8bf91d3bb997
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc
new file mode 100644
index 000000000000..5ef4f1ff11f6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc
@@ -0,0 +1,18 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "astro-5-cf-workers",
+ "main": "dist/_worker.js/index.js",
+ "compatibility_date": "2025-12-01",
+ "compatibility_flags": ["nodejs_compat"],
+ "vars": {
+ "SENTRY_DSN": "https://username@domain/123",
+ "SENTRY_ENVIRONMENT": "qa",
+ "SENTRY_TRACES_SAMPLE_RATE": "1.0",
+ "SENTRY_TUNNEL": "http://localhost:3031/"
+ },
+ "assets": {
+ "binding": "ASSETS",
+ "directory": "./dist"
+ }
+}
+
diff --git a/packages/astro/src/integration/cloudflare.ts b/packages/astro/src/integration/cloudflare.ts
new file mode 100644
index 000000000000..c07f93ed16dc
--- /dev/null
+++ b/packages/astro/src/integration/cloudflare.ts
@@ -0,0 +1,80 @@
+import { builtinModules } from 'module';
+import type { Plugin } from 'vite';
+
+// Build a set of all Node.js built-in module names, including both
+// bare names (e.g. "fs") and "node:" prefixed names (e.g. "node:fs").
+const NODE_BUILTINS = new Set(builtinModules.flatMap(m => [m, `node:${m}`]));
+
+/**
+ * A Vite plugin that suppresses the "Automatically externalized node built-in module"
+ * warnings that Vite emits when bundling for Cloudflare Workers.
+ *
+ * These warnings are expected because `@sentry/astro` re-exports `@sentry/node` on the
+ * server side, and `@sentry/node` (plus OpenTelemetry) import many Node.js built-in
+ * modules. Vite correctly externalizes them, but warns about it. These warnings are
+ * harmless since Cloudflare Workers support Node.js built-ins under the `node:` prefix.
+ */
+export function sentryCloudflareNodeWarningPlugin(): Plugin {
+ return {
+ name: 'sentry-astro-cloudflare-suppress-node-warnings',
+ enforce: 'pre',
+
+ config() {
+ return {
+ ssr: {
+ // Explicitly mark all Node.js built-in modules as external.
+ // This prevents Vite from emitting "Automatically externalized" warnings
+ // for each one during the SSR/Worker build.
+ external: [...NODE_BUILTINS],
+ },
+ };
+ },
+ };
+}
+
+/**
+ * A Vite plugin that ensures the Sentry server config is loaded at the
+ * top level of the Cloudflare Worker entry module, rather than only being
+ * injected into SSR page modules via `injectScript('page-ssr', ...)`.
+ *
+ * Without this, Astro actions and API routes never call `Sentry.init()`,
+ * because `injectScript('page-ssr')` only adds the import to page components.
+ *
+ * Additionally, this plugin wraps the Worker's default export handler with
+ * `@sentry/cloudflare`'s `withSentry` to provide:
+ * - `setAsyncLocalStorageAsyncContextStrategy()` for proper async context
+ * - Per-request isolation scopes via `wrapRequestHandler`
+ * - Trace context propagation
+ */
+export function sentryCloudflareVitePlugin(): Plugin {
+ return {
+ name: 'sentry-astro-cloudflare',
+ enforce: 'post',
+
+ transform(code, id) {
+ // Match the Astro SSR virtual entry — this becomes dist/_worker.js/index.js
+ // The resolved virtual module ID is `\0@astrojs-ssr-virtual-entry`
+ if (!id.includes('astrojs-ssr-virtual-entry')) {
+ return undefined;
+ }
+
+ // In @astrojs/cloudflare v12, the virtual entry module structure is:
+ // https://github.com/withastro/astro/blob/09bbdbb1e62c388eb405eeea03554c15e01f2957/packages/integrations/cloudflare/src/entrypoints/server.ts#L23
+ // We need to wrap `default` with `withSentry` before it's exported.
+ const defaultExportMatch = code.match(/export\s+default\s+([\w.]+)\s*;/);
+
+ if (!defaultExportMatch) {
+ return undefined;
+ }
+
+ const originalExpr = defaultExportMatch[1];
+ const wrappedExport = `export default withSentry(() => undefined, ${originalExpr});`;
+ const transformedCode = [
+ "import { withSentry } from '@sentry/cloudflare';",
+ code.replace(defaultExportMatch[0], wrappedExport),
+ ].join('\n');
+
+ return { code: transformedCode, map: null };
+ },
+ };
+}
diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts
index a96685ce8033..796d6f84a12b 100644
--- a/packages/astro/src/integration/index.ts
+++ b/packages/astro/src/integration/index.ts
@@ -1,7 +1,9 @@
import { sentryVitePlugin } from '@sentry/vite-plugin';
import type { AstroConfig, AstroIntegration, AstroIntegrationLogger } from 'astro';
import * as fs from 'fs';
+import { createRequire } from 'module';
import * as path from 'path';
+import { sentryCloudflareNodeWarningPlugin, sentryCloudflareVitePlugin } from './cloudflare';
import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets';
import type { SentryOptions } from './types';
@@ -160,20 +162,57 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
}
}
+ const isCloudflare = config?.adapter?.name?.startsWith('@astrojs/cloudflare');
+
+ if (isCloudflare) {
+ try {
+ const _require = createRequire(`${process.cwd()}/`);
+ _require.resolve('@sentry/cloudflare');
+ } catch {
+ logger.error(
+ 'You are using the Cloudflare adapter but `@sentry/cloudflare` is not installed. ' +
+ 'Please install the `@sentry/cloudflare` package in your project.',
+ );
+ process.exit(1);
+ }
+ }
+
if (sdkEnabled.server) {
const pathToServerInit = serverInitPath ? path.resolve(serverInitPath) : findDefaultSdkInitFile('server');
+
if (pathToServerInit) {
debug && logger.info(`Using ${pathToServerInit} for server init.`);
+ // Always inject the server config via `injectScript('page-ssr')`.
+ // This ensures Sentry.init() runs in dev mode (where the Vite plugin doesn't fire)
+ // and also serves as the fallback for non-Cloudflare adapters in production.
injectScript('page-ssr', buildSdkInitFileImportSnippet(pathToServerInit));
} else {
debug && logger.info('Using default server init.');
injectScript('page-ssr', buildServerSnippet(options || {}));
}
- // Prevent Sentry from being externalized for SSR.
- // Cloudflare like environments have Node.js APIs are available under `node:` prefix.
- // Ref: https://developers.cloudflare.com/workers/runtime-apis/nodejs/
- if (config?.adapter?.name.startsWith('@astrojs/cloudflare')) {
+ if (isCloudflare && command !== 'dev') {
+ // For Cloudflare production builds, additionally use a Vite plugin to:
+ // 1. Import the server config at the Worker entry level (so Sentry.init() runs
+ // for ALL requests, not just SSR pages — covers actions and API routes)
+ // 2. Wrap the default export with `withSentry` from @sentry/cloudflare for
+ // per-request isolation, async context, and trace propagation
+ //
+ // Note: We do NOT set `ssr.noExternal` here. The `@astrojs/cloudflare` adapter
+ // already configures Vite to bundle all dependencies for Workers. Explicitly
+ // adding `@sentry/node` to `noExternal` would cause Vite to emit dozens of
+ // warnings about auto-externalizing Node.js built-in modules that @sentry/node
+ // and its transitive dependencies (OpenTelemetry, etc.) import.
+ debug && logger.info('Adding Cloudflare Vite plugin to wrap Worker entry with withSentry.');
+ updateConfig({
+ vite: {
+ plugins: [sentryCloudflareNodeWarningPlugin(), sentryCloudflareVitePlugin()],
+ },
+ });
+ } else if (isCloudflare) {
+ // Prevent Sentry from being externalized for SSR.
+ // Cloudflare environments have Node.js APIs available under `node:` prefix.
+ // Ref: https://developers.cloudflare.com/workers/runtime-apis/nodejs/
updateConfig({
vite: {
ssr: {
@@ -187,7 +226,9 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
}
}
- const isSSR = config && (config.output === 'server' || config.output === 'hybrid');
+ // In Astro 5+, `config.output` is no longer explicitly set — having an adapter
+ // implies SSR capability. We check for the adapter to handle this correctly.
+ const isSSR = config && (config.output === 'server' || config.output === 'hybrid' || !!config.adapter);
const shouldAddMiddleware = sdkEnabled.server && autoInstrumentation?.requestHandler !== false;
// Guarding calling the addMiddleware function because it was only introduced in astro@3.5.0