Skip to content

Commit b306a35

Browse files
authored
feat(nextjs): Add cloudflare waitUntil detection (#18336)
This is part of the work done for supporting Next.js on cloudflare workers in #14931, one of the issues is our flusher didn't take cloudflare into account and the events were being cut off due to early shutdowns by the worker environment. We cannot use our core's `flushIfServerless` because it requires explicit `cloudflareWaitUntil` to be passed down to it. Also The symbol we are grabbing here is [specific to opennext](https://github.com/opennextjs/opennextjs-cloudflare/blob/b53a046bd5c30e94a42e36b67747cefbf7785f9a/packages/cloudflare/src/cli/templates/init.ts#L17) and isn't something that is globally available on the worker environment. I had initially created #18330 because I mistook it for a worker environment API thing. So this PR adds that detection to Next.js since it is relevant here and will use it if available, local tests show flushing is done correctly and events aren't being cut off.
1 parent a3875c5 commit b306a35

File tree

11 files changed

+170
-28
lines changed

11 files changed

+170
-28
lines changed

packages/nextjs/src/common/captureRequestError.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { RequestEventData } from '@sentry/core';
2-
import { captureException, headersToDict, vercelWaitUntil, withScope } from '@sentry/core';
3-
import { flushSafelyWithTimeout } from './utils/responseEnd';
2+
import { captureException, headersToDict, withScope } from '@sentry/core';
3+
import { flushSafelyWithTimeout, waitUntil } from './utils/responseEnd';
44

55
type RequestInfo = {
66
path: string;
@@ -42,6 +42,6 @@ export function captureRequestError(error: unknown, request: RequestInfo, errorC
4242
},
4343
});
4444

45-
vercelWaitUntil(flushSafelyWithTimeout());
45+
waitUntil(flushSafelyWithTimeout());
4646
});
4747
}

packages/nextjs/src/common/pages-router-instrumentation/_error.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { captureException, httpRequestToRequestData, vercelWaitUntil, withScope } from '@sentry/core';
1+
import { captureException, httpRequestToRequestData, withScope } from '@sentry/core';
22
import type { NextPageContext } from 'next';
3-
import { flushSafelyWithTimeout } from '../utils/responseEnd';
3+
import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd';
44

55
type ContextOrProps = {
66
req?: NextPageContext['req'];
@@ -54,5 +54,5 @@ export async function captureUnderscoreErrorException(contextOrProps: ContextOrP
5454
});
5555
});
5656

57-
vercelWaitUntil(flushSafelyWithTimeout());
57+
waitUntil(flushSafelyWithTimeout());
5858
}

packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ import {
1010
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
1111
setHttpStatus,
1212
startSpanManual,
13-
vercelWaitUntil,
1413
withIsolationScope,
1514
} from '@sentry/core';
1615
import type { NextApiRequest } from 'next';
1716
import type { AugmentedNextApiResponse, NextApiHandler } from '../types';
18-
import { flushSafelyWithTimeout } from '../utils/responseEnd';
17+
import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd';
1918
import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils';
2019

2120
export type AugmentedNextApiRequest = NextApiRequest & {
@@ -95,7 +94,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
9594
apply(target, thisArg, argArray) {
9695
setHttpStatus(span, res.statusCode);
9796
span.end();
98-
vercelWaitUntil(flushSafelyWithTimeout());
97+
waitUntil(flushSafelyWithTimeout());
9998
return target.apply(thisArg, argArray);
10099
},
101100
});

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

Lines changed: 51 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, setHttpStatus } from '@sentry/core';
2+
import { debug, fill, flush, GLOBAL_OBJ, setHttpStatus, vercelWaitUntil } from '@sentry/core';
33
import type { ServerResponse } from 'http';
44
import { DEBUG_BUILD } from '../debug-build';
55
import type { ResponseEndMethod, WrappedResponseEndMethod } from '../types';
@@ -54,3 +54,53 @@ export async function flushSafelyWithTimeout(): Promise<void> {
5454
DEBUG_BUILD && debug.log('Error while flushing events:\n', e);
5555
}
5656
}
57+
58+
/**
59+
* Uses platform-specific waitUntil function to wait for the provided task to complete without blocking.
60+
*/
61+
export function waitUntil(task: Promise<unknown>): void {
62+
// If deployed on Cloudflare, use the Cloudflare waitUntil function to flush the events
63+
if (isCloudflareWaitUntilAvailable()) {
64+
cloudflareWaitUntil(task);
65+
return;
66+
}
67+
68+
// otherwise, use vercel's
69+
vercelWaitUntil(task);
70+
}
71+
72+
type MinimalCloudflareContext = {
73+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
74+
waitUntil(promise: Promise<any>): void;
75+
};
76+
77+
/**
78+
* Gets the Cloudflare context from the global object.
79+
* Relevant to opennext
80+
* https://github.com/opennextjs/opennextjs-cloudflare/blob/b53a046bd5c30e94a42e36b67747cefbf7785f9a/packages/cloudflare/src/cli/templates/init.ts#L17
81+
*/
82+
function _getOpenNextCloudflareContext(): MinimalCloudflareContext | undefined {
83+
const openNextCloudflareContextSymbol = Symbol.for('__cloudflare-context__');
84+
85+
return (
86+
GLOBAL_OBJ as typeof GLOBAL_OBJ & {
87+
[openNextCloudflareContextSymbol]?: {
88+
ctx: MinimalCloudflareContext;
89+
};
90+
}
91+
)[openNextCloudflareContextSymbol]?.ctx;
92+
}
93+
94+
/**
95+
* Function that delays closing of a Cloudflare lambda until the provided promise is resolved.
96+
*/
97+
export function cloudflareWaitUntil(task: Promise<unknown>): void {
98+
_getOpenNextCloudflareContext()?.waitUntil(task);
99+
}
100+
101+
/**
102+
* Checks if the Cloudflare waitUntil function is available globally.
103+
*/
104+
export function isCloudflareWaitUntilAvailable(): boolean {
105+
return typeof _getOpenNextCloudflareContext()?.waitUntil === 'function';
106+
}

