diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/tests/autoinstrument.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/tests/autoinstrument.test.ts index 4139071b884f..792e61834ce1 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/tests/autoinstrument.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/tests/autoinstrument.test.ts @@ -97,10 +97,8 @@ for (const { title, binding, agentClass } of [ expect(rpcSpan.attributes['sentry.op']?.value).toBe('rpc'); expect(rpcSpan.attributes['sentry.origin']?.value).toBe('auto.faas.cloudflare.agents'); // Read back off the instance at runtime (`_ParentClass.name`), so it - // confirms the wrapper landed on the user's real class. Matched loosely - // because the transform renames the class it wraps to - // `__SENTRY_ORIGINAL___` and the bundler infers that name. - expect(rpcSpan.attributes['gen_ai.agent.name']?.value).toContain(agentClass); + // confirms the wrapper landed on the user's real class, with its name intact. + expect(rpcSpan.attributes['gen_ai.agent.name']?.value).toBe(agentClass); }); } diff --git a/packages/cloudflare/src/vite/transform.ts b/packages/cloudflare/src/vite/transform.ts index 5e06304ae6f8..1fdf647831c1 100644 --- a/packages/cloudflare/src/vite/transform.ts +++ b/packages/cloudflare/src/vite/transform.ts @@ -159,7 +159,7 @@ export function applyAutoInstrumentTransforms( needsImport: false, wrappedClasses: new Set(), topLevelClasses, - renamedLocals: new Set(), + wrappedLocals: new Map(), classWrappers: ctx.classWrappers, agentClasses: ctx.agentClasses ?? new Set(), workerEntrypointClasses: detectWorkerEntrypointClasses(ast), @@ -217,10 +217,10 @@ interface TransformState { */ topLevelClasses: Map; /** - * Local class names already renamed + wrapped, so two specifiers pointing at - * the same class don't produce duplicate bindings. + * Local class name → the binding holding its wrapper, so two exports of the same class share one + * wrapper and a default export of it can be re-pointed. */ - renamedLocals: Set; + wrappedLocals: Map; /** Class name → wrapper kind, keyed by the *exported* name (from config). */ classWrappers: Map; /** Local class names detected as `agents` Agents (see {@link TransformContext.agentClasses}). */ @@ -348,11 +348,15 @@ function wrapDefaultExport(node: ExportDefaultNode, ctx: TransformContext, state // `export default Foo` where `Foo` is a local class already wrapped by a named // export (e.g. a self-bound WorkerEntrypoint also used as the default handler). - // Wrapping again would produce `withSentry(withSentry(...))`. The binding still - // points at the wrapped class, so the default export counts as auto-wrapped. - if (decl.type === 'Identifier' && state.renamedLocals.has((decl as IdentifierNode).name)) { - state.autoWrapped.add(DEFAULT_EXPORT); - return; + // Wrapping again would produce `withSentry(withSentry(...))`, so the default + // export is re-pointed at the wrapper binding the named export created. + if (decl.type === 'Identifier') { + const wrappedName = state.wrappedLocals.get((decl as IdentifierNode).name); + if (wrappedName) { + state.ms.overwrite(decl.start, decl.end, wrappedName); + state.autoWrapped.add(DEFAULT_EXPORT); + return; + } } // `export default ` → `const __SENTRY_DEFAULT_EXPORT__ = ` @@ -417,36 +421,56 @@ function wrapInlineClassExport( if (!classId || !kind) return; const className = classId.name; - const renamedClass = `__SENTRY_ORIGINAL_${className}__`; // Strip the `export ` keyword state.ms.overwrite(exportNode.start, classDecl.start, ''); - // Rename the class to avoid a duplicate binding - state.ms.overwrite(classId.start, classId.end, renamedClass); - - // Insert the wrapped re-export after the class body - state.ms.appendLeft( - exportNode.end, - `\nexport const ${className} = __SENTRY__.${WRAPPER_METHODS[kind]}(${state.optionsFn}, ${renamedClass});\n`, - ); + const wrappedName = wrapLocalClass(className, classDecl, kind, state); + state.ms.appendLeft(exportNode.end, `export { ${wrappedName} as ${className} };\n`); state.wrappedClasses.add(className); state.autoWrapped.add(className); - state.renamedLocals.add(className); +} + +/** + * Wrap a class declared in this module and return the binding that holds the wrapper. + * + * The declaration is left exactly as written. Only the *export* has to become the wrapper, so the + * wrapper gets its own binding and the export is aliased to it, the same shape as the documented + * manual `instrument*WithSentry` pattern and the cross-module re-export path below. Renaming the + * declaration instead (`class __SENTRY_ORIGINAL_MyAgent__`) would also rename what the class reports + * as `Function.prototype.name`, which libraries read to identify the user's class: `@cloudflare/think` + * hands `this.constructor.name` to the AI SDK as the telemetry `functionId`, and that surfaced as a + * `gen_ai.invoke_agent __SENTRY_ORIGINAL_MyAgent__` span. + */ +function wrapLocalClass( + localName: string, + localClass: ClassDeclarationNode, + kind: ClassWrapperKind, + state: TransformState, +): string { + const existing = state.wrappedLocals.get(localName); + if (existing) return existing; + + const wrappedName = `__SENTRY_WRAPPED_${localName}__`; + state.wrappedLocals.set(localName, wrappedName); + state.ms.appendLeft( + localClass.end, + `\nconst ${wrappedName} = __SENTRY__.${WRAPPER_METHODS[kind]}(${state.optionsFn}, ${localName});\n`, + ); state.needsImport = true; + return wrappedName; } /** * Wrap the configured classes an `export { ... }` statement names. * - * A class *declared* in this module keeps the statement intact: the declaration is renamed and the - * wrapper takes over its binding, so the untouched specifier now exports the wrapped class. - * - * A class that lives in **another** module — imported and re-exported, or re-exported directly — has - * no local binding to overwrite (import bindings are immutable). Those specifiers are re-pointed at - * a fresh wrapper binding instead, which means rebuilding the statement; specifiers this plugin has - * no business touching are carried over verbatim. + * Each such specifier is re-pointed at a binding holding the wrapper, which means rebuilding the + * statement; specifiers this plugin has no business touching are carried over verbatim. For a class + * declared in this module the wrapper binding follows the declaration (see {@link wrapLocalClass}). + * A class that lives in **another** module — imported and re-exported, or re-exported directly — is + * first bound under a private name, since an import binding cannot be reassigned and the `from` form + * has no local binding at all. */ function wrapSpecifierExports(node: ExportNamedNode, ctx: TransformContext, state: TransformState): void { const specifiers = node.specifiers ?? []; @@ -459,7 +483,7 @@ function wrapSpecifierExports(node: ExportNamedNode, ctx: TransformContext, stat const kept: string[] = []; for (const specifier of specifiers) { - const pair = wrapCrossModuleSpecifier(specifier, sourceLiteral, ctx, state, prelude); + const pair = wrapSpecifier(specifier, sourceLiteral, ctx, state, prelude); if (pair) { wrappedPairs.push(pair); } else { @@ -478,15 +502,14 @@ function wrapSpecifierExports(node: ExportNamedNode, ctx: TransformContext, stat } /** - * 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 - * pair depends on are pushed onto `prelude`. + * Handle one export specifier, returning the `Wrapped as Exported` pair to emit for a class that + * gets wrapped. The import/wrapper statements a cross-module pair depends on are pushed onto + * `prelude`; a local class's wrapper binding is emitted after its declaration instead. * * Returns `undefined` when the specifier can stay exactly as written: it doesn't name a configured - * class, its class is declared locally (wrapped in place via {@link wrapLocalClassExport}, which - * takes over the binding the specifier already exports), or the binding is already hand-wrapped. + * class, or the binding is already hand-wrapped. */ -function wrapCrossModuleSpecifier( +function wrapSpecifier( specifier: ExportSpecifierNode, sourceLiteral: string | undefined, ctx: TransformContext, @@ -505,16 +528,14 @@ function wrapCrossModuleSpecifier( if (!exportedName || !localName || !kind) return 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. + // Without a `from` clause the specifier points at a module-local binding. if (!sourceLiteral) { const localClass = state.topLevelClasses.get(localName); if (localClass?.id) { - wrapLocalClassExport(localName, localClass, kind, ctx, state); + const wrappedName = wrapLocalClass(localName, localClass, kind, state); state.wrappedClasses.add(exportedName); - state.needsImport = true; - return undefined; + return `${wrappedName} as ${exportedName}`; } if (state.manuallyWrappedLocals.has(localName)) { @@ -549,23 +570,3 @@ function wrapCrossModuleSpecifier( return `${wrappedName} as ${exportedName}`; } - -/** Rename a locally declared class and rebind its original name to the wrapper. */ -function wrapLocalClassExport( - localName: string, - localClass: ClassDeclarationNode, - kind: ClassWrapperKind, - ctx: TransformContext, - state: TransformState, -): void { - const classId = localClass.id; - if (!classId || state.renamedLocals.has(localName)) return; - state.renamedLocals.add(localName); - - const renamedClass = `__SENTRY_ORIGINAL_${localName}__`; - state.ms.overwrite(classId.start, classId.end, renamedClass); - state.ms.appendLeft( - localClass.end, - `\nconst ${localName} = __SENTRY__.${WRAPPER_METHODS[kind]}(${state.optionsFn}, ${renamedClass});\n`, - ); -} diff --git a/packages/cloudflare/test/vite/autoInstrument.test.ts b/packages/cloudflare/test/vite/autoInstrument.test.ts index 17bd5141eb4e..7303e667ee6c 100644 --- a/packages/cloudflare/test/vite/autoInstrument.test.ts +++ b/packages/cloudflare/test/vite/autoInstrument.test.ts @@ -155,10 +155,11 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => { [ "import * as __SENTRY__ from '@sentry/cloudflare';", "import { WorkerEntrypoint } from 'cloudflare:workers';", - 'class __SENTRY_ORIGINAL_AdminEntry__ extends WorkerEntrypoint {', + 'class AdminEntry extends WorkerEntrypoint {', ' fetch() { return new Response("admin"); }', '}', - 'export const AdminEntry = __SENTRY__.withSentry(() => undefined, __SENTRY_ORIGINAL_AdminEntry__);', + 'const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry(() => undefined, AdminEntry);', + 'export { __SENTRY_WRAPPED_AdminEntry__ as AdminEntry };', '', ].join('\n'), ); @@ -181,7 +182,7 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => { const result = await plugin.transform.call({ parse: (c: string) => parseJS(c) }, code, join(dir, 'index.ts')); expect(result).toBeDefined(); - expect(result.code).toContain('export const AdminEntry = __SENTRY__.withSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry('); }); it('wraps a WorkerEntrypoint named via a services[].entrypoint self-binding (jsonc config)', async () => { @@ -210,7 +211,7 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => { const result = await plugin.transform.call({ parse: (c: string) => parseJS(c) }, code, join(dir, 'index.ts')); expect(result).toBeDefined(); - expect(result.code).toContain('export const BindingEntrypoint = __SENTRY__.withSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_BindingEntrypoint__ = __SENTRY__.withSentry('); }); it('does not wrap an entrypoint that is neither detected nor self-bound', async () => { @@ -266,7 +267,7 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => { const code = ["import { Agent } from 'agents';", 'export class MyAgent extends Agent {}'].join('\n'); const result = await tx(code); - expect(result.code).toContain('export const MyAgent = __SENTRY__.instrumentAgentWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyAgent__ = __SENTRY__.instrumentAgentWithSentry('); expect(result.code).not.toContain('instrumentDurableObjectWithSentry'); }); @@ -278,7 +279,7 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => { ].join('\n'); const result = await tx(code); - expect(result.code).toContain('export const MyAgent = __SENTRY__.instrumentAgentWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyAgent__ = __SENTRY__.instrumentAgentWithSentry('); }); it('wraps an Agent whose base class lives in another module', async () => { @@ -288,7 +289,7 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => { const code = ["import { MyBase } from './base';", 'export class MyAgent extends MyBase {}'].join('\n'); const result = await tx(code); - expect(result.code).toContain('export const MyAgent = __SENTRY__.instrumentAgentWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyAgent__ = __SENTRY__.instrumentAgentWithSentry('); }); it('keeps the Durable Object helper for a DO whose base class lives in another module', async () => { @@ -301,7 +302,7 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => { const code = ["import { MyBase } from './base';", 'export class MyAgent extends MyBase {}'].join('\n'); const result = await tx(code); - expect(result.code).toContain('export const MyAgent = __SENTRY__.instrumentDurableObjectWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyAgent__ = __SENTRY__.instrumentDurableObjectWithSentry('); expect(result.code).not.toContain('instrumentAgentWithSentry'); }); diff --git a/packages/cloudflare/test/vite/transform.test.ts b/packages/cloudflare/test/vite/transform.test.ts index 51dafe6d6b27..7e26dd77465b 100644 --- a/packages/cloudflare/test/vite/transform.test.ts +++ b/packages/cloudflare/test/vite/transform.test.ts @@ -132,11 +132,50 @@ describe('Durable Object class wrapping', () => { const result = transform(code, ctx)!; expect(result).toBeDefined(); - expect(result.code).toContain('class __SENTRY_ORIGINAL_MyDurableObject__'); + expect(result.code).toContain('class MyDurableObject extends DurableObject {'); expect(result.code).not.toContain('export class MyDurableObject'); - expect(result.code).toContain('__SENTRY__.instrumentDurableObjectWithSentry('); - expect(result.code).toContain('export const MyDurableObject ='); - expect(result.code).toContain('__SENTRY_ORIGINAL_MyDurableObject__'); + expect(result.code).toContain( + 'const __SENTRY_WRAPPED_MyDurableObject__ = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({}), MyDurableObject);', + ); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDurableObject__ as MyDurableObject };'); + }); + + // The wrapper must not touch the declaration: `Function.prototype.name` is what libraries read to + // identify the user's class (`@cloudflare/think` passes `this.constructor.name` to the AI SDK as the + // telemetry `functionId`, which becomes the `gen_ai.invoke_agent` span name), and a user-chosen + // `static name` has to keep winning too. + it('leaves the class declaration untouched and aliases the export instead', () => { + const code = [ + 'class DurableObject {}', + 'export class MyDurableObject extends DurableObject { static name = "user-chosen"; }', + ].join('\n'); + + const result = transform(code, ctx)!; + + expect(result.code).toContain('class MyDurableObject extends DurableObject { static name = "user-chosen"; }'); + expect(result.code).not.toContain('__SENTRY_ORIGINAL_'); + expect(result.code).not.toContain('Object.defineProperty'); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDurableObject__ as MyDurableObject };'); + }); + + it('shares one wrapper between two exports of the same local class', () => { + const code = [ + 'class DurableObject {}', + 'class MyDurableObject extends DurableObject {}', + 'export { MyDurableObject };', + 'export { MyDurableObject as Alias };', + ].join('\n'); + + const result = transform(code, { + classWrappers: doWrappers('MyDurableObject', 'Alias'), + optionsFn: '(env) => ({})', + })!; + + const wrapCount = (result.code.match(/instrumentDurableObjectWithSentry\(/g) ?? []).length; + expect(wrapCount).toBe(1); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDurableObject__ as MyDurableObject };'); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDurableObject__ as Alias };'); + expect(result.wrappedClasses).toEqual(new Set(['MyDurableObject', 'Alias'])); }); it('wraps multiple DO classes', () => { @@ -153,10 +192,10 @@ describe('Durable Object class wrapping', () => { const result = transform(code, multi)!; expect(result).toBeDefined(); - expect(result.code).toContain('export const DOA ='); - expect(result.code).toContain('export const DOB ='); - expect(result.code).toContain('class __SENTRY_ORIGINAL_DOA__'); - expect(result.code).toContain('class __SENTRY_ORIGINAL_DOB__'); + expect(result.code).toContain('export { __SENTRY_WRAPPED_DOA__ as DOA };'); + expect(result.code).toContain('export { __SENTRY_WRAPPED_DOB__ as DOB };'); + expect(result.code).toContain('class DOA extends DurableObject'); + expect(result.code).toContain('class DOB extends DurableObject'); }); it('ignores classes not listed in wrangler config', () => { @@ -181,12 +220,11 @@ describe('Durable Object class wrapping', () => { const result = transform(code, ctx)!; expect(result).toBeDefined(); - expect(result.code).toContain('class __SENTRY_ORIGINAL_MyDurableObject__'); + expect(result.code).toContain('class MyDurableObject extends DurableObject {'); expect(result.code).toContain( - 'const MyDurableObject = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({}), __SENTRY_ORIGINAL_MyDurableObject__);', + 'const __SENTRY_WRAPPED_MyDurableObject__ = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({}), MyDurableObject);', ); - // The original specifier export keeps exporting the wrapped binding. - expect(result.code).toContain('export { MyDurableObject };'); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDurableObject__ as MyDurableObject };'); expect(result.wrappedClasses).toEqual(new Set(['MyDurableObject'])); }); @@ -199,11 +237,11 @@ describe('Durable Object class wrapping', () => { const result = transform(code, ctx)!; expect(result).toBeDefined(); - expect(result.code).toContain('class __SENTRY_ORIGINAL_Internal__'); + expect(result.code).toContain('class Internal extends DurableObject {}'); expect(result.code).toContain( - 'const Internal = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({}), __SENTRY_ORIGINAL_Internal__);', + 'const __SENTRY_WRAPPED_Internal__ = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({}), Internal);', ); - expect(result.code).toContain('export { Internal as MyDurableObject };'); + expect(result.code).toContain('export { __SENTRY_WRAPPED_Internal__ as MyDurableObject };'); expect(result.wrappedClasses).toEqual(new Set(['MyDurableObject'])); }); @@ -293,20 +331,19 @@ describe('Durable Object class wrapping', () => { ].join('\n'); const result = transform(code, mixed)!; - // The local class keeps its binding (renamed declaration + wrapper), so its specifier is - // carried over untouched alongside the unrelated one. + // Both classes export through a wrapper binding; only the unrelated specifier is carried over. expect(result.code).toBe( [ "import * as __SENTRY__ from '@sentry/cloudflare';", "import { ImportedDO } from './do';", 'class DurableObject {}', - 'class __SENTRY_ORIGINAL_LocalDO__ extends DurableObject {}', - 'const LocalDO = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({}), __SENTRY_ORIGINAL_LocalDO__);', + 'class LocalDO extends DurableObject {}', + 'const __SENTRY_WRAPPED_LocalDO__ = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({}), LocalDO);', '', 'const unrelated = 1;', 'const __SENTRY_WRAPPED_ImportedDO__ = __SENTRY__._INTERNAL_wrapUnlessInstrumented(__SENTRY__.instrumentDurableObjectWithSentry, (env) => ({}), ImportedDO);', - 'export { __SENTRY_WRAPPED_ImportedDO__ as ImportedDO };', - 'export { LocalDO, unrelated };', + 'export { __SENTRY_WRAPPED_LocalDO__ as LocalDO, __SENTRY_WRAPPED_ImportedDO__ as ImportedDO };', + 'export { unrelated };', ].join('\n'), ); expect(result.wrappedClasses).toEqual(new Set(['LocalDO', 'ImportedDO'])); @@ -344,7 +381,7 @@ describe('Durable Object class wrapping', () => { const result = transform(code, { classWrappers: doWrappers('MyDurableObject'), optionsFn: '(env) => ({})' })!; expect(result.wrappedClasses).toEqual(new Set(['MyDurableObject'])); expect(result.code).toBe(code); - expect(result.code).not.toContain('__SENTRY_ORIGINAL_'); + expect(result.code).not.toContain('__SENTRY_WRAPPED_'); expect(result.code).not.toContain("import * as __SENTRY__ from '@sentry/cloudflare'"); }); @@ -378,9 +415,10 @@ describe('Workflow class wrapping', () => { const result = transform(code, ctx)!; expect(result).toBeDefined(); - expect(result.code).toContain('class __SENTRY_ORIGINAL_MyWorkflow__'); + expect(result.code).toContain('class MyWorkflow extends WorkflowEntrypoint {'); expect(result.code).not.toContain('export class MyWorkflow'); - expect(result.code).toContain('export const MyWorkflow = __SENTRY__.instrumentWorkflowWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyWorkflow__ = __SENTRY__.instrumentWorkflowWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyWorkflow__ as MyWorkflow };'); // A workflow must never be wrapped with the DO helper. expect(result.code).not.toContain('instrumentDurableObjectWithSentry'); expect(result.wrappedClasses).toEqual(new Set(['MyWorkflow'])); @@ -395,9 +433,9 @@ describe('Workflow class wrapping', () => { const result = transform(code, ctx)!; expect(result.code).toContain( - 'const MyWorkflow = __SENTRY__.instrumentWorkflowWithSentry((env) => ({}), __SENTRY_ORIGINAL_MyWorkflow__);', + 'const __SENTRY_WRAPPED_MyWorkflow__ = __SENTRY__.instrumentWorkflowWithSentry((env) => ({}), MyWorkflow);', ); - expect(result.code).toContain('export { MyWorkflow };'); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyWorkflow__ as MyWorkflow };'); expect(result.wrappedClasses).toEqual(new Set(['MyWorkflow'])); }); @@ -455,9 +493,10 @@ describe('WorkerEntrypoint class wrapping (structural)', () => { const result = transform(code, ctx)!; expect(result).toBeDefined(); - expect(result.code).toContain('class __SENTRY_ORIGINAL_AdminEntry__'); + expect(result.code).toContain('class AdminEntry extends WorkerEntrypoint {'); expect(result.code).not.toContain('export class AdminEntry'); - expect(result.code).toContain('export const AdminEntry = __SENTRY__.withSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_AdminEntry__ as AdminEntry };'); expect(result.wrappedClasses).toEqual(new Set(['AdminEntry'])); }); @@ -468,7 +507,8 @@ describe('WorkerEntrypoint class wrapping (structural)', () => { ].join('\n'); const result = transform(code, ctx)!; - expect(result.code).toContain('export const AdminEntry = __SENTRY__.withSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_AdminEntry__ as AdminEntry };'); }); it('wraps a class extending a namespace-imported WorkerEntrypoint', () => { @@ -478,7 +518,8 @@ describe('WorkerEntrypoint class wrapping (structural)', () => { ].join('\n'); const result = transform(code, ctx)!; - expect(result.code).toContain('export const AdminEntry = __SENTRY__.withSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_AdminEntry__ as AdminEntry };'); }); it('wraps a class via an indirect same-file base chain', () => { @@ -489,7 +530,8 @@ describe('WorkerEntrypoint class wrapping (structural)', () => { ].join('\n'); const result = transform(code, ctx)!; - expect(result.code).toContain('export const AdminEntry = __SENTRY__.withSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_AdminEntry__ as AdminEntry };'); expect(result.wrappedClasses).toEqual(new Set(['AdminEntry'])); }); @@ -502,9 +544,9 @@ describe('WorkerEntrypoint class wrapping (structural)', () => { const result = transform(code, ctx)!; expect(result.code).toContain( - 'const AdminEntry = __SENTRY__.withSentry((env) => ({}), __SENTRY_ORIGINAL_AdminEntry__);', + 'const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry((env) => ({}), AdminEntry);', ); - expect(result.code).toContain('export { AdminEntry };'); + expect(result.code).toContain('export { __SENTRY_WRAPPED_AdminEntry__ as AdminEntry };'); expect(result.wrappedClasses).toEqual(new Set(['AdminEntry'])); }); @@ -541,7 +583,8 @@ describe('WorkerEntrypoint class wrapping (config fallback)', () => { const code = ["import { BaseEntry } from './base';", 'export class AdminEntry extends BaseEntry {}'].join('\n'); const result = transform(code, ctx)!; - expect(result.code).toContain('export const AdminEntry = __SENTRY__.withSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_AdminEntry__ as AdminEntry };'); expect(result.wrappedClasses).toEqual(new Set(['AdminEntry'])); }); @@ -599,7 +642,8 @@ describe('agent class wrapping', () => { optionsFn: '(env) => ({})', })!; - expect(result.code).toContain('export const MyAgent = __SENTRY__.instrumentAgentWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyAgent__ = __SENTRY__.instrumentAgentWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyAgent__ as MyAgent };'); expect(result.code).not.toContain('instrumentDurableObjectWithSentry'); expect(result.wrappedClasses).toEqual(new Set(['MyAgent'])); }); @@ -613,7 +657,8 @@ describe('agent class wrapping', () => { optionsFn: '(env) => ({})', })!; - expect(result.code).toContain('export const MyDO = __SENTRY__.instrumentDurableObjectWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyDO__ = __SENTRY__.instrumentDurableObjectWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDO__ as MyDO };'); expect(result.code).not.toContain('instrumentAgentWithSentry'); }); @@ -631,8 +676,10 @@ describe('agent class wrapping', () => { optionsFn: '(env) => ({})', })!; - expect(result.code).toContain('export const MyAgent = __SENTRY__.instrumentAgentWithSentry('); - expect(result.code).toContain('export const MyDO = __SENTRY__.instrumentDurableObjectWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyAgent__ = __SENTRY__.instrumentAgentWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyAgent__ as MyAgent };'); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyDO__ = __SENTRY__.instrumentDurableObjectWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDO__ as MyDO };'); }); it('emits the expected source for a mixed Agent/chat-agent/DO entry', () => { @@ -658,14 +705,17 @@ describe('agent class wrapping', () => { "import { Agent } from 'agents';", "import { AIChatAgent } from '@cloudflare/ai-chat';", "import { DurableObject } from 'cloudflare:workers';", - 'class __SENTRY_ORIGINAL_MyAgent__ extends Agent {}', - 'export const MyAgent = __SENTRY__.instrumentAgentWithSentry((env) => ({ dsn: env.SENTRY_DSN }), __SENTRY_ORIGINAL_MyAgent__);', + 'class MyAgent extends Agent {}', + 'const __SENTRY_WRAPPED_MyAgent__ = __SENTRY__.instrumentAgentWithSentry((env) => ({ dsn: env.SENTRY_DSN }), MyAgent);', + 'export { __SENTRY_WRAPPED_MyAgent__ as MyAgent };', '', - 'class __SENTRY_ORIGINAL_MyChat__ extends AIChatAgent {}', - 'export const MyChat = __SENTRY__.instrumentAgentWithSentry((env) => ({ dsn: env.SENTRY_DSN }), __SENTRY_ORIGINAL_MyChat__);', + 'class MyChat extends AIChatAgent {}', + 'const __SENTRY_WRAPPED_MyChat__ = __SENTRY__.instrumentAgentWithSentry((env) => ({ dsn: env.SENTRY_DSN }), MyChat);', + 'export { __SENTRY_WRAPPED_MyChat__ as MyChat };', '', - 'class __SENTRY_ORIGINAL_MyDO__ extends DurableObject {}', - 'export const MyDO = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({ dsn: env.SENTRY_DSN }), __SENTRY_ORIGINAL_MyDO__);', + 'class MyDO extends DurableObject {}', + 'const __SENTRY_WRAPPED_MyDO__ = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({ dsn: env.SENTRY_DSN }), MyDO);', + 'export { __SENTRY_WRAPPED_MyDO__ as MyDO };', '', 'const __SENTRY_DEFAULT_EXPORT__ = { fetch() {} };', 'export default __SENTRY__.withSentry((env) => ({ dsn: env.SENTRY_DSN }), __SENTRY_DEFAULT_EXPORT__);', @@ -687,7 +737,8 @@ describe('agent class wrapping', () => { optionsFn: '(env) => ({})', })!; - expect(result.code).toContain('const LocalAgent = __SENTRY__.instrumentAgentWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_LocalAgent__ = __SENTRY__.instrumentAgentWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_LocalAgent__ as ConfiguredAgent };'); }); it('does not report a manually Agent-wrapped export as unwrapped', () => { @@ -749,8 +800,10 @@ describe('combined transforms', () => { ].join('\n'); const result = transform(code, mixed)!; - expect(result.code).toContain('export const MyDO = __SENTRY__.instrumentDurableObjectWithSentry('); - expect(result.code).toContain('export const MyWorkflow = __SENTRY__.instrumentWorkflowWithSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyDO__ = __SENTRY__.instrumentDurableObjectWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDO__ as MyDO };'); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyWorkflow__ = __SENTRY__.instrumentWorkflowWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyWorkflow__ as MyWorkflow };'); expect(result.wrappedClasses).toEqual(new Set(['MyDO', 'MyWorkflow'])); const importCount = (result.code.match(/import \* as __SENTRY__/g) ?? []).length; expect(importCount).toBe(1); @@ -771,8 +824,9 @@ describe('combined transforms', () => { expect(result).toBeDefined(); // DO wrapped - expect(result.code).toContain('class __SENTRY_ORIGINAL_MyDO__'); - expect(result.code).toContain('export const MyDO = __SENTRY__.instrumentDurableObjectWithSentry('); + expect(result.code).toContain('class MyDO extends DurableObject {'); + expect(result.code).toContain('const __SENTRY_WRAPPED_MyDO__ = __SENTRY__.instrumentDurableObjectWithSentry('); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDO__ as MyDO };'); // Default export wrapped expect(result.code).toContain('const __SENTRY_DEFAULT_EXPORT__ ='); @@ -796,10 +850,10 @@ describe('combined transforms', () => { // The named export wraps it once; the default re-export must not wrap again. const wrapCount = (result.code.match(/withSentry\(/g) ?? []).length; expect(wrapCount).toBe(1); - expect(result.code).toContain('const AdminEntry = __SENTRY__.withSentry('); + expect(result.code).toContain('const __SENTRY_WRAPPED_AdminEntry__ = __SENTRY__.withSentry('); expect(result.code).not.toContain('__SENTRY_DEFAULT_EXPORT__'); - // The default export still points at the (single-)wrapped binding. - expect(result.code).toContain('export default AdminEntry;'); + // The default export is re-pointed at the (single-)wrapped binding. + expect(result.code).toContain('export default __SENTRY_WRAPPED_AdminEntry__;'); }); it('handles the default export appearing before its named wrap in source order', () => { @@ -827,7 +881,7 @@ describe('combined transforms', () => { const result = transform(code, ctx)!; expect(result).toBeDefined(); // DO still wrapped - expect(result.code).toContain('export const MyDO ='); + expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDO__ as MyDO };'); // Default not double-wrapped expect(result.code).not.toContain('__SENTRY_DEFAULT_EXPORT__'); });