From 918617573c9157f9a42c00c3a6b44210933fa545 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:29:38 +0000 Subject: [PATCH] fix(v10/cloudflare): auto-instrument a re-exported default export Backport of: #25345 --- packages/cloudflare/src/vite/transform.ts | 29 ++++++++++- .../cloudflare/test/vite/transform.test.ts | 50 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/cloudflare/src/vite/transform.ts b/packages/cloudflare/src/vite/transform.ts index 915955ad51f8..701d2a37a3fb 100644 --- a/packages/cloudflare/src/vite/transform.ts +++ b/packages/cloudflare/src/vite/transform.ts @@ -482,6 +482,28 @@ function wrapSpecifierExports(node: ExportNamedNode, ctx: TransformContext, stat state.ms.overwrite(node.start, node.end, statements.join('\n')); } +/** + * `export { default } from './handler'`. The handler may already be wrapped in its own module, + * which this transform cannot see, so the emitted guard returns such a handler as-is. + */ +function wrapDefaultReexport( + localName: string, + sourceLiteral: string, + state: TransformState, + prelude: string[], +): string { + const target = '__SENTRY_REEXPORT_default__'; + prelude.push( + localName === DEFAULT_IMPORT + ? `import ${target} from ${sourceLiteral};` + : `import { ${localName} as ${target} } from ${sourceLiteral};`, + `const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, ${state.optionsFn}, ${target});`, + ); + state.needsImport = true; + state.autoWrapped.add(DEFAULT_EXPORT); + return '__SENTRY_DEFAULT_EXPORT__ as default'; +} + /** * Handle one export specifier, returning the `Wrapped as Exported` pair to emit when its class has * to be wrapped through a fresh binding — the cross-module case. The import/wrapper statements that @@ -508,7 +530,12 @@ function wrapCrossModuleSpecifier( ? resolveWrapperKind(exportedName, sourceLiteral ? undefined : localName, state) : undefined; - if (!exportedName || !localName || !kind) return undefined; + if (!exportedName || !localName) return undefined; + if (!kind) { + return exportedName === DEFAULT_IMPORT && sourceLiteral + ? wrapDefaultReexport(localName, sourceLiteral, state, prelude) + : undefined; + } // Without a `from` clause the specifier points at a module-local binding, which may already be // (or become) the wrapped class without touching the export statement itself. diff --git a/packages/cloudflare/test/vite/transform.test.ts b/packages/cloudflare/test/vite/transform.test.ts index 1ab71b9b0ba7..dccb97c70daf 100644 --- a/packages/cloudflare/test/vite/transform.test.ts +++ b/packages/cloudflare/test/vite/transform.test.ts @@ -75,6 +75,46 @@ describe('default export wrapping', () => { expect(result.code).toContain('__SENTRY__.withSentry('); }); + it('wraps a default re-export from another module', () => { + const code = "export { default } from './handler';"; + + expect(transform(code, ctx)!.code).toBe( + [ + "import * as __SENTRY__ from '@sentry/cloudflare';", + "import __SENTRY_REEXPORT_default__ from './handler';", + 'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', + 'export { __SENTRY_DEFAULT_EXPORT__ as default };', + ].join('\n'), + ); + }); + + it('wraps a named export of another module re-exported as default', () => { + const code = "export { handler as default } from './handler';"; + + expect(transform(code, ctx)!.code).toBe( + [ + "import * as __SENTRY__ from '@sentry/cloudflare';", + "import { handler as __SENTRY_REEXPORT_default__ } from './handler';", + 'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', + 'export { __SENTRY_DEFAULT_EXPORT__ as default };', + ].join('\n'), + ); + }); + + it('leaves sibling specifiers of a default re-export untouched', () => { + const code = "export { default, helper } from './handler';"; + + expect(transform(code, ctx)!.code).toBe( + [ + "import * as __SENTRY__ from '@sentry/cloudflare';", + "import __SENTRY_REEXPORT_default__ from './handler';", + 'const __SENTRY_DEFAULT_EXPORT__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.withSentry, (env) => ({}), __SENTRY_REEXPORT_default__);', + 'export { __SENTRY_DEFAULT_EXPORT__ as default };', + "export { helper } from './handler';", + ].join('\n'), + ); + }); + it('uses custom options callback', () => { const custom: TransformContext = { classWrappers: doWrappers(), @@ -889,6 +929,16 @@ describe('same-worker RPC binding floor', () => { expect(result.code).toContain('rpcTracePropagationBindings: ["SELF",'); }); + it('enables a self service binding when it wrapped a default re-export', () => { + const result = transform("export { default } from './handler';", { + classWrappers: doWrappers(), + optionsFn: '() => undefined', + sameWorkerBindings: [{ bindingName: 'SELF', className: DEFAULT_EXPORT }], + })!; + + expect(result.code).toContain('rpcTracePropagationBindings: ["SELF",'); + }); + it('enables a self service binding when the default export re-exports an already wrapped class', () => { const code = [ "import { WorkerEntrypoint } from 'cloudflare:workers';",