Skip to content

Commit 5951d2c

Browse files
authored
feat(cloudflare): Support Cloudflare types v5 & newer wrangler versions (#22180)
closes #22178 closes [JS-2999](https://linear.app/getsentry/issue/JS-2999) I updated the types for our dev packages, but kept the tests on the older version to cross-check compatibility as well for older versions. In the past I wanted to keep the oldest version in here to not break something by accident, but if we do that, we might miss on potential new features/options, such as the new `step.do` option, which we wouldn't have passed along. So IMO it is better to keep it latest here and test against old and new versions instead --- There are new types within the `ExecutionContext`, to support older versions where only `waitUntil` and `passThroughOnException` were required and the newer one which has more types in it, `ExecutionContextCompat` was created, that picks the types which were needed before and adds the newer `ExecutionContext` as well
1 parent 0b88425 commit 5951d2c

9 files changed

Lines changed: 72 additions & 25 deletions

File tree

‎packages/cloudflare/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,15 @@
6565
"@sentry/server-utils": "10.65.0"
6666
},
6767
"peerDependencies": {
68-
"@cloudflare/workers-types": "^4.x"
68+
"@cloudflare/workers-types": "^4.x || ^5.x"
6969
},
7070
"peerDependenciesMeta": {
7171
"@cloudflare/workers-types": {
7272
"optional": true
7373
}
7474
},
7575
"devDependencies": {
76-
"@cloudflare/workers-types": "4.20250922.0",
76+
"@cloudflare/workers-types": "5.20260710.1",
7777
"@types/node": "^18.19.1",
7878
"wrangler": "4.61.0"
7979
},

‎packages/cloudflare/src/client.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
22
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
33
import { DEBUG_BUILD } from './debug-build';
4+
import type { ExecutionContextCompat } from './executionContext';
45
import type { makeFlushLock } from './flush';
56
import type { CloudflareTransportOptions } from './transport';
67

@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
230231
* @see @sentry/core Options for more information.
231232
*/
232233
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
233-
ctx?: ExecutionContext;
234+
ctx?: ExecutionContextCompat;
234235
}
235236

236237
/**
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ExecutionContext } from '@cloudflare/workers-types';
2+
3+
/**
4+
* A structural subset of `ExecutionContext` that is compatible with both `@cloudflare/workers-types`
5+
* v4 and v5.
6+
*
7+
* v5 added `exports` and `tracing` as required members. Referencing the full `ExecutionContext` in
8+
* public input positions would force consumers on v4 (still allowed by our `peerDependencies` range)
9+
* to provide members their types don't have. We only ever use `waitUntil` (plus a runtime
10+
* `'storage' in ctx` check), so picking the members that exist in both majors keeps a context
11+
* constructed against either version assignable here.
12+
*/
13+
export type ExecutionContextCompat = Pick<ExecutionContext, 'waitUntil' | 'passThroughOnException'> | ExecutionContext;

‎packages/cloudflare/src/flush.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ExecutionContext } from '@cloudflare/workers-types';
22
import type { Client } from '@sentry/core';
33
import { debug, flush } from '@sentry/core';
44
import { DEBUG_BUILD } from './debug-build';
5+
import type { ExecutionContextCompat } from './executionContext';
56