packages/nextjs/src/common/withServerActionInstrumentation.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ import {
1111
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
1212
SPAN_STATUS_ERROR,
1313
startSpan,
14-
vercelWaitUntil,
1514
withIsolationScope,
1615
} from '@sentry/core';
17-
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
16+
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
1817
import { DEBUG_BUILD } from './debug-build';
1918
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
2019

@@ -155,7 +154,7 @@ async function withServerActionInstrumentationImplementation<A extends (...args:
155154
},
156155
);
157156
} finally {
158-
vercelWaitUntil(flushSafelyWithTimeout());
157+
waitUntil(flushSafelyWithTimeout());
159158
}
160159
},
161160
);

packages/nextjs/src/common/wrapMiddlewareWithSentry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ import {
99
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
1010
setCapturedScopesOnSpan,
1111
startSpan,
12-
vercelWaitUntil,
1312
winterCGRequestToRequestData,
1413
withIsolationScope,
1514
} from '@sentry/core';
16-
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
15+
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
1716
import type { EdgeRouteHandler } from '../edge/types';
1817

1918
/**
@@ -108,7 +107,7 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(
108107
});
109108
},
110109
() => {
111-
vercelWaitUntil(flushSafelyWithTimeout());
110+
waitUntil(flushSafelyWithTimeout());
112111
},
113112
);
114113
},

packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ import {
1212
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
1313
setCapturedScopesOnSpan,
1414
setHttpStatus,
15-
vercelWaitUntil,
1615
winterCGHeadersToDict,
1716
withIsolationScope,
1817
withScope,
1918
} from '@sentry/core';
2019
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
2120
import type { RouteHandlerContext } from './types';
22-
import { flushSafelyWithTimeout } from './utils/responseEnd';
21+
import { flushSafelyWithTimeout, waitUntil } from './utils/responseEnd';
2322
import { commonObjectToIsolationScope } from './utils/tracingUtils';
2423

2524
/**
@@ -96,7 +95,7 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
9695
}
9796
},
9897
() => {
99-
vercelWaitUntil(flushSafelyWithTimeout());
98+
waitUntil(flushSafelyWithTimeout());
10099
},
101100
);
102101

packages/nextjs/src/common/wrapServerComponentWithSentry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ import {
1313
SPAN_STATUS_ERROR,
1414
SPAN_STATUS_OK,
1515
startSpanManual,
16-
vercelWaitUntil,
1716
winterCGHeadersToDict,
1817
withIsolationScope,
1918
withScope,
2019
} from '@sentry/core';
2120
import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils';
2221
import type { ServerComponentContext } from '../common/types';
23-
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
22+
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
2423
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
2524
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
2625

@@ -117,7 +116,7 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>
117116
},
118117
() => {
119118
span.end();
120-
vercelWaitUntil(flushSafelyWithTimeout());
119+
waitUntil(flushSafelyWithTimeout());
121120
},
122121
);
123122
},

packages/nextjs/src/edge/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
setCapturedScopesOnSpan,
1616
spanToJSON,
1717
stripUrlQueryAndFragment,
18-
vercelWaitUntil,
1918
} from '@sentry/core';
2019
import { getScopesFromContext } from '@sentry/opentelemetry';
2120
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
@@ -24,7 +23,7 @@ import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attribu
2423
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
2524
import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests';
2625
import { isBuild } from '../common/utils/isBuild';
27-
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
26+
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
2827
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
2928
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
3029

@@ -142,7 +141,7 @@ export function init(options: VercelEdgeOptions = {}): void {
142141

143142
client?.on('spanEnd', span => {
144143
if (span === getRootSpan(span)) {
145-
vercelWaitUntil(flushSafelyWithTimeout());
144+
waitUntil(flushSafelyWithTimeout());
146145
}
147146
});
148147

packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import {
99
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
1010
setCapturedScopesOnSpan,
1111
startSpan,
12-
vercelWaitUntil,
1312
winterCGRequestToRequestData,
1413
withIsolationScope,
1514
} from '@sentry/core';
1615
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
17-
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
16+
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
1817
import type { EdgeRouteHandler } from './types';
1918

2019
/**
@@ -94,7 +93,7 @@ export function wrapApiHandlerWithSentry<H extends EdgeRouteHandler>(
9493
});
9594
},
9695
() => {
97-
vercelWaitUntil(flushSafelyWithTimeout());
96+
waitUntil(flushSafelyWithTimeout());
9897
},
9998
);
10099
},

0 commit comments

Comments
 (0)