diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index af2b4481553d..541b507a7d56 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -3,7 +3,7 @@ import { RPC } from '@sentry/conventions/op'; import { getDefaultIsolationScope, getIsolationScope, isObjectLike, startNewTrace } from '@sentry/core'; import type { DurableObject } from 'cloudflare:workers'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import type { CloudflareOptions } from './client'; import { getInstrumented, markAsInstrumented } from './instrument'; import { instrumentDurableObjectHandlers } from './instrumentations/instrumentDurableObjectHandlers'; @@ -53,7 +53,7 @@ export function constructInstrumentedDurableObject context: InstrumentedDurableObjectContext; frameworkManagedMethods: ReadonlySet; } { - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); const options = getFinalOptions(optionsCallback(env), env); // See InstrumentedDurableObjectContext — `ctx` is widened to `any` so the concrete // `DurableObjectState` type never enters the checker's relation graph in this module. diff --git a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts index 1071d79226c6..34c9d7562ed3 100644 --- a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts +++ b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts @@ -1,6 +1,6 @@ import type { RpcStub, WorkerEntrypoint } from 'cloudflare:workers'; import { RPC } from '@sentry/conventions/op'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from '../utils/asyncContextStrategy'; import type { CloudflareOptions } from '../client'; import { markAsInstrumented } from '../instrument'; import { getFinalOptions } from '../options'; @@ -151,7 +151,7 @@ export function instrumentWorkerEntrypoint< // Set up AsyncLocalStorage strategy ONCE at instrumentation time, not per-request // This is critical - calling this per-request would create a new AsyncLocalStorage // each time, breaking scope isolation for concurrent requests - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); const InstrumentedClass = new Proxy(WorkerEntrypointClass, { construct(target, [ctx, env]) { diff --git a/packages/cloudflare/src/utils/asyncContextStrategy.ts b/packages/cloudflare/src/utils/asyncContextStrategy.ts new file mode 100644 index 000000000000..d4afe1369f4e --- /dev/null +++ b/packages/cloudflare/src/utils/asyncContextStrategy.ts @@ -0,0 +1,12 @@ +import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; + +/** + * Installs the AsyncLocalStorage strategy unless an OpenTelemetry strategy is installed. Both use the same storage, + * so a switch during a request breaks it (#24603). + */ +export function setAsyncLocalStorageAsyncContextStrategyForWorker(): void { + if (!getAsyncContextStrategy(getMainCarrier()).withActiveSpan) { + setAsyncLocalStorageAsyncContextStrategy(); + } +} diff --git a/packages/cloudflare/src/withSentry.ts b/packages/cloudflare/src/withSentry.ts index d26cc30da60c..6c991af6ed38 100644 --- a/packages/cloudflare/src/withSentry.ts +++ b/packages/cloudflare/src/withSentry.ts @@ -1,4 +1,4 @@ -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import { instrumentExportedHandlerEmail } from './instrumentations/worker/instrumentEmail'; import { instrumentExportedHandlerFetch } from './instrumentations/worker/instrumentFetch'; import { instrumentExportedHandlerQueue } from './instrumentations/worker/instrumentQueue'; @@ -37,7 +37,7 @@ export function withSentry< return instrumentWorkerEntrypoint(optionsCallback as any, handler); } - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); try { // oxlint-disable-next-line typescript/no-explicit-any diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 62442cadf32d..f6c423315c3b 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -22,7 +22,7 @@ import type { WorkflowStepRollbackOptions, WorkflowTimeoutDuration, } from 'cloudflare:workers'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import type { CloudflareOptions } from './client'; import { flushAndDispose, getOriginalWaitUntil } from './flush'; import { markAsInstrumented } from './instrument'; @@ -254,7 +254,7 @@ export function instrumentWorkflowWithSentry< get(obj, prop, receiver) { if (prop === 'run') { return async function (event: WorkflowEvent

, step: WorkflowStep): Promise { - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); return withInvocationIsolationScope(async isolationScope => { const waitUntil = getOriginalWaitUntil(context).bind(context); diff --git a/packages/cloudflare/test/utils/asyncContextStrategy.test.ts b/packages/cloudflare/test/utils/asyncContextStrategy.test.ts new file mode 100644 index 000000000000..7f6346035a6c --- /dev/null +++ b/packages/cloudflare/test/utils/asyncContextStrategy.test.ts @@ -0,0 +1,28 @@ +import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core'; +import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; +import { afterEach, describe, expect, it } from 'vitest'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from '../../src/utils/asyncContextStrategy'; +import { resetSdk } from '../testUtils'; + +describe('setAsyncLocalStorageAsyncContextStrategyForWorker', () => { + afterEach(() => { + resetSdk(); + }); + + it('installs the AsyncLocalStorage strategy', () => { + setAsyncLocalStorageAsyncContextStrategyForWorker(); + + const strategy = getAsyncContextStrategy(getMainCarrier()); + expect(strategy.getTracingChannelBinding).toBeDefined(); + expect(strategy.withActiveSpan).toBeUndefined(); + }); + + it('keeps an installed OpenTelemetry strategy', () => { + setOpenTelemetryContextAsyncContextStrategy(); + const openTelemetryStrategy = getAsyncContextStrategy(getMainCarrier()); + + setAsyncLocalStorageAsyncContextStrategyForWorker(); + + expect(getAsyncContextStrategy(getMainCarrier())).toBe(openTelemetryStrategy); + }); +});