Skip to content

Commit 7824cfc

Browse files
JPeer264claude
andcommitted
fix(cloudflare): Keep an installed OpenTelemetry async context strategy
`withSentry`, Durable Objects, Workflows and `WorkerEntrypoint` install the AsyncLocalStorage async context strategy. When an OpenTelemetry strategy already uses the same storage, replacing it while a request runs makes the OpenTelemetry context manager read a Sentry scope store as a context, and every later request fails with `context.getValue is not a function`. The AsyncLocalStorage strategy is now only installed when no OpenTelemetry strategy is installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5f2f5f5 commit 7824cfc

6 files changed

Lines changed: 48 additions & 8 deletions

File tree

‎packages/cloudflare/src/durableobject.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { RPC } from '@sentry/conventions/op';
44
import { getDefaultIsolationScope, getIsolationScope, isObjectLike, startNewTrace } from '@sentry/core';
55
import type { DurableObject } from 'cloudflare:workers';
6-
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels';
6+
import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy';
77
import type { CloudflareOptions } from './client';
88
import { getInstrumented, markAsInstrumented } from './instrument';
99
import { instrumentDurableObjectHandlers } from './instrumentations/instrumentDurableObjectHandlers';
@@ -53,7 +53,7 @@ export function constructInstrumentedDurableObject<E, T extends DurableObject<E>
5353
context: InstrumentedDurableObjectContext;
5454
frameworkManagedMethods: ReadonlySet<string>;
5555
} {
56-
setAsyncLocalStorageAsyncContextStrategy();
56+
setAsyncLocalStorageAsyncContextStrategyForWorker();
5757
const options = getFinalOptions(optionsCallback(env), env);
5858
// See InstrumentedDurableObjectContext — `ctx` is widened to `any` so the concrete
5959
// `DurableObjectState` type never enters the checker's relation graph in this module.

‎packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { RpcStub, WorkerEntrypoint } from 'cloudflare:workers';
22
import { RPC } from '@sentry/conventions/op';
3-
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels';
3+
import { setAsyncLocalStorageAsyncContextStrategyForWorker } from '../utils/asyncContextStrategy';
44
import type { CloudflareOptions } from '../client';
55
import { markAsInstrumented } from '../instrument';
66
import { getFinalOptions } from '../options';
@@ -151,7 +151,7 @@ export function instrumentWorkerEntrypoint<
151151
// Set up AsyncLocalStorage strategy ONCE at instrumentation time, not per-request
152152
// This is critical - calling this per-request would create a new AsyncLocalStorage
153153
// each time, breaking scope isolation for concurrent requests
154-
setAsyncLocalStorageAsyncContextStrategy();
154+
setAsyncLocalStorageAsyncContextStrategyForWorker();
155155

156156
const InstrumentedClass = new Proxy(WorkerEntrypointClass, {
157157
construct(target, [ctx, env]) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core';
2+
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels';
3+
4+
/**
5+
* Installs the AsyncLocalStorage strategy unless an OpenTelemetry strategy is installed. Both use the same storage,
6+
* so a switch during a request breaks it (#24603).
7+
*/
8+
export function setAsyncLocalStorageAsyncContextStrategyForWorker(): void {
9+
if (!getAsyncContextStrategy(getMainCarrier()).withActiveSpan) {
10+
setAsyncLocalStorageAsyncContextStrategy();
11+
}
12+
}

‎packages/cloudflare/src/withSentry.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels';
1+
import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy';
22
import { instrumentExportedHandlerEmail } from './instrumentations/worker/instrumentEmail';
33
import { instrumentExportedHandlerFetch } from './instrumentations/worker/instrumentFetch';
44
import { instrumentExportedHandlerQueue } from './instrumentations/worker/instrumentQueue';
@@ -37,7 +37,7 @@ export function withSentry<
3737
return instrumentWorkerEntrypoint(optionsCallback as any, handler);
3838
}
3939

40-
setAsyncLocalStorageAsyncContextStrategy();
40+
setAsyncLocalStorageAsyncContextStrategyForWorker();
4141

4242
try {
4343
// oxlint-disable-next-line typescript/no-explicit-any

‎packages/cloudflare/src/workflows.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type {
2222
WorkflowStepRollbackOptions,
2323
WorkflowTimeoutDuration,
2424
} from 'cloudflare:workers';
25-
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels';
25+
import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy';
2626
import type { CloudflareOptions } from './client';
2727
import { flushAndDispose, getOriginalWaitUntil } from './flush';
2828
import { markAsInstrumented } from './instrument';
@@ -254,7 +254,7 @@ export function instrumentWorkflowWithSentry<
254254
get(obj, prop, receiver) {
255255
if (prop === 'run') {
256256
return async function (event: WorkflowEvent<P>, step: WorkflowStep): Promise<unknown> {
257-
setAsyncLocalStorageAsyncContextStrategy();
257+
setAsyncLocalStorageAsyncContextStrategyForWorker();
258258

259259
return withInvocationIsolationScope(async isolationScope => {
260260
const waitUntil = getOriginalWaitUntil(context).bind(context);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core';
2+
import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry';
3+
import { afterEach, describe, expect, it } from 'vitest';
4+
import { setAsyncLocalStorageAsyncContextStrategyForWorker } from '../../src/utils/asyncContextStrategy';
5+
import { resetSdk } from '../testUtils';
6+
7+
describe('setAsyncLocalStorageAsyncContextStrategyForWorker', () => {
8+
afterEach(() => {
9+
resetSdk();
10+
});
11+
12+
it('installs the AsyncLocalStorage strategy', () => {
13+
setAsyncLocalStorageAsyncContextStrategyForWorker();
14+
15+
const strategy = getAsyncContextStrategy(getMainCarrier());
16+
expect(strategy.getTracingChannelBinding).toBeDefined();
17+
expect(strategy.withActiveSpan).toBeUndefined();
18+
});
19+
20+
it('keeps an installed OpenTelemetry strategy', () => {
21+
setOpenTelemetryContextAsyncContextStrategy();
22+
const openTelemetryStrategy = getAsyncContextStrategy(getMainCarrier());
23+
24+
setAsyncLocalStorageAsyncContextStrategyForWorker();
25+
26+
expect(getAsyncContextStrategy(getMainCarrier())).toBe(openTelemetryStrategy);
27+
});
28+
});

0 commit comments

Comments
 (0)