diff --git a/packages/cloudflare/src/vite/autoInstrument.ts b/packages/cloudflare/src/vite/autoInstrument.ts index 6c7e8f9734fc..4ac366f3fb38 100644 --- a/packages/cloudflare/src/vite/autoInstrument.ts +++ b/packages/cloudflare/src/vite/autoInstrument.ts @@ -15,17 +15,27 @@ function normalizePath(path: string): string { // `.html`, … — sharing the entry's basename must never be treated as the entry. const JS_EXTENSION_REGEX = /\.[cm]?[jt]sx?$/; +// Re-exports everything the generated code uses, and adds the Next.js span handling and OpenTelemetry context. +const NEXTJS_CLOUDFLARE_MODULE = '@sentry/nextjs/cloudflare'; + export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPath?: string } = {}) { let wranglerConfig: WranglerConfig | undefined; let entryFilePath: string | undefined; let optionsFn = ENV_FALLBACK_OPTIONS_FN; let optionsImport: string | undefined; + let isVinext = false; return { name: 'sentry-cloudflare-auto-instrument', - configResolved(config: { root: string; logger?: { warn(msg: string): void } }): void { + configResolved(config: { + root: string; + plugins?: readonly { name?: string }[]; + logger?: { warn(msg: string): void }; + }): void { + isVinext = config.plugins?.some(plugin => plugin.name?.startsWith('vinext:')) ?? false; + const result = resolveWranglerConfig(config.root, options.wranglerConfigPath); if (!result) { // An explicit path that fails is a misconfiguration worth naming; @@ -119,6 +129,12 @@ export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPa }) : undefined; + // A vinext app with `@sentry/nextjs` installed gets `withSentry` of `@sentry/nextjs/cloudflare`. + const sentryModule = + isVinext && (await this.resolve?.(NEXTJS_CLOUDFLARE_MODULE, normalizedId)) + ? NEXTJS_CLOUDFLARE_MODULE + : undefined; + // No registration import is injected here: the orchestrion plugin's // subscribe-injection makes each bundled package self-register its channel // subscriber on the global marker, so wrapping the entry with `withSentry` @@ -129,6 +145,7 @@ export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPa optionsFn, optionsImport, sameWorkerBindings: wranglerConfig.sameWorkerBindings, + sentryModule, }); const wrappedClasses = result?.wrappedClasses ?? new Set(); diff --git a/packages/cloudflare/src/vite/transform.ts b/packages/cloudflare/src/vite/transform.ts index 1fdf647831c1..3674ed5dd032 100644 --- a/packages/cloudflare/src/vite/transform.ts +++ b/packages/cloudflare/src/vite/transform.ts @@ -114,6 +114,8 @@ export interface TransformContext { optionsFn: string; /** Import statement prepended when `optionsFn` references a separate module. */ optionsImport?: string; + /** Module the generated code imports `withSentry` and the class wrappers from. Defaults to `@sentry/cloudflare`. */ + sentryModule?: string; /** @see {@link import('./wranglerConfig').WranglerConfig.sameWorkerBindings} */ sameWorkerBindings?: readonly SameWorkerBinding[]; } @@ -198,7 +200,7 @@ export function applyAutoInstrumentTransforms( ms.prepend(buildMergedOptionsDeclaration(sameWorkerBindings, ctx.optionsFn, state)); } if (ctx.optionsImport) ms.prepend(ctx.optionsImport); - ms.prepend("import * as __SENTRY__ from '@sentry/cloudflare';\n"); + ms.prepend(`import * as __SENTRY__ from '${ctx.sentryModule ?? '@sentry/cloudflare'}';\n`); return { code: ms.toString(), diff --git a/packages/cloudflare/test/vite/autoInstrument.test.ts b/packages/cloudflare/test/vite/autoInstrument.test.ts index 7303e667ee6c..98d07b24c1cc 100644 --- a/packages/cloudflare/test/vite/autoInstrument.test.ts +++ b/packages/cloudflare/test/vite/autoInstrument.test.ts @@ -55,6 +55,58 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => { expect(result.code).toContain('__SENTRY__.withSentry('); }); + it('wraps the entry with `@sentry/nextjs/cloudflare` in a vinext build', async () => { + const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' }); + const plugin = sentryCloudflareAutoInstrumentPlugin(); + plugin.configResolved({ root: dir, plugins: [{ name: 'vinext:config' }] }); + + const code = 'export default { fetch() { return new Response("ok"); } };'; + const result = await plugin.transform.call( + { + parse: (c: string) => parseJS(c), + resolve: async (source: string) => (source === '@sentry/nextjs/cloudflare' ? { id: source } : null), + }, + code, + join(dir, 'src/index.ts'), + ); + + expect(result.code).toContain("import * as __SENTRY__ from '@sentry/nextjs/cloudflare';"); + expect(result.code).toContain('__SENTRY__.withSentry('); + }); + + it('wraps the entry with `@sentry/cloudflare` when `@sentry/nextjs/cloudflare` resolves outside a vinext build', async () => { + const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' }); + const plugin = sentryCloudflareAutoInstrumentPlugin(); + plugin.configResolved({ root: dir, plugins: [{ name: 'vite:react' }] }); + + const code = 'export default { fetch() { return new Response("ok"); } };'; + const result = await plugin.transform.call( + { + parse: (c: string) => parseJS(c), + resolve: async (source: string) => (source === '@sentry/nextjs/cloudflare' ? { id: source } : null), + }, + code, + join(dir, 'src/index.ts'), + ); + + expect(result.code).toContain("import * as __SENTRY__ from '@sentry/cloudflare';"); + }); + + it('wraps the entry with `@sentry/cloudflare` in a vinext build without `@sentry/nextjs`', async () => { + const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' }); + const plugin = sentryCloudflareAutoInstrumentPlugin(); + plugin.configResolved({ root: dir, plugins: [{ name: 'vinext:config' }] }); + + const code = 'export default { fetch() { return new Response("ok"); } };'; + const result = await plugin.transform.call( + { parse: (c: string) => parseJS(c), resolve: async () => null }, + code, + join(dir, 'src/index.ts'), + ); + + expect(result.code).toContain("import * as __SENTRY__ from '@sentry/cloudflare';"); + }); + it('leaves an already-manually-wrapped entry untouched', async () => { const { transform: tx, entryPath } = createPlugin('main = "src/index.ts"');