Skip to content

Commit 97c3369

Browse files
JPeer264claude
andcommitted
fix(nextjs): Skip init inside a request of @sentry/cloudflare
Next.js calls `register()` inside the first request. When the Worker is wrapped with `withSentry` from `@sentry/cloudflare`, the server and edge `init` then installed the OpenTelemetry async context strategy on the storage of that request, and every later request failed with `context.getValue is not a function`. On Cloudflare Workers, `init` now creates no client while a request of `withSentry` from `@sentry/cloudflare` runs, because that client already handles the request. The server `init` still adds the global event processors and the turbopack tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1e18758 commit 97c3369

5 files changed

Lines changed: 93 additions & 8 deletions

File tree

‎packages/nextjs/src/common/utils/responseEnd.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Span } from '@sentry/core';
2-
import { debug, fill, flush, GLOBAL_OBJ, setHttpStatus } from '@sentry/core';
2+
import { debug, fill, flush, getAsyncContextStrategy, getMainCarrier, GLOBAL_OBJ, setHttpStatus } from '@sentry/core';
33
import { vercelWaitUntil } from '@sentry/core/server';
44
import type { ServerResponse } from 'http';
55
import { DEBUG_BUILD } from '../debug-build';
@@ -105,3 +105,20 @@ export function cloudflareWaitUntil(task: Promise<unknown>): void {
105105
export function isCloudflareWaitUntilAvailable(): boolean {
106106
return typeof _getOpenNextCloudflareContext()?.waitUntil === 'function';
107107
}
108+
109+
/**
110+
* Whether a request of `withSentry` from `@sentry/cloudflare` runs. A client of `init` would then replace its async
111+
* context strategy while the request runs (#24603).
112+
*/
113+
export function isAsyncContextOwnedByCloudflare(): boolean {
114+
const strategy = getAsyncContextStrategy(getMainCarrier());
115+
// The AsyncLocalStorage strategy of `@sentry/cloudflare` has no `withActiveSpan`.
116+
const asyncLocalStorage = strategy.getTracingChannelBinding?.()?.asyncLocalStorage as
117+
| { getStore(): unknown }
118+
| undefined;
119+
return (
120+
!strategy.withActiveSpan &&
121+
asyncLocalStorage?.getStore() !== undefined &&
122+
(GLOBAL_OBJ as { navigator?: { userAgent?: string } }).navigator?.userAgent === 'Cloudflare-Workers'
123+
);
124+
}

‎packages/nextjs/src/edge/index.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/* eslint-disable import/export */
44
import {
55
applySdkMetadata,
6+
debug,
67
getGlobalScope,
78
getIsolationScope,
89
getRootSpan,
@@ -30,7 +31,12 @@ import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunn
3031
import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan';
3132
import { getNormalizedRequestFromAttributes } from '../common/utils/getNormalizedRequestFromAttributes';
3233
import { isBuild } from '../common/utils/isBuild';
33-
import { flushSafelyWithTimeout, isCloudflareWaitUntilAvailable, waitUntil } from '../common/utils/responseEnd';
34+
import {
35+
flushSafelyWithTimeout,
36+
isAsyncContextOwnedByCloudflare,
37+
isCloudflareWaitUntilAvailable,
38+
waitUntil,
39+
} from '../common/utils/responseEnd';
3440
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
3541
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
3642
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
@@ -106,6 +112,14 @@ export function init(options: VercelEdgeOptions = {}): void {
106112
];
107113
opts.ignoreSpans = [...(opts.ignoreSpans || []), ...nextjsIgnoreSpans];
108114

115+
if (isAsyncContextOwnedByCloudflare()) {
116+
DEBUG_BUILD &&
117+
debug.log(
118+
'The client of `withSentry` handles this Worker, so `init` creates no client. Set the options in `withSentry`.',
119+
);
120+
return;
121+
}
122+
109123
// Use appropriate SDK metadata based on the runtime environment
110124
if (isRunningOnCloudflare) {
111125
applySdkMetadata(opts, 'nextjs', ['nextjs', 'cloudflare']);

‎packages/nextjs/src/server/index.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolica
1919
import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils';
2020
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached';
2121
import { isBuild } from '../common/utils/isBuild';
22-
import { isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd';
22+
import { isAsyncContextOwnedByCloudflare, isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd';
2323
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
2424
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
2525
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
@@ -182,15 +182,18 @@ export function init(options: NodeOptions): NodeClient | undefined {
182182

183183
DEBUG_BUILD && debug.log('Initializing SDK...');
184184

185-
if (sdkAlreadyInitialized()) {
185+
// The client of `withSentry` from `@sentry/cloudflare` handles this request, so `init` creates none. The global
186+
// parts below still apply.
187+
const isOwnedByCloudflare = isAsyncContextOwnedByCloudflare();
188+
if (sdkAlreadyInitialized() && !isOwnedByCloudflare) {
186189
DEBUG_BUILD && debug.log('SDK already initialized');
187190
return;
188191
}
189192

190193
// Use appropriate SDK metadata based on the runtime environment
191194
applySdkMetadata(opts, 'nextjs', ['nextjs', cloudflareConfig ? 'cloudflare' : 'node']);
192195

193-
const client = nodeInit(opts);
196+
const client = isOwnedByCloudflare ? undefined : nodeInit(opts);
194197

195198
client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => {
196199
// There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in
@@ -292,7 +295,14 @@ export function init(options: NodeOptions): NodeClient | undefined {
292295
// The statement above can throw because process is not defined on the client
293296
}
294297

295-
DEBUG_BUILD && debug.log('SDK successfully initialized');
298+
if (isOwnedByCloudflare) {
299+
DEBUG_BUILD &&
300+
debug.log(
301+
'The client of `withSentry` handles this Worker, so `init` creates no client. Set the options in `withSentry`.',
302+
);
303+
} else {
304+
DEBUG_BUILD && debug.log('SDK successfully initialized');
305+
}
296306

297307
return client;
298308
}

‎packages/nextjs/test/edgeSdk.test.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Integration } from '@sentry/core';
2-
import { GLOBAL_OBJ } from '@sentry/core';
2+
import { getMainCarrier, GLOBAL_OBJ, withIsolationScope } from '@sentry/core';
3+
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
34
import * as SentryVercelEdge from '@sentry/vercel-edge';
45
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
56
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../src/common/span-attributes-with-logic-attached';
@@ -21,6 +22,9 @@ describe('Edge init()', () => {
2122

2223
afterEach(() => {
2324
vi.clearAllMocks();
25+
vi.unstubAllGlobals();
26+
27+
getMainCarrier().__SENTRY__ = undefined;
2428
});
2529

2630
it('inits the Vercel Edge SDK', () => {
@@ -52,6 +56,15 @@ describe('Edge init()', () => {
5256
);
5357
});
5458

59+
it('skips init on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => {
60+
vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' });
61+
setAsyncLocalStorageAsyncContextStrategy();
62+
63+
withIsolationScope(() => init({}));
64+
65+
expect(vercelEdgeInit).not.toHaveBeenCalled();
66+
});
67+
5568
describe('integrations', () => {
5669
// Options passed by `@sentry/nextjs`'s `init` to `@sentry/vercel-edge`'s `init` after modifying them
5770
type ModifiedInitOptions = { integrations?: Integration[]; defaultIntegrations: Integration[] };

‎packages/nextjs/test/serverSdk.test.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Integration } from '@sentry/core';
2-
import { GLOBAL_OBJ, getMainCarrier } from '@sentry/core';
2+
import { GLOBAL_OBJ, getGlobalScope, getMainCarrier, withIsolationScope } from '@sentry/core';
3+
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
34
import { getCurrentScope } from '@sentry/node';
45
import * as SentryNode from '@sentry/node';
56
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -18,10 +19,12 @@ function findIntegrationByName(integrations: Integration[] = [], name: string):
1819
describe('Server init()', () => {
1920
afterEach(() => {
2021
vi.clearAllMocks();
22+
vi.unstubAllGlobals();
2123

2224
getMainCarrier().__SENTRY__ = undefined;
2325

2426
delete process.env.VERCEL;
27+
delete (process as { turbopack?: boolean }).turbopack;
2528
});
2629

2730
it('inits the Node SDK', () => {
@@ -67,6 +70,34 @@ describe('Server init()', () => {
6770
expect(nodeInit).toHaveBeenCalledTimes(1);
6871
});
6972

73+
it('skips init and sets the `turbopack` tag on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => {
74+
vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' });
75+
setAsyncLocalStorageAsyncContextStrategy();
76+
(process as { turbopack?: boolean }).turbopack = true;
77+
78+
withIsolationScope(() => init({}));
79+
80+
expect(nodeInit).not.toHaveBeenCalled();
81+
expect(getGlobalScope().getScopeData().tags.turbopack).toBe(true);
82+
});
83+
84+
it('inits on Cloudflare Workers outside of a request of `withSentry` from `@sentry/cloudflare`', () => {
85+
vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' });
86+
setAsyncLocalStorageAsyncContextStrategy();
87+
88+
init({});
89+
90+
expect(nodeInit).toHaveBeenCalledTimes(1);
91+
});
92+
93+
it('inits outside of Cloudflare Workers when an AsyncLocalStorage strategy is installed', () => {
94+
setAsyncLocalStorageAsyncContextStrategy();
95+
96+
withIsolationScope(() => init({}));
97+
98+
expect(nodeInit).toHaveBeenCalledTimes(1);
99+
});
100+
70101
// TODO: test `vercel` tag when running on Vercel
71102
// Can't just add the test and set env variables, since the value in `index.server.ts`
72103
// is resolved when importing.

0 commit comments

Comments
 (0)