Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/nextjs/src/common/utils/responseEnd.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Span } from '@sentry/core';
import { debug, fill, flush, GLOBAL_OBJ, setHttpStatus } from '@sentry/core';
import { debug, fill, flush, getAsyncContextStrategy, getMainCarrier, GLOBAL_OBJ, setHttpStatus } from '@sentry/core';
import { vercelWaitUntil } from '@sentry/core/server';
import type { ServerResponse } from 'http';
import { DEBUG_BUILD } from '../debug-build';
Expand Down Expand Up @@ -105,3 +105,20 @@ export function cloudflareWaitUntil(task: Promise<unknown>): void {
export function isCloudflareWaitUntilAvailable(): boolean {
return typeof _getOpenNextCloudflareContext()?.waitUntil === 'function';
}

/**
* Whether a request of `withSentry` from `@sentry/cloudflare` runs. A client of `init` would then replace its async
* context strategy while the request runs (#24603).
*/
export function isAsyncContextOwnedByCloudflare(): boolean {
const strategy = getAsyncContextStrategy(getMainCarrier());
// The AsyncLocalStorage strategy of `@sentry/cloudflare` has no `withActiveSpan`.
const asyncLocalStorage = strategy.getTracingChannelBinding?.()?.asyncLocalStorage as
| { getStore(): unknown }
| undefined;
return (
!strategy.withActiveSpan &&
asyncLocalStorage?.getStore() !== undefined &&
(GLOBAL_OBJ as { navigator?: { userAgent?: string } }).navigator?.userAgent === 'Cloudflare-Workers'
);
}
11 changes: 10 additions & 1 deletion packages/nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunn
import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan';
import { getNormalizedRequestFromAttributes } from '../common/utils/getNormalizedRequestFromAttributes';
import { isBuild } from '../common/utils/isBuild';
import { flushSafelyWithTimeout, isCloudflareWaitUntilAvailable, waitUntil } from '../common/utils/responseEnd';
import {
flushSafelyWithTimeout,
isAsyncContextOwnedByCloudflare,
isCloudflareWaitUntilAvailable,
waitUntil,
} from '../common/utils/responseEnd';
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
Expand Down Expand Up @@ -106,6 +111,10 @@ export function init(options: VercelEdgeOptions = {}): void {
];
opts.ignoreSpans = [...(opts.ignoreSpans || []), ...nextjsIgnoreSpans];

if (isAsyncContextOwnedByCloudflare()) {
return;
}

// Use appropriate SDK metadata based on the runtime environment
if (isRunningOnCloudflare) {
applySdkMetadata(opts, 'nextjs', ['nextjs', 'cloudflare']);
Expand Down
9 changes: 6 additions & 3 deletions packages/nextjs/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolica
import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils';
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached';
import { isBuild } from '../common/utils/isBuild';
import { isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd';
import { isAsyncContextOwnedByCloudflare, isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd';
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
Expand Down Expand Up @@ -182,15 +182,18 @@ export function init(options: NodeOptions): NodeClient | undefined {

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

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

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

const client = nodeInit(opts);
const client = isOwnedByCloudflare ? undefined : nodeInit(opts);

client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => {
// There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/test/edgeSdk.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Integration } from '@sentry/core';
import { GLOBAL_OBJ } from '@sentry/core';
import { getMainCarrier, GLOBAL_OBJ, withIsolationScope } from '@sentry/core';
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
import * as SentryVercelEdge from '@sentry/vercel-edge';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../src/common/span-attributes-with-logic-attached';
Expand All @@ -21,6 +22,9 @@ describe('Edge init()', () => {

afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();

getMainCarrier().__SENTRY__ = undefined;
});

it('inits the Vercel Edge SDK', () => {
Expand Down Expand Up @@ -52,6 +56,15 @@ describe('Edge init()', () => {
);
});

it('skips init on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => {
vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' });
setAsyncLocalStorageAsyncContextStrategy();

withIsolationScope(() => init({}));

expect(vercelEdgeInit).not.toHaveBeenCalled();
});

describe('integrations', () => {
// Options passed by `@sentry/nextjs`'s `init` to `@sentry/vercel-edge`'s `init` after modifying them
type ModifiedInitOptions = { integrations?: Integration[]; defaultIntegrations: Integration[] };
Expand Down
33 changes: 32 additions & 1 deletion packages/nextjs/test/serverSdk.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Integration } from '@sentry/core';
import { GLOBAL_OBJ, getMainCarrier } from '@sentry/core';
import { GLOBAL_OBJ, getGlobalScope, getMainCarrier, withIsolationScope } from '@sentry/core';
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
import { getCurrentScope } from '@sentry/node';
import * as SentryNode from '@sentry/node';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
Expand All @@ -18,10 +19,12 @@ function findIntegrationByName(integrations: Integration[] = [], name: string):
describe('Server init()', () => {
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();

getMainCarrier().__SENTRY__ = undefined;

delete process.env.VERCEL;
delete (process as { turbopack?: boolean }).turbopack;
});

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

it('skips init and sets the `turbopack` tag on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => {
vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' });
setAsyncLocalStorageAsyncContextStrategy();
(process as { turbopack?: boolean }).turbopack = true;

withIsolationScope(() => init({}));

expect(nodeInit).not.toHaveBeenCalled();
expect(getGlobalScope().getScopeData().tags.turbopack).toBe(true);
});

it('inits on Cloudflare Workers outside of a request of `withSentry` from `@sentry/cloudflare`', () => {
vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' });
setAsyncLocalStorageAsyncContextStrategy();

init({});

expect(nodeInit).toHaveBeenCalledTimes(1);
});

it('inits outside of Cloudflare Workers when an AsyncLocalStorage strategy is installed', () => {
setAsyncLocalStorageAsyncContextStrategy();

withIsolationScope(() => init({}));

expect(nodeInit).toHaveBeenCalledTimes(1);
});

// TODO: test `vercel` tag when running on Vercel
// Can't just add the test and set env variables, since the value in `index.server.ts`
// is resolved when importing.
Expand Down
Loading