-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(v10/cloudflare): Auto-instrument the worker entry with withSentry #22540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
andreiborza
merged 1 commit into
ab/backport-22422-vite-test-runner
from
ab/backport-22433-autoinstrument-withsentry
Jul 23, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| interface Env { | ||
| SENTRY_DSN: string; | ||
| } | ||
|
|
||
| // A plain, unwrapped worker — no manual `Sentry.withSentry`. The | ||
| // `@sentry/cloudflare/vite` plugin's auto-instrumentation wraps the default | ||
| // export with `withSentry` at build time, sourcing options from | ||
| // `instrument.server.ts`. | ||
| export default { | ||
| async fetch(request: Request): Promise<Response> { | ||
| const url = new URL(request.url); | ||
|
|
||
| if (url.pathname === '/hello') { | ||
| return Response.json({ status: 'ok' }); | ||
| } | ||
|
|
||
| return new Response('Not found', { status: 404 }); | ||
| }, | ||
| } satisfies ExportedHandler<Env>; |
6 changes: 6 additions & 0 deletions
6
...oudflare-integration-tests/suites/vite-autoinstrument/default-export/instrument.server.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { defineCloudflareOptions } from '@sentry/cloudflare'; | ||
|
|
||
| export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ | ||
| dsn: env.SENTRY_DSN, | ||
| tracesSampleRate: 1.0, | ||
| })); |
21 changes: 21 additions & 0 deletions
21
dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import type { TransactionEvent } from '@sentry/core'; | ||
| import { expect, it } from 'vitest'; | ||
| import { createRunner } from '../../../runner'; | ||
|
|
||
| // The worker entry is a plain, unwrapped `export default {...}`. The runner | ||
| // detects `vite.config.mts`, runs `vite build`, and serves the generated output | ||
| // — so this transaction only arrives if the build-time transform wrapped the | ||
| // default export with `withSentry`. | ||
| it('auto-instruments a plain default-export handler', async ({ signal }) => { | ||
| const runner = createRunner(__dirname) | ||
| .expect(envelope => { | ||
| const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent; | ||
| expect(transactionEvent.transaction).toBe('GET /hello'); | ||
| expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); | ||
| expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); | ||
| }) | ||
| .start(signal); | ||
|
|
||
| await runner.makeRequest('get', '/hello'); | ||
| await runner.completed(); | ||
| }); |
16 changes: 16 additions & 0 deletions
16
...es/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/vite.config.mts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { cloudflare } from '@cloudflare/vite-plugin'; | ||
| import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite'; | ||
| import { defineConfig } from 'vite'; | ||
|
|
||
| export default defineConfig({ | ||
| // The Sentry plugin runs first so its build-time transform wraps the worker's | ||
| // default export before the Cloudflare plugin bundles it. | ||
| plugins: [ | ||
| cloudflare(), | ||
| sentryCloudflareVitePlugin({ | ||
| _experimental: { | ||
| autoInstrumentation: true, | ||
| }, | ||
| }), | ||
| ], | ||
| }); |
9 changes: 9 additions & 0 deletions
9
...ges/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/wrangler.jsonc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "$schema": "../../../node_modules/wrangler/config-schema.json", | ||
| "name": "cloudflare-vite-autoinstrument-default-export", | ||
| // `main` points at the source entry; the Sentry Vite plugin builds from it (so | ||
| // the auto-instrument transform runs) and the runner serves the built output. | ||
| "main": "index.ts", | ||
| "compatibility_date": "2025-06-17", | ||
| "compatibility_flags": ["nodejs_als"], | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import { buildOptionsImport, ENV_FALLBACK_OPTIONS_FN, resolveInstrumentFile } from './instrumentFile'; | ||
| import { applyAutoInstrumentTransforms, type ProgramBody } from './transform'; | ||
| import { resolveWranglerConfig, type WranglerConfig } from './wranglerConfig'; | ||
|
|
||
| // Vite normalizes module IDs to posix separators even on Windows, while | ||
| // `path.resolve` yields backslashes there — normalize before comparing. | ||
| function normalizePath(path: string): string { | ||
| return path.replace(/\\/g, '/'); | ||
| } | ||
|
|
||
| // Extensions the entry-module match may tolerate swapping (e.g. wrangler's | ||
| // `main` says `.ts` but the served module is `.js`). Anything else — `.css`, | ||
| // `.html`, … — sharing the entry's basename must never be treated as the entry. | ||
| const JS_EXTENSION_REGEX = /\.[cm]?[jt]sx?$/; | ||
|
|
||
| export function sentryCloudflareAutoInstrumentPlugin() { | ||
| let wranglerConfig: WranglerConfig | undefined; | ||
| let entryFilePath: string | undefined; | ||
|
|
||
| let optionsFn = ENV_FALLBACK_OPTIONS_FN; | ||
| let optionsImport: string | undefined; | ||
|
|
||
| return { | ||
| name: 'sentry-cloudflare-auto-instrument', | ||
|
|
||
| configResolved(config: { root: string; logger?: { warn(msg: string): void } }): void { | ||
| const result = resolveWranglerConfig(config.root); | ||
| if (!result) { | ||
| config.logger?.warn('[sentry] No parseable wrangler config found — auto-instrumentation disabled.'); | ||
| return; | ||
| } | ||
|
|
||
| wranglerConfig = result.config; | ||
| if (wranglerConfig.main) { | ||
| // `main` is already absolute (wrangler resolves it); just normalize | ||
| // separators so the entry-module comparison holds on Windows. | ||
| entryFilePath = normalizePath(wranglerConfig.main); | ||
| } | ||
|
|
||
| if (entryFilePath) { | ||
| const instrumentFilePath = resolveInstrumentFile(entryFilePath); | ||
| if (instrumentFilePath) { | ||
| const built = buildOptionsImport(entryFilePath, instrumentFilePath); | ||
| optionsFn = built.optionsFn; | ||
| optionsImport = built.importStmt; | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| transform( | ||
| this: { parse(code: string): ProgramBody; warn?(msg: string): void; environment?: { name?: string } }, | ||
| code: string, | ||
| id: string, | ||
| ): { code: string; map: unknown } | undefined { | ||
| if (!wranglerConfig || !entryFilePath) return undefined; | ||
|
|
||
| // The worker entry never belongs to the client (browser) environment. | ||
| // Skipping it keeps a same-basename sibling (e.g. a `src/index.tsx` | ||
| // client entry next to a `src/index.ts` worker) out of the browser bundle. | ||
| if (this.environment?.name === 'client') return undefined; | ||
|
|
||
| // Vite may append query/hash params to the module ID. | ||
| const normalizedId = normalizePath(id.replace(/[?#].*$/, '')); | ||
| if (normalizedId !== entryFilePath) { | ||
| // Tolerate a differing JS-flavored extension (e.g. `.js` vs `.ts`). | ||
| if (!JS_EXTENSION_REGEX.test(normalizedId) || !JS_EXTENSION_REGEX.test(entryFilePath)) return undefined; | ||
| if (normalizedId.replace(JS_EXTENSION_REGEX, '') !== entryFilePath.replace(JS_EXTENSION_REGEX, '')) { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| let ast: ProgramBody; | ||
| try { | ||
| ast = this.parse(code); | ||
| } catch { | ||
| // Raw TypeScript or syntax error — esbuild hasn't run yet (unlikely) | ||
| // or the file is genuinely broken. Either way, skip silently. | ||
| return undefined; | ||
| } | ||
|
|
||
| const result = applyAutoInstrumentTransforms(code, ast, { | ||
| optionsFn, | ||
| optionsImport, | ||
| }); | ||
|
|
||
| return result ?? undefined; | ||
| }, | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import MagicString from 'magic-string'; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Minimal ESTree node types for the AST nodes we inspect. | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| export interface BaseNode { | ||
| type: string; | ||
| start: number; | ||
| end: number; | ||
| } | ||
|
|
||
| export interface ProgramBody { | ||
| body: BaseNode[]; | ||
| } | ||
|
|
||
| interface CalleeNode { | ||
| type: string; | ||
| name?: string; | ||
| property?: { type: string; name?: string }; | ||
| } | ||
|
|
||
| interface CallExpressionNode extends BaseNode { | ||
| callee?: CalleeNode; | ||
| } | ||
|
|
||
| interface ExportDefaultNode extends BaseNode { | ||
| declaration: BaseNode; | ||
| } | ||
|
|
||
| function isCallToMethod(node: BaseNode, methodName: string): boolean { | ||
| if (node.type !== 'CallExpression') return false; | ||
| const callee = (node as CallExpressionNode).callee; | ||
| if (!callee) return false; | ||
| if (callee.type === 'Identifier' && callee.name === methodName) return true; | ||
| return ( | ||
| callee.type === 'MemberExpression' && callee.property?.type === 'Identifier' && callee.property.name === methodName | ||
| ); | ||
| } | ||
|
|
||
| export interface TransformContext { | ||
| optionsFn: string; | ||
| /** Import statement prepended when `optionsFn` references a separate module. */ | ||
| optionsImport?: string; | ||
| } | ||
|
|
||
| export interface TransformResult { | ||
| code: string; | ||
| map: ReturnType<MagicString['generateMap']>; | ||
| } | ||
|
|
||
| /** | ||
| * Rewrite the worker entry source to wrap its default export with `withSentry`. | ||
| * | ||
| * Exported (rather than inlined into the plugin) so it can be unit-tested with a | ||
| * plain AST and no Vite context. Returns `undefined` when nothing was wrapped. | ||
| */ | ||
| export function applyAutoInstrumentTransforms( | ||
| code: string, | ||
| ast: ProgramBody, | ||
| ctx: TransformContext, | ||
| ): TransformResult | undefined { | ||
| const ms = new MagicString(code); | ||
| const state: TransformState = { ms, needsImport: false }; | ||
|
|
||
| for (const node of ast.body) { | ||
| if (node.type === 'ExportDefaultDeclaration') { | ||
| wrapDefaultExport(node as ExportDefaultNode, ctx, state); | ||
| } | ||
| } | ||
|
|
||
| if (!state.needsImport) return undefined; | ||
|
|
||
| if (ctx.optionsImport) ms.prepend(ctx.optionsImport); | ||
| ms.prepend("import * as __SENTRY__ from '@sentry/cloudflare';\n"); | ||
|
|
||
| return { | ||
| code: ms.toString(), | ||
| map: ms.generateMap({ hires: true }), | ||
| }; | ||
| } | ||
|
|
||
| interface TransformState { | ||
| ms: MagicString; | ||
| needsImport: boolean; | ||
| } | ||
|
|
||
| function wrapDefaultExport(node: ExportDefaultNode, ctx: TransformContext, state: TransformState): void { | ||
| const decl = node.declaration; | ||
|
|
||
| // Already wrapped — leave it alone | ||
| if (isCallToMethod(decl, 'withSentry')) return; | ||
|
|
||
| // `export default <expr>` → `const __SENTRY_DEFAULT_EXPORT__ = <expr>` | ||
| // MagicString positions are always relative to the original source. | ||
| state.ms.overwrite(node.start, decl.start, 'const __SENTRY_DEFAULT_EXPORT__ = '); | ||
| state.ms.append(`\nexport default __SENTRY__.withSentry(${ctx.optionsFn}, __SENTRY_DEFAULT_EXPORT__);\n`); | ||
| state.needsImport = true; | ||
| } | ||
|
andreiborza marked this conversation as resolved.
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Entry match collapses
.tsand.tsxMedium Severity
Extension-tolerant entry matching strips any JS-like suffix, so a worker
mainofindex.tsalso matches a siblingindex.tsx. The client-env guard only covers the browser environment; other environments can still transform the wrong file and inject@sentry/cloudflareinto it.Reviewed by Cursor Bugbot for commit 1684122. Configure here.