diff --git a/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/index.ts b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/index.ts new file mode 100644 index 000000000000..852fe0a34fd9 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/index.ts @@ -0,0 +1,25 @@ +import { wrapRequestHandler } from '@sentry/cloudflare/request'; + +interface Env { + SENTRY_DSN: string; +} + +// Mirrors how Hydrogen (Remix) uses the SDK: only `wrapRequestHandler` from the +// `/request` subpath, without `nodejs_compat` compatibility flags. +// The subpath must stay free of Node.js-only modules. +export default { + async fetch(request, env, ctx) { + return wrapRequestHandler( + { + options: { + dsn: env.SENTRY_DSN, + traceLifecycle: 'static', + tracesSampleRate: 1, + }, + request, + context: ctx, + }, + () => new Response('ok'), + ); + }, +} satisfies ExportedHandler; diff --git a/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/test.ts b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/test.ts new file mode 100644 index 000000000000..b55df04197ff --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/test.ts @@ -0,0 +1,18 @@ +import { expect, it } from 'vitest'; +import { createRunner } from '../../../runner'; + +it('captures a transaction via wrapRequestHandler from the /request subpath without node compatibility flags', async ({ + signal, +}) => { + const runner = createRunner(__dirname) + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1] as any; + expect(transactionEvent.transaction).toBe('GET /'); + expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); + expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); + }) + .start(signal); + + await runner.makeRequest('get', '/'); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/wrangler.jsonc new file mode 100644 index 000000000000..8e2b7634629e --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "worker-name", + "compatibility_date": "2025-06-17", + "main": "index.ts", +} diff --git a/packages/cloudflare/src/baseSdk.ts b/packages/cloudflare/src/baseSdk.ts new file mode 100644 index 000000000000..bfa141222160 --- /dev/null +++ b/packages/cloudflare/src/baseSdk.ts @@ -0,0 +1,138 @@ +import type { Integration } from '@sentry/core'; +import { + consoleIntegration, + conversationIdIntegration, + dedupeIntegration, + functionToStringIntegration, + getIntegrationsToSetup, + GLOBAL_OBJ, + inboundFiltersIntegration, + initAndBind, + linkedErrorsIntegration, + requestDataIntegration, + stackParserFromStackParserOptions, +} from '@sentry/core'; +import type { CloudflareClientOptions, CloudflareOptions } from './client'; +import { CloudflareClient } from './client'; +import { makeFlushLock } from './flush'; +import { fetchIntegration } from './integrations/fetch'; +import { httpServerIntegration } from './integrations/httpServer'; +import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from './integrations/spotlight'; +import { setupOpenTelemetryTracer } from './opentelemetry/tracer'; +import { makeCloudflareTransport } from './transport'; +import { defaultStackParser } from './vendor/stacktrace'; + +/** + * Instantiate the channel-subscriber factories the `@sentry/cloudflare/vite` + * plugin registered on the global marker. The plugin splices a small snippet + * into each instrumented module that `.set`s its factory here (keyed by export + * name), so the marker holds one factory per package actually bundled. + * + * The marker is read directly instead of importing the factories, so a worker + * built without the plugin — where the channels never fire — ships none of this + * code. + * TODO(v11): Use `@sentry/server-utils/orchestrion` once we move to `nodejs_compat` by default. + */ +function getRegisteredChannelIntegrations(): Integration[] { + const registered = GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.integrations; + + return registered ? [...registered.values()].map(factory => factory()) : []; +} + +/** + * Get the default integrations that run on any Workers-compatible runtime, i.e. without the + * `nodejs_compat` compatibility flag. + * + * `getDefaultIntegrations` in `sdk.ts` extends this set with the integrations that do depend on + * Node.js APIs. Keeping the two apart is what allows `wrapRequestHandler` to stay usable on runtimes + * that cannot enable `nodejs_compat`, such as Shopify Oxygen. + */ +export function getBaseDefaultIntegrations(options: CloudflareOptions): Integration[] { + // TODO(v11): Drop this transitional gating and let `requestDataIntegration` rely on the resolved + // `dataCollection` defaults directly. Until then, preserve the historical Cloudflare behavior of not + // attaching cookies unless the user explicitly opts in via `sendDefaultPii` or `dataCollection.cookies`. + // eslint-disable-next-line typescript/no-deprecated + const cookiesEnabled = options.sendDefaultPii || options.dataCollection?.cookies != null; + return [ + // The Dedupe integration should not be used in workflows because we want to + // capture all step failures, even if they are the same error. + ...(options.enableDedupe === false ? [] : [dedupeIntegration()]), + // TODO(v11): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration` + // eslint-disable-next-line typescript/no-deprecated + inboundFiltersIntegration(), + functionToStringIntegration(), + conversationIdIntegration(), + linkedErrorsIntegration(), + fetchIntegration(), + httpServerIntegration(), + requestDataIntegration(cookiesEnabled ? undefined : { include: { cookies: false } }), + consoleIntegration(), + // The orchestrion diagnostics-channel subscribers (mysql, pg, …). The + // `@sentry/cloudflare/vite` plugin injects the channels at build time and, + // next to each, a snippet that registers the matching subscriber factory on + // the global marker. Read from there instead of importing them so bundles + // built without the plugin — where the channels would never fire — don't + // ship the code. + ...getRegisteredChannelIntegrations(), + ]; +} + +/** + * Initializes the Cloudflare SDK with the passed default integrations. + * + * The default integrations are injected rather than imported so that this module stays free of + * Node.js-only code. `request.ts` — which backs both `wrapRequestHandler` and the + * `@sentry/cloudflare/request` entry point, and therefore has to work on runtimes without the + * `nodejs_compat` compatibility flag — creates its client from here instead of from `sdk.ts`. + */ +export function initWithDefaultIntegrations( + options: CloudflareOptions, + getDefaultIntegrationsImpl: (options: CloudflareOptions) => Integration[], +): CloudflareClient | undefined { + if (options.defaultIntegrations === undefined) { + options.defaultIntegrations = getDefaultIntegrationsImpl(options); + } + + const flushLock = options.ctx ? makeFlushLock(options.ctx) : undefined; + delete options.ctx; + + const clientOptions: CloudflareClientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || makeCloudflareTransport, + flushLock, + }; + + /*! rollup-include-development-only */ + if (options.spotlight && !clientOptions.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { + clientOptions.integrations.push( + spotlightIntegration({ + sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, + }), + ); + } + /*! rollup-include-development-only-end */ + + /** + * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility + * via a custom trace provider. + * This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry. + * HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope. + * This should be good enough for many, but not all integrations. + */ + if (!options.skipOpenTelemetrySetup) { + setupOpenTelemetryTracer(); + } + + return initAndBind(CloudflareClient, clientOptions) as CloudflareClient; +} + +/** + * Initializes the Cloudflare SDK with only the default integrations from + * {@link getBaseDefaultIntegrations}, i.e. those that work without the `nodejs_compat` + * compatibility flag. + */ +export function initBaseSdk(options: CloudflareOptions): CloudflareClient | undefined { + return initWithDefaultIntegrations(options, getBaseDefaultIntegrations); +} diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 1e18e18ac059..1c8f1c501867 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -6,7 +6,8 @@ import type { CloudflareOptions } from './client'; import { ensureInstrumented } from './instrument'; import { instrumentEnv } from './instrumentations/worker/instrumentEnv'; import { getFinalOptions } from './options'; -import { wrapRequestHandler } from './request'; +import { wrapRequestHandlerWithInit } from './request'; +import { init } from './sdk'; import { instrumentContext } from './utils/instrumentContext'; import { extractRpcMeta } from './utils/rpcMeta'; import { getEffectiveRpcPropagation } from './utils/rpcOptions'; @@ -89,9 +90,13 @@ function instrumentDurableObjectHandlers>( original => new Proxy(original, { apply(target, thisArg, args) { - return wrapRequestHandler({ options, request: args[0], context }, () => { - return Reflect.apply(target, thisArg, args); - }); + return wrapRequestHandlerWithInit( + { options, request: args[0], context }, + () => { + return Reflect.apply(target, thisArg, args); + }, + init, + ); }, }), ); diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentFetch.ts b/packages/cloudflare/src/instrumentations/worker/instrumentFetch.ts index 8a27edc32073..4f96447e9e34 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentFetch.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentFetch.ts @@ -3,7 +3,8 @@ import type { env as cloudflareEnv, WorkerEntrypoint } from 'cloudflare:workers' import type { CloudflareOptions } from '../../client'; import { ensureInstrumented } from '../../instrument'; import { getFinalOptions } from '../../options'; -import { wrapRequestHandler } from '../../request'; +import { wrapRequestHandlerWithInit } from '../../request'; +import { init } from '../../sdk'; import { instrumentContext } from '../../utils/instrumentContext'; import { instrumentEnv } from './instrumentEnv'; @@ -35,7 +36,7 @@ export function instrumentExportedHandlerFetch target.apply(thisArg, args)); + return wrapRequestHandlerWithInit({ options, request, context }, () => target.apply(thisArg, args), init); }, }), ); @@ -62,7 +63,11 @@ export function instrumentWorkerEntrypointFetch( return Reflect.apply(target, thisArg, args); } - return wrapRequestHandler({ options, request, context }, () => Reflect.apply(target, thisArg, args)); + return wrapRequestHandlerWithInit( + { options, request, context }, + () => Reflect.apply(target, thisArg, args), + init, + ); }, }); } diff --git a/packages/cloudflare/src/pages-plugin.ts b/packages/cloudflare/src/pages-plugin.ts index de67eafd5506..02a66e37f821 100644 --- a/packages/cloudflare/src/pages-plugin.ts +++ b/packages/cloudflare/src/pages-plugin.ts @@ -1,7 +1,8 @@ import { setAsyncLocalStorageAsyncContextStrategy } from './async'; import type { CloudflareOptions } from './client'; import type { ExecutionContextCompat } from './executionContext'; -import { wrapRequestHandler } from './request'; +import { wrapRequestHandlerWithInit } from './request'; +import { init } from './sdk'; /** * Plugin middleware for Cloudflare Pages. @@ -57,6 +58,10 @@ export function sentryPagesPlugin< // A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only // uses `waitUntil` and a `'storage' in context` check, both of which this satisfies. const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat; - return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next()); + return wrapRequestHandlerWithInit( + { options, request: context.request, context: executionContext }, + () => context.next(), + init, + ); }; } diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 2adf4cbf2c24..e97d21c5c916 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -13,11 +13,11 @@ import { withIsolationScope, } from '@sentry/core'; import { captureIncomingRequestBody } from './integrations/httpServer'; -import type { CloudflareOptions } from './client'; +import { initBaseSdk } from './baseSdk'; +import type { CloudflareClient, CloudflareOptions } from './client'; import type { ExecutionContextCompat } from './executionContext'; import { flushAndDispose, getOriginalWaitUntil } from './flush'; import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils'; -import { init } from './sdk'; import { classifyResponseStreaming } from './utils/streaming'; function getRequestErrorMechanismType(context: ExecutionContextCompat | undefined): string { @@ -42,12 +42,35 @@ interface RequestHandlerWrapperOptions { captureErrors?: boolean; } +type InitSdk = (options: CloudflareOptions) => CloudflareClient | undefined; + /** - * Wraps a cloudflare request handler in Sentry instrumentation + * Wraps a cloudflare request handler in Sentry instrumentation. + * + * The client is set up with the default integrations that work without the `nodejs_compat` + * compatibility flag, so that this also works on runtimes that cannot enable it (e.g. Shopify + * Oxygen). On a runtime that has `nodejs_compat`, pass `defaultIntegrations: + * getDefaultIntegrations(options)` in `options` to get the full set instead. */ export function wrapRequestHandler( wrapperOptions: RequestHandlerWrapperOptions, handler: (...args: unknown[]) => Response | Promise, +): Promise { + return wrapRequestHandlerWithInit(wrapperOptions, handler, initBaseSdk); +} + +/** + * Same as {@link wrapRequestHandler}, but with the SDK initialization injected. + * + * Wrappers that are only reachable from the main entry point — where `nodejs_compat` is a + * requirement anyway — pass `init` from `sdk.ts` to get the full default integrations. + * + * @internal + */ +export function wrapRequestHandlerWithInit( + wrapperOptions: RequestHandlerWrapperOptions, + handler: (...args: unknown[]) => Response | Promise, + initSdk: InitSdk, ): Promise { return withIsolationScope(async isolationScope => { const { options, request, captureErrors = true } = wrapperOptions; @@ -61,7 +84,7 @@ export function wrapRequestHandler( const waitUntil = context ? getOriginalWaitUntil(context)?.bind(context) : undefined; const errorMechanismType = getRequestErrorMechanismType(context); - const client = init({ ...options, ctx: context }); + const client = initSdk({ ...options, ctx: context }); isolationScope.setClient(client); const urlObject = parseStringToURLObject(request.url); diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index ac85151846f0..4ad84eb8fee0 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -1,114 +1,21 @@ import type { Integration } from '@sentry/core'; -import { - consoleIntegration, - conversationIdIntegration, - dedupeIntegration, - functionToStringIntegration, - getIntegrationsToSetup, - GLOBAL_OBJ, - inboundFiltersIntegration, - initAndBind, - linkedErrorsIntegration, - requestDataIntegration, - stackParserFromStackParserOptions, -} from '@sentry/core'; -import type { CloudflareClientOptions, CloudflareOptions } from './client'; -import { CloudflareClient } from './client'; -import { makeFlushLock } from './flush'; -import { httpServerIntegration } from './integrations/httpServer'; -import { fetchIntegration } from './integrations/fetch'; -import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from './integrations/spotlight'; -import { setupOpenTelemetryTracer } from './opentelemetry/tracer'; -import { makeCloudflareTransport } from './transport'; -import { defaultStackParser } from './vendor/stacktrace'; +import { getBaseDefaultIntegrations, initWithDefaultIntegrations } from './baseSdk'; +import type { CloudflareClient, CloudflareOptions } from './client'; /** - * Instantiate the channel-subscriber factories the `@sentry/cloudflare/vite` - * plugin registered on the global marker. The plugin splices a small snippet - * into each instrumented module that `.set`s its factory here (keyed by export - * name), so the marker holds one factory per package actually bundled. + * Get the default integrations for the Cloudflare SDK. * - * The marker is read directly instead of importing the factories, so a worker - * built without the plugin — where the channels never fire — ships none of this - * code. - * TODO(v11): Use `@sentry/server-utils/orchestrion` once we move to `nodejs_compat` by default. + * This is the full set and requires the `nodejs_compat` compatibility flag. Runtimes that cannot + * enable it (e.g. Shopify Oxygen) go through `wrapRequestHandler`, which only sets up + * `getBaseDefaultIntegrations`. */ -function getRegisteredChannelIntegrations(): Integration[] { - const registered = GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.integrations; - - return registered ? [...registered.values()].map(factory => factory()) : []; -} - -/** Get the default integrations for the Cloudflare SDK. */ export function getDefaultIntegrations(options: CloudflareOptions): Integration[] { - // TODO(v11): Drop this transitional gating and let `requestDataIntegration` rely on the resolved - // `dataCollection` defaults directly. Until then, preserve the historical Cloudflare behavior of not - // attaching cookies unless the user explicitly opts in via `sendDefaultPii` or `dataCollection.cookies`. - // eslint-disable-next-line typescript/no-deprecated - const cookiesEnabled = options.sendDefaultPii || options.dataCollection?.cookies != null; - return [ - // The Dedupe integration should not be used in workflows because we want to - // capture all step failures, even if they are the same error. - ...(options.enableDedupe === false ? [] : [dedupeIntegration()]), - // TODO(v11): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration` - // eslint-disable-next-line typescript/no-deprecated - inboundFiltersIntegration(), - functionToStringIntegration(), - conversationIdIntegration(), - linkedErrorsIntegration(), - fetchIntegration(), - httpServerIntegration(), - requestDataIntegration(cookiesEnabled ? undefined : { include: { cookies: false } }), - consoleIntegration(), - // The orchestrion diagnostics-channel subscribers (mysql, pg, …). The - // `@sentry/cloudflare/vite` plugin injects the channels at build time and, - // next to each, a snippet that registers the matching subscriber factory on - // the global marker. Read from there instead of importing them so bundles - // built without the plugin — where the channels would never fire — don't - // ship the code. - ...getRegisteredChannelIntegrations(), - ]; + return [...getBaseDefaultIntegrations(options)]; } /** * Initializes the cloudflare SDK. */ export function init(options: CloudflareOptions): CloudflareClient | undefined { - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } - - const flushLock = options.ctx ? makeFlushLock(options.ctx) : undefined; - delete options.ctx; - - const clientOptions: CloudflareClientOptions = { - ...options, - stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), - transport: options.transport || makeCloudflareTransport, - flushLock, - }; - - /*! rollup-include-development-only */ - if (options.spotlight && !clientOptions.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { - clientOptions.integrations.push( - spotlightIntegration({ - sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, - }), - ); - } - /*! rollup-include-development-only-end */ - - /** - * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility - * via a custom trace provider. - * This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry. - * HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope. - * This should be good enough for many, but not all integrations. - */ - if (!options.skipOpenTelemetrySetup) { - setupOpenTelemetryTracer(); - } - - return initAndBind(CloudflareClient, clientOptions) as CloudflareClient; + return initWithDefaultIntegrations(options, getDefaultIntegrations); } diff --git a/packages/cloudflare/test/requestModuleGraph.test.ts b/packages/cloudflare/test/requestModuleGraph.test.ts new file mode 100644 index 000000000000..2100f4a47311 --- /dev/null +++ b/packages/cloudflare/test/requestModuleGraph.test.ts @@ -0,0 +1,82 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +/** + * `wrapRequestHandler` — exposed both from the main entry point and from `@sentry/cloudflare/request` + * — has to run on runtimes that cannot enable the `nodejs_compat` compatibility flag, such as Shopify + * Oxygen. A single `node:` import anywhere in its module graph breaks those workers at startup with + * `No such module "node:…"`. + * + * Bundlers usually tree-shake an unused Node.js import away, but that is not something to rely on, so + * assert on the module graph itself rather than on any bundler's output. + */ +const SRC_DIR = resolve(__dirname, '../src'); +const ENTRY_POINT = join(SRC_DIR, 'request.ts'); + +const IMPORT_SPECIFIER_REGEX = /\bfrom\s*['"]([^'"]+)['"]|\bimport\s*['"]([^'"]+)['"]/g; + +function resolveRelativeImport(importer: string, specifier: string): string { + const withoutExtension = join(dirname(importer), specifier); + const candidates = [`${withoutExtension}.ts`, join(withoutExtension, 'index.ts')]; + const resolved = candidates.find(candidate => existsSync(candidate)); + + if (!resolved) { + throw new Error(`Could not resolve '${specifier}' imported from '${relative(SRC_DIR, importer)}'`); + } + + return resolved; +} + +/** Walks the module graph of `entryPoint`, returning the local modules and the external specifiers. */ +function collectModuleGraph(entryPoint: string): { modules: string[]; externals: string[] } { + const modules = new Set(); + const externals = new Set(); + const queue = [entryPoint]; + + while (queue.length) { + const importer = queue.pop() as string; + + if (modules.has(importer)) { + continue; + } + modules.add(importer); + + for (const match of readFileSync(importer, 'utf8').matchAll(IMPORT_SPECIFIER_REGEX)) { + const specifier = match[1] ?? (match[2] as string); + + if (specifier.startsWith('.')) { + queue.push(resolveRelativeImport(importer, specifier)); + } else { + externals.add(specifier); + } + } + } + + return { + modules: [...modules].map(module => relative(SRC_DIR, module)).sort(), + externals: [...externals].sort(), + }; +} + +describe('module graph of `wrapRequestHandler`', () => { + const { modules, externals } = collectModuleGraph(ENTRY_POINT); + + it('contains no Node.js built-in module', () => { + expect(externals.filter(specifier => specifier.startsWith('node:'))).toEqual([]); + }); + + it('contains no package that depends on Node.js APIs', () => { + // `@sentry/server-utils` subscribes to `node:diagnostics_channel` and `@sentry/node` needs Node.js + // throughout. Both are only allowed in `sdk.ts`, `index.ts` and `vite/` (see `.oxlintrc.json`). + expect(externals.filter(specifier => /^@sentry\/(node|server-utils)(\/|$)/.test(specifier))).toEqual([]); + }); + + it('does not reach `async.ts`, the only shipped module importing `node:async_hooks`', () => { + expect(modules).not.toContain('async.ts'); + }); + + it('does not reach `sdk.ts`, which holds the default integrations that need `nodejs_compat`', () => { + expect(modules).not.toContain('sdk.ts'); + }); +}); diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index e16dc5bf4d4a..e1b5b99541b9 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -1,5 +1,5 @@ import type { CloudflareOptions } from '@sentry/cloudflare'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/cloudflare'; +import { getDefaultIntegrations, setAsyncLocalStorageAsyncContextStrategy } from '@sentry/cloudflare'; import { wrapRequestHandler } from '@sentry/cloudflare/request'; import { debug, getDefaultIsolationScope, getIsolationScope, getTraceData } from '@sentry/core'; import type { H3Event } from 'h3'; @@ -69,7 +69,9 @@ export const sentryCloudflareNitroPlugin = }); const requestHandlerOptions = { - options: cloudflareOptions, + // `wrapRequestHandler` only sets up the integrations that work without `nodejs_compat`. + // Nitro's Cloudflare presets require the flag anyway, so opt into the full set. + options: { defaultIntegrations: getDefaultIntegrations(cloudflareOptions), ...cloudflareOptions }, request, context: getCloudflareProperties(event).context, };