Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import sentry from '@sentry/astro';
// @ts-check
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';

// https://astro.build/config
export default defineConfig({
Expand All @@ -18,4 +19,7 @@ export default defineConfig({
allowedDomains: [{ hostname: 'localhost' }],
},
adapter: cloudflare(),
vite: {
plugins: [sentryCloudflareVitePlugin()],
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineCloudflareOptions } from '@sentry/cloudflare';

export default defineCloudflareOptions(env => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa',
tracesSampleRate: 1.0,
tunnel: 'http://localhost:3031/', // proxy server
debug: true,
}));

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface __BaseEnv_Env {
}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import('./sentry.server.config');
mainModule: typeof import('@astrojs/cloudflare/entrypoints/server');
}
interface Env extends __BaseEnv_Env {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compatibility_date": "2026-03-12",
"compatibility_flags": ["global_fetch_strictly_public", "nodejs_compat"],
"name": "astro-6-cf-workers",
"main": "./sentry.server.config.js",
"main": "@astrojs/cloudflare/entrypoints/server",
"assets": {
"directory": "./dist",
"binding": "ASSETS",
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

10 changes: 6 additions & 4 deletions packages/astro/src/integration/cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export function sentryCloudflareNodeWarningPlugin(): VitePlugin {
};
}

// The Worker entry module: `@astrojs/cloudflare` v12 builds its own virtual entry, and v13+ builds
// through `@cloudflare/vite-plugin`, whose entry ends in `export default mod.default ?? {};`.
const WORKER_ENTRY_IDS = ['astrojs-ssr-virtual-entry', 'virtual:cloudflare/worker-entry'];

/**
* 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
Expand All @@ -57,16 +61,14 @@ export function sentryCloudflareVitePlugin(): VitePlugin {
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')) {
if (!WORKER_ENTRY_IDS.some(entryId => id.includes(entryId))) {
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*;/);
const defaultExportMatch = code.match(/export\s+default\s+([^;]+?)\s*;/);

if (!defaultExportMatch) {
return undefined;
Expand Down
38 changes: 35 additions & 3 deletions packages/astro/src/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,18 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {

const isCloudflare = config?.adapter?.name?.startsWith('@astrojs/cloudflare');
const isCloudflareWorkers = isCloudflare && !isCloudflarePages();
// That plugin adds its own orchestrion transform and wraps the Worker entry, which inits the
// SDK from `instrument.server.*` or `env`. With its `autoInstrumentation: false`, the user's
// own `withSentry` does the init. Either way, none of ours are needed.
const usesCloudflareVitePlugin = isCloudflareWorkers && hasCloudflareVitePlugin(config);

// Wire up the orchestrion code transform so instrumented server-side dependencies (e.g.
// `mysql`, `ioredis`) get `diagnostics_channel` publishers injected into the SSR bundle at
// build time, with no manual plugin setup. The plugin opts out internally when
// `buildTimeInstrumentation` is `false`. Cloudflare Pages is skipped: it gets no
// `withSentry` wrap, so nothing would read the marker the injected snippets write, and
// keeping the transform off avoids bundling dead subscriber code.
if (sdkEnabled.server && (!isCloudflare || isCloudflareWorkers)) {
if (sdkEnabled.server && (!isCloudflare || isCloudflareWorkers) && !usesCloudflareVitePlugin) {
updateConfig({
vite: {
plugins: [sentryOrchestrionPlugin({ buildTimeInstrumentation }) as VitePlugin],
Expand Down Expand Up @@ -179,7 +183,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
// 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 {
} else if (!usesCloudflareVitePlugin) {
debug && logger.info('Using default server init.');
injectScript('page-ssr', buildServerSnippet(options || {}));
}
Expand All @@ -199,7 +203,9 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
debug && logger.info('Adding Cloudflare Vite plugin to wrap Worker entry with withSentry.');
updateConfig({
vite: {
plugins: [sentryCloudflareNodeWarningPlugin(), sentryCloudflareVitePlugin()],
plugins: usesCloudflareVitePlugin
? [sentryCloudflareNodeWarningPlugin()]
: [sentryCloudflareNodeWarningPlugin(), sentryCloudflareVitePlugin()],
},
});
} else if (isCloudflare) {
Expand Down Expand Up @@ -236,6 +242,32 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
};
};

// Every sub-plugin of `sentryCloudflareVitePlugin()` but its orchestrion transform has this prefix,
// and some are added whatever its options are.
const CLOUDFLARE_VITE_PLUGIN_NAME_PREFIX = 'sentry-cloudflare-';

/**
* Whether the user added `sentryCloudflareVitePlugin()` from `@sentry/cloudflare/vite`. It is
* detected by name because importing it here would load `@sentry/cloudflare` and `wrangler` for
* every Astro app.
*/
function hasCloudflareVitePlugin(config: AstroConfig): boolean {
const pending: unknown[] = [config.vite?.plugins];
while (pending.length > 0) {
const plugin = pending.pop();
if (Array.isArray(plugin)) {
pending.push(...plugin);
} else if (
typeof plugin === 'object' &&
plugin !== null &&
String((plugin as { name?: unknown }).name).startsWith(CLOUDFLARE_VITE_PLUGIN_NAME_PREFIX)
) {
return true;
}
}
return false;
}
Comment thread
cursor[bot] marked this conversation as resolved.

const possibleFileExtensions = ['ts', 'js', 'tsx', 'jsx', 'mjs', 'cjs', 'mts'];

function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined {
Expand Down
48 changes: 48 additions & 0 deletions packages/astro/test/integration/cloudflareVitePlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { sentryCloudflareVitePlugin } from '../../src/integration/cloudflare';

function transform(code: string, id: string) {
const plugin = sentryCloudflareVitePlugin() as { transform: (code: string, id: string) => unknown };
return plugin.transform(code, id);
}

describe('sentryCloudflareVitePlugin', () => {
it('wraps the default export of the @astrojs/cloudflare v12 virtual entry', () => {
const code = ['const _exports = createExports(manifest);', 'export default _exports.default;'].join('\n');

expect(transform(code, '\0@astrojs-ssr-virtual-entry')).toEqual({
code: [
"import { withSentry } from '@sentry/cloudflare';",
'const _exports = createExports(manifest);',
'export default withSentry(() => undefined, _exports.default);',
].join('\n'),
map: null,
});
});

it('wraps the default export of the @cloudflare/vite-plugin worker entry', () => {
const code = [
'import { getExportTypes } from "virtual:cloudflare/export-types";',
'import * as mod from "virtual:cloudflare/user-entry";',
'export * from "virtual:cloudflare/user-entry";',
'export default mod.default ?? {};',
'if (import.meta.hot) { import.meta.hot.accept(); }',
].join('\n');

expect(transform(code, '\0virtual:cloudflare/worker-entry')).toEqual({
code: [
"import { withSentry } from '@sentry/cloudflare';",
'import { getExportTypes } from "virtual:cloudflare/export-types";',
'import * as mod from "virtual:cloudflare/user-entry";',
'export * from "virtual:cloudflare/user-entry";',
'export default withSentry(() => undefined, mod.default ?? {});',
'if (import.meta.hot) { import.meta.hot.accept(); }',
].join('\n'),
map: null,
});
});

it('leaves other modules alone', () => {
expect(transform('export default handler;', '/src/pages/index.astro')).toBeUndefined();
});
});
Loading
Loading