-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(v10/cloudflare): Read wrangler config and resolve the Sentry options module #22538
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
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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
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,46 @@ | ||
| import type { env as cloudflareEnv } from 'cloudflare:workers'; | ||
| import type { CloudflareOptions } from './client'; | ||
|
|
||
| /** | ||
| * Define the Sentry options for a Cloudflare Worker in a dedicated module. | ||
| * | ||
| * This is the recommended way to configure the SDK when using the Vite plugin's | ||
| * auto-instrumentation: place an `instrument.server.{ts,js,mjs}` file next to | ||
| * the worker entry whose **default export** is the result of this function. The | ||
| * plugin picks it up automatically and hands it to `withSentry`. | ||
| * | ||
| * Unlike Node's `Sentry.init(...)`, the options cannot be applied at module | ||
| * load time on Cloudflare: the DSN and other settings typically come from the | ||
| * per-request `env`, which only exists inside the handler. Pass a callback to | ||
| * read from `env`, or a static object when no `env` access is needed — either | ||
| * way you get full type-checking and autocomplete on {@link CloudflareOptions}. | ||
| * | ||
| * At runtime this is a thin pass-through; it only normalizes a static object | ||
| * into a callback so the plugin always imports a `(env) => options` function. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // src/instrument.server.ts | ||
| * import { defineCloudflareOptions } from '@sentry/cloudflare'; | ||
| * | ||
| * export default defineCloudflareOptions((env) => ({ | ||
| * dsn: env.SENTRY_DSN, | ||
| * tracesSampleRate: 1.0, | ||
| * })); | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Static options — no `env` access needed | ||
| * export default defineCloudflareOptions({ tracesSampleRate: 1.0 }); | ||
| * ``` | ||
| */ | ||
| export function defineCloudflareOptions<Env = typeof cloudflareEnv>( | ||
| optionsOrCallback: CloudflareOptions | ((env: Env) => CloudflareOptions | undefined), | ||
| ): (env: Env) => CloudflareOptions | undefined { | ||
| if (typeof optionsOrCallback === 'function') { | ||
| return optionsOrCallback as (env: Env) => CloudflareOptions | undefined; | ||
| } | ||
|
|
||
| return () => optionsOrCallback; | ||
| } |
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,53 @@ | ||
| import { existsSync } from 'node:fs'; | ||
| import { dirname, relative, resolve } from 'node:path'; | ||
|
|
||
| // Fallback options callback used when no instrument file is present. Returning | ||
| // `undefined` makes the SDK read all configuration (DSN, release, environment, | ||
| // sample rate, …) from the worker's `env` at runtime. | ||
| export const ENV_FALLBACK_OPTIONS_FN = '() => undefined'; | ||
|
|
||
| // Identifier the generated import binds the user's options module to. | ||
| const OPTIONS_IMPORT_IDENTIFIER = '__SENTRY_OPTIONS_CALLBACK__'; | ||
|
|
||
| // Conventional, non-configurable name of the Sentry options module. It is | ||
| // looked up next to the worker entry file; its default export is the options | ||
| // callback `(env) => CloudflareOptions`. | ||
| const INSTRUMENT_FILE_BASENAME = 'instrument.server'; | ||
| const INSTRUMENT_FILE_EXTENSIONS = ['ts', 'mts', 'js', 'mjs', 'cjs']; | ||
|
|
||
| /** | ||
| * Locate the conventional `instrument.server.*` module sitting next to the | ||
| * worker entry file. Returns its absolute path, or `undefined` when absent. | ||
| */ | ||
| export function resolveInstrumentFile(entryFilePath: string): string | undefined { | ||
| const dir = dirname(entryFilePath); | ||
| for (const ext of INSTRUMENT_FILE_EXTENSIONS) { | ||
| const candidate = resolve(dir, `${INSTRUMENT_FILE_BASENAME}.${ext}`); | ||
| if (existsSync(candidate)) return candidate; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Build the `optionsFn` reference and `import` statement for the instrument | ||
| * module whose **default export** is the options callback | ||
| * `(env) => CloudflareOptions`. | ||
| * | ||
| * The import is emitted relative to `entryFilePath` because it is injected into | ||
| * the entry file's source. The file extension is kept: extensionless specifiers | ||
| * only resolve for extensions in Vite's default `resolve.extensions` (which | ||
| * excludes `.cjs`), and keeping it makes our probe order authoritative when | ||
| * several `instrument.server.*` files coexist. | ||
| */ | ||
| export function buildOptionsImport( | ||
| entryFilePath: string, | ||
| instrumentFilePath: string, | ||
| ): { optionsFn: string; importStmt: string } { | ||
| let relativePath = relative(dirname(entryFilePath), instrumentFilePath).replace(/\\/g, '/'); | ||
| if (!relativePath.startsWith('.')) relativePath = `./${relativePath}`; | ||
|
|
||
| return { | ||
| optionsFn: OPTIONS_IMPORT_IDENTIFIER, | ||
| importStmt: `import ${OPTIONS_IMPORT_IDENTIFIER} from '${relativePath}';\n`, | ||
| }; | ||
| } | ||
|
andreiborza marked this conversation as resolved.
|
||
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,67 @@ | ||
| import { existsSync } from 'node:fs'; | ||
| import { dirname, resolve } from 'node:path'; | ||
| import { type Unstable_Config, unstable_readConfig } from 'wrangler'; | ||
|
|
||
| /** | ||
| * The slice of the wrangler configuration the auto-instrument plugin cares | ||
| * about. `main` is an absolute path (wrangler resolves it against the config | ||
| * file's directory). | ||
| */ | ||
| export interface WranglerConfig { | ||
| main?: string; | ||
| durableObjects: Array<{ name: string; className: string }>; | ||
| } | ||
|
|
||
| /** | ||
| * Locate and resolve the wrangler configuration via wrangler's own | ||
| * `unstable_readConfig` — the API `@cloudflare/vite-plugin` uses. | ||
| * | ||
| * We only locate the file (probing `wrangler.json`, `.jsonc`, `.toml` inside | ||
| * `root` with wrangler's own precedence, since it discovers from `cwd` rather | ||
| * than an arbitrary root); wrangler then parses it, flattens the active | ||
| * environment (honoring `CLOUDFLARE_ENV`), and resolves `main` to an absolute | ||
| * path. Durable Object bindings are the active environment's, matching what the | ||
| * deployed Worker actually binds. | ||
| * | ||
| * Returns `undefined` when no config file is found or it can't be read/parsed | ||
| * (the caller warns and disables auto-instrumentation rather than failing the | ||
| * whole build). | ||
| */ | ||
| export function resolveWranglerConfig( | ||
| root: string, | ||
| explicitPath?: string, | ||
| ): { config: WranglerConfig; configDir: string } | undefined { | ||
| const configPath = explicitPath | ||
| ? resolve(root, explicitPath) | ||
| : ['wrangler.json', 'wrangler.jsonc', 'wrangler.toml'].map(name => resolve(root, name)).find(existsSync); | ||
|
|
||
| if (!configPath || !existsSync(configPath)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| let raw: Unstable_Config; | ||
| try { | ||
| // `hideWarnings` keeps wrangler's config diagnostics (e.g. missing DO | ||
| // migrations) out of the Vite build output. | ||
| raw = unstable_readConfig({ config: configPath }, { hideWarnings: true }); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
|
|
||
| const durableObjects: WranglerConfig['durableObjects'] = []; | ||
| const seenClassNames = new Set<string>(); | ||
| for (const binding of raw.durable_objects?.bindings ?? []) { | ||
| // `script_name` bindings reference a class exported by a *different* worker | ||
| // — there is nothing to wrap in this worker's entry file. | ||
| if (typeof binding?.class_name !== 'string' || binding.script_name || seenClassNames.has(binding.class_name)) { | ||
| continue; | ||
| } | ||
| seenClassNames.add(binding.class_name); | ||
| durableObjects.push({ name: binding.name, className: binding.class_name }); | ||
| } | ||
|
|
||
| return { | ||
| config: { main: raw.main, durableObjects }, | ||
| configDir: dirname(raw.configPath ?? configPath), | ||
| }; | ||
| } | ||
|
andreiborza marked this conversation as resolved.
|
||
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,28 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { defineCloudflareOptions } from '../src/defineCloudflareOptions'; | ||
|
|
||
| describe('defineCloudflareOptions', () => { | ||
| it('returns the callback unchanged', () => { | ||
| const callback = (env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN }); | ||
| expect(defineCloudflareOptions(callback)).toBe(callback); | ||
| }); | ||
|
|
||
| it('passes env through to the callback', () => { | ||
| const callback = defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ | ||
| dsn: env.SENTRY_DSN, | ||
| tracesSampleRate: 1.0, | ||
| })); | ||
|
|
||
| expect(callback({ SENTRY_DSN: 'https://example' })).toEqual({ | ||
| dsn: 'https://example', | ||
| tracesSampleRate: 1.0, | ||
| }); | ||
| }); | ||
|
|
||
| it('normalizes a static options object into a callback', () => { | ||
| const callback = defineCloudflareOptions({ tracesSampleRate: 0.5 }); | ||
|
|
||
| expect(typeof callback).toBe('function'); | ||
| expect(callback({} as never)).toEqual({ tracesSampleRate: 0.5 }); | ||
| }); | ||
| }); |
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.
Uh oh!
There was an error while loading. Please reload this page.