Skip to content

Commit f3f2a5f

Browse files
JPeer264claude
andcommitted
feat(cloudflare): Wrap vinext Worker entries with @sentry/nextjs/cloudflare
`sentryCloudflareVitePlugin` wrapped the Worker entry of a vinext app with `withSentry` of `@sentry/cloudflare`, so the app sent Cloudflare data but no Next.js spans. In a build with the vinext plugins, the generated code now imports from `@sentry/nextjs/cloudflare` when it resolves from the entry. Other builds, and vinext builds without `@sentry/nextjs`, keep `@sentry/cloudflare`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c3faaeb commit f3f2a5f

3 files changed

Lines changed: 73 additions & 2 deletions

File tree

‎packages/cloudflare/src/vite/autoInstrument.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,27 @@ function normalizePath(path: string): string {
1515
// `.html`, … — sharing the entry's basename must never be treated as the entry.
1616
const JS_EXTENSION_REGEX = /\.[cm]?[jt]sx?$/;
1717

18+
// Re-exports everything the generated code uses, and adds the Next.js span handling and OpenTelemetry context.
19+
const NEXTJS_CLOUDFLARE_MODULE = '@sentry/nextjs/cloudflare';
20+
1821
export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPath?: string } = {}) {
1922
let wranglerConfig: WranglerConfig | undefined;
2023
let entryFilePath: string | undefined;
2124

2225
let optionsFn = ENV_FALLBACK_OPTIONS_FN;
2326
let optionsImport: string | undefined;
27+
let isVinext = false;
2428

2529
return {
2630
name: 'sentry-cloudflare-auto-instrument',
2731

28-
configResolved(config: { root: string; logger?: { warn(msg: string): void } }): void {
32+
configResolved(config: {
33+
root: string;
34+
plugins?: readonly { name?: string }[];
35+
logger?: { warn(msg: string): void };
36+
}): void {
37+
isVinext = config.plugins?.some(plugin => plugin.name?.startsWith('vinext:')) ?? false;
38+
2939
const result = resolveWranglerConfig(config.root, options.wranglerConfigPath);
3040
if (!result) {
3141
// An explicit path that fails is a misconfiguration worth naming;
@@ -119,6 +129,12 @@ export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPa
119129
})
120130
: undefined;
121131

132+
// A vinext app with `@sentry/nextjs` installed gets `withSentry` of `@sentry/nextjs/cloudflare`.
133+
const sentryModule =
134+
isVinext && (await this.resolve?.(NEXTJS_CLOUDFLARE_MODULE, normalizedId))
135+
? NEXTJS_CLOUDFLARE_MODULE
136+
: undefined;
137+
122138
// No registration import is injected here: the orchestrion plugin's
123139
// subscribe-injection makes each bundled package self-register its channel
124140
// subscriber on the global marker, so wrapping the entry with `withSentry`
@@ -129,6 +145,7 @@ export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPa
129145
optionsFn,
130146
optionsImport,
131147
sameWorkerBindings: wranglerConfig.sameWorkerBindings,
148+
sentryModule,
132149
});
133150

134151
const wrappedClasses = result?.wrappedClasses ?? new Set<string>();

‎packages/cloudflare/src/vite/transform.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export interface TransformContext {
114114
optionsFn: string;
115115
/** Import statement prepended when `optionsFn` references a separate module. */
116116
optionsImport?: string;
117+
/** Module the generated code imports `withSentry` and the class wrappers from. Defaults to `@sentry/cloudflare`. */
118+
sentryModule?: string;
117119
/** @see {@link import('./wranglerConfig').WranglerConfig.sameWorkerBindings} */
118120
sameWorkerBindings?: readonly SameWorkerBinding[];
119121
}
@@ -198,7 +200,7 @@ export function applyAutoInstrumentTransforms(
198200
ms.prepend(buildMergedOptionsDeclaration(sameWorkerBindings, ctx.optionsFn, state));
199201
}
200202
if (ctx.optionsImport) ms.prepend(ctx.optionsImport);
201-
ms.prepend("import * as __SENTRY__ from '@sentry/cloudflare';\n");
203+
ms.prepend(`import * as __SENTRY__ from '${ctx.sentryModule ?? '@sentry/cloudflare'}';\n`);
202204

203205
return {
204206
code: ms.toString(),

‎packages/cloudflare/test/vite/autoInstrument.test.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,58 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => {
5555
expect(result.code).toContain('__SENTRY__.withSentry(');
5656
});
5757

58+
it('wraps the entry with `@sentry/nextjs/cloudflare` in a vinext build', async () => {
59+
const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' });
60+
const plugin = sentryCloudflareAutoInstrumentPlugin();
61+
plugin.configResolved({ root: dir, plugins: [{ name: 'vinext:config' }] });
62+
63+
const code = 'export default { fetch() { return new Response("ok"); } };';
64+
const result = await plugin.transform.call(
65+
{
66+
parse: (c: string) => parseJS(c),
67+
resolve: async (source: string) => (source === '@sentry/nextjs/cloudflare' ? { id: source } : null),
68+
},
69+
code,
70+
join(dir, 'src/index.ts'),
71+
);
72+
73+
expect(result.code).toContain("import * as __SENTRY__ from '@sentry/nextjs/cloudflare';");
74+
expect(result.code).toContain('__SENTRY__.withSentry(');
75+
});
76+
77+
it('wraps the entry with `@sentry/cloudflare` when `@sentry/nextjs/cloudflare` resolves outside a vinext build', async () => {
78+
const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' });
79+
const plugin = sentryCloudflareAutoInstrumentPlugin();
80+
plugin.configResolved({ root: dir, plugins: [{ name: 'vite:react' }] });
81+
82+
const code = 'export default { fetch() { return new Response("ok"); } };';
83+
const result = await plugin.transform.call(
84+
{
85+
parse: (c: string) => parseJS(c),
86+
resolve: async (source: string) => (source === '@sentry/nextjs/cloudflare' ? { id: source } : null),
87+
},
88+
code,
89+
join(dir, 'src/index.ts'),
90+
);
91+
92+
expect(result.code).toContain("import * as __SENTRY__ from '@sentry/cloudflare';");
93+
});
94+
95+
it('wraps the entry with `@sentry/cloudflare` in a vinext build without `@sentry/nextjs`', async () => {
96+
const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' });
97+
const plugin = sentryCloudflareAutoInstrumentPlugin();
98+
plugin.configResolved({ root: dir, plugins: [{ name: 'vinext:config' }] });
99+
100+
const code = 'export default { fetch() { return new Response("ok"); } };';
101+
const result = await plugin.transform.call(
102+
{ parse: (c: string) => parseJS(c), resolve: async () => null },
103+
code,
104+
join(dir, 'src/index.ts'),
105+
);
106+
107+
expect(result.code).toContain("import * as __SENTRY__ from '@sentry/cloudflare';");
108+
});
109+
58110
it('leaves an already-manually-wrapped entry untouched', async () => {
59111
const { transform: tx, entryPath } = createPlugin('main = "src/index.ts"');
60112

0 commit comments

Comments
 (0)