67
type FlushLock = {
78
readonly ready: Promise<void>;
@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
3435
*
3536
* By using the original waitUntil for flush operations, we bypass this issue.
3637
*/
37-
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
38+
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
3839
// eslint-disable-next-line @typescript-eslint/unbound-method
3940
const currentWaitUntil = context.waitUntil;
4041
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
4950
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
5051
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
5152
*/
52-
export function makeFlushLock(context: ExecutionContext): FlushLock {
53+
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
5354
const registry = getOrCreateFlushLockRegistry(context);
5455
let resolveAllDone: () => void = () => undefined;
5556
const allDone = new Promise<void>(res => {
@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
8182
return Object.freeze(lock);
8283
}
8384

84-
function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
85+
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
8586
// eslint-disable-next-line @typescript-eslint/unbound-method
8687
const waitUntil = context.waitUntil;
8788
const existingRegistry = flushLockRegistries.get(waitUntil);

‎packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
6969
if (prop === 'send') {
7070
const original = Reflect.get(target, prop, receiver) as Queue['send'];
7171

72-
return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
72+
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
7373
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
7474
Reflect.apply(original, target, [message, options]),
7575
);
@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
8282
this: unknown,
8383
messages: Iterable<MessageSendRequest>,
8484
options?: QueueSendBatchOptions,
85-
): Promise<void> {
85+
): ReturnType<Queue['sendBatch']> {
8686
const messageArray = Array.from(messages);
8787
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
8888
const size = getBodySize(m.body);

‎packages/cloudflare/src/pages-plugin.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
22
import type { CloudflareOptions } from './client';
3+
import type { ExecutionContextCompat } from './executionContext';
34
import { wrapRequestHandler } from './request';
45

56
/**
@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
5354
}
5455

5556
const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
56-
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
57-
context.next(),
58-
);
57+
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
58+
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
59+
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
60+
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
5961
};
6062
}

‎packages/cloudflare/src/request.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
1+
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
22
import {
33
captureException,
44
continueTrace,
@@ -14,20 +14,21 @@ import {
1414
} from '@sentry/core';
1515
import { captureIncomingRequestBody } from './integrations/httpServer';
1616
import type { CloudflareOptions } from './client';
17+
import type { ExecutionContextCompat } from './executionContext';
1718
import { flushAndDispose, getOriginalWaitUntil } from './flush';
1819
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
1920
import { init } from './sdk';
2021
import { classifyResponseStreaming } from './utils/streaming';
2122

22-
function getRequestErrorMechanismType(context: ExecutionContext | undefined): string {
23+
function getRequestErrorMechanismType(context: ExecutionContextCompat | undefined): string {
2324
// Durable Object fetch handlers use DO state as context (see instrumentDurableObjectWithSentry)
2425
return context && 'storage' in context ? 'auto.faas.cloudflare.durable_object' : 'auto.http.cloudflare';
2526
}
2627

2728
interface RequestHandlerWrapperOptions {
2829
options: CloudflareOptions;
2930
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
30-
context: ExecutionContext | undefined;
31+
context: ExecutionContextCompat | undefined;
3132
/**
3233
* If true, errors will be captured, rethrown and sent to Sentry.
3334
* Otherwise, errors are rethrown but not captured.

‎packages/cloudflare/src/workflows.ts‎

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ import {
1010
withScope,
1111
} from '@sentry/core';
1212
import type {
13+
WorkflowDelayDuration,
1314
WorkflowEntrypoint,
1415
WorkflowEvent,
1516
WorkflowSleepDuration,
1617
WorkflowStep,
1718
WorkflowStepConfig,
19+
WorkflowStepContext,
1820
WorkflowStepEvent,
21+
WorkflowStepRollbackOptions,
1922
WorkflowTimeoutDuration,
2023
} from 'cloudflare:workers';
2124
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {
7073

7174
public async do<T extends Rpc.Serializable<T>>(
7275
name: string,
73-
callback: (...args: unknown[]) => Promise<T>,
76+
callback: (ctx: WorkflowStepContext) => Promise<T>,
77+
rollbackOptions?: WorkflowStepRollbackOptions<T>,
7478
): Promise<T>;
75-
public async do<T extends Rpc.Serializable<T>>(
79+
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
7680
name: string,
77-
config: WorkflowStepConfig,
78-
callback: (...args: unknown[]) => Promise<T>,
81+
config: C,
82+
callback: (
83+
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
84+
) => Promise<T>,
85+
rollbackOptions?: WorkflowStepRollbackOptions<T>,
7986
): Promise<T>;
8087
public async do<T extends Rpc.Serializable<T>>(
8188
name: string,
82-
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
83-
maybeCallback?: (...args: unknown[]) => Promise<T>,
89+
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
90+
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
91+
maybeRollback?: WorkflowStepRollbackOptions<T>,
8492
): Promise<T> {
8593
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
8694
const scopeForStep = getCurrentScope();
8795

88-
const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
89-
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
96+
const hasConfig = typeof configOrCallback !== 'function';
97+
const config = hasConfig ? configOrCallback : undefined;
98+
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
99+
ctx: WorkflowStepContext,
100+
) => Promise<T>;
101+
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
102+
| WorkflowStepRollbackOptions<T>
103+
| undefined;
90104

91105
const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
92106
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
109123
attributes: {
110124
'cloudflare.workflow.timeout': config?.timeout,
111125
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
112-
'cloudflare.workflow.retries.delay': config?.retries?.delay,
126+
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
127+
'cloudflare.workflow.retries.delay':
128+
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
113129
'cloudflare.workflow.retries.limit': config?.retries?.limit,
114130
'cloudflare.workflow.attempt': attempt,
115131
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
118134
},
119135
async span => {
120136
try {
121-
const result = await userCallback(...args);
137+
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
122138
span.setStatus({ code: 1 });
123139
return result;
124140
} catch (error) {
@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
133149
);
134150
};
135151

136-
return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
152+
if (config) {
153+
return rollbackOptions
154+
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
155+
: this._step.do(name, config, instrumentedCallback);
156+
}
157+
158+
return rollbackOptions
159+
? this._step.do(name, instrumentedCallback, rollbackOptions)
160+
: this._step.do(name, instrumentedCallback);
137161
}
138162

139163
public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {

‎yarn.lock‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3137,6 +3137,11 @@
31373137
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
31383138
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==
31393139

3140+
"@cloudflare/workers-types@5.20260710.1":
3141+
version "5.20260710.1"
3142+
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
3143+
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==
3144+
31403145
"@cloudflare/workers-types@^4.20260426.0":
31413146
version "4.20260519.1"
31423147
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"

0 commit comments

Comments
 (0)